外部サイトからデータを集める仕組みを作るとき、私は設計書を先に書く。列構成を決め、取得経路を決め、どのタイミングで測るかを決める。今回もそうした。そのうえで実装し、テストを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万超えのサンプルを意図的に探す必要がある
  • 返信の混入を見つけるには、「タブごとに混ざるデータが違う可能性」を疑い、全タブを確認する必要がある
  • 検索ノイズを見つけるには、「検索結果に無関係な投稿が入る可能性」を疑う必要がある

いずれも疑うべき箇所を先に知っていなければ出てこない問いだ。知らないから設計しているのであって、レビューの丁寧さで埋まる差ではない。

私が取った順序はこうなった。

  1. 設計書を書く(列構成・取得経路・測定タイミング)
  2. 実装し、テストを書く
  3. 実データを20件通す
  4. 壊れた箇所を直し、なぜ設計で見抜けなかったかを設計書に追記する

3で3箇所直すことになり、結果として設計書の記述も3つ変わった。もし20件ではなく200件溜めてから気づいていたら、欠損したデータを含む200件を洗い直すことになっていた。

実データを通す前に決めておいたこと

もう1つ、今回やってよかったのは分析を始める前に「良い/悪い」を1つの数字で決める、と先に文書に書いたことだ。

指標を決めずに分析を始めると、出てきた結果に合う指標を後から選んでしまう。いいね数で見れば伸びている、コメント数で見れば伸びていない、という投稿は必ず出る。そのとき指標が決まっていなければ、都合の良い方を選べる。

これも実例が手元にあった。自分のアカウントで過去最高の反響だった投稿は、いいね率では全体平均を下回っていた(0.054%に対し平均0.239%)。コメント率で見ると平均の1.8倍で、1投稿で全投稿のコメント合計の19%を集めていた。

いいね数を指標に選んでいたら、過去最高の投稿を「平均以下」と判定していた。 指標の選び方ひとつで結論が逆になる。

ただし指標を決めるのも、データを見てからにした。現時点の蓄積は20件・1回分の測定しかなく、同じ投稿を測り直したデータが無い。数式を今決めても実測で裏付けられないので、「分析に着手する前に決まっていること」を条件として書くに留めてある。

まとめ

外部サイトからデータを集める仕組みで、設計が壊れた3箇所はいずれもエラーを出さずに間違ったデータを入れてくる形だった。

  • 表示形式が値の大きさで変わり、上位のデータだけが欠ける
  • 構造が同じで意味が違うデータ(返信)が混ざる
  • 収集元の都合で、無関係なデータが紛れる

そして3つとも、実データを20件通した時点で数分で見つかった。設計書のレビューでは出てこなかった。

外部サイトを相手にする仕組みを作るなら、テストが全部通った時点で完成とせず、小さくていいので実データを1回通す工程を設計に組み込むのがいい。200件溜めてからでは、洗い直す対象がその分増える。

関連記事

Xでフォローしよう

おすすめの記事