本章深入讲解 ElementTree 的高级特性,包括 XPath 查询、命名空间处理。
ElementTree 支持有限的 XPath 语法(1.0 子集),但是足以应对大多数查询需求。
下面介绍三种基础选择方式:
(1)直接子元素选择,仅一级子节点,语法为“父节点.find('子标签名')”或“父节点.findall()”。注意,直接子元素选择,只匹配直属一级子元素,不会递归深层后代。
(2)后代选择,选择所有层级子孙,XPath 前缀“.//”代表当前节点下任意深度所有后代。例如:self.root.find(f".//product[@sku='{sku}']") 表示从 root 开始,遍历所有层级,只要标签是 product、带对应 sku 属性都会被找到,不管嵌套多少层。
(3)当前元素,XPath 中“.”指代自身节点,当前遍历到的元素对象。单独“.”代表元素自己,结合“.//xxx”以当前元素为起点检索后代。
示例代码:
import xml.etree.ElementTree as ET
xml_data = '''
<company>
<department name="研发部">
<team name="后端组">
<employee level="senior">张三</employee>
<employee level="junior">李四</employee>
</team>
<team name="前端组">
<employee level="senior">王五</employee>
</team>
</department>
<department name="市场部">
<employee level="manager">赵六</employee>
</department>
</company>
'''
root = ET.fromstring(xml_data)
# 1. 子元素选择(直接子级)
# 所有 department 的直接子元素 team
teams = root.findall('./department/team')
print(f"共有 {len(teams)} 个团队")
# 2. 后代选择(所有层级)
# 所有 employee,无论在哪一层
all_employees = root.findall('.//employee')
print(f"共有 {len(all_employees)} 名员工")
# 3. 当前元素
# . 表示当前元素
first_dept = root.find('department')
employees_in_dept = first_dept.findall('.//employee')
print(f"第一个部门共有 {len(employees_in_dept)} 名员工")运行结果:
共有 2 个团队
共有 4 名员工
第一个部门共有 3 名员工ElementTree 内置的 XPath 子集支持属性筛选语法,用于根据标签属性精准匹配节点,搭配 find() / findall() / iterfind() 使用。
属性筛选统一包裹在方括号 [] 内,语法如下:
//标签[@属性名]
//标签[@属性名='目标值']详细说明:
@ 固定前缀,代表属性。
字符串值必须用单引号 '值',双引号部分版本兼容差,优先单引号。
仅支持等值匹配、存在判断,不支持模糊匹配(contains 等函数 ET 不支持)。
示例代码:
import xml.etree.ElementTree as ET
xml_data = '''
<company>
<department name="研发部">
<team name="后端组">
<employee level="senior">张三</employee>
<employee level="junior">李四</employee>
</team>
<team name="前端组">
<employee level="senior">王五</employee>
</team>
</department>
<department name="市场部">
<employee level="manager">赵六</employee>
</department>
</company>
'''
root = ET.fromstring(xml_data)
# [@attr] 选择有该属性的元素
# 所有有 name 属性的元素
named_elements = root.findall('.//*[@name]')
print("所有有 name 属性的元素:")
for elem in named_elements:
print(f" {elem.tag}: {elem.attrib['name']}")
# [@attr='value'] 选择属性值匹配的元素
# name 属性为"研发部"的 department
rd_dept = root.find('.//department[@name="研发部"]')
print(f"研发部: {rd_dept}")
# 查找特定团队的员工
backend_employees = root.findall('.//team[@name="后端组"]/employee')
for emp in backend_employees:
print(f"后端组成员: {emp.text}")运行结果:
所有有 name 属性的元素:
department: 研发部
team: 后端组
team: 前端组
department: 市场部
研发部: <Element 'department' at 0x00000217A8918720>
后端组成员: 张三
后端组成员: 李四谓词就是方括号 [],位置谓词是在标签后用数字下标筛选第 N 个匹配节点,属于 ElementTree 支持的 XPath 基础语法。语法如下:
标签[位置数字]注意:XPath 下标从 1 开始,不是 0,和 Python 列表相反。
示例代码:
import xml.etree.ElementTree as ET
xml_data = '''
<scores>
<student>小明</student>
<student>小红</student>
<student>小刚</student>
<student>小丽</student>
</scores>
'''
root = ET.fromstring(xml_data)
# [n] 选择第 n 个(从1开始计数)
first = root.find('student[1]')
print(first.text) # 小明
second = root.find('student[2]')
print(second.text) # 小红
# [last()] 选择最后一个
last = root.find('student[last()]')
print(last.text) # 小丽我们可以将前面的选择器进行组合,构建出更复杂的筛选器,例如:
import xml.etree.ElementTree as ET
xml_data = '''
<products>
<product category="electronics" status="active">
<name>手机</name>
<price>2999</price>
</product>
<product category="electronics" status="discontinued">
<name>旧款平板</name>
<price>999</price>
</product>
<product category="clothing" status="active">
<name>T恤</name>
<price>99</price>
</product>
</products>
'''
root = ET.fromstring(xml_data)
# 同时满足多个条件
active_electronics = root.findall(
'.//product[@category="electronics"][@status="active"]'
)
print(f"在售电子产品: {len(active_electronics)} 个")
# 查找价格大于1000的产品(需要遍历筛选)
expensive = [
p for p in root.findall('product') if int(p.find('price').text) > 1000
]
print(f"高价产品: {[p.find('name').text for p in expensive]}")运行结果:
在售电子产品: 1 个
高价产品: ['手机']关键代码详细说明:
# 推导式语法
# [表达式 for 变量 in 可迭代对象 if 条件]
# 表达式:每次循环最终放进列表的值
# for 变量 in 可迭代对象:循环主体
# if 条件(可选):过滤,只保留满足条件的元素
p for p in root.findall('product') if int(p.find('price').text) > 1000其中:
root.findall('product') 查询根节点直接子节点中所有 <product> 标签,返回 list[Element]。若要匹配任意层级 product,需写成 .//product。
for p in root.findall('product') 循环遍历每一个 <product> 节点,每个节点临时变量命名为 p
p.find('price') 在当前商品节点 p 内部,查找它的直接子节点 <price>
.text 取出 <price> 标签内的文本内容(XML 文本都是字符串,例如 "1299"、"899")
int(...) 把价格字符串转为数字,才能进行数值大小比较。如果不加 int 直接字符串对比会出错(如 "900" > "1000" 字符串比较结果为 True)。
if int(p.find('price').text) > 1000 只保留价格数值大于 1000 的 product 节点,不满足条件的节点直接丢弃,不会出现在最终列表。
最开头的 p,满足 if 条件时,把当前 product 节点 p 存入最终列表
XML 命名空间用于避免元素名称冲突,格式为 xmlns:前缀="URI"。例如:
<?xml version="1.0"?>
<root xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:media="http://search.yahoo.com/mrss/">
<dc:title>文档标题</dc:title>
<media:content url="image.jpg">
<media:title>图片标题</media:title>
</media:content>
</root>上面例子中分别定义两个命名空间,分别为:
xmlns:dc 命名空间,dc:title 表示 title 元素属于 dc 命名空间。
xmlns:media 命名空间,media:title、media:content 表示 title、content 元素属于 media 名称空间。
即使存在两个 title 元素,但是它们属于不同的命名空间,不会造成元素名称冲突。在实际工作中,这可以解决多个团队、多个企业交换数据时元素名称冲突。
下面通过实例演示,如何通过 ElementTree 来处理代理命名空间的 XML。例如:
import xml.etree.ElementTree as ET
# 默认命名空间是 http://www.w3.org/2005/Atom
xml_data = '''
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:media="http://search.yahoo.com/mrss/">
<title>我的博客</title>
<entry>
<title>文章标题</title>
<media:thumbnail url="thumb.jpg"/>
</entry>
</feed>
'''
# 解析XML内容
root = ET.fromstring(xml_data)
# 使用完整 URI 作为命名空间字典
namespaces = {
'atom': 'http://www.w3.org/2005/Atom',
'media': 'http://search.yahoo.com/mrss/'
}
# 带前缀的查询,atom 指向的是默认命名空间
# atom:title 等同于 title 标签
title = root.find('atom:title', namespaces)
print(title.text) # 我的博客
# 查找 media:thumbnail,即查询 http://search.yahoo.com/mrss/ 命名空间下面的 thumbnail 标签
thumbnail = root.find('.//media:thumbnail', namespaces)
print(thumbnail.get('url')) # thumb.jpg注意:在 XML 中,不带前缀的 <title> 属于默认命名空间 http://www.w3.org/2005/Atom,带 media: 前缀的才属于 http://search.yahoo.com/mrss/。
该方法用于给 XML 命名空间绑定自定义短前缀,但只影响输出序列化(tostring/write),不影响 find/findall 查询。例如:
import xml.etree.ElementTree as ET
# 默认命名空间是 http://www.w3.org/2005/Atom
xml_data = '''
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:media="http://search.yahoo.com/mrss/">
<title>我的博客</title>
<entry>
<title>文章标题</title>
<media:thumbnail url="thumb.jpg"/>
</entry>
</feed>
'''
# 注册命名空间前缀
ET.register_namespace('atom', 'http://www.w3.org/2005/Atom')
ET.register_namespace('media', 'http://search.yahoo.com/mrss/')
# 现在保存时会使用这些前缀
# {命名空间完整URI}元素本地名
# {http://www.w3.org/2005/Atom}feed
# └───────────────────────────┘└──┘
# 命名空间URI 标签名feed
root = ET.Element('{http://www.w3.org/2005/Atom}feed')
# 添加子元素
ET.SubElement(root, "dog").text = "大黄"
ET.SubElement(root, "cat").text = "小花"
# 格式化缩进,2空格缩进
ET.indent(root, space=" ")
# 输出到文件
tree = ET.ElementTree(root)
tree.write('output.xml', xml_declaration=True, encoding='UTF-8')运行实例,output.xml 文件的内容如下:
<?xml version='1.0' encoding='UTF-8'?>
<atom:feed xmlns:atom="http://www.w3.org/2005/Atom">
<dog>大黄</dog>
<cat>小花</cat>
</atom:feed>如果去掉示例中的 ET.register_namespace() 方法,会自动添加命名前缀,如下:
<?xml version='1.0' encoding='UTF-8'?>
<ns0:feed xmlns:ns0="http://www.w3.org/2005/Atom">
<dog>大黄</dog>
<cat>小花</cat>
</ns0:feed>ElementTree 默认加载整个文档到内存,该种方式对于小的 XML 文件还是非常方便,但是对于大文件就会存在问题,可能会导致内存耗尽。对于 XML 大文件,推荐使用 iterparse 进行流式处理,读取一个元素处理一个元素,然后释放内存,避免内存消耗过大。
方法语法如下:
ET.iterparse(source, events=("end",), parser=None)参数说明:
source:文件对象 / 文件路径
events:监听事件,常用 2 种:
start:读到标签开始<tag>时触发
end:读到完整闭合</tag>时触发(最常用)
简单示例:假如存在一个 input.xml 文件,文件的内容如下(通常是一个非常大的 XML 文件,如 1GB 大小,甚至更大):
<recodes>
<record id="1">
<serialNo>001</serialNo>
<userName>张三</userName>
<phone>13800138001</phone>
<registerTime>2026-01-05</registerTime>
<orderAmount>128.50</orderAmount>
<orderStatus>已完成</orderStatus>
<address>成都市武侯区XX街道1号</address>
</record>
<record id="2">
<serialNo>002</serialNo>
<userName>李四</userName>
<phone>13800138002</phone>
<registerTime>2026-01-08</registerTime>
<orderAmount>369.00</orderAmount>
<orderStatus>待发货</orderStatus>
<address>成都市锦江区XX街道2号</address>
</record>
<record id="3">
<serialNo>003</serialNo>
<userName>王五</userName>
<phone>13800138003</phone>
<registerTime>2026-01-10</registerTime>
<orderAmount>89.90</orderAmount>
<orderStatus>已退款</orderStatus>
<address>成都市成华区XX街道3号</address>
</record>
<record id="4">
<serialNo>004</serialNo>
<userName>赵六</userName>
<phone>13800138004</phone>
<registerTime>2026-01-12</registerTime>
<orderAmount>520.00</orderAmount>
<orderStatus>运输中</orderStatus>
<address>成都市青羊区XX街道4号</address>
</record>
<record id="5">
<serialNo>005</serialNo>
<userName>孙七</userName>
<phone>13800138005</phone>
<registerTime>2026-01-15</registerTime>
<orderAmount>76.30</orderAmount>
<orderStatus>已完成</orderStatus>
<address>成都市金牛区XX街道5号</address>
</record>
</recodes>写一段 Python 代码来解析该文件,并且统计记录总数和总计金额,例如:
import xml.etree.ElementTree as ET
def process_large_xml(filename):
"""流式处理大型 XML"""
count = 0
total_amount = 0.0
# iterparse 产生 (event, element) 对
# events=['end'] 表示元素结束标签时触发
for event, elem in ET.iterparse(filename, events=['end']):
# 如果标签元素是 record 才进行业务处理
if elem.tag == 'record':
# 查找金额信息,然后累加
amount = float(elem.find('orderAmount').text)
total_amount += amount
count += 1
# 关键:处理完后清空元素释放内存
elem.clear()
# 同时清空父元素的引用
parent = elem.getparent() if hasattr(elem, 'getparent') else None
if parent is not None:
parent.remove(elem)
return count, total_amount
if __name__ == '__main__':
count, total_amount = process_large_xml("input.xml")
print(f"count={count}, total_amount={total_amount}")运行示例,输出如下:
count=5, total_amount=1183.7输入文件依然使用前面的 input.xml,我们通过 ET.iterparse() 将每个记录下面的 phone 筛选出来,并且放入到一个新的 XML 文件中,例如:
import xml.etree.ElementTree as ET
def extract_items_by_category(input_file, output_file, target_category):
""" 从大型 XML 中提取特定类别的项目 """
# 创建新树的根
new_root = ET.Element('extracted_items')
for event, elem in ET.iterparse(input_file, events=['end']):
if elem.tag == 'record':
phone = elem.find('phone')
if phone is not None:
# 深拷贝元素到新树
new_item = ET.SubElement(new_root, 'phone')
new_item.text = phone.text;
# 清理
elem.clear()
# 格式化缩进,2空格缩进
ET.indent(new_root, space=" ")
# 保存结果
tree = ET.ElementTree(new_root)
tree.write(output_file, encoding='utf-8', xml_declaration=True)
if __name__ == '__main__':
extract_items_by_category("input.xml", "output.xml", "phone")运行示例,output.xml 文件的内容如下:
<?xml version='1.0' encoding='utf-8'?>
<extracted_items>
<phone>13800138001</phone>
<phone>13800138002</phone>
<phone>13800138003</phone>
<phone>13800138004</phone>
<phone>13800138005</phone>
</extracted_items>依然使用上面的 input.xml 文件,我们可以使用 ET.iterparse() 迭代统计 XML 文档中每个标签的个数,代码如下:
import xml.etree.ElementTree as ET
def count_elements_efficiently(filename):
"""高效统计 XML 元素数量"""
tag_counts = {}
for event, elem in ET.iterparse(filename, events=['start', 'end']):
# 记录开始处理的元素
if event == 'start':
tag = elem.tag
tag_counts[tag] = tag_counts.get(tag, 0) + 1
# 立即清理结束处理的元素
if event == 'end':
elem.clear()
return tag_counts
if __name__ == '__main__':
tag_counts = count_elements_efficiently("input.xml")
print(f"Tag counts: {tag_counts}")运行示例,输出如下:
Tag counts: {'recodes': 1, 'record': 5, 'serialNo': 5, 'userName': 5, 'phone': 5,
'registerTime': 5, 'orderAmount': 5, 'orderStatus': 5, 'address': 5}使用 ElementTree 库封装一个 XMLConfig 类,该类用来进行 XML 配置文件管理。该类提供了获取配置 get、设置配置 set、和 save 方法。代码如下:
import xml.etree.ElementTree as ET
from typing import Dict, Any
class XMLConfig:
"""基于 XML 的配置文件管理器"""
def __init__(self, filepath: str):
self.filepath = filepath
try:
self.tree = ET.parse(filepath)
self.root = self.tree.getroot()
except FileNotFoundError:
self.root = ET.Element('configuration')
self.tree = ET.ElementTree(self.root)
def get(self, key: str, default: Any = None) -> Any:
"""获取配置项,支持点号路径如 'database.host' """
parts = key.split('.')
current = self.root
for part in parts:
child = current.find(part)
if child is None:
return default
current = child
# 如果还有子元素,返回字典
if len(current) > 0:
return self._element_to_dict(current)
return current.text if current.text else default
def set(self, key: str, value: Any):
"""设置配置项"""
parts = key.split('.')
current = self.root
for part in parts[:-1]:
child = current.find(part)
if child is None:
child = ET.SubElement(current, part)
current = child
# 设置最终值
leaf_tag = parts[-1]
leaf = current.find(leaf_tag)
if leaf is None:
leaf = ET.SubElement(current, leaf_tag)
if isinstance(value, dict):
leaf.text = None
for k, v in value.items():
sub = leaf.find(k)
if sub is None:
sub = ET.SubElement(leaf, k)
sub.text = str(v)
else:
leaf.text = str(value)
def get_section(self, section: str) -> Dict[str, str]:
"""获取整个配置节"""
sec_elem = self.root.find(section)
if sec_elem is None:
return {}
return self._element_to_dict(sec_elem)
def _element_to_dict(self, element: ET.Element) -> Dict[str, Any]:
"""将元素转换为字典"""
result = {}
for child in element:
if len(child) > 0:
result[child.tag] = self._element_to_dict(child)
else:
result[child.tag] = child.text
return result
def save(self):
"""保存配置到文件"""
ET.indent(self.root, space=' ')
self.tree.write(self.filepath, encoding='utf-8', xml_declaration=True)
# 使用示例
if __name__ == '__main__':
# 创建配置
config = XMLConfig("config.xml")
config.set('app.name', '我的应用')
config.set('app.version', '1.0.0')
config.set('database.host', 'localhost')
config.set('database.port', '3306')
config.set('database.credentials', {'user': 'admin', 'password': 'secret'})
config.save()
# 读取配置
config2 = XMLConfig("config.xml")
print(f"应用名称: {config2.get('app.name')}")
print(f"数据库主机: {config2.get('database.host')}")
print(f"数据库配置: {config2.get_section('database')}")运行代码,输出如下:
应用名称: 我的应用
数据库主机: localhost
数据库配置: {'host': 'localhost', 'port': '3306', 'credentials': {'user': 'admin', 'password': 'secret'}}如果你有需要,可以自己修改上面的 XMLConfig 类,让他符合自己的需求。