第 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 道題答案開始練習
← 分頁與去重分詞與向量 →