首页
/ CPython 标准库 urllib.robotparser 深度解析:RobotFileParser 如何解析 robots.txt 并判定 URL 可抓取性

CPython 标准库 urllib.robotparser 深度解析:RobotFileParser 如何解析 robots.txt 并判定 URL 可抓取性

2026-09-07 16:44:31作者:牧宁李

本文以 CPython 标准库文档 urllib.robotparser.rst 为主体,系统讲解 RobotFileParser 类的完整 API、典型用法与行为边界,并深入到 Lib/urllib/robotparser.py 源码,剖析其状态机解析、规则最长匹配、User-Agent 匹配以及 HTTP 错误码处理等底层机制。读完后,你将能够编写合规的 Web 爬虫抓取器,准确回答“某个 user agent 能否抓取某个 URL”这一问题,并理解 crawl-delayrequest-ratesitemap 等指令在实现层面的真实语义。

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 节);
  • sitemapsSitemap: 指令的收集列表;
  • 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.hPY_VERSION "3.16.0a0" 可确认这是当前主干开发版本 3.16 的新特性)。

2.1 set_url(url)

设置 robots.txt 的 URL,参数同样支持字符串或 Request 对象。源码 set_url 实现 显示:若传入的是 Request 对象则取其 full_url,随后用 urllib.parse.urlsplit 拆分出 hostpath。这意味着通过 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.pyBaseRobotTest.setUp 就是 io.StringIO(self.robots_txt).readlines() 后直接 parse)。

2.3 can_fetch(useragent, url)

根据已解析的规则返回 True/False。行为要点(源码 can_fetch 实现):

  1. disallow_all 为真时直接返回 Falseallow_all 为真时返回 True
  2. 未调用过 read()last_checked 为 0)时返回 False——源码注释说明这是"宁可漏抓、不可误抓"的保守设计,防止用户在 read() 之前误调 can_fetch() 产生假阳性;
  3. url 先做归一化(normalize_uri),空 URL 视为 /
  4. /robots.txt 本身被隐式允许(RFC 9309 的隐含规则),返回 True
  5. 其余情况按 _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/1Crawl-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: 留空是"全部放行"的经典写法,见 CrawlDelayAndCustomAgentTestcybermapper 条目的 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) 的查找顺序(源码):

  1. 精确命中 groups(小写);
  2. 遍历所有 entry,用 Entry.applies_to 判断;
  3. 兜底取 * 条目;
  4. 都找不到返回 None——can_fetch 视其为允许。

Entry.applies_to 的规则值得注意:查询方 UA 先按 / 截断取名称部分并小写(如 MyBot/2.1mybot),条目侧的 agent(非 *)小写后做子串包含判断。因此条目 user-agent: bot 会匹配 MyBot——这是实现事实,编写爬虫识别自身条目时要意识到这种宽松匹配。

6.4 URL 归一化

匹配前,待判定 URL 会经 normalize_uri 做百分号编码往返归一化(先 unquotequotesurrogateescape 兜底),查询串逐段归一;规则模式侧则由 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-rateCrawlDelayAndCustomAgentTestCrawl-delay: 1Request-rate: 3/15 验证参数解析与"空 Disallow 全放行"。
  • SitemapSitemapTest 断言 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 站点的 Nutch agent 对根路径禁抓、对 /brian 放行等,与官方文档示例同源。

8. 实战要点与常见误区小结

结合文档与源码,使用 RobotFileParser 时值得牢记的边界行为:

  1. 必须先 read()(或 parse())再 can_fetch():未读取时 can_fetch 一律返回 Falsecrawl_delay/request_rate 一律返回 None
  2. /robots.txt 永远隐式允许,即使规则里 Disallow: / 也拦不住它;
  3. 404 是"放行"而非"错误":站点没放 robots.txt 时 allow_all 置位,所有 can_fetch 返回 True;但 403/401/5xx 一律全禁;
  4. Disallow: 空值 = 全部允许,这是为特定 agent 开绿灯的惯用写法;
  5. $ 锚定与 * 通配只支持 RFC 9309 的 glob 语法,$ 必须位于路径末尾,否则整条规则被忽略;
  6. 长驻爬虫的缓存策略:用 mtime() 记录上次抓取时间,周期性 read() 刷新;read() 内部会自动刷新时间戳(经 parse 中的 modified());
  7. UA 匹配是子串语义:查询 agent 名截掉版本号后,与条目 agent 做小写子串匹配,命中精确条目 → 模糊条目 → * 兜底;
  8. 解析对语法错误(非法 crawl-delay、坏 request-rate$ 位置错误)采取宽容跳过策略,不会抛异常中断整个文件解析。

以上机制使 urllib.robotparser 成为 CPython 标准库中一个麻雀虽小五脏俱全的组件:不到 400 行的 robotparser.py 完整实现了 RFC 9309 的抓取判定、通配符匹配与 HTTP 错误语义,配合 urllib.request 即可构建出既尊重站点抓取协议、又具备限速与 sitemap 发现能力的合规爬虫。

登录后查看全文
热门项目推荐
相关项目推荐