Cloud Functions with Puppeteer + Google Apps Script でスクレイピングサーバーをサクッと作る

2018/08/16 に Google Cloud Functions が Puppeteer をサポートしました :tada:
[Introducing headless Chrome support in Cloud Functions and App Engine](https://cloud.google.com/blog/products/gcp/introducing-headless-chrome-support-in-cloud-functions-and-app-engine)

Puppeteer が使えるようになったので、ヘッドレス Chrome を利用したスクレイピングが Cloud Functions でおこなえるようになりました。 Node v8 にも対応したので async/await も使えますよ！

しかし Cloud Functions の単体利用は使い勝手がちょっと悪いです。
それを補うために Google Apps Script（GAS）と併用します。
GAS を利用することでスプレッドシートに書き込んでデータの保存場所としたり、メールを送ったりなどが簡単に行なえます。

この2つを使う最大のメリットは、**環境構築が不要で、Web ブラウザだけでスクレイピングサーバーが作れるところです** :v:

本記事では例として、Qiita の人気記事を取得してスプレッドシートに書き出すというのを実際に作って解説していきます。

流れとしては次のようになります。

1. （GAS） Cloud Functions を HTTP でキック
2. （Cloud Functions） Puppeteer を利用し Web サイト をスクレイピング
3. （Cloud Functions） スクレイピングの結果を JSON で返却
4. （GAS） JSON を整形してスプレッドシートに格納

![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/2f83dd99-57f2-e7f4-458c-3c5ffc5ad932.png)



では早速作っていきましょう！

## スプレッドシートを作成
スプレッドシートを作成し、GAS のスクリプトエディタを開きます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/12352016-fc2b-7f70-d4f9-4b8e17bb31d1.png)

## GCP プロジェクトに移動
プロジェクト名を設定し、 Cloud Platform プロジェクトを開きます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/ccaf3445-72ef-b33d-297f-44a4925ecae1.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/482777da-0a01-3d83-65c5-0771f749a13d.png)

GAS に紐付いた GCP プロジェクトの画面が表示されます
![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/237c970a-6fac-4f29-8731-02101d32c9f1.png)


## Cloud Functions の画面に移動
左上のメニューから Cloud Functions の画面を開きます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/a4fb15ef-91b6-8a5f-71d2-6b4b0107e334.png)


### Billing 設定
Cloud Functions を使うには課金設定が必要です。
無料枠を超える分を使うとお金がかかってくるので注意しましょう。
※ ちょっとしたスクレイピングならかかりません。[^1]
[^1]: 200,000 GHz-seconds of compute time がまっさきに無料枠使うと思うので 200,000 / 1.4 / 60 / 60 = 40時間/月 ？？

![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/60ab132d-4e65-144d-50f8-2945813d3fcc.png)

