蜘蛛程序如何遵守 robots.txt:规则读取与请求控制实现

资讯3天前发布 jdkjadmin
0 00
蜘蛛程序如何遵守 robots.txt:规则读取与请求控制实现

蜘蛛程序如何遵守 robots.txt:规则读取与请求控制实现

编写网页蜘蛛时,读取 robots.txt 不是可有可无的礼节,而是请求调度流程的一部分。一个合规、可维护的爬虫应当在请求网页之前,确认目标路径是否允许抓取;在允许抓取后,还要控制请求频率,避免给站点造成不必要的压力。

不过,robots.txt 常被误解:它不是访问控制系统,也不能代替身份验证;不同爬虫对规则的支持程度可能不同;而且“允许抓取”不代表目标内容可以随意复制或使用。本文会说明协议规则、实现边界和常见误区,并提供一个基于 Python 标准库的示例。

一、robots.txt 的适用场景与作用范围

robots.txt 是网站发布给自动化客户端的抓取规则文件,通常位于一个源站的根路径,例如:

https://example.com/robots.txt

对于 https://example.com/catalog/item,应检查对应源站的 robots.txt。源站通常由协议、主机名和端口共同确定,因此下面这些地址不能简单视为同一个源站:

https://example.com/
http://example.com/
https://www.example.com/
https://example.com:8443/

默认端口和地址规范化可以按 URL 规则处理,但不能把不同协议、主机或非默认端口的规则混用。抓取子域名时,也要分别获取对应子域名的 robots.txt。

该文件适用于表达站点希望爬虫遵守的抓取范围约定,例如不抓取后台路径、降低请求频率,或为特定机器人提供不同规则。它不保证搜索引擎一定收录或排除某个页面,也不会自动约束不愿遵守规则的客户端。

二、规则的基本结构

robots.txt 使用以行为单位的文本规则。常见指令包括:

User-agent: ExampleBot
Disallow: /private/
Allow: /private/public/

User-agent: *
Disallow: /tmp/
  • User-agent 指定规则组适用的爬虫名称。抓取程序应使用稳定、可识别的 User-Agent 标识。
  • Disallow 表示不应抓取匹配的路径;空值通常表示没有禁止路径。
  • Allow 表示允许匹配的路径。当允许和禁止规则都能匹配时,通常按匹配路径的长度处理,较长、较具体的规则优先。
  • # 后的内容是注释。规则匹配针对路径,不是完整 URL;查询字符串等细节应依照所采用的协议和解析器处理。

同一规则组可以有多个 Disallow 和 Allow。robots.txt 的语法、匹配规则及文件读取行为可参考 RFC 9309。实际部署中,各站点和爬虫可能存在历史兼容差异,因此要选择清楚说明支持范围的解析器,并对边界行为做测试。

Crawl-delay 常见于某些站点,但它不是 RFC 9309 标准规则,标准库解析器也不一定支持。若要处理该字段,必须明确这是扩展行为,并为不支持该字段的情况定义自己的策略,不能把它当成所有网站通用的强制指令。

三、正确的执行顺序

爬虫不能先发出网页请求,再检查 robots.txt。一个基本的请求决策顺序是:

  1. 解析目标 URL,校验协议和目标域名。
  2. 按源站获取并解析 robots.txt,必要时使用有效缓存。
  3. 使用当前爬虫的 User-Agent 检查目标 URL 是否允许。
  4. 若允许,执行全局并发、每源站请求间隔等限速策略。
  5. 发送请求,并对重定向、响应错误和后续 URL 重新做判断。

robots.txt 的检查与限速是两类不同的控制:前者决定“能不能请求这个路径”,后者决定“多久请求一次”。仅仅实现其中之一,都不足以控制爬虫行为。

四、状态码与读取失败:需要明确策略

读取 robots.txt 时,不能把所有异常都当成“没有规则”。按照 RFC 9309 的处理原则,成功获取的规则应解析并遵守;对于表示文件不存在等未成功响应的情况,爬虫可以访问资源;如果 robots.txt 因服务器错误或网络不可达而无法访问,爬虫应暂时将站点视为不可抓取,并在之后重试。长时间缓存旧规则、网络故障时继续无限制抓取,都可能产生问题。

