GASで全シートをループ処理する方法

GAS

※本記事はプロモーションを含みます

目次

Google スプレッドシートを運用していると、月ごとに「4月」「5月」「6月」とシートが複製されていったり、拠点や担当者ごとに同じフォーマットのシートが数十枚並んだりする状況によく直面します。作業者はこれらすべてのシートに対して特定のセルに入っている売上合計を1枚のサマリーシートに抜き出したいときや、全シートのフォントや列幅を一括で統一したいとき、シートを1枚ずつ手作業でめくって作業するのは現実的ではありません。なので Google Apps Script (GAS) を使って自動化を図ることになりますが、作業者が単にシートをループさせるだけでも、思わぬ落とし穴に突き当たることが多々あります。

すべてのシートを取り出して順に処理する基本構造

作業者がGASでスプレッドシート内のすべてのシートを巡回したい場合、まずシート全体のリストを配列として取得し、それを走査していく形をとります。この処理は、定例の月次集計作業や、全社共通のフォーマット改訂などで「GAS 複数シート ループ」を実装する際の土台となるものです。

実務でこの処理をいつ使うかというと、たとえばスプレッドシート全体を対象としたデータクレンジングや、各シートに散らばった特定項目の抽出を行うタイミングです。ただし、作業者が何も考えずにループを回すと、処理の対象にしたくないシートまで巻き込んで更新してしまったり、想定外のインデックスずれが発生したりすることがあります。というのは、そうした予期せぬズレを避けるためには、まず現在開いているスプレッドシートから全シートを安全に取り出し、中身の名前を1つずつ確認できる最小のコードを把握しておく必要があるからです。処理を確認しましょう。

const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheets = ss.getSheets();
for(let i = 0; i < sheets.length; i++) {
  Logger.log(sheets[i].getName());
}

上記のコードは、アクティブなスプレッドシート内にあるすべてのシートオブジェクトを取得し、その名前をログに書き出すもっとも簡潔な処理です。ここで使っている getSheets() は、スプレッドシートに含まれるすべてのシートを、画面上のタブの並び順(左から右)に従った配列として返します。返される配列の各要素は Sheet クラスのインスタンスであり、通常の配列と同じように sheets[0] で一番左のシートにアクセスできます。

スクリプトはループ構文としては伝統的な for 文を採用していますが、これによってインデックス変数 i を用いた柔軟な制御が可能になります。ただし sheets.forEach(sheet => { ... }) という書き方でも同様の巡回は行えますが、途中で特定のシートが見つかった段階で処理を抜ける(break を使う)場合や、あえて逆順(右側のシートから左側へ)に走査したい要件がある現場では、通常の for 文で書いておくほうが調整しやすい場面が多くあります。コードを確認しましょう。

作業者がこの基本コードを実行すると、コンソールには左端のシートから右端のシートまでの名称が順番に出力されます。ただし、実際の業務シートでは「すべてのシートを無条件に同じように扱う」ということは稀です。というのは、目に見えているシートのほかに、過去の参照用データや計算用のシートが隠されていたり、集計結果を書き込むための親シートが存在していたりするためです。

非表示シートが混入して計算がずれる問題と除外処理

全シートをループ処理する際に現場で最も頻発するトラブルが、「非表示にしてあるシートまで処理対象に含まれてしまい、集計結果の数値がおかしくなる」という現象です。getSheets() の仕様として、ファイル内に存在するすべてのシートを取得するため、UI上で非表示(「シートを非表示」に設定された状態)になっているシートであっても、配列の中にそのまま格納されて返却されます。

たとえば、過去のテストデータが入った非表示シートや、関数で参照するためだけに一時的に隠してあるワークシートが存在する場合、作業者が上記の基本ループをそのまま走らせてセルの数値を足し上げると、本来合算してはいけない数値まで集計用シートに加算されてしまいます。なので、全シートの特定セルを一括置換するようなマクロを動かした際、隠してあったマスターシートの構造まで破壊してしまう危険があります。

このズレを回避するためには、作業者はループの中でシートが非表示状態であるかどうかを判定し、非表示であればスキップする処理を挟まなければなりません。GASにはそのためのメソッドとして isSheetHidden() が用意されています。プログラムが各シートオブジェクトに対して sheets[i].isSheetHidden() を呼び出すと、そのシートが隠されている場合は true、表示されている場合は false が返されます。

作業者はこれを利用して、ループの冒頭に除外判定を設けます。条件を確認しましょう。

const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheets = ss.getSheets();

for (let i = 0; i < sheets.length; i++) {
  const sheet = sheets[i];
  
  // 非表示シートはスキップする
  if (sheet.isSheetHidden()) {
    continue;
  }
  
  // ここに表示シートのみに対する処理を書く
  Logger.log('処理対象シート: ' + sheet.getName());
}

作業者がこのように書くことで、画面上に見えている通常業務のシートだけを安全に処理できるようになります。なので、現場の運用では「非表示シート」だけでなく、「集計シート自身」や「テンプレート用シート」も処理から除外したい要件がほぼ確実に出てきます。ただし、そうした場合はシート名を用いた判定も併用します。

