Puppeteerメモ1：ログイン後にファイルをメモリ上にダウンロード／メモ2：手動ログインしたブラウザに接続

# 記事の内容

- Puppeteerメモ1：ログインしないとアクセスできないファイルを、Puppeteerで自動ログイン後にrequest-promiseを使ってメモリ上にダウンロードします。
- Puppeteerメモ2：開発時は頻繁にアプリを起動するわけですが、Puppeteerにログイン処理させるとアプリ起動の度に頻繁にログインを行うことになり、クローリング先サイトから挙動不審とみなされかねません。そこで、アプリ起動前に手動でブラウザを立ち上げてログインまで行い、Puppeteerからそのブラウザに接続することを考えました。

<font color=red>記事追加（2021-12-27）</font>
メモ1でrequest-promiseを使ってファイルをダウンロードしましたが、ブラウザの外からダウンロードするよりも、ブラウザのFetch APIを使ってダウンロードした方がクロール先から不審に思われないかな？と考えて、そのバージョンを追加しました。

# クローリングするサイトをローカルに準備

本記事ではローカル環境でWordPressをインストール／起動して、クローリング先としました。

WordPressのインストールと起動方法（Docker利用）：
https://docs.docker.jp/compose/wordpress.html

本記事でのWordPressサイトのURLは
http://192.168.1.101:8000
です。

# ソースコード

GitHub： https://github.com/kanedaq/sample_puppeteer

記事全体を通して、.envは以下の通りです。

```

BASE_URL="http://192.168.1.101:8000"
BROWSER_URL="http://localhost:9222"
WORDPRESS_LOGIN="wai"
WORDPRESS_PASS="secret"
HIDE_BROWSER=0
SLEEP_MILLISECOND=2000
BROWSER_TIMEOUT_MILLISECOND=0
PAGE_TIMEOUT_MILLISECOND=0
```

## 改良前のコード（ソースファイル名：launch.ts）

Puppeteerでブラウザを自動操作して、WordPressにログインします。

```launch.ts

import * as puppeteer from "puppeteer";
import * as dotenv from "dotenv"

(async () => {
    let browserWordpress: puppeteer.Browser | null = null;

    try {
        let selector: string;

        // .env読み込み
        dotenv.config();
        const baseUrl = process.env.BASE_URL
        const isHeadless = (Number(process.env.HIDE_BROWSER) != 0);
        const sleepMsec = Number(process.env.SLEEP_MILLISECOND);
        const browserTimeoutMsec = Number(process.env.BROWSER_TIMEOUT_MILLISECOND);
        const pageTimeoutMsec = Number(process.env.PAGE_TIMEOUT_MILLISECOND);

        // Puppeteerを起動
        browserWordpress = await puppeteer.launch({
            timeout: browserTimeoutMsec,  // タイムアウト設定
            headless: isHeadless, // Headlessモードで起動するかどうか
            slowMo: 20, // 指定のミリ秒スローモーションで実行する
        });

        // 新しい空のページを開く.
        const pageWordpress: puppeteer.Page = await browserWordpress.newPage();
        await pageWordpress.setViewport({
            width: 1200,
            height: 800,
        });
        await pageWordpress.setDefaultNavigationTimeout(pageTimeoutMsec); // タイムアウト設定

        // ログインページへ
        const loginUrl = baseUrl + "/login";
        await pageWordpress.goto(loginUrl, { waitUntil: ["load", "networkidle0"] });
        await pageWordpress.waitForTimeout(sleepMsec);

        // ユーザー名を入力
        const wordpressLogin = process.env.WORDPRESS_LOGIN;
        selector = "#user_login";
        await pageWordpress.evaluate(selector => { document.querySelector(selector).value = ""; }, selector);
        await pageWordpress.type(selector, wordpressLogin);

        // パスワードを入力
        const wordpressPass = process.env.WORDPRESS_PASS;
        selector = "#user_pass";
        await pageWordpress.evaluate(selector => { document.querySelector(selector).value = ""; }, selector);
        await pageWordpress.type(selector, wordpressPass);

        // ログインボタンを押す
        // 参考ページ：
        // https://qiita.com/hnw/items/a07e6b88d95d1656e02f
        selector = "#wp-submit";
        await Promise.all([
            pageWordpress.waitForNavigation({ waitUntil: ["load", "networkidle0"] }),
            pageWordpress.click(selector),
        ]);
        await pageWordpress.waitForTimeout(sleepMsec);

        // ログアウト処理は省略した
        console.log("無事終了");
    }
    finally {
        // ブラウザを終了
        if (browserWordpress ?? false) {
            await browserWordpress.close();
        }
    }
})();
```

