Puppeteerで表示できない画面を探す

# Puppeteerで表示できない画面を取得しよう
さて問題です。**Puppeteer** とはなんでしょう？
**Puppeteer** とは、以下のようなことができるNode.jsのライブラリーです。

【**Puppeteer** でできること】
・ページのスクリーンショットやPDFの作成
・SPAをクロールしてプレレンダリングコンテンツを生成
・フォーム送信の自動化
などGoogle Chromeのブラウザでできることはほぼできるようです。

上記を駆使して、たとえば「ページ内の大量のデータから取得したいデータのみを抽出する」や「表示できないページを割り出す」などを行うことができ、普段の業務で自分で一つ一つ探して確認してたものを自動で行うことができるようになります。

※ 公式サイトは以下です

https://pptr.dev/

今回はこちらのライブラリーを使って、表示できない画面を探すツールを作りながら、
使用した各メソッドが何ができるメソッドなのかを説明していきたいと思います。

## 今回作ったもの
こちらのGitHubにて作成済みのものを公開しておりますので、よろしければクローンして動かしてみてください。

https://github.com/nenemim12721/find404

## 実装手順
それでは、実装を説明していきます。

### 要件
今回は、以下の要件でツールを作成しました。
① data.jsonに記載したwebページの中で表示できないものがあれば全部取得
② デバイスはSPでもPCでも選択して確認できるようにしたい
③ Puppeteerを使ってみる

①は、実務などで検証環境によってあるページとないページがある場合に、「どのページが存在するかを確認したいという場面で使用できたらいいな」という気持ちと、「コードがいまいちわからない人でも簡単に使えたらいいな」という気持ちから、JSONにURLを持たせるという要件を入れてます。
②は、サービスによってはスマホ画面とPC画面をサーバー側で出しわけしている場合に、URLだけでは対象のデバイスでの検証ができないことから要件に入れました。
③は完全にPuppeteerを習得して業務の中でもツールで楽できるようにしていきたいという気持ちがあり、筆者が使いたかったからという理由だけです。

### 手順① Puppeteerをインストールしよう
以下コマンドでPuppeteerをインストールします。
```terminal
npm i Puppeteer
```

### 手順② 必要なファイルを作る
今回、処理は`find404.js`に記載し、URLなどは`data.json`に記載しようと思いますので、
以下の階層構造になるように★マークがついているファイルを作成します。
```
find404 ┳ node_modules
        ┣ data.json ★
        ┣ find404.js ★
        ┣ package-lock.json
        ┗ package.json
```

### 手順③ data.jsonに引用したい情報を記載する
今回、JSONに持たせたいデータは`対象画面のURL`と`対象デバイス`です。
今回は以下の様にします。
```json
{
    "targetPageUrl": [
        "https://pptr.dev/next/",
        "https://pptr.dev/next/hogeFuga",
        "https://nodejs.org/ja/",
        "https://developer.mozilla.org/ja/"
    ],
    "targetDevice": {
        "sp": "iPhone 12"
    }
}
```

`targetPageUrl`には配列にしたURLを記載します。
上記サンプルは２つ目のURL(`https://pptr.dev/next/hogeFuga`)が、実際には存在しないページです。
`targetDevice.sp`には対象デバイスを指定しています。
指定できるデバイスの値は以下ページを参照してください。今回は`iPhone 12`を指定しています。

https://pptr.dev/next/api/puppeteer.knowndevices/

### 手順④ found404.jsに処理を書く
今回の完成系のコードは以下です。以下コードに沿って、`Puppeteer`で用意されているメソッドを解説していきます。
```js
const puppeteer = require('puppeteer');
const fs = require('fs');
const jsonPath = './data.json';
// 検索条件やURLをdata.jsonから取得する
const data = JSON.parse(fs.readFileSync(jsonPath));
// 使用するデバイスを取得
const device = data.targetDevice.sp ? puppeteer.KnownDevices[data.targetDevice.sp] : false;
// data.jsonカラ取得したURLのリスト
const targetUrl = data.targetPageUrl;
// 対象urlが指定されていなかったら処理を中止
const noTargetUrl = !targetUrl.length || targetUrl.some((url) => !url.length);
if (noTargetUrl) {
    console.log('data.jsonのtargetPageUrlにurlを指定してください><');
    return;
}

(async() => {
    // 新規ブラウザをシークレットブラウザで立ち上げる
    const browser = await puppeteer.launch({
        headless: false,
        timeout: 10000,
        ignoreDefaultArgs: ['--disable-extensions'],
    });
    const context = await browser.createIncognitoBrowserContext();
    // 新規タブを開く
    const page = await context.newPage();
    if (device) {
        // 開くデバイスを指定
        await page.emulate(device);
    }

    try {
        const notFound = [];
        for (url of targetUrl) {
            // ページにアクセスする
            const response = await page.goto(url);
            // ページにアクセスできないあるいは404が帰ってきた時
            if (!response || response.status() >= 400) {
                notFound.push(url);
            }
        }

        // 400番台のページをログ出力
        console.log('==============================');
        if (!notFound.length) {
            console.log('すべてのページが存在します');
        } else {
            console.log('以下のページが表示できません。');
            console.log('==============================');
            notFound.forEach((url) => {
                console.log(url)
            });
        }
        console.log('==============================');
    } catch(e) {
        if (e instanceof Error) {
            console.log(e.message);
        } else {
            console.log('予期せぬエラー');
        }
    } finally {
        // チェックが終了したらブラウザーを閉じる
        await browser.close();
    }

})();
```

