目录
如果您以前曾尝试过用 Python解析XML 文档,那么您就会知道这样的任务有多么困难。一方面,Python之禅承诺只有一种显而易见的方法来实现您的目标。同时,标准库遵循电池包含的座右铭,让您可以从多个 XML 解析器中进行选择。幸运的是,Python 社区通过创建更多 XML 解析库解决了这个过剩问题。
撇开玩笑不谈,所有 XML 解析器在充满或大或小的挑战的世界中都有自己的位置。熟悉可用的工具是值得的。
在本教程中,您将学习如何:
- 选择正确的 XML解析模型
- 使用标准库中的 XML 解析器
- 使用主要的 XML 解析库
- 使用数据绑定以声明方式解析 XML 文档
- 使用安全的 XML 解析器消除安全漏洞
您可以将本教程用作路线图,引导您了解 Python 中 XML 解析器的混乱世界。到最后,您将能够为给定的问题选择正确的 XML 解析器。要从本教程中获得最大收益,您应该已经熟悉XML及其构建块,以及如何在 Python 中处理文件。
选择正确的 XML 解析模型
事实证明,您可以使用一些与语言无关的策略来处理 XML 文档。每个都展示了不同的内存和速度权衡,这可以部分证明 Python 中可用的各种 XML 解析器是合理的。在下一节中,您将了解它们的差异和优势。
文档对象模型 (DOM)
从历史上看,第一个也是最广泛使用的解析 XML 的模型是 DOM,即文档对象模型,最初由万维网联盟 (W3C) 定义。您可能已经听说过 DOM,因为 Web 浏览器通过JavaScript公开了一个 DOM 接口,让您可以操作网站的 HTML 代码。XML 和 HTML 都属于同一个标记语言家族,这使得使用 DOM 解析 XML 成为可能。
DOM 可以说是最直接、最通用的模型。它定义了一些标准操作,用于遍历和修改以对象层次结构排列的文档元素。整个文档树的抽象表示存储在内存中,让您可以随机访问各个元素。
虽然 DOM 树允许快速和全方位的导航,但首先构建其抽象表示可能非常耗时。此外,作为一个整体,XML 会立即被解析,因此它必须相当小以适应可用内存。这使得 DOM 仅适用于中等大小的配置文件,而不适用于数 GB 的XML 数据库。
当方便比处理时间更重要并且内存不是问题时,请使用 DOM 解析器。一些典型的用例是当您需要解析一个相对较小的文档时,或者您只需要很少进行解析时。
XML 的简单 API (SAX)
为了解决 DOM 的缺点,Java 社区通过协作提出了一个库,该库随后成为解析其他语言 XML 的替代模型。没有正式的规范,只有邮件列表上的有机讨论。最终结果是一个基于事件的流 API,它对单个元素而不是整个树按顺序运行。
元素按照它们在文档中出现的相同顺序从上到下进行处理。解析器触发用户定义的回调来处理在文档中找到的特定 XML 节点。这种方法被称为“推送”解析,因为元素被解析器推送到您的函数。
如果您对元素不感兴趣,SAX 还允许您丢弃它们。这意味着它的内存占用比 DOM 低得多,并且可以处理任意大的文件,这对于索引、转换为其他格式等单程处理非常有用。
然而,查找或修改随机树节点很麻烦,因为它通常需要对文档进行多次传递并跟踪访问过的节点。SAX 也不方便处理深度嵌套的元素。最后,SAX 模型只允许只读解析。
简而言之,SAX 在空间和时间上都很便宜,但在大多数情况下比 DOM 更难使用。它适用于解析非常大的文档或实时解析传入的 XML 数据。
XML 流 API (StAX)
尽管在 Python 中不太流行,但解析 XML 的第三种方法构建在 SAX 之上。它扩展了流的思想,但使用了“拉”解析模型,这给了你更多的控制。您可以将 StAX 视为通过 XML 文档推进游标对象的迭代器,其中自定义处理程序按需调用解析器,而不是相反。
注意:可以组合多个 XML 解析模型。例如,您可以使用 SAX 或 StAX 快速找到文档中有趣的一段数据,然后在内存中构建仅该特定分支的 DOM 表示。
使用 StAX 可以让您更好地控制解析过程并允许更方便的状态管理。流中的事件仅在请求时使用,从而启用延迟评估。除此之外,它的性能应该与 SAX 相当,这取决于解析器的实现。
了解 Python 标准库中的 XML 解析器
在本节中,您将了解 Python 的内置 XML 解析器,您几乎可以在每个 Python 发行版中使用这些解析器。您将把这些解析器与示例可缩放矢量图形 (SVG)图像进行比较,这是一种基于 XML 的格式。通过使用不同的解析器处理相同的文档,您将能够选择最适合您的文档。
您将要保存在本地文件中以供参考的示例图像描绘了一张笑脸。它由以下 XML 内容组成:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY custom_entity "Hello">
]>
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="-105 -100 210 270" width="210" height="270">
<inkscape:custom x="42" inkscape:z="555">Some value</inkscape:custom>
<defs>
<linearGradient id="skin" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="yellow" stop-opacity="1.0"/>
<stop offset="75%" stop-color="gold" stop-opacity="1.0"/>
<stop offset="100%" stop-color="orange" stop-opacity="1"/>
</linearGradient>
</defs>
<g id="smiley" inkscape:groupmode="layer" inkscape:label="Smiley">
<!-- Head -->
<circle cx="0" cy="0" r="50"
fill="url(#skin)" stroke="orange" stroke-width="2"/>
<!-- Eyes -->
<ellipse cx="-20" cy="-10" rx="6" ry="8" fill="black" stroke="none"/>
<ellipse cx="20" cy="-10" rx="6" ry="8" fill="black" stroke="none"/>
<!-- Mouth -->
<path d="M-20 20 A25 25 0 0 0 20 20"
fill="white" stroke="black" stroke-width="3"/>
</g>
<text x="-40" y="75">&custom_entity; <svg>!</text>
<script>
<![CDATA[
console.log("CDATA disables XML parsing: <svg>")
const smiley = document.getElementById("smiley")
const eyes = document.querySelectorAll("ellipse")
const setRadius = r => e => eyes.forEach(x => x.setAttribute("ry", r))
smiley.addEventListener("mouseenter", setRadius(2))
smiley.addEventListener("mouseleave", setRadius(8))
]]>
</script>
</svg>
它以XML 声明开头,然后是文档类型定义 (DTD)和<svg>
根元素。DTD 是可选的,但如果您决定使用 XML 验证器,它可以帮助验证您的文档结构。根元素为特定于编辑器的元素和属性指定默认命名空间 xmlns
以及带前缀的命名空间 xmlns:inkscape
。该文件还包含:
- 嵌套元素
- 属性
- 注释
- 字符数据 (
CDATA
) - 预定义和自定义实体
继续,将 XML 保存在一个名为smiley.svg的文件中,然后使用现代 Web 浏览器打开它,这将运行最后出现的 JavaScript 代码段:
该代码向图像添加了一个交互式组件。当您将鼠标悬停在笑脸上时,它会眨眼。如果您想使用方便的图形用户界面 (GUI) 编辑笑脸,则可以使用矢量图形编辑器(例如Adobe Illustrator或Inkscape)打开文件。
注意:与 JSON 或 YAML 不同,XML 的某些功能可以被黑客利用。xml
Python 包中提供的标准 XML 解析器不安全且容易受到一系列攻击。要从不受信任的来源安全地解析 XML 文档,请首选安全的替代方案。您可以跳到本教程的最后一部分以了解更多详细信息。
值得注意的是,Python 的标准库定义了用于解析 XML 文档的抽象接口,同时让您提供具体的解析器实现。在实践中,您很少这样做,因为 Python 捆绑了Expat库的绑定,这是一种广泛使用的开源 XML 解析器,用 C 编写。标准库中的以下所有 Python 模块默认使用 Expat。
不幸的是,虽然 Expat 解析器可以告诉您文档是否格式正确,但它无法根据XML 架构定义 (XSD)或文档类型定义 (DTD)验证文档的结构。为此,您必须使用稍后讨论的第三方库之一。
xml.dom.minidom
: 最小的 DOM 实现
考虑到使用 DOM 解析 XML 文档可以说是最直接的方式,您不会对在 Python 标准库中找到 DOM 解析器感到惊讶。然而,令人惊讶的是,实际上有两个 DOM 解析器。
该xml.dom
包包含两个模块以在 Python 中处理 DOM:
xml.dom.minidom
xml.dom.pulldom
第一个是符合 W3C 规范相对较旧版本的 DOM 接口的精简实现。它提供了通过DOM API诸如定义的通用对象Document
,Element
以及Attr
。正如您即将发现的那样,该模块的文档很差,而且实用性非常有限。
第二个模块的名称有点误导,因为它定义了一个流式拉取解析器,它可以选择生成文档树中当前节点的 DOM 表示。你会发现有关的更多信息pulldom
分析器后。
有两个函数minidom
可以让您解析来自各种数据源的 XML 数据。一个接受文件名或文件对象,而另一个接受Python 字符串:
>>> from xml.dom.minidom import parse, parseString
>>> # Parse XML from a filename
>>> document = parse("smiley.svg")
>>> # Parse XML from a file object
>>> with open("smiley.svg") as file:
... document = parse(file)
...
>>> # Parse XML from a Python string
>>> document = parseString("""\
... <svg viewBox="-105 -100 210 270">
... <!-- More content goes here... -->
... </svg>
... """)
的三重引号字符串有助于嵌入多行字符串文字不使用继续符(\
)中的每一行的末尾。在任何情况下,您最终都会得到一个Document
实例,它展示了熟悉的 DOM 接口,让您可以遍历树。
除此之外,您将能够访问 XML 声明、DTD 和根元素:
>>> document = parse("smiley.svg")
>>> # XML Declaration
>>> document.version, document.encoding, document.standalone
('1.0', 'UTF-8', False)
>>> # Document Type Definition (DTD)
>>> dtd = document.doctype
>>> dtd.entities["custom_entity"].childNodes
[<DOM Text node "'Hello'">]
>>> # Document Root
>>> document.documentElement
<DOM Element: svg at 0x7fc78c62d790>
如您所见,即使 Python 中的默认 XML 解析器无法验证文档,它仍然允许您检查.doctype
DTD(如果存在)。请注意,XML 声明和 DTD 是可选的。如果缺少 XML 声明或给定的 XML 属性,则相应的 Python 属性将为None
.
要按 ID 查找元素,您必须使用Document
实例而不是特定的 parent Element
。示例 SVG 图像有两个具有id
属性的节点,但您找不到其中任何一个:
>>> document.getElementById("skin") is None
True
>>> document.getElementById("smiley") is None
True
对于只使用过 HTML 和 JavaScript 但以前从未使用过 XML 的人来说,这可能会令人惊讶。虽然 HTML 定义了某些元素和属性(例如<body>
或 )的语义id
,但 XML 没有为其构建块附加任何意义。您需要使用 DTD 或通过.setIdAttribute()
在 Python 中调用来显式地将属性标记为 ID ,例如:
定义风格 | 执行 |
---|---|
DTD | <!--ATTLIST linearGradient id ID #IMPLIED--> |
Python | linearGradient.setIdAttribute("id") |
但是,如果您的文档具有默认命名空间(示例 SVG 图像就是这种情况),则使用 DTD 并不足以解决问题。为了解决这个问题,你可以在 Python 中递归访问所有元素,检查它们是否具有该id
属性,并一次性将其指示为它们的 ID:
>>> from xml.dom.minidom import parse, Node
>>> def set_id_attribute(parent, attribute_name="id"):
... if parent.nodeType == Node.ELEMENT_NODE:
... if parent.hasAttribute(attribute_name):
... parent.setIdAttribute(attribute_name)
... for child in parent.childNodes:
... set_id_attribute(child, attribute_name)
...
>>> document = parse("smiley.svg")
>>> set_id_attribute(document)
您的自定义set_id_attribute()
函数采用一个父元素和一个可选的身份属性名称,默认为"id"
。当您在 SVG 文档上调用该函数时,所有具有id
属性的子元素都可以通过 DOM API 访问:
>>> document.getElementById("skin")
<DOM Element: linearGradient at 0x7f82247703a0>
>>> document.getElementById("smiley")
<DOM Element: g at 0x7f8224770940>
现在,您将获得与id
属性值对应的预期 XML 元素。
使用 ID 最多只能找到一个唯一元素,但您也可以通过标签名称找到一组相似元素。与.getElementById()
方法不同,您可以调用.getElementsByTagName()
文档或特定的父元素来缩小搜索范围:
>>> document.getElementsByTagName("ellipse")
[
<DOM Element: ellipse at 0x7fa2c944f430>,
<DOM Element: ellipse at 0x7fa2c944f4c0>
]
>>> root = document.documentElement
>>> root.getElementsByTagName("ellipse")
[
<DOM Element: ellipse at 0x7fa2c944f430>,
<DOM Element: ellipse at 0x7fa2c944f4c0>
]
请注意,.getElementsByTagName()
始终返回元素列表而不是单个元素或None
。在两种方法之间切换时忘记它是常见的错误来源。
不幸的是,不会包含<inkscape:custom>
这样的以命名空间标识符为前缀的元素。必须使用 搜索它们.getElementsByTagNameNS()
,它需要不同的参数:
>>> document.getElementsByTagNameNS(
... "http://www.inkscape.org/namespaces/inkscape",
... "custom"
... )
...
[<DOM Element: inkscape:custom at 0x7f97e3f2a3a0>]
>>> document.getElementsByTagNameNS("*", "custom")
[<DOM Element: inkscape:custom at 0x7f97e3f2a3a0>]
第一个参数必须是 XML 命名空间,它通常具有域名的形式,而第二个参数是标记名称。请注意,名称空间前缀无关紧要!要搜索所有命名空间,您可以提供通配符 ( *
)。
注意:要查找在您的 XML 文档中声明的名称空间,您可以查看根元素的属性。理论上,它们可以在任何元素上声明,但顶级元素是您通常可以找到它们的地方。
一旦你找到你感兴趣的元素,你就可以用它来遍历树。然而,另一个令人不快的怪癖minidom
是它如何处理元素之间的空白字符:
>>> element = document.getElementById("smiley")
>>> element.parentNode
<DOM Element: svg at 0x7fc78c62d790>
>>> element.firstChild
<DOM Text node "'\n '">
>>> element.lastChild
<DOM Text node "'\n '">
>>> element.nextSibling
<DOM Text node "'\n '">
>>> element.previousSibling
<DOM Text node "'\n '">
换行符和前导缩进被捕获为单独的树元素,这是规范所要求的。一些解析器让你忽略这些,但不是 Python 的。但是,您可以手动折叠此类节点中的空白:
>>> def remove_whitespace(node):
... if node.nodeType == Node.TEXT_NODE:
... if node.nodeValue.strip() == "":
... node.nodeValue = ""
... for child in node.childNodes:
... remove_whitespace(child)
...
>>> document = parse("smiley.svg")
>>> set_id_attribute(document)
>>> remove_whitespace(document)
>>> document.normalize()
请注意,您还必须.normalize()
将文档合并相邻的文本节点。否则,您最终可能会得到一堆只有空格的冗余 XML 元素。同样,递归是访问树元素的唯一方法,因为您无法使用循环遍历文档及其元素。最后,这应该给你预期的结果:
>>> element = document.getElementById("smiley")
>>> element.parentNode
<DOM Element: svg at 0x7fc78c62d790>
>>> element.firstChild
<DOM Comment node "' Head '">
>>> element.lastChild
<DOM Element: path at 0x7f8beea0f670>
>>> element.nextSibling
<DOM Element: text at 0x7f8beea0f700>
>>> element.previousSibling
<DOM Element: defs at 0x7f8beea0f160>
>>> element.childNodes
[
<DOM Comment node "' Head '">,
<DOM Element: circle at 0x7f8beea0f4c0>,
<DOM Comment node "' Eyes '">,
<DOM Element: ellipse at 0x7fa2c944f430>,
<DOM Element: ellipse at 0x7fa2c944f4c0>,
<DOM Comment node "' Mouth '">,
<DOM Element: path at 0x7f8beea0f670>
]
Elements 公开了一些有用的方法和属性,让您可以查询它们的详细信息:
>>> element = document.getElementsByTagNameNS("*", "custom")[0]
>>> element.prefix
'inkscape'
>>> element.tagName
'inkscape:custom'
>>> element.attributes
<xml.dom.minidom.NamedNodeMap object at 0x7f6c9d83ba80>
>>> dict(element.attributes.items())
{'x': '42', 'inkscape:z': '555'}
>>> element.hasChildNodes()
True
>>> element.hasAttributes()
True
>>> element.hasAttribute("x")
True
>>> element.getAttribute("x")
'42'
>>> element.getAttributeNode("x")
<xml.dom.minidom.Attr object at 0x7f82244a05f0>
>>> element.getAttribute("missing-attribute")
''
例如,您可以检查元素的命名空间、标签名称或属性。如果您要求缺少的属性,那么您将得到一个空字符串 ( ''
)。
处理命名空间属性并没有太大不同。您只需要记住相应地为属性名称添加前缀或提供域名:
>>> element.hasAttribute("z")
False
>>> element.hasAttribute("inkscape:z")
True
>>> element.hasAttributeNS(
... "http://www.inkscape.org/namespaces/inkscape",
... "z"
... )
...
True
>>> element.hasAttributeNS("*", "z")
False
奇怪的是,通配符 ( *
) 在这里并不像以前的.getElementsByTagNameNS()
方法那样起作用。
由于本教程仅涉及 XML 解析,因此您需要查看minidom
文档以了解修改 DOM 树的方法。它们大多遵循 W3C 规范。
如您所见,该minidom
模块并不是非常方便。它的主要优点是作为标准库的一部分,这意味着您无需在项目中安装任何外部依赖项即可使用 DOM。
xml.sax
:Python 的 SAX 接口
要开始在 Python 中使用 SAX,您可以使用与以前相同parse()
且parseString()
方便的函数,但要使用xml.sax
包中的函数。您还必须至少提供一个更多的必需参数,它必须是一个内容处理程序实例。本着 Java 的精神,您可以通过对特定基类进行子类化来提供一个:
from xml.sax import parse
from xml.sax.handler import ContentHandler
class SVGHandler(ContentHandler):
pass
parse("smiley.svg", SVGHandler())
内容处理程序在解析文档时接收与文档中的元素相对应的事件流。运行此代码不会做任何有用的事情,因为您的处理程序类是空的。要使其工作,您需要从超类中重载一个或多个回调方法。
启动您最喜欢的编辑器,键入以下代码,并将其保存在名为 的文件中svg_handler.py
:
# svg_handler.py
from xml.sax.handler import ContentHandler
class SVGHandler(ContentHandler):
def startElement(self, name, attrs):
print(f"BEGIN: <{name}>, {attrs.keys()}")
def endElement(self, name):
print(f"END: </{name}>")
def characters(self, content):
if content.strip() != "":
print("CONTENT:", repr(content))
这个修改后的内容处理程序将一些事件打印到标准输出上。SAX 解析器将为您调用这三个方法,以响应查找开始标记、结束标记和它们之间的一些文本。当您打开 Python 解释器的交互式会话时,导入您的内容处理程序并对其进行测试。它应该产生以下输出:
>>> from xml.sax import parse
>>> from svg_handler import SVGHandler
>>> parse("smiley.svg", SVGHandler())
BEGIN: <svg>, ['xmlns', 'xmlns:inkscape', 'viewBox', 'width', 'height']
BEGIN: <inkscape:custom>, ['x', 'inkscape:z']
CONTENT: 'Some value'
END: </inkscape:custom>
BEGIN: <defs>, []
BEGIN: <linearGradient>, ['id', 'x1', 'x2', 'y1', 'y2']
BEGIN: <stop>, ['offset', 'stop-color', 'stop-opacity']
END: </stop>
⋮
这本质上是观察者设计模式,它允许您逐步将 XML 转换为另一种分层格式。假设您想将该 SVG 文件转换为简化的JSON表示。首先,您需要将内容处理程序对象存储在一个单独的变量中,以便稍后从中提取信息:
>>> from xml.sax import parse
>>> from svg_handler import SVGHandler
>>> handler = SVGHandler()
>>> parse("smiley.svg", handler)
由于 SAX 解析器发出事件而不提供有关它找到的元素的任何上下文,因此您需要跟踪您在树中的位置。因此,将当前元素推入和弹出到堆栈中是有意义的,您可以通过常规Python 列表来模拟。您还可以定义一个辅助属性.current_element
,该属性将返回放置在堆栈顶部的最后一个元素:
# svg_handler.py
# ...
class SVGHandler(ContentHandler):
def __init__(self):
super().__init__()
self.element_stack = []
@property
def current_element(self):
return self.element_stack[-1]
# ...
当 SAX 解析器发现一个新元素时,您可以立即捕获其标记名称和属性,同时为子元素和值制作占位符,这两者都是可选的。现在,您可以将每个元素存储为一个dict
对象。用.startElement()
新的实现替换现有的方法:
# svg_handler.py
# ...
class SVGHandler(ContentHandler):
# ...
def startElement(self, name, attrs):
self.element_stack.append({
"name": name,
"attributes": dict(attrs),
"children": [],
"value": ""
})
SAX 解析器为您提供属性作为映射,您可以通过调用函数将其转换为普通Python 字典dict()
。元素值通常分布在多个部分上,您可以使用加号运算符 ( +
) 或相应的扩充赋值语句将这些部分连接起来:
# svg_handler.py
# ...
class SVGHandler(ContentHandler):
# ...
def characters(self, content):
self.current_element["value"] += content
以这种方式聚合文本将确保多行内容最终出现在当前元素中。例如,<script>
示例 SVG 文件中的标记包含六行 JavaScript 代码,这些代码会触发对characters()
回调的单独调用。
最后,一旦解析器偶然发现结束标记,您就可以从堆栈中弹出当前元素并将其附加到其父元素的子元素。如果只剩下一个元素,那么它将是您文档的根目录,您应该保留以备后用。除此之外,您可能希望通过删除具有空值的键来清理当前元素:
# svg_handler.py
# ...
class SVGHandler(ContentHandler):
# ...
def endElement(self, name):
clean(self.current_element)
if len(self.element_stack) > 1:
child = self.element_stack.pop()
self.current_element["children"].append(child)
def clean(element):
element["value"] = element["value"].strip()
for key in ("attributes", "children", "value"):
if not element[key]:
del element[key]
请注意,这clean()
是在类主体之外定义的函数。必须在最后进行清理,因为无法预先知道可能要连接的文本片段数量。您可以展开下面的可折叠部分以获得完整的内容处理程序代码。
用于 SVG 到 JSON 转换器的 SAX 处理程序显示隐藏
现在,是时候通过解析 XML、从内容处理程序中提取根元素并将其转储为 JSON 字符串来测试所有内容:
>>> from xml.sax import parse
>>> from svg_handler import SVGHandler
>>> handler = SVGHandler()
>>> parse("smiley.svg", handler)
>>> root = handler.current_element
>>> import json
>>> print(json.dumps(root, indent=4))
{
"name": "svg",
"attributes": {
"xmlns": "http://www.w3.org/2000/svg",
"xmlns:inkscape": "http://www.inkscape.org/namespaces/inkscape",
"viewBox": "-105 -100 210 270",
"width": "210",
"height": "270"
},
"children": [
{
"name": "inkscape:custom",
"attributes": {
"x": "42",
"inkscape:z": "555"
},
"value": "Some value"
},
⋮
值得注意的是,此实现与 DOM 相比没有内存增益,因为它像以前一样构建了整个文档的抽象表示。不同之处在于您制作了自定义字典表示而不是标准 DOM 树。但是,您可以想象在接收 SAX 事件时直接写入文件或数据库而不是内存。这将有效地解除您的计算机内存限制。
如果您想解析 XML 名称空间,那么您需要使用一些样板代码自己创建和配置 SAX 解析器,并实现略有不同的回调:
# svg_handler.py
from xml.sax.handler import ContentHandler
class SVGHandler(ContentHandler):
def startPrefixMapping(self, prefix, uri):
print(f"startPrefixMapping: {prefix=}, {uri=}")
def endPrefixMapping(self, prefix):
print(f"endPrefixMapping: {prefix=}")
def startElementNS(self, name, qname, attrs):
print(f"startElementNS: {name=}")
def endElementNS(self, name, qname):
print(f"endElementNS: {name=}")
这些回调接收有关元素命名空间的附加参数。要使 SAX 解析器实际触发这些回调而不是一些早期的回调,您必须显式启用XML 命名空间支持:
>>> from xml.sax import make_parser
>>> from xml.sax.handler import feature_namespaces
>>> from svg_handler import SVGHandler
>>> parser = make_parser()
>>> parser.setFeature(feature_namespaces, True)
>>> parser.setContentHandler(SVGHandler())
>>> parser.parse("smiley.svg")
startPrefixMapping: prefix=None, uri='http://www.w3.org/2000/svg'
startPrefixMapping: prefix='inkscape', uri='http://www.inkscape.org/namespaces/inkscape'
startElementNS: name=('http://www.w3.org/2000/svg', 'svg')
⋮
endElementNS: name=('http://www.w3.org/2000/svg', 'svg')
endPrefixMapping: prefix='inkscape'
endPrefixMapping: prefix=None
设置此功能会将元素name
转换为由命名空间的域名和标签名称组成的元组。
该xml.sax
包提供了一个不错的基于事件的 XML 解析器接口,该接口以原始 Java API 为模型。与 DOM 相比,它有些限制,但应该足以实现基本的 XML 流推送解析器,而无需求助于第三方库。考虑到这一点,Python 中提供了一个不那么冗长的拉式解析器,接下来您将对其进行探索。
xml.dom.pulldom
: 流式拉取解析器
Python 标准库中的解析器经常一起工作。例如,该xml.dom.pulldom
模块包装解析器xml.sax
以利用缓冲并分块读取文档。同时,它使用默认的 DOM 实现 fromxml.dom.minidom
来表示文档元素。但是,在您明确要求之前,这些元素一次处理一个,而不会产生任何关系。
注意: XML 命名空间支持在xml.dom.pulldom
.
虽然 SAX 模型遵循观察者模式,但您可以将 StAX 视为迭代器设计模式,它使您可以循环事件的平面流。再次,您可以调用从模块中导入的熟悉的parse()
或parseString()
函数来解析 SVG 图像:
>>> from xml.dom.pulldom import parse
>>> event_stream = parse("smiley.svg")
>>> for event, node in event_stream:
... print(event, node)
...
START_DOCUMENT <xml.dom.minidom.Document object at 0x7f74f9283e80>
START_ELEMENT <DOM Element: svg at 0x7f74fde18040>
CHARACTERS <DOM Text node "'\n'">
⋮
END_ELEMENT <DOM Element: script at 0x7f74f92b3c10>
CHARACTERS <DOM Text node "'\n'">
END_ELEMENT <DOM Element: svg at 0x7f74fde18040>
解析文档只需要几行代码。xml.sax
和之间最显着的区别xml.dom.pulldom
是缺少回调,因为你推动了整个过程。您在构建代码时有更多的自由,如果您不想,也不需要使用类。
请注意,从流中提取的 XML 节点具有在 中定义的类型xml.dom.minidom
。但是,如果您查看他们的父母、兄弟姐妹和孩子,您会发现他们对彼此一无所知:
>>> from xml.dom.pulldom import parse, START_ELEMENT
>>> event_stream = parse("smiley.svg")
>>> for event, node in event_stream:
... if event == START_ELEMENT:
... print(node.parentNode, node.previousSibling, node.childNodes)
<xml.dom.minidom.Document object at 0x7f90864f6e80> None []
None None []
None None []
None None []
⋮
相关属性为空。无论如何,pull 解析器可以通过混合方法帮助快速查找一些父元素并仅为根在其中的分支构建 DOM 树:
from xml.dom.pulldom import parse, START_ELEMENT
def process_group(parent):
left_eye, right_eye = parent.getElementsByTagName("ellipse")
# ...
event_stream = parse("smiley.svg")
for event, node in event_stream:
if event == START_ELEMENT:
if node.tagName == "g":
event_stream.expandNode(node)
process_group(node)
通过调用.expandNode()
事件流,您实质上是向前移动迭代器并递归地解析 XML 节点,直到找到父元素的匹配结束标记为止。结果节点将具有正确初始化属性的子节点。此外,您将能够对它们使用 DOM 方法。
通过结合两者的优点,pull 解析器为 DOM 和 SAX 提供了一个有趣的替代方案。它高效、灵活且易于使用,从而生成更紧凑和可读的代码。您还可以使用它更轻松地同时处理多个 XML 文件。也就是说,到目前为止提到的 XML 解析器都无法与 Python 标准库中的最后一个解析器相媲美。
xml.etree.ElementTree
:一个轻量级的 Pythonic 替代方案
到目前为止,您所了解的 XML 解析器可以完成工作。但是,它们不太符合 Python 的哲学,这并非偶然。虽然 DOM 遵循 W3C 规范并且 SAX 是根据 Java API 建模的,但两者都没有特别 Pythonic 的感觉。
更糟糕的是,DOM 和 SAX 解析器都感觉过时了,因为它们在CPython解释器中的一些代码已经超过二十年没有改变了!在撰写本文时,它们的实现仍然不完整,并且缺少 typeshed 存根,这会破坏代码编辑器中的代码完成。
同时,Python 2.5 为解析和编写 XML 文档带来了全新的视角——ElementTree API。它是一个轻量级、高效、优雅且功能丰富的界面,甚至一些第三方库也建立在它的基础上。要开始使用它,您必须导入xml.etree.ElementTree
模块,这有点麻烦。因此,习惯上这样定义别名:
import xml.etree.ElementTree as ET
在稍旧的代码中,您可能已经看到cElementTree
导入的模块。它的实现速度比用 C 编写的相同接口快几倍。今天,常规模块尽可能使用快速实现,因此您无需再费心了。
您可以通过采用不同的解析策略来使用 ElementTree API:
非增量 | 增量(阻塞) | 增量(非阻塞) | |
---|---|---|---|
ET.parse() |
✔️ | ||
ET.fromstring() |
✔️ | ||
ET.iterparse() |
✔️ | ||
ET.XMLPullParser |
✔️ |
非增量策略以类似DOM 的方式将整个文档加载到内存中。模块中有两个适当命名的函数,它们允许解析带有 XML 内容的文件或 Python 字符串:
>>> import xml.etree.ElementTree as ET
>>> # Parse XML from a filename
>>> ET.parse("smiley.svg")
<xml.etree.ElementTree.ElementTree object at 0x7fa4c980a6a0>
>>> # Parse XML from a file object
>>> with open("smiley.svg") as file:
... ET.parse(file)
...
<xml.etree.ElementTree.ElementTree object at 0x7fa4c96df340>
>>> # Parse XML from a Python string
>>> ET.fromstring("""\
... <svg viewBox="-105 -100 210 270">
... <!-- More content goes here... -->
... </svg>
... """)
<Element 'svg' at 0x7fa4c987a1d0>
解析文件对象或文件名并parse()
返回ET.ElementTree
类的实例,该实例代表整个元素层次结构。另一方面,解析字符串 withfromstring()
将返回特定的 root ET.Element
。
或者,您可以使用流式拉式解析器以增量方式读取 XML 文档,这会产生一系列事件和元素:
>>> for event, element in ET.iterparse("smiley.svg"):
... print(event, element.tag)
...
end {http://www.inkscape.org/namespaces/inkscape}custom
end {http://www.w3.org/2000/svg}stop
end {http://www.w3.org/2000/svg}stop
end {http://www.w3.org/2000/svg}stop
end {http://www.w3.org/2000/svg}linearGradient
⋮
默认情况下,iterparse()
仅发出end
与关闭 XML 标记关联的事件。但是,您也可以订阅其他事件。您可以使用字符串常量找到它们,例如"comment"
:
>>> import xml.etree.ElementTree as ET
>>> for event, element in ET.iterparse("smiley.svg", ["comment"]):
... print(element.text.strip())
...
Head
Eyes
Mouth
以下是所有可用事件类型的列表:
start
:元素的开始end
:元素的结尾comment
:评论元素pi
:处理指令,如XSLstart-ns
:命名空间的开始end-ns
:命名空间的结尾
缺点iterparse()
是它使用阻塞调用来读取下一个数据块,这可能不适合在单个执行线程上运行的异步代码。为了缓解这种情况,您可以查看XMLPullParser
,它有点冗长:
import xml.etree.ElementTree as ET
async def receive_data(url):
"""Download chunks of bytes from the URL asynchronously."""
yield b"<svg "
yield b"viewBox=\"-105 -100 210 270\""
yield b"></svg>"
async def parse(url, events=None):
parser = ET.XMLPullParser(events)
async for chunk in receive_data(url):
parser.feed(chunk)
for event, element in parser.read_events():
yield event, element
这个假设示例为解析器提供了可能相隔几秒钟到达的 XML 块。一旦有足够的内容,您就可以迭代解析器缓冲的一系列事件和元素。这种非阻塞增量解析策略允许在您下载多个 XML 文档时真正同时实时解析它们。
树中的元素是可变的、可迭代的和可索引的序列。它们的长度对应于它们的直系子节点的数量:
>>> import xml.etree.ElementTree as ET
>>> tree = ET.parse("smiley.svg")
>>> root = tree.getroot()
>>> # The length of an element equals the number of its children.
>>> len(root)
5
>>> # The square brackets let you access a child by an index.
>>> root[1]
<Element '{http://www.w3.org/2000/svg}defs' at 0x7fe05d2e8860>
>>> root[2]
<Element '{http://www.w3.org/2000/svg}g' at 0x7fa4c9848400>
>>> # Elements are mutable. For example, you can swap their children.
>>> root[2], root[1] = root[1], root[2]
>>> # You can iterate over an element's children.
>>> for child in root:
... print(child.tag)
...
{http://www.inkscape.org/namespaces/inkscape}custom
{http://www.w3.org/2000/svg}g
{http://www.w3.org/2000/svg}defs
{http://www.w3.org/2000/svg}text
{http://www.w3.org/2000/svg}script
标记名称的前缀可能是由一对花括号 ( {}
)括起来的可选命名空间。定义时,默认的 XML 命名空间也会出现在那里。请注意突出显示行中的交换分配如何使<g>
元素位于之前<defs>
。这显示了序列的可变性质。
这里还有一些值得一提的元素属性和方法:
>>> element = root[0]
>>> element.tag
'{http://www.inkscape.org/namespaces/inkscape}custom'
>>> element.text
'Some value'
>>> element.attrib
{'x': '42', '{http://www.inkscape.org/namespaces/inkscape}z': '555'}
>>> element.get("x")
'42'
此 API 的好处之一是它如何使用 Python 的本机数据类型。上面,它使用 Python 字典来表示元素的属性。在之前的模块中,这些被包装在不太方便的适配器中。与 DOM 不同,ElementTree API 不公开用于在任何方向遍历树的方法或属性,但有几个更好的替代方法。
正如您之前看到的,Element
该类的实例实现了序列协议,让您可以使用循环遍历它们的直接子代:
>>> for child in root:
... print(child.tag)
...
{http://www.inkscape.org/namespaces/inkscape}custom
{http://www.w3.org/2000/svg}defs
{http://www.w3.org/2000/svg}g
{http://www.w3.org/2000/svg}text
{http://www.w3.org/2000/svg}script
你得到根的直接孩子的序列。但是,要更深入地了解嵌套后代,您必须.iter()
在祖先元素上调用该方法:
>>> for descendant in root.iter():
... print(descendant.tag)
...
{http://www.w3.org/2000/svg}svg
{http://www.inkscape.org/namespaces/inkscape}custom
{http://www.w3.org/2000/svg}defs
{http://www.w3.org/2000/svg}linearGradient
{http://www.w3.org/2000/svg}stop
{http://www.w3.org/2000/svg}stop
{http://www.w3.org/2000/svg}stop
{http://www.w3.org/2000/svg}g
{http://www.w3.org/2000/svg}circle
{http://www.w3.org/2000/svg}ellipse
{http://www.w3.org/2000/svg}ellipse
{http://www.w3.org/2000/svg}path
{http://www.w3.org/2000/svg}text
{http://www.w3.org/2000/svg}script
根元素只有五个子元素,但总共有 13 个子元素。还可以通过使用可选参数仅过滤特定标签名称来缩小后代范围tag
:
>>> tag_name = "{http://www.w3.org/2000/svg}ellipse"
>>> for descendant in root.iter(tag_name):
... print(descendant)
...
<Element '{http://www.w3.org/2000/svg}ellipse' at 0x7f430baa03b0>
<Element '{http://www.w3.org/2000/svg}ellipse' at 0x7f430baa0450>
这一次,你只有两个<ellipse>
元素。请记住在标记名称中包含XML 名称空间,例如{http://www.w3.org/2000/svg}
,只要它已被定义。否则,如果您只提供标签名称而没有正确的命名空间,您最终会得到比最初预期更少或更多的后代元素。
使用 时处理命名空间更方便.iterfind()
,它接受前缀到域名的可选映射。要指示默认命名空间,您可以将键留空或分配任意前缀,稍后必须在标签名称中使用:
>>> namespaces = {
... "": "http://www.w3.org/2000/svg",
... "custom": "http://www.w3.org/2000/svg"
... }
>>> for descendant in root.iterfind("g", namespaces):
... print(descendant)
...
<Element '{http://www.w3.org/2000/svg}g' at 0x7f430baa0270>
>>> for descendant in root.iterfind("custom:g", namespaces):
... print(descendant)
...
<Element '{http://www.w3.org/2000/svg}g' at 0x7f430baa0270>
命名空间映射允许您使用不同的前缀引用相同的元素。令人惊讶的是,如果您<ellipse>
像以前一样尝试查找那些嵌套元素,则.iterfind()
不会返回任何内容,因为它需要一个XPath 表达式而不是一个简单的标记名称:
>>> for descendant in root.iterfind("ellipse", namespaces):
... print(descendant)
...
>>> for descendant in root.iterfind("g/ellipse", namespaces):
... print(descendant)
...
<Element '{http://www.w3.org/2000/svg}ellipse' at 0x7f430baa03b0>
<Element '{http://www.w3.org/2000/svg}ellipse' at 0x7f430baa0450>
巧合的是,该字符串"g"
恰好是相对于当前root
元素的有效路径,这就是该函数之前返回非空结果的原因。但是,要在 XML 层次结构中找到嵌套更深一层的省略号,您需要更详细的路径表达式。
ElementTree对XPath 迷你语言的语法支持有限,您可以使用它来查询 XML 中的元素,类似于 HTML 中的 CSS 选择器。还有其他方法可以接受这样的表达式:
>>> namespaces = {"": "http://www.w3.org/2000/svg"}
>>> root.iterfind("defs", namespaces)
<generator object prepare_child.<locals>.select at 0x7f430ba6d190>
>>> root.findall("defs", namespaces)
[<Element '{http://www.w3.org/2000/svg}defs' at 0x7f430ba09e00>]
>>> root.find("defs", namespaces)
<Element '{http://www.w3.org/2000/svg}defs' at 0x7f430ba09e00>
虽然.iterfind()
懒惰地产生匹配元素,.findall()
返回一个列表,并且.find()
只返回第一个匹配元素。同样,您可以使用以下命令提取包含在元素的开始和结束标记之间的文本.findtext()
或获取整个文档的内部文本.itertext()
:
>>> namespaces = {"i": "http://www.inkscape.org/namespaces/inkscape"}
>>> root.findtext("i:custom", namespaces=namespaces)
'Some value'
>>> for text in root.itertext():
... if text.strip() != "":
... print(text.strip())
...
Some value
Hello <svg>!
console.log("CDATA disables XML parsing: <svg>")
⋮
您首先查找嵌入在特定 XML 元素中的文本,然后是整个文档中的任何地方。按文本搜索是 ElementTree API 的一项强大功能。可以使用其他内置解析器复制它,但代价是代码复杂性增加和便利性降低。
ElementTree API 可能是其中最直观的一种。它是 Pythonic、高效、健壮和通用的。除非您有使用 DOM 或 SAX 的特定原因,否则这应该是您的默认选择。
探索第三方 XML 解析器库
有时,使用标准库中的 XML 解析器可能感觉就像拿起大锤敲碎坚果。在其他时候,情况正好相反,您希望有一个可以做更多事情的解析器。例如,您可能希望根据架构验证 XML 或使用高级 XPath 表达式。在这些情况下,最好查看PyPI 上可用的外部库。
在下面,您将找到一系列具有不同程度的复杂性和复杂性的外部库。
untangle
: 将 XML 转换为 Python 对象
如果您正在寻找一种可以将您的 XML 文档转换为 Python 对象的单行代码,那就别无所求。虽然它已经几年没有更新了,但该untangle
库可能很快就会成为您在 Python 中解析 XML 的最爱方式。只需要记住一个函数,它接受一个 URL、一个文件名、一个文件对象或一个 XML 字符串:
>>> import untangle
>>> # Parse XML from a URL
>>> untangle.parse("http://localhost:8000/smiley.svg")
Element(name = None, attributes = None, cdata = )
>>> # Parse XML from a filename
>>> untangle.parse("smiley.svg")
Element(name = None, attributes = None, cdata = )
>>> # Parse XML from a file object
>>> with open("smiley.svg") as file:
... untangle.parse(file)
...
Element(name = None, attributes = None, cdata = )
>>> # Parse XML from a Python string
>>> untangle.parse("""\
... <svg viewBox="-105 -100 210 270">
... <!-- More content goes here... -->
... </svg>
... """)
Element(name = None, attributes = None, cdata = )
在每种情况下,它都返回Element
类的一个实例。您可以使用点运算符访问其子节点,并使用方括号语法通过索引获取 XML 属性或子节点之一。例如,要获取文档的根元素,您可以像访问对象的属性一样访问它。要获取元素的 XML 属性之一,您可以将其名称作为字典键传递:
>>> import untangle
>>> document = untangle.parse("smiley.svg")
>>> document.svg
Element(name = svg, attributes = {'xmlns': ...}, ...)
>>> document.svg["viewBox"]
'-105 -100 210 270'
没有要记住的函数或方法名称。相反,每个解析的对象都是唯一的,因此您确实需要知道底层 XML 文档的结构才能使用untangle
.
要找出根元素的名称,请调用dir()
文档:
>>> dir(document)
['svg']
这会显示元素的直接子元素的名称。请注意,为其解析的文档untangle
重新定义了 的含义dir()
。通常,您调用此内置函数来检查类或 Python 模块。默认实现将返回属性名称列表,而不是 XML 文档的子元素。
如果给定的标签名称有多个孩子,那么您可以使用循环遍历它们或通过索引引用一个:
>>> dir(document.svg)
['defs', 'g', 'inkscape_custom', 'script', 'text']
>>> dir(document.svg.defs.linearGradient)
['stop', 'stop', 'stop']
>>> for stop in document.svg.defs.linearGradient.stop:
... print(stop)
...
Element <stop> with attributes {'offset': ...}, ...
Element <stop> with attributes {'offset': ...}, ...
Element <stop> with attributes {'offset': ...}, ...
>>> document.svg.defs.linearGradient.stop[1]
Element(name = stop, attributes = {'offset': ...}, ...)
您可能已经注意到该<inkscape:custom>
元素已重命名为inkscape_custom
. 不幸的是,该库不能很好地处理XML 名称空间,因此如果您需要依赖它,那么您必须寻找其他地方。
由于点表示法,XML 文档中的元素名称必须是有效的Python 标识符。如果不是,则将untangle
通过用下划线替换禁用字符来自动重写其名称:
>>> dir(untangle.parse("<com:company.web-app></com:company.web-app>"))
['com_company_web_app']
孩子的标签名称并不是您可以访问的唯一对象属性。元素有一些预定义的对象属性,可以通过调用来显示vars()
:
>>> element = document.svg.text
>>> list(vars(element).keys())
['_name', '_attributes', 'children', 'is_root', 'cdata']
>>> element._name
'text'
>>> element._attributes
{'x': '-40', 'y': '75'}
>>> element.children
[]
>>> element.is_root
False
>>> element.cdata
'Hello <svg>!'
在幕后,untangle
使用内置的 SAX 解析器,但由于该库是用纯 Python 实现的,并创建了大量重量级对象,因此性能相当差。虽然它旨在读取小型文档,但您仍然可以将其与另一种读取多 GB XML 文件的方法结合使用。
就是这样。如果您前往Wikipedia archives,您可以下载他们的压缩 XML 文件之一。顶部的应该包含文章摘要的快照:
<feed>
<doc>
<title>Wikipedia: Anarchism</title>
<url>https://en.wikipedia.org/wiki/Anarchism</url>
<abstract>Anarchism is a political philosophy...</abstract>
<links>
<sublink linktype="nav">
<anchor>Etymology, terminology and definition</anchor>
<link>https://en.wikipedia.org/wiki/Anarchism#Etymology...</link>
</sublink>
<sublink linktype="nav">
<anchor>History</anchor>
<link>https://en.wikipedia.org/wiki/Anarchism#History</link>
</sublink>
⋮
</links>
</doc>
⋮
</feed>
下载后大小超过 6 GB,非常适合本练习。这个想法是扫描文件以找到连续的开始和结束<doc>
标记,然后untangle
为了方便起见解析它们之间的XML片段。
内置mmap
模块允许您创建文件内容的虚拟视图,即使它不适合可用内存。这给人的印象是使用支持搜索和常规切片语法的巨大字节字符串。如果您对如何将此逻辑封装在Python 类中并利用生成器进行延迟评估感兴趣,请展开下面的可折叠部分。
解析 XML 的混合方法显示隐藏
无需深入细节,以下是如何使用此自定义类快速浏览大型 XML 文件,同时更彻底地检查特定元素的方法untangle
:
>>> with XMLTagStream("abstract.xml", "doc") as stream:
... for doc in stream:
... print(doc.title.cdata.center(50, "="))
... for sublink in doc.links.sublink:
... print("-", sublink.anchor.cdata)
... if "q" == input("Press [q] to exit or any key to continue..."):
... break
...
===============Wikipedia: Anarchism===============
- Etymology, terminology and definition
- History
- Pre-modern era
⋮
Press [q] to exit or any key to continue...
================Wikipedia: Autism=================
- Characteristics
- Social development
- Communication
⋮
Press [q] to exit or any key to continue...
首先,打开一个文件进行读取并指明要查找的标签名称。然后,您遍历这些元素并接收 XML 文档的解析片段。这几乎就像透过一个小窗户在一张无限长的纸上移动。这是一个相对表面级别的示例,忽略了一些细节,但它应该让您大致了解如何使用这种混合解析策略。
xmltodict
:将 XML 转换为 Python 字典
如果您喜欢 JSON 但不喜欢 XML,请查看xmltodict
,它试图弥合两种数据格式之间的差距。顾名思义,该库可以解析 XML 文档并将其表示为 Python 字典,这也恰好是 Python 中 JSON 文档的目标数据类型。这使得XML 和 JSON 之间的转换成为可能。
注意:字典是由键值对组成的,而 XML 文档本质上是分层的,在转换过程中可能会导致一些信息丢失。最重要的是,XML 具有属性、注释、处理指令和其他定义字典中没有的元数据的方法。
与目前其他的 XML 解析器不同,这个解析器需要一个 Python 字符串或一个类似文件的对象,以二进制模式打开读取:
>>> import xmltodict
>>> xmltodict.parse("""\
... <svg viewBox="-105 -100 210 270">
... <!-- More content goes here... -->
... </svg>
... """)
OrderedDict([('svg', OrderedDict([('@viewBox', '-105 -100 210 270')]))])
>>> with open("smiley.svg", "rb") as file:
... xmltodict.parse(file)
...
OrderedDict([('svg', ...)])
默认情况下,库返回OrderedDict
集合的实例以保留元素 order。但是,从 Python 3.6 开始,普通字典也保持插入顺序。如果您想改用常规字典,dict
请将作为dict_constructor
参数传递给parse()
函数:
>>> import xmltodict
>>> with open("smiley.svg", "rb") as file:
... xmltodict.parse(file, dict_constructor=dict)
...
{'svg': ...}
现在,parse()
返回一个具有熟悉文本表示的普通旧字典。
为避免XML 元素与其属性之间的名称冲突,库会自动为后者添加一个@
字符前缀。您也可以通过xml_attribs
适当地设置标志来完全忽略属性:
>>> import xmltodict
>>> # Rename attributes by default
>>> with open("smiley.svg", "rb") as file:
... document = xmltodict.parse(file)
... print([x for x in document["svg"] if x.startswith("@")])
...
['@xmlns', '@xmlns:inkscape', '@viewBox', '@width', '@height']
>>> # Ignore attributes when requested
>>> with open("smiley.svg", "rb") as file:
... document = xmltodict.parse(file, xml_attribs=False)
... print([x for x in document["svg"] if x.startswith("@")])
...
[]
另一个被默认忽略的信息是XML 命名空间声明。这些被视为常规属性,而相应的前缀成为标签名称的一部分。但是,如果需要,您可以扩展、重命名或跳过某些命名空间:
>>> import xmltodict
>>> # Ignore namespaces by default
>>> with open("smiley.svg", "rb") as file:
... document = xmltodict.parse(file)
... print(document.keys())
...
odict_keys(['svg'])
>>> # Process namespaces when requested
>>> with open("smiley.svg", "rb") as file:
... document = xmltodict.parse(file, process_namespaces=True)
... print(document.keys())
...
odict_keys(['http://www.w3.org/2000/svg:svg'])
>>> # Rename and skip some namespaces
>>> namespaces = {
... "http://www.w3.org/2000/svg": "svg",
... "http://www.inkscape.org/namespaces/inkscape": None,
... }
>>> with open("smiley.svg", "rb") as file:
... document = xmltodict.parse(
... file, process_namespaces=True, namespaces=namespaces
... )
... print(document.keys())
... print("custom" in document["svg:svg"])
... print("inkscape:custom" in document["svg:svg"])
...
odict_keys(['svg:svg'])
True
False
在上面的第一个示例中,标记名称不包含 XML 命名空间前缀。在第二个示例中,它们这样做是因为您请求处理它们。最后,在第三个示例中,您将默认命名空间折叠为 ,svg
同时使用None
.
Python 字典的默认字符串表示形式可能不够清晰。为了改进它的展示,你可以漂亮地打印它或将它转换成另一种格式,比如JSON或YAML:
>>> import xmltodict
>>> with open("smiley.svg", "rb") as file:
... document = xmltodict.parse(file, dict_constructor=dict)
...
>>> from pprint import pprint as pp
>>> pp(document)
{'svg': {'@height': '270',
'@viewBox': '-105 -100 210 270',
'@width': '210',
'@xmlns': 'http://www.w3.org/2000/svg',
'@xmlns:inkscape': 'http://www.inkscape.org/namespaces/inkscape',
'defs': {'linearGradient': {'@id': 'skin',
⋮
>>> import json
>>> print(json.dumps(document, indent=4, sort_keys=True))
{
"svg": {
"@height": "270",
"@viewBox": "-105 -100 210 270",
"@width": "210",
"@xmlns": "http://www.w3.org/2000/svg",
"@xmlns:inkscape": "http://www.inkscape.org/namespaces/inkscape",
"defs": {
"linearGradient": {
⋮
>>> import yaml # Install first with 'pip install PyYAML'
>>> print(yaml.dump(document))
svg:
'@height': '270'
'@viewBox': -105 -100 210 270
'@width': '210'
'@xmlns': http://www.w3.org/2000/svg
'@xmlns:inkscape': http://www.inkscape.org/namespaces/inkscape
defs:
linearGradient:
⋮
该xmltodict
库允许以相反的方式转换文档——即从 Python 字典转换回 XML 字符串:
>>> import xmltodict
>>> with open("smiley.svg", "rb") as file:
... document = xmltodict.parse(file, dict_constructor=dict)
...
>>> xmltodict.unparse(document)
'<?xml version="1.0" encoding="utf-8"?>\n<svg...'
如果有这样的需要,在将数据从 JSON 或 YAML 转换为 XML 时,字典可能会作为中间格式派上用场。
库中还有更多功能xmltodict
,例如流媒体,因此您可以自行探索它们。但是,这个库也有点过时了。此外,如果您真的正在寻找高级 XML 解析功能,那么它是您应该关注的下一个库。
lxml
:在类固醇上使用 ElementTree
如果您想要最好的性能、最广泛的功能和最熟悉的界面都包含在一个包中,那么安装lxml
并忘记其余的库。它是C 库libxml2和libxslt的Python 绑定,支持多种标准,包括 XPath、XML Schema 和 XSLT。
该库与 Python 的ElementTree API兼容,您在本教程前面已经了解了它。这意味着您可以通过仅替换单个导入语句来重用现有代码:
import lxml.etree as ET
这将为您带来巨大的性能提升。最重要的是,该lxml
库具有一组广泛的功能,并提供了不同的使用方式。例如,它允许您针对多种模式语言验证XML 文档,其中之一是 XML 模式定义:
>>> import lxml.etree as ET
>>> xml_schema = ET.XMLSchema(
... ET.fromstring("""\
... <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
... <xsd:element name="parent"/>
... <xsd:complexType name="SomeType">
... <xsd:sequence>
... <xsd:element name="child" type="xsd:string"/>
... </xsd:sequence>
... </xsd:complexType>
... </xsd:schema>"""))
>>> valid = ET.fromstring("<parent><child></child></parent>")
>>> invalid = ET.fromstring("<child><parent></parent></child>")
>>> xml_schema.validate(valid)
True
>>> xml_schema.validate(invalid)
False
Python 标准库中的 XML 解析器都没有验证文档的能力。同时,lxml
允许您定义一个XMLSchema
对象并通过它运行文档,同时与 ElementTree API 保持基本兼容。
除了 ElementTree API 之外,还lxml
支持替代的lxml.objectify接口,您将在稍后的数据绑定部分中介绍。
BeautifulSoup
: 处理格式错误的 XML
您通常不会使用此比较中的最后一个库来解析 XML,因为您经常会遇到Web 抓取HTML 文档。也就是说,它也能够解析 XML。BeautifulSoup带有一个可插拔的架构,让您可以选择底层解析器。在lxml
一个如前所述是由官方文档建议实际上,目前库支持的唯一XML解析器。
根据您要解析的文档类型、所需的效率和功能可用性,您可以选择以下解析器之一:
文件类型 | 解析器名称 | Python 库 | 速度 |
---|---|---|---|
HTML | "html.parser" |
- | 缓和 |
HTML | "html5lib" |
html5lib |
减缓 |
HTML | "lxml" |
lxml |
快速地 |
XML | "lxml-xml" 或者 "xml" |
lxml |
快速地 |
除了速度之外,各个解析器之间也存在显着差异。例如,当涉及到格式错误的元素时,其中一些比其他更宽容,而另一些则更好地模拟 Web 浏览器。
有趣的事实:库的名称指的是标签 soup,它描述了语法或结构上不正确的 HTML 代码。
假设您已经将lxml
和beautifulsoup4
库安装到您的活动虚拟环境中,您可以立即开始解析 XML 文档。您只需要导入BeautifulSoup
:
from bs4 import BeautifulSoup
# Parse XML from a file object
with open("smiley.svg") as file:
soup = BeautifulSoup(file, features="lxml-xml")
# Parse XML from a Python string
soup = BeautifulSoup("""\
<svg viewBox="-105 -100 210 270">
<!-- More content goes here... -->
</svg>
""", features="lxml-xml")
如果你不小心指定了一个不同的解析器,比如lxml
,那么库会<body>
为你添加缺少的 HTML 标签,例如到已解析的文档中。在这种情况下,这可能不是您想要的,因此在指定解析器名称时要小心。
BeautifulSoup 是解析 XML 文档的强大工具,因为它可以处理无效内容,并且具有用于提取信息的丰富 API。看看它如何处理错误嵌套的标签、禁止字符和放置不当的文本:
>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup("""\
... <parent>
... <child>Forbidden < character </parent>
... </child>
... ignored
... """, features="lxml-xml")
>>> print(soup.prettify())
<?xml version="1.0" encoding="utf-8"?>
<parent>
<child>
Forbidden
</child>
</parent>
不同的解析器会引发异常并在检测到文档有问题时立即投降。在这里,它不仅忽略了问题,而且还想出了解决其中一些问题的明智方法。元素现在正确嵌套并且没有无效内容。
使用 BeautifulSoup 定位元素的方法太多了,无法在此一一介绍。通常,您将调用.find()
或.findall()
在汤元素上的变体:
>>> from bs4 import BeautifulSoup
>>> with open("smiley.svg") as file:
... soup = BeautifulSoup(file, features="lxml-xml")
...
>>> soup.find_all("ellipse", limit=1)
[<ellipse cx="-20" cy="-10" fill="black" rx="6" ry="8" stroke="none"/>]
>>> soup.find(x=42)
<inkscape:custom inkscape:z="555" x="42">Some value</inkscape:custom>
>>> soup.find("stop", {"stop-color": "gold"})
<stop offset="75%" stop-color="gold" stop-opacity="1.0"/>
>>> soup.find(text=lambda x: "value" in x).parent
<inkscape:custom inkscape:z="555" x="42">Some value</inkscape:custom>
该limit
参数类似于LIMIT
MySQL 中的子句,它可以让您决定最多接收多少个结果。它将返回指定数量或更少的结果。这不是巧合。您可以将这些搜索方法视为具有强大过滤器的简单查询语言。
搜索界面非常灵活,但超出了本教程的范围。您可以查看库的文档以获取更多详细信息,或者阅读另一篇涉及 BeautifulSoup 的 Python网页抓取教程。
将 XML 数据绑定到 Python 对象
假设您希望通过低延迟WebSocket连接使用以 XML 格式交换的消息的实时数据馈送。出于本演示的目的,您将使用 Web 浏览器将鼠标和键盘事件广播到 Python 服务器。您将构建自定义协议并使用数据绑定将 XML 转换为原生 Python 对象。
数据绑定背后的想法是以声明方式定义数据模型,同时让程序弄清楚如何在运行时从 XML 中提取有价值的信息。如果您曾经使用过Django 模型,那么这个概念应该听起来很熟悉。
首先,从设计数据模型开始。它将包含两种类型的事件:
KeyboardEvent
MouseEvent
每个都可以代表一些专门的子类型,例如键盘的按键或按键释放以及鼠标的单击或右键单击。下面是响应按住Shift+2键组合生成的示例 XML 消息:
<KeyboardEvent>
<Type>keydown</Type>
<Timestamp>253459.17999999982</Timestamp>
<Key>
<Code>Digit2</Code>
<Unicode>@</Unicode>
</Key>
<Modifiers>
<Alt>false</Alt>
<Ctrl>false</Ctrl>
<Shift>true</Shift>
<Meta>false</Meta>
</Modifiers>
</KeyboardEvent>
该消息包含一个特定的键盘事件类型,时间戳,关键码和它的Unicode,以及修改键,例如Alt,Ctrl,或Shift。该Meta键通常是Win或Cmd键,这取决于你的键盘布局。
同样,鼠标事件可能如下所示:
<MouseEvent>
<Type>mousemove</Type>
<Timestamp>52489.07000000145</Timestamp>
<Cursor>
<Delta x="-4" y="8"/>
<Window x="171" y="480"/>
<Screen x="586" y="690"/>
</Cursor>
<Buttons bitField="0"/>
<Modifiers>
<Alt>false</Alt>
<Ctrl>true</Ctrl>
<Shift>false</Shift>
<Meta>false</Meta>
</Modifiers>
</MouseEvent>
然而,不是键,而是鼠标光标位置和一个位字段,对事件期间按下的鼠标按钮进行编码。位域为零表示未按下任何按钮。
一旦客户端建立连接,它就会开始用消息淹没服务器。该协议不包含任何握手、心跳、正常关闭、主题订阅或控制消息。您可以通过注册事件处理程序并WebSocket
在不到 50 行代码中创建一个对象,在 JavaScript中编写代码。
但是,实现客户端并不是本练习的重点。由于您不需要理解它,只需展开下面的可折叠部分以显示带有嵌入式 JavaScript 的 HTML 代码,并将其保存在一个您喜欢命名的文件中。
JavaScript 和 HTML 中的 WebSocket 客户端显示隐藏
客户端连接到侦听端口 8000 的本地服务器。一旦您将 HTML 代码保存在文件中,您就可以使用您喜欢的 Web 浏览器打开它。但在此之前,您需要实现服务器。
Python 不附带 WebSocket 支持,但您可以将该websockets
库安装到您的活动虚拟环境中。lxml
稍后您还将需要,所以现在是一次性安装这两个依赖项的好时机:
$ python -m pip install websockets lxml
最后,您可以搭建一个最小的异步 Web 服务器:
# server.py
import asyncio
import websockets
async def handle_connection(websocket, path):
async for message in websocket:
print(message)
if __name__ == "__main__":
future = websockets.serve(handle_connection, "localhost", 8000)
asyncio.get_event_loop().run_until_complete(future)
asyncio.get_event_loop().run_forever()
当您启动服务器并在 Web 浏览器中打开保存的 HTML 文件时,您应该会看到 XML 消息出现在标准输出中,以响应您的鼠标移动和按键操作。您可以同时在多个选项卡甚至多个浏览器中打开客户端!
使用 XPath 表达式定义模型
现在,您的消息以纯字符串格式到达。处理这种格式的消息不是很方便。幸运的是,您可以使用lxml.objectify
模块将它们转换为复合 Python 对象,只需一行代码:
# server.py
import asyncio
import websockets
import lxml.objectify
async def handle_connection(websocket, path):
async for message in websocket:
try:
xml = lxml.objectify.fromstring(message)
except SyntaxError:
print("Malformed XML message:", repr(message))
else:
if xml.tag == "KeyboardEvent":
if xml.Type == "keyup":
print("Key:", xml.Key.Unicode)
elif xml.tag == "MouseEvent":
screen = xml.Cursor.Screen
print("Mouse:", screen.get("x"), screen.get("y"))
else:
print("Unrecognized event type")
# ...
只要 XML 解析成功,您就可以检查根元素的常用属性,例如标签名称、属性、内部文本等。您将能够使用点运算符深入浏览元素树。在大多数情况下,库会识别合适的 Python 数据类型并为您转换值。
保存这些更改并重新启动服务器后,您需要在 Web 浏览器中重新加载页面以建立新的 WebSocket 连接。这是修改后的程序的示例输出:
$ python server.py
Mouse: 820 121
Mouse: 820 122
Mouse: 820 123
Mouse: 820 124
Mouse: 820 125
Key: a
Mouse: 820 125
Mouse: 820 125
Key: a
Key: A
Key: Shift
Mouse: 821 125
Mouse: 821 125
Mouse: 820 123
⋮
有时,XML 可能包含不是有效 Python 标识符的标记名称,或者您可能希望调整消息结构以适合您的数据模型。在这种情况下,一个有趣的选择是定义带有描述符的自定义模型类,这些描述符声明如何使用 XPath 表达式查找信息。这是开始类似于 Django 模型或Pydantic模式定义的部分。
您将使用自定义XPath
描述符和随附的Model
类,它们为您的数据模型提供可重用的属性。描述符需要一个 XPath 表达式,用于在接收到的消息中查找元素。底层实现有点高级,所以请随意从下面的可折叠部分复制代码。
XPath 描述符和模型类显示隐藏
假设您的模块中已经有所需的XPath
描述符和Model
抽象基类,您可以使用它们来定义KeyboardEvent
和MouseEvent
消息类型以及可重用的构建块以避免重复。有无数种方法可以做到这一点,但这里有一个例子:
# ...
class Event(Model):
"""Base class for event messages with common elements."""
type_: str = XPath("./Type")
timestamp: float = XPath("./Timestamp")
class Modifiers(Model):
alt: bool = XPath("./Alt")
ctrl: bool = XPath("./Ctrl")
shift: bool = XPath("./Shift")
meta: bool = XPath("./Meta")
class KeyboardEvent(Event):
key: str = XPath("./Key/Code")
modifiers: Modifiers = XPath("./Modifiers")
class MouseEvent(Event):
x: int = XPath("./Cursor/Screen/@x")
y: int = XPath("./Cursor/Screen/@y")
modifiers: Modifiers = XPath("./Modifiers")
该XPath
描述符允许懒惰的评价,这样的XML消息的元素在需要的时候只能看着了。更具体地说,只有在您访问事件对象上的属性时才会查找它们。此外,结果被缓存以避免多次运行相同的 XPath 查询。描述符还尊重类型注释并将反序列化数据自动转换为正确的 Python 类型。
使用这些事件对象与lxml.objectify
之前自动生成的事件对象没有太大区别:
if xml.tag == "KeyboardEvent":
event = KeyboardEvent(xml)
if event.type_ == "keyup":
print("Key:", event.key)
elif xml.tag == "MouseEvent":
event = MouseEvent(xml)
print("Mouse:", event.x, event.y)
else:
print("Unrecognized event type")
创建特定事件类型的新对象还有一个额外的步骤。但除此之外,它在独立于 XML 协议构建模型方面为您提供了更大的灵活性。此外,可以根据接收到的消息中的属性派生新的模型属性,并在此基础上添加更多方法。
从 XML 模式生成模型
实现模型类是一项乏味且容易出错的任务。但是,只要您的模型反映了 XML 消息,您就可以利用自动化工具根据 XML Schema 为您生成必要的代码。这种代码的缺点是它通常比手工编写的可读性差。
允许这样做的最古老的第三方模块之一是PyXB,它模仿了 Java 流行的JAXB库。不幸的是,它最后一次发布是在几年前,并且针对的是遗留的 Python 版本。您可以研究一个类似但积极维护的generateDS
替代方案,它从 XML 模式生成数据结构。
假设您有这个models.xsd
描述KeyboardEvent
消息的架构文件:
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="KeyboardEvent" type="KeyboardEventType"/>
<xsd:complexType name="KeyboardEventType">
<xsd:sequence>
<xsd:element type="xsd:string" name="Type"/>
<xsd:element type="xsd:float" name="Timestamp"/>
<xsd:element type="KeyType" name="Key"/>
<xsd:element type="ModifiersType" name="Modifiers"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="KeyType">
<xsd:sequence>
<xsd:element type="xsd:string" name="Code"/>
<xsd:element type="xsd:string" name="Unicode"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="ModifiersType">
<xsd:sequence>
<xsd:element type="xsd:string" name="Alt"/>
<xsd:element type="xsd:string" name="Ctrl"/>
<xsd:element type="xsd:string" name="Shift"/>
<xsd:element type="xsd:string" name="Meta"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
模式告诉 XML 解析器需要哪些元素、它们的顺序以及它们在树中的级别。它还限制了 XML 属性的允许值。这些声明与实际 XML 文档之间的任何差异都应使其无效并使解析器拒绝该文档。
此外,一些工具可以利用此信息生成一段代码,对您隐藏 XML 解析的详细信息。安装库后,您应该能够generateDS
在活动的虚拟环境中运行该命令:
$ generateDS -o models.py models.xsd
它将models.py
在与生成的 Python 源代码相同的目录中创建一个新文件。然后,您可以导入该模块并使用它来解析传入的消息:
>>> from models import parseString
>>> event = parseString("""\
... <KeyboardEvent>
... <Type>keydown</Type>
... <Timestamp>253459.17999999982</Timestamp>
... <Key>
... <Code>Digit2</Code>
... <Unicode>@</Unicode>
... </Key>
... <Modifiers>
... <Alt>false</Alt>
... <Ctrl>false</Ctrl>
... <Shift>true</Shift>
... <Meta>false</Meta>
... </Modifiers>
... </KeyboardEvent>""", silence=True)
>>> event.Type, event.Key.Code
('keydown', 'Digit2')
它看起来类似于lxml.objectify
前面显示的示例。不同之处在于,使用数据绑定强制遵守模式,而lxml.objectify
无论对象在语义上是否正确,都会动态生成对象。
使用安全解析器消除 XML 炸弹
Python 标准库中的 XML 解析器容易受到一系列安全威胁的攻击,这些安全威胁最多可能导致拒绝服务 (DoS)或数据丢失。公平地说,这不是他们的错。它们只是遵循 XML 标准的规范,它比大多数人知道的要复杂和强大。
注意:请注意,您应该明智地使用您即将看到的信息。您不希望最终成为攻击者,让自己面临法律后果,或面临终身禁止使用特定服务的情况。
最常见的攻击之一是XML Bomb,也称为十亿笑声攻击。该攻击利用DTD中的实体扩展来炸毁内存并尽可能长时间地占用CPU。阻止未受保护的 Web 服务器接收新流量所需的只是以下几行 XML 代码:
import xml.etree.ElementTree as ET
ET.fromstring("""\
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ELEMENT lolz (#PCDATA)>
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>""")
一个简单的解析器将尝试&lol9;
通过检查 DTD来解析放置在文档根目录中的自定义实体。但是,该实体本身多次引用另一个实体,后者又引用另一个实体,依此类推。当你运行上面的脚本时,你会注意到内存和处理单元有一些令人不安的事情:
看看主内存和交换分区如何在几秒钟内耗尽,而其中一个 CPU 以 100% 的容量工作。当系统内存变满时,录制会突然停止,然后在 Python 进程被杀死后恢复。
另一种流行的XXE攻击类型利用一般外部实体来读取本地文件并发出网络请求。尽管如此,从 Python 3.7.1 开始,默认情况下已禁用此功能以提高安全性。如果您信任您的数据,那么您可以告诉 SAX 解析器无论如何都要处理外部实体:
>>> from xml.sax import make_parser
>>> from xml.sax.handler import feature_external_ges
>>> parser = make_parser()
>>> parser.setFeature(feature_external_ges, True)
此解析器将能够读取您计算机上的本地文件。它可能会在类 Unix 操作系统上提取用户名,例如:
>>> from xml.dom.minidom import parseString
>>> xml = """\
... <?xml version="1.0" encoding="UTF-8"?>
... <!DOCTYPE root [
... <!ENTITY usernames SYSTEM "/etc/passwd">
... ]>
... <root>&usernames;</root>"""
>>> document = parseString(xml, parser)
>>> print(document.documentElement.toxml())
<root>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
⋮
realpython:x:1001:1001:Real Python,,,:/home/realpython:/bin/bash
</root>
通过网络将数据发送到远程服务器是完全可行的!
现在,您如何保护自己免受此类攻击?Python 官方文档显着地警告您使用内置 XML 解析器的风险,并建议在关键任务应用程序中切换到外部包。虽然不随 Python 分发,但它defusedxml
是标准库中所有解析器的直接替代品。
该库施加了严格的限制并禁用了许多危险的 XML 功能。它应该可以阻止大多数众所周知的攻击,包括刚才描述的两种攻击。要使用它,请从 PyPI 获取库并相应地替换您的导入语句:
>>> import defusedxml.ElementTree as ET
>>> ET.parse("bomb.xml")
Traceback (most recent call last):
...
raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
defusedxml.common.EntitiesForbidden:
EntitiesForbidden(name='lol', system_id=None, public_id=None)
就是这样!禁止的功能将不再通过。
结论
XML 数据格式是一种成熟且功能强大的标准,至今仍在使用,尤其是在企业环境中。选择正确的 XML 解析器对于在性能、安全性、合规性和便利性之间找到最佳平衡点至关重要。
本教程为您提供了详细的路线图,可以在 Python 中令人困惑的 XML 解析器迷宫中导航。您知道在哪里走捷径以及如何避免死胡同,从而为您节省大量时间。
在本教程中,您学习了如何:
- 选择正确的 XML解析模型
- 使用标准库中的 XML 解析器
- 使用主要的XML 解析库
- 使用数据绑定以声明方式解析 XML 文档
- 使用安全的 XML 解析器消除安全漏洞
现在,您了解了解析 XML 文档的不同策略以及它们的优点和缺点。有了这些知识,您就可以为您的特定用例选择最合适的 XML 解析器,甚至可以组合多个以更快地读取数 GB 的 XML 文件。