单靠多进程无法提升IO密集型爬虫性能,因其仅分散等待而非消除阻塞;单靠协程则在CPU解析时拖垮事件循环。正确方案是多进程(横向扩展)+ 协程(单核高并发),每个进程内用aiohttp并发请求、同步解析,避免阻塞loop。

为什么不能只用多进程或只用协程
单靠 multiprocessing 启一堆 requests.get() 进程,每个进程仍只能并发 1 个请求——它不解决 IO 阻塞,只是把“等 100 个页面”变成“10 个进程各等 10 个页面”,总耗时没本质下降;单靠 asyncio + aiohttp 虽能并发几千请求,但受限于单线程,CPU 密集型解析(如用 lxml 多次 parse、正则提取大量字段)会拖垮事件循环,导致协程调度卡顿甚至饿死。
多进程 + 协程的分工必须明确
核心原则:进程负责「横向扩展」,协程负责「纵向压榨单核 IO 并发」。每个子进程内只跑一个事件循环,用 aiohttp 并发发起请求,拿到响应后再用同步方式做解析(避免在协程里调用阻塞函数)。常见错误是直接在 async def 里调用 BeautifulSoup 或 lxml.etree.HTML(),这会让整个 event loop 停摆。
- 每个进程启动前,需调用
asyncio.set_event_loop_policy()(Windows/macOS 必须) - 不要在协程函数里用
time.sleep()或requests.get(),它们会阻塞当前 event loop - 解析逻辑务必放在
await fetch_data()返回后,在普通函数中执行 - 进程数建议设为
os.cpu_count() or 4,不是越多越好;协程并发数可设为 50–200,取决于目标站抗压能力和本地网络带宽
Pool.map() 传协程函数会报错,得绕开
multiprocessing.Pool.map() 只接受同步函数,直接传 async def 会抛 TypeError: can't pickle coroutine objects。正确做法是:在子进程内部启动事件循环,用 asyncio.run() 或 loop.run_until_complete() 执行协程任务。注意 Python 3.8+ 在子进程中调用 asyncio.run() 是安全的,旧版本需手动管理 loop。
def worker(urls_chunk):
import asyncio
import aiohttp
<pre class="brush:php;toolbar:false;">async def fetch_one(session, url):
try:
async with session.get(url, timeout=10) as resp:
return await resp.text()
except Exception:
return None
async def run_all():
async with aiohttp.ClientSession() as session:
tasks = [fetch_one(session, url) for url in urls_chunk]
return await asyncio.gather(*tasks, return_exceptions=True)
# 在子进程中运行协程
return asyncio.run(run_all())
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
立即学习“Python免费学习笔记(深入)”;
主进程调用:pool.map(worker, chunks),其中 chunks 是 URL 列表切片(如每块 50 个 URL)。
容易被忽略的稳定性细节
真实场景下,组合方案最常崩在三处:DNS 缓存未复用、连接池爆炸、异常未隔离。默认 aiohttp 每次新建 ClientSession 都会重建 DNS 缓存和 TCP 连接池,高频请求下极易触发 OSError: [Errno 24] Too many open files。必须显式配置:
- 复用
ClientSession实例,不要在循环里反复创建 - 设置
connector = aiohttp.TCPConnector(limit=100, limit_per_host=30, ttl_dns_cache=300) - 为每个进程单独设置
headers和timeout,避免共享状态引发竞态 - URL 分片要均匀,否则某些进程负载过重、提前退出,导致整体等待时间由最慢进程决定
真正跑通百万级抓取,关键不在并发数字多高,而在每个进程是否稳住自己的那一撮请求——协程负责“快”,多进程负责“扛”,少一个,都撑不住真实流量。

















