Puppeteerで自動ブラウザ操作するときによく使う操作一覧

[テックタッチアドベントカレンダー](https://qiita.com/advent-calendar/2019/techtouch)16日目を担当する@kosyです。
15日目は @terunuma による [PWA のクライアントサイドのみで手軽に画像をリサイズできるアプリを作ろうとした話](https://qiita.com/terunuma/items/886d09ee70ece5dece7f) でした。canvas、PWAの知識が深まる素晴らしい内容です。画像をリサイズしたいときに活用させていただきますね。

## はじめに
簡単にブラウザ操作を自動化できるPuppeteerについて、自身の備忘録も兼ねてよく使う操作を紹介します。
環境構築については以下の記事に分かりやすい説明が載っているため、本記事では省きます。
[Puppeteerのセットアップから使い方まで〜ブラウザ操作の自動化〜](https://qiita.com/kanoe/items/9043a81d28a1b733b2e2)

## 操作
#### ベース
```javascript:base.js
const puppeteer = require('puppeteer');

(async() => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    // ページ移動 
    await page.goto('http://操作したいページのアドレス.com'); 

    // 何か操作

    browser.close();
})();
```
これに操作を追加していくだけでOK

#### ページを移動する
```javascript
await page.goto('https://example.com');

// オプションを指定する場合
await page.goto('https://example.com', { waitUntil: 'domcontentloaded'});
```
 `page.goto()`でページ移動できます。
オプションの `{ waitUntil }`では、読み込み完了のタイミングを指定することができます。`domcontentloaded`を指定すると、DOMContentLoaded発火後に次の操作を実行させることできます。ページ遷移後すぐにCSSセレクタを探しに行くと、CSSがまだ読み込まれておらず操作エラーになることがあるため、指定しておくと快適です。

他にも`networkidle`というものがあります。これはネットワーク接続があるかどうかを確認するもので、以下の２種類あります。
- `networkidle0`: 500ms以内にネットワーク接続がなくなった時
- `networkidle2`: 500ms以内のネットワーク接続が2つ以下になった場合

#### クリックする
```javascript
page.click('CSSセレクタ');
```
指定したセレクタをクリックします。
clickして移動した先のページで何か操作したい場合は、`await page.waitForNavigation({ waitUntil: "domcontentloaded（もしくはnetworkidle）"});`をセットで書いておきます。そうすれば遷移先のページをちゃんと読み込み完了後に、その後の処理を実行してくれます。

#### 入力する
```javascript
page.type('CSSセレクタ', 'hoge');
```
指定したセレクタに'hoge'と入力します。

#### テキストを取得する
```javascript
const text = await page.$eval('CSSセレクタ', text => text.textContent)
```
`<div class="hoge">text</div>`のようにタグで囲まれたテキストを取得できます。

#### URLを取得する
```javascript
//aタグのリンク取得
const href = await page.$eval('CSSセレクタ', a => a.href);

//imgタグの画像URL取得
const src = await page.$eval('CSSセレクタ', img => img.src);
```
#### １つのセレクタから複数の要素を取得する
```javascript
const value = await page.$eval('CSSセレクタ', item => {
        href: item.href,
        textContent: item.textContent,
        innerHTML: item.innerHTML
        });
```

#### 待機する
```javascript
// 指定したセレクタが表示されるまで待機
await page.waitFor('.hoge');
await page.waitForSelector('.hoge');

// 5秒待機
await page.waitFor(5000);
```
#### スクリーンショットやPDFを取得する
```javascript
await page.screenshot({path: 'screenshot.png'}); 

await page.pdf({path: 'page.pdf'});
```
pathには保存場所や保存時のファイル名を指定できます。

#### 指定した要素が存在しているかどうか知りたい時
```javascript
const exists = await page.$('CSSセレクタ').then(res => !!res);

if(exists){
// 要素がある場合の処理
}else{
// 要素がない場合の処理
}
```
要素の有無で処理を分けたい場合などに使えますね。

#### 抽出したデータをファイルに出力する
```javascript
const fs = require('fs');

 // JSONで出力する
fs.writeFile('data/result.json', JSON.stringify(data),(err) => {
        if (err) throw err;
       console.log('done');
    });

// csvで出力する
fs.writeFile('data/result.csv', csv, (err) => {
    if (err) throw err;
    console.log('file saved');
  });
```
一番初めに`const fs = require('fs');`を定義しましょう。

## サンプル
じゃらんの札幌市中央区の観光スポット人気No.1の場所名・詳細リンク・画像URLを取得し、JSONファイルに出力します。

```javascript
const puppeteer = require('puppeteer');
const fs = require('fs');

(async () => {
  // puppeteerを起動
  const browser = await puppeteer.launch();

  // ページを開く
  const page = await browser.newPage();

  // じゃらんへ移動
  await page.goto('https://www.jalan.net/kankou/cit_011010000/', { waitUntil: 'networkidle2' });

  // 対象のセレクタがあるかどうか確認
  const isLoadingSucceeded = await page.$('.item > .item-listContents > .item-info > .rank-ico-01 > a').then(res => !!res);
  if (isLoadingSucceeded) {
    // 観光地名とリンクを取得
    spotName = await page.$eval('.item > .item-listContents > .item-info > .rank-ico-01 > a', item => ({
      href: item.href,
      textContent: item.textContent,
    }));
    // 観光地詳細ページへ移動
    await page.goto(`${spotName.href}`, { waitUntil: 'networkidle2' });
    // 画像URL取得
    spotImg = await page.$eval('.main > #galleryArea > .galleryArea-innerGallery > .detailGallery_box:nth-child(2) > img', img => img.src);
    // 観光地説明を取得
    spotAddress = await page.$eval('body > .container > .main > #aboutArea > p:nth-child(2)', text => text.textContent);

    const spotList = [];
    spotList.push(spotName.textContent, spotName.href, spotImg, spotAddress);

    // jsonで出力
    fs.writeFile('../data/sample.json', JSON.stringify(spotList), (err) => {
      if (err) throw err;
      console.log('done');
    });
  }

  await browser.close();
})();

```
##### 出力結果
```javascript
["札幌ステラプレイス","https://www.jalan.net/kankou/spt_guide000000176252/","https://cdn.jalan.jp/jalan/img/9/kuchikomi/1879/KL/8e82b_0001879684_2.jpg","センターとイーストの２つのゾーンに分かれ、レストランなど１６０店舗を超える日本最大級の巨大ショッピングモール。開放的な館内にはアーティストのオブジェや作品が配され、空間全体が楽しめるようにデザインされている。買う・食べる・観る・遊ぶ・感じるなど、新しいライフスタイルに出会えるスポット。"]
```

## おわりに
puppeteerでよく使う操作紹介でした。
もっと知りたくなった方は公式ドキュメントをぜひご覧ください。[Puppeteer API](https://github.com/puppeteer/puppeteer/blob/master/docs/api.md#pagepdfoptions)

最後のサンプルがなぜじゃらんなのか。なぜ観光地を取得しているのか。
それは１２月１９日に投稿予定の記事で明らかになります。

明日17日目の担当は@mxxxxkxxxxです！