筆者は課金設定が済んでいるので、Enable を押すだけで終わりですが、はじめての人は請求アカウントの作成が必要です。
請求アカウントを作ったら、 Billing Alert を設定しましょう。
[Billing Alertを設定して、クラウド死を防ごう！ - Qiita](https://qiita.com/sinmetal/items/0261667e6fa02355d0fa)


## Cloud Functions を作成
Enable API をクリックし、Cloud Functions を作成しましょう
![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/8dd5cba5-aa81-2b8d-6936-85cce80af830.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/4129eec9-d2ed-7ff9-d091-79b968eb79c5.png)

### Qiita の人気記事を取得する Functions を作成

次のように入力します。

|種類|設定値|
|:--|:--|
|Name|GetTrends|
|Memory allocated|1GB|
|Trigger|HTTP|
|Runtime|Node.js 8(Beta)|

![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/beeb82ff-aaad-0442-c787-59f2e82ebe6f.png)


つづいて、次の値を入力します。

|種類|設定値|
|:--|:--|
|Functions to Execute|getTrends|
|index.js|下のコード参照|
|package.json|下のコード参照|

`index.js` に次のコードを入力します。

```js:index.js
const puppeteer = require('puppeteer');
let page;

async function getBrowserPage() {
  // Launch headless Chrome. Turn off sandbox so Chrome can run under root.
  const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
  return browser.newPage();
}

exports.getTrends = async (req, res) => {

  if (!page) {
    page = await getBrowserPage();
  }

  await page.goto('https://qiita.com');

  const result = await page.evaluate(getTrendsJson);
  
  res.set('Content-Type', 'application/json');
  res.send(result);
};

function getTrendsJson() {
  return [...document.querySelectorAll('.tr-Item_body')].map(article => {
    const a = article.querySelector('a');
    const href = a.href;
    const title = a.textContent;

    const author = article.querySelector('.tr-Item_author').textContent;
    const time = article.querySelector('time').textContent;
    const like = article.querySelector('.tr-Item_likeCount').textContent;
    console.log(href, title, author, time, like);  

    return {href, title, author, time, like};
  });
}
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/04ebb2ee-5fbf-3a86-87d0-1f0565b9eb3d.png)


`package.json` に次のコードを入力します。
※ puppeteer を追加しています

```json:package.json
{
  "name": "sample-http",
  "version": "0.0.1",
  "dependencies": {
    "puppeteer": "^1.6.2"
  }
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/519574b4-b1fd-4831-7ecb-34b2c2cb074e.png)

最後に `Create` を押して作成完了です。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/e52f4469-79cb-7914-7b78-f77d9b4bc63e.png)


## Cloud Functions の動作確認
作成が終わったら緑のチェックマークが付きます。数分かかるので待ちましょう。
チェックマークがついたら Function 名をクリックします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/d8a85bb8-6f57-585d-c1ad-e4637123637a.png)

`Trigger` タブを選択し、URLをクリックします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/8b9423b6-85d1-d7f5-35b6-5bed798d39f7.png)

ヘッドレス Chrome の起動が遅いのか `20秒程度` かかります。
※お手軽ですが、ガッツリ動かすのには向いていないかも。

スクレイピングが行われて JSON が返却されていることを確認しましょう。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/31b1e458-35aa-7c97-0a3c-7cde00f325ee.png)


## GAS を作成
つづいて GAS を作成します。
先ほど作成した Cloud Functions を HTTP でキックしてスプレッドシートに書き出すコードを入力します。
※ URL は適宜自分で作成したものに書き換えてください

```js:コード.gs
var URL = 'https://us-central1-project-id-6338972891167330241.cloudfunctions.net/GetTrends';

function writeTrends() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var response = UrlFetchApp.fetch(URL).getContentText();
  var articles = JSON.parse(response);
  var values = articles.map(function (article) {
    return Object.keys(article).map(function (key) {
      return article[key];
    });
  });

  var range = sheet.getRange(1, 1, values.length, values[0].length);
  range.setValues(values);  
}
```

入力したら実行して見ましょう。

※途中で次のような画面が出てきたら、詳細を押したあとに出てくるいリンクから承認ができます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/20b2a849-8967-3e70-53a6-1cec0e7f4f97.png)


![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/1791935e-f41c-2e93-22f1-aed8a8c3f8a1.png)

実行が完了したらスプレッドシートを開き、スクレイピング結果が格納されていることを確認しましょう。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/35595/022eb1f7-6404-d3a4-a716-c01a2b3d143d.png)


## 有効活用への道
GAS は時間単位などで定期実行すること可能です。
また、メールの送信、Googleカレンダーへの登録なども簡単に行なえます。
外部の API も叩けるので Slack への通知なんかも出来ちゃいます。

ぜひスクレイピングを有効活用してみてください。

例として、筆者は RSS が提供されていない Web サイトを Puppeteer で定期巡回して、新しい投稿があったら（DOMに変更があったら）Slackに通知するようにしてたりします。
※ 前回取得した DOM をスプレッドシートに退避しておいて最新の DOM と比較しています。


## あとがき
スクレイピングは用法・用量を守って正しくお使いください