開発室ブログ

JavaScript Puppeteer 開発環境

PuppeteerでChromeを自動操作!

テストの自動化なんかを検証していたとき、E2Eテストってどうするんだ? Seleniumとかそういうの? などといろいろ調べていたことがあります。 JenkinsからE2Eテスト用スクリプト起動、テスト実行サーバーで走らせて、スクリーンショット取…って、サーバーに画面ねぇし…あ、Xvfbとか使えばできるんだ…めんどいな…うーん…と、もにょもにょしておりました。

Google Chromeのヘッドレスモード

いろいろ調べていたところ、Chromeにヘッドレスモードというものがあるのを知りました。
この機能を使うと、GUIのない環境でもChromeが使えてしまいます。Webアプリの操作やHTMLの取得はもちろん、スクリーンショット保存やPDF保存なども可能です。
そしてこれは “ブラウザ” なので、今っぽいなんかすごいフロントエンドでもキッチリ動くところも強みですね。

Node.jsライブラリ Puppeteer

Chromeを動かすためのライブラリが開発されています。

github.com

このPuppeteerというのを使えば、簡単にChromeの制御ができます。(v1.0.0のrcきてますね)

実際に動かしてみる

Linux * ヘッドレスで動かす、スクショを保存する、などはいろいろなところで解説されていると思うので、ここではWindowsGUI上で動かしてみます。PuppeteerはGUIアリでも制御可能です。今回の環境などは下記の通り。

  • Windows10 64bit
  • Node.js 8.9.2
  • Puppeteer 0.13.0
  • 検索ワードをコマンドライン引数で渡す
  • Googleトップから検索→適当なリンクをクリックして遷移

まずはNodeをインストールして

ダウンロード | Node.js

コマンドプロンプトにてPuppeteerをインストール

cd c:\コードを置く場所
npm i puppeteer

で、コードはこんな感じです。
PuppeteerどころかNode.jsすらよくわかってないですが、ひとまず動きました。

'use strict';

const puppeteer = require('puppeteer');
process.on('unhandledRejection', console.dir);

( async() => {
  // ブラウザ起動と設定
  const browser = await puppeteer.launch({
    // モード選択 / デフォルトtrue
    headless: false,
    // SSL証明書エラー無視する場合(オレオレ証明書など)
    "ignoreHTTPSErrors" : true,
    // 目視確認用に操作遅延(ms)
    "slowMo" : 200,

    // 他設定
    args: [
      // プロキシ使う場合はここ
      //'--proxy-server=192.168.100.252:8080',
      // Chromeウィンドウのサイズ
      '--window-size=1600,950',
      // Chromeウィンドウのポジション
      '--window-position=100,50',
      '--no-sandbox'
    ]
  });

  const page = await browser.newPage();

  // 画面の大きさ設定
  // Chromeのウィンドウ自体の大きさの調整ではないです
  await page.setViewport({width: 1600, height: 950});

  try {
    // Googleトップ開く  タイムアウト 5000ms
    await page.goto('https://www.google.co.jp', {waitUntil: 'networkidle2', timeout: 5000});

    // テキストボックスが来るまで待つ
    await page.waitFor('input[name=q]');

    // コマンドラインからの第一引数をテキストボックスに入力
    await page.type('input[name=q]', process.argv[2]);
    // Enterキーを押す
    await page.keyboard.press('Enter');

    // 指定したセレクタが来るまで待つ  タイムアウト 2500ms
    await page.waitForSelector('h3 a', {waitUntil: 'networkidle2', timeout: 2500});
    // 目視確認用:1500ms待つ
    await page.waitFor(1500);

    // 検索結果のページタイトルを取得してコンソールへ表示
    const links = await page.evaluate(() => {
      const anchors = Array.from(document.querySelectorAll('h3 a'));
      return anchors.map(anchor => anchor.textContent);
    });
    console.log(links.join('\n'));

    // 検索結果のページのリンクを適当選んでクリック
    let linkElmList = await page.$$('h3.r a');
    let target = getRandomArbitary(0, 4);
    await linkElmList[target].click({waitUntil: 'networkidle2', timeout: 2500});

    // 目視確認用:3000ms待つ
    await page.waitFor(3000);
  }
  catch (e) {
    // エラー出力
    console.log(e);

    // ブラウザを閉じて終了
    await browser.close();
    process.exit(200);
  }
  
  // ブラウザを閉じて終了
  await browser.close();
})();


