Google Apps Scriptにはスクリプトの実行時間に制限が設けられており、この記事執筆時点では6分間が上限となっています。(2022.9.15執筆)
最新情報は、下記のGoogleの公式ドキュメントで確認いただけます。
簡単な処理であれば、6分間の制約が問題になることはありませんが、
例えば、大量のファイルを一括作成したり、大量のユーザーをGoogleグループに登録したり・・・などという時に、6分間を超えてしまい、処理が中断されてしまうことがあります。
この記事では、その対処法を説明します。
前提:検証に使うスクリプト
説明・検証に使うためのスプレッドシート・スクリプトを用意しました。
// 対象のシートを取得する
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
/* ------------------
100回繰り返す処理
------------------- */
function batch() {
// 100回(2行目から101行目まで)処理する
for (let row = 2; row <= 101; row++) {
process10sec(row);
}
}
/* ---------------
10秒かかる処理
---------------- */
function process10sec(row) {
// B列 に開始時刻をセット
sheet.getRange(`B${row}`).setValue(Utilities.formatDate(new Date(), "JST", "HH:mm:ss"));
// 10秒かかる処理
Utilities.sleep(10 * 1000);
// C列 に終了時刻をセット
sheet.getRange(`C${row}`).setValue(Utilities.formatDate(new Date(), "JST", "HH:mm:ss"));
}
このスクリプト内のprocess10secは、以下のような処理内容になっています。
- B列に開始時刻を入力
- 10秒間まつ
- C列に終了時刻を入力
batch関数は、先述の処理をForループで100回呼び出す処理です。早速実行してみました。
10秒かかる処理を36回実行すると360秒(6分)なので、制限どおり6分のタイミングで処理が中断されていることがわかります。
この処理を、どうにか最後まで実行できるようにしましょう。
トリガーを自動追加して繰返し起動させる方法
まずは、制限時間まぎわに、トリガーを自動的に追加する方法を紹介します。
この方法のメリット・デメリットは以下の通りです。
- GASの知識で完結するので覚えることが少ない
- 時間指定の起動なども問題なく可能
- 6分間制限ほどではないが、実行時間の制限がある
- トリガーの設置、実行時間のチェック、処理済みの判定など、ややコードが複雑になる
今回の例では、batch関数が開始してから5分間経過したら、1分後にbatch関数を起動するトリガーを追加するコードを作成します。
まずは、トリガーを追加する関数を作成します。
// 1分後にbatch関数を起動するトリガーを作成
function createTrigger() {
ScriptApp.newTrigger("batch")
.timeBased()
.after(1 * 60 * 1000)
.create();
}
newTrigger(“batch”)の“batch”がトリガーで起動する関数名です。
また、.after(1 * 60 * 1000)の部分で、トリガーに設定する時刻を現在時刻の1分後に設定しています。
これらは、必要に応じてお好きなように変更してください。
次にbatch関数(処理を100回呼び出す関数)を修正します。
/* ------------------
100回繰り返す処理
------------------- */
function batch() {
const startTime = Date.now();
// 100回(2行目から101行目まで)処理する
for (let row = 2; row <= 101; row++) {
// 処理開始から5分以上経過したらトリガーを作って処理を終了
if (Date.now() - startTime > (5 * 60 * 1000)) {
createTrigger();
return;
}
// 処理済の行はスキップ
if (sheet.getRange(`C${row}`).getValue()) continue;
// 10秒かかる処理を実行
process10sec(row);
}
}
5行目、10行目:
開始から5分以上経過していたらトリガーを作成した上で処理を終了(return)します。
15行目:
トリガーで再度起動された際も、また最初から(2行目から)順に処理されます。
よって、既に処理された行についてはスキップをする必要があります。
この例では、C列(終了時刻)に値が入っていたら、処理済の行だと判断してスキップ(continue)しています。
※ この部分はあくまでサンプルなので、ご自身のスクリプトに適した方法でスキップしてください。
例えば、処理完了した行数をシートのセルや、スクリプトプロパティ等に記録してから処理を終了し、次の起動時は、その行数 + 1から処理再会する方法などが考えられます。
これでスクリプトができましたので、実行してみました。
無事、100行分の処理が完了しました。
最初の行の開始時間と、最後の行の終了時間をみると、22分ほど処理をしていたことになります。
また、スクリプトエディタで「実行数」を見ると、4回にわけて処理が実行されたことがわかります。
スクリプトエディタの「トリガー」画面にて、トリガーがちゃんと作成されていたことが確認できます。(処理完了後にこの画面を開いたので、各トリガーが無効と表示されています)
トリガーの中身はこんな感じです。時間主導型のトリガーが作成されていました。
なお、この方法の注意事項として、トリガーに関する制限があります。
- トリガーによるスクリプトの合計実行時間は、1日あたり90分(一般)、
または6時間(Workspaceアカウント)まで - 1つのスクリプトに対して設置できるトリガーは、1ユーザーあたり20個まで
1.の制限は、Workspaceアカウントなら6時間もあるので問題はなさそうです。
ただし、同じユーザーが他にも多くのスクリプトをトリガーで実行している場合は注意が必要かもしれません。
2.の制限については対処が必要です。
先述したコードのままだと、トリガーを手動で適宜削除していかないと、そのうち20個の制限を超過してしまいます。
不要になったトリガーをスクリプトで削除する
ということで、実行の都度、不要なトリガーが削除されるスクリプトに変更します。
まずは、トリガーを削除する関数deleteTriggersを追加します。
function deleteTriggers() {
const triggers = ScriptApp.getProjectTriggers();
for (const trigger of triggers) {
// batch を実行するトリガーだった場合は削除する
if (trigger.getHandlerFunction() === "batch") ScriptApp.deleteTrigger(trigger);
}
}
この関数では、batch関数を呼び出すトリガーをすべて削除します。
続いて、batch関数が起動された際にdeleteTriggersを呼び出し、不要なトリガーが削除されるようにします。
function batch() {
const startTime = Date.now();
// 不要なトリガーを削除する
deleteTriggers();
// 100回(2行目から101行目まで)処理する
for (let row = 2; row <= 101; row++) {
// 処理開始から5分以上経過したらトリガーを作って処理を終了
if (Date.now() - startTime > (5 * 60 * 1000)) {
// ... 以下省略 ...
この対応を行うことで、トリガーが20個を超えることによるエラーを防ぐことができるはずです。
ただし、もし毎日夜間に自動起動させたいなどの理由で、メインの関数(この例ではbatch関数)をトリガーで起動させている場合、このコードだと大元のトリガーも削除されてしまいます。
そのような場合は、
例:batch関数を呼び出すexecBach関数を作り、大元のトリガーではexecBach関数を起動する。
という対応で良いと思います。安易で恐縮ですが・・・。
// batch関数を呼び出すだけの関数。大元のトリガーではこの関数を起動する。
function execBatch() {
batch();
}
HtmlServiceを使う方法
次に、HtmlServiceを利用する方法を紹介します。
メリット・デメリットは以下のとおりです。
- 実行時間の制限をほぼ気にしなくて良い
- 並列処理も可能なため、場合によっては処理を大幅に高速化可能
- GASのトリガーによる起動ができないため、自動起動するには他の手段が必要
- 少し覚えることが多い(特に、html, JavaScript未経験者の場合)
最初の例では、batch関数がprocess10sec関数を100回呼び出そうとしていましたので、batch関数の実行時間が制限の6分を超過してしまいました。
この処理を、Google Apps Scriptの外で実行できれば、6分の制限を回避することができます。
HtmlServiceを使ってウェブアプリケーションを作成すれば、先述のbatch関数の処理をGoogle Apps Scriptとしてではなく、ブラウザで動作するJavaScriptとして実行することができます。
よって、Google Apps Scriptとしては、10秒で終わるprocess10secを100回実行するだけなので、実行時間は10秒という扱いになり、制限時間を気にする必要がありません。
・・・ということで、早速作ってみます。
まずは、スクリプトにHTMLファイルindex.htmlを追加します。
これは、ウェブアプリケーションの画面の元となるhtmlファイルです。
下記のとおり、index.htmlのコードに追記します。(色が濃い部分が追記した部分)
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<button onClick="doProcess10sec()">実行</button>
<script>
let row = 2;
function doProcess10sec() {
if (row > 101) return;
// GASの process10sec を実行する(引数は row)。完了したら doProcess10sec を再度実行する
google.script.run.withSuccessHandler(doProcess10sec).process10sec(row);
row += 1;
}
</script>
</body>
</html>
google.script.runは、HtmlServiceで作成したウェブアプリケーションから、GASのコードを実行する関数です。このコードでは、GASのprocess10sec関数に引数rowを渡して実行します。
.withSuccessHandler()は、GASのコードが正常に完了した際に()内の処理が実行されます。
上記コードだと、GASのprocess10sec関数が正常終了したら、html側のdoProcess10sec関数を呼び出すことになります。
次に、コード.gsにdoGet関数を追加します。
// doGet関数をコード.gsに追加する
function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
doGet関数は、ウェブアプリケーションにユーザーがアクセスした時に実行されます。
このスクリプトでは、indexを元にhtmlを構成し、ユーザー(ブラウザ)に返却します。
ここまでの手順で、ウェブアプリケーションの準備ができましたので、デプロイをします。
右上のデプロイボタンより、「新しいデプロイ」をクリック。
「種類の選択」の歯車マークから、「ウェブアプリ」を選択
説明文(任意)を入力し、ウェブアプリの実行ユーザーおよびアクセスできるユーザーを選択したら、デプロイボタンを押す。
デプロイが完了し、ウェブアプリのURLが表示されます。
早速ウェブアプリのURLにアクセスします。
実行ボタンを押すと、スクリプトが実行され、スプレッドシートが更新されていきます。
無事100行分の処理が完了しました。
最初の行の開始時間と、最後の行の終了時間をみると、21分ほど処理をしていたことになりますので、6分の制限による中断は発生しなかったことがわかります。
※ 10秒 × 100行 = 1,000秒なので所要時間は約17分のはずが、21分かかってしまいました。
これは、HTML側からGAS側に処理開始の指示をしたり、GAS側からHTML側に処理完了の応答をしたりするのに1〜2秒ほどかかることが原因だと思います。
並列処理で効率化も可能
これまで紹介したスクリプトでは、1行処理をする → 処理成功 → 次の1行を処理する → 処理成功 → 次の1行…というように、上から順番に1件ずつ処理を行なっていました。
しかし、上から順番に処理をする必要がないケースでは、並列で処理すると一気に効率が上がります。
・・・ということで、html側からGAS側のprocess10sec関数を並列で呼び出すよう修正してみました。
なお、Google Apps Scriptの制約で、1ユーザーにおけるスクリプトの同時実行数は30までとされているようですので、30並列で動くようにします。
※ ちなみに、理由はわかりませんが、60並列まではエラーなく実行でき、61並列以上にするとエラーが発生しました。が、サンプルコードは一応30並列にしておきます。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<button onClick="doProcess10sec()">実行</button>
<button onClick="runInParallel()">並列実行</button>
<script>
let row = 2;
const limit = 30;
function runInParallel() {
for (let i = 1; i <= limit; i++) {
doProcess10sec();
}
}
function doProcess10sec() {
if (row > 101) return;
// GASの process10sec を実行する(引数は row)。完了したら doProcess10sec を再度実行する
google.script.run.withSuccessHandler(doProcess10sec).process10sec(row);
row += 1;
}
</script>
</body>
</html>
色付きの行が今回修正した箇所です。
コードの修正が完了したら、以下の手順でウェブアプリのバージョンを更新してください。
デプロイボタン → デプロイを管理 → 鉛筆マーク(編集) → バージョンのプルダウンは新バージョンを選択 → デプロイボタン
表示されるURLからウェブアプリにアクセスすると、並列実行のボタンが増えているので、それをクリックします。
無事100行分の処理が終了していました。
処理時間を見ると、約46秒ほどで100件を処理しています。
1件ずつ処理した時は約21分かかっていたので、(ほぼ)並列で動かした分だけ(30倍近く)速くなったと言えるのではないでしょうか。
※ なお、並列に処理しているので、必ずしも上にある行の方が先に処理完了しているとは限りません。
補足:行数などを動的に指定する
ここまでのコードでは、処理対象行が 2行目 〜 101行目 までと決めうちになっていました。
しかし、実際の業務等では、行数が決まっていないケースも多いと思います。
その対応の例として、html側コードで if (row > 101) return と決め打ちで101にしている部分を、スプレッドシートの最終行が指定されるよう修正をします。
まず、GAS側のdoGet関数を変更します。
function doGet() {
const template = HtmlService.createTemplateFromFile('index');
template.lastRow = sheet.getLastRow();
return template.evaluate();
}
今までは、index.htmlの内容をそのまま返していたので、createHtmlOutputFromFileを使っていました。
今回は、index.htmlの内容をスクリプトで動的に変更するので、createTemplateFromFileを使います。
- createTemplateFromFileでhtmlのテンプレートを作り、
- テンプレートに受け渡す変数lastRowを定義、シートの最終行番号をセットし、
- テンプレートを元にhtmlを作って返す
・・・というイメージです。
index.htmlのコードは下記のように変更し、GAS側コードで定義したlastRowの値を受け取るようにします。
<script>
let row = 2;
const lastRow = <?!= lastRow ?>;
const limit = 30;
function runInParallel() {
for (let i = 1; i <= limit; i++) {
doProcess10sec();
}
}
function doProcess10sec() {
if (row > lastRow) return;
// GASの process10sec を実行する(引数は row)。完了したら doProcess10sec を再度実行する
google.script.run.withSuccessHandler(doProcess10sec).process10sec(row);
row += 1;
}
</script>
この方法で、スプレッドシートの内容などに応じて、ループの回数などを動的に設定することができます。
補足:夜間などに自動で起動させる
ここまで説明したHtmlServiceを使う方法では、
①ウェブアプリのURLにアクセスし、②実行ボタンを押す という人間による作業が必要でした。
そのため、このままでは夜間などに自動的に処理を実行させることができません。
力技ですが、常時起動させっぱなしのPCを1台用意して、所定の時間になったらウェブアプリを自動的に開くようにすれば対処できるかと思います。
なお、②の実行ボタンをPCに自動押させるのはやや難しいので、まずはスクリプトを修正し、ページを開いたらスクリプトが実行されるように修正します。
<script>
let row = 2;
const lastRow = <?!= lastRow ?>;
const limit = 30;
function runInParallel() {
for (let i = 1; i <= limit; i++) {
doProcess10sec();
}
}
function doProcess10sec() {
if (row > lastRow) return;
// GASの process10sec を実行する(引数は row)。完了したら doProcess10sec を再度実行する
google.script.run.withSuccessHandler(doProcess10sec).process10sec(row);
row += 1;
}
runInParallel();
</script>
1行追加するだけです。これで、ウェブアプリにアクセスしたら、すぐにrunInParallel関数が実行されるようになりました。
あとは、WindowsならタスクスケジューラーでURLを開く、Macならカレンダーアプリでweblocを開くなど、標準機能で、所定の時間になったらこのウェブアプリをブラウザで開くように設定すればOKだと思います。
以下は参考サイトです。
Windowsのタスクスケジューラーで実行する場合の参考サイト
Macのカレンダーアプリで実行する場合の参考サイト
コメント