Selenium Webdriver ではなく Puppeteer を使ってみる
最近とんと聞かなくなった Selenium Webdriver。最近は Puppeteer というヤツが流行っているらしい。なんか綴りが難しいな。w
目次
Puppeteer の概要
Puppeteer は、Selenium などと同じく、ブラウザの操作をプログラミングできるツール。主にヘッドレス Chrome を動かせて、最近 Firefox にも対応したとか。
Node.js 製で、npm からインストールできる。
puppeteer
と puppeteer-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
続いてコードを書いていく。最もシンプルなコードはこんな感じでいいかな。
index.js
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 で同期的に書けるようにしておくと楽。browser
や page
は、グローバルな変数にしておくと、関数に切り出しやすいかもしれない。
細かいところは以下で説明していく。
実行時のオプション解説
puppeteer.launch()
で色々とブラウザの設定をしているので、まずはそこの解説。
ブラウザの実行パスを指定する
executablePath
オプションで、Chrome ブラウザの実行パスを指定している。Windows と MacOS の標準的なパスは以下のとおり。
- Windows :
C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
- JS で書く時はバックスラッシュをエスケープする :
C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe
- GitBash など Cygwin 環境 :
/c/Program Files (x86)/Google/Chrome/Application/chrome.exe
- WSL から Windows 側のブラウザを指定する場合 :
/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe
- JS で書く時はバックスラッシュをエスケープする :
- MacOS :
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
ユーザプロファイルを指定する
今回は、普段使っている Chrome ブラウザのプロファイルを流用して、ウェブサービスへのログインを省略しようと思う。通常、Chrome の起動引数で指定する場合は --user-data-dir
と --profile-directory
で指定するが、Puppeteer のバグなのか、一癖あった。
まず、--profile-directory
オプションが効かない。なので --user-data-dir
で渡したパスの直下にある Default/
ディレクトリが参照される前提で、構成を用意しないといけない。通常のユーザプロファイルは以下のようなパスにある。
- Windows :
C:\Users\【ユーザ名】\AppData\Local\Google\Chrome\User Data
- エスケープ :
C:\\Users\\【ユーザ名】\\AppData\\Local\\Google\\Chrome\\User Data
- Cygwin 形式 :
/c/Users/【ユーザ名】/AppData/Local/Google/Chrome/User Data
- WSL から利用 :
/mnt/c/Users/【ユーザ名】/AppData/Local/Google/Chrome/User Data
- エスケープ :
- MacOS :
~/Library/Application Support/Google/Chrome
上のパスの直下に、Default
ディレクトリが用意されている状態にしたい。場合によっては目的のプロファイルが Profile 2
などというディレクトリ名で存在する場合もあるので、そういう時はディレクトリ名を Default
にリネームしてしまう。
操作の間隔
slowMo
オプションは、クリックや文字入力などの操作の間隔をミリ秒で指定するモノ。
文字入力時の遅延は page.type()
メソッドのオプションで指定できるので、この slowMo
オプションはその他の全般的な操作の間隔を早めにするか遅めにするか、の設定と思った方が良いだろう。
PC の処理性能やネットワーク通信速度、対象のウェブサイトの都合に合わせて、100ms とか 500ms とか、ゆっくりめに操作してあげた方が安定するかもしれない。
起動引数
args
プロパティは Chrome が受け取れる引数を指定する場所。画面サイズやら何やらを指定できる。
Puppeteer で操作すると、セッションを復元するかどうか尋ねるダイアログがよく出てしまう。コレを消すにはいくつか方法がある。
--kiosk
と--disable-session-crashed-bubble
オプションを併用する。代わりに全画面表示になる- 全画面表示はちょっとキモいので避けたい
--disable-session-crashed-bubble
オプション単体だと効果がない
--kiosk
と--incognito
オプションを併用する。代わりに全画面表示・かつシークレットモードになる- 今回は既存のクッキーを利用したいので却下
--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 の都合でまだ上手くいっていないが、夢がある。今後に期待。
参考文献
- ヘッドレス Chrome Node API 「Puppeteer」 - Qiita
- 起動オプションなど
- Windows-アプリケーション/Chrome/ユーザープロファイルフォルダの確認 - yanor.net/wiki
--user-data-dir
の指定方法と確認方法。chrome://version
の「プロフィールパス」で確認できる
- javascript - In Puppeteer how to switch to chrome window from default profile to desired profile - Stack Overflow
--user-data-dir
と--profile-directory
。結局--user-data-dir
直下のDefault
ディレクトリを参照しようとするみたい
- Puppeteerで次ページへの遷移を待つ - Qiita
- ページ遷移の待ち方
- puppeteerでの要素の取得方法 - Qiita
- 複数要素の取得方法。
$$eval()
を使ってtextContent
を取るのが良さそう
- 複数要素の取得方法。
- Chrome remote debugging from another machine - Stack Overflow
--remote-debugging-port
と--remote-debugging-address
オプション
- WSL2 refusing to connect to chrome.exe · Issue #5957 · puppeteer/puppeteer · GitHub
- WSL から呼ぶ時は
--remote-debugging-address
を指定するらしいが、上手く行かなかった
- WSL から呼ぶ時は
- puppeteer.connectを試す - hiroqn's Data.ByteString.Lazy.ByteString
puppeteer.connect()