El Mylar

映画・音楽作品の感想とか。

EC ナビ・PeX の「まいにちニュース」に気持ちを自動で回答するブックマークレットを作った

「EC ナビ」と「PeX」というポイントサイトを、2003年頃から使っている。その中に「まいにちニュース」というコンテンツがある。これは、ニュース記事の末尾に「いいね」ボタンや「Bad」ボタンなどがあり、記事を読んだ気持ちを答えることでポイントがもらえる仕組みだ。

今回はこのボタンを自動的に押下するブックマークレットを作ってみた。

完成形

完成形は以下。以下のブックマークレットを EC ナビや PeX の「まいにちニュース」の記事ページで実行すれば、「いいね」ボタンがあるところまでスクロールして「いいね」ボタンをクリックしてくれる。

javascript:(e=>['angry','sad','cool','like'].map(x=>'#submit-'+x).concat(['bad','sad','glad','good'].map(x=>['.btn_'+x,'.btn_feeling_'+x]).flat()).some(x=>(e=document.querySelector(x))&&(e.scrollIntoView(),e.click(),!0)))();

以下、開発経緯

最初に作ったコード

まずは押下したいボタンの要素を特定する。EC ナビの PC 版は .btn_feeling_XXX、EC ナビのスマホ版は .btn_XXX、PeX は #submit-XXX という名前のボタンを押下すれば良いことが分かった。ただし、EC ナビは「bad」「sad」「glad」「good」の4種類であるのに対して、PeX は「angry」「sad」「cool」「like」と、XXX 部分に入れる「感情」の文言が若干異なっていた。

[
  // EC ナビ PC        // スマホ    // PeX
  '.btn_feeling_bad' , '.btn_bad' , '#submit-angry',
  '.btn_feeling_sad' , '.btn_sad' , '#submit-sad'  ,
  '.btn_feeling_glad', '.btn_glad', '#submit-cool' ,
  '.btn_feeling_good', '.btn_good', '#submit-like'
]

最初に作ったのは以下のようなコード。配列を順に走査し、要素が見つかれば scrollIntoView() でその要素のところまでスクロールし、クリックするという動きにした。break を使って、一度クリックできたらループから抜けるようにした。

javascript:((s, i, e) => {
  for(i = s.length; i--; ) {
    if(e = document.querySelector(s[i])) {
      e.scrollIntoView();
      e.click();
      break;
    }
  }
})(
  [
    '.btn_feeling_bad' , '.btn_bad' , '#submit-angry',
    '.btn_feeling_sad' , '.btn_sad' , '#submit-sad'  ,
    '.btn_feeling_glad', '.btn_glad', '#submit-cool' ,
    '.btn_feeling_good', '.btn_good', '#submit-like'
  ]
);

変数 ie はローカル変数として使うため、ド頭に宣言だけしている。if 文の中で代入しながら存在チェックを行っているところがコード短縮化のミソ。1行にまとめた際は、for 文のブレース {} が除去できる。なお、for ループは文字数短縮のために末尾から順にループするイディオムを利用しているので、配列末尾の方に優先的にクリックさせたい要素を並べておくと良いだろう。

コレでも動作するのだが、なんとなく冗長な気がして、もう少し文字数を減らせないか試してみた。

対象要素の配列を短縮化

まず、.btn_feeling_$submit- といった文言が重複しているので、ココを自動生成できないか考えてみた。

const pexElems = ['angry', 'sad', 'cool', 'like'].map((name) => {
  return '#submit-' + name;
});

const ecNaviElems = ['bad', 'sad', 'glad', 'good'].map((name) => {
  return ['.btn_' + name, '.btn_feeling_' + name];
});

const useFlat        = pexElems.concat(ecNaviElems.flat());
const useConcatApply = Array.prototype.concat.apply(pexElems, ecNaviElems);

