robots.txt 只放搜索引擎、拦住训练型 AI 爬虫:可复制的配置、验证脚本与边界

0 阅读6分钟

NginxSEO爬虫WordPress站点的 AI 爬虫策略最后都会落到一个文件上:robots.txt。但大多数配置只做对了一半——规则写对了,却没人验证;或者把 Disallow: / 当成「内容不会被用」。这篇把配置、验证脚本和边界一次讲完,代码可以直接跑。

一、先分类:谁该放,谁该拦

用途典型 User-agent token建议
搜索索引Googlebot、Bingbot、Baiduspider、YandexBot、DuckDuckBot、Applebot放行,否则丢收录
搜索厂商的模型用途Google-Extended与 Googlebot 分开决策
语料 / 训练GPTBot、ClaudeBot、CCBot、Bytespider按你的内容策略定
用户触发的实时抓取ChatGPT-User、PerplexityBot和训练型分开看,别一刀切

两个提醒:

  • 这些 token 是各家自己公布的,会新增、会改名,配置前以对方文档为准;
  • Google-Extended 不影响 Googlebot 的抓取和收录,它管的是内容是否被用于特定用途——所以「拦 Google-Extended」和「能被 Google 搜到」并不冲突。

二、两种拓扑,选一个

A. 默认放行 + 点名拦(推荐)

User-agent: *
Allow: /

User-agent: GPTBot
Disallow: /

User-agent: ClaudeBot
Disallow: /

优点:新搜索引擎、站点监控、SEO 工具不会被误伤。缺点:新出现的训练爬虫在你补名单之前是放行的,所以必须配日志(第六节)。

B. 默认拒绝 + 点名放

User-agent: *
Disallow: /

User-agent: Googlebot
Allow: /

User-agent: Bingbot
Allow: /

优点:新爬虫默认进不来。缺点:会连社交预览、监控探针、无障碍工具、你没想到的搜索爬虫一起挡掉,等于用收录换安心。除非站点本来就不指望搜索流量,一般不建议。

三、规则语义里的四个坑

  1. 分组靠 User-agent 行,不靠空行。 一个 User-agent 后面连续的 Allow/Disallow 属于同一组,再出现 User-agent 就是新组。用注释分隔,别用空行去赌解析器实现。
  2. 最长匹配优先,长度相同 Allow 赢。
User-agent: *
Disallow: /wp-admin/
Allow: /wp-admin/admin-ajax.php$
  1. 只支持 * 和 $。 * 匹配任意字符,$ 锚定结尾。没有正则,也没有「一个字符通配」。
  2. 路径大小写敏感。 /Blog/ 和 /blog/ 是两条互不相干的规则。

补充一句:Crawl-delay 不是通用语义,主流搜索爬虫不以它为准,限速要去网关层做(见第七节)。

四、一份可以直接抄的配置

# ===== 搜索引擎:放行 =====
User-agent: Googlebot
Allow: /

User-agent: Bingbot
Allow: /

User-agent: Baiduspider
Allow: /

User-agent: YandexBot
Allow: /

User-agent: DuckDuckBot
Allow: /

User-agent: Applebot
Allow: /

# ===== 训练 / 语料型:拦 =====
User-agent: GPTBot
Disallow: /

User-agent: Google-Extended
Disallow: /

User-agent: ClaudeBot
Disallow: /

User-agent: CCBot
Disallow: /

User-agent: Bytespider
Disallow: /

# ===== 兜底:未点名 UA 默认放行,交给日志观察 =====
User-agent: *
Allow: /
Disallow: /wp-admin/
Allow: /wp-admin/admin-ajax.php$

Sitemap: https://example.com/sitemap.xml

几处说明:

  • Sitemap 行写绝对 URL,它和 User-agent 组之间没有从属关系;
  • Disallow: /wp-admin/ 只是减少无意义抓取,不是安全措施——robots.txt 是公开文件,写进去的路径等于告诉别人这里有什么;
  • 训练型那几个 UA 段按实际策略增删,不要照抄自己都没想清楚的 token;
  • 有多个域名或多个入口时,每个入口都要各自放一份,别只改主站。

五、写个判定器先验一遍

肉眼读 robots.txt 很容易读错,尤其是涉及最长匹配和多组规则的时候。下面这段是完整可运行的判定器,实现了分组、*/$ 通配和最长匹配优先:

# robots_check.py
import re
import urllib.request

ROBOTS_URL = 'https://example.com/robots.txt'
TARGETS = [
    ('Googlebot', '/blog/hello-world/'),
    ('Bingbot', '/blog/hello-world/'),
    ('GPTBot', '/blog/hello-world/'),
    ('ClaudeBot', '/blog/hello-world/'),
    ('CCBot', '/'),
    ('SomeNewBot', '/'),
]


def fetch(url):
    req = urllib.request.Request(url, headers={'User-Agent': 'robots-checker'})
    with urllib.request.urlopen(req, timeout=10) as resp:
        return resp.read().decode('utf-8', 'replace')


def parse(text):
    groups, cur, closed = [], None, True
    for raw in text.splitlines():
        line = raw.split('#', 1)[0].strip()
        if ':' not in line:
            continue
        key, val = line.split(':', 1)
        key, val = key.strip().lower(), val.strip()
        if key == 'user-agent':
            if closed or cur is None:
                cur = {'uas': [], 'rules': []}
                groups.append(cur)
                closed = False
            cur['uas'].append(val)
        elif key in ('allow', 'disallow') and cur is not None:
            cur['rules'].append((key, val))
            closed = True
    return groups