#### クラス
`Puppeteer`には以下のようなクラスが用意されており、各Classに用意されたメソッドを用いて処理を実現します。
実際には以下の記載よりも多くのクラスが存在しますが、今回は本記事のコードで使用しているものをピックアップして記載しています。

| Class名 | 概要 | コード上での使用箇所 |
|---|---|---|
| Puppeteer | Puppeteerのメインクラス | このクラスから直接生えているメソッドは今回は使用していません |
| PuppeteerNode | ブラウザのfetchとダウンロードのためのクラスで`require`で取得されます | 1行目の`puppeteer`で取得し、`const browser = await puppeteer.launch({})`でブランチを立ち上げるのに使用しています |
| Browser | `PuppeteerNode.launch()`や`Puppeteer.connect()`で生成されるクラスで、シークレットブラウザを指定するためのメソッドやブラウザを閉じるためのメソッドが用意されています | 本記事のコードでは`const context = await browser.createIncognitoBrowserContext();`の行で使用しています|
| Page | `Browser.newPage()`で生成されるクラスで、デバイスの指定やページ内の要素の取得やイベント実行などページとの対話を可能にするメソッドが用意されています | 本記事のコードでは、`await page.emulate(device);`でのデバイス指定や、`const response = await page.goto(url);`でのページへのアクセスに使用しています |
| HTTPResponse | `Page`クラスが受信するHTTPリクエストの応答です | 本記事のコードでは<code>if (!response &#x7c;&#x7c;response.status() >= 400) {}</code>でページが存在するかどうかの判定に使用しています |

#### KnownDevices

https://pptr.dev/api/puppeteer.knowndevices/

次の項で紹介する`Page.emulate()`の引数として使用できる、`Puppeteer`が用意しているデバイスのリストです。
コード一行目の`puppeteer`に用意されてますので以下のようにして使用可能です。
```js
const device = puppeteer.KnownDevices['iPhone 12'];
```
今回のコードでは、以下の行で設定を行っています。
```js
// 使用するデバイスを取得
const device = data.targetDevice.sp ? puppeteer.KnownDevices[data.targetDevice.sp] : false;
```

#### Page.emulate()
上記で説明した`KnownDevices`を使用して、デバイスを指定します。
`Page`クラスのメソッドです。使い方は以下の通りで、ページを開いてから使います。
```js
// 新規タブを開く
const page = await context.newPage();
await page.emulate(device);
```
上記で指定をすると以下のようにスマホ画面での実行がされます。(deviceには今回`iPhone 12`を指定しています)
![ダウンロード.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319623/f7738a01-1327-7553-275e-beae1b6efa56.gif)

#### PuppeteerNode.launch()
`PuppeteerNode`クラスのメソッドで、オプションなどを指定してブラウザのインスタンスの起動を行います。
以下は今回のコードですが、`headless`オプションを`false`に設定することで`Page.emulate()`の項目に載せたGIFのように、ブラウザが開かれて動いている様子が確認できます。デフォルトは`true`(headlessモード)です。`timeout`オプションはブラウザ起動までの待機時間の最大をms単位で指定が可能で、`0`を渡すとtimeoutの無効化ができます。
※ Windows実行時、Puppeteerでは、デフォルトで`--disable-extensions`フラグが有効かどうかを確認し、有効な場合に起動に失敗するのですが、`ignoreDefaultArgs: ['--disable-extensions']`を指定することで、回避して実行することが可能になります。
```js
// 新規ブラウザをシークレットブラウザで立ち上げる
const browser = await puppeteer.launch({
    headless: false,
    timeout: 10000,
    ignoreDefaultArgs: ['--disable-extensions'],
});
```

他にも指定できるoptionはあるのですが、今回は使っていないため割愛させていただきます。

#### Page.goto()
`Page`クラスのメソッドで、以下のようにURLを指定してあげることで、指定したページに遷移します。
```js
// ページにアクセスする
const response = await page.goto(url);
```
url以外にもオプションを引数に取ることができ、最大待機時間などを指定ができます。
また、本メソッドは`Promise<HTTPResponse | null>`を返却するメソッドとなっており、
以下のようにページアクセス時のステータスを取得することが可能です。
```js
// ページにアクセスできないあるいは404が帰ってきた時
if (!response || response.status() >= 400) {
    notFound.push(url);
}
```

ステータス以外にも、レスポンスに関連するHTTPヘッダーを持つオブジェクトやレスポンスに対するHTTPリクエストなどの取得も可能です。
ページ内で使っている非同期通信のエラー検知にも役立ちそうかと思います。


## まとめ
今回はページの存在チェックをして存在しない、あるいは見つからないページのURLをログで返してくれるツールを作りつつ、Puppeteerのクラスやメソッドについてツール内で使用したもの中心に説明をしてきました。
この記事をきっかけに、「Puppeteer試しにいじってみよう！」「意外と簡単に始められるね」と気軽に挑戦するきっかけにしていただけると嬉しいです。
今回ご紹介したクラスやメソッドだけでも、便利なツールの作成ができると思うのですが、他にも多くのクラスやメソッドが用意されてますので、ぜひ調べてお役立ちツールを作ってみてください！

## 参考
 - [Puppeteer](https://pptr.dev/)
 - [Puppeteerを使うなら知っておくべき現在警告が出る古いコード](https://iwb.jp/puppeteer-deprecated-code/)