Python XML 处理教程
虽然如今 JSON 凭借轻量、易读写的特性在 Web 前后端交互中占据主导,但 XML 的结构化约束、命名空间、混合内容等能力让它依然无法被替代,常见于企业应用、配置文件、行业数据交换等场景。
本文将带你快速上手 Python 内置的 XML 解析与生成工具,并用真实的天气接口演示完整流程。
1. 你看得到的 XML 身影
以下用例你一定多少接触过,它们都是 XML 的重要“根据地”:
- Android 的
activity_layout.xml 布局文件
- RSS 2.0 / Atom 新闻订阅源
- Word
.docx、Excel .xlsx 的底层压缩包内容
- 银行、保险等行业的 SOAP 接口请求/响应
- Jenkins Pipeline 配置(旧版或复杂需求常用)
- Maven 的
pom.xml、Web 应用的 web.xml
这些场景要么要求严格的格式验证,要么需要命名空间隔离,这正是 XML 所擅长的。
2. Python 内置解析器对比
Python 标准库提供了两种核心解析方案,没有绝对的好坏,唯有场景的适配。
DOM(Document Object Model)
“把整个文档树加载到内存里,想怎么改就怎么改”
✅ 优点
- 支持随机访问任意节点,父子、兄弟节点跳转十分方便
- 可以直接修改、添加、删除 XML 结构
- Python 内置的
xml.dom.minidom 是轻量级 DOM 实现,适合新手
❌ 缺点
- 内存消耗与 XML 文件大小成正比,大文件(超过 100 MB)可能直接撑爆内存
- 解析速度慢于事件驱动方案
SAX(Simple API for XML)
“边读边解析,遇到元素就触发事件,不回头”
✅ 优点
- 内存占用极低(仅保存当前上下文),能处理 GB 级别的文件
- 解析速度极快,C 语言实现的
xml.parsers.expat 性能拉满
❌ 缺点
- 只能单向顺序读取,无法回溯或修改前面的节点
- 需要自己编写事件处理逻辑(例如追踪当前所在的标签路径)
📌 实战优先法则:小文件、需要修改结构时用 DOM;大文件、只读解析首选 SAX。本文重点介绍高性能且通用的 SAX,同时也会简单带过 DOM 的用法。
3. 快速上手 SAX
SAX 的核心思想:注册三个钩子函数 → 喂入 XML 数据 → 解析器自动触发钩子。
三个钩子对应三种关键事件:
- 遇到开始标签(如
<book>, <title lang="en">)
- 遇到纯文本/CDATA(标签里的文字内容)
- 遇到结束标签(如
</book>, </title>)
3.1 基础示例:遍历所有节点
from xml.parsers.expat import ParserCreate
# 自定义 SAX 事件处理类
class SimpleSaxHandler:
def __init__(self):
# 用于追踪当前标签路径(复杂解析时必备)
self.tag_stack = []
def start_element(self, tag_name, attrs):
self.tag_stack.append(tag_name)
indent = " " * (len(self.tag_stack) - 1)
print(f"{indent}<{tag_name}", end="")
# 打印属性
for k, v in attrs.items():
print(f' {k}="{v}"', end="")
print(">")
def char_data(self, raw_text):
text = raw_text.strip()
if text:
indent = " " * len(self.tag_stack)
print(f"{indent}{text}")
def end_element(self, tag_name):
indent = " " * (len(self.tag_stack) - 1)
print(f"{indent}</{tag_name}>")
self.tag_stack.pop()
# 测试 XML
demo_xml = """<?xml version="1.0" encoding="UTF-8"?>
<tech_blog>
<title>Python XML 入门</title>
<tags>
<tag lang="Python">XML</tag>
<tag lang="Python">SAX</tag>
</tags>
</tech_blog>
"""
handler = SimpleSaxHandler()
parser = ParserCreate()
parser.StartElementHandler = handler.start_element
parser.EndElementHandler = handler.end_element
parser.CharacterDataHandler = handler.char_data
parser.Parse(demo_xml)
运行后你会看到带缩进的结构化输出,和原始 XML 长得几乎一样。
3.2 进阶技巧:处理分段文本
⚠️ 重要坑点:SAX 的 CharacterDataHandler 不会一次性返回标签内的完整文本。如果文本中包含换行、特殊字符,或解析器内部缓冲区满了,都可能触发多次回调。
解决方法:在内存中临时缓存文本片段,在结束标签时再拼接处理。
class FullTextSaxHandler:
def __init__(self):
self.tag_stack = []
self.text_parts = []
def start_element(self, tag_name, attrs):
self.tag_stack.append(tag_name)
# 每次进入新标签,清空上一个标签的文本片段
self.text_parts = []
def char_data(self, raw_text):
# 直接追加原始片段,不 strip,避免丢失有意保留的空白
self.text_parts.append(raw_text)
def end_element(self, tag_name):
full_text = "".join(self.text_parts).strip()
if full_text:
indent = " " * (len(self.tag_stack) - 1)
print(f"{indent}{tag_name}: {full_text}")
self.tag_stack.pop()
这样即使文本被切分,你最终拿到的也是完整的标签内容。
4. 如何生成 XML?
生成 XML 最朴素的方式就是字符串拼接,但一定要注意转义特殊字符(如 & → &、< → <),否则生成的文档可能不合法。
4.1 简单场景:字符串拼接 + 转义
用标准库的 xml.sax.saxutils.escape() 可以自动处理基本转义:
from xml.sax.saxutils import escape
def build_article(title: str, content: str) -> str:
xml_parts = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<article>',
f' <title>{escape(title)}</title>',
f' <content>{escape(content)}</content>',
'</article>'
]
return "\n".join(xml_parts)
# 测试带有“危险”字符的内容
print(build_article("Python & Go 对比", "1+1 < 3, Go > Python 语法糖?"))
4.2 复杂场景:用 ElementTree
当 XML 结构多层嵌套、属性繁多时,手写字符串极易出错。推荐使用标准库的 xml.etree.ElementTree:
import xml.etree.ElementTree as ET
def build_book_catalog():
# 创建根元素
root = ET.Element("catalog")
# 第一本书(带属性)
book1 = ET.SubElement(root, "book", id="bk001", price="49.9")
ET.SubElement(book1, "author").text = "Gambardella, Matthew"
ET.SubElement(book1, "title").text = "XML Developer's Guide"
ET.SubElement(book1, "genre").text = "Computer"
# 第二本书
book2 = ET.SubElement(root, "book", id="bk002", price="39.5")
ET.SubElement(book2, "author").text = "Ralls, Kim"
ET.SubElement(book2, "title").text = "Midnight Rain"
ET.SubElement(book2, "genre").text = "Fantasy"
# 写入文件
tree = ET.ElementTree(root)
tree.write("books.xml", encoding="utf-8", xml_declaration=True)
# 也可以直接返回字符串
return ET.tostring(root, encoding="unicode", xml_declaration=True)
print(build_book_catalog())
ElementTree 既支持 DOM 风格的随机访问,也提供了便捷的输出接口,非常适合作为 Python 中的「瑞士军刀」。
5. 实战:解析真实的天气 XML 接口
我们使用免费的 WeatherAPI 来演示 SAX 的完整解析流程。请将 API_KEY 替换成你自己申请的免费密钥(或者寻找其他提供 XML 输出的天气接口)。
from xml.parsers.expat import ParserCreate
from urllib.request import urlopen
from typing import Dict, Any
class WeatherSaxHandler:
def __init__(self):
self.result: Dict[str, Any] = {}
self.tag_stack = []
self.text_parts = []
def start_element(self, tag_name, attrs):
self.tag_stack.append(tag_name)
self.text_parts = []
# 从属性中直接提取城市名称(属性数据不会被分割)
if tag_name == "location":
self.result["city"] = attrs.get("name", "未知城市")
def char_data(self, raw_text):
self.text_parts.append(raw_text)
def end_element(self, tag_name):
full_text = "".join(self.text_parts).strip()
if full_text:
tag_path = "/".join(self.tag_stack) # 利用标签路径精准定位
if tag_path == "current/temp_c":
self.result["temperature"] = float(full_text)
elif tag_path == "current/condition/text":
self.result["weather_desc"] = full_text
elif tag_path == "current/wind_kph":
self.result["wind_speed"] = float(full_text)
self.tag_stack.pop()
def fetch_beijing_weather() -> Dict[str, Any]:
# 免费 API 密钥,请替换为你自己的
API_KEY = "your_actual_key_here"
API_URL = f"https://api.weatherapi.com/v1/current.xml?key={API_KEY}&q=Beijing&aqi=no"
try:
with urlopen(API_URL, timeout=4) as resp:
xml_data = resp.read().decode("utf-8")
handler = WeatherSaxHandler()
parser = ParserCreate()
parser.StartElementHandler = handler.start_element
parser.EndElementHandler = handler.end_element
parser.CharacterDataHandler = handler.char_data
parser.Parse(xml_data)
return handler.result
except Exception as e:
return {"error": str(e)}
if __name__ == "__main__":
weather = fetch_beijing_weather()
if "error" in weather:
print(f"获取天气失败:{weather['error']}")
else:
print("--- 北京实时天气 ---")
print(f"城市:{weather['city']}")
print(f"温度:{weather['temperature']}℃")
print(f"天气:{weather['weather_desc']}")
print(f"风速:{weather['wind_speed']} km/h")
整个流程就是:定义事件处理规则 → 网络获取原始 XML → SAX 解析 → 提取目标数据,清晰且高效。
6. 避坑与最佳实践
- SAX 文本必须合并:不要直接信任
CharacterDataHandler 返回的片段,永远在结束标签时组装。
- 必须转义特殊字符:生成 XML 时一定要用
escape()(或 ElementTree 的内置转义),解析时解析器会自动还原。
- 设置解析限制防攻击:Python 3.8+ 对实体扩展等已做默认限制,但仍建议不要解析来源不明的超大 XML。
- 高频解析推荐 lxml:如果需要 XPath、复杂命名空间、极高速度,可以引入第三方库
lxml,它比标准库更快、功能更强。
- 不要重复造轮子:解析 XML 的库已经非常成熟,不要自己手写字符串处理来解析。
7. 什么时候还选 XML?
即使 JSON 大行其道,以下场景 XML 依然是最佳选择:
- 需要 XSD / DTD 结构校验(如金融、医疗数据交换)
- 需要 命名空间 隔离同名标签(如不同版本的配置文件)
- 需要 混合内容(如 XHTML 里标签和文本交错出现)
- 对接 遗留的 SOAP 旧系统
如果你是新建项目,做配置优先考虑 YAML,前后端交互优先 JSON,高性能跨语言传输可选 Protocol Buffers 或 MessagePack。而 XML,在你需要「严谨、有序、跨组织交换」的场景下,仍是不可动摇的标准。