前面介绍了 XML、以及相关的 DTD、XML Schema
ElementTree 是 Python 标准库处理 XML 的核心工具。本章介绍其基础用法,包括解析、遍历、查询和简单修改。
注意点:对恶意 XML 数据存在安全风险,不建议直接解析不可信来源的 XML。
ElementTree 可以通过两种方式解析 XML 文档,分别为通过 XML 文件、XML 字符串。
假设当前目录下面存在 sample.xml 文件,内容如下:
<?xml version="1.0"?>
<root>
<item>内容</item>
</root>可以直接使用 ET.parse() 进行解析,参数传递文件名即可。如下:
import xml.etree.ElementTree as ET
# 解析XML文件,返回 ElementTree 对象
tree = ET.parse('sample.xml')
# 获取根元素
root = tree.getroot()
print(f"根元素标签: {root.tag}")ET.parse() 还可以接受文件对象,使用 with open() 打开文件,如下:
import xml.etree.ElementTree as ET
with open('sample.xml', 'r', encoding='utf-8') as f:
tree = ET.parse(f)
root = tree.getroot()
print(f"根元素标签: {root.tag}")运行代码,输出如下:
根元素标签: root如果你的 XML 不是存储在文件中的,而是内存中的一个字符串,或者是通过 HTTP 接口收到的 XML 内容。这就适合使用字符串方式去解析 XML 内容。例如:
import xml.etree.ElementTree as ET
# XML 字符串
xml_string = '''<?xml version="1.0"?>
<root>
<item>内容</item>
</root>'''
# 解析 XML
root = ET.fromstring(xml_string)
print(root.tag) # 输出: root每个 XML 元素都是一个 Element 对象,可以通过 Element 对象的属性来访问 XML 文档的某些数据。例如:
tag 访问 XML 元素标签名。
attrib 访问 XML 元素的属性字典。
text 访问 XML 元素的直接文本内容,注意:不包括子元素。
get() 该方法用来获取 XML 元素的单个属性,支持返回默认值。
简单示例:
import xml.etree.ElementTree as ET
xml_data = '''
<product sku="ABC-123" status="active">
<name>无线鼠标</name>
<price>59.99</price>
</product>
'''
root = ET.fromstring(xml_data)
# tag: 元素标签名
print(f"元素标签: {root.tag}")
# attrib: 属性字典
print(f"属性字典: {root.attrib}")
# text: 元素的直接文本内容(不包括子元素)
print(f"直接文本内容: {root.text}")
# get() 方法获取单个属性
print(f"SKU: {root.get('sku')}")
print(f"折扣: {root.get('discount', '0')}")运行结果:
元素标签: product
属性字典: {'sku': 'ABC-123', 'status': 'active'}
直接文本内容:
SKU: ABC-123
折扣: 0注意,由于 <product> 下面是子元素,没有直接文本,因此 root.text 返回的是空。
遍历 XML 是程序处理 XML 必须要面对的,例如使用接口获取用户列表信息,返回的是多个用户信息,我们需要通过遍历 XML,才能取出所有用户的信息。
根据实际的业务需要,遍历分为简单遍历和递归遍历。
在部分场景下,我们不需要进行深层次的遍历,仅仅遍历某个元素的所有子元素,例如:遍历用户的联系地址。
import xml.etree.ElementTree as ET
xml_data = '''
<user>
<id>100</id>
<name>张三</name>
<age>30</age>
<address>
<addr>北京市朝阳区</addr>
<addr>上海市浦东新区</addr>
<addr>广州市天河区</addr>
</address>
</user>
'''
root = ET.fromstring(xml_data)
# 遍历所有 addr 标签
addresses = root.findall('.//addr')
for addr in addresses:
print(f"地址: {addr.text}")
# 按索引访问
first_addr = addresses[0]
print(first_addr.text) # 北京市朝阳区运行结果:
地址: 北京市朝阳区
地址: 上海市浦东新区
地址: 广州市天河区
北京市朝阳区上面遍历方式,仅能遍历直接子元素,如果要遍历整个 XML 文档,而且 XML 文档的结构未知。我们需要通过遍历整个 XML 文档,找到符合条件的元素,则可以采用递归遍历的方式。
下面通过遍历整个 XML 文档来演示如何进行递归遍历:
import xml.etree.ElementTree as ET
def traverse(element, level=0):
"""递归遍历 XML 元素并打印结构和文本内容"""
# 根据层级增加缩进,打印当前元素的标签
indent = " " * level
# 打印当前元素的标签
print(f"{indent}<{element.tag}>")
# 如果元素有文本内容,打印文本(去除多余空白)
if element.text and element.text.strip():
print(f"{indent} 文本: {element.text.strip()}")
# 递归遍历子元素
for child in element:
traverse(child, level + 1)
# 打印当前元素的结束标签
print(f"{indent}</{element.tag}>")
# 使用
xml_data = '''
<company>
<department name="技术部">
<employee>
<name>张三</name>
<position>软件工程师</position>
</employee>
<employee>
<name>李四</name>
<position>产品经理</position>
</employee>
</department>
</company>
'''
traverse(ET.fromstring(xml_data))输出:
<company>
<department>
<employee>
<name>
文本: 张三
</name>
<position>
文本: 软件工程师
</position>
</employee>
<employee>
<name>
文本: 李四
</name>
<position>
文本: 产品经理
</position>
</employee>
</department>
</company>ElementTree 提供几种查找方法,匹配规则基于简化版 XPath。
find() 是 XML 节点查找方法,按传入标签名只找第一个匹配的子节点,找不到则返回 None。可以搭配 text 属性获取节点文本,get() 方法获取节点属性。例如:
import xml.etree.ElementTree as ET
xml_data = '''
<menu>
<item type="drink">咖啡</item>
<item type="food">三明治</item>
<item type="drink">茶</item>
</menu>
'''
root = ET.fromstring(xml_data)
# 查找第一个 item
first_item = root.find('item')
# 获取元素文本
print(first_item.text) # 咖啡
# 获取元素的 type 属性
print(first_item.get('type')) # drinkfindall() 方法查找当前节点下所有符合标签路径的子节点,返回节点列表,无匹配则返回空列表 []。
例如:查找所有的 <item> 元素,然后过滤出 type 为 drink 的元素。
import xml.etree.ElementTree as ET
xml_data = '''
<menu>
<item type="drink">咖啡</item>
<item type="food">三明治</item>
<item type="drink">茶</item>
</menu>
'''
root = ET.fromstring(xml_data)
# 查找所有 item
all_items = root.findall('item')
print(f"共 {len(all_items)} 个菜单项")
# 遍历并筛选
drinks = [item for item in all_items if item.get('type') == 'drink']
print(f"饮品: {[d.text for d in drinks]}") # ['咖啡', '茶']输出如下:
共 3 个菜单项
饮品: ['咖啡', '茶']iter() 是递归遍历方法,深度优先扫描当前元素下所有层级子节点,匹配指定标签。如果不传参数,则遍历全部子元素。如果传入标签字符串,则只返回该标签的所有后代(含孙子、重孙,不限直接子节点)。
例如:递归查找 XML 文档中的所有 <student> 元素。
import xml.etree.ElementTree as ET
xml_data = '''
<school>
<class name="一班">
<student>小明</student>
<student>小红</student>
</class>
<class name="二班">
<student>小刚</student>
</class>
</school>
'''
root = ET.fromstring(xml_data)
# 递归查找所有 student,无论嵌套多深
for student in root.iter('student'):
print(student.text)
# 输出: 小明, 小红, 小刚
# 不带参数遍历所有元素
print(f"总元素数: {sum(1 for _ in root.iter())}")通常除了解析 XML 外,还会有对 XML 进行编辑的需求。例如使用 XML 存放应用程序的配置信息,如 IDEA 中的项目信息,这就需要解析、编辑 XML 文档。
前面介绍了使用 ElementTree 对象的属性和 get()方法来获取元素的内容和属性。同样的,也可以使用该对象的属性设置内容,使用 set() 方法设置属性。例如:
import xml.etree.ElementTree as ET
xml_data = '''
<config>
<timeout>30</timeout>
<retries>3</retries>
</config>
'''
root = ET.fromstring(xml_data)
# 修改文本内容
timeout = root.find('timeout')
timeout.text = '60'
# 新增/修改属性
root.set('version', '2.0')
root.set('updated', '2024-01-01')
print(ET.tostring(root, encoding='unicode'))运行结果:
<config version="2.0" updated="2024-01-01">
<timeout>60</timeout>
<retries>3</retries>
</config>使用 ET.SubElement(父元素, 子标签名) 方法给指定的父节点创建并新增一个子标签,新建的子元素被直接挂载到父元素下。如果要给新创建的子元素赋值文本,可以使用 .text 属性。例如:
import xml.etree.ElementTree as ET
xml_data = '''
<config>
<timeout>30</timeout>
<retries>3</retries>
</config>
'''
root = ET.fromstring(xml_data)
# 创建子元素并添加
def add_item(parent, name, value):
item = ET.SubElement(parent, name)
item.text = value
return item
add_item(root, 'version', '1.0.1')
add_item(root, 'enabled', 'true')
# 查看生成的 XML
ET.indent(root, space=' ') # Python 3.9+ 支持缩进
print(ET.tostring(root, encoding='unicode'))运行结果:
<config>
<timeout>30</timeout>
<retries>3</retries>
<version>1.0.1</version>
<enabled>true</enabled>
</config>使用元素对象.remove(目标子元素) 删除直接子节点,不能删后代、不能通过标签名直接删。remove() 只能删调用者的一级子元素,跨层级无效,参数必须是完整 Element 对象,不能传字符串标签。
注意,删除后原 xml 树直接变更,无返回值。
例如:
import xml.etree.ElementTree as ET
xml_data = '''
<task_list>
<task id="1" status="done">写代码</task>
<task id="2" status="pending">测试</task>
<task id="3" status="done">文档</task>
</task_list>
'''
root = ET.fromstring(xml_data)
# 删除所有已完成的任务
for task in root.findall('task'):
if task.get('status') == 'done':
root.remove(task)
print(ET.tostring(root, encoding='unicode'))
# 只剩 <task id="2" ...>测试</task>运行结果:
<task_list>
<task id="2" status="pending">测试</task>
</task_list>使用 write() 保存 XML 文件
使用 ElementTree.write() 方法保存 XML 内容到 XML 文件,该方法接收文件路径 / 文件对象,直接将 XML 树写入本地文件。
参数包含:
file:保存路径
encoding:指定编码(如utf-8,避免中文乱码)
xml_declaration=True:自动生成头部 <?xml version="1.0" encoding="utf-8"?>
例如:
import xml.etree.ElementTree as ET
root = ET.Element('settings')
ET.SubElement(root, 'theme').text = 'dark'
ET.SubElement(root, 'language').text = 'zh-CN'
# 创建 ElementTree 对象
tree = ET.ElementTree(root)
# 写入文件(带 XML 声明)
tree.write('output.xml',
encoding='utf-8',
xml_declaration=True,
method='xml')
# Python 3.9+ 可以美化输出
def prettify_and_save(element, filename):
ET.indent(element, space=' ', level=0)
tree = ET.ElementTree(element)
tree.write(filename, encoding='utf-8', xml_declaration=True)
prettify_and_save(root, 'pretty_output.xml')运行成功后,没有美化输出的数据如下(output.xml):
<?xml version='1.0' encoding='utf-8'?>
<settings><theme>dark</theme><language>zh-CN</language></settings>如果采用美化输出,则数据如下(pretty_output.xml):
<?xml version='1.0' encoding='utf-8'?>
<settings>
<theme>dark</theme>
<language>zh-CN</language>
</settings>下面示例将利用上面学过的 XML 操作 API 实现一个基于 XML 文档的库存管理示例,代码如下:
import xml.etree.ElementTree as ET
from datetime import datetime
class InventoryManager:
def __init__(self, xml_file=None):
if xml_file:
self.tree = ET.parse(xml_file)
self.root = self.tree.getroot()
else:
self.root = ET.Element('inventory')
self.root.set('created', datetime.now().isoformat())
self.tree = ET.ElementTree(self.root)
def add_product(self, sku, name, qty, price):
"""添加新产品"""
# 检查是否已存在
existing = self.root.find(f".//product[@sku='{sku}']")
if existing is not None:
print(f"SKU {sku} 已存在")
return False
product = ET.SubElement(self.root, 'product')
product.set('sku', sku)
ET.SubElement(product, 'name').text = name
ET.SubElement(product, 'quantity').text = str(qty)
ET.SubElement(product, 'price').text = str(price)
return True
def get_stock(self, sku):
"""查询库存"""
product = self.root.find(f".//product[@sku='{sku}']")
if product is None:
return None
return {
'sku': sku,
'name': product.find('name').text,
'qty': int(product.find('quantity').text),
'price': float(product.find('price').text)
}
def update_stock(self, sku, new_qty):
"""更新库存数量"""
product = self.root.find(f".//product[@sku='{sku}']")
if product is None:
return False
product.find('quantity').text = str(new_qty)
return True
def list_low_stock(self, threshold=10):
"""列出库存不足的产品"""
low_stock = []
for product in self.root.findall('product'):
qty = int(product.find('quantity').text)
if qty < threshold:
low_stock.append({
'sku': product.get('sku'),
'name': product.find('name').text,
'qty': qty
})
return low_stock
def save(self, filename):
"""保存到文件"""
ET.indent(self.root, space=' ')
self.tree.write(filename, encoding='utf-8', xml_declaration=True)
# 使用示例
if __name__ == '__main__':
inv = InventoryManager()
# 添加产品库存
print("添加产品库存")
inv.add_product('A001', '机械键盘', 50, 299.0)
inv.add_product('A002', '无线鼠标', 8, 89.0)
inv.add_product('A003', '显示器支架', 3, 159.0)
# 查询库存
stock = inv.get_stock('A001')
print(f"查询库存: {stock['name']} (SKU: {stock['sku']}) - {stock['qty']} 件, 价格: {stock['price']} 元")
# 更新库存
print("更新库存")
inv.update_stock('A001', 45)
# 列出库存不足的产品
print("库存不足预警:")
for item in inv.list_low_stock(threshold=10):
print(f" {item['sku']}: {item['name']} (剩余 {item['qty']} 件)")
inv.save('inventory.xml')运行结果:
添加产品库存
查询库存: 机械键盘 (SKU: A001) - 50 件, 价格: 299.0 元
更新库存
库存不足预警:
A002: 无线鼠标 (剩余 8 件)
A003: 显示器支架 (剩余 3 件)更多内容请参考 https://docs.python.org/zh-cn/3.11/library/xml.etree.elementtree.html 文档。