Nightmare.JS を触ってみた

最近多くのクラウドサービスを利用するようになりました。

  • Google
  • Facebook
  • Slack
  • AWS
  • などなど

多くのクラウドサービスは API を公開しており、少しプログラムを書いたり ZapierIntegromat など使えばある程度が自動化できます。便利ですね。

ですが、中には API を公開していないクラウドサービスもあります。API はあるものも契約等をしなければ使えない API もあります。でもそんなクラウドサービスも自動化したい!と考えのあなた。できます。できます。

みんな大好き Selenium の出番です。これで仮想的にブラウザをポチポチしてもらえばいいんです!でも Selenium はコーディングが大変だし、記録も面倒。もっといい感じにコーディングできるのはないのか。ということで見つかりました。

Nightmare.js – A high-level browser automation library.

Phantom.js を聞いたことがあるかもしれないですが、これをもっと簡単に使えるようにしてくれたライブラリです。細かいことはドキュメントを読んでもらって、実際のコードを見てみましょう!

const Nightmare = require('nightmare');

const main = async () => {
  const today = new Date();
  const year = today.getFullYear();
  const month = today.getMonth(); // 1月 なら 0
  const date = today.getDate();

  const nightmare = Nightmare({
    show: true, // ここを false にすると Window が新たに開きません
  });

  await nightmare.goto('https://www.ana.co.jp/');
  await nightmare.viewport(1024, 768);
  await nightmare.click('.select-dep'); // 出発地の空港クリック
  await nightmare.wait(500);
  await nightmare.wait('#depApo_ticket_modal');
  await nightmare.click('#depApo_ticket_modal .modal-list-cat li[data-val="3"]'); // 関東・甲信越
  await nightmare.wait(500);
  await nightmare.click('#depApo_ticket_modal .reslut-box li[data-val="HND"]'); // 羽田
  await nightmare.wait(500);
  await nightmare.wait('#arrApo_ticket_modal');
  await nightmare.click('#arrApo_ticket_modal .modal-list-cat li[data-val="4"]'); // 東海・北陸
  await nightmare.wait(500);
  await nightmare.click('#arrApo_ticket_modal .reslut-box li[data-val="KMQ"]'); // 小松
  await nightmare.wait(500);
  await nightmare.wait('#calid1');
  await nightmare.click('.calendar-nav .next a'); // 翌月
  await nightmare.click(`#calid1 td[onclick="cal1.chgSet(${year}, ${month + 3}, ${date})"] a`); // 3ヶ月後の今日
  await nightmare.click(`#calid1 td[onclick="cal1.chgSet(${year}, ${month + 3}, ${date + 2})"] a`); // 3ヶ月 + 2日後の今日
  await nightmare.click('#module-dom .btn-search'); // 検索
  await nightmare.wait('#availabilityResultRecommendFare');
  await nightmare.screenshot(`./${year}-${month + 4}-${date}__${year}-${month + 4}-${date}__HND__KMQ.png`);
  const table = await nightmare.evaluate(() => {
    // ここのコードは実際にブラウザで実行されるコードです
    const innerTextTrim = element => element.innerText.trim().replace(/\r?\n/g, '');

    const table = [[]];

    const headerThList = document.querySelectorAll('#availabilityResultRecommendFare thead tr th');
    for (let i = 0; i < headerThList.length; i += 1) {
      if (headerThList[i].getAttribute('class') === 'availabilityResultTime') {
        table[0][i] = '結果';
      } else if (headerThList[i].getAttribute('class') === 'availabilityPremiumLabel') {
        table[0][i] = 'プレミアムクラス';
      } else {
        table[0][i] = innerTextTrim(headerThList[i]);
      }
    }

    const trList = document.querySelectorAll('#availabilityResultRecommendFare tbody tr');
    for (let j = 0; j < trList.length; j += 1) {
      const tr = trList[j];

      const flightTime = innerTextTrim(tr.querySelector('th p.availabilityResultFlightTime'));
      const flightDetailSpanList = tr.querySelectorAll('th p.availabilityResultFlightDetail span');

      const body = [
        flightTime + ' ' + innerTextTrim(flightDetailSpanList[0]) + ' ' + innerTextTrim(flightDetailSpanList[1]),
      ];

      const bodyTdList = tr.querySelectorAll('td');
      for (let k = 0; k < bodyTdList.length; k += 1) {
        const fee = innerTextTrim(bodyTdList[k]);
        const yenIndex = fee.indexOf('円');

        body.push(yenIndex !== -1 ? fee.substring(0, yenIndex + 1) : fee);
      }

      table.push(body);
    }

    return table;
  });

  table.forEach(row => console.log(row.join('\t')));

  await nightmare.end();
};

main();

今回のコードは ANA サイトから国内線羽田 – 小松間の実行日の3ヶ月後の今日から3ヶ月 + 2日後の今日の行きの航空券を探すところを自動化してみました。

実行結果はこちら

Tab を区切り文字にしているのでこのままコピーして Excel に貼り付ければいい感じに見れるようになります。

こんな感じで、ブラウザの動作も自動化ができるぞ!

えっ?帰りの航空券の値段がわからないって?それはみなさんへの宿題です!わからないことがあれば質問箱か問い合わせに書いてあるメールアドレスまでお願いします