アンケートサイトの色々な回答に一気に答えるブックマークレットを作った

これまで何回かに分けて作ってきた「アンケートサイト自動回答ブックマークレット」だが、その総まとめを作ってみた。

目次

サンプル

まずはこの総まとめツールでどんなことができるか、というサンプルを見せる。

見出し右側の「Execute」ボタンを押すと、各フォームに自動回答できる。

ブックマークレットを発表

この自動回答ができるブックマークレットは以下のとおり。

javascript:((d,s,e)=>{e=()=>{SurveyHelpers({cityName:'東京',districtName:'足立',age:25,ageRange:20,birthYear:1993,birthMonth:2,birthDate:24,gender:'女',marriage:'未婚',jobRegExp:'正社|社員'},{loop:5})};s=d.createElement('script');s.onload=e;s.src='http://let.st-hatelabo.com/neos21/let/hLHUwLzfytkR.bookmarklet.js';d.body.appendChild(s)})(document);

改行すると以下のとおり。

javascript:((d,s,e)=>{
  e=()=>{
    SurveyHelpers({
      cityName:'東京',
      districtName:'足立',
      age:25,
      ageRange:20,
      birthYear:1993,
      birthMonth:2,
      birthDate:24,
      gender:'女',
      marriage:'未婚',
      jobRegExp:'正社|社員'
    },{
      loop:5
    })
  };
  s=d.createElement('script');
  s.onload=e;
  s.src='http://let.st-hatelabo.com/neos21/let/hLHUwLzfytkR.bookmarklet.js';
  d.body.appendChild(s)
})(document);

大体分かると思うが、SurveyHelpers() の第1引数に書いた諸々の個人情報を、上手いことフォームに適用している、という仕組み。個人情報部分を任意の内容に変えたら、1行にまとめてブックマークレットとして登録すれば良い。

ブックマークレットの仕組み

このブックマークレットは、Hatena::Let というサービスを利用して作成している。

Hatena::Let は、「はてラボ」と呼ばれる「はてな」の実験的サービスの一つで、ブックマークレットを投稿できるサービスだ。ココに SurveyHelpers() の本体を投稿している。ソースコードは以下で確認できる。

/**
 * @title アンケート回答ブックマークレット
 * @description アンケートを自動回答するブックマークレット
 * @license MIT License
 * @author Neo http://neo.s21.xrea.com/
 * 
 * 以下のように個人情報と設定項目を指定し、このブックマークレットを読み込んで使う
 * 
 * ```
 * javascript:((d,s,e)=>{
 *   e=()=>{
 *     SurveyHelpers({
 *       cityName:'東京',
 *       districtName:'足立',
 *       age:25,
 *       ageRange:20,
 *       birthYear:1993,
 *       birthMonth:2,
 *       birthDate:24,
 *       gender:'女',
 *       marriage:'未婚',
 *       jobRegExp:'正社|社員'
 *     },{
 *       loop:5
 *     })
 *   };
 *   s=d.createElement('script');
 *   s.onload=e;
 *   s.src='http://let.st-hatelabo.com/neos21/let/hLHUwLzfytkR.bookmarklet.js';
 *   d.body.appendChild(s)
 * })(document);
 * 
 * // 1行にすると以下のとおり
 * javascript:((d,s,e)=>{e=()=>{SurveyHelpers({cityName:'東京',districtName:'足立',age:25,ageRange:20,birthYear:1993,birthMonth:2,birthDate:24,gender:'女',marriage:'未婚',jobRegExp:'正社|社員'},{loop:5})};s=d.createElement('script');s.onload=e;s.src='http://let.st-hatelabo.com/neos21/let/hLHUwLzfytkR.bookmarklet.js';d.body.appendChild(s)})(document);
 * ```
 * 
 */