HTTP 客户端库通常不会替业务代码自动实现全部协议语义。下面的示例采用保守策略:成功获取时解析规则;404 时视作没有 robots.txt;其他 HTTP 错误、网络错误或解析失败时暂不抓取。对于其他 4xx 状态,RFC 的处理空间与站点运营策略需要结合考虑;示例选择保守地拒绝抓取,而不是把所有 4xx 一律当成空规则。生产系统应将状态和决定记录下来,并按需求审查策略。

缓存也有边界。robots.txt 适合缓存以减少重复请求,但缓存应有过期时间;站点规则变化后,过期缓存不应无限期沿用。RFC 9309 对缓存时长给出了指导,常见做法是设置不超过 24 小时的有效期,除非 robots.txt 无法访问等情况需要按明确策略处理。不要把解析结果永久保存在进程内。

五、可运行的 Python 示例

以下程序使用 Python 3 标准库,不需要额外安装依赖。它做了几件事:按源站缓存 robots.txt、用 RobotFileParser 判断 URL、对每个源站串行控制最小请求间隔,并在爬取前检查规则。把示例 URL 替换成自己有权访问的测试站点即可运行。

示例为减少意外抓取,遇到 robots.txt 读取失败时采取拒绝策略。网页请求遇到重定向时不自动跟随:重定向目标可能是另一个路径、主机或协议,必须重新解析目标并检查其 robots.txt 后才能请求。生产程序还应增加整体并发限制、超时、日志和任务队列管理。

from __future__ import annotations

import threading
import time
from dataclasses import dataclass
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit, urlunsplit
from urllib.request import (
    HTTPRedirectHandler,
    Request,
    build_opener,
)
from urllib.robotparser import RobotFileParser


USER_AGENT = "ExampleResearchBot/1.0 (+https://example.org/bot)"
TIMEOUT_SECONDS = 10
MIN_INTERVAL_SECONDS = 2.0
ROBOTS_CACHE_SECONDS = 24 * 60 * 60


def origin_of(url: str) -> str:
    """返回 URL 的源站部分:协议、主机名和显式端口。"""
    parts = urlsplit(url)
    if parts.scheme not in ("http", "https") or not parts.hostname:
        raise ValueError(f"仅支持有效的 HTTP(S) URL:{url}")

    # urlsplit().netloc 保留认证信息,不应将其用于 robots.txt 地址。
    # 这里使用 hostname 和 port 重新构造,避免把 URL 用户名、密码带入请求。
    host = parts.hostname
    if ":" in host and not host.startswith("["):
        host = f"[{host}]"  # IPv6 地址需要方括号

    try:
        port = parts.port
    except ValueError as exc:
        raise ValueError(f"URL 端口无效:{url}") from exc

    default_port = (parts.scheme == "http" and port == 80) or (
        parts.scheme == "https" and port == 443
    )
    authority = host if port is None or default_port else f"{host}:{port}"
    return f"{parts.scheme}://{authority}"


def robots_url_for(url: str) -> str:
    return origin_of(url) + "/robots.txt"


@dataclass
class RobotsEntry:
    parser: RobotFileParser | None
    expires_at: float
    allowed_on_fetch_failure: bool = False


