Google Apps Scriptでトリガーを活用しよう【GAS】
Google Apps Scriptの中で地味ながら、その活用の幅が実に広いものとして、スクリプトトリガーがあります。いわゆる時限式で作動させるための仕組みなのですが、スクリプトトリガーは、スクリプトエディタの画面から入り、手動で登録するのが通常のフローです。しかし、「スクリプト内でテンポラリで時間トリガーを設置したい」であったり「トリガーの設置し直し」など、スクリプトエディタにいちいち入らず設定したいシーンがボチボチあります。そういった場合には、スクリプトからトリガーの設置や削除が出来ると便利です。二重に登録してしまったりすると、二回発動したり、片方しか発動しなかったりするので、慎重に設置をしましょう。
これらトリガーは大きく分けて4種類あり、1.時限作動式 2.開いた時 3.編集時 4.フォーム送信時を設置することが可能です。主に使うのは時限作動式と、フォーム送信時の2つになります。但し、このスクリプトトリガーはちょっと癖があるので、そこだけは注意しとかなければなりません。
目次
使用するクラス、メソッド
トリガーの設置
旧式トリガーの廃止
Google Apps Script初期の頃からあったトリガーメソッドが現在は2種類のメソッドに大別されており、古いgetScriptTriggersメソッドは廃止されました(2015年頃にはもうDeprecatedでした)
大別されて新設されてるのが
- getProjectTriggers(プロジェクト単位でトリガーを管理)
- getUserTriggers(ユーザ単位のトリガーを管理)
となっていますので、古いトリガーメソッドで書いていたスクリプトは上記のメソッドに置き換えましょう。基本的な使い方は一緒です。本エントリーでは、getProjectTriggersで統一しています。
スプレッドシート編
スプレッドシートでは、時間ベーストリガーの他にも、スプレッドシート特有のトリガー(シンプルトリガーと呼びます)を設置する事が出来ます。使用することの出来る特有のシンプルトリガーは、「onChange」、「onEdit」、「onFormSubmit」、「onOpen」、「onSelectionChange」の合計5つとなります。それぞれ、「変更時」「編集時」「フォーム送信時」「起動時」の4つに該当します。以下に各トリガー設置のスクリプトを記載します。
注意点として、これらシンプルトリガー内では、「openById」が使えません。使うと「SpreadsheetApp.openById を呼び出す権限がありません。」とエラーとなります。他にもUrlfetchAppなども使えません。これはこれらシンプルトリガーでは認証を要するメソッドが使えない為です。
どうしても使いたい場合は数秒後に発火する通常のトリガーを設置して擬似的に動かすしかありません。
※ちなみにonEditと書けば動作するこれらシンプルトリガーですが、通常通りトリガー設置画面からイベントの種類で編集時や起動時でonEditではない関数指定をすることでも動作します。
onChange(変更時)
変更時とは、構造変更や内容変更が発生した時に発動するトリガーです。VBAで言う所のAfterUpdateイベントと言えます。
function createSpreadsheetTrigger() { var onChangeTrigger = ScriptApp.newTrigger("実行する関数名") .forSpreadsheet(spreadsheetのIDなど) .onChange() .create();
onEdit(編集時)
編集時とは、編集時に毎回発動するトリガーです。VBAで言う所のBeforeUpdateイベントと言えます
var onChangeTrigger = ScriptApp.newTrigger("実行する関数名") .forSpreadsheet(spreadsheetのIDなど) .onEdit() .create();
onOpen(起動時)
起動時とは、そのスプレッドシートが開かれた時に発動するトリガーである。通常はメニューの登録などでよく使われている。
var onChangeTrigger = ScriptApp.newTrigger("実行する関数名") .forSpreadsheet(spreadsheetのIDなど) .onOpen() .create();
onSelectionChange(セル変更時)
2020年4月に新規に追加されたトリガーで、「選択してるセルが変更された時」に発火するトリガーです。以下のエントリーで詳しく紹介していますので、よかったらどうぞ。ただし、onEdit以上に使い方がちょっとむずかしい。シート切り替えただけでも発火してしまう。
フォーム編
殆ど、スプレッドシートと同じですが、Google Formで主に使うのは以下の1つのみ。このトリガーはスプレッドシート側でも使えなくはないですが、このトリガーをフォーム側ではなくスプレッドシート側で使うのは推奨しません。スプレッドシート側で使ったケースで、データがおかしく登録されたり、順番が狂ったり。。
制御するならフォーム側で使うべきです(なので、フォームで送信時に自動応答メール等実装する場合は、フォーム側でこのトリガーを使って装備しましょう)
onFormSubmit(フォーム送信時)
フォーム送信時とは、Googleフォームでユーザがフォームを送信する時に発火するトリガーです。このトリガーを使って処理を行う場合には、複数ユーザ同時送信であったり、処理のバッティングが起きないように、排他制御が必須になります。
また、フォーム送信時の処理やトリガーをスプレッドシート側に記述する人がいるのですが、これはオカシナ挙動(二重に処理がされていたり)などを引き起こす可能性があるため、スプレッドシート側ではなくフォーム側にトリガーとスクリプトは設置するようにしましょう。
var onChangeTrigger = ScriptApp.newTrigger("実行する関数名") .forSpreadsheet(SpreadsheetのIDなど) .onFormSubmit() .create();
時限作動式編
スクリプトトリガーで最も使用する機会が多く、また、設置できるパターンの多いトリガーです。結構細かく時間トリガーを設置することが出来ますが、あまりにも短い時間にバンバンデータを取り込むようなトリガーを設置してしまうと、GoogleのサーバーにBANを食らったり、正常に動作しなくなることも考えられるので、設置に当たっては気をつけましょう。また、このトリガーが設置されたものは、例えファイルがゴミ箱に行こうともトリガーは動き続けますので、捨てる場合には、トリガーを削除してから捨てるようにしましょう。
ここでこのトリガー使用上の注意点です。
- これらのトリガーは、すべて±15分のラグを持って作動するようなので、分単位での綿密なトリガー作動は期待してはいけません。
- 時間ベーストリガーの場合、SpreadsheetAppなどを使う時にはOpenByIdを利用するようにしましょう。getActiveSpreadsheetではエラーになる事があります。
- 同様の理由でgetuiなども無意味ですのでメッセージボックス関係などのコードは外しておきましょう。
ミリセカンド後に実行
単位がミリセカンド(1/1000秒)なので、通常は数秒後という形で設定して使う時間トリガーです。下記の例では10秒後に実行するよう設定しています。
var onChangeTrigger = ScriptApp.newTrigger("実行する関数名") .timeBased() .after(10 * 1000) .create();
特定日に実行
単位が特定日のスポット実行なので、日付型でデータを受け取っておき、atに続けて引数で渡してあげます。
var triggerDay = new Date(2012, 11, 1); var onChangeTrigger = ScriptApp.newTrigger("実行する関数名") .timeBased() .at(triggerDay) .create();
特定日に実行(深夜に実行)
特定日に実行のものと殆ど同じですが、こちらは、深夜付近で実行がされるトリガーです。atDateに続けて引数で渡してあげます。
var onChangeTrigger = ScriptApp.newTrigger("実行する関数名") .timeBased() .atDate(2013,1,1) .create();
○○時に実行
引数で取った数字(時間)に従って、その時になったら発動するトリガー。このトリガーは、この後に出てくる別のトリガーと組み合わせて使用します。下記ではeverydaysと組み合わせて使用しています。サンプルの例の場合、3日置きに午前5時〜6時の間でトリガーが発動します。
var onChangeTrigger = ScriptApp.newTrigger("実行する関数名") .timeBased() .atHour(5) .everyDays(3) .create();
毎日○○時間毎に実行
引数で取った数字(時間)に従って、毎日その時間にトリガーが発動します。下記の例では、毎日3時間毎にスクリプトが実行されます。
var onChangeTrigger = ScriptApp.newTrigger("実行する関数名") .timeBased() .everyHours(3) .create();
毎分○○ごとに実行
引数で取った数字(分)に従って、○○分毎にトリガーが発動しつづけます。指定できる引数は、1分・5分・10分・15分・30分が指定可能です。
var onChangeTrigger = ScriptApp.newTrigger("実行する関数名") .timeBased() .everyMinutes(10) .create();
指定週毎に指定曜日の指定時刻に実行
everyWeeksで毎週なのですが、引数に指定した数字にて、2週毎といった指定ができます。また、この指定オプションは、必ず、指定時刻とどの曜日なのか?を指定するatHourとonWeekDayの指定が必要です。
var onChangeTrigger = ScriptApp.newTrigger("実行する関数名") .timeBased() .atHour(9) .everyWeeks(1) .onWeekDay(ScriptApp.WeekDay.MONDAY) .create();
タイムゾーンの指定
Googleドキュメント全般には、それぞれプロパティを見るとわかるのですが、アメリカ合衆国になっていたりすることがあります。スクリプトでもそれがあり、場合によっては、変更しておかないと妙な挙動をするスクリプトを作る羽目になります。このオプションは、そのタイムゾーンを指定することの出来るメソッドです。
var onChangeTrigger = ScriptApp.newTrigger("実行する関数名") .timeBased() .atHour(12) .everyDays(1) .inTimezone("Asia/Tokyo") .create();
カレンダー編
いつの頃からなのか、いつの間にか追加されていて、あまり使う機会がなかったのでスルーしてたトリガーの1つに「カレンダー編集時に発火するトリガー」があります。このエントリーではこのカレンダートリガーを検証してみようと思います。
カレンダーオーナーのメアドにて
イベントソースでカレンダーからとし、詳細はカレンダー更新済み固定。この時、カレンダーのオーナーのメールアドレス(例えば自分自身のメアド)を登録する。
すると、カレンダーの作成や内容の更新の度に自身がオーナーのすべてのイベントが起こるたびに、トリガーが発火するようになります。自身のアドレスなので自身がオーナーのすべてのカレンダーで発生するようになります。発火させる関数の引数にeを入れておくと、色々と情報はとれますが、有用なのはcalendarIdとTriggerIdくらい。カレンダーIDはすべて自身のメアドになります。
使い所ですが、カレンダーに予定が作成されたら自動でユーザを追加し招待するといったものや、変更された場合に特定のスライドの日付を変更するなどなど。ただしトリガーからその変更内容を取得できません。
図:カレンダーを起点とするトリガー
特定カレンダーのIDにて
前項では自身のメアドを入れましたが、特定のカレンダーのIDを入れることも可能です。以下の手順で手に入れた特定のカレンダーのIDを入れることで、そのカレンダーの時にだけ発火する処理を実装することが可能です。
- 作成したカレンダー横の「︙」をクリック→設定と共有を開く
- 下の方にあるカレンダーの統合にカレンダーIDが記述されてるのでこれをコピー
- トリガーの入力欄に2.のIDを記入して保存する
図:カレンダーIDを取得する
変更内容を取得したい
せっかくのトリガーなのに、前述にあるように「トリガー発火での引数では変更内容が取得できない」という残念な仕様になっています。そこで、以下のような手法を利用することで、Calendar API経由で発火時に変更内容を取得させることが可能です。
事前準備
変更内容を取得するにはまず、GASにCalendar APIを追加します。
- 左サイドバーのサービスの+をクリックする
- Google Calendar APIを探して選択し、追加をクリックする
- また、カレンダーに1個以上のイベントが先にある必要がありますので、0の場合はダミーで入れておきましょう。
図:Calendar APIを追加しておく
初回実行用の関数
最初の一回だけ実行するカレンダー同期用トークンを取得し格納するだけの関数です。Calendar.Events.listにカレンダーIDをつけるだけ。オプションに余計なものを指定するとnextSyncTokenが返ってこないので要注意。
返ってきたら、それをプロパティに格納しておく。
//初回実行時にカレンダートークンを取得しておく function firstCalendarToken(){ //イベント格納用 let event; //カレンダーIDを指定する(primaryでデフォルトカレンダーの指定になる) let calendarId = 'primary'; let options = { timeMin: (new Date()).toISOString(), }; //Calendar APIで全イベントをまず取得させる try { event = Calendar.Events.list(calendarId,options); } catch (e) { console.log(e.message) } //トークンを取得する let token = event.nextSyncToken; //カレンダートークンを保存する let prop = PropertiesService.getScriptProperties(); prop.setProperty('caltoken', token); }
変更内容を取得
実際にカレンダートリガーから呼び出される関数内で前述で取得しておいたトークンを元に、イベントの変更内容を取得してみたいと思います。すると、変更した予定のイベントだけが取得されて、変更していない他のイベントの内容は含まれていません。
//カレンダーイベントあるたびに発火する関数 function calendarman() { //イベント格納用 let event; //カレンダートークンを取得する let prop = PropertiesService.getScriptProperties(); let token = prop.getProperty("caltoken"); //カレンダーIDを指定する let calendarId = 'primary'; //オプション指定する let options = { syncToken : token } //カレンダーイベントをCalendar APIにて取得する try { event = Calendar.Events.list(calendarId, options); } catch (e) { console.log(e.message) } //変更された差分だけが取得できる console.log(event.items); }
GASで設置する
Google Apps Scriptでカレンダートリガーを設置する場合のコードです。特にオプションとかも無いので単純なトリガーですが、コードで設置できることに意義があります。
var calendarTrigger = ScriptApp.newTrigger("ここに実行する関数を指定") .forUserCalendar("ここにカレンダーIDやメールアドレスを入れる") .onEventUpdated() .create();
トリガーの修正と削除
トリガーの修正・削除は、いずれもまずは削除から始まります。そして、修正だけはこの後に改めて設置をするメソッドを発行することになります。しかし、スクリプトトリガーは設置者以外のトリガーが見えなかったりするので、トリガー削除ルーチンがとても便利です。このルーチンの後に設置などのルーチンをつなげてあげれば、トリガーの設置し直しが完了するわけです。
function createTrigger() { var trigger =ScriptApp.newTrigger('clearcell') .timeBased() .everyMinutes(1) .create(); //新しいトリガーIDを取得する var temptriggerid = trigger.getUniqueId(); //トリガーIDを格納する prop.setProperty("trigger", temptriggerid) }
トリガー設置時に、getUniqueIdでトリガーのIDを取得可能です。これをスクリプトプロパティに格納しておく事で、後で指定のIDのトリガーのみを削除する事が可能になります。次のコードがその特定のIDのトリガーのみを削除するコードです。但し、スクリプトプロパティに入った値は文字列となってしまうので、deleteTriggerに引数としてtriggerIdを渡す場合には、事前にNumber(triggerId)といった形で数値型にしておく必要があります。
function deleteTrigger(triggerId) { var allTriggers = ScriptApp.getProjectTriggers(); for(var i=0; i < allTriggers.length; i++) { if (allTriggers[i].getUniqueId() == triggerId) { ScriptApp.deleteTrigger(allTriggers[i]); break; } } }
しかし、このルーチンは、triggerIdが引数で必要であるため、自分の場合、引数をなくして、以下のようにし「トリガー全削除ルーチン」に改造して使います。
function deleteTrigger() { var allTriggers = ScriptApp.getProjectTriggers(); for(var i=0; i < allTriggers.length; i++) { ScriptApp.deleteTrigger(allTriggers[i]); } }
但し、当たり前ですが、トリガー全削除ルーチンは、根こそぎ全部削除してしまいますので、キメ細かくトリガーセッティングをしているケースでは、何か別の仕組みが必要になってくると思います。自分の場合、別にトリガーをワンボタンで設置する為のルーチンを用意しておいてあります。
もっと柔軟にトリガーを設置してみる
一見すると便利そうなスクリプトトリガーという機能ですが、実はめちゃめちゃ融通が効きません。細く設定できそうなトリガーの条件設定なのですが、モノすごくアバウトにしか設定できません。いい事例が、毎日10:30に発動とか、そういったことが出来ません。30分っていう設定は、分トリガーにしかなく、他は時間トリガーしかないわけです。そうなると、時間トリガーしかなく、割りと大きな情報収集トリガーを2本発動させた後にPDF化して送信というスクリプトを書くとなると、3時間後とかになってしまいます。そこで、これを何とかしようというのが今回の目的。
※尚、2本の情報収集トリガーは同時に発動すると、データが壊れるので時間を空けて挙げなければならない。
解決したい課題(自分の事例)
- データは洗い替えで収集するので、毎回データ取得前には、データをクリアする必要性がある。(clearsheet関数を作成)
- 取得したデータに対してFilterでフィルタしたものをPDFにしているので、日付データを毎日設定し直す。(filterday関数を作成
- 日付と時刻指定トリガーを3つ設置する(本日の10:00, 本日の10:30, 本日の11:00)
作成するコードとトリガー
- 全トリガー削除関数を作成する
- データのクリア、日付の変更、全トリガーの削除、全トリガーの設置し直し(settingAllTrigger関数)という一連の作業を行うルーチン(specialdays関数とする)を作成する。このルーチンをトリガーとして、毎日AM1:00に発動するように設置する。この関数の中に、settingAllTrigger関数が最後に呼び出されるように含まれている)。
- 上記ルーチンを実行するトリガー(settingAllTrigger関数とする)を作成する
- ややこしいですが、settingAllTrigger関数もまた、全トリガー削除のルーチンの対象になります。
- 但し、onOpen関数は毎回設置されていないとちょっと困る事情があるので、settingAllTrigger関数に含めて置きます。
- 特定の日時でのトリガー設置の為に日付を構築する関数を用意してあげる。
で、結果的には・・・
- onOpenのトリガー
- specialdays関数のトリガー(毎日AM1:00で設定)
- 本日の日付と特定時刻で設定されているAM10:00発動のデータ収集トリガー1本目
- 本日の日付と特定時刻で設定されているAM10:30発動のデータ収集トリガー2本目
- 本日の日付と特定時刻で設定されているAM11:00発動のPDF化&メール送信トリガー
ソースコード
function onOpen() { /* ここに起動時のコードを記述する */ } function silentget1() { /* ここにデータ取得1本目のコードを記述する */ } function silentget2() { /* ここにデータ取得2本目のコードを記述する */ } function sheet2pdf() { /* ここに特定のシートをPDF化するコードを記述する */ } //今日の日付と指定された時刻で整形して返す関数 function getDate(clockman){ var date = new Date(); var year = date.getFullYear(); var month = date.getMonth() + 1; var date = date.getDate(); if (month < 10) { month = "0" + month; } if (date < 10) { date = "0" + date; } var strDate = year + "/" + month + "/" +date + " " + clockman; return strDate; } //自分自身のIDを取得するコード function setup(){ var sheet = SpreadsheetApp.getActiveSpreadsheet(); var myid = sheet.getId(); var scriptProperties = PropertiesService.getScriptProperties(); scriptProperties.setProperty("mysheetid", myid); return myid; } //トリガー設置の為の準備をする関数 function specialdays(){ //トリガー全削除 deleteTrigger(); //日付の変更 filterdays(); //シートのデータを全クリア clearsheet(); //トリガーを再設置する settingAllTrigger(); } //トリガーを全削除するルーチン function deleteTrigger() { var allTriggers = ScriptApp.getProjectTriggers(); for(var i=0; i < allTriggers.length; i++) { ScriptApp.deleteTrigger(allTriggers[i]); } } //トリガーを設置しなおすルーチン function settingAllTrigger(){ //このスプレッドシートのIDを取得する var Properties = PropertiesService.getScriptProperties(); var myid = Properties.getProperty("mysheetid"); //トリガー用の日付の作成 var trigger1 = new Date(); trigger1.setHours(10); trigger1.setMinutes(00); var trigger2 = new Date(); trigger2.setHours(10); trigger2.setMinutes(30); var trigger3 = new Date(); trigger3.setHours(11); trigger3.setMinutes(00); //設置済みトリガーの数を計測する var Triggers = ScriptApp.getProjectTriggers(); var TriLength = Triggers.length; //設置済みトリガーの数によって作業を分岐 if(TriLength == ""){ //トリガーがなにもない場合 var onChangeTrigger = ScriptApp.newTrigger("onOpen") .forSpreadsheet(myid) .onOpen() .create(); var onChangeTrigger = ScriptApp.newTrigger("specialdays") .timeBased() .atHour(1) .everyDays(1) .create(); var onChangeTrigger = ScriptApp.newTrigger("silentget1") .timeBased() .at(trigger1) .create(); var onChangeTrigger = ScriptApp.newTrigger("silentget2") .timeBased() .at(trigger2) .create(); var onChangeTrigger = ScriptApp.newTrigger("sheet2pdf") .timeBased() .at(trigger3) .create(); }else{ //トリガーがある場合 var deleteman = deleteTrigger(); var onChangeTrigger = ScriptApp.newTrigger("onOpen") .forSpreadsheet(getMySheetId()) .onOpen() .create(); var onChangeTrigger = ScriptApp.newTrigger("specialdays") .timeBased() .atHour(1) .everyDays(1) .create(); var onChangeTrigger = ScriptApp.newTrigger("silentget1") .timeBased() .at(trigger1) .create(); var onChangeTrigger = ScriptApp.newTrigger("silentget2") .timeBased() .at(trigger2) .create(); var onChangeTrigger = ScriptApp.newTrigger("sheet2pdf") .timeBased() .at(trigger3) .create(); } } //シートのデータをクリアする関数 function clearsheet(){ //このスプレッドシートのIDを取得する var Properties = PropertiesService.getScriptProperties(); var myid = Properties.getProperty("mysheetid"); //スクリプト本体 var ss = SpreadsheetApp.openById(myid); var sheet = ss.getSheetByName("database").getRange("A2:A18").getValues(); for(var i = 0;i<sheet.length;i++){ var sheetname = sheet[i][0]; var sheetrange = ss.getSheetByName(sheetname); var lastColumn = sheetrange.getLastColumn(); var lastRow = sheetrange.getLastRow(); sheetrange.getRange(2,1,lastRow,lastColumn).clear(); } } //データ出力日付を変更するルーチン function filterdays(){ //このファイルのIDを取得する var mybookid = PropertiesService.getScriptProperties(); var myid = mybookid.getProperty("mysheetid"); var sheet = SpreadsheetApp.openById(myid); var todayman = new Date(); var enddate = new Date(todayman.setDate(todayman.getDate() - 1)); var startdate = new Date(todayman.setDate(todayman.getDate() - 6)); //特定のセルに値を格納する sheet.getRangeByName("startday").setValue(getDate(startdate)); sheet.getRangeByName("endday").setValue(getDate(enddate)); sheet.getRangeByName("tokutoday").setValue(getDate(enddate)); }
- このスクリプト群は、一度そのスプレッドシートのIDをスクリプトプロパティに格納しておく必要があるので、最初の1回だけsetup()を実行する必要性があります。
- 当たり前ですが、最初の1回だけは、手動でトリガー群を設置しないといけないので、settingAllTrigger()を実行する必要性があります。以降は、トリガーが勝手にやってくれるので、必要ありません。
- 特定の日付のトリガー用に、trigger1~3の変数を用意して、日付と時刻をセットしてあげます。
特定の日時のトリガーを作る上でのポイント
下記は、今日の日付の10:40の日付型の値をtrigger1変数に受け取るコードです。時刻部分を、ユーザに入れさせるように、何かギミックを用意するのも悪くありませんね。
//今日の日付を取得し、10:40分をセットする var nowdate = new Date(); nowdate.setHours(10); nowdate.setMinutes(40);
下記は、受け取ったtrigger1の値をもとに、Script Triggerのat()をつかって、特定の日付・時刻を指定しています。
var onChangeTrigger = ScriptApp.newTrigger("silentget1") .timeBased() .at(nowdate) .create();
特定の日時で毎日トリガーを実行する
前述のトリガーは特定の日時なので、2021年2月10日10:30に実行といった事が出来ますが、その後のこのトリガーはもう使えません。使い切りのトリガーです。そこで、これにさらに一工夫を加えて毎日10:30に実行といったような形に改造をしようと思います。ポイントは
- 毎日特定のトリガーを設置するトリガーを用意する
- 毎日実行する本体のトリガーは実行したら自身を設置したトリガーを削除する
という点。よってこのトリガーの設置には2個のトリガーが必要になります。
//毎日実行するトリガーを設置するトリガー function setCustomTrigger(){ //トリガー設置(朝5時台にトリガーを設置させる) var onChangeTrigger = ScriptApp.newTrigger("exeCustomTrigger") .timeBased() .atHour(5) .everyDays(1) .create(); } //毎日実行する本体トリガー function exeCustomTrigger(){ //スクリプトプロパティを取得 var prop = PropertiesService.getScriptProperties(); //今日の日付を取得し、10:40分をセットする var nowdate = new Date(); nowdate.setHours(10); nowdate.setMinutes(40); //毎日10:40に実行するトリガーを設置する var onChangeTrigger = ScriptApp.newTrigger("silentget") .timeBased() .at(nowdate) .create(); //新しいトリガーIDを取得する var triggerid = onChangeTrigger.getUniqueId(); //スクリプトプロパティにtriggeridを格納する prop.setProperty("trigger", triggerid); } //exeCustomTriggerが実行する関数 function silentget(){ console.log("トリガー実行したよ"); //指定のトリガーIDのトリガーを削除する var prop = PropertiesService.getScriptProperties(); var trigger_id = prop.getProperty("trigger"); deltrigger(Number(trigger_id)); } //トリガーを削除する function deltrigger(triggerId){ var allTriggers = ScriptApp.getProjectTriggers(); for (var i = 0; i < allTriggers.length; i++) { if (allTriggers[i].getUniqueId() == triggerId) { ScriptApp.deleteTrigger(allTriggers[i]); break; } } }
- setCustomTriggerは最初の1回のみ実行して、exeCustomTriggerを呼び出すようにセット(時刻は朝の5時にセット)
- exeCustomTriggerが毎日実行される関数。当日の10:40分に実行されるsilentgetという関数を呼び出すトリガーをセットします。
- この時、トリガーIDをスクリプトプロパティに格納しておく
- silentget関数は毎日10:40分に実行後にセットされたトリガーを削除する
- 実行後にスクリプトプロパティに格納されてるトリガーIDに基づいてトリガーを削除しています。
これで毎朝5時にトリガー発動で朝10:40分に発動するトリガーがセットされ、10:40分後にそのトリガーは削除されるという繰り返しが実現できます。
特定のセルを編集したら1分後にセルをクリアする
onEditとトリガーを駆使して、特定のセルを編集したら、1分後にそのセルの中身をクリアするといったコードを作ってみます。今回の場合、onEditのトリガーを予め設置しておく必要があるため、deleteTrigger()でトリガー削除をしてしまうと、onEditのトリガーまで消えてしまいます。
ですので、トリガー設置時にtrigger_idを取得して、特定のトリガーだけを削除しつつこの課題を実現してみます。予め、シートのトリガーに対して、onEditを編集時として設置しておく必要があります。
//スプレッドシートのID var ssid = "ここにスプレッドシートのIDを入れておく"; function onEdit(e) { // シートの名前 var sheetName = e.source.getActiveSheet().getName(); // セル位置 var cellPosition = e.source.getActiveRange().getA1Notation(); //条件判定 if (sheetName == 'シート1') { if (cellPosition == 'A1') { //トリガー設置 createTrigger(); } } } //A1のセルをクリアする function clearcell(){ //セルを取得 var ss = SpreadsheetApp.openById(ssid).getSheetByName("シート1").getRange("A1"); //セルをクリア ss.clearContent(); //指定のトリガーIDのトリガーを削除する var prop = PropertiesService.getScriptProperties(); var trigger_id = prop.getProperty("trigger"); deltrigger(Number(trigger_id)); } //トリガーを設置する function createTrigger() { var prop = PropertiesService.getScriptProperties(); var trigger_id = prop.getProperty("trigger"); deltrigger(Number(trigger_id)); var trigger =ScriptApp.newTrigger('clearcell') .timeBased() .everyMinutes(1) .create(); //新しいトリガーIDを取得する var temptriggerid = trigger.getUniqueId(); //トリガーIDを格納する prop.setProperty("trigger", temptriggerid) } //トリガーを削除する function deltrigger(triggerId){ var allTriggers = ScriptApp.getProjectTriggers(); for (var i = 0; i < allTriggers.length; i++) { if (allTriggers[i].getUniqueId() == triggerId) { ScriptApp.deleteTrigger(allTriggers[i]); break; } } }
- スプレッドシートが開かれていなくても、トリガー発動で消せるように、ssidを設定しておきます。
- onEditで特定のシートの特定のセルだけ編集時にトリガー設置するように条件分岐
- createTrigger()では、既に仕込み済みのクリアするトリガーを予め削除するようにしています。
- トリガーは1分後にclearcell()を呼び出します。またこの時、trigger_idを取得し、スクリプトプロパティに格納しておく。
- deltrigger()ではスクリプトプロパティに格納されているtrigger_idを持って、特定のトリガーのみを削除します。
- clearcell()ではclearcontentでセルをクリア後に、トリガーを削除するようにしています。
図:事前にonEditのトリガーを仕込んでおく
営業日ベースでトリガーを発火させる
実際の業務の現場ではトリガーで自動処理は非常に便利な反面、休日にまでせっせとトリガーが発動して無駄に動作してしまうのは面倒です。そこで、「営業日ベースで指定の時刻に発火するトリガー」が必要になります。土日祝は処理を実行せず、またカレンダーにはないお休みの日なども対応させて、営業日にだけ動かせるようにしてみようと思います。
サンプルになるスプレッドシートはこちらです。
事前準備
自分のアカウントに日本の休日のカレンダーを追加しておきましょう。以下の手順で追加しておきます。
- Googleカレンダーを開く
- 左下のその他のカレンダーの横にある+ボタンをクリックし、カレンダーに登録をクリックする
- カレンダーに追加に、「ja.japanese#holiday@group.v.calendar.google.com」を追加する
- これで日本の祝日がカレンダーに表示されたと思います。
また、サンプルスプレッドシートを使う場合には、シートを開き、メニューにある「セットアップ」から初期化を実行し、同様にトリガー設置も実行しましょう。
スプレッドシートのお休みリストには日付と名前付きで土日祝以外の休みの日を記載します。
ソースコード
//日本の休日カレンダー var holiday = "ja.japanese#holiday@group.v.calendar.google.com"; //スプレッドシートのメニューを作る function onOpen() { var ui = SpreadsheetApp.getUi(); ui.createMenu('▶セットアップ') .addItem('初期化', 'getMySheetId') .addItem('トリガー設置', 'setWorkTrigger') .addToUi(); } //営業日かどうか?お休みリストにないか?を判定する function workdaychk() { //日付を取得する var nowdate = new Date(); //シート検索用日付 var chkday = formatDate(nowdate); //平日かどうかを判定する if(nowdate.getDay()== 0 || nowdate.getDay()== 6) { //土日の場合には処理を実行しない console.log("休日だよ"); return; } //祝日かどうかを判定する const jaHoliday = CalendarApp.getCalendarById(holiday); if(jaHoliday.getEventsForDay(nowdate).length > 0){ //祝日の場合カレンダーにイベントが存在する console.log("祝日だよ"); return false; } //スプレッドシートに記載の日付と一致するものがあるか判定 var Prop = PropertiesService.getScriptProperties(); var ssid = Prop.getProperty("sheetid"); var ss = SpreadsheetApp.openById(ssid).getSheetByName("お休みリスト").getRange("A2:B").getValues(); for(var i = 0;i<ss.length;i++){ //空欄はスルーする if(ss[i][0] == null || ss[i][0] == ""){ continue; } //日付をyyyy/mm/ddに変換 var ssdate = formatDate(ss[i][0]); if(ssdate == chkday){ //シート上のお休みと一致 console.log("法定外の休日"); return; } } //処理を実行する executeWork(); } //日付をyyyy/mm/ddに変換する function formatDate(date) { //日付の冒頭に0をつける var paddingZero = function(n) { return (n < 10) ? '0' + n : n; }; //シート検索用に日付をフォーマット var year = date.getFullYear(); var month = paddingZero(date.getMonth() + 1); var dateman = paddingZero(date.getDate()); var chkday = year + "/" + month + "/" + dateman; return chkday; } //トリガーを設置する //トリガーを設置しなおすルーチン function setWorkTrigger(){ //プロパティを取得する var Prop = PropertiesService.getScriptProperties(); var chkTrigger = Prop.getProperty("trigger"); //設置済みトリガーを削除する deleteTrigger(chkTrigger); //毎日12時に発火する var onChangeTrigger = ScriptApp.newTrigger("workdaychk") .timeBased() .everyHours(12) .create(); //新しいトリガーIDを取得する var temptriggerid = trigger.getUniqueId(); //トリガーIDを格納する Prop.setProperty("trigger", temptriggerid); //メッセージを表示 SpreadsheetApp.getUi().alert("トリガー設置完了"); } //トリガーを削除する function deleteTrigger(triggerId) { var allTriggers = ScriptApp.getProjectTriggers(); for(var i=0; i < allTriggers.length; i++) { if (allTriggers[i].getUniqueId() == triggerId) { ScriptApp.deleteTrigger(allTriggers[i]); break; } } } //自分自身のIDを取得するコード function getMySheetId(){ var sheet = SpreadsheetApp.getActiveSpreadsheet(); var myid = sheet.getId(); var Properties = PropertiesService.getScriptProperties(); Properties.setProperty("sheetid", myid); return myid; } //メインの処理をするルーチン function executeWork(){ console.log("自動作業を実行"); }
- トリガー発火で実際に自動作業をする関数はexecuteWorkに記述します
- トリガーでの呼び出しは基本毎日○時で発火でセットします。
- トリガーで呼び出される関数であるworkdaychk関数で、土日か?祝日か?スプレッドシート記載の日付か?を判定し、どれにも合致しない場合、営業日として判定し、executeWorkを実行します。
- Googleカレンダーの日本の祝日カレンダーに該当の日があった場合には祝日として判定して処理を終了させます。
今回は土日祝以外の日で休みとする場合には、お休みリストにある日付に記載して処理をしていますが、このリスト自体もGoogleカレンダーに専用のカレンダーを用意して、イベントを登録し、そのカレンダーIDを取得して日本の祝日と同様の処理を行えば、お休みリストをカレンダー上で管理が出来て便利です。
専用カレンダーで管理する場合、そのカレンダーIDが必要です。
専用のカレンダーを用意する
Googleカレンダーを開いて、お休みリスト用のカレンダーを用いて処理を行わせたい場合には、以下の手順でカレンダーを用意してカレンダーIDを取得します。スクリプトは自分自身の権限で動かしてるので共有等は不要です。
- Googleカレンダーを開く
- 左下のその他のカレンダーの横にある+ボタンをクリックし、新しいカレンダーを作成をクリックする
- お休みリストと名前をつけて、カレンダー作成をクリックする
- マイカレンダーに作られるので、開いてみると「カレンダー設定」があるのでクリックする
- 右サイドの下のほうにカレンダー統合があり、そこにカレンダーIDが記述されてるのでコピーする
前述のworkdaychk関数のうち、スプレッドシートに記載の日付と一致するものがあるか判定の部分を日本の祝日と同じコードに変えてあげる
//スプレッドシートに記載の日付と一致するものがあるか判定 var oyasumi = "ここに取得したカレンダーIDを記述する" const myholiday = CalendarApp.getCalendarById(oyasumi); if(myholiday.getEventsForDay(nowdate).length > 0){ //お休みカレンダーにイベントが存在する console.log("会社はお休みだよ"); return false; }
- oyasumiに取得したカレンダーIDを記述する
- 当日にイベントが記載されていればその日は土日祝以外のお休みとして判定される
カレンダーには会社のお休みとして適当なイベントを登録しておくと良いでしょう。専用のカレンダーなので余計なイベントを入れてしまうと、休日判定されてしまうので注意。
図:新規にカレンダーを用意しておく
図:カレンダーIDを取得する
Class構文のStaticメソッドをトリガーで設置する
現在のGoogle Apps ScriptはV8 Runtimeに対応してるので、ES2023までのJavaScript構文が使えるようになりました。その結果として、スクリプトトリガーでも、Class構文の中にあるstaticメソッドで定義した関数をスクリプトからであれば、設置することが可能になっています。まだ、Google Apps Scriptのトリガー設置画面上からは設置は出来ません。
参考事例は以下の通りです。以下の例であれば括弧なしでmailman.tomatoと指定して設置すれば良いです。
class mailman { static tomato(){ //メールを送信 MailApp.sendEmail({ to: GetUser(), subject: 'とまと', htmlBody: '🍅🍅🍅🍅🍅🍅🍅🍅', }); } } //現在のユーザのアドレスを取得 function GetUser() { var objUser = Session.getActiveUser(); return objUser.getEmail(); } //クラスのstaticメソッドをトリガーにセットする function setStaticTrigger(){ var onChangeTrigger = ScriptApp.newTrigger("mailman.tomato") .timeBased() .everyMinutes(10) .create(); }
図:スクリプトではStaticメソッドをトリガーで設置できる
トリガーに引数を渡す手法
スクリプトトリガーは実はデフォルトでは「関数の引数」を渡す事ができません。あくまでも関数を呼び出すだけで()をつけて引数を付けて、トリガーを設置する事が出来ません。そのため手動で設置する場合には、トリガー引数は設置が出来ないのです。
しかし、スクリプトから設置する場合には、ちょっとしたテクニックを使う事でスクリプトトリガーの関数に引数を設定する事が可能です。
//スクリプトプロパティに格納する情報のKEYの指定 var RECURRING_KEY = "recurring"; //トリガーを継続使用する場合のフラグ var ARGUMENTS_KEY = "arguments"; //トリガー関数の引数を格納する //トリガーの設置(5分毎に実行) function settingTrigger() { //トリガーをセットする var trigger = ScriptApp.newTrigger("TriggerFunction").timeBased() .everyMinutes(5) .create(); //トリガーの引数を保存する(継続使用しないので、falseを指定) setupTriggerArguments(trigger, "ここに引数", false); } //トリガーの引数をセットする function setupTriggerArguments(trigger, functionArguments, recurring) { //トリガーのIDを取得する var triggerUid = trigger.getUniqueId(); //トリガーの引数と継続使用フラグを配列に格納する var triggerData = {}; triggerData[RECURRING_KEY] = recurring; triggerData[ARGUMENTS_KEY] = functionArguments; //引数と継続使用フラグをスクリプトプロパティに格納する PropertiesService.getScriptProperties().setProperty(triggerUid, JSON.stringify(triggerData)); } //トリガーで実行される関数 function TriggerFunction(event) { //トリガーの引数情報を取得する var functionArguments = handleTriggered(event.triggerUid); console.info("関数の引数: %s ", functionArguments); } //トリガー情報を取得して返す function handleTriggered(triggerUid) { //トリガーIDに基づいてスクリプトプロパティから引数と継続使用フラグを取得する var scriptProperties = PropertiesService.getScriptProperties(); var triggerData = JSON.parse(scriptProperties.getProperty(triggerUid)); //継続使用フラグがfalseの場合はトリガーを削除する if (!triggerData[RECURRING_KEY]) { deleteTriggerByUid(triggerUid); } //継続使用する時は引数を返す return triggerData[ARGUMENTS_KEY]; } //トリガーをトリガーIDに基づいて削除する function deleteTriggerByUid(triggerUid) { if (!ScriptApp.getProjectTriggers().some(function (trigger) { if (trigger.getUniqueId() === triggerUid) { ScriptApp.deleteTrigger(trigger); return true; } return false; })) { console.error("id '%s' のトリガーが見つかりませんでした。", triggerUid); } PropertiesService.getScriptProperties().deleteProperty(triggerUid); } //トリガーの引数情報を削除する function deleteTrigger(trigger) { ScriptApp.deleteTrigger(trigger); deleteTriggerArguments(trigger.getUniqueId()); }
- スクリプトトリガー設置時に、スクリプトトリガーのID、引数、繰り返し使用フラグを生成する(settingTrigger関数にて)
- 上記で取得した情報を、スクリプトプロパティにトリガーのIDをキーとして、引数と繰り返し使用フラグをJSONとして保存
- トリガーで関数を呼び出された時に、呼び出された関数(TriggerFunction)からhandleTriggerdを呼び出してあげる
- TriggerFunctionの引数にeventがついていますが、これをつけることで、トリガーの情報(そのトリガーのIDなど)が取得できます。これを元にスクリプトプロパティからTriggerFunctionに渡したい引数情報や継続使用フラグを取り出します。
- handleTriggerdでは、継続使用フラグがTrueの時にはトリガーデータを返し、falseの場合はトリガー削除を実行する
- deleteTriggerByUidにてスクリプトトリガーを削除すると同時にスクリプトプロパティも削除する
- 同じ関数でもトリガー固有のIDはすべて違うので、実行時にトリガーIDに基づいて、異なる引数で複数同時にトリガーを設置することは可能です。
図:スクリプトプロパティに格納されたトリガー引数
図:実行数からログを確認。トリガーも無事削除されてる
ポイント
トリガー対象となっているスクリプト内では、自動的にそれらが動くわけなのですが、通常とはちょっとだけ異なる挙動になるのと、タイムアウトの5分を意識して作らなければなりません。もし、スクリプトの実行が失敗しますと、サーバーからメールが飛んできます。
- スクリプトトリガー作動時のユーザは設置者の権限となります。故に、メールアドレスなども設置者のメアドがログに残ることになります。
- 複数の人間が同じトリガーを設置してはなりません。また、他の人からは他の人間が設置したスクリプトトリガーは見えませんので要注意です。タブってトリガーが発動されることになります。
- 値を他のシートからかき集めて、集計するタイプのものは、タイムアウトに注意!
- 集計したさらに先に、PDFを作成して格納するようなルーチンを書く場合には、スクリプトトリガーの発動時間を当たり前ですが、ずらしておく必要性がある。でないと、集計されきってないのに、空のPDFが作成されたりする。
- 複雑な関数を自分で作成し、スプレッドシート内で関数として使っている場合、「読込中・・・」のままになり、これをPDF化するような自動化を行うと、全く帳票として使い物にならないものが生成されるので、なるべくなるべく、スプレッドシート上で使用する関数は標準の関数を使用すること(自作関数はどうしてもスピードでは圧倒的に負ける)。
- 時間トリガーはあくまでも指定した時間~時間のどこかで実行されるものなので、確実に何時何分という指定はできない。
- 通常は、getActiveSpreadsheetなどで取得しているシートは、openByIdにしておくこと。もちろん、自分自身のIDを取得するような関数も用意しておいて、予めスクリプトプロパティに格納し、呼び出す仕組みが望ましい。でないと、書き込みなどが出来ずに終わってしまう。
- スクリプトトリガーの設置では、引数をつけた関数の呼び出しが出来ないので、引数付きで呼び出したい場合には、引数付きの関数を呼び出す関数を作成しておいて、それをセットするようにすると良いでしょう。
実行日付がオカシイ場合
スクリプトトリガーがオカシナ時間に実行されるという場合には、タイムゾーン設定を疑う必要があります。GoogleスプレッドシートにはGASも含めて2個のタイムゾーン設定が存在しており、この設定がオカシイ場合、実行時間に大幅なズレが生じる可能性があります。以下の2点をチェックしましょう。
スプレッドシートのタイムゾーン
これはスプレッドシートそのものに設定されてるタイムゾーンです。大昔から存在するもので、主にシート状での関数などの挙動に関する日付等を担当しています。よって、スクリプトトリガーに直接影響があるかといったら微妙なのですが、トリガーで日付の書き込みなどを行う関数を取り扱う場合に影響が出るかもしれません。
以下の手順で確認しましょう。
- メニューより「ファイル」→「Googleスプレッドシートの設定」を開く
- タイムゾーンが「GMT+9:00 Tokyo」になっていればOK
かなり初期の頃この値がUSになっていたことがあり、自動でセルに日付を挿入した際に、USの時間が書き込まれてオカシイ挙動だった時代があります。
図:ここが狂ってるケースは多分今は殆どない
スクリプトエディタのタイムゾーン
さて問題は、スクリプトエディタ側にもこのタイムゾーンの設定があるのだけれど、この設定自体は表に出ていない点。スクリプトエディタのタイムゾーン設定はappsscript.jsonに入っており、デフォルトで非表示になっているため。以下の設定変更で表示し、中身を確認する必要があります。
- スクリプトエディタを開く
- 左サイドバーのプロジェクトの設定を開く
- appsscript.json マニフェストファイルをエディタで表示するにチェックを入れる
- あらためて、コードに戻るとファイルの項目に、appsscript.jsonが表示される
- 開くと中にtimeZoneの項目があり、ここが「Asia/Tokyo」になっていればOK
この設定はスクリプトトリガーの実行自体や、スクリプトでのnew Date()などの結果に影響を与えるのですが、なぜかAmerica/New_Yorkになってる人がいるようで、必ず治すようにしましょう。
図:この設定をオンにする
図:appsscript.jsonの設定は影響します
関連リンク
- トリガーのプログラム的管理
- よく使うTime Triggerのテンプレ
- GASの怖い話と対策となんか
- 無料でお手軽Cron!Google Apps Scriptを使ってみる。
- 【GAS】nearMinute の挙動について
- 静的プロパティとメソッド
- How can I pass a parameter to a time-based Google App Script trigger?
- GASのトリガーで関数に引数を渡したい
- スプレッドシートに組み込んだGASのトリガー「フォーム送信時」が正常に作動しなかった問題
- Google Calendar - SyncToken is missing in API response
- Google Apps Script and Trigger when new Calendar Event is added