乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • XML 基础语法

    • XML 概述
    • XML 文档结构
    • XML 元素
    • XML 属性
    • XML 语法规则
    • XML 命名空间
    • XML 注释与处理指令
  • DTD 与文档验证

    • DTD 概述
    • DTD 元素声明
    • DTD 属性声明
    • DTD 实体声明
    • DTD 元素与属性对比
    • DTD 完整示例
  • XML Schema 定义

    • XML Schema 概述
    • XSD 简单类型
    • XSD 复杂类型
    • XSD 命名空间与引用
  • XPath 节点定位

    • XPath 概述
    • XPath 路径表达式
    • XPath 谓词与函数
  • XSLT 转换

    • XSLT 概述
    • XSLT 模板与匹配
    • XSLT 控制结构
    • XSLT 输出控制
  • XML 解析技术

    • XML 解析概述
    • DOM 解析
    • SAX 解析
    • StAX 解析
    • XML 与 Java — JAXP
  • XML 在 Java 中的应用

    • Spring XML 配置
    • MyBatis XML 映射
    • pom.xml 与 Maven
    • web.xml 配置详解
  • 现代数据格式对比

    • XML 与 JSON 对比
    • XML 与 YAML 对比
  • XML 显示与浏览器集成

    • XML 在浏览器中的显示
    • XMLHttpRequest 与 AJAX
    • 服务器端 XML 处理
  • XML 进阶查询与链接

    • XQuery 查询语言
    • XLink 超链接
    • XML 验证工具使用
  • XML Web 服务(选读)

    • XML Web 服务概述
    • SOAP 协议详解
    • WSDL 服务描述
    • RSS 内容聚合
    • RDF 资源描述框架

XML 解析概述

本章定位 :建立 XML 解析技术的全景认知——树模型 vs 流模型,以及三种主流 API(DOM/SAX/StAX)的核心差异和选型决策。

定义与作用

XML 解析器是将 XML 文本转换为程序可操作数据结构的软件组件。你在代码中操作的不是原始 <book> 字符串,而是解析器为你构建的对象或事件序列。

解析模型分为两大类:

  • 树模型(Tree Model) :一次性将整个文档加载到内存,构建完整的节点树。代表:DOM
  • 流模型(Stream Model) :边读边处理,不构建完整树。代表:SAX(推式)、StAX(拉式)

选型的核心决策:内存 vs 灵活性的权衡。

核心原理:三种 API 对比

图解释 :三种 API 按"内存占用 vs 灵活性"排列。DOM 最灵活但最耗内存;SAX 最快最省内存但最不灵活;StAX 是折衷方案——比 SAX 好用,比 DOM 省内存。

语法/结构要点

三种 API 全景对比

特性DOMSAXStAX
模型树模型流模型(推式)流模型(拉式)
内存占用大(整棵树)小(不构建树)小(不构建树)
读取方向随机访问仅向前仅向前
修改能力增删改查只读只读
解析速度慢(构建树)快快
编程复杂度低高(状态机)中
适用场景小型文档、需反复查询/修改超大文档、一次性提取超大文档、按需部分读取
Java APIorg.w3c.domorg.xml.saxjavax.xml.stream

选型决策树

  • 文档 < 10MB 且需要修改? → DOM
  • 文档 > 100MB 只需提取部分信息? → SAX/StAX
  • 需要按条件中途停止? → StAX

完整示例:大翔做解析方案选型

场景说明

飞翔科技的运维 大翔 有 3 个场景需要解析 XML:

  1. 场景 A :一个 5KB 的 web.xml 配置文件,需要增删 servlet 配置
  2. 场景 B :一个 500MB 的服务器日志 XML,只需提取所有 ERROR 级别的 msg 节点文本
  3. 场景 C :一个 200MB 的日志 XML,需要找第一个 status="fail" 的条目后立即停止

大翔的选型

# 场景 A:DOM —— 小型配置,需要修改
from xml.dom import minidom

