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 配置(旧版本或复杂需求常用)
2. Python 内置解析器对比
Python 标准库提供了两种核心解析方案,没有绝对的好坏,只有场景的适配:
DOM(Document Object Model)
「把整棵树搬进内存,想改哪改哪」
✅ 优点:
- 支持随机访问任意节点,父子/兄弟节点跳转很方便
- 可直接修改、添加、删除 XML 结构
- Python 内置
xml.dom.minidom 是轻量级 DOM 实现,适合新手
❌ 缺点:
- 内存消耗与 XML 文件大小成正比,大文件(>100MB)直接崩内存
- 解析速度慢于事件驱动方案
SAX(Simple API for XML)
「边读边走,遇到节点触发事件,不回头」
✅ 优点:
- 内存占用极低(仅保存当前上下文),可处理 GB 级文件
- 解析速度极快
- Python 内置
xml.parsers.expat 是 C 语言实现,性能拉满
❌ 缺点:
- 只能单向顺序读取,不能回头看或修改前面的节点
- 需要自行写事件处理逻辑(比如追踪当前在哪个标签下)
📌 实战优先规则:小文件/需修改用 DOM,大文件/只读解析用 SAX。本文重点讲高性能且通用的 SAX,DOM 提一下基础用法即可。
3. SAX 快速上手
SAX 的核心逻辑是「注册三个钩子函数 → 喂入 XML 数据 → 自动触发钩子」。
钩子函数对应三个关键 XML 事件:
- 遇到开始标签(如
<ol>、<a href="/python">)
- 遇到纯文本/CDATA(如标签里的文字)
- 遇到结束标签(如
</ol>、</a>)
3.1 基础示例:遍历 XML 节点
from xml.parsers.expat import ParserCreate
# 自定义 SAX 事件处理类
class SimpleSaxHandler:
def __init__(self):
# 用于追踪当前在处理哪个标签路径(可选,复杂场景必备)
self.current_tag_stack = []
# 钩子1:开始标签触发
def start_element(self, tag_name, attrs_dict):
self.current_tag_stack.append(tag_name)
# 打印带缩进的开始标签,更直观
indent = " " * (len(self.current_tag_stack) - 1)
print(f"{indent}<{tag_name}", end="")
# 打印属性
for k, v in attrs_dict.items():
print(f' {k}="{v}"', end="")
print(">")
# 钩子2:纯文本触发(注意:可能被切分成多次调用!比如换行分割的文本)
def char_data(self, raw_text):
# 先去除首尾空白,判断是否为空文本(比如标签间的换行、空格)
cleaned_text = raw_text.strip()
if cleaned_text:
indent = " " * len(self.current_tag_stack)
print(f"{indent}{cleaned_text}")
# 钩子3:结束标签触发
def end_element(self, tag_name):
indent = " " * (len(self.current_tag_stack) - 1)
print(f"{indent}</{tag_name}>")
self.current_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
# 喂入 XML
parser.Parse(demo_xml)
运行后会输出带缩进的结构化 XML 树,和原格式差不多~
3.2 进阶技巧:合并大段文本
⚠️ 重要坑点:SAX 的 CharacterDataHandler 不会一次性返回标签内的完整文本——如果文本中有换行、特殊字符、或者 XML 解析器内部缓冲区满了,都会触发多次回调。
解决方法很简单:用一个列表临时存当前标签的文本片段,到结束标签时再拼接。
class FullTextSaxHandler:
def __init__(self):
self.current_tag_stack = []
# 临时存当前标签的文本片段
self.current_text_parts = []
def start_element(self, tag_name, attrs_dict):
self.current_tag_stack.append(tag_name)
# 每次进入新标签,清空上一个的文本片段
self.current_text_parts = []
def char_data(self, raw_text):
# 不在这里strip!避免丢失标签中间的有效空白(比如诗歌格式)
self.current_text_parts.append(raw_text)
def end_element(self, tag_name):
# 结束标签时再拼接+处理
full_text = "".join(self.current_text_parts).strip()
if full_text:
indent = " " * (len(self.current_tag_stack) - 1)
print(f"{indent}{tag_name}: {full_text}")
self.current_tag_stack.pop()
4. 如何生成 XML?
字符串拼接是最直接的,但要注意转义特殊字符(&→&、<→<等),否则生成的 XML 是无效的!
4.1 简单场景:字符串拼接
用标准库的 xml.sax.saxutils.escape() 转义即可:
from xml.sax.saxutils import escape
def simple_xml_builder(title: str, content: str) -> str:
# 注意:escape 只处理 & < >,如果需要处理引号可以加 quote=True
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(simple_xml_builder("Python & Go对比", "1+1 < 3, Go > Python 语法糖?"))
4.2 复杂场景:用 ElementTree
如果 XML 有多层嵌套、多个属性,拼接字符串容易出错,推荐用标准库的 xml.etree.ElementTree(它同时支持 DOM 风格的随机访问和写入):
import xml.etree.ElementTree as ET
def complex_xml_builder():
# 1. 创建根节点
root = ET.Element("book_catalog")
# 2. 创建子节点(带属性)
book1 = ET.SubElement(root, "book", id="bk001", price="49.9")
# 3. 给子节点添加文本
ET.SubElement(book1, "author").text = "Gambardella, Matthew"
ET.SubElement(book1, "title").text = "XML Developer's Guide"
ET.SubElement(book1, "genre").text = "Computer"
# 4. 再添加一本
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"
# 5. 转为 ElementTree 对象并写入文件
tree = ET.ElementTree(root)
# xml_declaration=True 自动加声明,encoding='utf-8' 避免乱码
tree.write("books.xml", encoding="utf-8", xml_declaration=True)
# 也可以转为字符串返回
return ET.tostring(root, encoding="unicode", xml_declaration=True)
print(complex_xml_builder())
5. 实战:解析真实的天气 XML 接口
我们用免费的 WeatherAPI 来演示完整流程(接口是我临时找的稳定免费 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.current_tag_stack = []
self.current_text_parts = []
def start_element(self, tag_name, attrs_dict):
self.current_tag_stack.append(tag_name)
self.current_text_parts = []
# 直接从属性提取城市(比文本更快,因为属性不会被分割)
if tag_name == "location":
self.result["city"] = attrs_dict.get("name", "未知城市")
def char_data(self, raw_text):
self.current_text_parts.append(raw_text)
def end_element(self, tag_name):
full_text = "".join(self.current_text_parts).strip()
if full_text:
# 用标签路径做唯一标识(比如只取 current/temp_c 的值)
tag_path = "/".join(self.current_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.current_tag_stack.pop()
def fetch_beijing_weather() -> Dict[str, Any]:
# 替换成你自己的 WeatherAPI 免费 key(或者找其他 XML 接口)
API_KEY = "b4e8f86b44654e6b86885330242207"
API_URL = f"https://api.weatherapi.com/v1/current.xml?key={API_KEY}&q=Beijing&aqi=no"
try:
# 4秒超时,避免卡死
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")
6. 避坑&最佳实践
- SAX 文本必须合并:别直接用
CharacterDataHandler 接收的 raw_text
- 必须转义特殊字符:生成 XML 时必用
escape(),解析 XML 时解析器会自动还原
- 设置解析限制防攻击:默认的 expat 解析器对嵌套深度、实体大小有一定限制,但为了防「XML 炸弹」,可以显式设置(不过 Python 3.8+ 已经默认做了)
- 频繁解析用 lxml:如果需要频繁解析 XML 或用 XPath,建议用第三方库
lxml(性能比内置库好,功能更全)
- 不要手动写 XML 解析器:轮子已经造得非常完美了
7. 什么时候还选 XML?
虽然 JSON 是 Web 首选,但以下场景 XML 更合适:
- 需要严格的 XSD/DTD 结构验证(比如金融、医疗数据)
- 需要命名空间(区分不同来源的同名标签)
- 需要混合内容(比如 HTML 这种标签+文本混排的格式,XML 原生支持)
- 对接遗留的 SOAP 接口(很多老系统还在用)
如果是新项目的配置文件,优先选 YAML;前后端交互优先选 JSON;高性能跨语言数据传输选 Protocol Buffers 或 MessagePack。