一个几十上百 MB 的列表页或数据文件,整棵树解析会把内存吃到文件的十几倍。lxml 的 iterparse 可以边读边处理边丢弃,但丢弃的写法有讲究。这篇在 5、25、100 MiB 的文件上量清楚内存、耗时和需要注意的坑。
--- 文件 5 MiB(磁盘上实际 5.0 MiB),42,762 行 <tr> --- 方式 耗时 峰值内存 净增 净增 / 文件大小 lxml.html.fromstring(整个文件读进内存再解析) 0.12 秒 118 MiB 97 MiB 19.4 倍 lxml.html.parse(文件路径) 0.11 秒 113 MiB 92 MiB 18.4 倍 iterparse(tag="tr"),不清理 0.18 秒 116 MiB 95 MiB 19.0 倍 iterparse + el.clear() 0.15 秒 34 MiB 13 MiB 2.7 倍 iterparse + el.clear() + 删掉前面的兄弟 0.16 秒 29 MiB 8 MiB 1.7 倍 selectolax Lexbor(整个文件读进内存) 0.48 秒 157 MiB 136 MiB 27.2 倍 标准库 html.parser,按 1 MiB 分块 feed 0.87 秒 40 MiB 19 MiB 3.8 倍 --- 文件 25 MiB(磁盘上实际 25.0 MiB),211,278 行 <tr> --- 方式 耗时 峰值内存 净增 净增 / 文件大小 lxml.html.fromstring(整个文件读进内存再解析) 0.66 秒 481 MiB 459 MiB 18.4 倍 lxml.html.parse(文件路径) 0.60 秒 475 MiB 454 MiB 18.2 倍 iterparse(tag="tr"),不清理 1.86 秒 482 MiB 461 MiB 18.4 倍 iterparse + el.clear() 0.91 秒 78 MiB 56 MiB 2.3 倍 iterparse + el.clear() + 删掉前面的兄弟 0.77 秒 52 MiB 30 MiB 1.2 倍 selectolax Lexbor(整个文件读进内存) 0.33 秒 681 MiB 660 MiB 26.4 倍 标准库 html.parser,按 1 MiB 分块 feed 3.99 秒 43 MiB 22 MiB 0.9 倍 --- 文件 100 MiB(磁盘上实际 100.0 MiB),839,781 行 <tr> --- 方式 耗时 峰值内存 净增 净增 / 文件大小 lxml.html.fromstring(整个文件读进内存再解析) 3.05 秒 1144 MiB 1122 MiB 11.2 倍 lxml.html.parse(文件路径) 2.61 秒 1670 MiB 1649 MiB 16.5 倍 iterparse(tag="tr"),不清理 22.63 秒 1620 MiB 1599 MiB 16.0 倍 iterparse + el.clear() 6.51 秒 238 MiB 217 MiB 2.2 倍 iterparse + el.clear() + 删掉前面的兄弟 3.04 秒 135 MiB 114 MiB 1.1 倍 selectolax Lexbor(整个文件读进内存) 1.59 秒 1061 MiB 1040 MiB 10.4 倍 (html.parser 在 100 MiB 的文件上没有跑,太慢)
整棵树的内存大约是文件的 11–19 倍(这个数字随结构变化,我没有解释 100 MiB 文件上 fromstring 与 parse 差别为什么有 11.2 对 16.5 倍)。iterparse 如果不清理,元素都还留在树里,和整棵树一样占内存,在 100 MiB 的文件上还慢到 22.63 秒;只 el.clear() 清空了元素内容,空壳仍留在父节点里,净增 217 MiB;再把前面的兄弟从父节点里删掉才只剩 114 MiB。
不传 encoding(libxml2 把没声明编码的 HTML 当 Latin-1,"¥" 读成 "Â¥"):ERR ValueError invalid literal for int() with base 10: 'Â¥0'
传 encoding="utf-8":提取 211278 行,价格合计 10140834,1.17 秒;代码要点:events=("end",)、tag="tr"、html=True,处理完一行就 clear() 并删除前面的兄弟selectolax 的 Lexbor 解析器很快,但要把整个文件读进内存,净增是文件的 10–27 倍。标准库 html.parser 可以分块 feed,内存很低,但在这个测试里慢得多(25 MiB 要 3.99 秒)。
整棵树:211278 行,价格合计 10140834;流式:211278 行,价格合计 10140834;两者一致:True
流式解析的 iterparse 对没有声明编码的 HTML 同样把它当 Latin-1,所以要显式传 encoding="utf-8"(第 16 篇讲过类似的问题)。传了之后,流式提取的行数和价格合计与整棵树完全一致。