トリガーの設置

スクリプトトリガーは、スクリプトエディタの画面から入り、手動で登録するのが通常のフローです。しかし、「スクリプト内でテンポラリで時間トリガーを設置したい」であったり「トリガーの設置し直し」など、スクリプトエディタにいちいち入らず設定したいシーンがボチボチあります。そういった場合には、スクリプトからトリガーの設置や削除が出来ると便利です。二重に登録してしまったりすると、二回発動したり、片方しか発動しなかったりするので、慎重に設置をしましょう。

トリガーの設置

スプレッドシート編

スプレッドシートでは、時間ベーストリガーの他にも、スプレッドシート特有のトリガーを設置する事が出来ます。使用することの出来る特有のトリガーは、「onChange」、「onEdit」、「onFormSubmit」、「onOpen」の合計4つとなります。それぞれ、「変更時」「編集時」「フォーム送信時」「起動時」の4つに該当します。以下に各トリガー設置のスクリプトを記載します。

onChange(変更時)

変更時とは、構造変更や内容変更が発生した時に発動するトリガーです。VBAで言う所のAfterUpdateイベントと言えます。

function createSpreadsheetTrigger() {
  var onChangeTrigger = ScriptApp.newTrigger("実行する関数名")
      .forSpreadsheet(ssId)
      .onChange()
      .create();

onEdit(編集時)

編集時とは、編集時に毎回発動するトリガーです。VBAで言う所のBeforeUpdateイベントと言えます

var onChangeTrigger = ScriptApp.newTrigger("実行する関数名")

      .forSpreadsheet(ssId)
      .onEdit()
      .create();

onFormSubmit(フォーム送信時)

フォーム送信時とは、スプレッドシート側から作成したフォーム上でデータが送信された時に発動するトリガーです。

var onChangeTrigger = ScriptApp.newTrigger("実行する関数名")

      .forSpreadsheet(ssId)
      .onFormSubmit()
      .create();

onOpen(起動時)

起動時とは、そのスプレッドシートが開かれた時に発動するトリガーである。通常はメニューの登録などでよく使われている。

var onChangeTrigger = ScriptApp.newTrigger("実行する関数名")

      .forSpreadsheet(ssId)
      .onOpen()
      .create();

フォーム編

フォームでは、時間ベーストリガーは使用することが出来ません。また、使用できるトリガーはリファレンス上では、「onFormSubmit」、「onOpen」しかありません(しかし、実際にトリガー設置画面だと、onEditがあったりします)。殆ど、スプレッドシートと同じですので、ここでは省略します。変更箇所は、.forSpreadsheetが .forFormになるだけです。

時間ベース編

スクリプトトリガーで最も使用する機会が多く、また、設置できるパターンの多いトリガーです。結構細かく時間トリガーを設置することが出来ますが、あまりにも短い時間にバンバンデータを取り込むようなトリガーを設置してしまうと、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();

毎日○○時間毎に実行

引数で取った数字(時間)に従って、毎日その時間にトリガーが発動します。下記の例では、毎日12時にスクリプトが実行されます。

var onChangeTrigger = ScriptApp.newTrigger("実行する関数名")
      .timeBased()
      .everyHours(12)
      .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("America/Los_Angeles")

.create();

トリガーの修正・削除

トリガーの修正・削除は、いずれもまずは削除から始まります。そして、修正だけはこの後に改めて設置をするメソッドを発行することになります。しかし、スクリプトトリガーは設置者以外のトリガーが見えなかったりするので、Google Styleで紹介されているトリガー削除ルーチンがとても便利です。このルーチンの後に設置などのルーチンをつなげてあげれば、トリガーの設置し直しが完了するわけです。

function deleteTrigger(triggerId) { var allTriggers = ScriptApp.getScriptTriggers(); for(var i=0; i < allTriggers.length; i++) { if (allTriggers[i].getUniqueId() == triggerId) { ScriptApp.deleteTrigger(allTriggers[i]); break; } } }

しかし、このルーチンは、triggerIdが引数で必要であるため、自分の場合、引数をなくして、以下のようにし「トリガー全削除ルーチン」に改造して使います。

function deleteTrigger() { var allTriggers = ScriptApp.getScriptTriggers(); for(var i=0; i < allTriggers.length; i++) { ScriptApp.deleteTrigger(allTriggers[i]); } }

但し、当たり前ですが、トリガー全削除ルーチンは、根こそぎ全部削除してしまいますので、キメ細かくトリガーセッティングをしているケースでは、何か別の仕組みが必要になってくると思います。

トリガー内でスクリプトを動かす時のヒント

トリガー対象となっているスクリプト内では、自動的にそれらが動くわけなのですが、通常とはちょっとだけ異なる挙動になるのと、タイムアウトの5分を意識して作らなければなりません。もし、スクリプトの実行が失敗しますと、サーバーからメールが飛んできます。

  1. 値を他のシートからかき集めて、集計するタイプのものは、タイムアウトに注意!
  2. 通常は、getActiveSpreadsheetなどで取得しているシートは、openByIdにしておくこと。もちろん、自分自身のIDを取得するような関数も用意しておいて、予めスクリプトプロパティに格納し、呼び出す仕組みが望ましい。でないと、書き込みなどが出来ずに終わってしまう。
  3. 集計したさらに先に、PDFを作成して格納するようなルーチンを書く場合には、スクリプトトリガーの発動時間を当たり前ですが、ずらしておく必要性がある。でないと、集計されきってないのに、空のPDFが作成されたりする。
  4. 複雑な関数を自分で作成し、スプレッドシート内で関数として使っている場合、「読込中・・・」のままになり、これをPDF化するような自動化を行うと、全く帳票として使い物にならないものが生成されるので、なるべくなるべく、スプレッドシート上で使用する関数は標準の関数を使用すること(自作関数はどうしてもスピードでは圧倒的に負ける)。
  5. 二人三人と複数の人間がスクリプトトリガーを設置するのはやめましょう。責任者を決め、その人間だけにすること。でないと、意味不明な誰かが置いた見えないトリガーだらけになり、何重にもトリガーが発動することになります。
  6. 時間トリガーはあくまでも指定した時間~時間のどこかで実行されるものなので、確実に何時何分という指定はできない。

もっと柔軟にトリガー設置してみる

概要

一見すると便利そうなスクリプトトリガーですが、実はめちゃめちゃ融通が効きません。細く設定できそうなトリガーの条件設定なのですが、モノすごくアバウトにしか設定できません。いい事例が、毎日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関数に含めて置きます。
  • 特定の日時でのトリガー設置の為に日付を構築する関数を用意してあげる(getDate()とする)。

で、結果的には・・・

毎日、AM1時にspecialdays関数が発動して全トリガーとデータの消去が行われた後、settingAllTrigger関数が呼ばれて新たに、以下のトリガーが設置されます

  1. onOpenのトリガー
  2. specialdays関数のトリガー(毎日AM1:00で設定)
  3. 本日の日付と特定時刻で設定されているAM10:00発動のデータ収集トリガー1本目
  4. 本日の日付と特定時刻で設定されているAM10:30発動のデータ収集トリガー2本目
  5. 本日の日付と特定時刻で設定されているAM11:00発動のPDF化&メール送信トリガー

の5本が、毎日設置されなおされて継続していくわけです。こうすることで、非常に柔軟性のあるトリガーを設置して細かく挙動をコントロールすることが可能になります。

ソースコード

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.getScriptTriggers();
  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(Utilities.formatDate(new Date() , 'JST' , getDate("10:00")));
  var trigger2 = new Date(Utilities.formatDate(new Date() , 'JST' , getDate("10:30")));
  var trigger3 = new Date(Utilities.formatDate(new Date() , 'JST' , getDate("11: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));
  
}
  1. このスクリプト群は、一度そのスプレッドシートのIDをスクリプトプロパティに格納しておく必要があるので、最初の1回だけsetup()を実行する必要性があります。
  2. 当たり前ですが、最初の1回だけは、手動でトリガー群を設置しないといけないので、settingAllTrigger()を実行する必要性があります。以降は、トリガーが勝手にやってくれるので、必要ありません。
  3. 特定の日付のトリガー用に、trigger1~3の変数を用意して、getDate()関数で整形して上げた値を返してあげています。

特定の日付時刻のトリガーを作るポイント

毎度毎度のことなのですが、Googleのリファレンスに載っていないので、色々情報を集めて、特定の日付・時刻によるトリガーの設置の為に、getDate関数を作成しました。引数に時間(HH:MM形式)を受けて、返り値として(YYYY/MM/DD HH:MM)の形で返すようにしています。getDate("10:30")とやると、返り値として本日の日付の10:30の日付型で値が返ってくるので、これをスクリプトトリガーのatに渡してあげるわけです。整形は、Utilities.formatDateで行っています。当たりまえですが、更にこれをnew Dateで日付型に戻して挙げるわけです。

下記は、今日の日付の10:40の日付型の値をtrigger1変数に受け取るコードです。時刻部分を、ユーザに入れさせるように、何かギミックを用意するのも悪くありませんね。

var trigger1 = new Date(Utilities.formatDate(new Date() , 'JST' , getDate("10:40")));

下記は、受け取ったtrigger1の値をもとに、Script Triggerのat()をつかって、特定の日付・時刻を指定しています。

var onChangeTrigger = ScriptApp.newTrigger("silentget1")

.timeBased()

.at(trigger1)

.create();

直接、at()の中に、【YYYY-MM-DD HH:MM】で書けたら楽なのにね・・・

関連リンク