GASでスプレッドシートの
変更履歴を自動で記録する方法

複数人で共有するシートで「いつ・誰が・どのセルを・何から何に変えたか」を、onEditトリガー(セル編集をきっかけに自動実行される仕組み)で別シートに自動記録する方法を、動くコードで解説します。

|対象: GAS / スプレッドシート / onEdit

Table of Contents

変更履歴を自動記録するメリット

Googleスプレッドシートにも「変更履歴」機能はありますが、セル単位で誰がいつ何を変えたかを一覧で追ったり、 特定の列だけを抜き出して集計したりするのは苦手です。GASで独自の履歴シートを作れば、必要な情報だけを表形式で残せます。

誤変更の原因追跡

金額や在庫数が想定外の値になったとき、いつ誰が変えたかをすぐ特定できる

共同編集の可視化

複数人で触るシートで、編集の責任範囲やタイミングを明確にできる

重要セルの監視

単価・締切など重要な列の変更だけを抜き出して通知や集計に使える

運用ログの蓄積

表形式で残るので、フィルタや関数でそのまま分析・レポート化できる

onEditで変更履歴を残す基本コード

まずは一番シンプルな形です。関数名をonEditにしておくと、セルを編集するたびにGASが自動で実行されます。引数e(イベントオブジェクト)には、編集された範囲や変更前後の値が入っています。

// 簡易トリガー: 関数名を onEdit にするだけで、編集のたびに自動実行される
function onEdit(e) {
  if (!e || !e.range) return;

  const sheet = e.range.getSheet();
  if (sheet.getName() === "変更履歴") return; // ログシート自身の編集は無視

  const logSheet = getOrCreateLogSheet(e.source);
  logSheet.appendRow([
    new Date(),                 // 日時
    sheet.getName(),            // シート名
    e.range.getA1Notation(),    // セル(例: B3)
    e.oldValue == null ? "" : e.oldValue, // 変更前
    e.value == null ? "" : e.value,       // 変更後
  ]);
}

function getOrCreateLogSheet(ss) {
  let logSheet = ss.getSheetByName("変更履歴");
  if (!logSheet) {
    logSheet = ss.insertSheet("変更履歴");
    logSheet.appendRow(["日時", "シート名", "セル", "変更前", "変更後"]);
  }
  return logSheet;
}

e.oldValueは変更前の値、e.valueは変更後の値です。値が空のときにundefinedになることがあるため、nullチェックをして空文字に置き換えています。

簡易トリガーとインストール型トリガーの違い

基本コードのonEditは「簡易トリガー」と呼ばれ、設定なしで動きますが権限に強い制限があります。次の違いを押さえておきましょう。

簡易トリガー(onEdit)

設定不要ですぐ動く。ただしメール送信や他ファイルへのアクセスなど、権限が必要な処理はできない。編集者のメール取得も基本的に空になる。

インストール型トリガー

ScriptApp.newTriggerで作成する。作成者の権限で動くため、メール送信・編集者の取得・外部連携などができる。関数名は自由(例: onEditLogger)。

変更履歴を残すだけなら簡易トリガーでも十分です。ただし「誰が編集したか」を記録したい、変更をメールやSlackに通知したい、という場合はインストール型トリガーが必要になります。以降のコードはインストール型を前提に、関数名をonEditLoggerに変えて進めます。

編集者のメールアドレスを記録する

「誰が変えたか」を残すと履歴の価値が一気に上がります。同じGoogle Workspace(組織)内であれば、Session.getActiveUser().getEmail()で編集者のメールを取得できることが多いです。組織外や共有設定によっては空になるため、取得できないケースも想定しておきます。

// インストール型トリガーから実行する想定の関数
function onEditLogger(e) {
  if (!e || !e.range) return;

  const ss = e.source;
  const sheet = e.range.getSheet();
  if (sheet.getName() === "変更履歴") return;

  const logSheet = getOrCreateLogSheet(ss);
  logSheet.appendRow([
    new Date(),
    getEditorEmail(e),          // 編集者のメールアドレス
    sheet.getName(),
    e.range.getA1Notation(),
    e.oldValue == null ? "" : e.oldValue,
    e.value == null ? "" : e.value,
  ]);
}

function getEditorEmail(e) {
  try {
    // 同一組織内なら取得できることが多い。取れない場合は空文字になる
    const email = (e.user && e.user.getEmail())
      ? e.user.getEmail()
      : Session.getActiveUser().getEmail();
    return email || "(不明)";
  } catch (err) {
    return "(取得不可)";
  }
}

取得可否は環境に依存します。空になる場合があることを前提に、"(不明)"を入れて履歴の行自体は必ず残す設計にしておくと、後から追跡しやすくなります。

複数セルの貼り付けに対応する

ここが実務で一番つまずくポイントです。e.oldValuee.valueは「1つのセルを編集したとき」だけ値が入ります。コピー&ペーストで複数セルをまとめて変更すると、これらは空になります。 そのため、範囲(e.range)をループして現在の値を1セルずつ記録します。

function onEditLogger(e) {
  if (!e || !e.range) return;

  const range = e.range;
  const sheet = range.getSheet();
  if (sheet.getName() === "変更履歴") return;

  const logSheet = getOrCreateLogSheet(e.source);
  const now = new Date();
  const email = getEditorEmail(e);

  const numRows = range.getNumRows();
  const numCols = range.getNumColumns();

  // 単一セルの編集: oldValue / value がそのまま使える
  if (numRows === 1 && numCols === 1) {
    logSheet.appendRow([
      now, email, sheet.getName(), range.getA1Notation(),
      e.oldValue == null ? "" : e.oldValue,
      e.value == null ? "" : e.value,
    ]);
    return;
  }

  // 複数セル(貼り付け等): oldValue は取得できないため現在値のみ記録
  const values = range.getValues();
  const rows = [];
  for (let r = 0; r < numRows; r++) {
    for (let c = 0; c < numCols; c++) {
      rows.push([
        now, email, sheet.getName(),
        range.getCell(r + 1, c + 1).getA1Notation(),
        "(複数セル編集のため取得不可)",
        values[r][c],
      ]);
    }
  }
  if (rows.length > 0) {
    logSheet.getRange(logSheet.getLastRow() + 1, 1, rows.length, 6).setValues(rows);
  }
}

