あとで見るURLを二重登録しない|GASで読書・動画ウォッチリストをdry-run監査

あとで見るURLを二重登録しない|GASで読書・動画ウォッチリストをdry-run監査

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

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

「あとで読む」「あとで見る」を追加したはずなのに、同じ記事や動画が何行も並ぶ。重複に気づかないまま候補だけが増えると、次に見るものを決める時間まで長くなります。

この記事では、Googleスプレッドシートのウォッチリストを変更せずに読み取り、URLの重複と入力不備を別シートへ記録する無料のGAS例を紹介します。削除や統合は自動で行いません。最初は必ずコピーしたシートと少量のサンプルで確認してください。

先に知っておきたいこと

  • 対象は自分用の本、記事、動画、教材のURLです。
  • URL末尾の追跡パラメータやページ内位置を除いて比較します。
  • 元のWatchlistシートは変更しません。
  • AuditLogシートへ行番号、判定、短い照合キーだけを追記します。元URLやメモ本文はログへ残しません。
  • ログを見たあと、残す行や直すURLは人が判断します。
  • 外部サイトの本文取得、動画ダウンロード、AI要約、自動削除は行いません。

準備するシート

スプレッドシートにWatchlistという名前のシートを作り、1行目へ次の列を用意します。

  • 種別
  • タイトル
  • URL
  • 状態
  • 次に見る日
  • メモ

たとえば同じ動画を通常URLと共有URLで2回登録したり、記事URLへutm_sourceなどの追跡情報が付いたりしている状態を、コピーしたシートで再現して試せます。

コピーして使える最小コード

function auditWatchlistDuplicatesFree() {
  const DRY_RUN = true;
  const SOURCE_SHEET = 'Watchlist';
  const LOG_SHEET = 'AuditLog';
  const MAX_ROWS = 500;

  if (!DRY_RUN) {
    throw new Error('この無料例はDRY_RUN: trueのまま実行してください。');
  }

  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const source = spreadsheet.getSheetByName(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 > MAX_ROWS) {
    throw new Error('最初は500行以内に絞って確認してください。');
  }

  const headers = values[0].map(value => value.trim());
  const titleIndex = headers.indexOf('タイトル');
  const urlIndex = headers.indexOf('URL');
  if (titleIndex < 0 || urlIndex < 0) {
    throw new Error('1行目に「タイトル」と「URL」が必要です。');
  }

  const runId = Utilities.getUuid().slice(0, 8);
  const seen = new Map();
  const logs = [];

  values.slice(1).forEach((row, offset) => {
    const sourceRow = offset + 2;
    const title = row[titleIndex].trim();
    const rawUrl = row[urlIndex].trim();

    if (!title && !rawUrl) return;
    if (!title) {
      logs.push([new Date(), runId, sourceRow, 'INPUT_HOLD', '', 'タイトルが空です']);
      return;
    }
    if (!rawUrl) {
      logs.push([new Date(), runId, sourceRow, 'INPUT_HOLD', '', 'URLが空です']);
      return;
    }

    const canonicalUrl = normalizeWatchlistUrlFree_(rawUrl);
    if (!canonicalUrl) {
      logs.push([new Date(), runId, sourceRow, 'INPUT_HOLD', '', 'httpまたはhttpsのURLを確認してください']);
      return;
    }

    const key = shortHashFree_(canonicalUrl);
    if (seen.has(canonicalUrl)) {
      logs.push([new Date(), runId, sourceRow, 'DUPLICATE', key, '先に登録された行: ' + seen.get(canonicalUrl)]);
    } else {
      seen.set(canonicalUrl, sourceRow);
      logs.push([new Date(), runId, sourceRow, 'OK', key, '重複なし']);
    }
  });

  const logSheet = spreadsheet.getSheetByName(LOG_SHEET) || spreadsheet.insertSheet(LOG_SHEET);
  if (logSheet.getLastRow() === 0) {
    logSheet.appendRow(['実行日時', 'run_id', '元の行', '判定', '照合キー', '確認メモ']);
  }
  if (logs.length > 0) {
    logSheet.getRange(logSheet.getLastRow() + 1, 1, logs.length, logs[0].length).setValues(logs);
  }

  console.log(JSON.stringify({
    dryRun: DRY_RUN,
    checkedRows: values.length - 1,
    logRows: logs.length,
    sourceUpdated: false
  }));
}

