上周接了一个「把某站点公开列表页拉全」的小任务。需求不复杂:分页抓取、落成 CSV,供后面分析用。结果两天里卡在超时和翻页边界上,写篇复盘给以后的自己。
背景
- 目标:公开列表页,大约三千条
- 约束:只能用公开接口 / 页面,不能登录绕过
- 工具:Python、
httpx、简单解析
一开始我写得很急:循环翻页、直接 get、解析、写入。本地试了三页看起来正常,一上全量就炸。
卡点一:超时
对方响应偶尔到 8–12 秒。我默认超时 5 秒,于是大量失败,重试又叠上去,反而更堵。
后来改成:
- 连接超时短一点(例如 5s)
- 读超时放宽(例如 20s)
- 失败只重试 2–3 次,指数退避
- 429 / 5xx 和网络错误才重试;4xx 里除了 429 直接记日志跳过
超时不是「设越大越好」。太大浪费线程/协程,太小制造噪声。要和对方真实 P95 对齐,再留一点余量。
卡点二:分页
站点用 ?page=,但最后几页行为很坑:
- 空页返回 200 + 空列表(不是 404)
- 超出范围偶尔仍返回最后一页内容(重复)
我加了两个停止条件:
- 本页条目数为 0
- 本页第一条 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 条,和页面估算接近。耗时比「暴力并发」慢,但中途几乎没被临时封。
带走三条:
- 先测边界页,再写全量循环
- 超时 / 重试 / 退避 和业务去重一样重要
- 礼貌不是道德装饰,是稳定性的一部分
下次类似任务,我会先写一个「只跑 5 页 + 最后 2 页」的探测脚本,再开全量。
评论