Puppeteerでスクレイピング

今更感はあるのですが、Puppeteerでスクレイピングしてみました。


## 環境
Node.js 12.18.3
Puppeteer 5.5.0
TypeScript 4.1.3

## やったこと
#### 環境の準備
まず、適当なディレクトリを初期化して、必要なモジュールをインストールしていきます。

```
$ npm init -y
$ npm i puppeteer
$ npm i -D typescript ts-node @types/node @types/puppeteer
$ npx tsc --init
```

今回、TypeScriptは[ts-node](https://github.com/TypeStrong/ts-node)を使って実行します。
そのため、`package.json`に下記のスクリプトを追記します。  

```package.json
〜略〜
"scripts": {
  "start": "ts-node src/index.ts"
},
〜略〜
```

#### 実際のコード
配列で用意しておいたURLを直列で順番にスクレイピングしていく、というサンプルを作っていきます。 
先に全体のコードを置いておきます。  

```src/index.ts
const puppeteer = require('puppeteer');
const path = require('path');
const fs = require('fs');

// スクレイピング対象のURL
const urls = ['https://qiita.com/', 'https://developer.mozilla.org/en-US/'];

// スクレイピング
const crawl = async (url: string) => {

    // ファイル名用の現在日付作成
    const now = (() => {
        const d = new Date();
        return `${d.getFullYear()}_${(d.getMonth()+1)}_${d.getDate()}_${d.getHours()}-${d.getMinutes()}-${d.getSeconds()}`;
    })();

    // ブラウザー開く
    const browser = await puppeteer.launch({
        headless: false,
        slowMo: 50,
        defaultViewport: {
            width: 1280,
            height: 800
        }
    });

    // 新規タブ
    const page = await browser.newPage();

    //  URLへアクセス
    await page.goto(url);

    // ScreenShot保存
    const imgPath = path.join('./ss', `${now}.png`);
    await page.screenshot({
        path: imgPath,
        fullPage: true,
    });

    // ドキュメントの情報を取得
    const metaData = await page.evaluate(() => {

        return {
            'title': document.querySelector('title')?.textContent,
            'description': (<HTMLMetaElement>document.querySelector('meta[name="description"]'))?.content,
            'h1': document.querySelector('h1')?.textContent,
        };
    });

    // セッション終了
    await browser.close();

    return {
        img: imgPath,
        ...metaData
    }
};

// 対象URL分スクレイピング処理を実行する
const handleCrawler = async () => {
    const r = [];
    for (let v of urls) {
        r.push(await crawl(v));
    }
    console.log(r);
};

(async () => {
    // スクリーンショット保存用のディレクトリがない場合
    if (!fs.existsSync('ss')) {
        // ScreenShot保存ディレクトリ作成後、実行
        fs.mkdir('ss', () => {
            handleCrawler();
        });
    }
    // 保存用ディレクトリが既存の場合、そのまま実行
    else {
        handleCrawler();
    }
})();
```

#### 何をやっているか

まずは、Puppeteerを使って、Chromeを起動します。  

```src/index.ts
// 〜略〜
// ブラウザー開く
const browser = await puppeteer.launch({
    headless: false,
    slowMo: 50,
    defaultViewport: {
        width: 1280,
        height: 800
    }
});
```
今回、実際にChromeが起動しているところを確認したいので、`headless`にfalseを指定して、Chromeがnon-headlessで起動するように指定しています。また、`slowMo`を指定することで、指定されたミリ秒数分、操作を遅延させています。

その後、タブを開いて対象URLに遷移し、スクリーンショットを保存します。

```src/index.ts
// 〜略〜
// ScreenShot保存
const imgPath = path.join('./ss', `${now}.png`);
await page.screenshot({
    path: imgPath, // ここでスクリーンショットを保存するローカルのパスを指定
    fullPage: true,
    // type: 'jpeg',
    // quality: 0
});
```
オプションにローカルのパスを指定すると、そこにスクリーンショットが保存されます。
qualityオプションを渡すことで画像の解像度を指定することができます。
試しに使ってみたところ、ページ全体のスクリーンショットが1.3MBほどあったページも、`quality: 0`を指定すると88KBほどになりました。サーバーの容量に制限があるときなどには使えるかも知れません。

続いて、ドキュメントの情報を取得しています。

```src/index.ts
// ドキュメントの情報を取得
const metaData = await page.evaluate(() => {

    return {
        'title': document.querySelector('title')?.textContent,
        'description': (<HTMLMetaElement>document.querySelector('meta[name="description"]'))?.content,
        'h1': document.querySelector('h1')?.textContent,
    };
});
```
今回ここで少しハマったのですが、素直に

```typescript
'description': document.querySelector('meta[name="description"]')?.content,
```
としてしまうと

```
Property 'content' does not exist on type 'Element'.
```

と怒られてしまいました。　　

`HTMLElement`のインターフェースには`content`というプロパティが無いことが原因なようで、`HTMLMetaElement`にキャストしてあげる必要があったようです。
大変助かりました。>https://qiita.com/vsanna/items/201d4af29086a01b6b12

#### 実行
実際に上記のソースコードを実行してみます。
`npm start`で実行されます。

```
$ npm start
```

実行したターミナルの標準出力に、スクレイピングの結果が出力されました。

```:出力結果
[
  {
    img: 'ss/2020_12_16_16-56-39.png',
    title: 'Qiita',
    description: 'Qiitaは、プログラマのための技術情報共有サービスです。 プログラミングに関するTips、ノウハウ、メモを簡単に記録 &amp; 公開することができます。',
    h1: 'How developers code is here.'
  },
  {
    img: 'ss/2020_12_16_16-56-44.png',
    title: 'MDN Web Docs',
    description: 'The MDN Web Docs site provides information about Open Web technologies including HTML, CSS, and APIs for both Web sites and progressive web apps. It also has some developer-oriented documentation for Mozilla products, such as Firefox Developer Tools.',
    h1: 'Resources for developers, by developers.'
  }
]
```
`/ss`配下にはスクリーンショットが保存されているのが確認できました。

```
$ ls -la ss
```

```:出力結果
total 3512
drwxr-xr-x  4 xxxx  staff      128 Dec 16 16:56 .
drwxr-xr-x  9 xxxx  staff      288 Dec 16 16:56 ..
-rw-r--r--  1 xxxx  staff  1340232 Dec 16 16:56 2020_12_16_16-56-39.png
-rw-r--r--  1 xxxx  staff   453752 Dec 16 16:56 2020_12_16_16-56-44.png
```

## 最後に
Puppeteer自体の使い方もとてもシンプルで、思っていたよりも簡単にWebスクレイピングを実装できました。
扱いやすいライブラリにめちゃくちゃ感謝です。（つづりが難しい...