Python3 ElementTree 高级用法

🎉摘要:本文深入讲解Python ElementTree库的高级应用,涵盖XPath查询(基础选择器、属性筛选、位置谓词、多条件组合)、XML命名空间(Namespace)的注册与处理,以及使用iterparse方法高效处理大型XML文件的流式解析技术,并附有实战案例:封装一个XML配置文件管理器。

本章深入讲解 ElementTree 的高级特性,包括 XPath 查询、命名空间处理。

对 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 存入最终列表

命名空间(Namespace)处理

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 元素,但是它们属于不同的命名空间,不会造成元素名称冲突。在实际工作中,这可以解决多个团队、多个企业交换数据时元素名称冲突。

处理带命名空间的 XML

下面通过实例演示,如何通过 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/。

使用 register_namespace()

该方法用于给 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>

大型 XML 文件处理

ElementTree 默认加载整个文档到内存,该种方式对于小的 XML 文件还是非常方便,但是对于大文件就会存在问题,可能会导致内存耗尽。对于 XML 大文件,推荐使用 iterparse 进行流式处理,读取一个元素处理一个元素,然后释放内存,避免内存消耗过大。

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 类,让他符合自己的需求。

 

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