// https://developer.mozilla.org/ja/
// min から max までの乱数を返す関数
function getRandomArbitary(min, max) {
  return Math.floor( Math.random() * (max - min + 1) ) + min;
}

こんだけです。べんりですね。
実行は

cd c:\コードを置く場所
node コードの名前.js 単語

こんな感じ。 node test.js 白菜 みたいな感じでしょうか。 複数ワードで検索するときは node test.js "クリスマスケーキ 2017" のようにダブルクォートで囲みます。

実行すると、Chromeがボッと立ち上がって勝手に動いてくれるはず…!
(下の画像のように、自動制御である旨の表示が出ます)

f:id:ajdev:20171207171526p:plain
まだ予約してない。いつものケーキ屋さんはもう間に合わない予感。

ハマったところとか

以下、こまったところです。

SSLとプロキシの設定が良くわかんなかった

ignoreHTTPSErrorsproxy-server をどう指定するのか、わりと調べたかも。まぁ、公式にちゃんとあるんですが。
焦ってググらないで、まずは公式をちゃんと読まないとダメだなーと思いました。

puppeteer/proxy.js at master · GoogleChrome/puppeteer · GitHub

puppeteer/api.md at master · GoogleChrome/puppeteer · GitHub

スクショがうまく取れない

ページがロードされる前に await page.screenshot() が走ってしまうと、ページが崩れた状態で保存されてしまうことがあります。page.waitFor()page.waitForSelector() で待つようにしましょう。
これは、ボタンやリンククリックでのページ遷移でも同様です。

ヘッドレスだと動きが見えない

いや、当たり前なんですがね。
でも見えないんで「は? 見ればわかるだろ…」みたいな当たり前の挙動を見落としてしまい、しばらくハマりました。

自分が遭遇したのは 「白菜」で検索したのに「白猫テニス」で検索している という現象でした。
スクショを取って調べるも、Googleトップでは「白菜」ときっちり入力されていて、次の検索結果画面ではどう見ても「白猫テニス」になっている。他の単語でテストしてみても、同じような状況が再現したり、しなかったり。
ホントにわかんなくなったので、Windowsマシンに持ってきて、GUI上で動かしてみました。
入力のようすを見ると「 “白” ”菜” 」と漢字で1文字ずつ入力されて、サジェストが表示されていました。よく見るとサジェストには「白」つながりの候補が並んでいて、「白猫テニス」も入ってる! あー、そこにいたのか!

f:id:ajdev:20171207112643p:plain
サジェストの裏に「

Google 検索」ボタンが隠れている

たぶんですが、入力完了して「白菜」サジェストに切り替わる前に await page.click('input[type="submit"]') が走り、「Google 検索」ボタンの位置とかぶった「白」サジェストをクリックしてしまったのかな? と思います。
どのみちサジェストはESCキー押すなり、別の場所をクリックするなりしないと消えないっぽいので、 await page.keyboard.press('Enter') に変更しました。

いつもは「”h” “a” “k” “u” “s” “a” “i”」と入力して変換、Enterで遷移するので気づきませんでした。そして、サジェストのこともすっかり忘れていました。

怪しいときは、GUIで走らせて目視確認すると何かわかるかもしれません。
というかスクリプトを作ったら、一通りGUIで目視確認したほうが安心かなぁ。

テスト以外に何かに使えるかなー

とか考えましたが、別にヘッドレスブラウザじゃなくてもいいよね? みたいな感じになってしまって、思いつかない…。スクレイピングするにしても、ConoHaやAWSの安いサーバー(CentOS 7)で複数ガーっと走らせると結構つらい。やっぱ重いんだなーってなる。
ただ、Windowsで気楽に動くのはいいなと思いました。

まぁ自分としては、自動制御してる感じがなんかかっこよくて好きなので、それで満足です。

RecentPost