class RobotsPolicy:
    def __init__(self, user_agent: str, cache_seconds: int = ROBOTS_CACHE_SECONDS):
        self.user_agent = user_agent
        self.cache_seconds = cache_seconds
        self._cache: dict[str, RobotsEntry] = {}
        self._lock = threading.Lock()

    def _load(self, target_url: str) -> RobotsEntry:
        robots_url = robots_url_for(target_url)
        parser = RobotFileParser()
        parser.set_url(robots_url)

        request = Request(
            robots_url,
            headers={"User-Agent": self.user_agent},
        )

        try:
            # urlopen 对 robots.txt 的重定向可能自动跟随。
            # 若要限制重定向主机,生产程序应使用自定义重定向策略校验目标。
            from urllib.request import urlopen

            with urlopen(request, timeout=TIMEOUT_SECONDS) as response:
                status = response.getcode()
                if status < 200 or status >= 300:
                    return RobotsEntry(None, time.time() + 60)
                text = response.read().decode("utf-8", errors="replace")
                parser.parse(text.splitlines())
                return RobotsEntry(parser, time.time() + self.cache_seconds)

        except HTTPError as exc:
            if exc.code == 404:
                # 文件不存在:示例选择按无禁止规则处理。
                empty_parser = RobotFileParser()
                empty_parser.parse([])
                return RobotsEntry(
                    empty_parser, time.time() + self.cache_seconds
                )

            # 示例对其他 HTTP 错误采取保守策略,不继续抓取。
            return RobotsEntry(None, time.time() + 60)

        except (URLError, TimeoutError, OSError, ValueError):
            # 网络故障或读取失败时暂时拒绝,短时间后再尝试。
            return RobotsEntry(None, time.time() + 60)

    def can_fetch(self, url: str) -> bool:
        origin = origin_of(url)
        now = time.time()

        with self._lock:
            entry = self._cache.get(origin)
            if entry is None or entry.expires_at <= now:
                entry = self._load(url)
                self._cache[origin] = entry

        if entry.parser is None:
            return entry.allowed_on_fetch_failure

        return entry.parser.can_fetch(self.user_agent, url)


