Edited at
Node.jsDay 12

puppeteerを使った開発の勘所


はじめに

この記事は、Node.js Advent Calendar 2018 12日目の記事です。

この記事では、これまでpuppeteerを使っていくつか実装したことを元に、puppeteerで開発するためにはどうやって考え、どう進めれば良いかを私の感覚で説明するものです。

puppeteerはHeadless Chromeを操作するためのライブラリです。すなわちHeadless Chromeの話でもあります。

※あくまでも私の考えるベストプラクティスになります。


今まで作ったもの

https://github.com/taminif/bettingInTheIPAT

https://github.com/taminif/buyInYodobashi


puppeteerとは

Node.jsでHeadless Chromeを操作するライブラリです。Chrome DevTools(開発者ツール)を使って操作しています。


GoogleChromeチームが作成・公開しています。

Node v6.4.0 以上で動作しますが、async/awaitに対応している Node v7.6.0 以上を使うのが無難です。


開発するときのベースの考え方


基本的な操作

ブラウザの操作について、細かく言えばたくさんあると思うのですが、基本はこの四つになると思います。


  • 入力

  • 操作

  • 遷移

  • 待機

人がやることを機械にやらせるので、puppeteerでも同様の操作を行なっていきます。

人と機械の違いは、人は操作対象を目で探して手で選択しますが、機械には出来ません。そのため、操作対象を探す処理と操作する処理が必要になります。


開発の勘所


Selector

上でも書いた通り、操作対象を探す必要があります。HTMLの要素を使って探していきます。

まず、ページ全体から探す場合ですが、この場合はPage Objectを使用します。


const browser = await puppeteer.launch({headless: false});
const page = await browser.newPage(); // これを使用していきます


IDがある場合

この場合は単純です。 # を使ってID名を指定します。

要素が存在する限り、確実に取得できます。


await page.click('#js_i_login0'); // IDがjs_i_login0のボタンをクリックする


IDがない場合

この場合、別のSelectorを使って目的の要素を取得する必要があります。具体的にはClassや要素名を使って取得します。

ただし、これらの場合HTMLの使用上複数の要素となる可能性があるため取得した後で自分が使用したい要素を絞り込む必要があります。


// `$` 一つだと一つのObjectが返る
const buyButtonDiv = await page.$('.strcBtn30'); // buyButtonDivはObjectで返ってくる(保証はないがページ先頭の要素)
// `$` 二つだと複数のObjectが返る
const buyButtonDivs = await page.$$('.strcBtn30'); // buyButtonDivsはArrayで返ってくる

確実に指定するためには $$ で複数の要素を取得した後、自分の使用したい要素を探します。


for (const buyButtonDiv of buyButtonDivs) {
const buyButton = await buyButtonDiv.$('a.btnRed');
if (buyButton !== null) {
await buyButton.click();
await page.waitForSelector('#sc_i_buy');
break;
}
}

ここでは buyButtonDiv から要素を指定しています。

取得したHTML Objectを使用してさらに要素を取得することが可能で、この場合は取得した要素の中にある要素のみ対象となります。Classが使い回されている場合でも、特定した要素の中に一つしかないことを確認できていればClassでも特定の一つの要素を取得することができます。


余談: Puppeteer Recorderはどうやっているか

Puppeteer RecorderというChrome拡張があります。自分のブラウザの操作をpuppeteerのコードに落とし込むものです。

Puppeteer Recorder

記録してみるとわかりますが、これはHTMLのトップレベルから順々に子要素を指定しています。

そのため、要素ごとに綺麗にIDやClassを指定しているサイトは上手いこと記録されますが、IDやClassが一切指定されていないサイトや、ほとんど共通のClassを使用しているサイトでは上手く動いてくれません

(レガシーなサイトはかなりつらい)


プロパティ取得

要素を取得した後、その要素の属性やテキストを取得して判定や処理を行いたい場合も取得が可能です。 $ で取得したオブジェクトは ElementHandle という JSHandle を継承したClassが帰ってきます。これをそのまま出力してもオブジェクトの出力結果が返るだけなので、プロパティを指定してやる必要があります。

