CPython 标准库 urllib.robotparser 深度解析:RobotFileParser 如何解析 robots.txt 并判定 URL 可抓取性
本文以 CPython 标准库文档 urllib.robotparser.rst 为主体,系统讲解 RobotFileParser 类的完整 API、典型用法与行为边界,并深入到 Lib/urllib/robotparser.py 源码,剖析其状态机解析、规则最长匹配、User-Agent 匹配以及 HTTP 错误码处理等底层机制。读完后,你将能够编写合规的 Web 爬虫抓取器,准确回答“某个 user agent 能否抓取某个 URL”这一问题,并理解 crawl-delay、request-rate、sitemap 等指令在实现层面的真实语义。
1. 模块定位:为爬虫回答“这个 URL 能不能抓”
urllib.robotparser 提供 RobotFileParser 这一个类,用于读取某个网站发布的 robots.txt 文件,并回答“特定 user agent 是否允许抓取该网站上的某个 URL”。robots.txt 的规范格式由 RFC 9309 定义——源码文件头部的注释 也明确声明本模块是 "implemented as specified in RFC 9309"。
该类只关注一个 robots.txt,其内部状态包括:
entries/groups:解析出的全部条目(Entry)及按 user agent 建立的索引;disallow_all/allow_all:两个全局开关,由read()阶段的 HTTP 错误码决定(详见第 4 节);sitemaps:Sitemap:指令的收集列表;last_checked:最近一次抓取robots.txt的时间戳,供长驻爬虫判断是否需要重新读取。
源码位于 Lib/urllib/robotparser.py,对外仅导出 RobotFileParser(__all__ = ["RobotFileParser"]),另外定义了模块级命名元组 RequestRate = collections.namedtuple("RequestRate", "requests seconds") 作为 request_rate() 的返回值类型。
2. RobotFileParser 完整 API
构造函数签名为 RobotFileParser(url=''),url 可以是 robots.txt 的 URL 字符串,也可以是 urllib.request.Request 对象(这一点是本文所在开发版本新增的能力,文档以 versionchanged:: next 标注;结合 Include/patchlevel.h 中 PY_VERSION "3.16.0a0" 可确认这是当前主干开发版本 3.16 的新特性)。
2.1 set_url(url)
设置 robots.txt 的 URL,参数同样支持字符串或 Request 对象。源码 set_url 实现 显示:若传入的是 Request 对象则取其 full_url,随后用 urllib.parse.urlsplit 拆分出 host 和 path。这意味着通过 Request 传入自定义 User-Agent 请求头后,服务端会据此返回不同的 robots.txt 内容。
2.2 read() 与 parse(lines)
read():发起 HTTP 请求读取robots.txt并喂给解析器。源码 read 实现 中,成功路径为f.read()后按 UTF-8(surrogateescape错误处理)解码、splitlines()再交给parse()。parse(lines):直接解析行列表,不发起网络请求。这是单元测试中绕过网络的方式(见 test_robotparser.py 的BaseRobotTest.setUp就是io.StringIO(self.robots_txt).readlines()后直接parse)。
2.3 can_fetch(useragent, url)
根据已解析的规则返回 True/False。行为要点(源码 can_fetch 实现):
disallow_all为真时直接返回False,allow_all为真时返回True;- 未调用过
read()(last_checked为 0)时返回False——源码注释说明这是"宁可漏抓、不可误抓"的保守设计,防止用户在read()之前误调can_fetch()产生假阳性; - 对 url 先做归一化(
normalize_uri),空 URL 视为/; /robots.txt本身被隐式允许(RFC 9309 的隐含规则),返回True;- 其余情况按
_find_entry(useragent)找到条目后交给entry.allowance(url)做规则匹配。
2.4 mtime() 与 modified()
mtime() 返回上次抓取 robots.txt 的时间(即 last_checked),modified() 将其重置为当前时间。这对长驻爬虫很关键:周期性检查站点 robots.txt 是否更新时,以 mtime() 作为缓存失效依据。注意 parse() 一进来就会调用 self.modified()(parse 源码),所以即使直接 parse 本地行而不走 read(),mtime() 也不会为 0。
2.5 crawl_delay(useragent)(Python 3.6 加入)
返回指定 user agent 对应条目的 Crawl-delay 值;无该参数、参数不适用、或语法非法时返回 None。注意其前提是先有过 mtime()(未读取过直接返回 None)。
2.6 request_rate(useragent)(Python 3.6 加入)
返回 Request-rate 参数值,类型为命名元组 RequestRate(requests, seconds),同样在不适用或语法非法时返回 None。
2.7 site_maps()(Python 3.8 加入)
返回 Sitemap: 参数内容构成的列表;无该参数时返回 None。源码 site_maps 实现 很简单:self.sitemaps 非空即返回列表。
3. 官方文档示例:两种典型用法
3.1 基本用法(直接传 URL 字符串)
以下示例继承自官方文档(对应 NetworkTestCase 测试所访问的真实站点 http://www.pythontest.net/):
>>> import urllib.robotparser
>>> rp = urllib.robotparser.RobotFileParser()
>>> rp.set_url("http://www.pythontest.net/robots.txt")
>>> rp.read()
>>> rrate = rp.request_rate("*")
>>> rrate.requests
1
>>> rrate.seconds
1
>>> rp.crawl_delay("*")
6
>>> rp.can_fetch("*", "http://www.pythontest.net/")
True
>>> rp.can_fetch("*", "http://www.pythontest.net/no-robots-here/")
False
从该站点的 robots.txt 可见:Request-rate: 1/1、Crawl-delay: 6 生效于通配 agent;/no-robots-here/ 路径被 Disallow 覆盖,故 can_fetch 返回 False。
3.2 使用 Request 对象携带 User-Agent 头
set_url() 支持传入带自定义请求头的 Request 对象(本开发版本新特性)。有些站点会按访问方的 User-Agent 返回不同 robots.txt:
>>> import urllib.robotparser
>>> import urllib.request
>>> rp = urllib.robotparser.RobotFileParser()
>>> rp.set_url(urllib.request.Request("http://www.pythontest.net/robots.txt", headers={"User-Agent": "IsraBot"}))
>>> rp.read()
>>> rp.can_fetch("*", "http://www.pythontest.net/")
True
>>> rp.can_fetch("*", "http://www.pythontest.net/no-robots-here/")
False
这一能力在测试中也有对应验证:UserAgentSiteTestCase 构造了一个会拒绝 Python-urllib 前缀 UA(返回 403)、放行其他 UA 的本地站点,验证了"默认 UA 触发 403 → disallow_all 置位"与"换用 cybermapper UA → 正常解析"两条路径,这正是自定义 User-Agent 请求头的实战价值所在。
4. read() 的 HTTP 错误码语义:4xx 放行,5xx/403 全禁
read() 对 HTTP 错误的处理是 RFC 9309 语义落地的关键,源码 read 异常分支 分三类:
| HTTP 状态码 | 处理 | 语义依据 |
|---|---|---|
| 401 / 403 | disallow_all = True |
访问 robots.txt 被拒,通常意味着整站禁止抓取 |
| 其他 4xx(含 404) | allow_all = True |
RFC 9309 第 2.3.1.3 节:爬虫可以访问服务器上任何资源 |
| 5xx | disallow_all = True |
RFC 9309 第 2.3.1.4 节:爬虫必须假设完全禁止 |
注意一个易忽略的细节:401/403 被单独拎出来归入"全禁",而其他 4xx(例如 404 找不到文件、418 Teapot)则放行。这与 LocalNetworkTestCase 中的四组用例一一对应:testForbidden(403 → 不可抓)、testNotFound(404 → 可抓)、testTeapot(418 → 可抓)、testServiceUnavailable(503 → 不可抓)。
5. parse() 状态机:如何把文本行变成条目
parse(lines) 用三状态有限状态机工作(源码):
- 状态 0:起始状态,尚未见到
User-agent; - 状态 1:已见到
User-agent行; - 状态 2:已见到
Allow/Disallow行。
逐行处理流程为:先截掉 # 起的注释并 strip(),空行跳过;再按第一个 : 切成指令名与参数,指令名统一转小写。各指令的行为:
user-agent:若状态已是 2(当前 entry 已有规则行)则先把当前 entry 入库并新建一个;否则把 token 追加进entry.useragents,进入状态 1。一个 entry 可以挂多个User-agent行(对应同一组规则)。disallow/allow:状态非 0 时构造RuleLine(path, allowance)追加进entry.rulelines,进入状态 2;构造抛ValueError时静默丢弃该行。crawl-delay:先校验line[1].strip().isdecimal()防止非法语法导致崩溃,合法才int()存入entry.delay。request-rate:按/拆成两段且都必须为十进制整数,才构造RequestRate元组存入entry.req_rate。sitemap:与 user-agent 无关,直接追加进self.sitemaps,不改变解析状态机状态(源码注释引用了 sitemap 协议中"该指令与 User-agent 行无关,可放文件任意位置"的约定)。
循环结束后若状态非 0,收尾的 entry 也会被入库。文档特别提到解析器容忍 User-agent 行前面没有空行的宽松写法(源码注释 "We allow that a user-agent: line is not preceded by one or more blank lines")。
_add_entry 在入库时按小写 user agent 建立 groups 索引;同一 agent 已存在时用 merge_entries 合并:useragents 取交集、rulelines 顺序拼接、delay/req_rate 以新条目非 None 值优先(merge_entries 源码)。
6. 规则匹配核心:RuleLine、通配符与最长匹配
6.1 RuleLine:一条 Allow/Disallow 规则
RuleLine 构造 的关键行为:
- 空路径 + Disallow 视为 Allow 全部(RFC 9309 中
Disallow:留空是"全部放行"的经典写法,见 CrawlDelayAndCustomAgentTest 中cybermapper条目的Disallow:空值); **连续星号折叠为单个*;- 支持
$结尾表示全匹配(fullmatch),$出现在路径中间则抛ValueError(该行在parse阶段被静默丢弃); - 含
*的规则经translate_pattern编译成正则;不含*的走纯字符串前缀/全等比较,避免正则开销。
translate_pattern 把路径按 * 切段并 re.escape 各段,中间段包成 (?>.*?段)(原子组,回溯时不回头重匹配该段),末段变成 .*段,最终用 match/fullmatch 锚定匹配。这比朴素 .* 展开更高效且行为一致。
RuleLine.applies_to(filename) 返回匹配长度 + 1(不匹配返回 0),这个"匹配长度"正是最长匹配裁决的度量。
6.2 Entry.allowance:最长匹配优先,平局禁抓优先
Entry.allowance 遍历所有 rulelines,取 applies_to 返回值最大(匹配最长)的规则作为最终裁决;长度相同且当前裁决是允许时,若遇到禁抓规则则翻转为禁抓——即"最长匹配优先,平局时 Disallow 优先",这正是 RFC 9309 的匹配算法。
6.3 User-Agent 匹配:子串匹配而非全等
_find_entry(useragent) 的查找顺序(源码):
- 精确命中
groups(小写); - 遍历所有 entry,用
Entry.applies_to判断; - 兜底取
*条目; - 都找不到返回
None——can_fetch视其为允许。
Entry.applies_to 的规则值得注意:查询方 UA 先按 / 截断取名称部分并小写(如 MyBot/2.1 → mybot),条目侧的 agent(非 *)小写后做子串包含判断。因此条目 user-agent: bot 会匹配 MyBot——这是实现事实,编写爬虫识别自身条目时要意识到这种宽松匹配。
6.4 URL 归一化
匹配前,待判定 URL 会经 normalize_uri 做百分号编码往返归一化(先 unquote 再 quote,surrogateescape 兜底),查询串逐段归一;规则模式侧则由 normalize_pattern 做同样的处理(但保留 *、$ 通配字符)。can_fetch 中还用私有接口 _urlsplit(url, '') 保留空 query,源码 TODO 注明这是等待公开 API 支持该特性的临时方案。
7. 测试用例如何印证上述机制
Lib/test/test_robotparser.py(约 880 行)是该模块行为的权威验证:
- 通配符条目:UserAgentWildcardTest 验证
User-agent: *下Disallow: /cyberworld/map/、Disallow: /foo.html的边界(/、/test.html可抓,/foo.html整页禁抓)。 - RFC 9309 第 5.1 节官方示例:SimpleExampleTest 完整复刻了 RFC 中 foobot/barbot/bazbot/quxbot 四组 agent 的 good/bad URL 清单,覆盖
Disallow: *.gif$的 glob 全匹配、多User-agent共享条目(barbot/bazbot)以及只有 UA 行没有规则行(quxbot,等价全放行)等情形——这是与规范逐条对齐的回归测试。 - Crawl-delay / Request-rate:CrawlDelayAndCustomAgentTest 用
Crawl-delay: 1与Request-rate: 3/15验证参数解析与"空 Disallow 全放行"。 - Sitemap:SitemapTest 断言
site_maps()返回两个 sitemap URL 列表。 - HTTP 错误码分支:LocalNetworkTestCase 用本地
HTTPServer模拟 403/404/418/503 四种状态码,逐条验证第 4 节表格中的disallow_all/allow_all结果。 - User-Agent 相关站点:UserAgentSiteTestCase 验证了以
Request对象携带自定义 UA 读取不同 robots.txt 的完整链路。 - 真实网络回归:NetworkTestCase 依赖
network支持,断言pythontest.net站点的Nutchagent 对根路径禁抓、对/brian放行等,与官方文档示例同源。
8. 实战要点与常见误区小结
结合文档与源码,使用 RobotFileParser 时值得牢记的边界行为:
- 必须先
read()(或parse())再can_fetch():未读取时can_fetch一律返回False,crawl_delay/request_rate一律返回None; /robots.txt永远隐式允许,即使规则里Disallow: /也拦不住它;- 404 是"放行"而非"错误":站点没放 robots.txt 时
allow_all置位,所有can_fetch返回True;但 403/401/5xx 一律全禁; Disallow:空值 = 全部允许,这是为特定 agent 开绿灯的惯用写法;$锚定与*通配只支持 RFC 9309 的 glob 语法,$必须位于路径末尾,否则整条规则被忽略;- 长驻爬虫的缓存策略:用
mtime()记录上次抓取时间,周期性read()刷新;read()内部会自动刷新时间戳(经parse中的modified()); - UA 匹配是子串语义:查询 agent 名截掉版本号后,与条目 agent 做小写子串匹配,命中精确条目 → 模糊条目 →
*兜底; - 解析对语法错误(非法
crawl-delay、坏request-rate、$位置错误)采取宽容跳过策略,不会抛异常中断整个文件解析。
以上机制使 urllib.robotparser 成为 CPython 标准库中一个麻雀虽小五脏俱全的组件:不到 400 行的 robotparser.py 完整实现了 RFC 9309 的抓取判定、通配符匹配与 HTTP 错误语义,配合 urllib.request 即可构建出既尊重站点抓取协议、又具备限速与 sitemap 发现能力的合规爬虫。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00