アンケートサイトで自動回答するブックマークレットを作るためのノウハウ集

これまで何度か紹介してきた、アンケートサイトで使える自動回答系のブックマークレットたち。

最近のアンケートサイトには、バナー広告の誤クリックを狙った「広告付きアンケート」や、商品やサービスの宣伝そのものな「広告アンケート」といったアンケートが多い。これらはどういう回答をしてもあまり整合性は見られていないようで、バナー広告の誤クリックを狙って「次へ」ボタンが遅れて表示されるといった罠が仕掛けられていたりして、とても悪質だ。

そんな悪質なアンケートをちゃちゃっと始末するには、ブックマークレットを作るのが良いだろう。今回は自分が「自動回答ブックマークレット」を作る上でよくやるノウハウを紹介していく。

目次

ブックマークレットの雛形

最近の自分がよく作るブックマークレットの雛形は以下のような感じ。

javascript:(async(d,q,a,w)=>{
  // ココにコードを書いていく
  d[q]('#example')?.click();  // document.querySelector・要素があればクリックするが、要素がなくてもエラーにしない
  await w(1000);  // Wait・Sleep
  d[q+a]('input[type="button"]').forEach(e=>e.click());  // document.querySelectorAll
})(
  document,
  'querySelector',
  'All',
  s=>new Promise(r=>setTimeout(r,s))
);

ブラウザが async やオプショナル・チェイニング (?.) を解釈してくれるようになったので、ブックマークレットでもそうした機能をガンガン利用していけるようになった。

よく使う document.querySelectordocument.querySelectorAll は、無名関数の仮引数を用意しておいて、d[q]d[q+a] といった短いコードで書けるようにしてある。

要素をクリックしてから少し待機して次の処理に移動したい時のために、setTimeoutPromise を組み合わせた wait 関数を用意してある。setTimeout だけだとコールバック関数がどんどんネストしていって煩雑になりがちだが、asyncawait を使えば同期処理のように書けて便利だ。

いずれも、短い変数名を利用してコンパクトに書いているが、拙作の Bookmarkletify などを使って1行に圧縮することが前提なのであれば、可読性を重視して任意の変数名を付けたりしてもらった方が良いだろう。

オプショナル・チェイニングを使う

DOM 操作において厄介なのは、対象の要素が存在しなかった場合の挙動だ。

これまでは以下のような書き方で、要素の存在チェックをしながら処理していた。

// 愚直に if 文
const element = document.querySelector('#submit-button');
if(element) {
  element.click();
}
else {
  console.log('ボタンが見つかりませんでした');
}

// もしくは && や || 演算子を使って…
let element;
(element = document.querySelector('#submit-button')) && element.click();
(element = document.querySelector('#submit-button')) || console.log('ボタンが見つかりませんでした');

要素の存在によって後続処理が変わる場合は、こうした分岐処理は未だ必要だが、「要素が存在すれば処理し、要素が存在しなければ何もせず次に進む」という流れで良ければ、オプショナル・チェイニングを使ってコードを短くできる。変数も必要なくなるので気持ちが良い。

document.querySelector('#submit-button'))?.click();

ちなみに document.querySelectorAll() は、当てはまる要素が一つもない場合は空の配列 (厳密には要素数 0 の NodeList) になるので、forEach() を繋げて書いておいてもトラブルにはならない (1回も実行されずに終わるだけ)。

document.querySelectorAll('.button').forEach(element => element.click());

querySelectorAll と高階関数を組み合わせる

document.querySelectorAll() の戻り値は純粋な配列ではないので、forEach() は使えるが find()reduce() などが使えなかったりする。

こうした高階関数を使いたい場合は、querySelectorAll の結果を配列に詰め直してやる必要がある。具体的にはスプレッド構文を使うと短く実装できるだろう。

// 「次へ」と書かれている button 要素を1つ探し出す
const targetButton = [...document.querySelectorAll('button')].find(element => element.innerText.trim() === '次へ');

