Selenium Webdriver ではなく Puppeteer を使ってみる

最近とんと聞かなくなった Selenium Webdriver。最近は Puppeteer というヤツが流行っているらしい。なんか綴りが難しいな。w

目次

Puppeteer の概要

Puppeteer は、Selenium などと同じく、ブラウザの操作をプログラミングできるツール。主にヘッドレス Chrome を動かせて、最近 Firefox にも対応したとか。

Node.js 製で、npm からインストールできる。

puppeteerpuppeteer-core という2つのパッケージがある。puppeteer の方は、Chromium ブラウザを一緒に落としてきて、内部に持つ作りになっている。一方の puppeteer-core は、ブラウザをダウンロードしない。puppeteer-core が Puppeteer 本体で、puppeteer は「Puppeteer 本体 + ブラウザ」というワケ。ブラウザを内部に持っていれば、実行するマシン環境に依存しにくいのだが、ブラウザが 2・300MB 近くあるので、頻繁にインストールするには少々重たい感じ。一長一短。

今回、自分はホストマシンにインストール済の Chrome ブラウザを流用したかったので、puppeteer-core をメインに使うことにした。

インストールと実装

お試しリポジトリは以下に作った。

まずは puppeteer-core をインストールする。本稿執筆時点では v5.2.1 が最新だった。

$ npm i -S puppeteer-core

続いてコードを書いていく。最もシンプルなコードはこんな感じでいいかな。

const puppeteer = require('puppeteer-core');

(async () => {
  let browser = null;
  
  try {
    browser = await puppeteer.launch({
      headless: false,  // ヘッドレスにしない
      executablePath: '/PATH/TO/chrome.exe',  // Chrome ブラウザのパス
      // Chrome の仕様で、userDataDir もしくは --user-data-dir (どちらでも良い) で指定したディレクトリの直下の「Default」ディレクトリを探しに行ってしまう
      // --profile-directory もうまく効かないので、利用したいユーザデータを「…/User Data/Default」ディレクトリで配置するようにしておく
      userDataDir: '/PATH/TO/User Data',
      // page.type() の文字ごとにこの間隔 (ms) が開く
      slowMo: 10,
      ignoreHTTPSErrors: true,
      defaultViewport: {
        width: 1280,
        height: 720
      },
      args: [
        '--no-sandbox',
        '--disable-infobars',
        '--disable-session-crashed-bubble',  // セッションを復元するダイアログを非表示にする…効かない
        //'--kiosk',  // 最大化表示・メニューバーがなくなる。--disable-session-crashed-bubble と併用するとダイアログを消せる
        '--restore-last-session',  // --disable-session-crashed-bubble が効かないのでリストアさせることにする
        '--window-position=0,0',
        '--window-size=1280,720',
        // 起動済のブラウザにアタッチする時 (puppeteer.connect() 時) に以下のようなオプションがあるが、うまくいかず
        //'--remote-debugging-address=0.0.0.0',
        //'--remote-debugging-port=9222',
      ]
    });
    
    const page = await browser.newPage();
    await page.setViewport({ width: 1280, height: 720 });
    
    // 任意のページに遷移する : 画面遷移と読み込みを待つ
    await Promise.all([
      page.goto('https://google.co.jp/'),
      page.waitForNavigation({ waitUntil: 'networkidle2' })
    ]);
    
    const pageTitle = await page.title();
    console.log('遷移しました', pageTitle);
    
    // 適当に待って終了する
    await page.waitFor(1000);
  }
  catch(error) {
    console.error(error);
  }
  finally {
    // ブラウザを閉じる
    if(browser) {
      await browser.close();
    }
  }
})();

基本 Promise なので、async・await で同期的に書けるようにしておくと楽。browserpage は、グローバルな変数にしておくと、関数に切り出しやすいかもしれない。

細かいところは以下で説明していく。

実行時のオプション解説

puppeteer.launch() で色々とブラウザの設定をしているので、まずはそこの解説。

ブラウザの実行パスを指定する

executablePath オプションで、Chrome ブラウザの実行パスを指定している。Windows と MacOS の標準的なパスは以下のとおり。

ユーザプロファイルを指定する

今回は、普段使っている Chrome ブラウザのプロファイルを流用して、ウェブサービスへのログインを省略しようと思う。通常、Chrome の起動引数で指定する場合は --user-data-dir--profile-directory で指定するが、Puppeteer のバグなのか、一癖あった。

