Python 标准库的 XML 工具已足够应对大多数场景,但在特定需求下,第三方库能显著提升开发效率和处理能力。本章介绍两款常用工具:xmltodict(简化开发)和 lxml(高性能处理)。
xmltodict 将 XML 转换为 Python 字典,利用字典操作处理 XML,大幅降低学习成本。
使用 pip install 命令安装 xmltodict 库,如下:
pip install xmltodict安装日志:
(base) PS D:\> pip install xmltodict
Defaulting to user installation because normal site-packages is not writeable
Collecting xmltodict
Downloading xmltodict-1.0.4-py3-none-any.whl.metadata (14 kB)
Downloading xmltodict-1.0.4-py3-none-any.whl (13 kB)
Installing collected packages: xmltodict
Successfully installed xmltodict-1.0.4使用如下命令验证依赖安装是否正确:
(base) PS D:\$share_dir> python
Python 3.13.9 | packaged by Anaconda, Inc. | (main, Oct 21 2025, 19:09:58) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
Ctrl click to launch VS Code Native REPL
>>> import xmltodict如果 import 没有抛出任何错误,说明安装成功。下面通过实例来介绍 xmltodict 库的用法
使用 xmltodict.parse(XML 字符串) 方法快速将 XML 转换为 Python 字典类型,例如:
import xmltodict
xml_data = '''<?xml version="1.0"?>
<library>
<name>市图书馆</name>
<books>
<book id="1">
<title>Python编程</title>
<author>张三</author>
<price>59.00</price>
</book>
<book id="2">
<title>数据分析</title>
<author>李四</author>
<price>89.00</price>
</book>
</books>
</library>
'''
# 解析为字典
data = xmltodict.parse(xml_data)
# 查看类型
print(type(data)) # <class 'dict'>
# 查看字典的键
print(data.keys()) # dict_keys(['library'])
# 访问数据
library = data['library']
print(library['name']) # 市图书馆
# 访问书籍列表
books = library['books']['book']
print(f"共 {len(books)} 本书")
for book in books:
print(f" {book['title']} ({book['author']}) - ¥{book['price']}")
print(f" ID: {book['@id']}") # 属性以 @ 开头运行示例,输出如下:
<class 'dict'>
dict_keys(['library'])
市图书馆
共 2 本书
Python编程 (张三) - ¥59.00
ID: 1
数据分析 (李四) - ¥89.00
ID: 2使用 xmltodict.unparse() 方法将 Python 字典对象快速转换为 XML 字符串。
方法定义如下:
xmltodict.unparse(
data, # 字典对象
pretty=True, # 是否美化缩进
indent=" ", # 自定义缩进符,默认4空格
newl="\n", # 换行符
full_document=True, # 是否头部带 <?xml ...> 声明
encoding="utf-8" # xml头部编码
)示例代码:
import xmltodict
# 构建数据结构
data = {
'order': {
'@id': 'ORD-2024-001',
'@date': '2024-01-15',
'customer': '张三',
'items': {
'item': [
{
'@sku': 'ITEM-001',
'name': '机械键盘',
'qty': 1,
'price': 299.00
},
{
'@sku': 'ITEM-002',
'name': '鼠标垫',
'qty': 2,
'price': 29.00
}
]
},
'total': 357.00
}
}
# 转换为 XML
xml_output = xmltodict.unparse(data, pretty=True)
print(xml_output)注意,上述字典对象中,以 @ 开头的字段将当做 XML 文档中某个标签的属性。例如,@id、@date 当做 order 标签的属性。
运行示例,输出如下:
<?xml version="1.0" encoding="utf-8"?>
<order id="ORD-2024-001" date="2024-01-15">
<customer>张三</customer>
<items>
<item sku="ITEM-001">
<name>机械键盘</name>
<qty>1</qty>
<price>299.0</price>
</item>
<item sku="ITEM-002">
<name>鼠标垫</name>
<qty>2</qty>
<price>29.0</price>
</item>
</items>
<total>357.0</total>
</order>xmltodict 库规定,XML 标签上的属性会统一用“@属性名”作为字典 key,标签中间文本用“#text”作为字典的 KEY。
示例 1:解析带属性单节点 XML
import xmltodict
xml_str = '''
<setting name="theme" type="str">dark</setting>
'''
d = xmltodict.parse(xml_str)
print(d)
# 输出:
# {'setting': {'@name': 'theme', '@type': 'str', '#text': 'dark'}}
# 取值
name = d["setting"]["@name"]
val = d["setting"]["#text"]
print("name:", name, "value:", val)运行实例,输出:
{'setting': {'@name': 'theme', '@type': 'str', '#text': 'dark'}}
name: theme value: dark示例 2: 使用 unparse() 将字典对象反向还原为 XML
import xmltodict
data = {
"setting": {
"@name": "language", # 属性
"@type": "str", # 属性
"#text": "zh-CN" # 标签内部文本
}
}
xml_out = xmltodict.unparse(data, pretty=True)
print(xml_out)运行实例,输出如下:
<?xml version="1.0" encoding="utf-8"?>
<setting name="language" type="str">zh-CN</setting>默认情况下,xmltodict 对一个元素和多个元素返回的类型不一致,规则如下:
同层级多个同名标签:自动解析为 list
同层级单个同名标签:默认解析为 dict,不会生成列表
例如,<students>下面存在一个 <student> 时返回字典类型,如果存在多个 <student> 时返回列表类型。这对程序处理不友好,一不小心就会出现错误。为了解决这个问题,可以使用 force_list:
force_list=("标签名",):强制该标签无论几条都转为列表,解决单条时类型不统一的坑
示例代码:
import xmltodict
# 默认情况下,单个元素不会变成列表
xml_data = '''<?xml version="1.0"?>
<config>
<setting name="theme">dark</setting>
</config>
'''
data = xmltodict.parse(xml_data)
print(type(data['config']['setting'])) # <class 'dict'> - 只有一条时不是列表
# 使用 force_list 强制指定标签为列表
data = xmltodict.parse(xml_data, force_list=('setting',))
print(type(data['config']['setting'])) # <class 'list'>
# 多个元素,默认会解析为列表
xml_data = '''<?xml version="1.0"?>
<config>
<setting name="theme">dark</setting>
<setting name="language">zh-CN</setting>
</config>
'''
data = xmltodict.parse(xml_data)
print(type(data['config']['setting'])) # <class 'list'>
print(len(data['config']['setting'])) # 2运行示例,输出如下:
<class 'dict'>
<class 'list'>
<class 'list'>
2什么是命名空间前面已经介绍了,不在赘述。默认情况下,直接 xmltodict.parse(xml_str) 得到字典键会带前缀,例如:
import xmltodict
xml_data = '''<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:media="http://search.yahoo.com/mrss/">
<title>我的博客</title>
<media:thumbnail url="thumb.jpg"/>
</feed>
'''
data = xmltodict.parse(xml_data)
print(data)运行示例,输出如下:
{'feed': {'@xmlns': 'http://www.w3.org/2005/Atom', '@xmlns:media': 'http://search.yahoo.com/mrss/', 'title': '我的博客', 'media:thumbnail': {'@url': 'thumb.jpg'}}}输出的 key 中,“media:thumbnail ”携带了命名空间前缀。使用、取值极麻烦,xmltodict 提供两种方案处理命名空间。
在使用 xmltodict.parse() 解析 XML 时添加如下参数:
process_namespaces=True:自动移除命名空间前缀,把标签名统一为本地名
namespace_collapse:是否合并同名不同命名空间节点,默认 True
例如:
import xmltodict
xml_data = '''<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:media="http://search.yahoo.com/mrss/">
<title>我的博客</title>
<media:thumbnail url="thumb.jpg"/>
</feed>
'''
# 定义命名空间映射,值为None代表移除该命名空间
ns_map = {
"http://www.w3.org/2005/Atom": None,
"http://search.yahoo.com/mrss/": None
}
# 必须同时开 process_namespaces + 传入 namespaces
data = xmltodict.parse(xml_data, process_namespaces=True, namespaces=ns_map)
print(data)
print(data['feed']['title']) # 输出: 我的博客运行示例,输出如下:
{'feed': {'@xmlns': {'': 'http://www.w3.org/2005/Atom', 'media': 'http://search.yahoo.com/mrss/'}, 'title': '我的博客', 'thumbnail': {'@url': 'thumb.jpg'}}}
我的博客此时,我们就可以直接取值 data['feed']['title']。
如果需要前缀,由于业务需要不能剥离。我们可以使用 namespaces 参数自定义别名映射,即使 XML 文档的命名空间前缀变了也不影响我们解析。
这种方式非常适合需要区分多个同名标签、不想完全去掉命名空间的场景,手动给 URI 绑定短别名。
例如:
import xmltodict
xml_data = '''<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:media="http://search.yahoo.com/mrss/">
<title>我的博客</title>
<media:thumbnail url="thumb.jpg"/>
</feed>
'''
# 定义命名空间映射,值为None代表移除该命名空间
ns_map = {
"http://www.w3.org/2005/Atom": "a",
"http://search.yahoo.com/mrss/": "m"
}
# 必须同时开 process_namespaces + 传入 namespaces
data = xmltodict.parse(xml_data, process_namespaces=True, namespaces=ns_map)
print(data)
print(data['a:feed']['a:title']) # 输出: 我的博客运行示例,输出如下:
{'a:feed': {'@xmlns': {'': 'http://www.w3.org/2005/Atom', 'media': 'http://search.yahoo.com/mrss/'}, 'a:title': '我的博客', 'm:thumbnail': {'@url': 'thumb.jpg'}}}
我的博客lxml 基于 C 语言库 libxml2 和 libxslt,性能远超纯 Python 实现,同时支持 XPath 1.0/2.0、XSLT、Schema 验证等高级功能。
使用 pip install 安装 lxml 库,如下:
pip install lxml下面演示 lxml 库如何快速解析 XML 文档,也分为从字符串解析和从文件解析:
(1)从字符串解析 XML,可用于通过 API 接口获取的 XML 内容,或者从数据库读取出来的 XML 字符串,例如:
from lxml import etree
# 从字符串解析
# 字符串可以来自远程API接口或数据库、文件内容
xml_data = '<root><item id="1">内容</item></root>'
root = etree.fromstring(xml_data.encode('utf-8'))
print(root.tag) # root
print(root[0].get('id')) # 1(2)直接从文件解析 XML,即将 XML 内容保存到 XML 文档中,给一个文档地址,就可以进行解析,不要将文档内容读取出来进行解析。假如存在 simple.xml 文档,内容如下:
<?xml version="1.0"?>
<message id="1">
<to>张三</to>
<content>你好</content>
</message>使用 lxml 解析该文档,如下:
from lxml import etree
# 从文件解析
tree = etree.parse('sample.xml') # 返回 ElementTree
root = tree.getroot()
print(root.tag) # message
print(root[0].text) # 张三运行示例,输出如下:
message
张三lxml 支持完整的 XPath 1.0,功能远超 ElementTree。下面快速熟悉一下 XPath 1.0 的语法:
(1)节点选择语法
| 表达式 | 含义 |
| /root | 根节点,绝对路径 |
| //product | 全局任意位置所有 product 节点 |
| . | 当前节点 |
| .. | 父节点 |
| ./price | 当前节点下 price 子节点 |
| @id | 当前节点 id 属性 |
(2)条件过滤,XPath1.0 支持数字、字符串、数值比较
| 表达式 | 描述 |
| //product[price > 10] | 价格大于10的商品 |
| //product[stock = 0] | 库存等于0 |
| //product[@type='fruit'] | 属性匹配 |
| //product[1] | 取第1个商品(下标从1开始) |
| //product[last()] | 最后一个商品 |
| //name[contains(.,'苹果')] | 包含文本 |
(3)XPath 1.0 内置核心函数
字符串函数:
string(节点):转为字符串,如://book[string-length(string(price))=5]
concat(a,b,c):拼接字符串,如://book[concat(author,price)='曹雪芹29.99']
contains(str,sub):是否包含子串,如://book[contains(author,'芹')]
starts-with(str,pre):开头匹配,如://book[starts-with(author,'曹')]
substring(s,start,len):截取,起始位从 1 开始,如://book[substring(author,2,1)='雪']
string-length():字符串长度,例如://book[string-length(author)=3]
数值函数:
number(节点):文本转数字,空 / 非数字返回 0。例如://book[number(price)>0]
sum(节点集合):求和(只能单字段集合,不能相乘再求和)。例如:sum(//price)
floor() / ceiling() / round() 取整,例如:floor(sum(//price))、ceiling(sum(//price))、round(sum(//price))
+ - * div mod 四则运算(除法用 div,不能用 /),例如:string(//book/year + //book/price)
布尔函数:
true() ,例如://book[true()]
false(),例如://book[not(false())]
not(条件) 取反,例如://book[not(false())]
节点集合函数:
last() 最后一个节点下标,如://book[last()][position()=last()]
position() 当前节点位置,如://book[1][position()=1]
count(节点) 统计总数,如:count(//book)
简单示例:
from lxml import etree
xml_data = '''
<products>
<product id="1" category="electronics">
<name>手机</name>
<price>2999</price>
<stock>100</stock>
</product>
<product id="2" category="clothing">
<name>T恤</name>
<price>99</price>
<stock>500</stock>
</product>
<product id="3" category="electronics">
<name>耳机</name>
<price>599</price>
<stock>200</stock>
</product>
</products>
'''
root = etree.fromstring(xml_data.encode('utf-8'))
# 条件选择:价格大于1000的产品
expensive = root.xpath('//product[price > 1000]/name/text()')
print(f"高价产品: {expensive}")
# 选择具有特定属性的元素
electronics = root.xpath('//product[@category="electronics"]/name/text()')
print(f"电子产品: {electronics}")
# 选择第N个元素
second_product = root.xpath('//product[2]/name/text()')
print(f"第二个产品: {second_product}")
# 使用 contains() 进行模糊匹配
phones = root.xpath('//product[contains(name, "机")]/name/text()')
print(f"含'机'的产品: {phones}")运行示例,输出如下:
高价产品: ['手机']
电子产品: ['手机', '耳机']
第二个产品: ['T恤']
含'机'的产品: ['手机', '耳机']lxml 支持类似 jQuery 的 CSS 选择器,例如:
from lxml import etree
from lxml.cssselect import CSSSelector
xml_data = '''
<html>
<body>
<div class="product" data-id="1">产品A</div>
<div class="product featured" data-id="2">产品B</div>
<div class="product" data-id="3">产品C</div>
</body>
</html>
'''
tree = etree.fromstring(xml_data.encode('utf-8'))
# 使用 CSS 选择器
sel = CSSSelector('.product.featured')
featured = sel(tree)
print(f"精选产品: {[e.text for e in featured]}")
# 多条件选择
all_products = CSSSelector('div.product')
print(f"所有产品数: {len(all_products(tree))}")运行示例,输出如下:
精选产品: ['产品B']
所有产品数: 3和 xmltodict 类似,也支持自定义命名空间前缀,访问时就可以使用自定义的命名空间前缀固定访问,即使后续 XML 文档的前缀变了,命名空间地址没有变依然能够访问。如下:
from lxml import etree
# 注意,<?xml version="1.0"?> 必须放在第一行
# 否则会跑出如下错误:
# lxml.etree.XMLSyntaxError: XML declaration allowed only at the start of the document, line 2, column 6
xml_data = '''<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:m="http://search.yahoo.com/mrss/">
<title>我的博客</title>
<m:thumbnail url="thumb.jpg" width="100"/>
</feed>
'''
tree = etree.fromstring(xml_data.encode('utf-8'))
# 定义命名空间前缀
ns = {
'atom': 'http://www.w3.org/2005/Atom',
'media': 'http://search.yahoo.com/mrss/'
}
# 使用命名空间查询
title = tree.xpath('//atom:title/text()', namespaces=ns)
print(f"标题: {title}")
thumbnails = tree.xpath('//media:thumbnail/@url', namespaces=ns)
print(f"缩略图: {thumbnails}")运行示例,输出如下:
标题: ['我的博客']
缩略图: ['thumb.jpg']大型文件处理
<?xml version="1.0" encoding="utf-8"?>
<logs>
<!-- info 普通日志 -->
<log level="info">系统启动成功,服务端口8080已监听</log>
<log level="info">用户id=1001发起登录请求</log>
<log>数据库连接池初始化完成(无level属性,默认info)</log>
<!-- warning 警告日志 -->
<log level="warning">内存占用超过80%,建议扩容服务器</log>
<log level="warning">接口请求耗时大于2000ms,存在性能隐患</log>
<!-- error 错误日志 -->
<log level="error">数据库查询异常:Table 'user' not exists</log>
<log level="error">文件上传失败,存储空间不足</log>
<log level="error">第三方支付接口超时,重试3次仍失败</log>
<!-- 可无限复制下方log节点模拟超大文件 -->
<log level="info">定时任务001执行完成</log>
<log level="warning">缓存命中率低于60%</log>
<log level="error">Redis连接断开</log>
</logs>lxml 同样支持 iterparse,但速度更快:
from lxml import etree
def fast_count_elements(filename, tag):
"""
快速统计XML文件中指定标签的元素总数(流式解析,低内存占用)
采用lxml.iterparse 流式逐行读取大文件,无需一次性加载整个XML到内存
:param filename: str XML文件路径,支持超大XML文件
:param tag: str 需要统计的目标标签名
:return: int 匹配该标签的元素总个数
"""
# 计数器,记录匹配标签的元素数量
count = 0
# 流式迭代解析XML,仅监听元素结束事件,只捕获指定tag节点
for event, elem in etree.iterparse(filename, events=('end',), tag=tag):
# 每匹配到一个目标标签,计数+1
count += 1
# 清空当前节点内部数据,释放内存
elem.clear()
# 循环删除当前节点之前的所有兄弟节点,彻底释放已解析资源,防止内存持续上涨
while elem.getprevious() is not None:
del elem.getparent()[0]
return count
# 统计 huge_log.xml 文件中 <log> 标签的总条数
count = fast_count_elements('huge_log.xml', 'log')
print(f"共有 {count} 条记录")运行示例,输出如下:
共有 11 条记录