Googleフォーム回答の二重処理を通知前に発見|GASで回答行をdry-run監査
ツールおじ|GoogleアカウントをAIエージェント化
フォーム回答の通知を自動化したあとで困りやすいのが、同じ内容を二度処理したり、必須項目が空のまま担当者へ回したりする事故です。
この記事では、回答シートを変更せずに、入力不備と重複候補だけを確認するGASを紹介します。通知やメール送信は行わず、問題のある行番号と理由だけをログへ出す無料の安全確認用コードです。
先に知っておきたいこと
- Googleフォームと回答先スプレッドシートが連携済みであることを前提にします。
- 回答内容、氏名、メールアドレスはログへ出しません。
- シートへの書き込み、通知、メール送信、行削除は行いません。
- 重複判定はタイムスタンプ、メールアドレス、お問い合わせ内容の完全一致候補です。別表記や意味の近い回答までは判定しません。
- 大量データを一度に読まないよう、初期設定では回答500行を上限にします。
準備する回答シート
シート名は「フォームの回答 1」、1行目には次の見出しを用意します。
- タイムスタンプ
- メールアドレス
- お問い合わせ内容
実際のフォームで見出し名が異なる場合は、コード先頭の設定だけを変更してください。まず個人情報を含まないテスト回答を3件ほど用意し、正常1件、必須項目が空の1件、同内容の重複候補1件で確認すると安全です。
コピーして使える最小コード
function auditFormResponseRowsFree() {
const CONFIG = {
DRY_RUN: true,
SHEET_NAME: 'フォームの回答 1',
MAX_ROWS: 500,
REQUIRED_HEADERS: ['タイムスタンプ', 'メールアドレス', 'お問い合わせ内容']
};
if (CONFIG.DRY_RUN !== true) {
throw new Error('この無料例はDRY_RUN=trueのまま使用してください。');
}
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) {
throw new Error('対象シートが見つかりません。シート名を確認してください。');
}
const lastRow = sheet.getLastRow();
const lastColumn = sheet.getLastColumn();
if (lastRow < 2 || lastColumn < 1) {
console.log(JSON.stringify({ dryRun: true, checkedRows: 0, issueCount: 0 }));
return;
}
if (lastRow - 1 > CONFIG.MAX_ROWS) {
throw new Error('回答行が上限を超えています。期間を分けて確認してください。');
}
const values = sheet.getRange(1, 1, lastRow, lastColumn).getDisplayValues();
const headers = values[0].map(function (value) {
return String(value || '').trim();
});
const indexes = {};
CONFIG.REQUIRED_HEADERS.forEach(function (header) {
const index = headers.indexOf(header);
if (index < 0) {
throw new Error('必須見出しが見つかりません: ' + header);
}
indexes[header] = index;
});
const seen = {};
const issues = [];
values.slice(1).forEach(function (row, offset) {
const rowNumber = offset + 2;
const timestamp = String(row[indexes['タイムスタンプ']] || '').trim();
const email = String(row[indexes['メールアドレス']] || '').trim().toLowerCase();
const message = String(row[indexes['お問い合わせ内容']] || '').trim();
const rowIssues = [];
if (!timestamp) rowIssues.push('TIMESTAMP_EMPTY');
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) rowIssues.push('EMAIL_INVALID');
if (!message) rowIssues.push('MESSAGE_EMPTY');
if (timestamp && email && message) {
const rawKey = [timestamp, email, message].join('\u001f');
const digest = Utilities.computeDigest(
Utilities.DigestAlgorithm.SHA_256,
rawKey,
Utilities.Charset.UTF_8
);
const fingerprint = digest.map(function (byte) {
return ('0' + ((byte + 256) % 256).toString(16)).slice(-2);
}).join('');
if (seen[fingerprint]) {
rowIssues.push('DUPLICATE_OF_ROW_' + seen[fingerprint]);
} else {
seen[fingerprint] = rowNumber;
}
}
if (rowIssues.length) {
issues.push({ row: rowNumber, issues: rowIssues });
}
});
const result = {
dryRun: true,
checkedRows: values.length - 1,
issueCount: issues.length,
issueRows: issues.slice(0, 50),
truncated: issues.length > 50
};
console.log(JSON.stringify(result));
return result;
}実行前の準備
1. 回答先スプレッドシートのコピーを作り、個人情報を含まないテスト回答を用意します。
2. 拡張機能からApps Scriptを開き、コードを貼り付けて保存します。
3. シート名と3つの見出し名を実物に合わせます。
4. auditFormResponseRowsFreeを選び、少量のテスト回答で実行します。
5. 実行ログのcheckedRows、issueCount、issueRowsを確認します。
6. 問題行を人が確認し、修正後にもう一度実行します。
ログの読み方
- TIMESTAMP_EMPTY: タイムスタンプが空です。
- EMAIL_INVALID: メールアドレスが空、または基本形式に合いません。
- MESSAGE_EMPTY: お問い合わせ内容が空です。
- DUPLICATE_OF_ROW_数字: 先に出た行と3項目が完全一致しています。
ログには行番号と理由だけが出ます。回答本文やメールアドレスをログへ残さないため、問題の詳細は権限のある担当者が元シートで確認してください。
通知を有効にする前のチェックリスト
- 回答1件につき処理記録を1件だけ残せるか確認する。
- 同じ回答を再処理するときの担当者ルールを決める。
- 通知先へ送る項目を必要最小限にする。
- 実在する回答を使う前に、テスト用コピーで確認する。
- トリガーの所有者と停止方法を記録する。
- エラー時に自動再送せず、人がログを確認できるようにする。
制限と安全上の注意
このコードは重複候補を検出するだけで、回答の正しさ、本人性、契約成立、注文確定などを判断しません。タイムスタンプや文章が少しでも異なる回答は別件として扱います。
通知や外部サービス連携を追加する場合は、送信先、個人情報、再送条件、実行上限を別途確認してください。最初からフォーム送信トリガーへつながず、手動実行のdry-run結果を確認してから設計を進めるのが安全です。
まとめ
フォーム回答の自動通知は便利ですが、その前に入力不備と二重処理候補を見つけられる状態を作ることが大切です。まずは読み取り専用の監査を実行し、問題行を人が確認できる運用から始めてください。
注文フォームの回答を、入金・発送状況まで整理したい方へ
個人販売の注文・発送管理GAS Proを見る

GasおじのTips商品一覧を見る
GasおじのTips商品一覧を見る

※このコードは学習・検証用の例です。実データへ適用する前にコピー環境で確認してください。Google公式・Google公認の商品ではありません。
