速读#
这篇标题里有 requests 和 BeautifulSoup,但重点不是 HTML 怎么解析,而是“能抓”和“该抓”的边界。爬虫水平不体现在抓得多猛,而体现在频率、身份、timeout、robots.txt 这些分寸上;克制同时保护对方和自己。
水平在分寸,不在抓得猛#
把请求发得又多又快,技术上没有任何门槛——几行并发代码就能把频率推到对方明显吃不消的量级。真正难的是分寸感:对方的页面是对方花钱花算力供出来的资源,我的并发、频率、身份,是否在该用的范围内。
「能抓到更多」在技术上永远成立;「该不该抓到这个程度」是另一个问句。把两个问句分开,是这篇要立的判断。
我坚持的几条#
import timeimport requestsfrom requests.adapters import HTTPAdapterfrom urllib3.util import Retry
USER_AGENT = "s1oopx-learner/1.0 (+mailto:you@example.com)"
with requests.Session() as session: session.headers["User-Agent"] = USER_AGENT retry = Retry( total=3, backoff_factor=1, status_forcelist=(429, 502, 503, 504), allowed_methods={"GET", "HEAD"}, respect_retry_after_header=True, ) session.mount("https://", HTTPAdapter(max_retries=retry)) resp = session.get(url, timeout=(3.05, 15)) # 连接超时、读取超时 resp.raise_for_status() if "text/html" not in resp.headers.get("Content-Type", ""): raise ValueError("expected an HTML response") time.sleep(1) # 再请求同一站点前限速| 该做的 | 原因 |
|---|---|
| 看 robots.txt | 把站点给自动客户端的最低限度信号纳入决策 |
| 设置连接与读取 timeout | 建连和响应卡住时都能退出 |
| 做每站点限速 | 控制持续频率,而不只是单次并发 |
| 带可识别 User-Agent | 说明用途并提供有效联系方式 |
示例里的邮箱必须换成真实可联系地址。timeout 和限速既保护对方,也保护自己。Requests 的 timeout 不是整个请求的绝对总时长:元组分别限制连接阶段,以及底层套接字在多久收不到新数据后放弃;流式响应仍可能持续更久。固定 sleep(1) 适合单进程小脚本,多 worker 时则要共享每个目标站点的速率限制,否则每个进程都“睡一秒”,合起来仍可能很猛。
robots.txt 值得单独说一句:它不是访问控制,也不是抓取许可,只是站点给自动客户端的公开规则。遵守它不能替代服务条款、官方 API 规则、版权、个人信息和适用法律判断;robots 没禁止,也不代表任何用途都被允许。User-Agent 同理:写真实用途和可联系信息,而不是伪装浏览器去绕过封禁、验证码或登录边界。遇到明确拒绝就停下来,优先申请 API、数据导出或书面许可。
失败是常态,不是意外#
写爬虫很快撞上一个事实:网络请求不是本地函数调用,超时、临时 5xx、限流和页面结构变化都会发生。程序应该明确记录失败并继续处理其他任务,而不是静默吞掉或无限重试。
resp = session.get(url, timeout=(3.05, 15))resp.raise_for_status() # 4xx / 5xx 显式抛出来,别静默往下解析不加这行,拿到一个 404 页面接着往下 BeautifulSoup,解析出一堆空值还不知道为什么——错误要在发生的那一层暴露,别让它伪装成数据问题漏到下一层。
raise_for_status() 也只看状态码。站点可能用 200 返回登录页、验证码或错误模板,所以解析前还要检查最终 URL、Content-Type 和页面中应存在的稳定标记;解析后校验必填字段数量,结构突然归零时先停,不把空结果覆盖进已有数据。
失败之后怎么办也有分寸:只对幂等请求和明确的临时错误做有限重试,优先遵守服务器的 Retry-After,再使用有上限的指数退避;多个 worker 同时运行时,还应在站点级调度中加入随机抖动,避免一起重试。400、401、403、404 通常不是“多试几次就好”,POST 等有副作用操作更不能默认重放。上面的 Retry 和 HTTPAdapter 已经复用 Requests 自带的 urllib3 能力,不需要再手写一个容易无限循环的重试器。
重试用尽后把 URL、状态码、尝试次数和时间记下来,稍后人工判断;不要把响应正文整份写日志,其中可能含个人信息或令牌。
能抓 ≠ 该抓#
- 公开 ≠ 允许:页面公开不代表授权你高频抓
- 能复现登录态绕过 ≠ 该绕过:已越过「公开数据」边界
- 能收集 ≠ 该保存:只保留完成目的所需字段,并设置删除期限
我没把「能抓到」当目标函数。这条线画在哪,取决于我愿不愿意把这套做法讲出来给对方看——讲不出口的,基本就不该做。
别把礼貌绝对化到不敢抓#
在条款、用途和频率都允许的前提下抓取公开数据,是正常的自动化方式。守的是范围、频率、身份、数据最小化和退出机制,不是因为风险存在就停止一切自动访问;涉及商业使用、个人信息或大规模再分发时,应先做具体的合规判断,而不是从一篇技术文章推导法律结论。
后来用 Flask 调外部 API,基本就是这套排列组合:带 UA、带 timeout、控制频率。
有一次技术上几百 QPS 能跑,我仍选单线程加 2 秒 sleep——不急,没必要把别人服务器打爆。这个选择不是技术限制,是边界感。 后来延伸到运维:能开的端口不代表该开,能放的权不代表该放。
