python xml.etree
# 从Python XML解析到真实世界的映射聊聊xml.etree的那些事最近在帮一个朋友处理他公司的数据交换项目看到他把XML文件当作JSON来解析踩了一堆坑让我决定写写这个被很多人忽视的库。xml.etree是Python标准库里处理XML的模块它不像lxml那样功能齐全也不如minidom那般笨重但在大多数场景下它恰恰是最顺手的那把刀。它到底是什么样的存在xml.etree本质上把XML文档映射成了一棵内存里的树。这个想法其实很朴素就像我们整理照片时按“年-月-日”建文件夹每个文件夹里放着对应的照片。XML里的标签就是文件夹标签里的文本和属性就是照片。但这种映射有一个有趣的地方在真正的文件系统里一个照片只能在一个文件夹里但XML允许一个节点被多个节点引用——不过那是XPath的事etree默认不玩这个。早些年做接口测试的时候遇到过一种奇葩的XML结构服务端把数据藏在注释里。etree的ElementTree类有个_comment的私有方法可以访问注释节点但官方文档只字不提。这种小技巧只有在反复踩坑后才能发现。它能做什么不能做什么日常工作中etree最适合处理三层以内的配置文件和中等规模的(几百MB以内)数据交换。比如公司内部的服务配置文件、简单的报表模板、甚至可以用来保存一些中间计算结果。但要是非要用它处理GB级别的XML数据那就有点不理智了。记得有次试图用etree解析一个2GB的基因组注释文件结果内存直接爆炸。后来换成iterparse配合生成器逐行处理才算把问题解决。这就是它和SAX模式的区别etree默认是一次性加载虽然iterparse提供了流式处理的能力但处理大型文件时还是不如专门的流式解析器来得顺手。有个不太为人知的点是etree对命名空间的处理相当别扭。比如{http://example.com}root这种形式读起来很费劲。这时候有人会建议用lxml但如果你只能使用标准库不妨试试在解析前自己预处理一下命名空间映射。怎么用得顺手写代码最怕的就是为了简洁而牺牲可读性。etree的API设计其实挺讲究但很多人用着用着就走偏了。最常见的场景是把XML转为字典。我见过有人这样写defxml_to_dict(element):result{}forchildinelement:iflen(child)0:result[child.tag]child.textelse:result[child.tag]xml_to_dict(child)returnresult这样写看起来很妙但遇到重复的子标签就傻眼了。比如一个订单里有多个商品上面的代码只会保留最后一个。更稳妥的做法是遇到同名的子标签就自动收集成列表但这样又会破坏键值对的简单映射关系。折中的方案是在创建Element对象时就规划好数据结构。比如fromxml.etree.ElementTreeimportElement,SubElement,tostring orderElement(order)order_idSubElement(order,id)order_id.text001# 如果有多个商品可以预判用列表存储foritemin[apple,banana]:productSubElement(order,product)nameSubElement(product,name)name.textitem这种方式看似啰嗦但在后期维护时能省下大把时间。毕竟XML的容错性比JSON强太多——它允许你写非法的字符只是解析时会报错而已。一些实践中的小技巧处理XML时最头疼的是那些混在文本里的特殊字符。比如符号在XML里必须写成amp;。etree在生成XML时会自动帮你做转义但如果你从数据库直接取数据拼接字符串那就相当危险了。另一个容易被忽略的是编码问题。XML文件的头部通常会声明编码比如?xml version1.0 encodingUTF-8?。但如果你用文本编辑器改过文件编码声明可能和实际内容不一致。etree在解析时会根据声明自动处理编码但如果遇到声明和实际不符的情况它会报错。一个取巧的办法是先以二进制模式读取文件然后用fromstring方法解析。说到性能问题每次写XML前的缩进是个挺烦人的事。etree生成的XML默认是紧凑型的所有内容挤在一行。如果想让人眼看得舒服可以自己写个漂亮的格式化函数defprettify(elem,level0):indent\nlevel* iflen(elem):ifnotelem.textornotelem.text.strip():elem.textindent ifnotelem.tailornotelem.tail.strip():elem.tailindentforchildinelem:prettify(child,level1)ifnotchild.tailornotchild.tail.strip():child.tailindentelse:ifleveland(notelem.tailornotelem.tail.strip()):elem.tailindent这个函数会在每个层级前插入两个空格的缩进读起来舒服很多。和其他同类工具的比较在Python生态里解析XML的主流选择无非是xml.etree、minidom和lxml这三家。minidom最大的问题是它保留了DOM的全部细节包括注释、处理指令等。如果你只是想拿到标签里的内容它反而会因为信息太多而显得臃肿。lxml是个强劲的对手它基于C库实现速度是etree的好几倍。特别是它的XPath和XSLT支持对于复杂的文档处理是绝对的利器。但它的缺点也很明显需要额外安装而且如果只用到基本功能完全是杀鸡用牛刀。反观etree它最大的优势就是“够用且不用装”。对于大多数日常任务它的性能完全够用。记得有次需要在CI环境里处理一个配置文件那个环境连pip都用不了etree就这样自然而然派上了用场。还有一个很少被提及的点etree的错误信息比lxml友好得多。遇到格式错误的XMLetree会明确告诉你“第几行第几列出了问题”而lxml有时只会给出一个笼统的“解析错误”就完事了。写到最后想起一个有意思的事。有一次帮一个朋友调试代码他把JSON文件误存成XML格式然后用etree去解析结果自然是一堆乱码。但有趣的是etree居然还能识别出部分结构只是标签名变成了乱码字符。这个细节说明设计者在容错方面确实下过功夫只是这种容错反而可能掩盖了真正的 bug。所以在用etree的时候最好在解析完后做个基本的结构验证确认标签名和预期一致。