doc = minidom.parse("web.xml")
servlets = doc.getElementsByTagName("servlet")
# 删除第一个 servlet
first = servlets[0]
first.parentNode.removeChild(first)
print(f"剩余 servlet 数: {len(servlets)-1}")

# 场景 B:SAX —— 超大文件,一次性提取
from xml.sax import parse, ContentHandler

class ErrorHandler(ContentHandler):
    def __init__(self):
        self.in_error = False
    def startElement(self, name, attrs):
        if name == "error":
            self.in_error = True
    def characters(self, content):
        if self.in_error:
            print(f"ERROR: {content.strip()}")
    def endElement(self, name):
        if name == "error":
            self.in_error = False

parse("server_log.xml", ErrorHandler())

# 场景 C:StAX —— 超大文件,找到即停
import xml.etree.ElementTree as ET
# Python 的 iterparse 是类 StAX 拉式解析
for event, elem in ET.iterparse("server_log.xml", events=("start",)):
    if elem.tag == "entry" and elem.get("status") == "fail":
        print(f"第一个失败: {elem.find('msg').text}")
        elem.clear()  # 清理内存
        break

操作结果

大翔用 3 种 API 分别处理了 3 个场景,验证了选型决策:场景 A 用 DOM 方便修改,场景 B 用 SAX 不爆内存,场景 C 用 StAX 找到即停无需解析全文件。

易错场景

错误一:用 DOM 解析超大文件导致 OOM

小崔接到任务——从 500MB 的日志 XML 中提取错误信息。他直接套用平时处理小文件的经验:

# ❌ 错误:DOM 把整个文档加载进内存
from xml.dom import minidom

doc = minidom.parse("server_log_500mb.xml")
errors = doc.getElementsByTagName("error")
for e in errors:
    print(e.firstChild.data)
# 运行结果:MemoryError —— 500MB XML 展开为 DOM 树后内存占用 3~5 倍

问题分析 :DOM 构建完整节点树,每个节点都是独立对象,500MB 的 XML 展开后内存可达 2~3GB。小崔的 8GB 笔记本直接卡死。

# ✅ 正确:用 SAX 流式处理,逐条读取不堆积
from xml.sax import parse, ContentHandler

class ErrorExtractor(ContentHandler):
    def __init__(self):
        self.in_error = False
    def startElement(self, name, attrs):
        if name == "error":
            self.in_error = True
    def characters(self, content):
        if self.in_error:
            print(content.strip())
    def endElement(self, name):
        if name == "error":
            self.in_error = False

parse("server_log_500mb.xml", ErrorExtractor())
# 运行结果:内存稳定在几十 MB,逐条输出全部 error 节点内容

大翔点评 :选型先看文件大小。超过 100MB 就别想 DOM——这不是优化问题,是能不能跑的问题。

错误二:混淆 SAX 的推式模型与 StAX 的拉式模型

白歌面试时问大翔:"SAX 和 StAX 都是流式处理,你怎么区分?"大翔的回答暴露了常见误解——认为两者只是 API 风格不同、本质一样。

问题分析 :

模型控制方类比
SAX(推式)解析器 控制节奏,回调应用自动传送带——你站在终点,来什么接什么
StAX(拉式)应用 控制节奏,主动调用 next()手动提款机——你想取才取,随时可停
# SAX(推式):解析器推数据,你无法控制何时开始/停止读取下一个元素
from xml.sax import parse, ContentHandler

class MyHandler(ContentHandler):
    def startElement(self, name, attrs):
        print(f"收到: {name}")  # 解析器主动触发,你不能"先跳过几个再读"

parse("data.xml", MyHandler())

# StAX(拉式):应用主动拉取,想停就停
import xml.etree.ElementTree as ET

for event, elem in ET.iterparse("data.xml", events=("start",)):
    if elem.tag == "target":
        print(f"找到目标: {elem.text}")
        break  # ✅ 应用主动停止,解析器立即结束

白歌总结 :SAX 是解析器"喂"你数据,StAX 是你自己去"取"数据。"找到即停"的场景只有 StAX 能做到——SAX 无法在回调中途告诉解析器"别再读了"。