class NoRedirectHandler(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        # 不跟随重定向,避免未检查新 URL 就向其发送请求。
        return None


class PerOriginLimiter:
    def __init__(self, interval_seconds: float):
        self.interval_seconds = interval_seconds
        self._last_request: dict[str, float] = {}
        self._locks: dict[str, threading.Lock] = {}
        self._guard = threading.Lock()

    def wait(self, url: str) -> None:
        origin = origin_of(url)

        with self._guard:
            lock = self._locks.setdefault(origin, threading.Lock())

        with lock:
            now = time.monotonic()
            previous = self._last_request.get(origin)
            if previous is not None:
                remaining = self.interval_seconds - (now - previous)
                if remaining > 0:
                    time.sleep(remaining)

            self._last_request[origin] = time.monotonic()


def fetch_one(url: str, policy: RobotsPolicy, limiter: PerOriginLimiter) -> None:
    if not policy.can_fetch(url):
        print(f"robots.txt 不允许或规则无法确认,跳过:{url}")
        return

    limiter.wait(url)
    request = Request(url, headers={"User-Agent": USER_AGENT})
    opener = build_opener(NoRedirectHandler)

    try:
        with opener.open(request, timeout=TIMEOUT_SECONDS) as response:
            print(f"状态码:{response.status},URL:{url}")
            # 演示程序只读取少量内容;真实爬虫应限制下载大小。
            body = response.read(1024)
            print(f"读取字节数:{len(body)}")
    except HTTPError as exc:
        if 300 <= exc.code < 400:
            print(f"收到重定向 {exc.code},未自动跟随:{url}")
            print("应解析 Location,重新检查目标 URL 的 robots.txt 后再决定。")
        else:
            print(f"HTTP 错误 {exc.code}:{url}")
    except (URLError, TimeoutError, OSError) as exc:
        print(f"请求失败:{url};原因:{exc}")


if __name__ == "__main__":
    policy = RobotsPolicy(USER_AGENT)
    limiter = PerOriginLimiter(MIN_INTERVAL_SECONDS)

    # 请换成允许测试的站点和路径。
    fetch_one("https://example.com/", policy, limiter)

六、示例中的重要边界

1. Python 解析器不等于完整的爬虫策略

urllib.robotparser 是标准库中便于使用的规则解析器,但它不会替程序负责请求调度、缓存失效、网络故障策略或重定向安全。不同语言和版本的解析器对扩展字段、编码和边缘匹配行为也可能有所不同。上线前应使用符合目标需求的测试样例验证规则,尤其是 Allow 与 Disallow 同时匹配、通配符以及末尾标记等情况。

示例将 robots.txt 按 UTF-8 解码,错误字节以替代字符处理。这是一个简单的实现选择,并不意味着所有异常文本都能被正确解释。需要广泛兼容时,应根据 RFC 9309 及目标站点情况选择成熟实现,并对解析失败保持可观测性。

2. 重定向必须重新做决策

初始 URL 允许抓取,不代表重定向后的 URL 也允许。重定向可能改变路径,也可能跨到另一个主机或协议。如果 HTTP 客户端自动跟随,程序可能在没有检查新目标 robots.txt 的情况下发出请求。示例对网页请求关闭自动跟随,收到 3xx 后由调用方决定如何处理。

robots.txt 本身也可能重定向。不同实现对 robots.txt 重定向的处理方式不尽相同;若允许自动跟随,应考虑限制跳转次数,并检查最终目标是否仍在可接受的范围内。尤其不应让一个不可信 URL 借重定向诱导爬虫访问内网地址。

3. 频率限制不只看 robots.txt

示例中的间隔限制是每个进程内、每个源站的最小间隔,不是全局分布式限速器。多进程、多台机器或多个独立爬虫实例同时运行时,每个实例都等待两秒,合计请求速率仍可能很高。生产环境要按实际部署方式实现共享调度或协调机制,并设置最大并发数、总请求预算和失败退避策略。

还应关注服务器响应的 429 Too Many Requests 和 Retry-After。遇到限流信号时应暂停或退避,不能只依赖 robots.txt 中的规则继续请求。对重复的 5xx 错误也应逐步延长重试间隔,避免故障时加重服务压力。

4. URL 规范化必须谨慎

规则匹配针对 URL 的路径表示,不应随意把路径转小写、删除斜杠、解码百分号编码或重排查询参数。服务器可能把看似相近的 URL 视作不同资源;程序在预处理时改变 URL,可能导致检查的路径与实际请求的目标不一致。应使用一致的 URL 解析和请求流程,并避免对规则匹配的输入做未经验证的改写。

七、robots.txt 不是安全机制

robots.txt 的核心用途是表达自动化爬虫应遵循的访问偏好,不是用来保护机密数据的权限控制。禁止规则对善意爬虫有提示作用,但不会阻止浏览器、恶意脚本或其他客户端读取公开可访问的 URL。某些爬虫甚至会把禁止路径暴露在公开的 robots.txt 中,反而让路径名称更容易被发现。

需要保护的数据应通过身份验证、授权校验、网络隔离和访问控制实现。敏感信息不应仅因为写了 Disallow 就视为安全。反过来,robots.txt 允许抓取也不代表爬虫拥有内容的再发布权;仍需遵守适用的法律、网站条款、隐私要求和数据使用约定。

八、常见错误清单

  • 只在程序启动时读取一次:规则可能变化,应采用有期限的缓存,并在过期后重新读取。
  • 读取失败就当作空规则:网络错误和服务器错误不应与 404 混为一谈;示例选择在无法确认规则时暂不抓取。
  • 只检查首页路径:每个待请求 URL 都应按其实际目标路径判断,重定向后的 URL 也要重新判断。
  • 只按域名缓存:协议和端口可能不同,缓存键应对应正确的源站。
  • 把 Crawl-delay 当作标准指令:它是常见扩展,不保证解析器或所有爬虫都支持。
  • 以为限速等于遵守 robots.txt:限速不会自动过滤禁止路径;规则检查与流量控制要分别实现。
  • 以为 robots.txt 能阻止未授权访问:它不是身份验证和安全边界。
  • 忽视爬虫自己的身份标识:发送的 HTTP User-Agent 应与用于规则匹配的标识一致,并保持稳定、可识别。

九、上线前的检查建议

部署前,至少验证不同源站的 robots.txt 不会串用;确认规则不允许时不会发出网页请求;测试 404、其他 4xx、5xx、超时和 DNS 失败时的行为;验证缓存过期后会重新读取;并确认重定向不会绕过新 URL 的规则检查。还应记录 robots.txt 的获取状态、规则判断结果、限速等待时间和跳过原因,避免遇到问题时无法还原爬虫为何发送请求。

一个可靠的蜘蛛程序不只是“能读 robots.txt”,还要把解析、缓存、错误处理、重定向和调度组合成明确的请求策略。先采用保守、可审计的默认行为,再根据协议要求与业务需要扩展,通常比遇到例外时临时放宽规则更安全。

© 版权声明

相关文章