“lxml 是 C 写的,线程应该能并行吧?”在爬虫里这是个常见假设。这篇用 hashlib、zlib 这样已知释放 GIL 的操作,和纯 Python 循环这样已知不释放的操作做参照,把 lxml 的解析、xpath、序列化逐项量一遍,看多线程到底能不能加速。
任务 单次 1线程 2线程 4线程 8线程 对照:hashlib.sha256(8 MiB) 3.3 ms 52 ms 1.95x 3.81x 5.61x 对照:zlib.compress(8 MiB) 132.7 ms 2138 ms 1.99x 3.89x 5.63x 对照:纯 Python 循环 13.2 ms 213 ms 0.99x 0.98x 1.00x 对照:re.findall(Python 正则,文本) 1.0 ms 16 ms 0.92x 0.91x 0.92x
sha256 和 zlib 在 8 线程下达到约 5.6 倍,说明这台机器上线程确实能并行;纯 Python 循环和 re.findall 约 1 倍(正则略慢于 1)。后面所有 lxml 数字都和这两端比。
任务 单次 1线程 2线程 4线程 8线程 lxml.html.fromstring(bytes) 14.5 ms 274 ms 0.90x 0.98x 0.90x etree.HTML(bytes)(C 层 HTML 解析器) 15.6 ms 299 ms 1.08x 1.09x 1.08x lxml.html.parse(文件路径) 14.9 ms 279 ms 0.98x 0.98x 0.68x lxml.html.parse(BytesIO) 17.8 ms 322 ms 1.15x 1.15x 1.14x
lxml.html.fromstring、etree.HTML、lxml.html.parse(不论文件还是 BytesIO)在 8 线程下都在 0.68–1.15 倍之间,几乎完全串行。etree.HTML 直接调用 C 层的解析器,结果和 lxml.html 一样,说明不是 Python 包装层的原因。解析器内部为什么不能并行,我没有查源码。
任务 单次 1线程 2线程 4线程 8线程 etree.fromstring(XML bytes) 3.4 ms 67 ms 1.31x 1.68x 2.10x etree.parse(XML 文件路径) 3.5 ms 70 ms 1.35x 1.65x 1.98x
同一个库的 XML 解析器可以并行,8 线程约 2 倍(fromstring 2.10 倍,从文件 1.98 倍),但远没有 sha256 那样接近线性。
任务 单次 1线程 2线程 4线程 8线程 xpath(已解析的树,每线程用自己的树) 3.6 ms 101 ms 1.11x 1.11x 1.07x cssselect 风格:iter + 逐元素读 text 2.1 ms 36 ms 0.92x 0.99x 0.99x etree.tostring(序列化) 8.1 ms 132 ms 1.69x 3.80x 4.14x
xpath 和逐元素取值没有加速,序列化 tostring 约 4.1 倍(多次运行 4.1–5.2 倍)。所以“解析 + 抽取”流程里线程基本帮不上忙,只有写出去的那一步能并行。要并行解析和抽取,上一篇的进程池才是办法。