みどりのさるのエンジニア

Google Cloud でWebスクレイピングして結果を通知するBotを作成

2022年09月27日

会社でG.I.Gプログラムに参加する機会を得たので、勉強がてら定期的にWebスクレイピングして結果をSlackに通知するBotをGoogle Cloudで構築してみました。

全体構成

Cloud Functionsでスクレイピングを実行してSlackに結果を通知するHTTP関数を用意して、Cloud Scheduler で定期的にCloud FunctionsにHTTPリクエストを送信するジョブを実行することでBotを構築します。

構成図

スクレイピングを実装

まずはローカルでスクレイピングを実行するコードを実装します。
HTMLの取得と解析にはaxioscheerioを利用します。

const cheerio = require('cheerio');
const axios = require('axios');

const scrape = async () => {
  const res = await axios.get(
    'https://xxxx/xxxx'
  );
  const $ = cheerio.load(res.data);

  // (省略)

  return 'スクレピングの結果';
};

const main = async () => {
  const result = await scrape();
  console.log(result);
}

main();

Slackへの通知を実装

続いてスクレイピング結果をSlackへ通知する部分を実装します。
Slackへのメッセージ送信にはIncoming WebhookでWebhook URLを発行してPOSTリクエストを送信します。

const notifyToSlack = (message) => {
  return axios.post(
    "https://hooks.slack.com/services/xxxxx/xxxxxx/xxxxx",
    {
      text: message,
    }
  );
}

const main = async () => {
  const result = await scrape();
  notifyToSlack(result);
}

main();

Google Cloud CLIのインストール

Google Cloudの構築を始める前にターミナルで操作するためにCLIツールをインストールしておきます。
ここではHomebrewでインストールをしていますが、インストール方法は何でも大丈夫です。

$ brew install --cask google-cloud-sdk

gcloud コマンドでGoogle Cloudを操作するために認証が必要なためログインをしておきます。

$ gcloud auth login

プロジェクトの作成

Google Cloud サービスを構築するためのプロジェクトをGoogle Cloudで作成しておきます。

Cloud FunctionsのHTTPハンドラ関数を実装

Google Cloudでアプリケーションを構築してきます。
Cloud Functionsの詳細なガイドについては公式のガイドを参照してください。

Cloud Functionsで関数を実行するには、Functions Framework for Node.jsでHTTPリクエストのハンドラ関数を登録する必要があります。

最初に@google-cloud/functions-frameworkをインストールします。

$ yarn add @google-cloud/functions-framework

続いてスクレイピングと通知の処理をHTTPハンドラ関数として登録します。

const functions = require("@google-cloud/functions-framework");
const cheerio = require("cheerio");

functions.http("checkReservation", async (_req, res) => {
  const result = await scrape();
  await notifyToSlack(result);
  res.send("ok");
});

// (省略)

デプロイ前にローカルでテスト用のサーバーを起動して関数が実行されるか確認してみましょう。

{
  "scripts": {
    "start": "functions-framework --target=checkReservation"
  }
}
$ yarn start
Serving function...
Function: checkReservation
Signature type: http
URL: http://localhost:8080/

HTTPリクエストを送信してSlackへメッセージが送信されるのを確認できれば成功です。

# 別タブ等で実行
$ curl http://localhost:8080/checkReservation
ok

Cloud Functionsへデプロイ

詳細は公式ガイドのCloud Functions の関数をデプロイするで確認できます。

関数をデプロイするにはgcloud functions deployコマンドを実行します。

$ gcloud functions deploy YOUR_FUNCTION_NAME \
[--gen2] \
--region=YOUR_REGION \
--runtime=YOUR_RUNTIME \
--source=YOUR_SOURCE_LOCATION \
--entry-point=YOUR_CODE_ENTRYPOINT \
TRIGGER_FLAGS

今回はHTTPリクエストでトリガーしたいので次のようにデプロイコマンドを作成しました。

  • --gen2は第2世代のCloud Functionsにデプロイすることを指定しています。
  • --trigger-httpはHTTPリクエストで関数をトリガーするために指定します。
  • --allow-unauthenticatedは認証なしで関数を呼び出し可能にするために指定します。Cloud Functionsはデフォルトで関数の呼び出しに認証が必要になっているので、このフラグを指定しています。
$ gcloud functions deploy reservation-notify \
  --gen2 
  --region=asia-northeast1 \
  --runtime=nodejs16 \
  --source=. \
  --entry-point=checkReservation \
  --trigger-http \
  --allow-unauthenticated \
  --project=reservation-notify

このコマンドをnpmスクリプトとして登録してデプロイを実行します。

{
  "scripts": {
    "deploy": "gcloud functions deploy reservation-notify --gen2 --region=asia-northeast1 --runtime=nodejs16 --source=. --entry-point=checkReservation --trigger-http --allow-unauthenticated --project=reservation-notify"
  }
}
$ yarn deploy
...
uri: https://reservation-vacancy-notify-xxxxxx.run.app

デプロイが成功したら発行されたURLが確認できるので、HTTPリクエストを送信してみます。
Slackにメッセージが送信されたら成功です。

$ curl https://reservation-vacancy-notify-xxxxxx.run.app
ok

Cloud Schedulerで定期実行

gcloud scheduler jobs create httpコマンドでHTTPリクエストのジョブを作成できます。
定期実行のスケジュールはcrontabの書式で設定し、タイムゾーンを指定する場合はList of tz database time zones
にあるタイムゾーンを指定できます。

今回は先程のCloud Functionsへ5分おきにHTTPリクエストを送信するジョブを作成してみます。

5分ほど待ってSlackにメッセージが飛んでくるのを確認できたら成功です。

$ gcloud scheduler jobs create http check-reservation \
  --schedule="*/5 * * * *" \
  --time-zone=Asia/Tokyo \
  --location=asia-northeast1 \
  --uri="https://reservation-vacancy-notify-rkqokacy7q-an.a.run.app" \
  --http-method=GET \
  --project="reservation-notify" \