W 工作 Work

一次爬虫任务的复盘:超时、分页与礼貌

复盘一次 Python 爬虫:超时重试怎么设、分页怎么走稳、以及怎样少给对方服务器添麻烦。

工作

上周接了一个「把某站点公开列表页拉全」的小任务。需求不复杂:分页抓取、落成 CSV,供后面分析用。结果两天里卡在超时和翻页边界上,写篇复盘给以后的自己。

背景

  • 目标:公开列表页,大约三千条
  • 约束:只能用公开接口 / 页面,不能登录绕过
  • 工具:Python、httpx、简单解析

一开始我写得很急:循环翻页、直接 get、解析、写入。本地试了三页看起来正常,一上全量就炸。

卡点一:超时

对方响应偶尔到 8–12 秒。我默认超时 5 秒,于是大量失败,重试又叠上去,反而更堵。

后来改成:

  • 连接超时短一点(例如 5s)
  • 读超时放宽(例如 20s)
  • 失败只重试 2–3 次,指数退避
  • 429 / 5xx 和网络错误才重试;4xx 里除了 429 直接记日志跳过

超时不是「设越大越好」。太大浪费线程/协程,太小制造噪声。要和对方真实 P95 对齐,再留一点余量。

卡点二:分页

站点用 ?page=,但最后几页行为很坑:

  • 空页返回 200 + 空列表(不是 404)
  • 超出范围偶尔仍返回最后一页内容(重复)

我加了两个停止条件:

  1. 本页条目数为 0
  2. 本页第一条 id 与上一页第一条相同(防循环)

同时把「已见 id」放进 set,写入前去重。比迷信 total_pages 字段靠谱——那个字段有一次是错的。

礼貌爬取(真的有用)

不是口号,是少被封、少给对方添乱:

import asyncio
import httpx

HEADERS = {
    "User-Agent": "WDResearchBot/0.1 (+https://github.com/wdfortest; personal research)",
    "Accept": "text/html,application/json",
}

async def fetch(client: httpx.AsyncClient, url: str) -> httpx.Response:
    await asyncio.sleep(0.8)  # 固定间隔,别打满
    return await client.get(url, headers=HEADERS, timeout=httpx.Timeout(5.0, read=20.0))

另外:

  • 并发压到 2–3,别开 20 个工人「炫技」
  • 遵守 robots.txt 里能看懂的规则
  • User-Agent 写清楚用途与联系方式(哪怕只是 GitHub)
  • 失败率升高就主动降速或停一晚再跑

结果与带走的东西

最终全量跑通,去重后约 2.9k 条,和页面估算接近。耗时比「暴力并发」慢,但中途几乎没被临时封。

带走三条:

  1. 先测边界页,再写全量循环
  2. 超时 / 重试 / 退避 和业务去重一样重要
  3. 礼貌不是道德装饰,是稳定性的一部分

下次类似任务,我会先写一个「只跑 5 页 + 最后 2 页」的探测脚本,再开全量。

评论