假设你收到一条需求:训练用的 AI 爬虫别抓了,但搜索引擎收录和对话里的引用要保住。直觉是打开 robots.txt 写一条 Disallow: /,改完上线,过几天发现两件事对不上——日志里还是有一堆陌生 UA 在跑,而搜索那边的引用量也没了。
下面按 robots.txt 的声明、Nginx 的执行、以及夹在中间的验证与取证分开说。文中的配置可以照着改,域名和 IP 段换成自己的。
先把爬虫按用途分开,否则一定会误伤
同一个厂商的爬虫通常不止一个 UA,职责不同。挡错一个,丢的不是训练语料,是现有的曝光。
| 用途 | 常见 UA token | 挡掉之后发生什么 |
|---|---|---|
| 搜索索引 | Googlebot、Bingbot、OAI-SearchBot、PerplexityBot |
页面不再进入该引擎的索引,后续的引用和跳转都没了来源 |
| 用户触发的检索 | ChatGPT-User、Claude-User、Perplexity-User |
用户在对话里贴你的链接,抓取端拿不到内容,回答里引不出你的页面 |
| 训练语料 | GPTBot、Google-Extended、ClaudeBot、CCBot、Bytespider、meta-externalagent、Applebot-Extended |
不影响当前收录,影响的是未来模型对你的"印象" |
Google-Extended 是个特殊项:它不是一个独立爬虫,没有自己的 UA,抓取动作由 Googlebot 发起,Google-Extended 只控制 Google 是否把这些内容用于生成式模型的训练与 grounding。直接后果是:在 access log 里你永远看不到 Google-Extended 这个 UA;你也没法用 Nginx 按 UA 去拦它,拦 Googlebot 会连搜索一起废掉。这一项只能通过 robots.txt 声明,验证方式是看 Search Console 的 robots.txt 报告是否成功读取并解析。
各家会调整 UA 与用途的对应关系,落地前对一遍官方文档,别照着博客里的老列表抄。
robots.txt 的分组,决定了它只能做"点对点"
robots.txt 是按 User-agent 分组的,一个组内所有规则只对声明了这个 token 的爬虫生效。这条特性正是"只挡一只"的基础。
几个必须记住的匹配规则:
- 一个爬虫只读最匹配它自己的那一组,其余组对它无效。所以
User-agent: GPTBot的组和User-agent: *的组是互相独立的,前者的Disallow: /不会波及后者。 - 组与组之间必须用空行分隔。少一个空行,两条
User-agent会被合并成一组,效果全靠猜。 - 同一组里同时出现
Allow和Disallow时,按路径长度取最长匹配;长度相同时,Google 取Allow。其他实现对这一点的处理不完全一致,所以别在同一组里制造这种冲突。 - 只写
User-agent: GPTBot却不跟任何规则,等于什么都没限制。 Disallow的路径支持前缀匹配,*和$通配在 Google 那边可用,其他爬虫支持程度不一,别把关键策略押在通配上。
一份只挡训练、放行检索的模板:
# 训练 / 语料型:整站拒绝
User-agent: GPTBot
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: Bytespider
Disallow: /
User-agent: meta-externalagent
Disallow: /
User-agent: Applebot-Extended
Disallow: /
# 用户触发 / 检索型:显式放行,防止被上面的规则连带
User-agent: ChatGPT-User
Allow: /
User-agent: OAI-SearchBot
Allow: /
User-agent: Claude-User
Allow: /
User-agent: Claude-SearchBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Perplexity-User
Allow: /
# 其余未点名的爬虫:沿用站点原有策略
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
那些 Allow: / 的显式放行组看起来多余——未点名的爬虫本来就走到 * 组。它们真正的作用是防患于未然:将来有人为了挡一只新出现的训练爬虫,往 User-agent: * 里塞了 Disallow: /,这些具名放行组还能把它们捞回来。
还有一个常见写法错误值得单独说:User-agent: * 不能用来做兜底拒绝。你一旦在里面写 Disallow: /,所有没被具名点到的爬虫——包括你还没听说过的那只搜索爬虫——全部被挡。兜底只能兜"允许"。
robots.txt 管不到的地方
它只是一份声明,没有任何强制力。几样东西在它的作用范围之外:
不遵守声明的抓取方。无论对方是商业爬虫还是批量采集脚本,Disallow 对它们只是一行文本。
伪装 UA 的请求。UA 是一个客户端自己填的请求头,写 GPTBot 不代表它真是 GPTBot。只按 UA 字符串拦截,等于把开关交给了对面。
频率。robots.txt 里的 Crawl-delay 不被主流爬虫支持,就算支持,它约束的也是节奏,管不了总量。
所以服务器层要做的是补上声明缺失的那部分:让不守规矩的成本变高。
顺序很重要:先记录,再限流,最后才封
直接上 403 最常见的后果,是你在源站日志里再也看不到这些爬虫了——CDN 或 WAF 在边缘就把请求吃掉,回源的记录干干净净。半个月后老板问"AI 爬虫到底来了多少",你答不上来,因为你亲手把观测手段删了。
先只记录,不改动作。在 http 块里加 map 和日志格式:
http {
# 按 UA 打标签,注意 map 的正则按顺序匹配,先命中先返回
map $http_user_agent $ai_bot_tag {
default "-";
"~*GPTBot" "gptbot";
"~*OAI-SearchBot" "oai-searchbot";
"~*ChatGPT-User" "chatgpt-user";
"~*ClaudeBot" "claudebot";
"~*Claude-User" "claude-user";
"~*CCBot" "ccbot";
"~*Bytespider" "bytespider";
"~*meta-externalagent" "meta-externalagent";
"~*PerplexityBot" "perplexitybot";
}
log_format ai '$remote_addr\t$time_local\t$status\t$ai_bot_tag\t$http_user_agent';
server {
listen 443 ssl;
server_name example.com;
access_log /var/log/nginx/ai-access.log ai;
# 先注释掉,观察一周再打开
# if ($ai_block) { return 403; }
}
}
制表符分隔是为了后面好解析。跑一周,先看清楚:哪些 UA 在来、来了多少、抓的是哪些路径。用 awk 就能出个粗表:
awk -F'\t' '$4 != "-" {print $4}' /var/log/nginx/ai-access.log \
| sort | uniq -c | sort -rn
如果不想自己搭这套日志流水线,AumCrawl 这个免费插件做的事情就是按 UA 记录 AI 爬虫的命中情况,可以拿来做同样的观察,省掉 map 和 log_format 这一步(插件页)。数据出来之后,再决定拦谁。
限流比封禁更合适
观察完你会发现,真正该处理的多半是一两个抓得特别凶的,未必是整个训练类爬虫。这时候用限流,既能压住量,又不会在对方合规时把你自己的页面从未来的模型里彻底抹掉。
http {
map $ai_bot_tag $limit_key {
default ""; # 空字符串的请求不计入配额
"gptbot" $binary_remote_addr;
"ccbot" $binary_remote_addr;
"bytespider" $binary_remote_addr;
}
limit_req_zone $limit_key zone=ai_rl:10m rate=30r/m;
server {
location / {
limit_req zone=ai_rl burst=10 nodelay;
limit_req_status 429;
# ...
}
}
}
rate=30r/m 是每个 IP 每分钟 30 次,量级按你自己日志里的实际分布调,别照抄。超了返回 429 而不是 403,语义是要求对方放慢,不表示禁止访问,也方便你在日志里区分这两类事件。
UA 可以伪造,所以需要一次来源校验
到这一步才处理"对面自称是谁"的问题。思路是反查:拿来源 IP 做反向 DNS,再把解析出来的主机名正向解析一次,看能不能回到同一个 IP。两次都对上,这个 UA 才有一定可信度。
下面这个脚本读刚才那份制表符日志,只校验能确定后缀的两家,其余不打结论:
#!/usr/bin/env python3
"""从 nginx 日志中抽取自称 Googlebot / bingbot 的 IP,做 forward-confirmed rDNS 校验。"""
import re
import socket
import sys
CLAIMS = {
"googlebot": ".googlebot.com",
"bingbot": ".search.msn.com",
}
LINE = re.compile(
r"^(?P<ip>[^\t]+)\t(?P<time>[^\t]+)\t(?P<status>\d{3})\t(?P<tag>[^\t]+)\t(?P<ua>.*)$"
)
def fcrdns(ip: str):
try:
host = socket.gethostbyaddr(ip)[0]
_, _, addrs = socket.gethostbyname_ex(host)
except OSError:
return None
return host if ip in addrs else None
def main(path: str) -> None:
seen = set()
with open(path, encoding="utf-8", errors="replace") as fh:
for line in fh:
m = LINE.match(line.rstrip("\n"))
if not m:
continue
ip, ua = m.group("ip"), m.group("ua")
for name, suffix in CLAIMS.items():
if name not in ua.lower():
continue
if (name, ip) in seen:
continue
seen.add((name, ip))
host = fcrdns(ip)
ok = bool(host) and host.endswith(suffix)
print(f"{name}\t{ip}\t{host or '-'}\t{'OK' if ok else 'UNVERIFIED'}")
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "/var/log/nginx/ai-access.log")
跑法:
python3 fcrdns_check.py /var/log/nginx/ai-access.log
输出里 UNVERIFIED 的那批,就是自称 Googlebot 但来源对不上的请求。注意这个脚本只对后缀确定的两家下判断,其他爬虫的后缀各有各的公布方式,别凭印象往 CLAIMS 里加。
要把校验结果落到拦截上,Nginx 本身不做反向 DNS,得靠子请求:
location / {
auth_request /botcheck;
# ...
}
location = /botcheck {
internal;
proxy_pass http://127.0.0.1:8081/verify;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-User-Agent $http_user_agent;
}
后端用标准库起一个就够:
#!/usr/bin/env python3
"""auth_request 后端:UA 自称可信爬虫时,用 forward-confirmed rDNS 校验来源 IP。"""
import socket
from http.server import BaseHTTPRequestHandler, HTTPServer
CLAIMS = [
("googlebot", ".googlebot.com"),
("bingbot", ".search.msn.com"),
]
_cache = {
}
def verify(ip: str):
if ip in _cache:
return _cache[ip]
try:
host = socket.gethostbyaddr(ip)[0]
_, _, addrs = socket.gethostbyname_ex(host)
ok = ip in addrs
except OSError:
host, ok = "", False
_cache[ip] = (host, ok)
return _cache[ip]
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
ip = self.headers.get("X-Real-IP", "")
ua = self.headers.get("X-User-Agent", "").lower()
for token, suffix in CLAIMS:
if token not in ua:
continue
host, ok = verify(ip)
if not (ok and host.endswith(suffix)):
self.send_response(403)
self.end_headers()
return
break
self.send_response(204)
self.end_headers()
def log_message(self, *args):
pass
if __name__ == "__main__":
HTTPServer(("127.0.0.1", 8081), Handler).serve_forever()
auth_request 需要 ngx_http_auth_request_module,主流发行版的包默认带。上面这个缓存没有过期时间,生产上给它加个 TTL,否则一次 DNS 抖动会被记很久。还有一点:这只对"自称是某家爬虫"的请求生效,对不报身份的采集脚本无效——那类请求交给限流处理更实际。
上了 CDN 之后要注意的地方
第一个是日志口径。CDN 回源时 $remote_addr 是边缘节点的 IP,不是真实访客。不配 real_ip 的话,限流会把所有流量算到几个节点头上,rDNS 校验也全是节点地址,结论完全失真。
# 把 10.0.0.0/8 换成你所用 CDN 公布的回源网段
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
第二个是观测断层。一旦在 CDN 或 WAF 层做了 UA 拦截,请求根本到不了源站,你的 ai-access.log 里就看不到它们,看起来像是"爬虫不来了"。要么把拦截策略全部放在源站,要么在 CDN 侧也保留一份请求日志,两边对得上账。
改完怎么验:一份可以照着抄的检查单
配置改完别直接走,逐项过一遍:
# 1. robots.txt 本身能不能取到,有没有被缓存成旧版本
curl -s https://example.com/robots.txt | head -40
# 2. 用不同 UA 打一次,看状态码是否符合预期
for ua in \
"Mozilla/5.0 (compatible; GPTBot/1.1; +https://openai.com/gptbot)" \
"Mozilla/5.0 (compatible; ChatGPT-User/1.0; +https://openai.com/bot)" \
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
"Mozilla/5.0 (compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot)"; do
printf '%-78s ' "$ua"
curl -s -o /dev/null -w '%{http_code}\n' -A "$ua" https://example.com/
done
对照预期:训练型应当被限流到 429 或保持 200(如果你只做 robots 声明);ChatGPT-User 和 OAI-SearchBot 必须还是 200。任何一个检索型爬虫出现 403,说明 map 里的正则写宽了,或者 if 的位置放错了。
然后按顺序看这几点:
- robots.txt 里每条
User-agent后面是不是都跟了规则,组之间是不是都有空行。 User-agent: *组里有没有残留的Disallow。- Search Console 的 robots.txt 报告能不能正常抓取并解析——这是
Google-Extended唯一能确认生效的地方。 - 限流打开后,429 的比例是不是集中在你想限的那几个 UA 上,有没有误伤正常访客。
- CDN 侧如果也配了 UA 规则,和源站的规则是不是一致。两边不一致时,以更严的那边为准,容易拦过头。
最后还有一条走不通的路
robots.txt 没有强制力,Nginx 也只能挡住到达你这台机器的请求。遇到持续越界抓取,顺序是:日志留存(完整 UA、IP、时间、路径)→ 通过对方公布的滥用举报渠道联系 → 交给 CDN 或云厂商的滥用通道 → 最后才是法律途径。前面几步成本低、见效快,法律途径在多数情况下不划算,真要走之前找律师,别拿网上的判例当结论。
整套东西的落地顺序只有一句:先让日志说话,再让 robots 表态,最后才让 Nginx 动手。反过来做,你会在两周后发现既没有数据,也没有回头路。