优先用asyncio.create_task() + asyncio.wait(),因其支持统一超时、批量取消和按需重试;而gather()仅适合简单拉取已知URL且容错由上层处理的场景。

asyncio.create_task() 和 asyncio.gather() 选哪个?
爬虫任务并发启动时,asyncio.create_task() 和 asyncio.gather() 都能发请求,但行为差异直接影响可控性和错误处理。asyncio.gather() 会等待所有任务完成,任一异常(如超时、404)直接抛出,整个批次中断;而 create_task() 启动后可单独 await 或用 asyncio.wait() 管理生命周期。
- 如果需要统一控制超时、批量取消或按需重试,优先用
create_task()+asyncio.wait() - 如果只是简单拉取一批已知URL且容错交给上层(比如重试外层逻辑),
gather()更简洁 - 注意:
gather(return_exceptions=True)能把异常转为Exception对象而非中断,但会掩盖真实失败位置,调试困难
tasks = [asyncio.create_task(fetch(url)) for url in urls]
done, pending = await asyncio.wait(tasks, timeout=10.0)
for t in done:
try:
result = t.result()
except Exception as e:
print(f"Task failed: {e}")
aiohttp.ClientSession() 必须复用,不能每次新建
每个 aiohttp.ClientSession() 实例内部维护连接池和 cookie jar,频繁创建销毁会导致端口耗尽、TLS握手开销激增,甚至触发 OSError: [Errno 24] Too many open files。
- 全局只初始化一次
ClientSession,通过参数传入协程或用依赖注入方式共享 - 切勿在循环里写
async with aiohttp.ClientSession() as session:—— 这等价于每次新建会话 - 若需不同 headers 或 timeout,用
session.get(url, headers=..., timeout=...)而非新建 session
# ✅ 正确:复用 session session = aiohttp.ClientSession() results = await asyncio.gather(*[fetch(session, url) for url in urls]) await session.close() <h1>❌ 错误:每次新建</h1><p>async def fetch_bad(url): async with aiohttp.ClientSession() as session: # 每次都 new async with session.get(url) as resp: return await resp.text()</p>
如何安全处理 DNS 解析超时和连接拒绝?
异步爬虫卡在 DNS 或 TCP 握手是常见瓶颈,aiohttp 默认不设 DNS 超时,且底层 asyncio.open_connection() 在网络不通时可能阻塞数秒甚至更久。
- 显式配置
aiohttp.TCPConnector的limit(默认 100)、limit_per_host(防被封)和force_close(避免 TIME_WAIT 占用) - 设置
client_timeout=aiohttp.ClientTimeout(total=10, connect=5),其中connect控制 DNS+TCP 阶段,比total更关键 - 遇到
asyncio.TimeoutError或aiohttp.ClientConnectorError时,不要直接重试 —— 先检查是否是目标域名解析失败(socket.gaierror),避免无意义刷请求
connector = aiohttp.TCPConnector(
limit=50,
limit_per_host=5,
force_close=False,
enable_cleanup_closed=True
)
timeout = aiohttp.ClientTimeout(total=10, connect=3, sock_read=7)
session = aiohttp.ClientSession(connector=connector, timeout=timeout)
Python 3.11 的 taskgroup 为什么比 asyncio.wait() 更适合爬虫?
asyncio.TaskGroup(Python 3.11 新增)提供结构化并发语义:子任务异常会自动 cancel 其他正在运行的任务,并透出第一个异常 —— 这比手动 wait() + cancel() 更可靠,尤其适合“一批 URL 中任一失败就中止全部”的场景。
立即学习“Python免费学习笔记(深入)”;
- 但注意:它不适用于需要差异化错误处理的场景(比如部分失败仍要收集结果)
-
TaskGroup内部使用create_task(),所以仍需复用ClientSession,否则 session 关闭时机难控制 - 若需带返回值的批量执行,必须用
taskgroup.create_task()而非裸 await,否则无法被 group 管理
async def fetch_all(urls):
connector = aiohttp.TCPConnector(limit=10)
timeout = aiohttp.ClientTimeout(total=8)
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(fetch(session, url)) for url in urls]
return [t.result() for t in tasks] # 所有成功才返回
实际跑起来你会发现,DNS 缓存、连接复用、异常传播路径这三处稍不留意就会让并发数虚高、失败静默或资源泄漏——不是加了 async/await 就自动高并发,而是每层 IO 调用都要对齐异步生命周期。


















