应选lxml而非xml.etree.ElementTree,因其底层C实现支持增量解析、XPath 2.0子集、严格命名空间处理及更强容错性,尤其适合处理大型、畸形或含DTD/命名空间的第三方XML配置文件。

为什么不用xml.etree.ElementTree而选lxml
因为 xml.etree.ElementTree 在处理大型配置文件(比如超 10MB、嵌套深、含命名空间或 DTD 声明)时容易抛出 ParseError 或内存暴涨;lxml 底层用 C 实现,支持增量解析、XPath 2.0 子集、更严格的命名空间处理,且对 malformed XML 容错更强——尤其当你面对的是第三方导出的“看似合法实则带冗余空格/注释/编码声明”的配置文件时。
实操建议:
- 安装时明确指定
lxml而非仅lxml-python(后者是旧包名,已废弃) - 若在 CentOS/RHEL 上编译失败,先装
libxml2-devel和libxslt-devel - 避免直接用
etree.fromstring()加载含外部 DTD 的配置——改用etree.parse()配合禁用网络加载的 parser
如何安全加载含命名空间和编码声明的配置文件
很多 XML 配置文件开头类似:。直接用 etree.parse() 会把默认命名空间当作无前缀的 {http://example.com/ns},导致后续 find() 全部失效。
正确做法:
- 用
etree.XMLParser(resolve_entities=False, no_network=True)创建 parser,防止意外加载远程 DTD - 用
etree.parse(file_path, parser=parser)加载,而非fromstring() - 提取根元素后,用
root.nsmap查看命名空间映射,再构造带前缀的 XPath,例如:root.xpath('//ns:server', namespaces={'ns': 'http://example.com/ns'}) - 若配置文件声明了
encoding="GBK"但实际内容是 UTF-8,需先用open(file_path, 'rb')读二进制,再交给etree.fromstring(),避免 decode 错误
用 XPath 快速提取多层嵌套配置项(避免 for 循环嵌套)
当配置结构类似 ,逐层 .find('service').find('endpoint').find('host').text 易出 None 异常且难维护。
推荐写法:
- 用
//service[@name='db']/endpoint/host/text()一行取值,返回 list,空则为 [] - 用
//service[./endpoint/port = '5432']/@name反向查属性,适合条件过滤 - 对重复结构(如多个
),用root.xpath('//param') + [p.get('key') for p in params]比递归遍历快 3–5 倍 - 注意:XPath 中
text()返回字符串,.返回元素对象,别混用导致类型错误
修改并保存配置时如何保留原始缩进与注释
lxml 默认序列化会抹掉所有空白和注释,这对需要人工审阅的配置文件是灾难。关键不是“能不能”,而是“怎么开开关”。
必须设置:
- 加载时加
parser = etree.XMLParser(strip_cdata=False, remove_blank_text=False) - 保存前调用
etree.indent(tree, space=' ')(lxml ≥ 4.5.0 才支持) - 写入时用
tree.write(file_path, encoding='utf-8', xml_declaration=True, pretty_print=True) - 若需保留注释,不能用
find()后直接赋值——要改用getparent().replace(old_elem, new_elem),否则注释节点会被丢弃
最易被忽略的一点:如果你在解析后手动新增了元素,但没显式设置其 tail 属性(即元素后的空白文本),pretty_print=True 也无法恢复原有换行位置——此时得用 etree.tostring() 手动拼接或依赖 lxml.html 的美化逻辑。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











