跳到正文
阿里雲開發者社區 AI·· 4 小時前AI 評分61

只阻擋訓練型 AI 爬蟲、放行搜尋與引用:robots.txt 分組、Nginx 攔截與衝突驗證

只挡训练型 AI 爬虫、放行搜索与引用:robots.txt 分组、Nginx 拦截与冲突验证

AI 導讀

指南整理如何按用途區分 AI 爬蟲,透過 robots.txt 分組及 Nginx 記錄、限流和來源校驗,限制訓練抓取並保留搜尋索引與對話引用。Google-Extended 沒有獨立 UA,不能在 Nginx 按 UA 攔截,只能透過 robots.txt 設定,並在 Search Console 檢查解析結果。

正文

简介: 假设你收到一条需求:训练用的 AI 爬虫别抓了,但搜索引擎收录和对话里的引用要保住。直觉是打开 robots.txt 写一条 Disallow: /,改完上线,过几天发现两件事对不上——日志里还是有一堆陌生 UA 在跑,而搜索那边的引用量也没了。 下面按 robots.txt 的声明、Nginx 的执行、以及夹在中间的验证与取证分开说。文中的配置可以照着改,域名和 IP 段换成自己的。

假设你收到一条需求:训练用的 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 动手。反过来做,你会在两周后发现既没有数据,也没有回头路。

來源:阿里雲開發者社區 AI · developer.aliyun.com