beautifulsoup 本身不支持异步,因其是纯 cpu 密集型解析器,无 i/o 操作;提速需分离网络请求(用 httpx.asyncclient 异步并发)与 html 解析(用 multiprocessing 同步分发),避免 gil 限制和事件循环开销。

BeautifulSoup 本身不支持异步,强行套 asyncio 不会加速解析;真正要提速,得把「网络请求」和「解析」拆开,让请求异步化,解析仍用同步但走多进程分发。
为什么不能直接 await BeautifulSoup()
BeautifulSoup 是纯 CPU 密集型的 HTML 解析器,所有操作都在本地内存完成,不涉及 I/O。给它加 async 关键字或扔进 asyncio.create_task() 不会产生并发效果,反而因事件循环调度引入额外开销。常见错误是写成:
async def parse(html):
soup = BeautifulSoup(html, "lxml") # ❌ 这行仍是同步阻塞
return soup.find("title").text
这类代码在 async 函数里跑,但实际执行时仍是逐个解析,毫无并发收益。
requests + asyncio 并发请求 + multiprocessing 解析
高效组合的关键在于职责分离:用 aiohttp 或 httpx.AsyncClient 并发拉取 HTML,拿到结果后,把大批量 html 字符串丢给 multiprocessing.Pool 或 concurrent.futures.ProcessPoolExecutor 做解析。这样既绕过了 GIL 对解析的限制,又避免了 requests 同步阻塞。
- 不要用
requests.get()配asyncio—— 它不是协程,会阻塞整个 event loop - 推荐用
httpx.AsyncClient:API 接近 requests,原生支持async,且可复用连接 - 解析函数必须是普通同步函数,例如
def parse_page(html: str) -> dict:,才能被进程池安全调用 - 注意
html字符串大小:过大时跨进程传递有开销,必要时可传 URL + 共享 session key,让子进程自己 fetch(但需处理 Cookie/headers 同步)
multiprocessing.Pool 解析时的坑
子进程无法继承主进程的全局状态,比如已配置好的 requests.Session、全局 logger、或自定义的 BeautifulSoup 解析器实例。常见报错:
AttributeError: Can't pickle local object 'create_session.<locals>.session'</locals>
解决方式:
- 所有依赖都通过参数显式传入,比如把
parser类型("lxml"或"html.parser")作为参数,而不是传一个已初始化的BeautifulSoup对象 - 避免在解析函数里 import 模块(虽通常不会错,但某些环境如 Windows 下 spawn 方式会重复 import,导致意外行为)
- 若需共享配置(如 User-Agent),建议用
functools.partial预设参数,而非闭包捕获 - 进程数别盲目设高——超过 CPU 核心数太多反而因上下文切换拖慢整体速度;一般设为
os.cpu_count() or 4较稳妥
完整流程示意(无框架精简版)
以下逻辑能跑通,不含异常重试、限速、UA 轮换等工程细节,但体现了核心分工:
import asyncio
import httpx
from multiprocessing import Pool
from bs4 import BeautifulSoup
<p>def parse_html(html: str) -> str:
soup = BeautifulSoup(html, "lxml")
return soup.title.string if soup.title else ""</p><p>async def fetch_all(urls):
async with httpx.AsyncClient(follow_redirects=True) as client:
tasks = [client.get(url) for url in urls]
responses = await asyncio.gather(*tasks)
return [r.text for r in responses if r.is_success]</p><p>def main(urls):</p><h1>异步抓取</h1><pre class="brush:python;toolbar:false;">htmls = asyncio.run(fetch_all(urls))
# 多进程解析
with Pool(4) as pool:
titles = pool.map(parse_html, htmls)
return titles
这个结构里,fetch_all 负责并发下载,parse_html 是纯 CPU 工作单元,Pool.map 自动分发到多个进程。两阶段之间用内存列表传递,简单直接。
最容易被忽略的是:解析函数不能依赖任何主进程中的对象状态,也不能调用任何带副作用的全局函数(比如写文件、改全局变量)。一旦跨进程边界,就只剩参数和返回值是可信的。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











