两个世界的最佳方案:将 XPath 与 XmlReader 组合在一起
达雷奥巴桑乔和霍华德浩
Microsoft Corporation
2004 年 5 月 5 日
总结: Dare Obasanjo 讨论 XPathReader,它提供使用 XPath 感知 XmlReader 以高效方式筛选和处理大型 XML 文档的功能。 使用 XPathReader,可以按顺序处理大型文档,并提取 XPath 表达式匹配的标识子树。 (11 个打印页面)
简介
大约一年前,我阅读了蒂姆·布雷(Tim Bray)一篇题为 “XML 对于程序员来说太难”的文章,他抱怨推送模型 API(如 SAX)处理大量 XML 的繁琐性质。 Tim Bray 将 XML 的理想编程模型描述为类似于在 Perl 中使用文本,其中一个模型可以通过使用正则表达式匹配感兴趣的项来处理文本流。 下面是 Tim Bray 的文章的摘录,展示了他理想的 XML 流编程模型。
while (<STDIN>) {
next if (X<meta>X);
if (X<h1>|<h2>|<h3>|<h4>X)
{ $divert = 'head'; }
elsif (X<img src="/^(.*\.jpg)$/i>X)
{ &proc_jpeg($1); }
# and so on...
}
Tim Bray 并不是唯一渴望此 XML 处理模型的人。 在过去的几年里,我处理的各种人一直在努力创建一个编程模型,用于处理 XML 文档流的方式类似于使用正则表达式处理文本流。 本文介绍这项工作的最终结果- XPathReader。
查找贷款书籍:XmlTextReader 解决方案
为了清楚地表明 XPathReader 与 XmlReader 的现有 XML 处理技术相比,我创建了一个执行基本 XML 处理任务的示例程序。 以下示例文档描述了我拥有的一些书籍,以及他们当前是否被借给朋友。
<books>
<book publisher="IDG books" on-loan="Sanjay">
<title>XML Bible</title>
<author>Elliotte Rusty Harold</author>
</book>
<book publisher="Addison-Wesley">
<title>The Mythical Man Month</title>
<author>Frederick Brooks</author>
</book>
<book publisher="WROX">
<title>Professional XSLT 2nd Edition</title>
<author>Michael Kay</author>
</book>
<book publisher="Prentice Hall" on-loan="Sander" >
<title>Definitive XML Schema</title>
<author>Priscilla Walmsley</author>
</book>
<book publisher="APress">
<title>A Programmer's Introduction to C#</title>
<author>Eric Gunnerson</author>
</book>
</books>
下面的代码示例显示我借给书籍的人员的姓名,以及我借给他们的书籍。 代码示例应生成以下输出。
Sanjay was loaned XML Bible by Elliotte Rusty Harold
Sander was loaned Definitive XML Schema by Priscilla Walmsley
XmlTextReader Sample:
using System;
using System.IO;
using System.Xml;
public class Test{
static void Main(string[] args) {
try{
XmlTextReader reader = new XmlTextReader("books.xml");
ProcessBooks(reader);
}catch(XmlException xe){
Console.WriteLine("XML Parsing Error: " + xe);
}catch(IOException ioe){
Console.WriteLine("File I/O Error: " + ioe);
}
}
static void ProcessBooks(XmlTextReader reader) {
while(reader.Read()){
//keep reading until we see a book element
if(reader.Name.Equals("book") &&
(reader.NodeType == XmlNodeType.Element)){
if(reader.GetAttribute("on-loan") != null){
ProcessBorrowedBook(reader);
}else {
reader.Skip();
}
}
}
}
static void ProcessBorrowedBook(XmlTextReader reader){
Console.Write("{0} was loaned ",
reader.GetAttribute("on-loan"));
while(reader.NodeType != XmlNodeType.EndElement &&
reader.Read()){
if (reader.NodeType == XmlNodeType.Element) {
switch (reader.Name) {
case "title":
Console.Write(reader.ReadString());
reader.Read(); // consume end tag
break;
case "author":
Console.Write(" by ");
Console.Write(reader.ReadString());
reader.Read(); // consume end tag
break;
}
}
}
Console.WriteLine();
}
}
将 XPath 用作 XML 的正则表达式
首先,我们需要一种方法,以与文本流中字符串的正则表达式相同的方式为 XML 流中感兴趣的节点执行模式匹配。 XML 已有一种用于匹配名为 XPath 的节点的语言,该语言可用作良好的起点。 XPath 存在一个问题,它阻止它被使用,而无需修改为以流式处理方式匹配大型 XML 文档中的节点的机制。 XPath 假定整个 XML 文档存储在内存中,并允许需要对文档进行多次传递的操作,或者至少需要将大部分 XML 文档存储在内存中。 以下 XPath 表达式是此类查询的示例:
/books/book[author='Frederick Brooks']/@publisher
如果该查询具有值为“Frederick Brooks”的子作者元素,则查询将返回 book 元素的发布者属性。 如果不缓存数据而不是流分析程序的典型数据,则无法执行此查询,因为 发布者 属性必须在 book 元素上看到时缓存,直到查看子 作者 元素及其值检查为止。 根据文档和查询的大小,必须在内存中缓存的数据量可能相当大,并找出要缓存的数据可能相当复杂。 为了避免不得不处理同事 Arpan Desai 这些问题,请提出一个适用于仅前向处理 XML 的 XPath 子集的建议。 此 XPath 子集在他的论文 《顺序 XPath 简介》中进行了介绍。
顺序 XPath 中的标准 XPath 语法有几个更改,但最大的变化是轴的使用限制。 现在,某些轴在谓词中有效,而其他轴仅在顺序 XPath 表达式的非谓词部分有效。 我们已将轴分类为三个不同的组:
- 通用轴: 提供有关当前节点上下文的信息。 可以在顺序 XPath 表达式中的任何位置应用它们。
- 转发轴: 提供有关流中上下文节点前面的节点的信息。 它们只能在位置路径上下文中应用,因为它们正在寻找“未来”节点。 例如“child.'” 如果“child”位于路径中,我们可以成功选择给定路径的子节点。 但是,如果“child”位于谓词中,则无法选择当前节点,因为无法向前查看其子节点来测试谓词表达式,然后回退读取器以选择节点。
- 反向轴: 基本上与正向轴相反。 例如“parent”。如果父节点位于位置路径中,则我们希望返回特定节点的父级。 再次,由于我们不能向后移动,因此不能在位置路径或谓词中支持这些轴。
下表显示了 XPathReader 支持的 XPath 轴:
| 类型 | Axes | 支持的位置 |
|---|---|---|
| 通用轴 | attribute, namespace, self | XPath 表达式中的任意位置 |
| 前向轴 | 子级、后代、后代或自我,关注以下同级 | XPath 表达式中的任意位置(谓词除外) |
| 反向轴 | 上级、上级或自我、父级、上级、上级兄弟姐妹 | 不支持 |
XPathReader 不支持某些 XPath 函数,因为它们还需要在内存中缓存 XML 文档的大部分,或者能够回溯 XML 分析程序。 完全不支持 count () 和 sum () 等函数,而 local-name () 和 namespace-uri () 等函数仅在上下文节点上请求这些属性时) 指定任何 (参数时才起作用。 下表列出了 XPath 函数,这些函数要么不受支持,要么在 XPathReader 中受其某些功能限制。
| XPath 函数 | 支持的子集 | 说明 |
|---|---|---|
| number last () | 不支持 | 在缓冲的情况下无法正常工作 |
| 节点集 (计数) | 不支持 | 在缓冲的情况下无法正常工作 |
| string local-name (node-set?) | string local-name () | 不能将节点集用作参数 |
| string namespace-uri (node-set?) | string namespace-uri () | 不能将节点集用作参数 |
| (node-set?) 字符串名称 | 字符串名称 () | 不能将节点集用作参数 |
| 节点集) (数字求和 | 不支持 | 在缓冲的情况下无法正常工作 |
在 XPathReader 中对 XPath 做出的最后一个主要限制是禁止测试元素或文本节点的值。 XPathReader 不支持以下 XPath 表达式:
/books/book[contains(.,'Frederick Brooks')]
如果上面的查询包含文本“Frederick Brooks”,则选择 book 元素。 为了能够支持此类查询,可能需要缓存文档的大部分内容, 并且 XPathReader 需要能够回退其状态。 但是,支持测试属性、注释或处理指令的值。 XPathReader 支持以下 XPath 表达式:
/books/book[contains(@publisher,'WROX')]
上述 XPath 的子集已足够减少,以便使 XPath 能够提供内存高效、基于 XPath 的 XML 分析程序,类似于与文本流匹配的正则表达式。
第一次查看 XPathReader
XPathReader 是 XmlReader 的子类,支持上一部分所述的 XPath 子集。 XPathReader 可用于处理从 URL 加载的文件,也可以分层到 XmlReader 的其他实例上。 下表显示了 XPathReader 添加到 XmlReader 的方法。
| 方法 | 说明 |
|---|---|
| 匹配 XPathExpression) ( | 测试当前定位读取器的节点是否与 XPathExpression 匹配。 |
| 匹配 (字符串) | 测试当前定位读取器的节点是否与 XPath 字符串匹配。 |
| 匹配 int) ( | 测试读取器当前定位的节点是否与读取器的 XPathCollection 中指定索引处的 XPath 表达式匹配。 |
| MatchesAny (ArrayList) | 测试当前定位读取器的节点是否与列表中的任何 XPathExpressions 匹配。 |
| ReadUntilMatch () | 继续读取 XML 流,直到当前节点与指定的 XPath 表达式之一匹配。 |
以下示例使用 XPathReader 打印库中每本书的标题:
using System;
using System.Xml;
using System.Xml.XPath;
using GotDotNet.XPath;
public class Test{
static void Main(string[] args) {
try{
XPathReader xpr = new XPathReader("books.xml", "//book/title");
while (xpr.ReadUntilMatch()) {
Console.WriteLine(xpr.ReadString());
}
Console.ReadLine();
}catch(XPathReaderException xpre){
Console.WriteLine("XPath Error: " + xpre);
}catch(XmlException xe){
Console.WriteLine("XML Parsing Error: " + xe);
}catch(IOException ioe){
Console.WriteLine("File I/O Error: " + ioe);
}
}
}
XPathReader 在处理 XML 流时不必跟踪当前节点上下文,XPathReader 与 XmlTextReader 进行传统的 XML 处理具有明显的优势。 在上面的示例中,应用程序代码不必担心 游戏元素的内容 是否显示和打印是 书籍 元素的子元素,而不是通过显式跟踪状态,因为 XPath 已执行此操作。
谜题的另一部分是 XPathCollection 类。 XPathCollection 是 XPathReader 应与之匹配的 XPath 表达式的集合。 XPathReader 仅匹配其 XPathCollection 对象中包含的节点。 此匹配是动态的,这意味着可以在分析过程中根据需要从 XPathCollection 中添加和删除 XPath 表达式。 这允许性能优化,在需要测试之前,不会针对 XPath 表达式执行测试。 XPathCollection 还用于指定 XPathReader 在与 XPath 表达式匹配节点时使用的前缀<>命名空间绑定。 以下代码片段演示如何完成此操作:
XPathCollection xc = new XPathCollection();
xc.NamespaceManager = new XmlNamespaceManager(new NameTable());
xc.NamespaceManager.AddNamespace("ex", "http://www.example.com");
xc.Add("//ex:book/ex:title");
XPathReader xpr = new XPathReader("books.xml", xc);
查找贷款书籍:XPathReader 解决方案
现在,我们已经了解了 XPathReader,现在是时候看看可以使用 XmlTextReader 对 XML 文件进行处理的改进程度。 下面的代码示例使用名为“查找贷款书籍:XmlTextReader 解决方案”部分中的 XML 文件,并应生成以下输出:
Sanjay was loaned XML Bible by Elliotte Rusty Harold
Sander was loaned Definitive XML Schema by Priscilla Walmsley
XPathReader Sample:
using System;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using GotDotNet.XPath;
public class Test{
static void Main(string[] args) {
try{
XmlTextReader xtr = new XmlTextReader("books.xml");
XPathCollection xc = new XPathCollection();
int onloanQuery = xc.Add("/books/book[@on-loan]");
int titleQuery = xc.Add("/books/book[@on-loan]/title");
int authorQuery = xc.Add("/books/book[@on-loan]/author");
XPathReader xpr = new XPathReader(xtr, xc);
while (xpr.ReadUntilMatch()) {
if(xpr.Match(onloanQuery)){
Console.Write("{0} was loaned ", xpr.GetAttribute("on-loan"));
}else if(xpr.Match(titleQuery)){
Console.Write(xpr.ReadString());
}else if(xpr.Match(authorQuery)){
Console.WriteLine(" by {0}", xpr.ReadString());
}
}
Console.ReadLine();
}catch(XPathReaderException xpre){
Console.WriteLine("XPath Error: " + xpre);
}catch(XmlException xe){
Console.WriteLine("XML Parsing Error: " + xe);
}catch(IOException ioe){
Console.WriteLine("File I/O Error: " + ioe);
}
}
}
此输出大大简化了原始代码块,几乎与使用正则表达式处理文本流一样高效。 看来我们已经达到了 Tim Bray 非常适合用于处理大型 XML 流的 XML 编程模型。
XPathReader 的工作原理
XPathReader 通过创建编译为抽象语法树的 XPath 表达式集合来匹配 XML 节点, (AST) ,然后在从基础 XmlReader 接收传入节点时走此语法树。 通过遍历 AST 树,将生成查询树并将其推送到堆栈上。 计算查询要匹配的节点的深度,并将其与 XmlReader 的 Depth 属性进行比较,因为 XML 流中遇到节点。 从System.Xml中的类的基础代码中获取用于为 XPath 表达式生成 AST 的代码 。Xpath,在 共享源公共语言基础结构 1.0 版本中作为源代码的一部分提供。
AST 中的每个节点实现定义以下三种方法的 IQuery 接口:
internal virtual object GetValue(XPathReader reader);
internal virtual bool MatchNode(XPathReader reader);
internal abstract XPathResultType ReturnType()
GetValue 方法返回输入节点的值相对于查询表达式的当前方面。 MatchNode 方法测试输入节点是否与分析的查询上下文匹配,而 ReturnType 属性指定查询表达式计算的 XPath 类型。
XPathReader 的未来计划
根据 Microsoft 中各种人员发现 XPathReader 的有用程度,包括随此实现变体一起随附的BizTalk Server,我决定为项目创建 GotDotNet 工作区。 我希望看到一些功能,例如,将 EXSLT.NET 项目中 的一些函数集成到 XPathReader 中,并支持更广泛的 XPath。 想要进一步开发 XPathReader 的开发人员可以加入 GotDotNet 工作区。
结束语
XPathReader 通过利用 XPath 的强大功能并将其与 XmlReader 基于拉取的 XML 分析器模型的灵活性相结合,为处理 XML 流提供了一种强大的方法。 System.Xml的组合设计允许将 XPathReader 分层到 XmlReader 的其他实现上,反之亦然。 使用 XPathReader 处理 XML 流的速度几乎与使用 XmlTextReader 的速度一样快,但同时与使用 XmlDocument 的 XPath 一样可用。 真的是两个世界最好的。
Dare Obasanjo 是 Microsoft WebData 团队的成员,其中还开发.NET Framework、Microsoft XML Core Services (MSXML) (MSXML) 和 Microsoft Data Access 组件 (MDAC) 的 System.Xml 和 System.Data 命名空间中的组件。
Howard Hao 是 WebData XML 团队测试中的软件设计工程师,是 XPathReader 的主要开发人员。
随时在 GotDotNet 上的 极端 XML 消息板上 发布有关本文的任何问题或评论。