Python 异步抓取实战:连接复用、并发限流与超时控制

举报
yd_230003037 发表于 2026/11/07 11:37:36 2026/11/07
【摘要】 用 asyncio + aiohttp 改写采集脚本后踩的三个坑:连接池必须复用、并发要用 Semaphore 设闸门、超时拆成连接/读取/总时长三层。附可直接复用的代码与重试退避写法。

做数据采集时,最容易被低估的不是解析逻辑,而是"请求怎么发"。同步 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 限流、把超时拆成三层,这三步做完,同样的机器上跑同样的目标,耗时通常能降到原来的十分之一左右,而且不容易把对方站点打出异常流量。


代码不复杂,难的是知道该在哪里设闸门。上面这几段可以直接拿去改,参数按目标站点的承受能力调就行。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。