Blog スタッフブログ

システム開発

[GAS]GoogleAppsScriptの書き出し処理でタイムアウト回避

こんにちは、株式会社MIXシステム開発担当のBloomです。

今回はGoogle Apps Script(GAS)で大量のファイルを生成する際などに行き当たる実行時間制限を回避する方法について、

お仕事の中で得た知見を共有させていただきます。

GASの実行時間制限

GASには一度のスクリプト実行において6分の実行時間の制限があります。簡単な処理ではなかなかこの制限には引っかかりませんが、ファイル書き出しや外部API通信が含まれる処理を実行する際に制限に引っかかってしまい一括処理ができなくなる場合があります。

今回はこの実行時間制限を回避しながらなるべく一括で処理したい時の対処法を紹介します。

スクリプトを分割実行する

例えばスプレッドシートに1000行のデータがあり、それぞれの行から1ファイルずつGoogleドライブへ出力する処理を考えてみます。

values.forEach((row) => {
  folder.createFile(row[0] + '.txt', row[1]);
});

このような処理でもファイル生成には少し時間がかかるため、件数が増えていくと途中で処理が終了してしまう場合があります。そのため、大量のデータを扱う場合は一度のスクリプト実行でですべて処理するのではなく、処理を複数回に分割する方法が有効です。今回は1000行を一度に処理せず100行ずつ処理させる例を掲載します。

const SHEET_NAME = 'シート1';
const OUTPUT_FOLDER_ID = '出力先フォルダID';
const BATCH_SIZE = 100;

function startExport() {
  const properties = PropertiesService.getScriptProperties();

  // 2行目、へっだ列の次の列から処理開始
  properties.setProperty('NEXT_ROW', '2');

  exportFiles();
}

function exportFiles() {
  const properties = PropertiesService.getScriptProperties();

  const sheet = SpreadsheetApp
    .getActiveSpreadsheet()
    .getSheetByName(SHEET_NAME);

  const folder = DriveApp.getFolderById(OUTPUT_FOLDER_ID);

  const lastRow = sheet.getLastRow();
  const lastColumn = sheet.getLastColumn();

  const startRow = Number(
    properties.getProperty('NEXT_ROW') || 2
  );

  if (startRow > lastRow) {
    properties.deleteProperty('NEXT_ROW');
    return;
  }

  const endRow = Math.min(
    startRow + BATCH_SIZE - 1,
    lastRow
  );

  const values = sheet
    .getRange(
      startRow,
      1,
      endRow - startRow + 1,
      lastColumn
    )
    .getValues();

  values.forEach((row, index) => {
    const rowNumber = startRow + index;

    const fileName = `${rowNumber}.txt`;
    const fileContent = row.join(',');

    folder.createFile(
      fileName,
      fileContent,
      MimeType.PLAIN_TEXT
    );
  });

  const nextRow = endRow + 1;

  if (nextRow <= lastRow) {
    properties.setProperty(
      'NEXT_ROW',
      String(nextRow)
    );

    createNextTrigger();
  } else {
    properties.deleteProperty('NEXT_ROW');
  }
}

ここではPropertiesServiceへ次に処理する行番号を保存しています。1回目で2~101行目を処理した場合は、次回開始位置として102が保存される形です。

続いて、次の100件を自動的に処理するためのトリガーを作成します。

function createNextTrigger() {
  ScriptApp.newTrigger('exportFiles')
    .timeBased()
    .after(60 * 1000)
    .create();
}

これで100件の処理が完了すると、約1分後に再びexportFiles()が実行されます。1000件の場合は、1~100件、101~200件、201~300件・・・901~1000件と処理を分割できるため、1回の実行時間制限に引っかかりにくくなります。

注意点

注意点として、トリガーの重複に注意しましょう。上記のコード例では何度もstartExport()を実行した場合などに、同じトリガーが複数作成される可能性があります。実際に利用する場合は、既存のトリガーを削除してから作成しておくと安全です。

function createNextTrigger() {
  ScriptApp.getProjectTriggers().forEach((trigger) => {
    if (
      trigger.getHandlerFunction() === 'exportFiles'
    ) {
      ScriptApp.deleteTrigger(trigger);
    }
  });

  ScriptApp.newTrigger('exportFiles')
    .timeBased()
    .after(60 * 1000)
    .create();
}

これで常に次回実行用のトリガーが1つだけ存在する状態になります。これだけで長いファイルを1操作で一括書き出しができるようになりました。良かったですね。