const betItemText = await page.$('#bet-item');

const betItemTextValue = await betItemText.getProperty('value');
const betValue = await betItemTextValue.jsonValue();

二点、注意する必要があります。 getProperty でプロパティを指定しますが、ここでの戻り値もまた JSHandle です。値を取り出すために、 jsonValue を呼び出す必要があります。

もう一つは、これらのfunctionはPromiseを返すということです。その場で値が必要であれば、awaitを記述する必要があります。


wait

await ではありません。 wait です。例えばボタンをクリックした時に、JSの描画や処理や遷移を待つ必要がある場合に使用します。このドキュメントの通り wait はたくさんの種類があります。ここではそのノウハウについて書きました。


最初は余裕をもったwaitFor

慣れないうちは waitFor を使うと良いです。


await page.waitFor(1000); // ミリ秒

sleep などと用途は同じです。SPAの描画を待つ時やリンクをクリックした時など、本来はそれぞれ用途の違ったwaitがあるのですが、うまく動いてくれない時や確実に処理を進めたい時に使うと良いです。例えばリンクをクリックした時に10秒あれば確実に次の画面を表示できるようなところで、まず実装してみたい時などは多少綺麗でなくてもこれで実装していくとスムーズです。


慣れてきたらwaitForSelector, waitForNavigation

waitForSelector は指定したSelectorが描画されるまで待機します。


await page.waitForSelector('#sc_i_buy');

waitForNavigation は画面の読み込みが完了するまで待機します。読み込み完了のタイミングも指定することが可能です。


await page.waitForNavigation({waituntil: 'domcontentloaded'}); // DOMContentLoadedが発火するまで待機

これらをうまく使うことによって、より高速な処理を実装することが可能です。SPAで構成されたサイトを操作する場合には waitForSelector が有効など、使いこなすためのノウハウは多く存在します。

これら以外にもwaitFor〜はありますが、私自身もまだ使ったことはありません。待ちたい処理に適切に使用することが効率をあげるために必要なことなので、必要なケースになった場合は使用を検討しましょう。


Promiseとawait

ここまで何度か出てきましたが、puppeteerのほとんどの処理は Promise<> が返ります。また、返り値を使って次の処理を行うことが多く、ほぼ全ての処理を await を書いて待つことになります。結果、実装されたソースコードは await だらけになります。 await を忘れて意図した通りに動かないケースがハマりポイントになるため、 await をほぼ常に書くようにしましょう。


もっと細かい開発


マウス・キーボード操作

マウスやキーボードを使う処理も実装できます。


// マウス処理
const mouse = page.mouse;
const clickElement = await page.$('.first');
const rect = await clickElement.boundingBox();
await Promise.all([
mouse.move(parseFloat(rect.x + 20), parseFloat(rect.y + 20)),
page.waitFor(1000),
mouse.click(parseFloat(rect.x + 20), parseFloat(rect.y + 20), {
button: 'left',
clickCount: 1,
delay: 0,
}),
page.waitForSelector('.selection-amount'),
]);

マウス処理は div タグに onClick イベントが指定されているような、 page.click(); が動かないところに有効です。


// キーボード処理
await amountItem.press('0');
await amountItem.press('0');
await amountItem.press('1');

こちらも keyPress イベントが指定されているタグに対して有効です。


無限wait


await page.waitForSelector('.ecOrderFinishInfo', {timeout: 0});

待機秒数を0にすると、Selectorが出現するまで無限に待機します。購入処理など、時間がかかるけどこれで終了するような場面で有効です。


まとめ

puppeteerの開発ノウハウについて、自分がこれまで経験したケースを元にほぼほぼ全て記載できたと思います。よろしければ参考にしてください。

ログインが必要なサイトを操るのはとても楽しく、自分が日常的にやっていることも自動化できるかもしれません。サイトが修正されるたびに更新する必要はありますが、作ってしまえばあとは動かすだけで楽になるかもしれません。ぜひやってみてください。