ET.parse()因加载全量XML致OOM,iterparse()流式解析+clear()清理+end事件+精准XPath+禁用DTD+小文件合并分块可显著提速降内存。

用 xml.etree.ElementTree.iterparse() 替代 ET.parse()
大批量 XML 解析慢,往往是因为一次性加载整个文档到内存,再构建树结构。对几百 MB 的文件,ET.parse() 会卡住甚至 OOM。iterparse() 是流式解析,边读边处理,内存占用稳定在 KB~MB 级别。
关键点:它不建完整树,只在触发事件(如 start、end)时返回当前元素,适合只关心特定标签的场景。
- 必须手动调用
elem.clear()清理已处理子树,否则内存仍会缓慢上涨 - 事件类型选
end更安全——此时元素所有子节点已解析完毕,文本/属性可直接用 - 避免在
start里做 heavy 操作,因为父元素还没闭合,后续可能被重复访问 - 示例片段:
for event, elem in ET.iterparse(file_path, events=('end',)): if elem.tag == 'record': process(elem) elem.clear() # 必须加
提前过滤标签,减少 elem.iter() 遍历开销
很多人习惯用 root.iter('field') 找所有字段,但若 XML 层级深、嵌套多,每次调用都会递归遍历整棵子树。当单个 record 下有上百个 field,而你只关心其中 3 个,这种遍历就是纯浪费。
- 优先用
elem.find('field[@name="status"]')精准定位,比iter()快 5–10 倍(实测 10MB 文件) - 如果需匹配多个固定 name,改用
elem.findall('.//field[@name="a" or @name="b"]'),XPath 支持简单逻辑 - 避免
elem.iter()后再用if elem.get('name') == ...过滤——这是双重遍历 - 注意:
find()只查直接子节点,要查任意后代得加.//前缀
关闭 DTD 和外部实体解析,防止隐式网络请求或 XXE
默认情况下,ElementTree 会尝试解析 DTD 声明(哪怕没用),遇到 DOCTYPE 或 <!ENTITY 就可能触发 HTTP 请求、读本地文件,不仅慢,还存在 XXE 风险。生产环境 XML 往往不含 DTD,关掉能提速 10%~30%。
- 用
XMLParser显式禁用:parser = ET.XMLParser(resolve_entities=False, no_network=True) tree = ET.parse(file_path, parser)
- 如果 XML 确实含合法 DTD 且必须校验,改用
defusedxml库替代原生xml.etree,它默认加固 - 检查错误信息:
xml.etree.ElementTree.ParseError: mismatched tag有时是 DTD 解析失败伪装的,先关掉再试
小文件合并 + 分块处理比单线程逐个解析更稳
当有上千个小 XML(比如每份 100KB),用 for 循环逐个 ET.parse(),Python GIL 和频繁 IO 切换会让 CPU 利用率长期低于 30%。不如批量读入、内存解析、结果聚合。
- 把多个小文件内容拼成一个伪根节点(如
<batch>...</batch>),再用iterparse()流式处理,省去重复初始化开销 - 按 50–200 个文件为一批,用
concurrent.futures.ProcessPoolExecutor处理——注意别超 4 进程,太多反而因序列化拖慢 - 别用
ThreadPoolExecutor加速解析——ElementTree是 C 实现,GIL 不释放,线程没用 - 如果文件路径分散,先用
os.scandir()预扫描,避免glob每次都走目录树
实际跑起来会发现,最耗时的环节往往不是解析本身,而是你没清掉的 elem 引用、反复的 XPath 编译、或者某个隐藏的 DTD 请求。这些点不盯住,换再快的库也白搭。











