封全站判“允许“,封目录判“禁止“:tri-geo 体检 74 分那次我拆了 31 行 judge_ua

你刚把 robots.txt 里五家爬虫全改成了 Disallow: /------字节的 Bytespider、百度的 Baiduspider、360、搜狗、神马,一个没留。改完顺手跑一次 GEO 体检,想看看分数会掉成什么样。

74 分。跟改之前一模一样,一个数字都没动。

我第一反应是自己改错文件了。回去 cat 了一遍,五条 Disallow 好好地躺在里面。那就只剩一个可能:体检脚本压根没读懂这几行。

31 行里出问题的只有一条正则

tri-geo 的爬虫判定全在 scripts/site_signal.pyjudge_ua(401--431 行)。它逐行扫 robots.txt,把对当前 UA 生效的行收进 applicable,再遍历这个列表下结论:

python 复制代码
# scripts/site_signal.py:420-425
for line in applicable:
    if re.match(r"(?i)^disallow\s*:\s*/?\s*$", line):
        return True
    if re.match(r"(?i)^disallow\s*:", line):
        return False
return True

第一条正则本意是处理 Disallow: 后面跟空值的情况------按 robots 规范空值等于什么都不禁,返回 True 没毛病。可 \s*/?\s*$ 里那个 /?Disallow: / 也匹配上了,而 Disallow: / 恰恰是"禁止全站"的标准写法。

于是就有了开头那一幕:封得越彻底越放行,只封一个子目录反倒被判禁止。

同一个 UA,六种写法跑下来是这样:

robots.txt 片段 真实语义 judge_ua
Disallow: / 禁止全站 True(允许)
Disallow: /admin 禁止 /admin False(禁止)
Disallow: 空值,允许全部 True(允许)
Allow: / + Disallow: / 禁止全站 True(允许)
Disallow: / + Allow: /public 除 /public 外全禁 True(允许)
无该 UA 声明 未声明即允许 True(允许)

六行里翻了两行,翻的还都是"全站封禁"这两种最常见的写法。

还有个细节值得单独说:applicable 这个列表不分 UA 组。User-agent: * 下面的 Allow: /,会和后面 User-agent: BytespiderDisallow: / 收进同一个列表,遍历时先撞上哪条就按哪条返回,跟这行究竟属于哪个组没关系。"先放行全站、再单独封某一家"是真实 robots.txt 里最常见的写法,这一版实现认不出来------实测一律返回 True。

连带后果:那道封顶门永远够不着

这条正则不只是让分数偏高。site_signal.py:463 还挂了一道一票否决:

python 复制代码
# scripts/site_signal.py:463-465
if crawl_detail.get("blocked_verified_crawlers") == sum(1 for _, _, v in CRAWLERS if v):
    veto.append({"code": "CRAWLER_BLOCKED_ALL", "type": "cap60",
                 "message": "全部已核验 AI/搜索爬虫被 robots.txt 禁止,评分封顶 60"})

触发条件是被封的已核验爬虫数等于 5。而 blockedjudge_ua 累加出来的------Disallow: / 判 True,永远加不上去。

同一份 good-page 夹具,我换了三种 robots 喂进去:

场景 rc 总分 封顶 crawl_llms 被封 veto
不喂 robots / llms.txt 0 66 --- 0(status=missing) ---
正常 robots + llms.txt 0 74 --- 85 0
5 家全 Disallow: /(真封全站) 0 74 --- 85 0
5 家全 Disallow: /private(只封一个目录) 1 60 60 15 5 CRAWLER_BLOCKED_ALL
只封 Bytespider(/private 写法) 0 73 --- 71 1

第三行就是开头那次。全站封死,跟完全开放时一个字节都不差。第四行封的是个站点上从没存在过的 /private,反倒被封顶 60。

我个人最在意的不是分数高了几分,是这道封顶门从设计到实现都挺像样,唯独触发路径被上游堵死了。一个从来不会响的警报比没装警报更坑人------它会让你以为自己有兜底。

顺带撞见的第二件事:自己给自己当来源

拆完 robots 我顺手翻了 citation_check.py。它有两份词表:UNSOURCED_CLAIM_RE 抓疑似捏造句式,SOURCE_HINT_RE 判断这段话有没有声明来源。判定是"触发了数据/研究类表述 没有来源声明"才记 risky。

问题是有两个词同时躺在两份表里。

text 复制代码
研究表明   UNSOURCED_CLAIM_RE 命中   SOURCE_HINT_RE 命中   → 永不告警
研究显示   UNSOURCED_CLAIM_RE 命中   SOURCE_HINT_RE 未命中
数据显示   UNSOURCED_CLAIM_RE 命中   SOURCE_HINT_RE 命中   → 永不告警
统计显示   UNSOURCED_CLAIM_RE 命中   SOURCE_HINT_RE 未命中
权威机构   UNSOURCED_CLAIM_RE 命中   SOURCE_HINT_RE 未命中
专家表示   UNSOURCED_CLAIM_RE 命中   SOURCE_HINT_RE 未命中

两句本质一样的空头断言,实测待遇相反:

bash 复制代码
$ python -c "import citation_check as c; print(len(c.detect_unsourced('研究表明,采用结构化数据的页面被引擎引用的概率明显更高。')))"
0          # 放行

$ python -c "import citation_check as c; print(len(c.detect_unsourced('权威机构指出,该指标将持续走高。')))"
1          # 拦截

