背景
Kindle のセール情報をまとめるサイトを自動で更新しています。
セール集約サイトを見に行って、実施中のセールを記事にする、という単純な仕組みです。
同じセールを二重に載せないよう、重複チェックは最初から入れてありました。
入れてあったのに、今朝すり抜けました。その話です。
何が起きたか
朝のランで、集約サイトから拾った3件を記事にしようとしました。
そのうち2件が、前夜のランで既に公開済みのセールでした。
- カドサマー第4弾 KADOKAWAコミック99円
- 早川書房 夏の大感謝セール 50%オフ
前夜の記事はこの slug で入っていました。
sale-kadokawa-kadosummer-dai4dan-99yen-20260808
sale-hayakawa-natsu-daikansha-50off-20260808
そして今朝、私が作ろうとしていたのがこれです。
sale-kadokawa-kadosummer-dai4dan-comic-99yen-20260809
sale-hayakawa-natsu-daikansha-50off-3000satsu-20260809
見比べれば同じセールだと分かります。
でも、機械には分かりませんでした。
重複チェックは正しく動いていた
チェックの中身は下記のようなものです。
articles は data/articles.json に入っている既存記事の配列、entry はこれから足そうとしている記事です。
const known = new Set(articles.map(a => a.slug));
if (known.has(entry.slug)) {
console.log("[skip] 既存:", entry.slug);
return false; // 追加しない
}
articles.push(entry);
known に入っているのは既存の slug、比べているのも新しい slug。
コードは間違っていません。
問題は slug の末尾に日付を入れていたことです。
-20260808 と -20260809。
同じセールでも、記事を作った日が違えば slug が変わる。
Set の照合は当然すり抜けます。
なぜ日付を入れていたのか
理由はありました。
同じ出版社が月に何度もセールをやるので、sale-kadokawa-99yen のような slug だと衝突するからです。
日付を足すと確実に一意になる。
一意性のために足したものが、重複検出を壊した、という形です。
「一意なID」と「同じものかどうかの判定キー」を、同じ文字列で兼ねようとしたのが間違いでした。
この2つは要求が逆で、前者は必ず違ってほしいし、後者は同じ題材なら同じであってほしい。
どう直すか
記事の識別子(slug)とは別に、題材の識別子を持つ。
うちの場合は「出版社 × セール名 × 終了日」で足ります。
// 題材の識別子。日付ではなく「そのセール自体」を指す
function dealKey(d) {
return [d.publisher, d.saleName, d.deadline ?? "no-deadline"]
.map(s => String(s).trim().toLowerCase().replace(/\s+/g, "-"))
.join(":");
}
// 例: "kadokawa:カドサマー第4弾-コミック99円:2026-08-20"
これを state に貯めておいて、追加前に照合します。
const knownDeals = new Set(state.knownDealKeys);
const key = dealKey(entry);
if (knownDeals.has(key)) {
console.log("[skip] 同じセール:", key);
return false;
}
slug は今までどおり日付入りで一意にしたまま、判定だけ別のキーでやる。
2つの要求を、2つのフィールドに分ける、というだけの話です。
気づけたのは偶然だった
正直に書くと、私はこれに自力で気づいていません。
デプロイ直前の別の検査(作業ツリーが origin より遅れていないか)が落ちて、
git pull --rebase したあとに data/articles.json を眺めていたら、
同じ内容の記事が並んでいるのが目に入った、という順序でした。
その検査が無ければ、たぶんそのまま公開していたと思います。
重複チェックが「動いていた」ことがかえって厄介で、ログには何も出ません。
skip も出ないし、エラーも出ない。ただ2本目が増えるだけです。
黙って通る失敗は、落ちる失敗よりずっと見つけにくい…。
対象のサイトはこれです。
https://manga.autoarticles.net/
一意なIDと同一性の判定キーは別物として持つ。
今回のように日付やタイムスタンプを識別子に混ぜている箇所があるなら、たぶん同じ穴が空いています。
本記事はAI補助で執筆した、個人開発の紹介記事です。