## Puppeteerメモ1：ログイン後にファイルをメモリ上にダウンロード（ソースファイル名：download.ts）

ブラウザを自動操作してログインを行い、ブラウザからCookieを取得してrequest-promiseに渡し、管理ページ内の任意のhtmlをダウンロードします。
launch.tsに「行頭が+の行」を追加しました。

```download.ts

import * as puppeteer from "puppeteer";
import * as dotenv from "dotenv"
+import * as rp from "request-promise";

(async () => {
    let browserWordpress: puppeteer.Browser | null = null;

    try {
        let selector: string;

        // .env読み込み
        dotenv.config();
        const baseUrl = process.env.BASE_URL
        const isHeadless = (Number(process.env.HIDE_BROWSER) != 0);
        const sleepMsec = Number(process.env.SLEEP_MILLISECOND);
        const browserTimeoutMsec = Number(process.env.BROWSER_TIMEOUT_MILLISECOND);
        const pageTimeoutMsec = Number(process.env.PAGE_TIMEOUT_MILLISECOND);

        // Puppeteerを起動
        browserWordpress = await puppeteer.launch({
            timeout: browserTimeoutMsec,  // タイムアウト設定
            headless: isHeadless, // Headlessモードで起動するかどうか
            slowMo: 20, // 指定のミリ秒スローモーションで実行する
        });

        // 新しい空のページを開く.
        const pageWordpress: puppeteer.Page = await browserWordpress.newPage();
        await pageWordpress.setViewport({
            width: 1200,
            height: 800,
        });
        await pageWordpress.setDefaultNavigationTimeout(pageTimeoutMsec); // タイムアウト設定

        // ログインページへ
        const loginUrl = baseUrl + "/login";
        await pageWordpress.goto(loginUrl, { waitUntil: ["load", "networkidle0"] });
        await pageWordpress.waitForTimeout(sleepMsec);

        // ユーザー名を入力
        const wordpressLogin = process.env.WORDPRESS_LOGIN;
        selector = "#user_login";
        await pageWordpress.evaluate(selector => { document.querySelector(selector).value = ""; }, selector);
        await pageWordpress.type(selector, wordpressLogin);

        // パスワードを入力
        const wordpressPass = process.env.WORDPRESS_PASS;
        selector = "#user_pass";
        await pageWordpress.evaluate(selector => { document.querySelector(selector).value = ""; }, selector);
        await pageWordpress.type(selector, wordpressPass);

        // ログインボタンを押す
        // 参考ページ：
        // https://qiita.com/hnw/items/a07e6b88d95d1656e02f
        selector = "#wp-submit";
        await Promise.all([
            pageWordpress.waitForNavigation({ waitUntil: ["load", "networkidle0"] }),
            pageWordpress.click(selector),
        ]);
        await pageWordpress.waitForTimeout(sleepMsec);

+        // ブラウザのCookieを利用して、管理ページ内の任意のURLからhtmlをダウンロード
+        selector = "#welcome-panel > div > div > div:nth-child(1) > p > a";
+        const requestOptions = {
+            method: "GET",
+            // uri: baseUrl + "wp-admin/customize.php?autofocus[panel]=themes",
+            uri: await pageWordpress.$eval(selector, el => el.getAttribute("href")),
+            headers: {
+                Cookie: (await pageWordpress.cookies()).map(cookie => cookie.name + '=' + cookie.value).join(';')
+            }
+        }
+        const body = await rp(requestOptions);
+        console.log(body);
+        console.log("----");
+        console.log(requestOptions);
+
        // ログアウト処理は省略した
        console.log("無事終了");
    }
    finally {
        // ブラウザを終了
        if (browserWordpress ?? false) {
            await browserWordpress.close();
        }
    }
})();
```