まず、--profile-directory オプションが効かない。なので --user-data-dir で渡したパスの直下にある Default/ ディレクトリが参照される前提で、構成を用意しないといけない。通常のユーザプロファイルは以下のようなパスにある。

上のパスの直下に、Default ディレクトリが用意されている状態にしたい。場合によっては目的のプロファイルが Profile 2 などというディレクトリ名で存在する場合もあるので、そういう時はディレクトリ名を Default にリネームしてしまう。

操作の間隔

slowMo オプションは、クリックや文字入力などの操作の間隔をミリ秒で指定するモノ。

文字入力時の遅延は page.type() メソッドのオプションで指定できるので、この slowMo オプションはその他の全般的な操作の間隔を早めにするか遅めにするか、の設定と思った方が良いだろう。

PC の処理性能やネットワーク通信速度、対象のウェブサイトの都合に合わせて、100ms とか 500ms とか、ゆっくりめに操作してあげた方が安定するかもしれない。

起動引数

args プロパティは Chrome が受け取れる引数を指定する場所。画面サイズやら何やらを指定できる。

Puppeteer で操作すると、セッションを復元するかどうか尋ねるダイアログがよく出てしまう。コレを消すにはいくつか方法がある。

  1. --kiosk--disable-session-crashed-bubble オプションを併用する。代わりに全画面表示になる
    • 全画面表示はちょっとキモいので避けたい
    • --disable-session-crashed-bubble オプション単体だと効果がない
  2. --kiosk--incognito オプションを併用する。代わりに全画面表示・かつシークレットモードになる
    • 今回は既存のクッキーを利用したいので却下
  3. --restore-last-session オプションを使う。代わりに最後に開いていたタブが復元されるので、タブ数が増える
    • タブ数が増えるのがキモいが、まだマシかな

ということで、今回は 3. の方法を採用している。

画面操作

puppeteer.launch()browser を用意したら、browser.newPage() 新たなページを開いて、変数 page を使用していく。

任意の URL に遷移するには page.goto() を使用するが、ページの読み込みが完了するまで待つため、以下のイディオムを使う。

await Promise.all([
  page.goto('https://google.co.jp/'),
  page.waitForNavigation({ waitUntil: 'networkidle2' })
]);

waitForNavigation()goto() の完了後に呼んでも意味がないので、Promise.all() で両方の Resolve を待機する。

後は色々な API があるので詳細は割愛するが、Selenium と似たような感じで、CSS セレクタで要素を指定して要素を操作したり、内容を参照したりできる。

未解決 : WSL での利用

WSL2 側で Node.js と Puppeteer を用意し、Windows 側の Chrome を操作したかったのだが、両者のネットワークを飛び越えられずに断念した。Windows → WSL2 の通信では localhost が上手く解決されるのだが、 WSL2 → Windows の通信は localhost ではダメなので、上手くいかないみたい。/etc/resolv.conf に記載されているのが Windows 側の localhost 相当なので、コレを利用して puppeteer.connect() で繋いでみようとかしたけど、コレも上手くいかず断念している。

結局 GitSDK を Windows 側に入れ、ポータブルな Node.js を Windows 側に用意して、WSL2 を使わずに運用している。コレなら Windows 側の Chrome をヘッドありで動かせるのだが、なんだかなぁ…。

ヘッドレスで良くて、Windows 側の Chrome ブラウザを使う気がなければ、WSL2 側に Chromium をインストールしてしまってヘッドレスで動かすのもアリだろう。WSL2 のみで完結させるということ。

今のところ、Windows と WSL をまたいだ Puppeteer 操作はできていない。

以上

今回はココまで。

Chrome ブラウザを内蔵した puppeteer パッケージもあり、基本は Chrome ブラウザしか扱えないものの、その代わり環境構築が簡単で、Selenium のように Java や Selenium Hub や何やらを用意せずとも、Node.js だけで完結できる手軽さが魅力。

DOM 操作を行う API は覚え直しになるし、textContent の扱いなんかにちょっと面倒臭さはあるものの、Selenium よりは気楽な感じがある。

Windows と WSL をまたいだ操作は、WSL の都合でまだ上手くいっていないが、夢がある。今後に期待。

参考文献