本物の抽選を、静かに捨てていた 4 つの実例:正規表現・年のない日付・まとめて聞く・null
トレカダッシュ 開発の記録 / 2026年8月〜9月
iOS アプリ「トレカダッシュ」の集める処理には、「これは対象外」と判断して捨てる場所がたくさんあります。捨てる判定は、安全側に見えます。間違えても、画面にゴミが出ないだけで、誰も困らない、ように見えるからです。
ところが、この種の間違いは、気づくきっかけがありません。本物の抽選を捨てていても、画面には何も出ないからです。この記事では、そうやって本物を静かに捨てていた、または捨てかけた、4 つの実例を書きます。
数字と日付は、2026 年 8 月 28 日〜9 月 20 日の作業の記録から書いています。
1. 「x.com」が、「e-starbox.com」に一致していた(8 月 31 日)
応募のリンクが、SNS のリンクなら、お店のページではないので、採用しません。そのために、除外するサイトの名前を並べて、1 つの正規表現(文字列のパターン)にしていました。
new RegExp(["x\\.com", "note\\.com", ...].join("|"))
この書き方は、サイト名の途中にも一致します。x\.com は、e-starbox.com の後半に当たります。note\.com は mynote.com に当たります。
実害は、抽選販売のサービス「抽選ロトボックス」の応募ページ(shoplottery.e-starbox.com)で出ていました。このサービスの応募リンクは、「SNS だから店のリンクではない」と判断されて、一度も採用されていませんでした。
直し方は、ホスト名だけの項目は、「// の直後か、ドットの直後」から始まるときだけ一致させる((?:^|//|\.) を前に付ける)ことでした。足したあとは、通したい URL と、弾きたい URL の両方で、13 例を試しました。除外のリストは安全側に見えるので、間違っていても、誰も疑いませんでした。
2. 年の書いていない日付を、6 年前と読んだ(8 月 28 日)
まとめサイトや店の告知は、「8 月 30 日(日)」のように、年を書きません。この文字列を AI に読ませて、締切を取っていました。
入荷情報のサイトの取り込みで、ポケモンカードの案件が、軒並み2020 年 8 月 30 日(6 年前)として返ってきました。締切が過去だと、入口の関門が弾きます。その結果、このサイト経由のポケモンカードの案件が、まるごと取り込まれていませんでした。ワンピースやドラゴンボールは、たまたま 2026 年と読まれていたので、気づきにくい状態でした。
直し方は、AI に渡す指示に、今日の日付と、「今日より前の年を補ってはいけない」を入れることでした。読める案件は、0 件から 46 件になりました。「投稿日と矛盾しない年」とだけ書いても、投稿日が分からないときに、何年になるのかが決まらないので、足りません。
3. まとめて聞いた答えの、数がずれた(9 月 19 日)
AI への問い合わせは、1 件ずつ送ると、回数分の費用がかかります。そこで、複数の投稿の「販売の形式」を、まとめて 1 回で聞く仕組みを入れました。複数行の投稿の本文を、「1. 本文」「2. 本文」と番号をつけて、改行でつないで渡していました。
AI から見ると、投稿の中の改行と、投稿どうしの区切りが、区別できません。X の監視を再開した 1 日だけで、返ってきた答えの数が合わなかった回が 8 回ありました。数が合わないときは、その組を全部「対象外」にする作りだったので、その組の本物の抽選は、まとめて捨てられました。
もっと厄介なのは、数が合った組でも、別の投稿の答えが当たっていたことです。遊戯王カードの抽選が、対象外として捨てられていました。
直し方は、まとめて渡すときは、JSON の配列([{no, text}])か、本文に出てこない見出し(「【記事 N】」)で区切り、返事にも番号を含めさせることです。数が合わないときは、既定値の「対象外」に倒さず、1 件ずつ聞き直します。
4. 「決められない」を、「無い」と読んだ(9 月 20 日)
ある関数は、投稿の文章から、トレカのジャンルを推定して返します。2 つ以上のジャンルが書かれていると、「決められない」ので null を返します。
別の処理が、この関数を使って、「カードの語が無い(=玩具だけの告知)」を判定していました。!関数(text) という書き方です。ここで、「決められない(null)」が「無い」と読まれました。
ポケモンカード、ワンピース、遊戯王、ドラゴンボール、ガンプラ、と並べる、まとめの投稿は、複数のジャンルが書かれているので null になります。それが「カードの語ゼロ」と読まれ、さらに「ガンプラ」が見つかったので、玩具の告知と判定されました。
もし、このまま動いていたら、毎朝 5:15 に動く一括の非表示の処理が、表示中の本物の抽選 129 件(30th CELEBRATION や、世界最強の戦士など、9 つの商品)を、伏せるところでした。止められたのは、一括の処理を動かす前に、件数だけを数える下見(ドライラン)をしたからです。配備の前に、129 件という数字が出て、気づきました。
直し方は、「無い」を判定したいときは、見つかったジャンルを全部返す関数を使い、その長さが 0 かどうかで見ることでした。
分かったこと
捨てる判定の間違いは、苦情が出ない。だから自分で数える。
- 除外のリストも、間違える。安全側に見える判定ほど、疑われません。
- 通したいものでも試す。弾きたいものだけで試すと、通したいものまで弾いていても、気づきません。(1 の 13 例は、両方でした)
- AI には、前提を全部渡す。今日の日付も、区切りも、書かないと、AI は黙って別の前提で答えます。
- 数が合わないときに、「対象外」に倒さない。分からないときの既定値が「捨てる」だと、本物がまとめて消えます。
- 「無い」と「決められない」は、別の答え。戻り値の
nullを、無条件に否定しない。 - 毎日の一括の処理は、動かす前に、件数を数える。129 件が、事前に分かりました。
この話に出てくるもの
- トレカダッシュ — トレカの抽選・予約・先着の情報を集める iPhone アプリ
- 確認できなかったときに、通していた — 通すほうに倒れた失敗。こちらは、捨てるほうに倒れた失敗
- 一括処理が「全記事」に当たった — 一括の処理の事故。動かす前に数える話
- 「取れた」と読んでいたのは、ただの案内ページだった — エラーが出ない失敗
ほかの記録
- 配備のたびに、日次の処理が丸ごと走り直していた:48 時間で 477 回、その日の Gemini 費用の 46.6% — 1 日 1 回のはずの処理が 43 回走り、Gemini の費用の 46.6% を占めていた。タイムアウトした回は最終実行時刻が書かれず、再起動のたびに取りこぼし扱いされていた。逆に、配備が 8 時に重なって、ダイジェストが 1 通も届かなかった問題も。
- 通知は取り消せない:直近 72 時間の通知の 19% が誤通知で、関門が通知の「後」に効いていた — 直近 72 時間に通知した 644 件のうち 122 件(19%)が誤通知だった。記事の関門は後から記事を下げるが、通知は保存の約 0.6 秒後に飛ぶ。下げた 30 件のうち 25 件はもう通知済みだった。通知を送る側の入口に同じ関門を通した記録。
- Gemini の費用を削る前に、計器を疑った:単価・内訳・キャッシュの 3 つが壊れていて、キャッシュの条件は測り直すと変わっていた — Gemini の費用の計測は 3 か所とも壊れていた(単価は 1/2.6 の過小表示、31 の用途が 1 つに潰れ、キャッシュ未計上)。キャッシュが効く条件は 9 月は約 8,000、10 月は約 4,000 トークンと食い違った。指示文を水増しすると判定が 9% ずれた。
コメント
感想や、記事の間違いの指摘は、ここへ。書くには、GitHub のアカウントが要ります。
押すと、giscus(giscus.app)と GitHub に接続します。押さなければ、接続しません。詳しくはあつかいに書いています。