首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >别只看 robots.txt:从 nginx 与 CDN 日志里查出哪些 AI 爬虫真的抓过你的站

别只看 robots.txt:从 nginx 与 CDN 日志里查出哪些 AI 爬虫真的抓过你的站

原创
作者头像
用户10345225
发布于 2026-10-23 15:46:26
发布于 2026-10-23 15:46:26
770
举报

robots.txt 是一份「你对爬虫的声明」,它记录的是你希望发生什么,而不是发生过什么。要判断某个 AI 爬虫是否真的抓过你的站,唯一的证据是访问日志。

下面是一套可以每周跑一次的流程:确认日志字段 → 按 UA 聚合 → 拉行为画像 → IP 校验 → 判定抓取质量 → 落成放行/拦截策略。

为什么 robots.txt 不能当证据

  • 它只是一个文本文件。爬虫读没读、读了之后守不守,只有日志能回答。
  • 你无法从 robots.txt 得知某个 UA 是否来过、来了多少次、抓了哪些页面。
  • 反过来,日志里出现的 UA 也可能是伪造的,所以需要 IP 校验交叉验证。

一句话:robots.txt 用来看「声明」,日志用来看「事实」,两者要能对上。

步骤 1:确认日志里真的有可用的 UA

nginx 的 combined 格式已经包含 $http_user_agent:

代码语言:nginx
复制
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" '
                '"$http_user_agent"';
access_log /var/log/nginx/access.log main;

先确认字段对不对:

代码语言:bash
复制
tail -n 3 /var/log/nginx/access.log

一行里应该能清楚看到 "GET /some-path HTTP/1.1" 200 12345 "-" "Mozilla/5.0 ..."。如果 UA 位置是个 -,说明日志格式没打开,后面所有分析都无从谈起。

站点前面有 CDN 的情况

源站日志里的 $remote_addr 会是 CDN 回源节点的 IP。这时日志能判断「抓了什么」,不能判断「谁抓的」。两种做法:

  1. 在 nginx 上还原真实客户端 IP(网段填你所用的 CDN 公布的回源段):
代码语言:nginx
复制
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
  1. 或者直接以 CDN 控制台导出的访问日志为准(字段通常包含客户端 IP、UA、URL、状态码、响应字节、时间),源站日志只做内容侧核对。

验证还原是否生效:

代码语言:bash
复制
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -5

如果前几名全是同一两个网段,多半还是 CDN 节点 IP,说明还原没生效。

步骤 2:先看全貌,按 UA 聚合

三十秒版本:

代码语言:bash
复制
awk -F'"' '{c[$6]++} END {for (u in c) printf "%8d  %s\n", c[u], u}' /var/log/nginx/access.log \
  | sort -rn | head -30

这一眼能看到:谁在大量抓、有没有空 UA、有没有可疑字符串混在一堆正常浏览器 UA 里。

历史日志一起看(按天轮转、已压缩的):

代码语言:bash
复制
zgrep -ai 'gptbot' /var/log/nginx/access.log*.gz | wc -l

步骤 3:把一个爬虫的「行为画像」拉出来

光有请求数不够,要看它抓了什么、抓成功没有。保存下面这段为 ai_crawler_report.py(按 combined 格式解析):

代码语言:python
复制
#!/usr/bin/env python3
import re
import sys
import collections

LINE = re.compile(
    r'(?P<ip>\S+) \S+ \S+ \[(?P<ts>[^\]]+)\] '
    r'"(?P<method>[A-Z]+) (?P<path>\S+)[^"]*" '
    r'(?P<status>\d{3}) (?P<size>\S+) "[^"]*" "(?P<ua>[^"]*)"'
)

SUSPECT = re.compile(
    r'gptbot|oai-searchbot|chatgpt-user|claudebot|claude-user|perplexitybot|'
    r'ccbot|bytespider|amazonbot|applebot|meta-externalagent',
    re.I,
)

def main(paths):
    stats = collections.defaultdict(lambda: {
        'hits': 0, 'ips': set(), 'status': collections.Counter(),
        'paths': collections.Counter(), 'first': None, 'last': None,
    })

    for path in paths:
        with open(path, encoding='utf-8', errors='replace') as fh:
            for line in fh:
                m = LINE.match(line)
                if not m:
                    continue
                hit = SUSPECT.search(m.group('ua'))
                if not hit:
                    continue
                b = stats[hit.group(0).lower()]
                b['hits'] += 1
                b['ips'].add(m.group('ip'))
                b['status'][m.group('status')] += 1
                b['paths'][m.group('path').split('?')[0]] += 1
                b['first'] = b['first'] or m.group('ts')
                b['last'] = m.group('ts')

    for name, b in sorted(stats.items(), key=lambda kv: -kv[1]['hits']):
        print(f"\n== {name} ==")
        print(f"  hits={b['hits']}  unique_ips={len(b['ips'])}")
        print(f"  window: {b['first']}  ->  {b['last']}")
        print("  status: " + ", ".join(f"{k}x{v}" for k, v in b['status'].most_common(5)))
        print("  top paths:")
        for p, n in b['paths'].most_common(8):
            print(f"    {n:>7}  {p}")

if __name__ == '__main__':
    main(sys.argv[1:] or ['/var/log/nginx/access.log'])

用法:

代码语言:bash
复制
python3 ai_crawler_report.py /var/log/nginx/access.log

# 历史压缩日志
gunzip -c /var/log/nginx/access.log.1.gz > /tmp/access.1.log
python3 ai_crawler_report.py /tmp/access.1.log

注意:上面那份 UA 名单只是「常见字符串」,用来缩小排查范围,不能当作身份证明——UA 是客户端自己写的,谁都能复制。身份要靠下一步。

步骤 4:用 IP 反查校验 UA 的真假