def pattern_len(pattern, path):
    if pattern == '':
        return -1
    if '*' not in pattern and '$' not in pattern:
        return len(pattern) if path.startswith(pattern) else -1
    body = ''
    for ch in pattern:
        if ch == '*':
            body += '.*'
        elif ch == '$':
            body += '$'
        else:
            body += re.escape(ch)
    return len(pattern) if re.match('^' + body, path) else -1


def is_allowed(groups, ua, path):
    ua_l = ua.lower()
    best = None
    for g in groups:
        if not any(u == '*' or u.lower() in ua_l for u in g['uas']):
            continue
        for kind, pat in g['rules']:
            n = pattern_len(pat, path)
            if n < 0:
                continue
            cand = (n, 1 if kind == 'allow' else 0)
            if best is None or cand > best:
                best = cand
    return True if best is None else bool(best[1])


if __name__ == '__main__':
    groups = parse(fetch(ROBOTS_URL))
    for ua, path in TARGETS:
        verdict = 'ALLOW' if is_allowed(groups, ua, path) else 'DENY'
        print(f'{ua:<12}{path:<24}{verdict}')

按第四节的配置跑,输出大致是:

Googlebot   /blog/hello-world/     ALLOW
Bingbot     /blog/hello-world/     ALLOW
GPTBot      /blog/hello-world/     DENY
ClaudeBot   /blog/hello-world/     DENY
CCBot       /                     DENY
SomeNewBot  /                     ALLOW

最后一行是关键:默认放行拓扑下,任何没被点名的 UA 一定是 ALLOW。这就是为什么日志不能省。

六、日志里看到的才算数

robots.txt 没有强制力,判断它有没有生效只能看请求。nginx combined 格式里,UA 是第 6 个双引号字段:

# 1) 各 UA 的请求量排名
awk -F'"' '{print $6}' /var/log/nginx/access.log \
  | awk '{print $1}' \
  | sort | uniq -c | sort -rn | head -20
# 2) 被你在 robots.txt 里拦掉的 UA,实际还来了多少
awk -F'"' '$6 ~ /GPTBot|CCBot|ClaudeBot|Bytespider/ {print $6}' /var/log/nginx/access.log \
  | awk '{print $1}' | sort | uniq -c | sort -rn
# 3) 这些请求打了哪些路径
awk -F'"' '$6 ~ /GPTBot/ {print $2}' /var/log/nginx/access.log \
  | awk '{print $2}' | sort | uniq -c | sort -rn | head -20

如果被拦的 UA 还在大量出现,只有三种解释:对方不遵守、UA 是伪造的、或者你的配置没生效(缓存、多域名、CDN 边缘没同步)。这三种在日志里长得一模一样,得结合路径和来源 IP 再分辨。

要按爬虫分别看进站量、分别放行或拦截,可以用 AumCrawl,它做的就是「看见哪些 AI 爬虫在抓你的站,并按爬虫分别放行或拦截」这一件事。

七、边界:robots.txt 到此为止

配完之后,必须清楚它能做和不能做的:

  • 不遵守的爬虫:协议没有强制力,也没有惩罚机制,写了不等于执行了;
  • UA 可以伪造:改一行 header 就绕过去了,服务端看到的 UA 只是对方的一句自述;
  • 表达不了「可检索、不可训练」:除非厂商自己把用途拆成独立 token(Googlebot / Google-Extended 就是拆过的例子),否则你只能整段放或整段拦;
  • 不能限速:它管「哪些路径」,不管「多少次、多快」;
  • 不能撤回:已经被抓走的内容,事后改 robots.txt 也拿不回来;
  • 挡不住间接获取:搜索快照、聚合站、他人转载、第三方数据集,都不经过你的 robots.txt。

所以完整的策略是分层的:robots.txt 表达意愿,网关做限速与拒绝,内容分级控制暴露面,必要时再上法务手段。给 AI 侧一个「欢迎读什么」的说明(llms.txt 这类社区提案)也可以有,但它同样是引导性的,没有强制力。

八、WordPress 站怎么落地

WordPress 的 robots.txt 默认是动态输出的,没有物理文件。用 robots_txt filter 追加最省事:

add_filter('robots_txt', function ($output, $public) {
    if (! $public) {
        return $output;
    }

    $extra = <<<TXT

# ===== custom: block training crawlers =====
User-agent: GPTBot
Disallow: /

User-agent: Google-Extended
Disallow: /

User-agent: CCBot
Disallow: /

User-agent: ClaudeBot
Disallow: /
TXT;

    return $output . $extra;
}, 10, 2);

两个注意点:

  • 站点根目录一旦存在物理 robots.txt,WordPress 就不再输出虚拟版本,改 filter 也不会生效;
  • 上 CDN 或多域名时,每个入口都要单独 curl 验一遍,别只看源站。
curl -s https://example.com/robots.txt | grep -iE 'user-agent|allow|disallow|sitemap'

九、上线前清单

  • 搜索爬虫逐条列出并放行
  • 训练型爬虫按策略点名拦截
  • 判定器脚本跑过,结果符合预期
  • 连续观察一周日志,确认被拦 UA 的实际行为
  • 网关层有兜底限速,不依赖 Crawl-delay
  • Sitemap 指向真实存在的地址

最后一句话:内容策略决定「值不值得抓」,robots.txt 只决定「允不允许抓」。把这两件事分开想,就不会写成拍脑袋的 Disallow: /。


如果还想让 AI 侧准确知道你希望它读什么、怎么读,可以从 AumViso 的 llms.txt 与结构化数据入手——它是引导,不是强制,配合上面的配置一起用才完整。