このように、共通する文言を map() で付与して配列を生成してみた。EC ナビの方は .btn_XXX (スマホ版) と .btn_feeling_XXX(PC 版) とを同時に生成してみたかったのだが、上の変数ecNaviElems` は二重の配列として生成されている。

二重の配列を展開・フラット化するには、Array.prototype.flat() という関数が策定されている。コレを使って ecNaviElems を平たくし、pexElems と結合すれば良い。

別の方法で、Array.prototype.concat.apply(baseArray, nestedArray) といったイディオムもある。Array.prototype.flat() が動かないブラウザではコチラが使えるだろう。

というワケで、配列の宣言部分は次のようなコードに短縮化できた。

['angry', 'sad', 'cool', 'like']
  .map(x => '#submit-' + x)
  .concat(
    ['bad', 'sad', 'glad', 'good']
      .map(x => ['.btn_' + x, '.btn_feeling_' +x ])
      .flat()
  );

外側の map().concat() しているのが PeX 向けの配列で、内側で map().flat() しているのが EC ナビ向けの配列だ。

forbreak のイディオムを Array.prototype.some() に変更

次に、forbreak で処理していた部分を短くできないか見てみた。

配列を順に操作している時にループを抜ける方法には、Array.prototype.some() も存在することに気付いた。要素が見つかって、クリックができたら return true してやれば、以降の要素は走査されないワケだ。

allElems.some((name) => {
  if(elem = document.querySelector(name)) {
    elem.scrollIntoView();
    elem.click();
    return true;
  }
});

if 文に合致しなかった場合は何も返していないので undefined (Falsy な値) が返ったことになり、ループ処理が続く。

コレをこのまま1行にしても、イマイチ短くならない。しかし、&& 演算子を使った書き方にかえてやると、短くできそうだ。ちなみにこの && 演算子のことは「論理積」「論理 AND」演算子と呼ぶ。バイナリ論理演算子の一種だ。

allElems.some( name => (elem = document.querySelector(name)) && (elem.scrollIntoView(), elem.click(), true) );
  • (elem = document.querySelector(name)) で代入と存在チェックを兼ねる
    • 要素が存在しない場合はこの時点で false になるので、&& 演算子以降は処理されない
  • 要素が存在すると (elem.scrollIntoView(), elem.click(), true) 部分が実行され、最後の true が返るので、some() のループがココで中断される
    • 最後の true を忘れると、(elem.scrollIntoView(), elem.click()) だけでは undefined (Falsy) となり、some() のループが全要素に対して行われてしまう

完成形のおさらい

というワケで、全体を結合するとこのようなコードになる。

javascript:(e => 
  ['angry', 'sad', 'cool', 'like']
    .map(x => '#submit-' + x)
    .concat(
      ['bad', 'sad', 'glad', 'good']
        .map(x => ['.btn_' + x, '.btn_feeling_' + x])
        .flat()
    )
    .some(x =>
      (e = document.querySelector(x)) && (e.scrollIntoView(), e.click(), !0)
    )
)();
  • 全体の即時関数は (() => {})(); と書くより、適当な仮引数を1つ与えて (e => {})(); とする方が () より1文字減らせる。今回は e = document.querySelector() で DOM 要素を代入する変数のために仮引数を1つ用意できた
  • やっていることは allElems.some() の1処理だけなので、即時関数のブレース {} も除去して (e => allElems.some())() という構成にした。全体の即時関数の戻り地はどうでもいいのでこのように省いて良い
  • document.querySelector() の引数に渡す CSS クラス名や ID 名を、.map().concat() .map().flat() を駆使して構築する
    • もしココで要素の順番をシャッフルしたければ、.some() の手前で .sort(n=>Math.random()-.5) みたいなコードを入れれば、雑なシャッフルができる
  • some() の中は && 演算子を使って短縮化している

コレのスペースを除去して1行にまとめたのが冒頭のコード。

javascript:(e=>['angry','sad','cool','like'].map(x=>'#submit-'+x).concat(['bad','sad','glad','good'].map(x=>['.btn_'+x,'.btn_feeling_'+x]).flat()).some(x=>(e=document.querySelector(x))&&(e.scrollIntoView(),e.click(),!0)))();

その他

ブックマークレットを作るための短縮化には、拙作の @neos21/bookmarkletify という npm パッケージが有効かと思われる。コチラも合わせてドウゾ。

www.npmjs.com

以前作った、ポイントサイトやアンケートサイトで使えるブックマークレットは、別ブログ Corredor の方でいくつか紹介している。コチラもよかったらドウゾ。

neos21.hatenablog.com

neos21.hatenablog.com

neos21.hatenablog.com

neos21.hatenablog.com

neos21.hatenablog.com

neos21.hatenablog.com

以上。