Python3 ElementTree 基础操作

🎉摘要:本文详细介绍了 Python 标准库 ElementTree 处理 XML 的核心方法,包括从文件和字符串解析 XML、遍历与查找元素、修改内容、添加删除子元素以及保存 XML 文件。通过完整示例演示了如何使用 ElementTree 进行库存管理,适合 Python 开发者学习 XML 处理技术。

前面介绍了 XML、以及相关的 DTD、XML Schema

ElementTree 是 Python 标准库处理 XML 的核心工具。本章介绍其基础用法,包括解析、遍历、查询和简单修改。

注意点:对恶意 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 必须要面对的,例如使用接口获取用户列表信息,返回的是多个用户信息,我们需要通过遍历 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() 查找第一个匹配

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'))  # drink

findall() 查找所有匹配

findall() 方法查找当前节点下所有符合标签路径的子节点,返回节点列表,无匹配则返回空列表 []。

例如:查找所有的 <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() 按标签名递归查找

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 进行编辑的需求。例如使用 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>

保存 XML 文件

使用 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 文档。

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号