xml作为一种可扩展标记语言,至今仍广泛应用于配置文件、接口报文、数据存储等领域。Python标准库中的xml.etree.ElementTree模块(以下简称ElementTree)提供了轻量且高效的API,不需要安装任何第三方库就可以完成xml的解析、查找、修改与生成。这篇文章用实际代码演示ElementTree的完整用法,读完即可独立处理常见的xml数据任务。

一、先搞清楚xml的树形结构模型
在动手写代码之前,需要先理解ElementTree看待xml文件的方式。一个xml文件会被解析成一棵由Element节点组成的树,根节点只有一个,下面可以挂任意层级的子节点。以下面这个简单的配置文件为例:
<?xml version="1.0" encoding="utf-8"?>
<config>
<database host="127.0.0.1" port="3306">
<username>root</username>
<password>123456</password>
</database>
<debug>true</debug>
</config>这段xml中,<config>是根节点,<database>是它的子节点,并且携带了host和port两个属性。<username>这类叶子节点包含文本内容。ElementTree把每一个节点抽象成Element对象,它有四个最常用的属性:tag表示标签名,attrib是以字典形式存放的属性,text是标签之间的文本,tail是标签结束之后的文本。理解这四个属性,后面所有的查找和修改操作都会变得非常直观。
还要注意一点,属性和子节点是两种不同的数据组织方式。像host和port这种成对出现的键值,写成属性更紧凑;而username这种可能包含结构化内容的,写成子节点更灵活。读取时两者使用的API完全不同,属性通过attrib或get()方法访问,子节点则通过遍历或查找方法获取。
二、解析xml文件的几种方式
ElementTree提供了两种最基础的解析入口:一是ET.parse(),接收文件路径,返回ElementTree对象;二是ET.fromstring(),接收字符串,直接返回根Element对象。前者适合处理磁盘上的文件,后者常用于处理网络接口返回的xml报文。
import xml.etree.ElementTree as ET
# 方式一:从文件解析
tree = ET.parse('config.xml')
root = tree.getroot()
# 方式二:从字符串解析
xml_str = '<config><debug>true</debug></config>'
root2 = ET.fromstring(xml_str)
print(root.tag) # 输出: config
print(root.attrib) # 输出: {}拿到根节点后,ET.parse()返回的tree对象还负责后续写回文件的操作,而fromstring()返回的只是Element,如果要保存,需要先用ET.ElementTree(root)包装一层。这是初学者经常混淆的地方,建议根据数据来源选择:文件操作走parse,字符串操作走fromstring。
对于特别大的xml文件(比如几百MB的日志导出),一次性加载整棵树会占用大量内存,这时可以改用ET.iterparse()进行增量解析。它按顺序逐个事件地处理节点,处理完的节点可以调用elem.clear()释放内存,实测可以把内存占用控制在很低的水平。不过日常处理中小型配置文件,直接用parse就够了。
三、遍历与查找节点的方法
最直接的遍历方式是for循环,配合iter()方法可以递归遍历所有后代节点。看下面的例子:
import xml.etree.ElementTree as ET
tree = ET.parse('config.xml')
root = tree.getroot()
# 只遍历直接子节点
for child in root:
print(child.tag, child.attrib)
# 递归遍历所有层级,找出全部username节点
for elem in root.iter('username'):
print(elem.tag, elem.text)如果目标明确,用find()和findall()配合xpath风格的路径表达式效率更高。find()返回第一个匹配的元素,找不到返回None;findall()返回所有匹配元素组成的列表。路径表达式中,直接写标签名表示查找直接子节点,前面加.//表示在整个子树中递归查找。
import xml.etree.ElementTree as ET
root = ET.parse('config.xml').getroot()
# 查找直接子节点
db = root.find('database')
print(db.get('host')) # 输出: 127.0.0.1
print(db.get('port', '3306')) # 第二个参数是默认值
# 递归查找所有username
users = root.findall('.//username')
for u in users:
print(u.text)ElementTree还支持有限的xpath语法,例如[@host='127.0.0.1']可以按属性筛选:root.findall(".//database[@host='127.0.0.1']")。需要完整xpath功能的话,可以安装第三方库lxml,它的API与ElementTree高度兼容,切换成本很低。
四、修改节点内容与属性
修改操作同样简单。改文本就是给text赋值,改属性用set(),删除属性用pop()操作attrib字典即可。
import xml.etree.ElementTree as ET
tree = ET.parse('config.xml')
root = tree.getroot()
# 修改文本
db = root.find('database')
db.find('password').text = 'newpass123'
# 修改和删除属性
db.set('port', '3307')
db.attrib.pop('host')
tree.write('config.xml', encoding='utf-8', xml_declaration=True)动态构建新的xml结构时,用ET.SubElement()在指定父节点下创建子元素,用remove()删除子元素。最后调用tree的write()方法保存,参数xml_declaration=True会在文件头部写入xml声明,encoding='utf-8'确保中文不乱码,这两个参数建议始终带上。
import xml.etree.ElementTree as ET
root = ET.Element('students')
s1 = ET.SubElement(root, 'student', {'id': '1001'})
ET.SubElement(s1, 'name').text = '张三'
ET.SubElement(s1, 'score').text = '92'
# 删除score节点
s1.remove(s1.find('score'))
ET.ElementTree(root).write('students.xml',
encoding='utf-8',
xml_declaration=True)有一个体验上的小缺陷要提一下:ElementTree的write方法默认不做缩进,整个文件会挤在一行。Python 3.9之后可以直接调用ET.indent(tree, space=' ')美化格式,老版本则建议用xml.dom.minidom重新格式化一遍再输出。
五、常见踩坑点与注意事项
第一是编码问题。如果xml文件声明是utf-8但实际保存成了gbk,解析时会直接抛出解析异常,遇到这种情况先确认文件真实编码。第二是命名空间,带xmlns的xml在查找时必须把命名空间前缀写进路径,例如root.findall('{http://ippipp.com/ns}item'),直接写item是找不到的,这是处理第三方接口报文时最高频的坑。
第三是安全方面,解析不受信任的xml数据时要警惕xml外部实体注入(XXE)攻击。虽然标准库的ElementTree默认不解析外部实体,相对安全,但换用lxml时务必关闭resolve_entities相关选项。第四,find()返回None时不做判断就访问属性会触发AttributeError,建议养成先判空再取值的习惯。
总结一下,ElementTree覆盖了xml处理的绝大多数场景:parse和fromstring负责读取,find与findall负责定位,text、set、SubElement负责修改和构建,write负责落盘。把这些API组合起来,配置文件读写、接口报文解析这类任务都能轻松搞定。只有在需要完整xpath或高性能处理超大文件时,才需要考虑lxml这样的第三方方案。
Python xml解析ElementTreexml数据读写修改时间:2026-09-03 15:35:05