两条路子,能同时用最好。

① 反解 + 前向确认(FCrDNS):先反查 PTR,再用 PTR 正查回来,看是否回到同一个 IP。

代码语言:bash
复制
check_ip() {
  ip="$1"
  ptr=$(dig +short -x "$ip" | head -1 | sed 's/\.$//')
  printf '%-16s PTR=%s ' "$ip" "${ptr:-<none>}"
  if [ -n "$ptr" ] && dig +short "$ptr" A | grep -qxF "$ip"; then
    echo "FCrDNS=OK"
  else
    echo "FCrDNS=FAIL"
  fi
}

# 只查请求量靠前的 IP,避免对全量做 DNS 查询
awk '{print $1}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -rn | head -50 | awk '{print $2}' \
  | while read -r ip; do check_ip "$ip"; done

② 对方公布的 IP 段:如果爬虫运营方公开了 IP 段文件,下载后用 grep -F -f 或现成的网段比对工具校验。这一条比反解更硬。

两个结果叠加,才有结论:

UA 声明

IP 校验

结论

声称是某个 AI 爬虫

PTR / 网段对得上

基本可信,进入抓取质量判定

声称是某个 AI 爬虫

校验不通过

伪装流量,按普通可疑流量处理

普通浏览器 UA

PTR 指向爬虫网段

有爬虫改了 UA,单独标注

步骤 5:区分「碰过」「真抓」「抓成功」

有的爬虫只取 /robots.txt 就走了,这不算抓过你的内容。三个判据一起看:

  1. 抓的是内容页:路径里大部分是文章页、商品页,而不是只有 /robots.txt、/、/sitemap.xml。
  2. 状态码以 2xx 为主:大量 403/429 说明它被拦了或被限速了;大量 404 说明它在抓已经不存在的 URL。
  3. 有时间跨度:跨多天、有稳定频次的才算「在持续抓」,单日一两次多半是偶发。

状态码分布一行就能出:

代码语言:bash
复制
grep -ai 'GPTBot' /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -rn

再看看它有没有抓到不该给的内容(后台、站内搜索结果、购物车、结算页):

代码语言:bash
复制
grep -ai 'GPTBot' /var/log/nginx/access.log \
  | awk '{print $7}' \
  | grep -aE '(/wp-admin/|/search\?|\?s=|/cart|/checkout)' \
  | sort | uniq -c | sort -rn | head

有输出就说明,「不该抓」的清单需要用服务器层规则落实,而不是写在 robots.txt 里等它自觉。

步骤 6:CDN 日志怎么和源站日志对齐

  • 时区:CDN 日志通常是 UTC,nginx 日志是服务器本地时区。按小时比对之前先统一。
  • 字段:确认导出的是客户端 IP 而不是回源 IP,确认 UA 字段没被截断。
  • 路径:CDN 日志里可能是完整 URL(含协议和域名),源站日志是 path,比对前先归一化。
  • 去向:在 CDN 上被拦掉的请求不会出现在源站日志里。判断「它在抓但被拦了」,只能看 CDN 或 WAF 日志。

一个实用分工:用 CDN 日志判定「谁来过、来了多少」,用源站日志判定「抓到了什么、返回了什么」,两边按时间和 path 关联。

步骤 7:把结论落成策略

有了「哪个爬虫、多久来一次、抓了哪些路径、成功率如何」这张表,策略才有依据。常见的三类处理:

  • 检索型爬虫:希望它抓,重点保证它能拿到干净的 HTML、状态码正常、不被登录墙挡住。
  • 训练型爬虫:按你自己的内容策略决定放行还是拦截。
  • 伪装流量:不看 UA,只看 IP 段和行为,直接按可疑流量处理。

这里要强调:robots.txt 是请求,执行点在服务器 / CDN / WAF。 想让拦截真正生效,规则必须落在 nginx、CDN 或 WAF 上,而不是只写一行 Disallow。

如果站点跑在 WordPress 上,想在站内持续看到「哪些 AI 爬虫在抓、各自抓了多少」,可以用免费插件 AumCrawl,它做的就是这件事,并且能按爬虫分别放行或拦截:crawlers。

每周 15 分钟的执行清单

动作

命令 / 位置

看什么

确认日志在写

tail -n 3 access.log

UA 字段是否存在

UA 全貌

awk -F'"' '{c[$6]++}...'

有没有新增可疑 UA

拉行为画像

ai_crawler_report.py

请求量、路径、状态码分布

IP 校验

check_ip / 官方网段

是否存在伪装

质量判定

状态码 + 路径

是「碰过」还是「真抓」

策略落地

nginx / CDN / WAF

拦截是否真的生效

归档

日志按月打包

便于回溯趋势

两个容易踩的坑

  • 只看一天日志就下结论:抓取频次有波动,单日样本很容易把偶发当常态。
  • 把 UA 当身份:UA 只是客户端自己填的一串字符串。永远用 IP 侧的证据做交叉验证。

做完这一遍,你会得到一张属于自己站点的「AI 爬虫访问清单」——它比任何 robots.txt 都更能说明实际发生了什么。


AumCrawl 是免费插件,可以在 WordPress 后台直接装:AumCrawl。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 为什么 robots.txt 不能当证据
  • 步骤 1:确认日志里真的有可用的 UA
    • 站点前面有 CDN 的情况
  • 步骤 2:先看全貌,按 UA 聚合
  • 步骤 3:把一个爬虫的「行为画像」拉出来
  • 步骤 4:用 IP 反查校验 UA 的真假
  • 步骤 5:区分「碰过」「真抓」「抓成功」
  • 步骤 6:CDN 日志怎么和源站日志对齐
  • 步骤 7:把结论落成策略
  • 每周 15 分钟的执行清单
  • 两个容易踩的坑
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档