こんにちは。
AIチームのデータアナリストの柾屋です。
最近Google Apps Script(以下、GAS)を使って自動通知BOT(mail, slack)を作る機会が増えており、個人的に詰まったポイントをまとめたいと思います。
普段はpythonを使うことが多いのですが、GASがJavaScriptベースのため毎回基本的な部分で詰まってしまいます(汗)
そもそもGASとは?
既にご存じの方が多いかと思いますが、以下サービスの概要です。
・Google プロダクト全体でタスクと統合して自動化できるクラウドベースの JavaScript プラットフォーム。
・ボタンやユーザーアクション、時間ベースのスケジュールなどに応じて自動化のトリガーを簡単に作ることができる。
・Googleスプレッドシートでオリジナルの関数(外部APIを叩く関数など)を作り、組み込み関数と同じように使うことができる。
GASを活用することで、GCP上のデータ活用はもちろん、SlackやChatwork、Gmailなどを使って社内や社外のメンバーに通知するようなBOTを作ることも可能です。
最近は以下のようなBOTを作ることが多いです。
・スプレッドシートのデータコネクタでカスタムクエリを実施してデータ取得
・取得した内容をGASでslackやGmailで自動通知
それでは早速、詰まったポイントをまとめて紹介していきます。
for文
基本部分ですがpythonを使っていると、JavaScriptだとどうやるんだっけ?といつも迷ってしまうので、自分の復習も兼ねてまとめます。
JavaScriptの場合は”in”と”of”を使い分ける必要あり
pythonの場合だと、以下のように”in”を使えば回数を指定したい場合も、リスト内の要素を利用したい場合も対応が可能です。
#リストの要素数を回数として指定する場合
temp_list = ['a', 'b', 'c']
for i in range(len(temp_list)):
print(i)
---結果---
0
1
2
------
#リストの中身を使いたい場合
temp_list = ['a', 'b', 'c']
for t in temp_list:
print(t)
---結果---
a
b
c
------
#追記:indexとvalueの両方を使いたい場合
l = ['a', 'b', 'c']
for index, value in enumerate(l):
print(index, value)
---結果---
0 a
1 b
2 c
------
ただ、JavaScriptの場合では下記のように書く必要があるようでした。
・回数などの指定として利用したい場合は”in”
・リストの中身を使いたい場合は”of”
//リストの要素数を回数として指定する場合(pythonのリストの中身を使う場合と一緒)
var temp_list = ['a', 'b', 'c']
for (t in temp_list){
Logger.log(t)
}
---結果---
16:03:28 情報 0.0
16:03:28 情報 1.0
16:03:28 情報 2.0
------
//JavaScriptだとこっちの書き方の方が一般的?
var temp_list = ['a', 'b', 'c']
for (let i=0; i < temp_list.length; i++){
Logger.log(i)
}
---結果---
16:04:30 情報 0
16:04:30 情報 1
16:04:30 情報 2
------
//リストの中身を使いたい場合
var temp_list = ['a', 'b', 'c']
for (t of temp_list){
Logger.log(t)
}
---結果---
16:09:56 情報 a
16:09:56 情報 b
16:09:56 情報 c
------
//追記:indexとvalueを使いたい場合
var l = ['a', 'b', 'c']
for (let i=0; i < l.length; i++){
Logger.log(i)
Logger.log(l[i])
}
---結果---
10:49:56 情報 0.0
10:49:56 情報 a
10:49:56 情報 1.0
10:49:56 情報 b
10:49:56 情報 2.0
10:49:56 情報 c
------
GASには実行時間に制限がある
GASでは最短で1分置きにプログラムを実行する”トリガー(自動実行のようなイメージ)”を設定することができます。
ただ、GASでは実行の制限とトリガーの制限があります。
無料アカウントだと特に1日に90分という制限がついてしまいます。
そのため、無料アカウント(90分/dayの実行時間上限)で1分ごとにトリガー(1440回/day)を設定したい場合は、1回あたりの実行時間を3.75秒に収める必要があり、1回あたりの実行時間をかなり短縮しなければいけません。
スプレッドシートの”ARRAYFORMULA関数”はシート全体に適用される
スプレッドシートを活用する場合、”関数漏れ※1”がないようにARRAYFORMULA関数を使うことが多いのですが、for文の際に詰まるポイントとなってしまいました。
※1:「関数が入っていない行にデータが入ってしまい、予期せぬ挙動につながること」という意味合いで記載しています。
スプレッドシートはデフォルトで1,000行表示されます。関数を1,000行全部に入れておいたとしても、1,001行目以降までデータが増えてしまった場合、関数が入っていない行が存在する状態になってしまいます。
データは3行までしか入っていないのですが、データのある”最終行までforループを回す”と1,020行目まで実施されてしまうのです。
このせいで数秒で終わるはずの処理が何十秒もかかってしまっていることに気づきました。
//スプレッドシートのシートから最終行を取得し、for ループを回す
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet_mail = ss.getSheetByName("send_list");
var lastRow = sheet_mail.getLastRow();
//最終行までforループを実行
for (let i = 0; i < lastRow - 1 ; i++){
Logger.log(i)
}
---結果---
16:17:31 情報 0.0
16:17:31 情報 1.0
16:17:31 情報 2.0
16:17:31 情報 3.0
〜省略〜
16:17:31 情報 1018.0
16:17:31 情報 1019.0
16:17:31 情報 1020.0
------
色々と検証していると、どうもARRAYFORMULA関数がいけないらしいということに気づきました。”空白の場合でも、関数データが最終行まで入っている”という判定になっているようです。
ただ、”関数漏れ”の観点からARRAYFORMULA関数は引き続き利用したかったため、for ループの最終行判定を変更する方針にしました。
//現在の最終行の判定
var lastRow = sheet_mail.getLastRow();
↓↓↓
//フィルターを使った最終行の判定
var lastRow2 = sheet_mail.getRange("a1:a").getValues().filter(String).length;
その結果、意図したデータ範囲内でforループを回すことができました。
//スプレッドシートのシートから最終行を取得し、for ループを回す
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet_mail = ss.getSheetByName("send_list");
//var lastRow = sheet_mail.getLastRow();
var lastRow2 = sheet_mail.getRange("a1:a").getValues().filter(String).length;
//最終行までforループを実行
for (let i = 0; i < lastRow2 - 1 ; i++){
Logger.log(i)
}
---結果---
16:43:13 情報 0.0
16:43:13 情報 1.0
16:43:13 情報 2.0
------
GASには時間判定をつけた方が良い
大抵の活用の場合は営業時間内でGASを利用しており、深夜帯などは実行しなくても良いというケースが大半です。
しかし、GASの場合はトリガーのスケジューリング時に”この時間だけ実行する”などの期間を指定することができません。
ただ、夜間帯も実行してしまうと冒頭で説明した”GASのトリガー制限”に引っかかってしまう可能性も出てきてしまいます。
そんな場合にオススメなのが、時間判定の関数をプラスすることです。
function judge_hour() {
const time = new Date();
//9~17の数字範囲を判定する正規表現
const regWorkHours = /^(1[0-7]|8|9|7|)$/;
if (regWorkHours.test(time.getHours())) {
Logger.log('営業時間内')
//実行したい関数を以下に記載
send_email()
}
}
これにより通常3-6秒かかっている処理を営業時間外の場合は、0.3-0.6秒に抑えることができます。1日のGASの実行時間をかなり節約することにつながります。
▼営業時間内の処理時間
▼営業時間外の処理時間
データコネクトをGASで更新する場合はtry…catch構文がマスト!
スプレッドシートの機能の1つにデータコネクタというものがあります。
データコネクタを活用することでBigQueryに接続し、データをスプレッドシート上に落としてくることができます。
このデータコネクタは「更新スケジュール」を設定することができるのですが、最短で1h間隔でしかできません。
1hよりももっと短い場合でデータを更新したい場合は、GASを活用してこのデータコネクタを更新させることができます。
try{
console.log("スタート")
var date = new Date();
var spreadsheet = SpreadsheetApp.getActive();
// データの更新
spreadsheet.setActiveSheet(spreadsheet.getSheetByName("抽出データ"), true);
SpreadsheetApp.enableAllDataSourcesExecution();
spreadsheet.getRange("A2").getDataSourceTables()[0].forceRefreshData();
//データ更新が成功した場合の次の処理を記載
Logger.log('データ更新完了')
send_conv_lists()
}catch(e){
Logger.log('更新エラー' + e.message)
}
ただ、GASを活用してデータコネクタを実行した際に、該当データがない場合など(空白)は実行エラー(失敗しました)が発生してしまいます。
そのため、データコネクタをGASで更新させる場合にはtry…catchをマストで入れておく必要があります。
ネストされたJSON型でデータを取り出したい際に[object Object]と表示される
下記のJSONの配列からkeyとvalueの組み合わせを取得したい場合を想定します。
parameters =
["{\"no201\":\"2\"}",
"{\"no211\":\"1\"}",
"{\"no212\":\"1\"}",
"{\"no401_pn\":{\"person\":{\"name\":\"山田太郎\"}}}",
"{\"no405\":\"はい\"}",
"{\"no406\":\"テストテストテスト以上です\"}"]
以下のように実行をしてしまうと、”no401_pn”の際にネストされたJSONに対応ができずに[object Object]と表示されてしまいます。
var list_num = parameters.length
for (let i = 0; i < list_num; i++){
var obj = JSON.parse(parameters[i]);
var key_lists = Object.keys(obj)
for (let key of key_lists){
answer += "\n" + key + " : " + obj[key]
}
}
そこで、ネストかどうか判定する手段として、以下の修正を行いました。
1.“typeof”で”object”かどうかを判定
2.“object”であった場合は”JSON.stringify()”で表示させる
var list_num = parameters.length
for (let i = 0; i < list_num; i++){
var obj = JSON.parse(parameters[i]);
var key_lists = Object.keys(obj)
for (let key of key_lists){
if (typeof obj[key] == 'object'){
answer += "\n" + key + " : " + JSON.stringify(obj[key])
}else{
answer += "\n" + key + " : " + obj[key]
}
}
}
これで無事にネストされたJSONでも表示させることができました。
JavaScriptでarray型をdict型にするのってどうやる?
データを扱う場合にリスト(array)型ではなく、dict(辞書)型の方が便利な機会が多いです。
pythonを使っていると、”to_dict”などの関数があるのですが、JavaScriptだとどうやるんだっけ?といつも忘れてしまいます(汗)
結論、ベストプラクティスが分からず、毎回forループを使ってdict型にしています(笑)
var temp_list = [["NO", "質問名"], ["no201", "質問1"], ["no211", "質問2"], ["no212", "質問3"]]
var dict = {}
for (i in temp_list){
dict[temp_list[i][0]] = String(temp_list[i][1])
}
---結果---
{NO=質問名, no212=質問3, no211=質問2, no201=質問1}
------
もし詳しい方いらっしゃいましたら是非教えてください!
終わりに
4月から本格的に実務でのコードを使った作業をすることが多く、毎回詰まりながらも先人の知恵を借りてなんとか形にできることが増えてきました。
いかにシンプルに考え、シンプルな実装をするかということを目標に今後も励んでいきたいと思います!