積読・動画リストのカテゴリ表記ゆれを集計前に発見|GASでdry-run監査
ツールおじ|GoogleアカウントをAIエージェント化
積読リストを見返したとき、「本」「書籍」「BOOK」が別カテゴリとして集計され、件数が合わなくなったことはありませんか。表記ゆれを直そうとして一括置換すると、意図しないセルまで変えてしまう不安もあります。
この記事では、Googleスプレッドシートのカテゴリ列を読み取り、正規化した場合の候補を別シートへ出す無料のGAS例を紹介します。元の行は更新せず、自動削除・自動置換も行いません。 まず候補だけを確認し、人が修正対象を決めるためのdry-run監査です。
このサンプルでできること・できないこと
- 前後の空白、連続する空白、全角・半角、英字の大文字・小文字を比較用にそろえます。
- 「本」「書籍」「book」を `BOOK`、「動画」「video」を `VIDEO` のように候補へまとめます。
- 元の `Watchlist` シートは読み取りだけにし、セル、行、並び順を変更しません。
- 確認結果は `CategoryAudit` シートへ追記します。
- タイトル本文やURLは監査ログへ残しません。ログに出すのは行番号、カテゴリ候補、判定だけです。
- 内容を読んでカテゴリを推測したり、外部サイトへアクセスしたりはしません。
- 修正・削除・統合は自動では行いません。最終判断は人が行います。
カテゴリ名は組織や個人の運用ルールによって異なります。この記事の候補は例なので、実データへ使う前に `ALIASES` と `ALLOWED_CATEGORIES` を自分の分類へ合わせてください。
準備するシート
スプレッドシートに `Watchlist` シートを作り、1行目へ次の列を用意します。
- タイトル
- カテゴリ
- 状態
- 次に見る日
テスト用コピーへ、カテゴリが `本`、` 書籍 `、`BOOK`、`動画`、`Video`、空欄の行を数件だけ入れます。タイトルは架空の文字列で構いません。実際の読書メモや個人情報を入れずに動作を確認できます。
監査結果は `CategoryAudit` へ出力されます。見出しはスクリプトが初回だけ作成します。
コピーして使える最小コード
const CATEGORY_AUDIT_CONFIG = Object.freeze({
DRY_RUN: true,
SOURCE_SHEET: 'Watchlist',
AUDIT_SHEET: 'CategoryAudit',
MAX_ROWS: 500,
ALLOWED_CATEGORIES: ['BOOK', 'VIDEO', 'ARTICLE', 'COURSE'],
ALIASES: Object.freeze({
'本': 'BOOK',
'書籍': 'BOOK',
'book': 'BOOK',
'動画': 'VIDEO',
'video': 'VIDEO',
'記事': 'ARTICLE',
'article': 'ARTICLE',
'講座': 'COURSE',
'course': 'COURSE'
})
});
function auditWatchlistCategoriesFree() {
validateCategoryAuditConfig_();
const lock = LockService.getDocumentLock();
if (!lock.tryLock(5000)) {
throw new Error('別の監査が実行中です。少し待ってから再実行してください。');
}
try {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const source = spreadsheet.getSheetByName(
CATEGORY_AUDIT_CONFIG.SOURCE_SHEET
);
if (!source) {
throw new Error('Watchlistシートが見つかりません。');
}
const values = source.getDataRange().getDisplayValues();
if (values.length < 2) {
throw new Error('見出し行の下にテストデータを1行以上入れてください。');
}
if (values.length - 1 > CATEGORY_AUDIT_CONFIG.MAX_ROWS) {
throw new Error('最初は500行以内に絞って確認してください。');
}
const headers = values[0].map(value => value.trim());
const titleIndex = headers.indexOf('タイトル');
const categoryIndex = headers.indexOf('カテゴリ');
if (titleIndex < 0 || categoryIndex < 0) {
throw new Error('1行目に「タイトル」と「カテゴリ」が必要です。');
}
const runId = Utilities.getUuid().slice(0, 8);
const checkedAt = new Date();
const auditRows = [];
const counts = { OK: 0, REVIEW: 0, INPUT_HOLD: 0 };
values.slice(1).forEach((row, offset) => {
const sourceRow = offset + 2;
const title = String(row[titleIndex] || '').trim();
const rawCategory = String(row[categoryIndex] || '');
if (!title && !rawCategory.trim()) return;
if (!title) {
counts.INPUT_HOLD += 1;
auditRows.push([
checkedAt, runId, sourceRow, 'INPUT_HOLD', '',
'タイトルが空です'
]);
return;
}
const normalized = normalizeCategoryKey_(rawCategory);
if (!normalized) {
counts.INPUT_HOLD += 1;
auditRows.push([
checkedAt, runId, sourceRow, 'INPUT_HOLD', '',
'カテゴリが空です'
]);
return;
}
const suggested = CATEGORY_AUDIT_CONFIG.ALIASES[normalized] || '';
if (!suggested) {
counts.REVIEW += 1;
auditRows.push([
checkedAt, runId, sourceRow, 'REVIEW', '',
'対応表にないカテゴリです'
]);
return;
}
const alreadyCanonical = rawCategory.trim() === suggested;
const status = alreadyCanonical ? 'OK' : 'REVIEW';
counts[status] += 1;
auditRows.push([
checkedAt, runId, sourceRow, status, suggested,
alreadyCanonical ? '変更候補なし' : '表記候補を確認してください'
]);
});
if (!auditRows.length) {
throw new Error('確認対象の行がありません。');
}
const auditSheet = spreadsheet.getSheetByName(
CATEGORY_AUDIT_CONFIG.AUDIT_SHEET
) || spreadsheet.insertSheet(CATEGORY_AUDIT_CONFIG.AUDIT_SHEET);
if (auditSheet.getLastRow() === 0) {
auditSheet.appendRow([
'実行日時', 'run_id', '元の行', '判定', 'カテゴリ候補', '確認メモ'
]);
}
auditSheet.getRange(
auditSheet.getLastRow() + 1,
1,
auditRows.length,
auditRows[0].length
).setValues(auditRows);
console.log(JSON.stringify({
dryRun: CATEGORY_AUDIT_CONFIG.DRY_RUN,
checkedRows: values.length - 1,
auditRows: auditRows.length,
counts,
sourceUpdated: false
}));
} finally {
lock.releaseLock();
}
}
function normalizeCategoryKey_(value) {
return String(value || '')
.normalize('NFKC')
.trim()
.replace(/\s+/g, ' ')
.toLowerCase();
}
function validateCategoryAuditConfig_() {
if (CATEGORY_AUDIT_CONFIG.DRY_RUN !== true) {
throw new Error('安全のためDRY_RUNはtrue固定です。');
}
if (!Number.isInteger(CATEGORY_AUDIT_CONFIG.MAX_ROWS) ||
CATEGORY_AUDIT_CONFIG.MAX_ROWS < 1 ||
CATEGORY_AUDIT_CONFIG.MAX_ROWS > 500) {
throw new Error('MAX_ROWSは1〜500で指定してください。');
}
const allowed = new Set(CATEGORY_AUDIT_CONFIG.ALLOWED_CATEGORIES);
Object.values(CATEGORY_AUDIT_CONFIG.ALIASES).forEach(category => {
if (!allowed.has(category)) {
throw new Error('ALIASESに許可外のカテゴリがあります: ' + category);
}
});
}実行手順
- 元のスプレッドシートをコピーし、コピー側で `Watchlist` を作ります。
- 「拡張機能」からApps Scriptを開き、コードを貼り付けます。
- `DRY_RUN: true` と `MAX_ROWS: 500` 以下を確認します。
- 実行関数に `auditWatchlistCategoriesFree` を選び、初回の権限内容を確認して許可します。
- `CategoryAudit` シートで `REVIEW` と `INPUT_HOLD` を確認します。
- 実行ログで `sourceUpdated` が `false` になっていることを確認します。
- 元の行を直す場合は、自動置換を追加せず、候補と実データを一件ずつ見比べて手動で修正します。
判定の見方
- `OK`: すでに許可済みの標準表記です。
- `REVIEW`: 対応表から標準候補を出せた行、または対応表にないカテゴリです。
- `INPUT_HOLD`: タイトルかカテゴリが空で、集計前に入力確認が必要です。
このコードは、対応表にないカテゴリを勝手に既存分類へ寄せません。たとえば「Podcast」を `VIDEO` に含めるかは運用者が決める必要があるため、候補なしの `REVIEW` として止めます。
安全に使うためのポイント
- 元データへ `setValue`、`setValues`、`deleteRow`、`sort` を実行しません。
- 書き込み先は監査専用の `CategoryAudit` だけです。
- 一度に500行を超える場合は停止します。最初は10行程度の架空データで確認してください。
- タイトル本文、URL、メモを監査ログへ出しません。
- `run_id` を付けるため、複数回の結果を見分けられます。
- ドキュメントロックを使い、同時実行による監査ログの混在を抑えます。
- 監査結果を見ても、自動置換や自動削除は行いません。
失敗しやすい点と制限
- 見出しの「タイトル」「カテゴリ」が完全一致しないと停止します。
- `ALIASES` にない分類は、誤変換を避けるため候補を出しません。
- NFKC正規化は全角英数字や空白の比較に役立ちますが、言葉の意味までは判定しません。
- Apps Scriptとスプレッドシートには実行時間・セル数などの利用上限があります。
- 共有シートでは、監査シートを追加できる編集権限が必要です。閲覧権限だけの場合は実行できません。
- 組織の分類ルールや保存期間、個人情報の扱いは、このコードでは判断しません。
公開前チェックリスト
- [ ] コピーしたシートと架空データで試した
- [ ] `DRY_RUN` が `true` のまま
- [ ] `ALIASES` と許可カテゴリが自分の運用ルールに合っている
- [ ] 監査ログへタイトル、URL、メモ、個人情報を追加していない
- [ ] 元データを更新・削除・並べ替える処理を追加していない
- [ ] `INPUT_HOLD` と候補なしの `REVIEW` を人が確認した
- [ ] 実行ログで `sourceUpdated: false` を確認した
まとめ
カテゴリ表記ゆれは、集計後の数字を直すより、集計前に候補を見える化した方が安全です。この無料コードは、比較用の表記だけを正規化し、元のウォッチリストを変更せずに監査結果を残します。
積読・保存動画・あとで読む記事を、状態や優先度、次に見る日までまとめて管理したい方へ

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

※この記事は学習・検証用の例です。実データへ適用する前にコピー環境で権限、入力、監査ログを確認してください。Google公式・Google公認の商品ではありません。成果や完全な分類を保証するものではありません。
