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 事件:

  1. 遇到开始标签(如 <ol><a href="/python">
  2. 遇到纯文本/CDATA(如标签里的文字)
  3. 遇到结束标签(如 </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?

字符串拼接是最直接的,但要注意转义特殊字符&&amp;<&lt;等),否则生成的 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. 避坑&最佳实践

  1. SAX 文本必须合并:别直接用 CharacterDataHandler 接收的 raw_text
  2. 必须转义特殊字符:生成 XML 时必用 escape(),解析 XML 时解析器会自动还原
  3. 设置解析限制防攻击:默认的 expat 解析器对嵌套深度、实体大小有一定限制,但为了防「XML 炸弹」,可以显式设置(不过 Python 3.8+ 已经默认做了)
  4. 频繁解析用 lxml:如果需要频繁解析 XML 或用 XPath,建议用第三方库 lxml(性能比内置库好,功能更全)
  5. 不要手动写 XML 解析器:轮子已经造得非常完美了

7. 什么时候还选 XML?

虽然 JSON 是 Web 首选,但以下场景 XML 更合适:

  • 需要严格的 XSD/DTD 结构验证(比如金融、医疗数据)
  • 需要命名空间(区分不同来源的同名标签)
  • 需要混合内容(比如 HTML 这种标签+文本混排的格式,XML 原生支持)
  • 对接遗留的 SOAP 接口(很多老系统还在用)

如果是新项目的配置文件,优先选 YAML;前后端交互优先选 JSON;高性能跨语言数据传输选 Protocol BuffersMessagePack