実行すると、ダウンロードしたhtmlと、requestOptionsがログに出力されました。

```

（前略）
twenty-twenty-one\/#dark-mode-s
upport\">\u30c0\u30fc\u30af\u30e2\u30fc\u30c9\u306b\u3064\u3044\u3066\u306e\u8a73\u7d30\u306f\u3053\u3061\u3089<\/a>\u3092\u3054\u89a7\u304f\
u3060\u3055\u3044\u3002<\/p><p>\u30c0\u30fc\u30af\u30e2\u30fc\u30c9\u306f\u30da\u30fc\u30b8\u306e\u53f3\u4e0b\u306b\u3042\u308b\u30dc\u30bf\u
30f3\u304b\u3089\u3082\u6709\u52b9\u5316\u30fb\u7121\u52b9\u5316\u3067\u304d\u307e\u3059\u3002<\/p>","instanceNumber":27};
})( _wpCustomizeSettings.controls );
                </script>
                </div>
</body>
</html>

----
{
  method: 'GET',
  uri: 'http://192.168.1.101:8000/wp-admin/customize.php?autofocus[panel]=themes',
  headers: {
    Cookie: '（略）'
  }
}
無事終了
```

### <font color=red>（2021-12-27追加）Fetch APIバージョン（ソースファイル名：download_fetch.ts）</font>

```download_fetch.ts

import * as puppeteer from "puppeteer";
import * as dotenv from "dotenv"

async function fetchGetAsText(page: puppeteer.Page, url: string) {
    return page.evaluate(async (url) => {
        // ここのスコープのコードはブラウザに渡される
        try {
            const response_fetch = await window.fetch(
                url,
                {
                    method: "GET"
                }
            );

            // headers
            let headers = {};
            for (const hh of response_fetch.headers) {
                headers[hh[0]] = hh[1];
            }

            return {
                // "type": response_fetch.type,
                // "url": response_fetch.url,
                // "redirected": response_fetch.redirected,
                "status": response_fetch.status,
                // "ok": response_fetch.ok,
                // "statusText": response_fetch.statusText,
                "headers": JSON.stringify(headers),
                "body": await response_fetch.text(),
                // "bodyUsed": false
            }
        }
        catch (e) {
            return {
                "status": "400",
                "headers": {},
                "body": e.toString(),
            }
        }
    }, url);
}

(async () => {
    let browser: puppeteer.Browser | null = null;

    try {
        let selector: string;

        // .env読み込み
        dotenv.config();
        const baseUrl = process.env.BASE_URL;
        const wordpressLogin = process.env.WORDPRESS_LOGIN;
        const wordpressPass = process.env.WORDPRESS_PASS;
        const isHeadless = (Number(process.env.HIDE_BROWSER) != 0);
        const sleepMsec = Number(process.env.SLEEP_MILLISECOND);
        const browserTimeoutMsec = Number(process.env.BROWSER_TIMEOUT_MILLISECOND);
        const pageTimeoutMsec = Number(process.env.PAGE_TIMEOUT_MILLISECOND);

        // Puppeteerを起動
        browser = await puppeteer.launch({
            timeout: browserTimeoutMsec,  // タイムアウト設定
            headless: isHeadless, // Headlessモードで起動するかどうか
            slowMo: 20, // 指定のミリ秒スローモーションで実行する
        });

        // 新しい空のページを開く.
        const pageWordpress: puppeteer.Page = await browser.newPage();
        await pageWordpress.setViewport({
            width: 1200,
            height: 800,
        });
        await pageWordpress.setDefaultNavigationTimeout(pageTimeoutMsec); // タイムアウト設定

        // ログインページへ
        const loginUrl = baseUrl + "/login";
        await pageWordpress.goto(loginUrl, { waitUntil: ["load", "networkidle0"] });
        await pageWordpress.waitForTimeout(sleepMsec);

        // ユーザー名を入力
        selector = "#user_login";
        await pageWordpress.evaluate(selector => { document.querySelector(selector).value = ""; }, selector);
        await pageWordpress.type(selector, wordpressLogin);

        // パスワードを入力
        selector = "#user_pass";
        await pageWordpress.evaluate(selector => { document.querySelector(selector).value = ""; }, selector);
        await pageWordpress.type(selector, wordpressPass);

        // ログインボタンを押す
        // https://qiita.com/hnw/items/a07e6b88d95d1656e02f
        selector = "#wp-submit";
        await Promise.all([
            pageWordpress.waitForNavigation({ waitUntil: ["load", "networkidle0"] }),
            pageWordpress.click(selector),
        ]);
        await pageWordpress.waitForTimeout(sleepMsec);

        // ブラウザのJavaScriptで、管理ページ内の任意のURLからhtmlをダウンロード
        // const uri = baseUrl + "/wp-admin/customize.php?autofocus[panel]=themes";
        selector = "#welcome-panel > div > div > div:nth-child(1) > p > a";
        const uri = await pageWordpress.$eval(selector, el => el.getAttribute("href"));
        const response_download = await fetchGetAsText(pageWordpress, uri);
        console.log(response_download.body);
        console.log("----");
        console.log(response_download.headers);
        console.log("----");
        console.log(response_download.status);

        // ログアウト処理は省略した
        console.log("無事終了");
    }
    finally {
        // ブラウザを終了
        if (browser ?? false) {
            await browser.close();
        }
    }
})();
```

