外部サイトからデータを集める仕組みを作るとき、私は設計書を先に書く。列構成を決め、取得経路を決め、どのタイミングで測るかを決める。今回もそうした。そのうえで実装し、テストを602件通し、lintも通した。
それから実データを20件だけ通した。3箇所が壊れていた。
壊れ方には共通点があった。どれも「データが取れない」ではなく「間違ったデータが、正常に見える顔をして入ってくる」形だった。エラーにならないので、テストも通る。気づくには実データを見るしかなかった。
この記事では、その3箇所が何だったか、なぜ机上で見抜けなかったかを書く。対象はSNS(Threads)の投稿データ収集だが、起きたことは外部サイトからデータを集める仕組み全般に当てはまる。
何を作っていたか
競合アカウントの投稿を集めて、何が伸びているかを分析する仕組みだ。構成はこうなっている。
- ブラウザで検索結果を開き、アクセシビリティツリーとして読み取る(ここは人が操作する)
- ツリーを解析して、投稿者・本文・投稿日時・いいね数・コメント数に構造化する
- Googleスプレッドシートへ追記する
- 同じ投稿を24時間後・1週間後に測り直し、伸びの傾きを取る
APIは使っていない。Threadsの公開APIでは他人の投稿の反響を取れないためで、画面から読むしかない。解析部分は純粋関数にして、テストを書きながら実装した。
設計時点では、この方針に穴があるとは思っていなかった。
1. 日本語の万表記で、上位のデータだけが欠けた
いいね数を取り出す正規表現を、こう書いていた。
/generic "([\d,]+)"/
1,531のようなカンマ区切りに対応している。設計時に実際のツリーを1画面分見て、この形だと確認したうえで書いた。
実データ20件を通すと、4件でいいね数が空になった。ツリーを見ると、こうなっていた。
img "「いいね!」"
generic "2.6万"
Threadsは1万以上を「2.6万」と日本語表記する。[\d,]+は当然これを拾わない。
問題はここからだ。空になった4件を並べると、こうなった。
| アカウント | ツリーの表記 | 修正前 | 修正後 |
|---|---|---|---|
| 投稿A | 3.9万 | 空 | 39000 |
| 投稿B | 3.1万 | 空 | 31000 |
| 投稿C | 2.6万 | 空 | 26000 |
| 投稿D | 1.4万 | 空 | 14000 |
この4件は、いいね数の上位4件とそのまま一致した。
万表記は1万以上でしか出ない。つまり最も伸びた投稿ほどこの形になる。この仕組みの目的は「伸びた投稿の構造を知ること」なので、伸びた投稿だけデータが欠けるのは、目的に対して最悪の壊れ方だった。
しかもエラーは出ない。空文字が入るだけなので、「まだ反応が付いていない投稿だな」と見える。実データの上位と突き合わせなければ、そのまま運用に乗っていた。
なぜ机上で見抜けなかったか
設計時に実際のツリーを確認していた。それでも見つからなかったのは、そのとき見た投稿がいずれも1万未満だったからだ。
サンプルを見る量の問題ではない。1画面分を丁寧に読んでも、その画面に1万超えの投稿が無ければ、この分岐は存在しないのと同じに見える。表示形式が値の大きさで変わる仕様は、小さい値のサンプルをいくら眺めても現れない。
2. 返信が、独立した投稿として記録された
収集経路は2つある。「上位検索結果」(過去に伸びた投稿が並ぶ)と「最近」(公開直後の投稿が並ぶ)だ。伸びの傾きを取るには後者が要るので、「最近」タブも実測した。
こちらでは別の壊れ方をした。他人の投稿への返信が、独立した投稿として記録されていた。
ツリー上では、返信はこう表れる。
link href="/@hey_shuya/post/DdlOiC1FKWK"
generic "2026年9月22日火曜日 16:44"
generic "に返信"
generic "@uko_muci_kou"
generic "仕事人間なので、じゃあ代わりに仕事行ってくれと…"
投稿URLがあり、投稿者がいて、日時があり、本文がある。構造としては通常の投稿と区別がつかない。違うのはgeneric "に返信"という行が挟まっていることだけだ。
これを放置すると、「他人の投稿への反応」が「そのアカウントが考えて出した投稿」として蓄積される。投稿の型を分析するのが目的なので、対象として不適切だ。さらに、後述するアカウント候補の自動抽出にも波及して、「返信しかしていないアカウント」が候補に混ざる。
なぜ机上で見抜けなかったか
設計時に「上位検索結果」しか見ていなかったからだ。
そちらには返信がほとんど並ばない。同じ検索語、同じ画面の別タブというだけで、混ざるデータの種類が変わる。「検索結果を解析する」という1つの処理に見えていたものが、実は経路ごとに別の前提を持っていた。
実際、2つの経路は数字で見るとこれだけ違った。
| 観点 | 上位検索結果 | 最近 |
|---|---|---|
| 公開からの経過 | 数ヶ月前が中心(2週間以内は20件中5件) | 大半が1時間以内 |
| 数字が付いている投稿 | 20件中15件 | 20件中3件 |
| 返信の混入 | ほぼ無い | 多い |
この表は、設計を1つ変えることにもなった。「最近」タブは数字が付いている投稿が20件中3件しかない。1回目の測定ではほぼ全件が空になるので、単発で回しても空行が増えるだけだ。伸びの傾きは「空→数字」ではなく「数字→数字」でしか取れないから、この経路は再測定とセットでなければ意味を持たない。
3. 検索語と無関係な投稿が紛れ込んだ
3つ目は、集めたデータから「まだ名簿に無いアカウント」を自動で洗い出す部分で出た。
実データ20件を通すと14件の候補が出た。上位は問題なかったが、下位4件が育児と全く関係なかった。
| 投稿内容 |
|---|
| 「タイヤワックスした🛞、艶が出た」 |
| 「今日のお題むずかった….」 |
「ワンオペ育児」で検索した結果に、こうした投稿が混ざる。検索エンジン側の都合なので、こちらでは防げない。
対処として、いいね数もコメント数も1つも取れていないアカウントを候補から外した。反応が付いていない投稿ほど、この種のノイズである確率が高い。加えて、数字が無ければ採否を判断する材料も無い。14件が9件になり、残った9件はすべて育児系のアカウントだった。
これは正解を狙った絞り込みではなく、判断材料が無いものを人に見せないという整理だ。全件見たい場合に戻せるオプションも残した。
3つに共通していたこと
並べると、壊れ方の性質が見えてくる。
| 箇所 | 見た目 | 実際 |
|---|---|---|
| 万表記 | 「反応がまだ無い投稿」 | 最も伸びた投稿のデータ欠落 |
| 返信 | 「普通の投稿」 | 他人への反応 |
| 無関係な投稿 | 「候補アカウント」 | 検索ノイズ |
どれも例外を投げない。 型エラーにもならない。空文字や、構造的に正しい行として入ってくる。だから単体テストも統合テストも通る。私が書いたテストは602件すべて通っていた。
テストが検証しているのは「自分が想定した入力に対して、想定した出力が出るか」だ。想定そのものが外れている場合、テストは何も言わない。外部サイトのデータを扱うとき、想定が外れる余地は自分のコードより外側にある。
実データを20件通すのが、レビューより早かった
今回、3つの発見にかかった時間は短い。検索結果を開いてツリーを保存し、パーサに通して結果を眺めるだけなので、1回あたり数分だ。
対して、この3つを机上のレビューで見つけるのは難しい。
- 万表記を見つけるには、「表示形式が値の大きさで変わる可能性」を疑い、1万超えのサンプルを意図的に探す必要がある
- 返信の混入を見つけるには、「タブごとに混ざるデータが違う可能性」を疑い、全タブを確認する必要がある
- 検索ノイズを見つけるには、「検索結果に無関係な投稿が入る可能性」を疑う必要がある
いずれも疑うべき箇所を先に知っていなければ出てこない問いだ。知らないから設計しているのであって、レビューの丁寧さで埋まる差ではない。
私が取った順序はこうなった。
- 設計書を書く(列構成・取得経路・測定タイミング)
- 実装し、テストを書く
- 実データを20件通す
- 壊れた箇所を直し、なぜ設計で見抜けなかったかを設計書に追記する
3で3箇所直すことになり、結果として設計書の記述も3つ変わった。もし20件ではなく200件溜めてから気づいていたら、欠損したデータを含む200件を洗い直すことになっていた。
実データを通す前に決めておいたこと
もう1つ、今回やってよかったのは分析を始める前に「良い/悪い」を1つの数字で決める、と先に文書に書いたことだ。
指標を決めずに分析を始めると、出てきた結果に合う指標を後から選んでしまう。いいね数で見れば伸びている、コメント数で見れば伸びていない、という投稿は必ず出る。そのとき指標が決まっていなければ、都合の良い方を選べる。
これも実例が手元にあった。自分のアカウントで過去最高の反響だった投稿は、いいね率では全体平均を下回っていた(0.054%に対し平均0.239%)。コメント率で見ると平均の1.8倍で、1投稿で全投稿のコメント合計の19%を集めていた。
いいね数を指標に選んでいたら、過去最高の投稿を「平均以下」と判定していた。 指標の選び方ひとつで結論が逆になる。
ただし指標を決めるのも、データを見てからにした。現時点の蓄積は20件・1回分の測定しかなく、同じ投稿を測り直したデータが無い。数式を今決めても実測で裏付けられないので、「分析に着手する前に決まっていること」を条件として書くに留めてある。
まとめ
外部サイトからデータを集める仕組みで、設計が壊れた3箇所はいずれもエラーを出さずに間違ったデータを入れてくる形だった。
- 表示形式が値の大きさで変わり、上位のデータだけが欠ける
- 構造が同じで意味が違うデータ(返信)が混ざる
- 収集元の都合で、無関係なデータが紛れる
そして3つとも、実データを20件通した時点で数分で見つかった。設計書のレビューでは出てこなかった。
外部サイトを相手にする仕組みを作るなら、テストが全部通った時点で完成とせず、小さくていいので実データを1回通す工程を設計に組み込むのがいい。200件溜めてからでは、洗い直す対象がその分増える。