错误三:忘记在 StAX/SAX 中释放已处理节点

小崔用 StAX 解析大文件,发现内存还是在涨:

# ❌ 错误:节点不清除,iterparse 会持有所有已解析元素的引用
import xml.etree.ElementTree as ET

count = 0
for event, elem in ET.iterparse("huge_data.xml", events=("end",)):
    if elem.tag == "record":
        count += 1
        # 没有 elem.clear() —— 所有 record 元素全部堆积在内存中
print(f"共 {count} 条记录")
# 运行结果:随着循环进行,内存持续增长,最终可能 OOM

问题分析 :iterparse 虽然边读边解析,但已处理的节点如果不清除,根元素会持续持有它们的引用,形成"隐形 DOM 树"。Java 的 XMLStreamReader 同理——用完后必须 close() 释放底层 I/O 流。

# ✅ 正确:处理完立刻清除节点,切断引用链
import xml.etree.ElementTree as ET

count = 0
for event, elem in ET.iterparse("huge_data.xml", events=("end",)):
    if elem.tag == "record":
        count += 1
        elem.clear()  # ✅ 切断引用,让 GC 回收
print(f"共 {count} 条记录")
# 运行结果:内存稳定在低位,大文件解析毫无压力

大翔提醒 :elem.clear() 是 iterparse 的最佳拍档,千万别忘。另外 Java 中 XMLStreamReader.close() 和 SAXParser 的 InputSource 关闭也同样重要。

错误四:对只向前读取的 SAX/StAX 试图随机访问

小崔用 SAX 解析 XML,读到一半时想返回去重新读之前的节点:

# ❌ 错误:SAX/StAX 的光标只能向前,无法回退
from xml.sax import parse, ContentHandler

class BadHandler(ContentHandler):
    def __init__(self):
        self.first_price = None

    def startElement(self, name, attrs):
        if name == "book" and self.first_price is None:
            # ❌ 先记下第一个 book,等读到第二个 book 时再回头找第一个的价格?
            # 做不到。SAX 的光标已经过去了。
            pass

问题分析 :SAX 和 StAX 的光标只向前移动,就像磁带播放——听过就过了,无法倒带(除非你自己把它录下来)。这与 DOM 的随机访问形成根本区别。

# ✅ 正确:如果后续需要之前的数据,在回调中自己保存
from xml.sax import parse, ContentHandler

class SmartHandler(ContentHandler):
    def __init__(self):
        self.current_tag = ""
        self.current_book = {}
        self.books = []  # ✅ 自己维护一个轻量数据结构

    def startElement(self, name, attrs):
        self.current_tag = name
        if name == "book":
            self.current_book = {"id": attrs.get("id")}

    def characters(self, content):
        if self.current_tag in ("title", "price") and self.current_book is not None:
            self.current_book[self.current_tag] = content.strip()

    def endElement(self, name):
        if name == "book":
            self.books.append(self.current_book)  # ✅ 持久化到列表

parse("books.xml", SmartHandler())
# 现在可以随机访问 self.books 中任意一本书的 title 和 price

白歌总结 :流式解析器只给你一次读取的机会。如果需要随机访问,要么切换到 DOM(小文件),要么在回调中自己维护数据结构(大文件)。没有第三条路。

面试考点

考点参考答案要点
DOM、SAX、StAX 的核心区别?DOM 树模型全文档进内存,支持随机读写;SAX 推式流模型,事件回调,只读前向;StAX 拉式流模型,应用控制读取节奏,可中途停止
处理 500MB XML 文件该用什么解析方式?SAX 或 StAX。DOM 会把整个文件加载到内存导致 OOM。SAX 适合全量提取,StAX 适合按需部分读取
SAX 和 StAX 的"推-拉"区别?SAX 是解析器推数据给应用(回调),应用被动接收;StAX 是应用拉数据(调用 next()),应用主动控制节奏
下一页
DOM 解析