【xml.etree.ElementTree,一个解析XML的 Python 库!】零依赖搞定结构化数据
在现代软件开发中,XML(可扩展标记语言)作为一种跨平台、自描述的结构化数据格式,依然活跃在各种关键场景中。Python标准库中的xml.etree.ElementTree(以下简称ET)正是处理XML的瑞士军刀——它提供了一个简单而高效的API用于XML数据的解析和创建,而且它是Python的内置标准库,无需任何安装步骤,在任何Python环境中都能直接使用。
在实际生活中,ET的应用随处可见:从XML配置文件中读取应用程序设置;解析RSS/Atom订阅源获取最新的博客文章;处理第三方API返回的XML格式数据;分析财务报表或法律文书中的结构化元数据;甚至,很多老旧系统的数据导出格式仍然是XML,ET可以帮你轻松将这些数据迁移到现代应用。它的核心价值在于零依赖、轻量级、API直观——相比xml.dom.minidom的繁重接口和xml.sax的事件驱动模型,ET提供了一套Python式的树形结构操作方式,让XML处理像操作Python列表一样自然。
安全提示:如果你需要处理不可信或未经验证的数据,请注意Python官方文档中关于XML安全性的警告——恶意构造的XML可能导致拒绝服务攻击或信息泄露。对于生产环境,建议考虑使用
defusedxml等安全增强库。
一、安装库
由于xml.etree.ElementTree是Python标准库的一部分,不需要任何安装步骤,直接导入即可使用:
python
import xml.etree.ElementTree as ET
print("ElementTree已就绪")
从Python 3.3开始,模块会自动使用最快的实现(C语言加速版本),旧的
xml.etree.cElementTree模块已被标记为弃用,官方推荐统一使用xml.etree.ElementTree。
二、基本用法——4步掌握XML解析核心操作
以下4个步骤,带你完成从加载文件到提取数据的完整流程。
1. 加载并解析XML文件
python
import xml.etree.ElementTree as ET
# 方式一:从文件读取XML(最常用)
tree = ET.parse('data.xml')
root = tree.getroot()
print(f"根元素: {root.tag}")
# 方式二:从字符串读取XML
xml_string = '<catalog><book id="1">Python基础</book></catalog>'
root = ET.fromstring(xml_string)
ET.parse()从文件读取并返回一个ElementTree对象,通过.getroot()获取根元素;ET.fromstring()则直接从XML字符串构建根元素。
2. 查找元素(find / findall / iter)
python
# 查找第一个匹配的子元素
first_book = root.find('book')
# 查找所有匹配的子元素(返回列表)
all_books = root.findall('book')
# 递归遍历所有后代元素
for elem in root.iter('book'):
print(elem.tag, elem.attrib)
# .find() 返回第一个匹配子元素,默认只查直接子节点;
# .findall() 返回所有匹配元素的列表;
# .iter() 递归遍历所有后代元素,比`.findall('.//...')`更高效[reference:7]。
3. 获取元素属性和文本内容
python
for book in root.findall('book'):
book_id = book.get('id') # 获取属性(推荐方式,不存在返回None)
book_id_with_default = book.get('id', 'unknown') # 设置默认值
title = book.find('title').text if book.find('title') is not None else None
print(f"ID: {book_id}, Title: {title}")
# 获取元素属性字典
attrs = book.attrib # 返回字典副本
注意:直接使用book.find('title').text在元素不存在时会抛出AttributeError,建议先判断元素是否存在。
4. 创建和构建XML文档
python
# 创建根元素
root = ET.Element("catalog")
# 添加子元素
book1 = ET.SubElement(root, "book")
book1.set("id", "1") # 设置属性
# 添加文本内容
title = ET.SubElement(book1, "title")
title.text = "Python编程"
# 添加另一个子元素
book2 = ET.SubElement(root, "book", id="2") # 也可以在创建时设置属性
ET.SubElement(book2, "title").text = "数据结构与算法"
# 保存为文件
tree = ET.ElementTree(root)
tree.write("output.xml", encoding="utf-8", xml_declaration=True)
使用ET.Element()创建根元素,ET.SubElement(parent, tag)创建子元素并自动建立父子关系,最后通过ElementTree.write()序列化为XML文件。
三、高级用法——生产环境必备技巧
1. 处理XML命名空间
带有命名空间的XML是解析中最容易踩坑的地方——findall()默认忽略命名空间,必须显式传入命名空间字典。
python
# 示例XML带命名空间
xml_with_ns = '''<root xmlns="http://example.com/ns">
<item>数据1</item>
<item>数据2</item>
</root>'''
root = ET.fromstring(xml_with_ns)
# ❌ 错误写法:返回空列表
# items = root.findall('item')
# ✅ 正确写法:显式传入命名空间映射
ns = {'ns': 'http://example.com/ns'}
items = root.findall('ns:item', ns)
# 注册命名空间前缀(输出时保持美观)
ET.register_namespace('', 'http://example.com/ns') # 默认命名空间
ET.register_namespace('xsi', 'http://www.w3.org/2001/XMLSchema-instance')
核心原则:命名空间由URI唯一标识,不要依赖XML原始文件中的前缀名。将URI抽成常量,避免拼写错误。
2. 流式解析大文件(iterparse)
处理超大XML文件时,ET.parse()会将整个文档加载到内存。使用iterparse()可以边读边处理,大幅降低内存占用。
python
import xml.etree.ElementTree as ET
# 逐元素处理,不一次性加载整个文件
for event, elem in ET.iterparse('huge_file.xml', events=('end',)):
if elem.tag == 'book':
# 处理完一个book元素就释放内存
print(f"处理书籍: {elem.get('id')}")
elem.clear() # 清空当前元素,释放内存
# 可选:清理父级引用
if elem.getparent() is not None:
elem.getparent().remove(elem)
注意:iterparse()的events参数可以设为('start', 'end'),'end'事件表示元素解析完成,此时其子元素都已可用。
3. 异常处理与XML校验
python
from xml.etree.ElementTree import ParseError
try:
tree = ET.parse('config.xml')
root = tree.getroot()
except ParseError as e:
print(f"XML解析失败: {e}")
except FileNotFoundError:
print("文件不存在")
except Exception as e:
print(f"其他错误: {e}")
4. 使用XPath进行复杂查询
ET支持有限的XPath语法,适合进行元素过滤和深度查找。
python
# 查找所有book元素
books = root.findall('.//book')
# 查找带有特定属性的元素([@attrib="value"])
book_with_id1 = root.find(".//book[@id='1']")
# 查找第2个book元素
second_book = root.findall('.//book')[1]
# 查找所有title元素
titles = [elem.text for elem in root.findall('.//title')]
# 带有命名空间的XPath
ns = {'ns': 'http://example.com/ns'}
items = root.findall('.//ns:item', ns)
注意:ET的XPath支持有限(如不支持@id>1这类比较表达式),对于复杂的XPath查询,建议使用第三方库lxml。
四、实际应用场景——三个有深度的案例,日常就能用
场景一:RSS新闻阅读器(自动抓取博客更新)
RSS是一种基于XML的新闻订阅格式,很多博客和新闻网站都提供RSS/Atom订阅源。以下脚本自动解析RSS源,提取最新文章标题、链接和发布时间,并生成一个简单的终端阅读器。
python
import xml.etree.ElementTree as ET
import urllib.request
from datetime import datetime
def parse_rss_feed(feed_url):
"""解析RSS/Atom订阅源,返回文章列表"""
try:
# 获取RSS内容
with urllib.request.urlopen(feed_url, timeout=10) as resp:
content = resp.read().decode('utf-8')
root = ET.fromstring(content)
# 处理命名空间(常见的RSS命名空间)
ns = {
'rss': 'http://purl.org/rss/1.0/',
'content': 'http://purl.org/rss/1.0/modules/content/',
'dc': 'http://purl.org/dc/elements/1.1/'
}
articles = []
# RSS 2.0格式:根为rss,channel下有item元素
channel = root.find('channel')
if channel is not None:
for item in channel.findall('item'):
title = item.findtext('title', '无标题')
link = item.findtext('link', '')
pub_date = item.findtext('pubDate', '')
# 处理可能带命名空间的描述(如content:encoded)
desc_elem = item.find('description')
description = desc_elem.text if desc_elem is not None else ''
articles.append({
'title': title.strip(),
'link': link,
'pub_date': pub_date,
'summary': description[:150] + '...' if len(description) > 150 else description
})
return articles
except Exception as e:
print(f"解析失败 [{feed_url}]: {e}")
return []
# 示例:解析一个RSS源
feeds = [
'https://www.ruanyifeng.com/blog/atom.xml', # 阮一峰的网络日志
]
for url in feeds:
print(f"\n📰 阅读源: {url}")
articles = parse_rss_feed(url)
for idx, article in enumerate(articles[:5], 1):
print(f"{idx}. {article['title']}")
print(f" 🔗 {article['link']}")
print(f" 📅 {article['pub_date'][:16] if article['pub_date'] else '日期未知'}")
这个案例展示了ET与urllib的协同使用,以及如何处理带命名空间的XML(如RSS中的content:encoded)。
场景二:XML配置文件管理(应用程序配置热更新)
很多桌面应用和嵌入式设备使用XML存储配置信息。以下脚本展示了如何读取配置、修改配置并保存回文件。
python
import xml.etree.ElementTree as ET
import os
class ConfigManager:
def __init__(self, config_path):
self.path = config_path
self.tree = None
self.root = None
self.reload()
def reload(self):
"""重新加载配置文件"""
if os.path.exists(self.path):
self.tree = ET.parse(self.path)
self.root = self.tree.getroot()
else:
# 创建默认配置
self.root = ET.Element('config')
self.tree = ET.ElementTree(self.root)
self._create_defaults()
self.save()
def _create_defaults(self):
"""创建默认配置结构"""
app = ET.SubElement(self.root, 'app')
ET.SubElement(app, 'name').text = 'MyApplication'
ET.SubElement(app, 'version').text = '1.0.0'
window = ET.SubElement(self.root, 'window')
ET.SubElement(window, 'width').text = '1024'
ET.SubElement(window, 'height').text = '768'
ET.SubElement(window, 'theme').text = 'light'
database = ET.SubElement(self.root, 'database')
ET.SubElement(database, 'host').text = 'localhost'
ET.SubElement(database, 'port').text = '3306'
def get(self, xpath, default=None):
"""通过XPath获取配置值"""
elem = self.root.find(xpath)
return elem.text if elem is not None else default
def set(self, xpath, value):
"""通过XPath设置配置值,自动创建缺失的路径"""
parts = xpath.split('/')
current = self.root
for i, part in enumerate(parts[:-1]):
child = current.find(part)
if child is None:
child = ET.SubElement(current, part)
current = child
final = current.find(parts[-1])
if final is None:
final = ET.SubElement(current, parts[-1])
final.text = str(value)
def save(self):
"""保存配置到文件"""
self.tree.write(self.path, encoding='utf-8', xml_declaration=True)
print(f"配置已保存到 {self.path}")
def display(self):
"""显示所有配置"""
for elem in self.root.iter():
if len(elem) == 0 and elem.text: # 叶子节点
print(f"{self._get_xpath(elem)}: {elem.text}")
def _get_xpath(self, elem):
"""获取元素的XPath路径"""
path = []
current = elem
while current is not None:
path.append(current.tag)
current = self._get_parent(current)
return '/'.join(reversed(path))
def _get_parent(self, elem):
"""获取父元素(需要手动维护或使用lxml)"""
for parent in self.root.iter():
if elem in list(parent):
return parent
return None
# 使用示例
config = ConfigManager('app_config.xml')
print(f"当前主题: {config.get('window/theme')}")
config.set('window/theme', 'dark')
config.set('window/width', '1280')
config.save()
这个案例体现了ET在实际应用中的配置管理能力,支持按需读取、动态修改和持久化保存。
场景三:Excel与XML双向转换(数据迁移工具)
很多企业系统的导出/导入格式是XML,而日常办公更习惯Excel。以下脚本实现了一个轻量级的XML↔Excel转换器。
python
import xml.etree.ElementTree as ET
import csv
def xml_to_csv(xml_file, csv_file, record_tag='row'):
"""将XML转换为CSV格式
示例XML格式:
<data>
<row>
<name>张三</name>
<age>28</age>
<city>北京</city>
</row>
</data>
"""
tree = ET.parse(xml_file)
root = tree.getroot()
# 收集所有可能的字段
fields = set()
rows = []
for record in root.findall(f'.//{record_tag}'):
row = {}
for elem in record:
fields.add(elem.tag)
row[elem.tag] = elem.text or ''
rows.append(row)
# 按字段名排序,保持输出一致性
fields = sorted(fields)
# 写入CSV
with open(csv_file, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
writer.writerows(rows)
print(f"已将 {len(rows)} 条记录从 {xml_file} 导出到 {csv_file}")
def csv_to_xml(csv_file, xml_file, root_tag='data', record_tag='row'):
"""将CSV转换为XML格式"""
root = ET.Element(root_tag)
with open(csv_file, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row_dict in reader:
record = ET.SubElement(root, record_tag)
for key, value in row_dict.items():
if value: # 跳过空值
elem = ET.SubElement(record, key)
elem.text = value
tree = ET.ElementTree(root)
tree.write(xml_file, encoding='utf-8', xml_declaration=True)
print(f"已将CSV数据转换为 {xml_file}")
# 使用示例
# xml_to_csv('data.xml', 'output.csv', 'row')
# csv_to_xml('input.csv', 'output.xml', 'employees', 'employee')
这个案例展示了ET如何作为数据管道中的通用网关,在不同格式之间架起桥梁,适用于数据清洗、迁移和系统对接场景。
五、结尾:总结与互动
xml.etree.ElementTree虽然不像lxml那样支持完整的XPath 1.0或XSLT转换,但它作为Python标准库的零依赖原生XML处理器,在绝大多数日常XML处理任务中已经足够胜任。它的API设计贴合Python的简洁哲学,让你能够用最少的代码完成XML的解析、遍历、修改和生成。无论是解析配置文件、处理RSS订阅源,还是轻量级的数据清洗与转换,ET都能给你提供稳定可靠的解决方案。
那么,现在轮到你了:你日常工作中有没有遇到过需要解析XML的场景?是处理来自第三方API的XML响应、解析系统导出的日志报告,还是写一个爬虫抓取RSS新闻?如果你曾经被findall()死活找不到元素气得抓狂,很可能就是命名空间在作祟——欢迎在评论区分享你的故事或代码片段。我也会在后续的文章中专门聊聊如何用lxml弥补ET的不足(比如更强大的XPath支持和更优雅的命名空间管理),以及如何用defusedxml加固XML解析的安全防线。动手试试吧,XML的世界比你想象的更有趣!
更多推荐

所有评论(0)