実行すると、レスポンスのbody（ダウンロードしたhtml）、headers、statusがログに出力されました。

```

（前略）
twenty-twenty-one\/#dark-mode-support\">\u30c0\
u30fc\u30af\u30e2\u30fc\u30c9\u306b\u3064\u3044\u3066\u306e\u8a73\u7d30\u306f\u3053\u3061\u3089<\/a>\u3092\u3054\u89a7\u304f\u3060\u3055\u3044
\u3002<\/p><p>\u30c0\u30fc\u30af\u30e2\u30fc\u30c9\u306f\u30da\u30fc\u30b8\u306e\u53f3\u4e0b\u306b\u3042\u308b\u30dc\u30bf\u30f3\u304b\u3089\u
3082\u6709\u52b9\u5316\u30fb\u7121\u52b9\u5316\u3067\u304d\u307e\u3059\u3002<\/p>","instanceNumber":27};
})( _wpCustomizeSettings.controls );
                </script>
                </div>
</body>
</html>

----
{"cache-control":"no-cache, must-revalidate, max-age=0","connection":"Keep-Alive","content-encoding":"gzip","content-type":"text/html; charset
=UTF-8","date":"Mon, 27 Dec 2021 02:34:36 GMT","expires":"Wed, 11 Jan 1984 05:00:00 GMT","keep-alive":"timeout=5, max=93","referrer-policy":"s
trict-origin-when-cross-origin","server":"Apache/2.4.51 (Debian)","transfer-encoding":"chunked","vary":"Accept-Encoding","x-frame-options":"SA
MEORIGIN","x-powered-by":"PHP/7.4.27"}
----
200
無事終了
```

## Puppeteerメモ2：手動ログインしたブラウザに接続（ソースファイル名：connect.ts）

puppeteer.launch()の代わりにpuppeteer.connect()を使用します。
launch.tsとの差分は以下の通りです。

