「実ブラウザだけを数える」つもりが破られていた — 1つのIPが3種のUAを使い分けていた

以前、PWAマニフェストの取得数を数えれば実ブラウザだけを拾えるという記事を書いた。ページを描画したブラウザしか manifest.webmanifest を要求しないので、HTMLだけ抜いていくスクレイパーは原理的に混ざらない、という話だった。

その手法が破られていた。マニフェストをきちんと取得するスクレイパーが実在した。

きっかけは、新しく追加したページ群のアクセスが良く見えたことだった。「人が来ているなら増やそう」と考えて中身を確認したところ、そのアクセスはほぼ1つのIPからだった

数字が3段階で変わった

同じ対象を3通りの見方で測ったら、結論が毎回ひっくり返った。

見方評価
単純なアクセス数(PV)良い
マニフェスト取得で絞る同時期の他ページの約6倍で、さらに良い
相手のIPを1つずつ確認人の閲覧はゼロ

最初の2段階だけ見て「伸びている」と判断していたら、根拠のないまま量産に踏み切っていた。

相手の正体

問題のIPを調べたら、こうだった。

  • 3種類のUAを使い分けている — デスクトップChrome / iPhone / Android
  • マニフェストを415回取得している — 判定を素通りする
  • 679ページを巡回している
  • 深夜4時に半分が集中 — 生活リズムがない

「デスクトップとスマホを持っている人」に見えるが、1か月で679ページを読み、その半分を深夜4時に読む人はいない。

UAだけ見れば完全にブラウザで、is_bot の判定も素通りする。マニフェストも取りに来る。手がかりは「行動の量」しか残っていなかった。

閾値を実測で決める

「何ページ以上なら機械か」を勘で決めたくなかったので、分布を見た。

マニフェストを取得したIPについて、期間内のユニークページ数を数える。

   1〜1    パス :  3%
   2〜3    パス : 16%
   4〜10   パス : 77%   ← ここに集中
  11〜30   パス :  2%
  31〜100  パス :  0.4%
 101〜300  パス :  0.4%
 301〜     パス :  1%    ← 1IPあたり5〜6万パスの相手を含む

96%が10ページ以下だった。中央値は5ページ、90パーセンタイルは6ページ。

一方で上位には、1つのIPで5万〜6万ページを巡回している相手が複数いた。桁が4つ違う。

分布がここまで割れていれば、境界の置き方に神経質になる必要はない。90パーセンタイルの5倍にあたる30ページを境にした。熱心な読者を巻き込まず、機械だけを落とせる位置になる。

大事なのは数字そのものより、勘で置かなかったことだと思っている。分布を見る前は「100ページくらいか」と考えていたが、実際の読者は5ページだった。

実装

マニフェストを取得したIPから、ページ数が多すぎる相手を外すだけ。

    /**
     * 1つの IP を「人が見ている」と認める、期間内のユニークページ数の上限。
     *
     * ★実測(2026-09-01・30日)で決めた。マニフェストを取得したIPのうち
     *   **96%が10パス以下**(中央値5・90パーセンタイル6)。10を超えたのは全体の4%だけで、
     *   その上位は1IPあたり5〜6万パスと明らかに機械だった。
     *   90パーセンタイルの5倍にあたる30を境にすれば、熱心な読者を巻き込まずに機械を落とせる。
     */
    public const MAX_HUMAN_PATHS = 30;

    /**
     * 実ブラウザの表示回数(推定)。マニフェスト取得だけを数える。
     * path+created_at の複合索引が効くので、期間集計でも数十msで返る。
     *
     * ★マニフェストを取得するスクレイパーは素通りする。人の数を知りたいときは
     *   {@see realBrowserIps} で相手を絞ってから数えること。
     */
    public function scopeBrowserBeacons(Builder $query): Builder
    {
        return $query->where('path', self::BROWSER_BEACON_PATH);
    }

期間を渡す形にしたのは、集計期間ごとに閾値の意味が変わるからだ。7日で30ページと90日で30ページでは重みが違う。呼び出し側が期間を持っているので、そこで判断させる。

効果

判定を入れ替えたら、「実ブラウザ表示」の数字が53%減った。半分以上が機械だったことになる。

外したIPの数は全体のごく一部(2%程度)だが、1IPあたりの巡回量が桁違いなので、件数への影響はこれだけ大きくなる。少数の重い相手が数字を支配していた。

管理画面では従来の数字と新しい数字を並べて表示することにした。片方だけ出すと「なぜ前より減ったのか」が分からなくなるし、差そのものが「どれだけ機械が混ざっているか」の指標になる。

限界と注意(正直に書いておく)

これも破られる。 相手がページ数を絞れば通り抜ける。1日3ページずつ巡回されたら、区別する手がかりはもう無い。今回の対策は「今いる相手」に効くだけで、恒久的な解決ではない。

熱心な読者を巻き込む可能性がある。 1か月で30ページ以上読む人は除外される。実測では全体の2%なので割り切ったが、サイトの性質によっては痛い。辞書や事典のように「1回の訪問で数ページ」が普通のサイトだから成り立つ話で、連載や漫画のサイトでは閾値がまるで違うはずだ。自分のサイトの分布を測ってから決めてほしい。

IP単位の判定には限界がある。 大学や企業から多数の人が同じIPで来ると、合算されて除外されうる。逆に相手がIPを分散させれば1IPあたりの数は下がる。

そもそも「人が何回見たか」を正確に知る方法はない。 これは推定を少しましにする話であって、真実にたどり着く話ではない。数字は「これ以下ではない」ではなく「せいぜいこの程度」と読むのが安全だと思う。

まとめ

  • マニフェストの取得数だけでは実ブラウザを判定しきれない。 取得してくるスクレイパーが実在する
  • UAも is_bot も素通りされたとき、残る手がかりは行動の量だった
  • 閾値は勘で置かず、分布を測って決める。 実際の読者は想像よりずっと少ないページ数しか見ていない
  • 判定を入れ替えたら数字が 53%減った。少数の重い相手が全体を支配していた
  • 数字が良く見えたら、まず相手を確かめる。 3段階の見方で結論が毎回変わった。
    最初の2段階で止めていたら、根拠のない判断をしていた
  • これも破られる。恒久的な対策ではなく、今いる相手への対処だと理解しておく

前提となる手法については以前の記事に書いた。あわせて読んでほしい。

コメント

タイトルとURLをコピーしました