robots.txt 向实现 Robots Exclusion Protocol 的自动客户端传达抓取规则,可以减少对昂贵或无用路径的遍历,但服务器不会因此自动按照文件内容拦截请求。RFC 9309 明确区分这些规则与访问授权。应把它当作面向合作爬虫的公开接口,真正能否读取受保护资源,仍由认证和授权机制决定。配置中写了禁止抓取,不等于服务器已经建立了权限边界。
对准目标爬虫与实际路径
标准文件位于相应方案和 authority 的 /robots.txt,放在应用子目录下的同名文件,不会自动成为整个源站的策略。User-agent 分组标识规则面向的爬虫产品,通配分组按协议为其他代理提供适用规则。路径规则针对请求目标匹配,大小写不同可能产生不同结果。路由变化时应重新检查具体 URL,不能只看规则文字仍然像原来的样子。
文件应以 UTF-8 纯文本形式提供,并明确实际响应。宽泛的静态站回退规则,可能在 robots 路径返回二百和首页内容;这只证明 HTTP 已经交付,不能证明爬虫取得了预期策略。复杂的 Allow、Disallow 组合、转义和通配模式,需要对照协议及真实爬虫实现验证。一个简单解析器案例不能替整个规则集合证明所有边界行为正确。
坏掉的样子
在 robots.txt 中列出私有路径,并不会隐藏路径;公开的规则文件反而可能告诉别人它在哪里。客户端可以完全不读策略就直接请求,合作爬虫自己作出的不抓取决定,也不会转化为源站上的访问规则。确实需要限制的内容,应在请求授权的位置执行限制;Disallow 行不能替代账号权限或其他实际访问检查。公开规则和私有资源的保护条件必须分开维护。
抓取与收录也是不同问题。Google 文档说明,被禁止抓取的 URL 仍可能因为其他来源的链接出现在搜索结果里。如果爬虫无法读取页面,也可能看不到页面上希望它执行的 noindex 指令。公开内容需要按目标选择合适的收录控制,私有内容则要使用真实访问限制。不能认为阻止内容下载,就会自动移除所有已存在的搜索记录或外部引用。
策略获取失败同样有不同语义。RFC 9309 允许爬虫在文件不可用时访问资源,例如收到四开头的 HTTP 响应;如果服务器或网络错误导致文件无法到达,例如五开头的响应,爬虫起初必须假定完全禁止。规范还定义了长期失败与缓存副本的处理。部署坏了 robots 文件,可能改变抓取行为,不应只把它当成少了一份装饰性文本。
爬虫缓存还会带来发布延迟。RFC 9309 通常建议不要使用超过二十四小时的缓存策略,但文件无法到达时存在例外;这不是所有爬虫都会准点更新的承诺。源站已经成功更新,不代表每个爬虫都重新取到了规则。应分别保存当前响应、预期策略和实际观察到的爬虫行为,不能拿其中一种证据替另一种。sitemap 指针有助于发现地址,但不会授予访问权限,也不能保证每个列出的网址一定被抓取或收录。发现、抓取、读取与收录,是需要区分的多个步骤。
快速验证
保存为 robots.py,运行 python3 robots.py。临时回环 HTTP 服务提供一份简短 robots 文件及无害演示内容。脚本先获取实际策略,再把收到的文本交给 Python 的 RobotFileParser。选择的路径只使用简单 Disallow 前缀,没有规则冲突,也不涉及复杂通配处理;这样可以把验证集中在规则建议与服务器实际拦截的区别上。
from http.client import HTTPConnection
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
from urllib.robotparser import RobotFileParser
import json
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/robots.txt':
status, data = 200, b'User-agent: *\nDisallow: /private/\n'
elif self.path in ('/private/report', '/public'):
status, data = 200, b'public demonstration data\n'
else:
status, data = 404, b'missing\n'
self.send_response(status)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *args):
pass
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
thread = Thread(target=server.serve_forever)
thread.start()
try:
origin = f'http://127.0.0.1:{server.server_port}'
def fetch(path):
connection = HTTPConnection('127.0.0.1', server.server_port, timeout=5)
try:
connection.request('GET', path)
response = connection.getresponse()
return response.status, response.read().decode()
finally:
connection.close()
status, rules = fetch('/robots.txt')
assert status == 200
parser = RobotFileParser(origin + '/robots.txt')
parser.parse(rules.splitlines())
decisions = {path: parser.can_fetch('ExampleBot', origin + path)
for path in ('/private/report', '/public', '/Private/report')}
assert decisions == {'/private/report': False, '/public': True, '/Private/report': True}
direct_status, direct_body = fetch('/private/report')
assert direct_status == 200 and direct_body == 'public demonstration data\n'
print(json.dumps({'parser_decisions': decisions, 'direct_http_status': direct_status,
'direct_http_body': direct_body, 'search_index_tested': False}))
finally:
server.shutdown()
thread.join()
server.server_close()
解析器拒绝 /private/report,允许 /public,也允许大小写不同的 /Private/report。这些只是返回给调用方的判断。随后脚本故意不采用解析器的拒绝结果,而是直接发送普通 HTTP GET 请求 /private/report。服务器仍返回二百和公开演示文字,证明策略文件没有自动安装访问屏障。这里的“private”只是演示路径名,里面没有真实私密材料。
本轮没有重跑这个未改动的套接字实验。实验没有任何搜索引擎或索引服务参与,只验证这个解析器在选定输入上的匹配,以及客户端决定与源站执行之间的区别。它不能证明标准库完整符合 RFC 的所有细节,也不能预测每个爬虫的行为。发布策略之前,应检查代表性的允许与拒绝地址,核对实际响应,并独立确认资源的授权要求已满足。这样才能把抓取偏好与真正的访问保护都落实到各自正确的位置。
来源
- RFC 9309: Robots Exclusion Protocol,建议性规则、路径匹配与可用性处理,已核验 2026-10-15。
- Google: introduction to robots.txt,抓取控制及其不能保证阻止收录的边界,已核验 2026-10-15。
- Python 3.14: urllib.robotparser,本地实验实际使用的解析器接口,已核验 2026-10-15。