因为beautifulsoup和lxml解析html是cpu密集型操作,受python gil限制,多线程无法真正并行;而多进程可绕过gil、充分利用多核cpu,实测提速2.5–3.8倍,且lxml进程安全、解析更快。

为什么用多进程而不是多线程解析HTML
Python 的 BeautifulSoup 和 lxml 解析 HTML 是 CPU 密集型操作,GIL 会严重限制多线程的并行效率。实测中,10 个线程解析 100 个页面,耗时可能比单线程还高;而用 multiprocessing.Pool,CPU 利用率能接近核心数 × 100%,提速通常在 2.5–3.8 倍(取决于解析复杂度和 CPU 核心数)。
注意:不是所有场景都适合——如果解析逻辑里混了大量 requests.get(),那瓶颈在 I/O,该用异步(aiohttp+asyncio),而不是多进程。
Pool.map() 解析 HTML 的标准写法
把「下载」和「解析」严格分离:先用 requests 或其他方式批量获取 HTML 字符串(或文件路径),再把这批字符串丢给 Pool.map() 并行解析。不要在 worker 函数里再发网络请求——这会导致连接池争抢、DNS 阻塞、甚至被封 IP。
- worker 函数必须是顶层函数(不能是类方法或闭包),否则
Pool无法序列化传递 - 传入
Pool的数据最好是str或bytes,避免传入带状态的对象(如未关闭的requests.Session) - 解析库推荐用
lxml:比BeautifulSoup快 3–5 倍,且线程/进程安全;若必须用bs4,确保安装的是lxml或html.parser作为 backend,别用html5lib
示例 worker:
def parse_html(html_str):
from lxml import etree
tree = etree.HTML(html_str)
title = tree.xpath('//title/text()')
links = tree.xpath('//a/@href')
return {'title': title[0] if title else '', 'links': links}
常见报错:PicklingError: Can't pickle <function ...></function>
这是最常卡住新手的问题:你定义了解析函数在类内部、用了 lambda、或者函数引用了模块外的变量(比如全局 session 或未导入的库)。multiprocessing 底层靠 pickle 传递函数和参数,不支持这些。
- 解决方案:把解析函数写在模块顶层,所有依赖(如
lxml、re)都在函数内导入 - 别在函数里用
self、nonlocal、外部闭包变量 - 如果必须共享配置(如 XPath 表达式),用普通参数传入,别用默认参数绑定可变对象
错误示范:parse_func = lambda x: BeautifulSoup(x, 'lxml') → 会报错;正确写法是定义命名函数,并在函数体里 import。
控制并发数和内存占用的关键参数
Pool 默认进程数等于 CPU 核心数,但 HTML 解析常伴随大字符串(一个 2MB 的 HTML 页面,在 lxml 构建树时可能占 10MB 内存),开太多进程容易 OOM。建议显式设置 processes:
- 内存紧张时设为
min(4, os.cpu_count()) - 解析含大量 JS 渲染内容的页面(需先跑无头浏览器),进程数务必 ≤ 2,否则 Chromium 实例爆炸
- 用
pool.map()而非pool.imap_unordered()保证顺序;若不需要顺序且想流式处理,用imap+chunksize=1避免大列表阻塞
别忘了 pool.close() 和 pool.join(),尤其在脚本末尾或循环中重复创建 Pool 时,否则子进程变成僵尸进程。
真正麻烦的从来不是写几行 Pool 代码,而是 HTML 结构千奇百怪导致的 XPath 失效、编码自动检测翻车、script 标签干扰解析——这些得靠每个 worker 里加 try/except 和 fallback 逻辑,而不是指望并发本身帮你兜底。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











