Puppeteerを使った開発の勘所

## はじめに
この記事は、[Node.js Advent Calendar 2018](https://qiita.com/advent-calendar/2018/nodejs) 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` 以上を使うのが無難です。

### 2019/11/02追記
Puppeteerのメジャーバーションが2にアップデートされ、Node.js v6はサポート外になり、Puppeteerを動かすためにはNode.js v8以上が必要になりました。

## 開発するときのベースの考え方
### 基本的な操作
ブラウザの操作について、細かく言えばたくさんあると思うのですが、基本はこの四つになると思います。

- 入力
- 操作
- 遷移
- 待機

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

## 開発の勘所
### Selector
上でも書いた通り、操作対象を探す必要があります。HTMLの要素を使って探していきます。
まず、ページ全体から探す場合ですが、この場合はPage Objectを使用します。

```javascript

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

#### IDがある場合
この場合は単純です。 `#` を使ってID名を指定します。
要素が存在する限り、確実に取得できます。

```javascript

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

#### IDがない場合
この場合、別のSelectorを使って目的の要素を取得する必要があります。具体的にはClassや要素名を使って取得します。
ただし、これらの場合HTMLの使用上複数の要素となる可能性があるため取得した後で自分が使用したい要素を絞り込む必要があります。

```javascript

// `$` 一つだと一つのObjectが返る
const buyButtonDiv = await page.$('.strcBtn30'); // buyButtonDivはObjectで返ってくる（保証はないがページ先頭の要素）
// `$` 二つだと複数のObjectが返る
const buyButtonDivs = await page.$$('.strcBtn30'); // buyButtonDivsはArrayで返ってくる
```
確実に指定するためには `$$` で複数の要素を取得した後、自分の使用したい要素を探します。

```javascript

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](https://chrome.google.com/webstore/detail/puppeteer-recorder/djeegiggegleadkkbgopoonhjimgehda)

記録してみるとわかりますが、これはHTMLのトップレベルから順々に子要素を指定しています。
そのため、要素ごとに綺麗にIDやClassを指定しているサイトは上手いこと記録されますが、IDやClassが一切指定されていないサイトや、ほとんど共通のClassを使用しているサイトでは上手く動いてくれません
（レガシーなサイトはかなりつらい）

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

```javascript
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の描画や処理や遷移を待つ必要がある場合に使用します。[このドキュメントの通り](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagewaitforselectororfunctionortimeout-options-args) `wait` はたくさんの種類があります。ここではそのノウハウについて書きました。

#### 最初は余裕をもったwaitFor
慣れないうちは `waitFor` を使うと良いです。

```javascript

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

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

#### 慣れてきたらwaitForSelector, waitForNavigation
`waitForSelector` は指定したSelectorが描画されるまで待機します。

```javascript

await page.waitForSelector('#sc_i_buy');
```

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

```javascript

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

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

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

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

## もっと細かい開発

### マウス・キーボード操作

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

```javascript

// マウス処理
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();` が動かないところに有効です。

```javascript

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

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

### 無限wait

```javascript

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

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

## まとめ
Puppeteerの開発ノウハウについて、自分がこれまで経験したケースを元にほぼほぼ全て記載できたと思います。よろしければ参考にしてください。
ログインが必要なサイトを操るのはとても楽しく、自分が日常的にやっていることも自動化できるかもしれません。サイトが修正されるたびに更新する必要はありますが、作ってしまえばあとは動かすだけで楽になるかもしれません。ぜひやってみてください。