Python 异步抓取实战:连接复用、并发限流与超时控制
做数据采集时,最容易被低估的不是解析逻辑,而是"请求怎么发"。同步 requests 一把梭在几十个页面上还撑得住,一旦到几百上千个页面,耗时和内存都会失控。用 asyncio + aiohttp 改写之后,我踩了三个坑,这里把结论和代码一起记下来。
一、连接池必须复用,不要让每个任务自己开连接
最常见的写法是每个协程里 aiohttp.ClientSession(),看起来干净,实际上每建一次 Session 就新建连接池和 TLS 握手,几百个任务下来握手开销比下载数据还大,还容易把本地端口耗尽。
正确做法是整个生命周期共用一个 Session,并显式设置连接数上限:
session = aiohttp.ClientSession(
connector=aiohttp.TCPConnector(limit=50, limit_per_host=10, ttl_dns_cache=300),
timeout=aiohttp.ClientTimeout(total=20),
)
async with session:
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
limit 是全局连接上限,limit_per_host 是单站点上限。后者比前者更重要:对同一个域名开太多并发,服务端会判定为异常流量直接拒绝,而不是排队。
二、并发不等于无限并发,用 Semaphore 做闸门
gather 一次丢进去一万个任务,事件循环会瞬间创建一万个协程,内存飙升,而且服务端压力不可控。我习惯把并发度压到 20 左右,用信号量兜住:
sem = asyncio.Semaphore(20)
async def fetch(session, url):
async with sem:
async with session.get(url) as resp:
resp.raise_for_status()
return await resp.text()
信号量放在 get 之前,而不是之后——放在之后只限制了读响应体,请求已经打出去了,起不到限流作用。
三、超时分成三层,别只设一个总的
只设 total 超时的问题在于:DNS 卡住、TLS 卡住、读响应体卡住,三种情况的合理等待时间完全不同。拆开设置更稳:
timeout = aiohttp.ClientTimeout(total=20, connect=5, sock_read=10)
connect 管建连,sock_read 管单次读取。总超时留给兜底,避免某一个阶段无限期挂着。另外重试要带指数退避,不能立即重打:
async def fetch_with_retry(session, url, retries=3):
for i in range(retries):
try:
return await fetch(session, url)
except (aiohttp.ClientError, asyncio.TimeoutError):
if i == retries - 1:
raise
await asyncio.sleep(2 ** i)
四、异常不要用 gather 直接吞掉
gather 默认遇到第一个异常就向上抛,前面的结果全丢。加 return_exceptions=True 会收集异常对象,但要注意后面必须自己过滤:
for item in results:
if isinstance(item, Exception):
failed.append(item)
else:
ok.append(item)
否则统计出来的"成功数"会把异常对象也算进去,最后发现数据少了却查不出原因。
五、落盘也要异步,别在事件循环里做同步 IO
采集完直接 open().write() 是最常见的性能陷阱:同步写文件会阻塞事件循环,所有协程一起卡住。要么用 aiofiles 异步写,要么把写操作丢进线程池:
await loop.run_in_executor(None, save_json, path, data)
数据量大的时候,更好的做法是先在内存里聚合成批,每 500 条落一次盘,既减少 IO 次数,也降低进程被杀时丢数据的风险。
小结
异步抓取的性能提升,八成来自"连接复用 + 并发有上限"这两件事,剩下的才是语法层面的优化。把 Session 提到外层、用 Semaphore 限流、把超时拆成三层,这三步做完,同样的机器上跑同样的目标,耗时通常能降到原来的十分之一左右,而且不容易把对方站点打出异常流量。
代码不复杂,难的是知道该在哪里设闸门。上面这几段可以直接拿去改,参数按目标站点的承受能力调就行。
- 点赞
- 收藏
- 关注作者
评论(0)