第 40 课 · 实战 · 19 分钟

Python robots.txt 与礼貌抓取

许可、限速和隐私,都是爬虫的一部分。

robots.txtUser-agentCrawl-delay隐私

供应商的 robots.txt 禁止抓取 /wholesale/,而那正是 Maya 想要的页面。许可现在也是解析器的一部分。当答案是「不行」时,你怎么办?

解析器能工作,不代表你有权收集所有能访问的内容。robots.txt 发布抓取偏好,站点条款规定额外规则,隐私法规还可能限制个人数据。礼貌是技术问题:表明身份、限制请求、缓存结果,并在站点拒绝时停止。 你还会让 RobotFileParser 应用路径规则,并解析重试时间。

阅读站点公布的规则

robots.txt 是站点根目录下的纯文本文件。它包含面向不同 User-agent 的分组,以及 Allow、Disallow、Crawl-delay 和 Sitemap 等指令。它是抓取偏好,不是许可证,也不能替代站点条款。

robots = "User-agent: *\nDisallow: /private\nAllow: /docs\nCrawl-delay: 2"
rules = {}
for line in robots.splitlines():
    if ":" in line:
        key, value = line.split(":", 1)
        rules[key.strip().lower()] = value.strip()
print(rules["disallow"])
print(rules["crawl-delay"])

一个很小的键值视图

遵守匹配的 User-agent 分组

规则按 User-agent 分组。没有更具体分组匹配时,* 分组才适用。生产爬虫应使用维护良好的 robots 解析库;这里的小函数只展示问题形状,不是完整标准实现。

def rules_for(text, agent):
    active = False
    rules = []
    for line in text.splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        key, _, value = line.partition(":")
        key = key.strip().lower()
        value = value.strip()
        if key == "user-agent":
            active = value == "*" or value.lower() == agent.lower()
        elif active and key in {"allow", "disallow"}:
            rules.append((key, value))
    return rules

text = "\n".join([
    "User-agent: *",
    "Disallow: /private",
    "User-agent: GoodBot",
    "Allow: /private/public",
])
print(rules_for(text, "GoodBot"))

按分组读取,而不是拍平

请求前先检查路径

路径检查应发生在 URL 进入队列之前。更长、更具体的规则通常优先,Allow 可以覆盖范围更大的 Disallow。把本地实现当成护栏,而不是法律意见。

rules = [("allow", "/docs"), ("disallow", "/private")]
path = "/private/report"
decision = "allowed"
for kind, prefix in rules:
    if path.startswith(prefix):
        decision = kind
print(decision)

简单的路径护栏

放慢速度,少收数据

遵守 Crawl-delay 和 Retry-After,遇到 429 或 503 要退避,缓存已经拿到的页面。只收集任务需要的字段。不要绕过登录、付费墙、验证码或访问控制,也不要在没有合法依据时收集个人数据。

record = {"name": "Ada", "email": "ada@example.com", "bio": "Python"}
public = {key: value for key, value in record.items() if key != "email"}
print(public)

只保留任务需要的字段

让 RobotFileParser 判断路径规则

不要用一个子串检查来实现 robots.txt 的优先级。urllib.robotparser 会处理分组、通配 user-agent,以及更长的 Allow/Disallow 路径。测试时解析一份保存的规则,再用 can_fetch() 查询具体 user-agent 和路径。

from urllib.robotparser import RobotFileParser

lines = [
    "User-agent: *",
    "Disallow: /private",
    "Allow: /private/public",
    "",
    "User-agent: BadBot",
    "Disallow: /",
]
robots = RobotFileParser()
robots.parse(lines)
for path in ["/", "/private", "/private/public"]:
    print(path, robots.can_fetch("PyPracticeBot", path))
print(robots.can_fetch("BadBot", "/"))

具体分组和更长路径

解析 Retry-After 和 Crawl-delay

Retry-After 可以是秒数,也可以是 HTTP 日期。Crawl-delay 是请求间隔提示,不是站点能承受任意并发的承诺。两者都转成有上限的等待;缺失或格式错误时用保守默认值。

from datetime import datetime, timezone
from email.utils import parsedate_to_datetime

def retry_seconds(value, now):
    try:
        return max(0, int(value))
    except (TypeError, ValueError):
        try:
            when = parsedate_to_datetime(value)
        except (TypeError, ValueError):
            return 60
        return max(0, int((when - now).total_seconds()))

now = datetime(2026, 10, 28, 12, 0, tzinfo=timezone.utc)
for raw in ["120", "Wed, 28 Oct 2026 12:02:00 GMT", "bad"]:
    print(raw, retry_seconds(raw, now))
print("Crawl-delay", 2)

秒数、日期和兜底值

本课要点

  • robots.txt 发布抓取偏好,不是许可证,也不构成法律意见。
  • User-agent 分组决定哪些 Allow 和 Disallow 规则适用。
  • 路径检查应在请求入队前完成,而不是下载之后。
  • 遇到 429 和 503 要退避,并遵守 Crawl-delay 与 Retry-After。
  • 不要绕过访问控制,也不要在没有合法依据时收集个人数据。
  • 用真正的 robots 解析器;Retry-After 要封顶,Crawl-delay 只当提示。

常见错误

  • 因为页面公开可访问,就认为 robots.txt 可选。
  • 忽略 429,立即重复请求。
  • 任务只需要两个字段,却收集页面上的所有内容。
  • 用 startswith() 判断 robots 规则,忽略更长的 Allow 路径。
轮到你

把一段小型 robots.txt 解析成字典,打印 Disallow 路径和 Crawl-delay,并解释生产爬虫为什么需要完整解析器。

动手试试

改改变量、先预测输出,再运行代码。第一次点击“运行代码”时会下载 Python 运行时,可能需要几秒钟。

浏览器内 Python本地运行 · 无需账号
首次运行会从 CDN 下载 Pyodide。修改后点击“运行代码”。

练习本课

12 道题,作答后立即看到解释。

查看 12 道题答案开始练习
← 分页与去重分词与向量 →