Puppeteer でスクレイピングを行う方法

### 前提条件

* Yarn がインストールされている

### 作成手順

1. 作業用ディレクトリを作成
1. コマンドライン上で作業用ディレクトリに移動
1. `yarn init -y` を実行して package.json を生成
1. `yarn add puppeteer` を実行。スクレイピングは Puppeteer で行う
1. 次のような内容の index.js を作成。`node index.js` を実行すると「Example Domain」が出力される。これは `<h1>` 要素のテキストである

```javascript
const puppeteer = require("puppeteer");

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto("https://example.com");
  await page.waitForSelector("h1");

  const elem = await page.$("h1");
  const text = await elem.evaluate(el => el.textContent);
  console.log(text);

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

### 色々

* `await browser.newPage()` によって Page オブジェクトが取得できる。これは、ブラウザでいう `document` に相当すると思う
* `document.querySelector()` は `page.$()` と似ている
* `document.querySelectorAll()` は `page.$$()` と似ている
* `await page.$()` によって要素のようなものが取得できる
* 要素のようなものは、そのままでは属性値もテキストも取得できない。取得するためには `.evaluate()` を利用する
* 要素のようなものを `e` と置いたとき、`e.evaluate(el => el.textContent)` でテキストを、`e.evaluate(el => el.getAttribute("href"))` で属性値を取得できる。これらの返す値は Promise である

### 便利な記述

* `await page.waitForSelector()` : セレクタに一致する要素が現れるまで待機する
* `await page.goto()` : 特定のページに遷移する
* `await page.waitForTimeout(1000)` : 1秒間何もしない。スクレイピング対象のWebサイトに負荷をかけないために使う
* `await page.evaluate(() => { /* ... */ })` : コールバック関数はブラウザのコンテキストで実行される。よってブラウザ上のあらゆる情報が取得できる。たとえば次のように書くと、ドキュメントの横幅と縦幅が取得できる

```javascript
const rect = await page.evaluate(() => {
  return {
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight,
  };
});
```

`page.$()` や `page.$$()` は `page.evaluate()` で代用可能。たとえば最初のプログラムは次のように書ける。フロントエンジニアはこちらのほうが覚えることが少なくて楽かも。

```javascript
const puppeteer = require("puppeteer");

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto("https://example.com");
  await page.waitForSelector("h1");

  const text = await page.evaluate(() => {
    const h1 = document.querySelector("h1");
    return h1.textContent;
  });
  console.log(text);

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

### 色々その2

* 書き捨てのプログラムであればピュアな JavaScript でOK。VSCode を使えば十分に補完が効いてくれる
* TypeScript で書きたい場合は、作業用ディレクトリに移動後、以下のコマンド群を実行する。index.ts を編集すると自動で再実行してくれる（以下でインストールしている[nodemon](https://github.com/remy/nodemon/)は22.9kスター、[ts-node](https://github.com/TypeStrong/ts-node)は9.1kスターを獲得しているのでどちらも信用できると思う）

```
yarn init -y
yarn add typescript ts-node nodemon
touch index.ts
yarn nodemon index.ts
```

もしかすると足りないパッケージがあるかもしれない。手元の環境では、この3つのパッケージをインストールするだけで正常に実行できた。

* Puppeteer の `page.evaluate()` に渡す関数内では `document` などを使うが、何も設定せずに TypeScript で `document` を参照すると「`document` は見つからない」と指摘される。`yarn tsc --init` で tsconfig.json を作成し、`lib: []` の配列に `"DOM"` を追加することで解決する。TypeScript を使った Puppeteer のコードは次のようになる


```typescript
import puppeteer from "puppeteer";

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto("https://example.com");
  await page.waitForSelector("h1");
  
  const text = await page.evaluate(() => {
    return document.querySelector("h1")?.textContent ?? null;
  });
  console.log(text);

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

### まとめ

まずはこの手順でスクレイピングプログラムを作成し、完成したらひとつの関数にまとめて、Cloud Functionで使ったりBigQueryに結果を保存したりすると良さそう。そのときのことを考えると、TypeScriptであらかじめ型付けしておいたほうが楽。

ということで、記事中にある以下のコマンド群はこれから頻繁に使っていくと思う。

```
yarn init -y
yarn add typescript ts-node nodemon
touch index.ts
yarn nodemon index.ts
```