异步并发:asyncio 与 aiohttp
前置知识
本文基于模型知识整理,建议核对官方文档(见 参考资料)。
一句话定义
asyncio 用单线程事件循环在等待网络 I/O 时切换执行其他协程,配合 aiohttp 可用极低资源实现数百并发请求——爬虫是异步模型最典型的受益场景。
为什么重要
抓取是纯 I/O 密集型工作:95% 的时间在等响应。同步代码 1000 个请求 × 0.2 秒 ≈ 200 秒;异步并发 20 路约 10–20 秒完成同样的活,且内存占用远低于线程池。它是自建下载器与 kp-036 云原生的技术底座。
前置知识
kp-006(HTTP 客户端经验)、kp-017(并发必须配限速)。
核心概念
- 事件循环:调度就绪协程的单线程循环;协程在
await处让出控制权。 - 协程函数:
async def定义,调用返回协程对象,需await或交给循环执行。 - Semaphore:并发闸门,限制同时在途的请求数。
- gather(return_exceptions=True):并发执行一批协程且不让单个失败炸掉全局。
- aiohttp.ClientSession:异步版 Session,连接池复用,必须作为上下文管理器使用。
原理与机制
同步与异步的耗时对比模型(1000 请求、单请求 0.2s、服务端不瓶颈):
同步串行: 1000 × 0.2s ≈ 200s
同步+20线程: ≈ 10s (线程切换+内存开销大)
asyncio+20并发: ≈ 10s (单线程, 开销极小)
上限由「并发度 × 远端接受度」决定 → 礼貌限速仍是前提
标准模板(Semaphore 限并发 + 客户端超时 + 失败隔离):
import asyncio, aiohttp
async def fetch(session, sem, url):
async with sem: # 并发闸门
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=20)) as r:
r.raise_for_status()
return await r.text()
except Exception as e:
return {"error": str(e), "url": url} # 失败也返回, 不炸全局
async def main(urls):
sem = asyncio.Semaphore(10) # 并发上限 10
async with aiohttp.ClientSession() as session: # 全局一个 Session
results = await asyncio.gather(
*(fetch(session, sem, u) for u in urls),
return_exceptions=True,
)
return results
asyncio.run(main(["https://books.toscrape.com/"] * 30))
三条铁律:协程内禁用 time.sleep(会卡死整个循环,须用 asyncio.sleep);全局只建一个 ClientSession;并发上限与 QPS 必须显式设置(Semaphore/令牌桶),否则等于对目标站发动压力测试。
实例或案例
把 kp-010 的「列表收集 URL + 详情抓取」异步化:先 gather 并发抓完 50 个列表页汇总 5000 个详情 URL,再以并发 10 分批 gather 详情页——总耗时从小时级降到分钟级,而代码只比同步版多 5 行。
常见误区
- 误区一:协程里用阻塞调用。
requests、time.sleep、同步 DB 驱动都会冻结事件循环;要么换异步库,要么loop.run_in_executor包裹。 - 误区二:并发数设到几百。 触发对端限流甚至封禁,也打满本机 fd;10–20 并发 + 限速是礼貌默认值。
- 误区三:
gather不带return_exceptions。 一个 404 让整批任务报废;收集失败项单独重试才是工程做法。
自测题
- 为什么爬虫特别适合 asyncio?
答:爬虫是 I/O 密集(绝大部分时间在等网络),事件循环在等待期切换协程即可,无需线程开销。
asyncio.Semaphore(10)放在哪一层才正确?
答:包住每次请求的临界区(async with sem 内发起请求),使全任务同时在途请求 ≤ 10。
- 协程中调用
time.sleep(1)的后果?
答:阻塞整个事件循环,所有协程停摆 1 秒;必须用 asyncio.sleep。
与其他知识点的关系
kp-018 的 Scrapy 内部正是 Twisted 异步框架(同思想不同实现);kp-022 的 worker 通常以 asyncio 编写;kp-036 无头集群的调度层也依赖异步。
延伸阅读
Python 官方 asyncio 文档「Coroutines and Tasks」;aiohttp 官方 quickstart。