0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スクレイピングの設計が実データで壊れた3つの箇所、机上で見抜けなかった理由

0
Posted at

外部サイトからデータを集める仕組みを作るとき、私は設計書を先に書く。列構成を決め、取得経路を決め、どのタイミングで測るかを決める。今回もそうした。そのうえで実装し、テストを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件溜めてからでは、洗い直す対象がその分増える。

関連記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?