Cloud Functions for Firebase と Puppeteer でスクレイピング

## はじめに

Cloud Functions for FirebaseとPuppeteerを使ってブラウザ操作を自動化したので、そのときやったことを書いていきます。

## なぜCloud Functions for Firebase？

最初はGCPの純粋なCloud Functionsを使うことを考えていたのですが、Cloud Functions for Firebaseの方が定期実行や開発・デプロイ方法がわかりやすいように感じたのでこちらを採用しました。

### 定期実行

GCPの場合：[Pub/Sub を使用して Cloud ファンクションをトリガーする](https://cloud.google.com/scheduler/docs/tut-pub-sub?hl=ja)
Cloud Functions for Firebaseの場合：[関数のスケジュール設定](https://firebase.google.com/docs/functions/schedule-functions?hl=ja)

GCPで定期実行をさせる場合はこちらの公式サイトのように、関数定義とスケジュールの定義が別々になるようで、わかりにくいと感じました。

対してCloud Functions for Firebaseの場合は関数定義の中にスケジューリングも含めることができるので、管理がしやすくわかりやすいです。

### 開発・デプロイ

GCPの場合：[Cloud Functions の作成](https://cloud.google.com/functions/docs/writing?hl=ja),[ローカルマシンからのデプロイ](https://cloud.google.com/functions/docs/deploying/filesystem?hl=ja)
Cloud Functions for Firebaseの場合：[はじめに: 最初の関数の記述、テスト、デプロイ](https://firebase.google.com/docs/functions/get-started?hl=ja)

Cloud Functions for Firebaseはfirebase-toolsを使うことで、簡単に作成・デプロイができそうです。

## なぜPuppeteer？

これは特に大した理由はありません

- 最近TypeScriptを書きまくってる
- PythonのScrapy, Seleniumはやったことあるので、違うのでやってみたい

といった感じです。

## Cloud Functions for Firebaseの料金

実装に入る前に、まずは料金の確認です。
左がSpark(無料プラン)、右がBlaze(従量制プラン)です。
https://firebase.google.com/pricing?hl=ja

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209015/0c675470-bda6-a9b0-61e9-6543a2dd8627.png" alt="image-20201105013405112" style="zoom:50%;" />


Cloud Functions for Firebaseを使う場合、Blazeプランにアップデートする必要があります。
ただ、アップデートしたとしても、無料枠が結構大きいので、簡単なものであれば無料枠内でおさまりそうです。

しかし、見積もりをせずに実行に移ってしまうのは危険なので、CPU-secondsに関して計算してみます。

`puppeteer`がある程度容量が必要なことを考慮し、512MBにあげる場合を考えます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209015/836ef0da-d456-15e0-57cf-6d664931ec50.png" style="zoom:50%;" />


CPU-secondsの無料枠は`200,000 GHz 秒`なので、 
`200,000 / 0.8 = 250,000(s) ≒ 4166.7(m) ≒ 69.4(h)`
使えることになります。

今回私がやろうとしているのは1日1回できれば良いので、問題なさそうです。

次に、注意すべきこととして、`Container storage`というものには無料枠がありません。
しかし、詳細は[デプロイ費用](https://cloud.google.com/functions/pricing?hl=ja#deployment_costs)に書いているのですが、非常に小額みたいなので、気にしないことにします。
（一円未満であれば一円が請求されるのでしょうか🤔?）

**(2020/12/03 追記)**
やはり一円が請求されていました。
これくらいなら全然問題ないですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209015/b78aa99a-c176-dab9-ac82-3004d8bb8504.png)


## やったこと

### プロジェクト作成

まずは、
https://console.firebase.google.com/u/0/?hl=ja
からプロジェクトを作成

Cloud Functions が使いたいだけなのでアナリティクスは不要

### セットアップ

私の環境ではfirebase周りの環境を作成してしまっているので、詳しくはこちらの公式をご覧ください。
[はじめに: 最初の関数の記述、テスト、デプロイ](https://firebase.google.com/docs/functions/get-started?hl=ja)

以下は私の環境で行ったことです。
firebase-tools等を既にインストールしている場合は同じようなことをするのではないかと思います。

Cloud Functions for FirebaseではNode 12が使えるみたいなので、Node12を準備します。

```bash
# nodenvを導入しているので、nodenvでインストール
nodenv install 12.19.0 # 2020/11/07 で最新のもの
mkdir work_space
cd work_space
nodenv local 12.19.0 # 2020/11/08で最新
```

functions について初期化

```
firebase init functions
```

しかし、 not found

```
nodenv: firebase: command not found

The `firebase' command exists in these Node versions:
  14.2.0
```

Firebase-toolsをインストール

```
npm install -g firebase-tools
```

再び init

```
firebase init functions
```

設定

```bash
? Please select an option: Use an existing project
? Select a default Firebase project for this directory: {作成したプロジェクトを選択}
? What language would you like to use to write Cloud Functions? TypeScript
? Do you want to use ESLint to catch probable bugs and enforce style? Yes
? Do you want to install dependencies with npm now? No #yarn使うので
```

ここまで行うと、以下のようなファイル構成になります

```
.
├── .firebaserc
├── .gitignore
├── .node-version
├── firebase.json
└── functions
    ├── .eslintrc.js
    ├── .gitignore
    ├── package.json
    ├── src
    │   └── index.ts
    └── tsconfig.json
```

prettier等も導入したいので、もう少し追加します。

```
cd functions
yarn add -D prettier eslint-config-prettier eslint-plugin-prettier
```

`.eslintrc.js`を修正

```js
extends: [
  "plugin:import/errors",
  "plugin:import/warnings",
  "plugin:import/typescript",
  "eslint:recommended", # 追加
  "plugin:@typescript-eslint/eslint-recommended", # 追加
  "plugin:@typescript-eslint/recommended", # 追加
  "plugin:prettier/recommended" # 追加
],
```

`.prettierrc`を作成

```bash
echo '{\n\t"printWidth": 120\n}' > .prettierrc
```

git設定

```
cd ..
git init
```

#### ホットリロード

Cloud Functions for Firebaseはローカルで実行することができます。
とても便利なのですが、デフォルトの状態ではtypescriptでホットリロードができないので、少し修正します。

package.json修正

```json
"scripts": {
  ...
  "watch": "tsc -w",
  "watch-serve": "firebase emulators:start --only functions",
  "watch-shell": "firebase functions:shell",
  ...
},
```

ターミナルで `yarn watch`を実行し、他のターミナルで`yarn watch-serve`とすると、修正がリアルタイムで反映されます。

一つのターミナルでやりたい場合
[Firebase Functions x TypeScript デバッグをHotReload](https://qiita.com/tokicat/items/d92254a2749946188950)

### 実装

いよいよ実装していきます。

まずはpuppeteerのインストール

```
yarn add puppeteer
yarn add -D @types/puppeteer
```

#### コードを書く

```typescript
import * as functions from "firebase-functions";
import * as puppeteer from "puppeteer";

const runtimeOpts: functions.RuntimeOptions = {
  timeoutSeconds: 300,
  memory: "512MB",
};

export const helloWorld = functions
  .region("asia-northeast1")
  .runWith(runtimeOpts)
  .https.onRequest(async (request, response) => {
    const isDebug = process.env.NODE_ENV !== "production";
    functions.logger.info("NODE_ENV: ", process.env.NODE_ENV);

    const browser = await puppeteer.launch({
      headless: isDebug ? false : true,
      args: [
        "--disable-gpu",
        "--disable-dev-shm-usage",
        "--disable-setuid-sandbox",
        "--no-first-run",
        "--no-sandbox",
        "--no-zygote",
        "--single-process", // <- this one doesn't works in Windows
      ],
    });
    const page = await browser.newPage();
    await page.goto("https://qiita.com/");

    const title = await page.title();
    await browser.close();
    functions.logger.info(title);
    response.send(title);
  });

```

#### コード解説

##### isDebug & argsの設定

参考：[FirebaseのCloud Functionでpuppeteerを動かす解説](http://kesin.hatenablog.com/entry/firebase_gcf_puppeteer)
参考：https://github.com/puppeteer/puppeteer/issues/3120#issuecomment-415553869

ローカル実行時はブラウザの動作を見れるようにします。

```typescript
const isDebug = process.env.NODE_ENV !== "production";
functions.logger.info("NODE_ENV: ", process.env.NODE_ENV);

const browser = await puppeteer.launch({
  headless: isDebug ? false : true,
  args: [
    "--disable-gpu",
    "--disable-dev-shm-usage",
    "--disable-setuid-sandbox",
    "--no-first-run",
    "--no-sandbox",
    "--no-zygote",
    "--single-process", // <- this one doesn't works in Windows
  ],
});
```

##### browser.close()

これを忘れるとChromiumが立ち上がり続けます。

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

#### ローカル実行

- ターミナル1：`yarn watch`

- ターミナル2：`yarn serve`

- ターミナル3：`curl {url}`
  - `yarn serve`で出てくる `http function initialized ({url})`の {url}
  - Qiitaと出てくれば成功です👏

### デプロイ

では最後にデプロイして、ちゃんと動くか確認しましょう。
※デプロイ時間の無料枠は`120min/day`です。

```
yarn deploy
```

表示されたURLにアクセスすると,Qiitaと表示されました👏👏
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209015/586cad38-2464-5699-8e52-6179c9c4de9f.png)

## おわりに
Cloud Functions for Firebaseでスクレイピングをすることができました。
スクレイピングの中身を変更すれば、自分がやりたいことができるはずです💪
[関数のスケジュール設定](https://firebase.google.com/docs/functions/schedule-functions?hl=ja)を参考にすれば、定期実行も可能です💪
## 参考
[Pub/Sub を使用して Cloud ファンクションをトリガーする](https://cloud.google.com/scheduler/docs/tut-pub-sub?hl=ja)
[関数のスケジュール設定](https://firebase.google.com/docs/functions/schedule-functions?hl=ja)
[Cloud Functions の作成](https://cloud.google.com/functions/docs/writing?hl=ja),[ローカルマシンからのデプロイ](https://cloud.google.com/functions/docs/deploying/filesystem?hl=ja)
[はじめに: 最初の関数の記述、テスト、デプロイ](https://firebase.google.com/docs/functions/get-started?hl=ja)
https://firebase.google.com/pricing?hl=ja
[デプロイ費用](https://cloud.google.com/functions/pricing?hl=ja#deployment_costs)
[はじめに: 最初の関数の記述、テスト、デプロイ](https://firebase.google.com/docs/functions/get-started?hl=ja)
[Firebase Functions x TypeScript デバッグをHotReload](https://qiita.com/tokicat/items/d92254a2749946188950)
[FirebaseのCloud Functionでpuppeteerを動かす解説](http://kesin.hatenablog.com/entry/firebase_gcf_puppeteer)