PuppeteerでWebスクレイピングしてハマったところをまとめてみた

## 備忘録
`Puppeteer`を使ってWebスクレイピングをやっていた時にハマったことを整理しておく。

### インストール編
`Puppeteer`がそもそも起動しなくて困ったことがあった。

#### puppeteerが動作しない（Ubuntu）
初めてpuppeteerを起動した際にChromiumが立ち上がらなかったことがあった。
ログにhttps://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md にアクセスしてみろと書かれてたのでアクセスしたら、どうやらパッケージをインストールする必要があるとのこと。
端末に以下のパッケージが抜けてないか確認し、必要に応じてインストールする必要があるらしい。

```
gconf-service
libasound2
libatk1.0-0
libatk-bridge2.0-0
libc6
libcairo2
libcups2
libdbus-1-3
libexpat1
libfontconfig1
libgcc1
libgconf-2-4
libgdk-pixbuf2.0-0
libglib2.0-0
libgtk-3-0
libnspr4
libpango-1.0-0
libpangocairo-1.0-0
libstdc++6
libx11-6
libx11-xcb1
libxcb1
libxcomposite1
libxcursor1
libxdamage1
libxext6
libxfixes3
libxi6
libxrandr2
libxrender1
libxss1
libxtst6
ca-certificates
fonts-liberation
libappindicator1
libnss3
lsb-release
xdg-utils
wget
```

すべてインストールしたら問題なく起動した。

#### puppeteerが動作しない（Windows10）
今度はWindows編。
Windowsで実行すると`puppeteer.launch`で

```
Error: spawn UNKNOWN
```
が発生してChromiumが起動しない。

とりあえず https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md を参考に

```index.js
const browser = await puppeteer.launch({
  ignoreDefaultArgs: ['--disable-extensions'],
});
```
と書き直して再度実行したが動かないまま。
他に手段がないか模索していたところ、この[GitHub Issue](https://github.com/GoogleChrome/puppeteer/issues/4054)が参考になった。

どうやら`puppeteer`経由でインストールされた`Chromium`に問題があるようだ。

試しに私も以下の手順でやってみた。
 1. https://download-chromium.appspot.com/ から`Chromium`をダウンロード
 2. `chrome-win.zip`を**Desktop**上で解凍(重要)
 3. `node_module/puppeteer/.local-chromium/win64-n/`内にある`chrome-win`ディレクトリを解凍したものと置き換える

その後再度実行すると問題なくChromiumが起動した。
しかし、ほかのところで解凍すると何故か上手くいかず一度ここでハマった。


### パフォーマンスチューニング編
過去にWebスクレイピングを行うにあたり、対象のデータ全てを取得するのに同じサイトにリクエストを大量に投げなくてはならないことがあり、**相手のサーバーに負荷をかけないように注意する必要があった(下手するとDoS攻撃とみなされかねない)**。同時に自身の端末（サーバー）にも極度の負荷がかからないように調整する必要があり、そのために奮闘したことをまとめてみた。

#### 処理時間が長いためLambdaが使えない
最初にこちらのリソースの確保にあたり、`AWS Lambda`を使って対応できないか考えた。
LambdaはTimeoutの時間を最大15分に設定することができる。
しかしスクレイピングの処理時間は長いと数時間かかる上に、今回はリクエスト数が多いため複数立てても足りないことが分かった。

結局`EC2`を複数立ち上げて対応することにした。

#### sequencialに処理が行われない
方針として、各端末に対して事前に用意していたURL情報をテキストファイルに書き込み、その後`fs.createReadStream`と`readline`を使ってURLを一つずつ読み込みスクレイピングを実行するようにした。

しかし、試しに実行すると`readline.on`で**全て非同期に処理が行われてしまう問題が発生した。(`forEach`と同じ類)**。
これでは仮にURLのリストが1万件あったとしたら、全てのリクエストが一斉に送られてしまう。
(先にソケットが切れるか端末のCPUやメモリが枯渇するだろうけど)

非同期で処理が行われないよう方法を模索した結果、ある程度の効率を考慮したうえで以下の方法で対応することにした。
1. URLリストのテキストファイルをあらかじめ複数に分割しておく
2. パラメータにテキストファイル名を持つ非同期関数を作成し、その中に`fs.readFileSync`でテキストファイル内のURL情報を取得、その後`split`で配列化してforループを回すことで**逐次処理にした処理を記載する**
3. パラメータにそれぞれのファイル名を渡して関数を実行。

#### メモリリークが発生

```
Failed to launch chrome!
(node:25974) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 exit listeners added. Use emitter.setMaxListeners() to increase limit
```

最初のエラーメッセージからおそらくpuppeteer周りで問題が発生していることがわかったので原因調査を行った。
原因は以下の箇所によってメモリリークが発生していたためだったことが分かった。

```scraping.js
    for(let url of urls) {
        let browser = await puppeteer.launch();
        let page = await browser.newPage();
        await page.goto(url);

        // 処理

        await browser.close();
    }
```
要因として二つ考えられた。

1. ブラウザ開きすぎ
繰り返し文の中で`puppeteer.launch()`をしているため、これではURLの数だけブラウザを立ち上げることになってしまう。
**ブラウザは一度だけ立ち上げれば十分。**

2. page.close()してない
ページを開いたが閉じていなかった。
**使わないページは閉じておくこと。**

修正して以下のように書き直したところ、メモリ使用量が改善されて問題が解消した。

```scraping.js
 let browser = await puppeteer.launch(); // 呼び出すのは一度だけ
    for(let url of urls) {
        let page = await browser.newPage();
        await page.goto(url);

        // 処理

        await page.close(); // 不要なページは都度閉じる
    }
 await browser.close();   // ブラウザも忘れずに閉じる
```