Google Apps Script で日次のバッチ処理を手軽に作ろう

## まえがき
この記事は、 [GAOGAO Advent Calendar 2022 ことしもGAOGAOまつりです](https://qiita.com/advent-calendar/2022/gaogao) の 3日目の記事として公開されています。

こんにちは。 [GAOGAO](https://gaogao.asia/) にてスタートアップスタジオの採用・人事担当をしております [@mass-min](https://twitter.com/masumi_sugae) と申します。 GAOGAO では秀吉と呼ばれています。どうぞよろしくお願いいたします。

## 結論
下記のような作業、Google Apps Script で自動化できます！
- 特定のAPIを毎日叩き情報を取得する
- Slack に毎日投稿する

## 細かい作業が面倒くさい
わたくし、毎度繰り返し行う細かい作業が苦手です。毎朝する行動や、毎月絶対に発生する細かなタスクなどが本当にできません。三日坊主もいいところ。いや、3日も続いてたらそれはめっちゃ続いてます。褒め称えたい。

採用・人事業務の中でも細かい作業は多々発生します。毎週スカウトDMの送信数と返信数の関係を記録したり、月末月初は請求書まとめたり、いろいろです。手動で作業をしていると本当に面倒ですし、ミスも起きます。漏れも起きますしね。そろそろそういうのはナシにしたい。

ということでなんとかして解決したいわけですが、私も元々はエンジニアを自称して仕事をしてきた身です。採用・人事担当とはいえ、面倒事は自分で自動化して解決しようではありませんか。

今の世の中は本当に便利になったもので、ちょっとコードを書くだけで自動化が簡単にできちゃいます。今回はタイトルの通り、[Google Apps Script](https://developers.google.com/apps-script)(以下 GAS)を使って面倒事を自動化してみましょう。目指すはウォレスとグルミットです。

参考: ウォレスとグルミットの自動化されたモーニングルーティーン。早くこれになりたい

https://www.youtube.com/watch?v=UtRhl-2ChFU

## なぜにGAS?
いろいろツールがある中で、なぜ GAS を選択するのでしょうか？私は下記の2点を大きなメリットと捉え選択しています。
- リモート環境を自前で用意することなく、任意のタイミングでのバッチ処理が実行できる
    - もちろん任意のタイミングでの手動実行もできる
- Google スプレッドシートとの連携がライブラリの導入なしでとても簡単にできる

バッチ処理自体は AWS Lambda 等でもできますが、小さなツールであればわざわざ AWS アカウントを作って Lambda の環境を用意して...とするまでもないのが現実です。その点 GAS は環境を用意する手間がほとんどかからないため、コスパがとてもよいです。

## 特定のAPIを毎日叩き情報を取得する
それでは実際に面倒事を自動化する例を見てみましょう。
何か API を叩いて、その結果をスプレッドシートに保存する、ということを考えます。

[{JSON} Placeholder](https://jsonplaceholder.typicode.com/) というサービスにて架空の TODO リストの REST API が公開されているので、これを毎日叩き「一番時間がかかっている TODO」をスプレッドシートに格納します。

※ ただし、「一番時間がかかっている TODO」は下記の定義とします。
- https://jsonplaceholder.typicode.com/todos で、下記の条件を満たすもの
    - userId が 1
    - completed が false
    - 配列の1つ目の要素(idでのソートはサーバー側で既に行われているものとする)

### GAS コードを書く

スプレッドシートで `Google Apps Script サンプル` というファイルを作成し、 `APIデータの取得` というシート名でシートを作ります。
その後スプレッドシートのメニューバー Extensions > Apps Script から、Google Apps Script のエディタ画面へ行き、下記の2つのファイルを作成しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/fa224d64-9c03-2acb-6f2a-f82e86021795.png)

```javascript:FetchMostTimeConsumingTodo.gs
function makeQueryString(obj) {
  return Object.keys(obj).map(function(key) {
    return `${key}=${obj[key]}`
  }).join('&')
}

async function fetchMostTimeConsumingTodo() {
  return new Promise((resolve, reject) => {
    let apiUrl = 'https://jsonplaceholder.typicode.com/todos'
    const paramsObj = {userId: 1, completed: false}
    const queryString = makeQueryString(paramsObj)
    if (queryString) {
      apiUrl = `${apiUrl}?${queryString}`
    }

    const response = UrlFetchApp.fetch(apiUrl)
    const data = JSON.parse(response)
    const mostTimeConsumingTodo = data[0]
    return resolve(mostTimeConsumingTodo)
  })
}

function insertTodoToSpreadSheet(todo) {
    const sheetName = 'APIデータの取得'
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName)
    sheet.appendRow(Object.values(todo))
}

async function run() {
  const mostTimeConsumingTodo = await fetchMostTimeConsumingTodo()
  insertTodoToSpreadSheet(mostTimeConsumingTodo)
}
```
スプレッドシートに行を追加するコードは `insertTodoToSpreadSheet()` にて実装していますが、とてもシンプルですね！こんなに便利なライブラリが最初から用意されている GAS はとても使いやすいです。

ではこれを実行してみます。 run ボタンをクリック。初回実行時は権限付与の可否を問われるので、許可してください。
![Screen Shot 2022-12-03 at 12.56.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/41c850b2-a198-df23-55dc-7f4f77d20aa5.png)

すると...どうでしょう！関数が実行され、スプレッドシートに「一番時間がかかっている TODO」が追加されました！:clap:
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/b30037b8-b0e5-c241-977d-5f61d4f57306.png" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/0628cb11-ea7d-9726-cb86-a83f3868af11.png" width="400">

これでコードの内容はOKそうですね。
例えば自社サービスから生えているAPIをコールして毎日データを更新しダッシュボードを作る、なんてときには、APIコールのURLや認証部分とレスポンス整形の部分さえ改良してあげれば上記のコードがほぼそのまま使えます。仮にAPIがなくても、APIコール部分をスクレイピングのロジックに変えれば情報は取れますね。

### トリガー実行を設定する
次はいわゆるバッチ処理の設定です。crontab の GUI 版みたいなものが GAS 上で設定できます。
Google Apps Script の画面左側のメニューから Triggers を選択。画面右下の Add Triggers よりトリガーを設定します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/c9ddc1bd-44cb-c706-f3bf-e7b700df3084.png" width="300">
![Screen Shot 2022-12-03 at 13.23.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/934f170e-4666-87d3-24c2-33974d935187.png)

トリガー設定画面が起動するので、実行したい関数(今回の場合は `run`)とイベントソースを選択します。イベントソースは色々ありますが、毎日、毎時、毎分など時間ベースでの実行をしたい場合は `Time-driven` を選択してください。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/673b511b-3dee-0a76-2abf-87ec976b6d94.png" width="400">

Time-driven を選択すると、その下に更に項目が増えます。
- type of time based trigger ... 下記より実行種別を選択
    - 特定日時 ... 自分のいるタイムゾーンにて日時選択
    - 分単位 ... 1, 5, 10, 15, 30分単位から選択
    - 時間単位 ... 1, 2, 4, 6, 8, 12時間単位から選択
    - 日単位 ... 毎日何時から何時の間に実行するか選択
    - 週単位 ... 毎週何曜日の何時から何時の間に実行するか選択
    - 月単位 ... 毎月何日の何時から何時の間に実行するか選択

上記を見ると分かるように、「毎週月、水、金曜日の12時-13時の間に実行する」というようなトリガーは1つのトリガーでは実現できません。月曜日用、水曜日用、金曜日用でそれぞれトリガーを分けるか、あるいは毎日実行にして関数内で特定の曜日は内部処理をスキップする実装をするかしてください。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/83e9914e-f7c0-e87d-ae39-d34e3a5a1db1.png" width="300">

実行種別を選択したら、Save を押してトリガー設定を完了させます。ここでは試しに毎分関数を実行するよう設定してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/b3b73394-1c0d-b896-d054-278190a3f0f1.png)

しばらく待って左側のメニューから Execution を選択し実行ログを見てみると、`Type` が `Time-Driven` になっている実行ログが追加されていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/2de45108-7a7e-5135-fb4b-92991227f4df.png)

スプレッドシートを見ても、順調に行が追加されていることが分かりますね！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/76a1bf2b-0ad9-32d8-3f39-500b54dc04f0.png" width="400">

ということで、もうこれでわざわざ自分で1日1回APIを叩いたりページ内の情報をコピーしたりしてスプレッドシートにまとめる必要はなくなったわけです。GAS がよしなにやってくれます、最高！

### Slack 自動投稿
例として、先程スプレッドシートにまとめた情報を Slack に投稿することを考えます。
Slack の API キーはもう既に取得してあるものとして、そのキーを使って GAS から Slack にメッセージを送ります。

[SlackApp](https://github.com/soundTricker/SlackApp) というライブラリが有志によって公開されています。これを使わせていただきましょう。

GAS Editor 画面より Add a library を選択。これは npm や yarn の GAS 版のようなものになります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/78155a65-757c-48e2-b84e-684f0a84710c.png" width="300">

SlackApp を追加するため、下記 ID を入力し Look up。
`1on93YOYfSmV92R5q59NpKmsyWIQD8qnoLYk-gkQBI92C58SPyA2x1-bq`

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/25ffe817-1bc7-e11b-5178-a42fbf45145b.png" width="300">

SlackApp が出てくるので、最新バージョンが選ばれていることを確認して Add。これでコード内で SlackApp が使えるようになります。

先程書いた `FetchMostTimeConsumingTodo.gs` に `sendSlackMessage()` を実装し、 `run()` の中で実行開始時と終了時にそれぞれ Slack メッセージを送信するよう修正します。

```javascript:FetchMostTimeConsumingTodo.gs

function sendSlackMessage(message) {
  const slackApiKey = PropertiesService.getScriptProperties().getProperty('SLACK_API_KEY')
  const slackApp = SlackApp.create(slackApiKey)
  const channelId = "#google-app-script-sample"
  slackApp.postMessage(channelId, message)
}

async function run() {
  sendSlackMessage('データ取得開始')
  const mostTimeConsumingTodo = await fetchMostTimeConsumingTodo()
  insertTodoToSpreadSheet(mostTimeConsumingTodo)
  sendSlackMessage('データ取得終了')
}
```

APIキーはコード管理したくない値なので、コード上にベタ書きせずスクリプト固有のプロパティとして設定します。コード内にもあるように、 Slack API キーは `SLACK_API_KEY` という名前でプロパティを設定します。(`PropertiesService.getScriptProperties().getProperty(<プロパティ名>)` でプロパティの値が取得できます)
GAS の Property Setting ページ下部から `Add script property` を選択し、APIキーのプロパティを作成します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/d82243e3-7a1f-70bc-9ef2-fdca5da25ad4.png" width="300">

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/e13d94f4-0e9b-3aec-0dea-ab46e532b960.png)

![Screen Shot 2022-12-03 at 14.16.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/fe56482d-838c-6785-4a9e-3ec420bb8406.png)

プロパティ作成後、`run()` を実行すると...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155576/b733ae2c-c319-6828-d239-e0ebc84dd5b0.png)

ちゃんと通知が届きましたね！
今回はベタ打ちのメッセージを毎回送信するようにしていますが、取得したデータの内容をメッセージとして整形し引数として渡してあげれば Slack メッセージから簡易的に情報を得ることも可能ですね。

# まとめ
下記のような作業、Google Apps Script で自動化できます。
- 特定のAPIを毎日叩き情報を取得する
- Slack に毎日投稿する

今回の記事で1日1回のデータ取得 -> Slackへメッセージ投稿 までが自動化できたので、これで本来やりたかった仕事にフォーカスできます。
みなさんもお手持ちの細かい作業、どんどん自動化しちゃいましょう！目指せウォレスとグルミット！

参考: ウォレスとグルミットの自動化されたモーニングルーティーン。早くこれになりたい(再)

https://www.youtube.com/watch?v=UtRhl-2ChFU