- Published on
訪問者の97%はボットだった ― UAを信じずに正体を5段で分類する
- Authors

- Name
- muzzledskr
個人ブログのアクセスを 9 日ぶん、1 リクエスト 1 行の生ログで貯めて、全 IP を「それが何者か」で分類した。結果、1198 IP のうち人間候補は 31 個(2%)。残り 97% はボットだった。
この記事は、その分類をどういう基準で・なぜその順番でやったかの記録である。集計だけ見ていると分からず、そして UA(User-Agent)を証拠に使うと必ず間違える、という話でもある。
TL;DR
- 全 1198 IP を排他的に分類 → ボット 97% / 人間候補 2%
- UA は自己申告。単独では証拠にならない。 データセンターから来る「iPhone MobileSafari」が 297 IP、ブラウザ UA を名乗りながら JS/CSS を一切読まないスクレイパーが 76 IP
- 単一シグナルでは足りない。検証済みボット判定だけでは 48% しか捕まらない
- 判定は 5 段。ASN(接続元ネットワーク)と行動(アセットを読んだか)を重ねて初めて 97% に到達する
前提:なぜ生ログなのか
Cloudflare の集計 API(httpRequestsAdaptiveGroups)は「どのステータスが何件」までしか答えない。「その IP が何者か」は分からない。そこで生ログ(httpRequestsAdaptive、無料プランでも GraphQL から取れる)を 15 分ごとに S3 へ NDJSON で貯め、DuckDB で読む。この構成自体は別記事に書いた。
生ログには 1 行ごとに clientIP / userAgent / clientASNDescription(接続元ネットワーク)/ clientRequestPath / verifiedBotCategory(Cloudflare が検証したボット種別)が入る。この 5 列で分類する。
UA を証拠にしてはいけない
分類基準の話に入る前に、最初に捨てるべき前提を先に潰す。「User-Agent を見ればブラウザか判別できる」——これは誤りだ。UA はクライアントが自己申告する文字列で、いくらでも詐称できる。
実データで見せる。分類の結果、データセンター(AWS・Alibaba・OVH 等)から来た IP が名乗っていた UA はこうだった。
| UA | IP 数 |
|---|---|
| iOS / MobileSafari | 297 |
| Windows / Chrome | 69 |
| MacOSX / Chrome | 9 |
iPhone の Safari が、データセンターから 297 個。 iPhone はデータセンターのラックには無い。この UA は嘘である。
逆に、ブラウザ UA を名乗りながら JS/CSS を一切読まない(=ページをレンダリングしていない)IP も 76 個いた。
| 名乗った UA | IP 数 |
|---|---|
| Windows / Chrome | 22 |
| Android / ChromeMobile | 14 |
| MacOSX / Chrome | 4 |
Chrome を名乗るなら、ページを開けば必ず /_next/static/ の JS バンドルと CSS を取りに行く。それが 0 なら、Chrome の顔をした何かだ。
私は実際、この詐称に一度引っかかった。別の調査で www の 522 エラーを踏んでいた IP 群を見て「MobileSafari だから実ユーザーだ」と判断し、後で撤回している。UA を見た瞬間に負けていた。
分類:5 段を順に当てる
そこで、UA を「参考情報」に格下げし、より詐称しにくいシグナルを軸に据える。上から順に当てはめ、最初に当たった段で確定する(排他的分類)。順番に意味がある。
① 検証済みボット — Cloudflare が正体を確証している
verifiedBotCategory が空でない IP。Googlebot や Bingbot、SNS のリンクプレビュー(Facebook / Twitter)など、Cloudflare が逆引きや署名で正体を確認済みのボット。これは詐称ではない(Cloudflare 側で検証しているため)。
585 IP(48%)。 ここで半分が確定する。だが逆に言えば、検証済みボット判定だけでは残り半分を取りこぼす。
② UA が自白している
userAgent に bot crawl spider curl wget python go-http okhttp 等を含む IP。①で検証されなかったが、自分でボットだと名乗っているもの。隠す気のないスクレイパーや自作クローラー。
74 IP(6%)。
③ 攻撃者
/.env /.git/config /.aws/credentials /wp-admin/ xmlrpc.php /vendor/phpunit/ など、正規の訪問者が絶対に踏まないパスを叩いた IP。認証情報や CMS 脆弱性の探索。
26 IP(2%)。 このサイトは Next.js で WordPress も PHP も使わないので、これらは 100% 悪意である。
④ データセンター発 ★ここが本命
①〜③に当たらず、接続元 ASN がクラウド/VPS 事業者の IP。Amazon・Google・OVH・Hetzner・Alibaba・各種ホスティング。
406 IP(33%)。 これが最大の隠れ層だった。UA は正常なブラウザを名乗る(前述の「データセンターの MobileSafari」がこれ)。だから UA だけ見ると人間に見える。ASN を見て初めてボットと分かる。
人間は家庭用 ISP か携帯キャリアから来る。データセンターから「ブラウザ」で来るのは、ヘッドレスブラウザによるスクレイピングか、各種の自動巡回だ。
⑤ アセット未取得 — ブラウザ UA を騙る
①〜④に当たらず、ブラウザ UA を名乗るのに /_next/static/ を 1 つも取得していない IP。76 IP(6%)。 ④をすり抜けた(住宅 IP を使う)スクレイパーがここで残る。
⑥ 人間候補
上のどれにも当たらず、アセットを取得している(=実際にページをレンダリングした)IP。31 IP(2%)。
結果
| 段 | 分類 | IP 数 | 割合 |
|---|---|---|---|
| ① | 検証済みボット | 585 | 48% |
| ② | UA が自白 | 74 | 6% |
| ③ | 攻撃者 | 26 | 2% |
| ④ | データセンター発 | 406 | 33% |
| ⑤ | アセット未取得(UA 詐称) | 76 | 6% |
| ⑥ | 人間候補 | 31 | 2% |
| ボット計 | 1167 | 97% |
個人ブログのトラフィックの 97% はボット。人間は 2%。
そして重要なのは各段の効き方だ。①だけでは 48% しか取れない。 ④のデータセンター発 33% は、UA が正常なので UA では絶対に落とせない。⑤の 6% は UA を騙る。「1 つのシグナルで判定」は、この時点でどう転んでも過半数を取りこぼす。
なぜ順番が要るのか
排他的に、上から当てる設計にしたのは、シグナルが重なるからだ。あるスクレイパーは「データセンター発」かつ「UA 詐称」かつ「攻撃パスも叩く」ことがある。もし各基準を独立にカウントすると同じ IP を三重に数えてしまう。
順番は確度の高い順にした。①(Cloudflare が検証済み)が最も確実で、⑥(消去法で残った人間候補)が最も弱い。強い証拠から先に確定させ、残りを次の段に送る。これは監視のフィルタ設計そのもので、誤検知を避けたい判定は確度順に並べるのが定石だ。
副産物:WAF ルールが安全に書ける
分類③(攻撃者)を抽出したついでに、正規ボット(①の 585 IP)が攻撃パスを叩いた回数を数えたら、0 件だった。Googlebot が /.env を取りに来ることはない。
つまり「攻撃パスをブロックする」WAF ルールは、正規ボットを 1 件も誤爆しないことがデータで保証できる。感覚で WAF を書くと検索エンジンを弾いて SEO を壊すが、先にログを分類しておけば、それが起きないことを事前に確認できる。
まとめ
- UA は自己申告。証拠に使うと、データセンターの iPhone を実ユーザーと数える。
- 判定は複数シグナルの重ね合わせ。単一では過半数を取りこぼす。ASN と行動(アセット取得)が UA より強い証拠になる。
- 個人ブログでも訪問者の 97% はボット。GA が見せる「ユーザー数」は、この 2% の、さらに一部(計測ビーコンに乗った分)でしかない。
判定ロジックは全部 SQL で書ける。件数はすべて上に出した実数で、再現できる。
一次情報
- Cloudflare ― GraphQL Analytics API(
httpRequestsAdaptiveは全プランで利用可) - Cloudflare ― Verified Bots(
verifiedBotCategoryの検証方法) - DuckDB ― httpfs で S3 上の JSON を直読み