たとえば「まとめ」「Template」「設定」といった名前のシート、あるいは先頭が「_(アンダースコア)」で始まる管理用シートをスキップしたい場合は、作業者は以下のように条件を追加して対応します。記述を確認しましょう。

const EXCLUDE_NAMES = ['まとめ', 'Template', '設定'];

for (let i = 0; i < sheets.length; i++) {
  const sheet = sheets[i];
  const sheetName = sheet.getName();
  
  if (sheet.isSheetHidden() || EXCLUDE_NAMES.includes(sheetName)) {
    continue;
  }
  
  // 実際の業務データシートのみを処理
}

業務用のスプレッドシートは自分一人だけが触るものとは限らず、他のメンバーが勝手に作業用の一時シートを増やしたり非表示にしたりすることが日常茶飯事です。なので、作業者がループ処理を組む際は、最初から「余計なシートが存在することを前提」としたガード節を入れておくことが極めて重要です。コードを確認しましょう。

全シート巡回時に陥りやすい実行時間超過とAPI呼び出しの最適化

次に問題となるのが、シート数が20枚、30枚と増えていった際の処理速度の著しい低下と、GASの6分間実行時間制限(Google Workspace の一部でも同様の制約)による途中停止です。

「全シートのA1セルの値を取得して集計する」「全シートの1行目を太字にして背景色を変える」といった処理を行う際、作業者がループの中で愚直に SpreadsheetApp の各種操作メソッドを何度も呼び出していると、あっという間に実行制限に引っかかります。というのは、GASにおいて SpreadsheetApp や Sheet クラスのメソッドを呼び出す処理は、Google のサーバー側と通信を行うオーバーヘッドが大きいため、ループの内側で細かい読み書きを繰り返すと極端に遅くなるからです。

たとえば、各シートから複数行の明細データを抜き出して1枚のシートに統合するような業務では、作業者はシートごとに getRange() や getValue() を個別に呼ぶのではなく、シートごとの範囲データを一括で二次元配列として取得(getValues())し、メモリ上で JavaScript の配列操作を行ってから、最後に一度だけ書き込む設計にする必要があります。構築手順を確認しましょう。

もし作業者が各シートの最終行までデータを取り出して合算したいのであれば、以下のようにメモリ上で集計を進める構造をとるのが確実です。実装を確認しましょう。

const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheets = ss.getSheets();
const allData = [];

for (let i = 0; i < sheets.length; i++) {
  const sheet = sheets[i];
  if (sheet.isSheetHidden() || sheet.getName() === '統合データ') {
    continue;
  }
  
  const lastRow = sheet.getLastRow();
  if (lastRow < 2) {
    // ヘッダー行しかない、または空のシートはスキップ
    continue;
  }
  
  // 1回の呼び出しでシート内のデータ行全体を取得
  const values = sheet.getRange(2, 1, lastRow - 1, sheet.getLastColumn()).getValues();
  
  // メモリ上で配列に結合していく
  for (let j = 0; j < values.length; j++) {
    allData.push(values[j]);
  }
}

// 最後に集計先シートへ一括書き込み
if (allData.length > 0) {
  const targetSheet = ss.getSheetByName('統合データ');
  targetSheet.getRange(2, 1, allData.length, allData[0].length).setValues(allData);
}

作業者がこの書き方を採用すれば、シート数が50枚あっても、各シートに対するAPI呼び出しは getLastRow()、getLastColumn()、getRange().getValues() の数回だけに抑えられます。なので、ループの内部でセルごとに書き込みを行うような書き方と比べると、所要時間は数十倍単位で変わってきます。作業者が全シートを巡回するスクリプトを組むときは、「シートを行き来する回数」と「セルを操作する回数」を極限まで減らし、データは一度配列に落とし込んでから処理するという意識を持つことが、処理落ちを防ぐことにつながります。処理を確認しましょう。

ループ処理中にシート自体の追加や削除を行う場合のトラップ

作業者が全シートを巡回しながら「古い不要なシートを削除する」「条件に一致するシートを別名で複製する」といった、シートの構造そのものを変更する処理を行う場合、さらなる罠が存在します。というのは、配列のインデックスと実際のシート一覧の同期ズレが発生するからです。

冒頭のコードで示したように、const sheets = ss.getSheets(); を実行した瞬間、その時点でのシート一覧が静的な配列として変数 sheets に格納されます。ただし、その後でループの中で ss.deleteSheet(sheets[i]) を呼び出してシートを削除すると、スプレッドシート上からはシートが1枚消えますが、手元の sheets 配列の要素数はそのまま残ります。

この状態で作業者が通常の for (let i = 0; i < sheets.length; i++) を回し続けるとどうなるでしょうか。たとえばインデックス 0 のシートを削除した場合、本来インデックス 1 にあったシートがスプレッドシート上では先頭(0番目)に繰り上がります。なので、ループの次の周回では i = 1 として処理が進むため、繰り上がってきたシートが丸ごとスキップされてしまい、意図しないシートが処理をすり抜けることになります。

タイトルとURLをコピーしました