あとで見るURLを二重登録しない|GASで読書・動画ウォッチリストをdry-run監査
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公認の商品ではありません。著作権や各サービスの利用条件はご自身で確認してください。学習成果や業務成果を保証するものではありません。