function normalizeWatchlistUrlFree_(rawUrl) {
  const match = rawUrl.trim().match(/^(https?):\/\/([^\/?#]+)(\/[^?#]*)?(?:\?([^#]*))?(?:#.*)?$/i);
  if (!match) return '';

  const protocol = match[1].toLowerCase();
  let host = match[2].toLowerCase();
  let path = match[3] || '/';
  let query = match[4] || '';

  if (host === 'youtu.be') {
    const videoId = path.replace(/^\//, '').split('/')[0];
    if (!videoId) return '';
    host = 'www.youtube.com';
    path = '/watch';
    query = 'v=' + encodeURIComponent(videoId);
  }

  const keptParams = query
    .split('&')
    .filter(Boolean)
    .filter(part => {
      const key = part.split('=')[0].toLowerCase();
      return !key.startsWith('utm_') && !['fbclid', 'gclid'].includes(key);
    })
    .sort();

  if (path !== '/') path = path.replace(/\/$/, '');
  return protocol + '://' + host + path + (keptParams.length ? '?' + keptParams.join('&') : '');
}

function shortHashFree_(value) {
  const digest = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, value);
  return Utilities.base64EncodeWebSafe(digest).slice(0, 12);
}

実行手順

1. 元のスプレッドシートをコピーし、コピー側の拡張機能からApps Scriptを開きます。

2. コードを貼り付け、auditWatchlistDuplicatesFreeを選びます。

3. DRY_RUN: trueのまま実行し、初回だけ必要な権限を内容確認して許可します。

4. AuditLogシートでDUPLICATEとINPUT_HOLDを確認します。

5. 元のWatchlistを自動修正せず、残す行と直すURLを一件ずつ判断します。

6. 実行ログにsourceUpdatedがfalseと出ていることを確認します。

コードが行っている安全策

  • 元データは読み取りだけにし、行削除や並べ替えをしません。
  • 500行を超えたら停止し、最初から大量処理しません。
  • タイトルまたはURLが空ならINPUT_HOLDとして止めます。
  • httpとhttps以外の文字列は比較対象にしません。
  • 追跡パラメータとページ内位置を除いて照合します。
  • ログには元URLではなく、比較用URLから作った短いハッシュだけを残します。
  • 同じURLでも内容の更新版や言語違いである場合があるため、自動統合しません。

失敗しやすい点

URLが同じでも、ログイン状態や地域によって内容が変わるサービスがあります。短縮URLやリダイレクト先を外部へ問い合わせる処理は、この無料例には入れていません。

また、Google Apps Scriptには実行時間やサービス利用量の上限があります。大量データを一度に処理せず、コピーしたシートで少量ずつ確認してください。

公開前チェックリスト

  • 元データを自動削除しない
  • 実URLや個人的なメモをログへ残さない
  • DUPLICATEを機械的に削除せず、人が中身を確認する
  • 著作物の本文や動画を取得・複製しない
  • Googleアカウントの権限画面を読んでから許可する
  • 実行ログでsourceUpdatedがfalseになっている

まとめ

ウォッチリストの整理は、先に消すことではなく、重複候補を安全に見えるようにすることから始められます。この例ではURLを正規化して照合し、元データを変更せず、確認用ログだけを残します。

状態、優先度、次に見る日、週次レビューまでまとめて管理したい方向けに、関連する有料Tipsがあります。

読書・動画ウォッチリスト整理GAS Proを見る

Gasおじの他の商品もTips内で確認できます。

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

※この記事は学習・検証用の例です。実データへ適用する前にコピー環境で確認してください。Google公式・Google公認の商品ではありません。著作権や各サービスの利用条件はご自身で確認してください。学習成果や業務成果を保証するものではありません。


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

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

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

この記事のライター

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

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

このライターが書いた他の記事

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

    ¥2,980
    1 %獲得
    (29 円相当)
  • GASのtry-catch入門|元データを守るエラー処理とログの実装例

  • Gasおじ式 X投稿キュー自動実行GAS Pro|危ない時だけ止まる

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

関連のおすすめ記事

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

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

    SmartSenior

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

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

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

  • 【Python】 Con-Que 【データベース接続】

    ¥3,000
    1 %獲得
    (30 円相当)
    GoGorilla

    GoGorilla