同一份"疑似捏造句式"词表,两个最高频的词拿到了豁免,剩下四个没有。你要是写稿习惯用"研究表明"开头,这条检测对你等于不存在。

旁边那个 DATA_CLAIM_RE 也有类似的取舍。它只认 %、万、亿、元、倍和四位年份,"提升了 34 个百分点"实测不触发数据主张检测,改写成"提升 34%"才触发。把"7 天试用""3 家客户"这类泛指量词排除掉是刻意为之(脚本注释里写了,怕误报),方向我认,可"个百分点"和中文数字也一并被排除了。

这里插句题外话。我另一边在做产品雷达鸭,它攒竞品资料时会给每条结论标出处,标不出来的宁可整条丢掉,也不写"研究显示"。所以看到这种"自己给自己发通行证"的判定我会格外敏感------可能反应过度了三秒,但方向我觉得没偏。

我怎么改的

按组解析 robots,别把不同 UA 组的规则混进一个列表;Disallow: / 归禁止,空值归允许:

python 复制代码
def judge_ua_fix(robots: str, ua: str) -> bool:
    groups, names, lines = [], [], []
    for raw in robots.splitlines():
        line = raw.split("#", 1)[0].strip()
        if not line:
            continue
        if line.lower().startswith("user-agent:"):
            if names:
                groups.append((names, lines))
            names, lines = [line.split(":", 1)[1].strip().lower()], []
        elif names:
            lines.append(line)
    if names:
        groups.append((names, lines))
    target, best, best_len = ua.lower(), None, -1
    for g_names, g_lines in groups:          # 取最长匹配的那组,别串组
        for n in g_names:
            if n == "*" or target.startswith(n):
                if len(n) > best_len:
                    best, best_len = g_lines, len(n)
    if best is None:
        return True
    for line in best:
        if line.lower().startswith("disallow:"):
            v = line.split(":", 1)[1].strip()
            return False if v else True      # "/" 封全站,"" 允许全部
    return True

六种写法回测,翻面的三处全部归位;五家 Disallow: /blocked=5,封顶门正常触发,rc=1、总分 60。

重来一次我大概会直接用标准库的 urllib.robotparser,而不是自己撸这 31 行。评分脚本里造轮子真不划算------Allow 优先、最长匹配、通配符这些边界,自己写一遍基本等于把前人踩过的坑重新踩一遍。

还有一处对不上

crawl_llms 在不喂 robots 时记 0 分并标 status=missing,但这个 0 照样按 10% 权重进总分。同一份页面,喂不喂 robots 差了 8 分(66 → 74)。脚本头注释第 16 行写着"未提供的输入维度不计分也不猜测",实现里是计了 0 分。这两处得改一个,现在谁也说不清 66 分到底代表"没查"还是"很差"。

llms.txt 那一档我也顺手摸了下:分档是 0/30/50/70/90,只看标题数和链接数,没标题没链接给 30,有 1 个标题且链接 ≤2 给 50,标题 ≥2 且链接 ≥3 才 70。我丢了个只有 1 行标题、1 条链接的文件进去拿到 50 分------比不写多一半。这套分档我理解是鼓励先有再好,可一旦进了评分,它就容易被当成"llms.txt 已经做了"的证据,而实际内容可能就一句话。

我到现在也没想明白,一个标着一票否决的封顶门是怎么在验收里活下来的------T4/T5 覆盖的是捏造和广告法红线,爬虫封顶这一档没有对应用例。下个版本我打算先拿这五种 robots 写法做成固定夹具塞进 tests/fixtures,不然修完还会再漂。


我是老三,10+ 年软件开发经验,软件设计师、注册人工智能工程师;专注鸿蒙 ArkTS 北向开发与 Web 前端,也在折腾 AI 自动化,不定期在 CSDN 写点实战踩坑。

本文遵循 MIT 协议,转载请注明出处。

安装:skillhub install tri-geo

相关推荐
赋创小助手1 小时前
Qwen3.8-27B 本地推理 Benchmark 解析:llama.cpp、vLLM、SGLang 与长 Context 的性能差异
服务器·人工智能·大模型·qwen·vllm·sglang·context长度
Wendy不吃榴莲1 小时前
Seko教程:AI 短视频分镜怎么做?零基础用 6 格讲清一个 30 秒故事(2026 最新版)
人工智能
YOLO数据集集合1 小时前
无人机低空影像语义分割数据集 | 语义分割 遥感影像 无人机低空 地物分类 Potsdam Vaihingen LoveDA 9080期
人工智能·深度学习·yolo·目标检测·计算机视觉·语义分割·无人机数据集
海宇数据1 小时前
零信任架构实战:基于海宇手机消费区间验证构建自动化信用分类网关
人工智能·ai·工具分享
连线Insight1 小时前
逃离Agent坟场:企业需要产业智能体操作系统
人工智能
码流子1 小时前
几万路监控怎么接:高速公路视频汇聚平台的架构设计与落地坑点
大数据·人工智能·物联网·算法·架构
2601_949499941 小时前
工业 POF 器件国产化新思路:芯瑞科技DT‑2532Z 解决安华高 HFBR‑2532Z 供应链卡点
运维·网络·人工智能·科技·光模块
zhangfeng11331 小时前
cann 华为npu 算子开发直接访问L2高速缓存 的可行性分析
人工智能·ai编程·算子·npu·cann
fanxiaohui121381 小时前
2026符合上飞机标准的充电宝推荐南孚传应,质量稳定又安全
运维·服务器·人工智能·科技