function SurveyHelpers(myInfo, settings) {
  // 引数未指定の場合は中止する
  if(!myInfo || !settings) {
    return;
  }
  
  // 個人情報
  const cityName     = myInfo.cityName     || '都道府県';  // 都道府県 : 「都道府県」は書かない
  const districtName = myInfo.districtName || '行政区';    // 行政区 : 東京23区用・「区」は書かない
  const age          = myInfo.age          || 999;         // 年齢
  const ageRange     = myInfo.ageRange     || 999;         // 年齢層 : 「20代」とか「20~29歳」とかの選択肢用
  const birthYear    = myInfo.birthYear    || 9999;        // 誕生年
  const birthMonth   = myInfo.birthMonth   || 13;          // 誕生月
  const birthDate    = myInfo.birthDate    || 32;          // 誕生日
  const gender       = myInfo.gender       || '性別';      // 性別 : '男' か '女' あたりを想定
  const marriage     = myInfo.marriage     || '婚姻状態';  // 婚姻状態 : '未婚' か '既婚' あたりを想定
  const jobRegExp    = myInfo.jobRegExp    || '職業';      // 職業 : 正規表現 "()" で囲んで OR 検索するので "|" で区切る
  
  // 設定項目
  const loop = settings.loop || 5;  // 親要素を遡る階層数
  
  
  // セレクトボックス選択
  // --------------------------------------------------------------------------------
  
  // セレクトボックスで使用する条件まとめ
  const optionConditions = {
    // 住所か年齢か誕生日
    something: new RegExp(cityName
                          + '|' + districtName
                          + '|' + age + '.*[歳|才]'
                          + '|' + birthYear
                          + '|' + birthMonth + '.*月'
                          + '|' + birthDate + '.*日')
  };
  
  // select 要素を探索する
  Array.prototype.forEach.call(document.querySelectorAll('select'), (select) => {
    // その select 要素内の option 要素で探索が終わった場合は処理を中断するためのフラグ
    let finished = false;
    
    Array.prototype.forEach.call(select.querySelectorAll('option'), (option) => {
      // この select 要素が探索済なら中断する
      if(finished) {
        return;
      }
      
      const innerHTML = option.innerHTML;
      
      if(optionConditions.something.test(innerHTML)) {
        // いずれかの情報に合致したら option 要素を選択する
        option.selected = true;
        finished = true;
      }
      else if(/1|2/.test(innerHTML)) {
        // 1 か 2 が含まれていたら月か日のセレクトボックスと予想して処理する
        let isMonth = false;
        let isDate  = false;
        
        // その option 要素が所属する select 要素を全探索して、セレクトボックスが月か日のセレクトボックスかどうか判定する
        Array.prototype.forEach.call(select.querySelectorAll('option'), (selectOption) => {
          const selectOptionInnerHTML = selectOption.innerHTML;
          
          if(selectOptionInnerHTML.includes(12)) {
            // 12 を含む選択肢があれば「月」セレクトボックスと予想する
            isMonth = true;
          }
          else if(selectOptionInnerHTML.includes(13)) {
            // 13 を含む選択肢があれば「月」ではなく「日」セレクトボックスと予想する
            isMonth = false;
            isDate = true;
          }
          else if(selectOptionInnerHTML.includes(32)) {
            // 32 を含む選択肢があれば「月」でも「日」でもない (都道府県セレクトボックスなどの項番と判定)
            isMonth = false;
            isDate = false;
          }
        });
        
        // 「月」セレクトボックスもしくは「日」セレクトボックスと予想した時に対象の option 要素を選択する
        if((isMonth && innerHTML.includes(birthMonth)) || (isDate && innerHTML.includes(birthDate))) {
          option.selected = true;
          finished = true;
        }
      }
    });
  });
  
  
  // テキストボックス入力
  // --------------------------------------------------------------------------------
  
  // Type が text か tel の要素を探索する
  Array.prototype.forEach.call(document.querySelectorAll('[type=text],[type=tel]'), (textbox) => {
    // 親要素に遡っていくための変数
    let parent = textbox;
    // 親要素を遡っての探索が済んでいることを示すフラグ
    let finished = false;
    
    // 親要素を遡る
    for(let i = 0; i < loop; i++) {
      // 探索済なら中断する
      if(finished) {
        continue;
      }
      
      // 親要素の innerHTML を取得する
      parent = parent.parentNode;
      const innerHTML = parent.innerHTML;
      
      // 親要素の innerHTML からそれらしい文言を見付けたら対応する値を設定する
      if(/歳|才/.test(innerHTML)) {
        textbox.value = age;
        finished = true;
      }
      else if(innerHTML.includes('年')) {
        textbox.value = birthYear;
        finished = true;
      }
      else if(innerHTML.includes('月')) {
        textbox.value = birthMonth;
        finished = true;
      }
      else if(innerHTML.includes('日')) {
        textbox.value = birthDate;
        finished = true;
      }
    }
  });
  
  
  // ラジオボタン選択
  // --------------------------------------------------------------------------------
  
  // ラジオボタンで使用する条件まとめ
  const radioConditions = {
    // 都道府県か行政区か性別か年齢層か職業か婚姻状態
    something: new RegExp(cityName
                          + '|' + districtName
                          + '|' + gender
                          + '|' + ageRange + '.*[~|代](?!未満)'
                          + '|' + '(' + jobRegExp + ')'
                          + '|' + marriage)
  };
  
  // 1つ前に探索したラジオボタンの情報を控えておく : よりラジオボタンに近い階層で該当項目を見付けた方を優先させるため
  const radioPrev = {
    name: '',
    loop: -1
  };
  
  // Type が radio の要素を探索する
  Array.prototype.forEach.call(document.querySelectorAll('[type=radio]'), (radio) => {
    // 異なるラジオボタン群が出てきたら、直前に探索したラジオボタンの情報をリセットする
    if((radioPrev.name !== '' && radioPrev.loop !== -1) && radio.name !== radioPrev.name) {
      radioPrev.name = '';
      radioPrev.loop = -1;
    }
    
    // 親要素に遡っていくための変数
    let parent = radio;
    // 親要素を遡っての探索が済んでいることを示すフラグ
    let finished = false;
    
    // 親要素を遡る
    for(let i = 0; i < loop; i++) {
      // 探索済なら中断する
      if(finished) {
        continue;
      }
      
      // 親要素の innerHTML を取得する
      parent = parent.parentNode;
      let innerHTML = parent.innerHTML;
      
      // そのラジオボタン群で初めての場合か、より近いラジオボタンを見付けたら
      if(radioConditions.something.test(innerHTML)
         && ((radioPrev.name === '' && radioPrev.loop === -1) || (radio.name === radioPrev.name && i < radioPrev.loop))) {
        // 探索したラジオボタンの情報として登録しておく
        radioPrev.name = radio.name;
        radioPrev.loop = i;
        radio.checked = true;
        finished = true;
      }
    }
  });
}

script 要素を生成してこのコードを示す URL を指定し、bodyappendChild() することで有効にしている。ただしこのブックマークレットは関数を定義するだけなので、script 要素の onloadSurveyHelpers() を呼ぶ処理を付与してある、という仕組みだ。

script 要素を生成して外部サイト上の .js ファイルを読み込むことでブックマークレットを実行する、というアイデアは多くあるが、ブックマークレットに特化した投稿サービスが上手いことあって助かった。外部ファイルにしてあるので、改行やインデントを入れっぱなしでも使える。可読性が高く、更新しやすいスクリプトを運用できるので、メンテナンスもしやすいだろう。

一応、以下の Gist にソースコードのバックアップを用意しておいた。Hatena::Let がもし終了してしまった時は、このスクリプトをどこか別のところでホスティングすれば良いかな。


以上。ぜひご利用ください。