```connect.ts

import * as puppeteer from "puppeteer";
import * as dotenv from "dotenv"

(async () => {
-    let browserWordpress: puppeteer.Browser | null = null;
-
-    try {
-        let selector: string;
-
        // .env読み込み
        dotenv.config();
        const baseUrl = process.env.BASE_URL
-        const isHeadless = (Number(process.env.HIDE_BROWSER) != 0);
+        const browserUrl = process.env.BROWSER_URL
        const sleepMsec = Number(process.env.SLEEP_MILLISECOND);
-        const browserTimeoutMsec = Number(process.env.BROWSER_TIMEOUT_MILLISECOND);
        const pageTimeoutMsec = Number(process.env.PAGE_TIMEOUT_MILLISECOND);

-        // Puppeteerを起動
-        browserWordpress = await puppeteer.launch({
-            timeout: browserTimeoutMsec,  // タイムアウト設定
-            headless: isHeadless, // Headlessモードで起動するかどうか
-            slowMo: 20, // 指定のミリ秒スローモーションで実行する
-        });
+        // 手動でログイン済のブラウザにPuppeteerから接続する。
+        // ブラウザ起動コマンド：
+        //     cd node_modules\puppeteer\.local-chromium\win64-938248\chrome-win
+        //     .\chrome --remote-debugging-port=9222
+        // ログインページ（あらかじめ手動でログインする）：
+        //     http://192.168.1.101:8000/login
+        const browserWordpress = await puppeteer.connect({
+            browserURL: browserUrl,
+            slowMo: 20, // 指定のミリ秒スローモーションで実行する
+        });

        // 新しい空のページを開く.
        const pageWordpress: puppeteer.Page = await browserWordpress.newPage();
        await pageWordpress.setViewport({
            width: 1200,
            height: 800,
        });
        await pageWordpress.setDefaultNavigationTimeout(pageTimeoutMsec); // タイムアウト設定

-        // ログインページへ
-        const loginUrl = baseUrl + "/login";
-        await pageWordpress.goto(loginUrl, { waitUntil: ["load", "networkidle0"] });
-        await pageWordpress.waitForTimeout(sleepMsec);
-
-        // ユーザー名を入力
-        const wordpressLogin = process.env.WORDPRESS_LOGIN;
-        selector = "#user_login";
-        await pageWordpress.evaluate(selector => { document.querySelector(selector).value = ""; }, selector);
-        await pageWordpress.type(selector, wordpressLogin);
-
-        // パスワードを入力
-        const wordpressPass = process.env.WORDPRESS_PASS;
-        selector = "#user_pass";
-        await pageWordpress.evaluate(selector => { document.querySelector(selector).value = ""; }, selector);
-        await pageWordpress.type(selector, wordpressPass);
-
-        // ログインボタンを押す
-        // 参考ページ：
-        // https://qiita.com/hnw/items/a07e6b88d95d1656e02f
-        selector = "#wp-submit";
-        await Promise.all([
-            pageWordpress.waitForNavigation({ waitUntil: ["load", "networkidle0"] }),
-            pageWordpress.click(selector),
-        ]);
-        await pageWordpress.waitForTimeout(sleepMsec);
-
+        // 管理ページへ
+        const adminUrl = baseUrl + "/wp-admin";
+        await pageWordpress.goto(adminUrl, { waitUntil: ["load", "networkidle0"] });
+        await pageWordpress.waitForTimeout(sleepMsec);
+
-        // ログアウト処理は省略した
+        // ログアウトはログイン同様、手動で行うことにする
        console.log("無事終了");
})();
```

connect.tsのソースコード全体は以下の通りです。

```connect.ts

import * as puppeteer from "puppeteer";
import * as dotenv from "dotenv"

(async () => {
        // .env読み込み
        dotenv.config();
        const baseUrl = process.env.BASE_URL
        const browserUrl = process.env.BROWSER_URL
        const sleepMsec = Number(process.env.SLEEP_MILLISECOND);
        const pageTimeoutMsec = Number(process.env.PAGE_TIMEOUT_MILLISECOND);

        // 手動でログイン済のブラウザにPuppeteerから接続する。
        // ブラウザ起動コマンド：
        //     cd node_modules\puppeteer\.local-chromium\win64-938248\chrome-win
        //     .\chrome --remote-debugging-port=9222
        // ログインページ（あらかじめ手動でログインする）：
        //     http://192.168.1.101:8000/login
        const browserWordpress = await puppeteer.connect({
            browserURL: browserUrl,
            slowMo: 20, // 指定のミリ秒スローモーションで実行する
        });

        // 新しい空のページを開く.
        const pageWordpress: puppeteer.Page = await browserWordpress.newPage();
        await pageWordpress.setViewport({
            width: 1200,
            height: 800,
        });
        await pageWordpress.setDefaultNavigationTimeout(pageTimeoutMsec); // タイムアウト設定

        // 管理ページへ
        const adminUrl = baseUrl + "/wp-admin";
        await pageWordpress.goto(adminUrl, { waitUntil: ["load", "networkidle0"] });
        await pageWordpress.waitForTimeout(sleepMsec);

        // ログアウトはログイン同様、手動で行うことにする
        console.log("無事終了");
})();
```