積読・動画リストのカテゴリ表記ゆれを集計前に発見|GASでdry-run監査

積読・動画リストのカテゴリ表記ゆれを集計前に発見|GASでdry-run監査

ツールおじ|GoogleアカウントをAIエージェント化

ツールおじ|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);
    }
  });
}

実行手順

  1. 元のスプレッドシートをコピーし、コピー側で `Watchlist` を作ります。
  2. 「拡張機能」からApps Scriptを開き、コードを貼り付けます。
  3. `DRY_RUN: true` と `MAX_ROWS: 500` 以下を確認します。
  4. 実行関数に `auditWatchlistCategoriesFree` を選び、初回の権限内容を確認して許可します。
  5. `CategoryAudit` シートで `REVIEW` と `INPUT_HOLD` を確認します。
  6. 実行ログで `sourceUpdated` が `false` になっていることを確認します。
  7. 元の行を直す場合は、自動置換を追加せず、候補と実データを一件ずつ見比べて手動で修正します。

判定の見方

  • `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公認の商品ではありません。成果や完全な分類を保証するものではありません。


あなたも記事の投稿・販売を
始めてみませんか?

Tipsなら簡単に記事を販売できます!
登録無料で始められます!

Tipsなら、無料ですぐに記事の販売をはじめることができます Tipsの詳細はこちら
 

この記事の販売者

ツールおじ|GoogleアカウントをAIエージェント化

おう、Gasおじだ。 AIツールとSaaSに散々突っ込んで遠回りした結果、最後に強かったのは「今あるGoogleアカウント」だった。 今ではAIエージェントであらゆるタスクを自動化して無双してるぞ。 GAS、スプレッドシート、フォーム、Gmail、AIエージェントを組み合わせれば、個人でも小さな会社でも、手作業だらけの業務はかなり自動化できる。 ここでは机上のAI論じゃなく、現場で動くテンプレ、コード、手順だけを置いていくぞ。 高いツールを増やす前に、まずお前さんのGoogleアカウントを仕事するエージェントに変えようじゃないか。

この販売者が書いた他の記事

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)
  • Gmailの自動ラベルが競合する前に確認|GASで仕分けルールをdry-run監査

  • 毎朝のToDoを区分・優先度・期限つきでまとめる Morning ToDo Mail Pro

    ¥2,980
    1 %獲得
    (29 円相当)

関連のおすすめ記事

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化

  • USBメモリで持ち運べるLinux Mint 22.1と永続的な書き込み領域の作成法【Rufus だけで作成する方法を初心者向けに徹底解説】

    ¥500
    1 %獲得
    (5 円相当)
    SmartSenior

    SmartSenior

  • 毎朝のToDoを区分・優先度・期限つきでまとめる Morning ToDo Mail Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化