XML作为一种结构化数据表示方式,常用于配置文件、接口报文和数据交换。读取所有节点不是简单地把整棵树打印出来,而是要理解元素、属性、文本和尾部文本之间的关系。解析XML通常有两种思路:一种是把整棵文档树加载进内存,另一种是基于事件流逐段处理。前者使用简单,适合中小文件,后者内存占用低,适合大型日志或数据集。

围绕读取、遍历、修改和写回这几个环节,下面整理一些常用代码和容易忽略的细节。
一、根据文件规模选择合适的解析方式
如果XML文件只有几百KB到几MB,使用xml.etree.ElementTree的parse或fromstring是最直接的方式。它会构建一棵完整的元素树,根节点通过getroot获得。这种方式的优势是节点关系清晰,可以反复遍历同一份数据,也能方便地用findall和find定位子节点。下面是一个最基础的结构展示。
import xml.etree.ElementTree as ET
tree = ET.parse('config.xml')
root = tree.getroot()
print(root.tag)
当文件达到几十MB甚至上百MB时,仍用parse会占用大量内存,因为整棵树的节点会同时存在。此时推荐使用iterparse。它按事件返回节点,可以在处理完某个节点后调用clear释放内存。iterparse适合只需要提取部分字段或逐条处理记录的场景。示例代码按end事件读取record节点,处理完立刻清理。
for event, elem in ET.iterparse('large.xml', events=('end',)):
if elem.tag == 'record':
process(elem)
elem.clear()
需要说明的是,ElementTree内置的XPath支持有限,如果后续要做复杂条件定位,可以直接使用lxml,它的API与ElementTree高度兼容,并且xpath方法支持更多语法。中小项目从ElementTree起步,遇到性能或XPath瓶颈再迁移到lxml,是比较稳妥的路径。
二、遍历所有节点并提取标签、属性与文本
要读取所有节点,最常见的做法是调用root.iter()。这个生成器会递归返回当前节点及其所有后代节点,包含根节点本身。每个元素对象都有tag、attrib、text和tail四个属性:tag是标签名,attrib是属性字典,text是开始标签后紧跟的文本,tail是该元素结束标签之后到下一个元素之前的文本。很多人只读取text,导致元素之间的文本内容丢失。
for elem in root.iter():
tag = elem.tag
attrs = elem.attrib
text = elem.text
tail = elem.tail
print(tag, attrs, text, tail)
如果只想查看某个标签下的所有同名节点,可以给iter传入标签名。比如root.iter('book')只会返回所有book元素。下面这段代码会提取所有book的id属性和title文本,适合快速汇总节点数据。
for book in root.iter('book'):
book_id = book.get('id')
title = book.findtext('title')
print(book_id, title)
提取文本时要注意,很多格式化过的XML会包含换行和缩进,这些也属于节点的text或tail。例如<title>XML入门</title>中间没有空白,text就是XML入门;但如果标签和文本之间插入了换行,text会包含换行和空格。因此判断有效文本时最好先strip再判断,避免把空白当成数据。
for elem in root.iter():
if elem.text and elem.text.strip():
print(elem.tag, '=', elem.text.strip())
三、修改节点与写回时容易踩坑
修改XML节点通常分为改文本、改属性和改结构三类。改文本直接给elem.text赋值,改属性使用set方法。例如要把所有price乘以1.2,并更新某本书的id,可以这样写。重点在于修改完成后必须调用tree.write,否则内存中的树虽然变了,文件并不会更新。
for book in root.findall('book'):
price = book.find('price')
if price is not None:
price.text = str(round(float(price.text) * 1.2, 2))
if book.get('id') == 'bk101':
book.set('id', 'bk101-new')
tree.write('books_updated.xml', encoding='UTF-8', xml_declaration=True)
如果XML内容来自字符串而不是文件,使用ET.fromstring会返回根元素Element,而不是ElementTree对象。Element没有write方法,此时应该用ET.tostring转换回字符串。很多人在这里报AttributeError,原因就是对象类型没分清。下面演示字符串方式修改节点后输出新XML。
root = ET.fromstring(xml_string)
for elem in root.iter('status'):
elem.text = 'active'
new_xml = ET.tostring(root, encoding='unicode')
print(new_xml)
另一个常见问题是查找路径。findall('book')只能匹配直接子节点,如果book不在根节点下一层,而是嵌套在items下面,这个调用会返回空列表。我们第一个代码示例中root.findall('book')能生效,是因为book确实是root的直接子节点。对于嵌套结构,建议统一使用.//book这种后代查找写法,避免因为层级变化导致代码失效。
四、用XPath定位节点并完成批量更新
ElementTree的findall和find支持一部分XPath表达式。查找所有后代book节点可以写成.//book,按属性过滤可以写成.//book[@id='bk102']。下面的代码演示如何定位特定id的book并输出作者。
for book in root.findall(".//book[@id='bk102']"):
author = book.findtext('author')
print(author)
但ElementTree内置XPath不支持按文本内容过滤、contains函数、轴选择等复杂语法。如果业务中经常需要根据文本查找节点,建议切换到lxml。lxml的xpath方法兼容标准XPath 1.0,可以写contains和text(),代码几乎不用改动。
from lxml import etree
tree = etree.parse('books.xml')
for book in tree.xpath("//book[contains(title, 'Python')]"):
print(book.findtext('author'))
使用XPath时还要注意命名空间。如果XML带有默认命名空间,例如<root xmlns="http://ipipp.com">,直接写root.findall('book')或者.//book都会失败。ElementTree会要求你把命名空间写进路径,或者读取前先去除命名空间。lxml同样需要在xpath中传入namespaces字典。这个问题在实际接接口数据时出现频率很高,排查时可以先打印root.tag,确认tag中是否带有类似{http://ipipp.com}的前缀。
整体来看,读写XML所有节点的关键不在“所有”两个字,而在于根据文件规模和结构选择合适的方法。ElementTree适合大多数中小型XML的遍历、修改和写回,lxml适合复杂XPath,iterparse适合大文件。实际项目中,建议把读取节点和提取字段的代码封装成独立函数,返回列表或字典,避免在业务逻辑里混入大量树操作。只要注意命名空间、文本空白、findall层级和写回对象类型这几个细节,XML节点的读写可以被压缩成一套稳定可复用的模板。
XML解析ElementTree节点遍历修改时间:2026-10-05 08:00:19