送客クリックに来ていた謎アクセスの調査と挙動ベースのボット判定
今回やったこと / この記事について
運営中の複数Webメディア間を結ぶ相互送客リンクやRSSアンテナのクリックログを精査したところ、記録された送客クリックの8割以上がごく一部のクローラによる異常なアクセスで占められていることが判明した。
調査の結果、一般的なUser-Agent文字列のブラックリスト判定を巧みにすり抜ける巡回クローラの存在が明らかになり、これに対処するため「アクセス挙動(時間間隔・巡回パターン)に基づく判定ルール」と「連打検知時の過去ログ遡及判定」を導入した。
本記事では、総クリック85,818件のログから判明した異常アクセスの実態、従来のUser-Agent判定が破綻していた理由、そして新たに設計・配備した挙動ベース判定システムと8万件の過去ログ再判定バッチの実装を記録する。
ログ調査:85,818件中7万件が「わずか13IP」に集中していた
送客リンクの集計値に違和感を覚え、蓄積されていたクリックログ(計85,818件)を集計・分析したところ、極端な偏りが浮き彫りになった。
| 指標 | 測定値 |
|---|---|
| 総クリック件数 | 85,818 件 |
| 上位13IPによるクリック数 | 約 70,000 件(全体の 81%) |
| 異常IPの1日あたりクリック頻度 | 1IPあたり 日50〜133回 |
| クリックの時間間隔 | リンク掲載順に 4〜8秒間隔 で連続クリック |
ログのタイムスタンプとリンク先URLを時系列で追跡した結果、その挙動は人間のブラウジングとは完全にかけ離れていた。
同一のIPアドレスが、ページ上に掲載されている送客先リンクの上から下まで、正確に4〜8秒の間隔を空けて順番にすべて踏み抜いていたのである。これは一般ユーザーの回遊ではなく、アーカイブ巡回やリンク先スクレイピングを目的とした自動クローラの規則正しい巡回パターンであった。
この7万件もの機械的アクセスが「送客クリック数」として正常カウントされていたため、メディア間の送客効率や読者動向の分析が大きく歪められていた。
なぜ従来のUser-Agent判定をすり抜けていたのか
システムには元々、一般的なボット除外ロジック(UA文字列中に bot, crawler, spider 等が含まれているかを調べる正規表現チェック)が組み込まれていた。
しかし、今回検出された上位IP群はこのチェックをことごとく通過していた。User-Agent文字列の記録を開始した直後のログ131件を詳細に分析したところ、以下の偽装パターンが判明した。
- 古いAndroid端末テンプレートの使い回し(28件): 数世代前のAndroid端末やマイナーな特定キャリア端末のUser-Agent文字列を固定で送信し続けていた。単体で見れば「古い端末を使っている一般ユーザー」に見えるため、キーワードマッチでは弾かれない。
- iOS端末の偽装(12件):
User-Agent上は「iPhone」を名乗っているにもかかわらず、レンダリングエンジンのバージョン表記にSafariではなくChrome系の
AppleWebKit/537.36が含まれるなど、不自然なヘッダー構成になっていた。 - 極端に古いChromeブラウザの固定値送信: 最新バージョンから大きく乖離した固定のデスクトップChrome UAを偽装していた。
ボット側が一般的なブラウザのUser-Agentを名乗っている以上、ヘッダー文字列のキーワード検査だけに頼る防御は限界を迎えていた。
挙動ベース判定の設計と「遡及判定」の必要性
ヘッダー文字列の偽装に惑わされず確実に機械的巡回を検出するため、アクセス者の「挙動(振る舞い)」を評価基準とする判定ロジックへ刷新した。
1. 2つの挙動判定ルール
新システムでは、以下のいずれかに該当するクリックを自動的にボットとして判定する。
- ルール1(短時間乱打): 同一IPアドレスが、60秒以内に3つ以上の異なる送客先媒体のリンクをクリックした場合。
- ルール2(累積過多): 同一IPアドレスからのクリックが、過去24時間で30回以上に達した場合。
2. 連打検知時の「遡及判定」アーキテクチャ
挙動ベース判定を設計する上で不可欠だったのが、過去に遡ってフラグを立て直す「遡及判定(back-fill judgment)」の仕組みである。
巡回クローラが送客リンクを順番にクリックしていく際、その挙動は以下のように推移する。
[10:00:00] リンクAをクリック(1件目) → この時点では「普通の1クリック」に見える
[10:00:05] リンクBをクリック(2件目) → 2件目でもまだ一般ユーザーの可能性が残る
[10:00:10] リンクCをクリック(3件目) → ★ここで「60秒以内に3媒体」が成立!ボット確定
もし3件目のアクセス時だけにボット判定を下した場合、1件目と2件目は「正常なクリック」としてデータベースに残ってしまう。巡回クローラが上位13IPで7万回も踏んでいる環境では、この取りこぼしだけで膨大なノイズが統計に残ることになる。
そのため、3件目でボットと判定された瞬間、同一IPによる直近のクリック(1件目・2件目)も即座にデータベースを遡ってボットフラグ(is_bot = 1)に更新する設計とした。
あわせて、判定理由を追跡できるようデータベースに bot_reason カラムを新設し、管理画面で「挙動連打」「日次超過」「UA異常」などの内訳をグラフィカルに確認できるようにした。
過去ログ8万件の再判定:タイムアウトとの戦い
挙動ベースの新ロジックが完成した後、過去に蓄積された85,818件の生ログに対しても同様の基準を適用し、過去の数値を正常化する必要があった。
しかし、PHPの実行環境には max_execution_time(通常30秒程度)の制限が存在する。8万行のテーブルに対して単純に「全行の自己結合(同一IPの前後アクセス検索)」を実行したところ、4月分の31,791行を処理しようとした時点でクエリが爆発し、タイムアウトで処理が中断してしまった。
この問題を解決するため、バッチ処理に以下の工夫を施した。
1. 自己結合の対象を未判定行に絞り込む
連打判定を行う際、すでに過去のステップで「ボット」または「正常」と確定した行まで結合対象に含めていたことがクエリ遅延の原因であった。候補クエリを is_bot IS NULL(未判定行)に限定することで、処理ステップが進むにつれて対象レコードが激減し、計算量を大幅に圧縮した。
2. 「月+手順」による進捗ステート管理と15秒予算制御
再判定処理を月単位(例: 2026年4月、5月……)と処理手順(手順1: UAチェック、手順2: 挙動連打チェック、手順3: 日次超過チェック)の組み合わせで管理した。
各リクエストの実行時間を最大15秒で制限し、タイムアウト直前に現在の進捗(例: 2026-05:step2)をオプションテーブルに保存して正常終了する。管理画面からボタンを再度クリックすると、中断した地点から正確に再開できるように設計した。
これにより、サーバーのリソースを圧迫することなく、8万件超の過去ログ全数に対する挙動再判定を安全に完了させることができた。
得られた知見とまとめ
- ヘッダー偽装時代における挙動判定の優位性: User-Agent等のリクエストヘッダー偽装が容易になった現在、リクエスト単体の文字列検査だけで高度なスクレイパーを防ぐことは困難である。「どのリンクを、どれだけの間隔で、何件踏んだか」というステートフルな挙動分析こそが、誤検知を抑えやすい有効な防壁となる。
- 多段階アクセスにおける遡及処理の重要性: 複数回の行動パターンをもって初めて異常と判定できるシステムでは、判定が確定した瞬間に過去のトリガーアクションへ遡及して是正を掛ける仕組みが不可欠である。
- 大量ログの再判定バッチは「時間予算+ステート保持」で組む: Webサーバー上で走る管理用バッチは、いつタイムアウトや通信切断に見舞われても安全に再開できるよう、処理単位の分割とチェックポイント記録を前提に設計すべきである。
更新履歴
- 2026-09-07: 初版起稿(送客クリックボット対策および過去ログ再判定の実装に基づく)