// スプレッド構文は Array.from() と書いても同義だが、若干文字数が長くなる
const targetButton = Array.from(document.querySelectorAll('button')).find(element => element.innerText.trim() === '次へ');

属性セレクタで要素を絞り込む

querySelectorquerySelectorAll では、CSS の属性セレクタを使用できる。コレを上手く使うと、予め目的の要素群を絞り込みやすい。

属性セレクタは [attr^="前方一致"][attr*="部分一致"][attr$="後方一致"] などの他、[attr*="value" i] と書くと大文字・小文字を区別せず認識したりしてくれる。

querySelectorAll はカンマ区切りで複数のセレクタを書けるので、コレも併用してみたい。

// label 要素の for 属性値が「Q」または「q」で始まる要素
const qLabels = document.querySelectorAll('label[for^="Q" i]');

// input 要素の value 属性値に「次へ」「進む」「送信」を含む要素
const targetValues = document.querySelectorAll('input[value=*"次へ"],input[value=*"進む"],input[value=*"送信"]');

要素内のテキストで絞り込む

input[type="button"] の場合は、属性セレクタを使って要素を絞り込みやすいが、button 要素の場合は value 属性ではなく要素内にテキストを書く形なので、属性セレクタでは絞り込めない。

そんな時は Array#find() 内で Element#innerText を参照して絞り込むしかない。

// button 要素の値が「次へ」「進む」「送信」のいずれかである要素 (完全一致)
const targetValues = [...document.querySelectorAll('button')].find(element => ['次へ', '進む', '送信'].includes(element.innerText.trim()));

// 指定文字列を「含む」絞り込み
const targetValues = [...document.querySelectorAll('button')].find(element => (/次|進|送信/u).test(element.innerText.trim()));

element.innerText は単純な String (文字列) なので、Array#includes() で「いずれかの文字列に一致する」という条件を書いたり、RegExp (正規表現) と組み合わせて「いずれかの文字列を含む」といった条件にしたりできるだろう。余計な空白文字を除去するために trim() をかませておくと良いだろう。

要素群からランダムに1つの要素を取り出す

querySelectorAll で抽出した要素群から、ランダムに1つの要素を取り出すイディオム。

例えば、取得した複数のボタンのうち、ランダムに抽出した1つのボタンをクリックするような処理であれば、次のように書けば良い。

// 3行に分けて実装
const buttons = document.querySelectorAll('button');
const target = buttons[Math.floor(Math.random() * button.length)];
if(target) target.click();

// ワンライナー (カンマ演算子 + オプショナル・チェイニング) で実装
(buttons = document.querySelectorAll('button'), buttons[Math.floor(Math.random() * buttons.length)])?.click();

最後の1つを取り出す時は reverse() を使う方法もある。

// 添字を指定しようとすると一時的な変数 (buttons) を用意する必要がある
const buttons = document.querySelectorAll('button');
const lastButton = buttons[buttons.length - 1];

// reverse() を使えば変数を省ける
const lastButton = [...document.querySelectorAll('button')].reverse()[0];

iframe 内の DOM 要素を操作する

iframe 内の DOM 要素も操作できる。iframe 要素配下の contentWindow.document と下っていけば良い。

const iframe = document.querySelector('iframe');
const innerLabels = iframe.contentWindow.document.querySelectorAll('label');

以上

これらのノウハウを組み合わせて、

  1. input 要素ないしは label 要素を適当に click() する
  2. button 要素や [type="submit"] などを適当に click() する

といったコードを書いていくことで、アンケートに自動的に回答して次のページに進むスクリプトになるだろう。

個人的には、最近のブラウザでオプショナル・チェイニングが使えるようになったことで、DOM 要素の存在チェックを省ける機会が増えて助かっている。

待機する必要がありそうな時は async を利用して setTimeout を Promise 化して使うことで、コールバック関数のネストを回避できるので、コレも活用していきたい。