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 数据 → 解析器自动触发钩子

三个钩子对应三种关键事件:

  1. 遇到开始标签(如 <book>, <title lang="en">
  2. 遇到纯文本/CDATA(标签里的文字内容)
  3. 遇到结束标签(如 </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 最朴素的方式就是字符串拼接,但一定要注意转义特殊字符(如 &&amp;<&lt;),否则生成的文档可能不合法。

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. 避坑与最佳实践

  1. SAX 文本必须合并:不要直接信任 CharacterDataHandler 返回的片段,永远在结束标签时组装。
  2. 必须转义特殊字符:生成 XML 时一定要用 escape()(或 ElementTree 的内置转义),解析时解析器会自动还原。
  3. 设置解析限制防攻击:Python 3.8+ 对实体扩展等已做默认限制,但仍建议不要解析来源不明的超大 XML。
  4. 高频解析推荐 lxml:如果需要 XPath、复杂命名空间、极高速度,可以引入第三方库 lxml,它比标准库更快、功能更强。
  5. 不要重复造轮子:解析 XML 的库已经非常成熟,不要自己手写字符串处理来解析。

7. 什么时候还选 XML?

即使 JSON 大行其道,以下场景 XML 依然是最佳选择:

  • 需要 XSD / DTD 结构校验(如金融、医疗数据交换)
  • 需要 命名空间 隔离同名标签(如不同版本的配置文件)
  • 需要 混合内容(如 XHTML 里标签和文本交错出现)
  • 对接 遗留的 SOAP 旧系统

如果你是新建项目,做配置优先考虑 YAML,前后端交互优先 JSON,高性能跨语言传输可选 Protocol BuffersMessagePack。而 XML,在你需要「严谨、有序、跨组织交换」的场景下,仍是不可动摇的标准。