前面十篇练的全是”怎么从网上拿东西”:抓网页、抠表格、提交表单、带登录拿、批量下载。但有一句老话得在第47篇正式讲:程序能干,不等于可以随便干。今天新概念只有一个:
robots.txt——网站贴在大门口的”欢迎参观 / 后院别进”告示牌,程序进门之前先看一眼。再配一个老手艺的新用法:time.sleep限速,让程序做个”走马观花的体面访客”,而不是”把人家门槛踏破的暴走团”。学完这篇,你的爬虫才算毕业:不光会拿,还知道什么能拿、拿多快、拿多少。
一、真实场景:一封”友好”的律师函
你把第43篇的爬虫升级了一下:盯上了一个行业资讯网站,每小时跑一次,把所有新文章扒下来存进自己的资料库。头两天美滋滋——数据哗哗地来。第三天早上,程序报错了:Connection refused。浏览器一打开,人家网站正常得很,就你的程序被拒之门外——你的 IP 被封了。
你心想:封就封,我换个网络接着爬。于是改成每 10 秒狂抓一次,一晚上抓了人家三万页。第四天,收到的不是新数据,是网站管理员的一封邮件,措辞客气但意思很硬:”贵方的自动化访问已对我方服务器造成明显负载,请立即停止,否则我方将保留进一步追究的权利。”
你懵了:网上公开的东西,我”看”一下怎么还犯法了?
还真有可能。这家网站的 robots.txt 里白纸黑字写着 Crawl-delay: 10(抓一抓歇 10 秒)和一堆 Disallow(不欢迎抓的目录),你的程序一眼没看,全程 0 间隔狂抓,还扒了人家明确不让碰的会员内容区。“网页在浏览器里能打开”只代表”人能看”,不代表”程序可以随便拿”——这中间隔着一整套”礼仪”。
其实这套礼仪就三层,一层比一层容易被忽略:
- 看告示:门口的
robots.txt说了哪里能进、哪里不能进、欢迎多快来一次——先看再进门; - 控制速度:别像暴走团一样把人家服务器踩塌,
time.sleep每抓一页歇一拍; - 少拿、认账:只拿你需要的(别把人家整个网站搬空),请求头里写上你是谁(联系方式),出事了人家能先给你发封邮件而不是直接发律师函。
三层全会,你的程序才算个体面访客。今天的核心是第一层:
进门先看
robots.txt:它是网站自己贴的”接待须知”——虽然不带锁,但违反它的后果是实打实的(封 IP、发邮件、吃官司)。
二、新概念只有一个:robots.txt
它是什么
robots.txt 是一个纯文本文件,放在网站根目录下,全世界约定俗成:任何正规爬虫,访问任何网站的第一件事,都是先请求 https://网站域名/robots.txt 看一眼。它长这样(这是 python.org 的真实告示,节选):
User-agent: *
Disallow: /admin/
Disallow: /downloads/tmp/
Crawl-delay: 10
User-agent: BadBot
Disallow: /
逐行翻译成人话:
| 原文 | 人话 |
|---|
|——|——|
User-agent: * |
“下面这些规矩,对所有程序访客生效”(* = 所有人) |
|---|---|
Disallow: /admin/ |
“后台目录 /admin/ 不许进“ |
Disallow: /downloads/tmp/ |
“临时下载区也不许进“ |
Crawl-delay: 10 |
“每两次请求之间至少歇 10 秒“ |
User-agent: BadBot + Disallow: / |
“专门给叫 BadBot 的爬虫:整个站都不许进“ |
没写 Disallow 的地方呢?默认欢迎。所以 robots.txt 的逻辑是”黑名单”:不禁止的都允许,禁止的别碰。
一个容易误会的点先说清:robots.txt 是”君子协定”,不是”锁”。它挡不住坏人——恶意爬虫根本不看它。它真正的意义有两层:一是法律和道德上的界线(人家明示了不欢迎,你硬闯,真出事时这是对你不利的证据);二是给好人省事(人家明确说了哪不许进,你照做,双方都省心)。就像小区门口贴的”施工区域请勿入内”:拦不住翻墙的,但你大摇大摆走进去出了事,责任算你的。
怎么读它:urllib 三行代码
标准库里就有现成的”告示牌阅读器”,不用自己解析:
from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url("https://www.python.org/robots.txt")
rp.read() # 下载并读懂告示牌
print(rp.can_fetch("*", "https://www.python.org/downloads/")) # True 可以进
print(rp.can_fetch("*", "https://www.python.org/admin/")) # False 禁区!
print(rp.crawl_delay("*")) # 10 人家要求歇 10 秒
can_fetch(你的名字, 想抓的网址)→True允许、False禁止。这就是全部核心;crawl_delay(你的名字)→ 人家要求的最小间隔秒数(没写就是None);- 你的名字填什么?填你程序的自称,对应告示牌里的
User-agent。普通情况填"*"(套用”对所有人”的那组规矩)就行;如果你的爬虫起了名字(比如"my-research-bot"),就填名字——人家可能给你单独立规矩。
类比记忆:
robots.txt是餐厅门口的告示——”包房请勿入内””谢绝自带酒水””高峰期请排队”。服务员不拦你溜进包房,但你溜进去了被请出来、甚至报警,告示就是证据。can_fetch就是进门前问一句:”我这个区域能进吗?”
老手艺新用法:time.sleep 限速
第二个护身符 time.sleep(秒数) 你在第23篇(番茄钟)就认识了——暂停几秒再往下走。爬虫里的用法简单到只有半行:
import time
for page in pages:
resp = requests.get(page)
# ... 处理这一页 ...
time.sleep(2) # 歇 2 秒再抓下一页
别小看这一行:你等 2 秒,人家服务器的负载就是原来的几十分之一。人手动浏览网页时,看一页至少十几秒,服务器伺候一个”人”毫无压力;无间隔爬虫相当于一秒钟点几百次刷新——服务器眼里这不是访客,是 DDoS 攻击的前奏。封 IP 是轻的,压垮人家服务、影响真实用户,那就真是”程序能干,但不该干”了。
三、动手:写一个”讲礼貌”的爬虫
把前几篇的手艺攒起来,加两样新护身符,做成合体的礼貌爬虫模板。继续用本地假网站练习(别拿真网站开刀)。
第 1 步:假网站先贴出”告示牌”
# fake_site.py — 本地练习网站(先跑这个)
from flask import Flask, Response
app = Flask(__name__)
ROBOTS = """User-agent: *
Disallow: /vip/
Crawl-delay: 2
"""
PAGES = {
"/news/1": "公告一
本周五下午培训。
",
"/news/2": "公告二
报销截止到月底。
",
"/vip/secret": "内部消息
这页不许爬虫进。
",
}
@app.route("/robots.txt")
def robots():
return Response(ROBOTS, mimetype="text/plain")
@app.route("/")
def show(page):
body = PAGES.get("/" + page, "404
")
return body
if __name__ == "__main__":
app.run("127.0.0.1", 5003)
第 2 步:礼貌爬虫(先看告示,再限速,最后才拿)
# polite_crawler.py
import time
import requests
from urllib.robotparser import RobotFileParser
BASE = "http://127.0.0.1:5003"
UA = "my-research-bot/1.0 (联系: me@example.com)" # 自报家门
# 1) 进门先看告示
rp = RobotFileParser()
rp.set_url(BASE + "/robots.txt")
rp.read()
delay = rp.crawl_delay("*") or 2 # 人家没规定就默认歇 2 秒
# 2) 逐页抓,每页之前问一次"能进吗",抓完歇一拍
targets = ["/news/1", "/news/2", "/vip/secret"]
for path in targets:
url = BASE + path
if not rp.can_fetch("*", url): # 禁区!绕开
print(f"跳过(robots.txt 禁止): {url}")
continue
try:
resp = requests.get(url, headers={"User-Agent": UA}, timeout=10)
print(f"抓到 {url}: 状态 {resp.status_code}, {len(resp.text)} 字")
except requests.RequestException as e:
print(f"失败 {url}: {e}")
time.sleep(delay) # 3) 限速:歇够再走
print("爬取结束——全程没碰禁区,速度也守规矩。")
跑起来(先 python3.14 fake_site.py,另开终端再 python3.14 polite_crawler.py):
抓到 http://127.0.0.1:5003/news/1: 状态 200, 27 字
抓到 http://127.0.0.1:5003/news/2: 状态 200, 27 字
跳过(robots.txt 禁止): http://127.0.0.1:5003/vip/secret
爬取结束——全程没碰禁区,速度也守规矩。
注意三个细节:can_fetch 在循环里每次抓之前问,而不是开头问一次——因为不同路径规矩可能不同(有的站 /public/ 放行、/vip/ 禁止);crawl_delay 拿不到就用自己定的默认值(or 2),永远别假设”没写就是不用等”;请求头里带上 User-Agent 联系方式——这是”体面访客”的名片,疫苗打在坑3。
跟AI说需求的参考说法:
“帮我写个 Python 爬虫模板:抓一批网页之前,先读网站的 robots.txt,禁止的路径跳过并打印原因;每次请求之间按 crawl_delay 限速(没有就默认 2 秒);请求头里带自定义 User-Agent 和我的联系邮箱;单页失败要报错但不影响后面的。”
四、改进:从”守规矩”到”真体面”
三层礼仪的后两层,跟AI协作补全:
- 限速升级成”匀速+随机抖动”:固定 2 秒间隔的请求序列,服务器一眼就能认出”这是机器”。真人浏览是不规则的。跟AI说:”把 time.sleep(2) 改成 time.sleep(random.uniform(1.5, 3.5)),并解释为什么随机间隔更礼貌。”(
random是第14篇密码生成器的老朋友)——记住:随机抖动是为了”更像体面的人”,不是为了”更好地伪装着干坏事”,边界还是 robots.txt 说了算。 - 拿得少:只抓增量。每天全量扒人家整个网站是最招恨的。第43篇的”去重”升级一下:把抓过的 URL 或最新一条的发布时间存进
seen.txt,下次只抓没抓过的。跟AI说:”给爬虫加增量逻辑:已见的 URL 存文件,下次只抓新的,最后汇报’新增 N 条、跳过 M 条’。” - 认账:User-Agent 写清楚你是谁。默认的
python-requests/2.x等于”匿名访客”,出问题了人家只能直接封。规范的自报家门格式是程序名/版本 (用途; 联系方式),比如news-collector/1.0 (科研用途; me@example.com)。人家有意见会先发邮件——一封邮件换一封律师函,怎么算都值。
五、三个必踩的坑(先打疫苗)
坑1:根本不看 robots.txt,或者”看了也不当回事”。最常见的心态是”网上公开的凭什么不让我爬”——注意 robots.txt 禁的往往不是”看”,而是自动化批量拿。人家禁 /vip/ 是保护会员内容,禁全站高频率访问是保护服务器。更隐蔽的坑是看错了对象:robots.txt 是每个域名一份的,爬 a.com 之前读的 a.com/robots.txt 对 b.com 无效;还有些站点对不同 User-agent 规矩不同,你自报的名字一改,规矩就变了。疫苗:访问任何新域名,第一件事读它的 robots.txt;can_fetch 在每次请求前问,路径逐条判。判断标准就一条:你的程序跑完,can_fetch 从没返回过 False 被你无视。
坑2:不设间隔(或者间隔太短),被封 IP 还觉得冤。这是新手爬虫的头号死法:for url in urls: requests.get(url)——几百个请求在几秒内轰过去,服务器日志里你就是一条 DDoS 尾巴。注意坑2的另一半:robots.txt 没写 Crawl-delay ≠ 允许无间隔。没写只是”人家没特别要求”,你自己也得定个底线(一般 1~5 秒起步,人流量大的站更慢)。还有个反面教材:被封了就换 IP、改 UA 继续硬闯——这就从”不懂礼仪”滑向”恶意爬取”了,性质完全不同。疫苗:每请求必 time.sleep,间隔遵守 crawl_delay 或自定底线,被封了先反省速度,不是换马甲。
坑3:User-Agent 留空或冒充浏览器。requests 默认的 UA 是 python-requests/x.x,一眼机器;而改成 Mozilla/5.0 ... 冒充 Chrome 呢?更糟——冒充浏览器去干浏览器不会干的事(无间隔狂抓、扒禁区),是把”不礼貌”升级成”欺骗”,真有纠纷时性质更差。疫苗:UA 自报家门:程序名/版本 (用途; 联系邮箱),光明正大。这也是给自己留后路——网站管理员想叫停时能联系上你,一封邮件就能解决的事,犯不上升级。
六、跟AI协作的正确姿势(礼仪特供版)
- 动手前先问AI”这么爬合适吗”:把目标网站和你的计划(抓什么、多久抓一次、拿来干什么)告诉AI:”我想每天抓这个网站的新闻标题做个人索引,合适吗?要看什么?”——AI 会带你走一遍 robots.txt、频率、用途(个人学习 vs 商用转载的法律边界)的检查清单。这一步在写代码之前做。
- 让AI把”礼仪”包进爬虫模板:跟AI说:”把我的爬虫改成礼仪版:先读 robots.txt、按 crawl_delay 限速(带随机抖动)、UA 自报家门、只抓增量、每 10 页打印一次进度。”从此你以后所有爬虫都在这个模板上改,礼仪默认开启,不用每次想起来。
- 读不懂告示就让AI翻译:把
robots.txt原文贴给AI:”这个告示牌什么意思?我的爬虫想抓 /news/ 和 /data/ ,允许吗?”——AI 会逐行翻译User-agent、Allow、Disallow、Crawl-delay、Sitemap的意思,比你自己猜准得多。 - 用途变了要重新问:从”个人学习”改成”给公司做数据分析”、从”看标题”改成”全文转载”,法律和礼仪的边界就变了。跟AI说清用途,别让AI替你打擦边球——AI 提醒不了你没说的事。
七、今日小练习
练习1:跑通礼貌爬虫,亲眼看”禁区被跳过”。先跑 fake_site.py,再跑 polite_crawler.py,确认输出里 /news/1、/news/2 抓到了、/vip/secret 打印”跳过(robots.txt 禁止)”。然后把假网站的 Disallow: /vip/ 删掉重启,再跑一次——观察禁区这次被抓到了,理解”能不能进”完全由告示牌说了算。
提示:如果打印的是”失败”而不是”跳过”,说明 can_fetch 都返回了 True、卡在网络层——检查假网站是不是还在跑(终端里有没有 Running on http://127.0.0.1:5003);rp.read() 失败时 can_fetch 可能全返回 True,所以假网站必须先启动。
练习2:给真实网站”读告示牌”,但只读不爬。用 RobotFileParser 读三个真实网站的 robots.txt(比如 www.python.org、news.ycombinator.com、你自己单位的官网),打印各自的 crawl_delay 和”首页允许进吗”。对比三家的告示牌风格,回答:哪家最欢迎爬虫?哪家写了 Crawl-delay?
提示:crawl_delay 返回 None 表示人家没特别要求;有的网站 robots.txt 可能有几百行(多是给特定搜索引擎立的规矩),用 User-agent: * 那组规矩判断你自己就行。这一题只读告示牌、不抓页面——练的是”进门先看规矩”的肌肉记忆。
练习3(动脑+选做):把第43篇的爬虫升级成”礼仪版”。翻出之前的分页爬虫,加四件套:①robots.txt 检查(禁止的路径跳过并打印原因);②限速(crawl_delay 或默认 2 秒,用 random.uniform 抖动);③UA 自报家门带邮箱;④增量抓取(抓过的 URL 存 seen.txt,下次跳过)。验收:连续跑两遍,第二遍应该打印”新增 0 条、跳过 N 条”。选做挑战:再加一个”礼貌上限”——单次运行最多抓 50 页,到了就停并打印”今日配额用完”,把”少拿”也变成代码。
提示:seen.txt 一行存一个 URL,用第9篇学的文件读写就行——”读文件拆成集合(第8篇),抓到新 URL 就写一行并加进集合”。随机抖动记得 import random;礼貌上限用一个计数器,if 已抓数 >= 50: break。
下篇预告
下一篇(第48篇):阶段项目:价格监控工具 — 会爬、会存、会提醒的完整小系统。把阶段4的全部手艺攒成一个真项目:定时抓价格(爬虫+礼仪)、历史存 CSV(第25篇)、跌破目标价就通知你(第38篇 API 的活学活用)。那是网络篇的收官之战——做完它,”从网上拿数据”这件事你就出师了。
发表回复