複数セル編集では変更前の値を取得できないため、その旨を正直に記録に残しています。1行ずつappendRowすると遅くなるので、まとめてsetValuesで書き込むのがポイントです。

特定シート・特定列だけを記録する

すべての編集を記録すると履歴が膨大になります。重要なシートや列だけに絞るには、 編集された場所を判定して対象外なら早めにreturnします。監視条件を1か所の設定オブジェクトにまとめておくと、後からの調整が楽になります。

// 監視したいシートと列を1か所にまとめる
// 値の配列が空なら「全列を記録」、番号を入れると「その列だけ記録」(1始まり)
const WATCH_CONFIG = {
  "受注データ": [3, 5], // C列・E列だけ記録
  "顧客マスタ": [],     // 全列を記録
};

function onEditLogger(e) {
  if (!e || !e.range) return;

  const sheet = e.range.getSheet();
  const config = WATCH_CONFIG[sheet.getName()];
  if (config === undefined) return; // 監視対象外のシートは無視

  const col = e.range.getColumn();
  if (config.length > 0 && config.indexOf(col) === -1) return; // 対象外の列は無視

  const logSheet = getOrCreateLogSheet(e.source);
  logSheet.appendRow([
    new Date(),
    getEditorEmail(e),
    sheet.getName(),
    e.range.getA1Notation(),
    e.oldValue == null ? "" : e.oldValue,
    e.value == null ? "" : e.value,
  ]);
}

トリガーをコードで設定する

インストール型トリガーは、次の関数を一度だけ手動実行すれば登録できます。実行時に権限の承認を求められるので許可してください。 二重登録を防ぐため、既存の同名トリガーを削除してから作り直す形にしています。

// この関数を一度だけ手動実行すると、インストール型トリガーが登録される
function createOnEditTrigger() {
  const ss = SpreadsheetApp.getActive();

  // 二重登録を防ぐため、既存の同名トリガーを先に削除する
  ScriptApp.getProjectTriggers().forEach((t) => {
    if (t.getHandlerFunction() === "onEditLogger") {
      ScriptApp.deleteTrigger(t);
    }
  });

  ScriptApp.newTrigger("onEditLogger")
    .forSpreadsheet(ss)
    .onEdit()
    .create();
}

実務での注意点

1. onEditは「セル値の編集」でしか発火しない

行や列の挿入・削除、書式変更、スクリプトによるsetValueでの書き込みではonEditは動きません。プログラムからの変更も記録したい場合は、書き込み側で明示的にログ関数を呼ぶ必要があります。

2. 処理は軽く保つ

onEditは編集のたびに動くため、重い処理を入れると編集の反応が鈍くなります。履歴の追記のように短時間で終わる処理に絞り、集計や通知はまとめて別トリガーで行うと安定します。

3. 履歴シートの肥大化に備える

長く運用すると行数が増え続けます。月ごとにシートを分ける、一定行数を超えたら古い行をアーカイブする、といった仕組みを時間主導トリガーで回しておくと、動作が重くなるのを防げます。

まとめ

GASのonEditトリガーを使えば、スプレッドシートの変更履歴を「いつ・誰が・どのセルを・変更前後」まで自分専用の履歴シートに自動記録できます。 誰が編集したかを残すならインストール型トリガー、複数セルの貼り付けには範囲ループ、履歴を絞るなら早期returnと、 つまずきやすいポイントを押さえれば実務でそのまま使えます。まずは基本コードから始めて、必要な機能を足していきましょう。

よくある質問

簡易トリガー(関数名をonEditにするだけで動くもの)は設定不要で手軽ですが、権限が制限され、他ファイルへのアクセスやメール送信ができません。編集者のメールアドレス取得やGmail通知など権限が必要な処理を行いたい場合は、ScriptApp.newTriggerで作るインストール型トリガーを使います。

同じGoogle Workspace(組織)内であれば、インストール型トリガーからSession.getActiveUser().getEmail()やe.user.getEmail()で取得できることが多いです。ただし組織外のアカウントや共有設定によっては空になる場合があります。取得できないケースを想定し、空のときは「(不明)」などを記録しておくと安全です。

記録できますが、注意点があります。onEditのイベントオブジェクトのe.oldValueとe.valueは単一セルを編集したときだけ入ります。複数セルの貼り付けではこれらが空になるため、e.rangeの範囲をループして現在値を記録します。変更前の値は取得できないため、その旨を記録に残すのが現実的です。

できます。イベントオブジェクトのe.range.getSheet().getName()で編集されたシート名を、e.range.getColumn()で列番号を判定し、対象外なら早期にreturnして記録をスキップします。監視対象を設定オブジェクトにまとめておくと、シートや列が増えても管理しやすくなります。

長期運用では行数が増えるため、月ごとにシートを分ける、一定行数を超えたら古い行を削除・アーカイブする、といった運用を組み込むと安定します。時間主導トリガーで毎月1日に古い履歴を別シートへ退避させる関数を回すのがおすすめです。

GAS開発・業務Webシステムを
相談する。

変更履歴の記録以外にも、フォーム連携・帳票出力・Gmail自動化など、日々の業務を自動化するGAS開発をご相談いただけます。

GAS Webシステム開発を見る