两全其美:将 XPath 与 XmlReader 相结合

 

敢奥巴桑乔和霍华德豪
Microsoft Corporation

2004 年 5 月 5 日

下载XPathReader.exe示例文件

总结: 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”,则查询将返回 publisher属性。 如果不缓存比流式分析程序通常更多的数据,则无法执行此查询,因为在 book 元素上看到发布者属性时,必须缓存发布者属性,直到看到子作者元素并检查其值。 根据文档和查询的大小,必须在内存中缓存的数据量可能相当大,并且弄清楚要缓存的内容可能相当复杂。 为了避免必须处理这些问题,同事 Arpan Desai 提出了一个适用于 XML 仅向前处理的 XPath 子集的建议。 他的论文《顺序 XPath 简介》中介绍了 XPath 的这一子集。

顺序 XPath 中的标准 XPath 语法有一些更改,但最大的变化是轴的使用限制。 现在,某些轴在谓词中有效,而其他轴仅在顺序 XPath 表达式的非谓词部分有效。 我们已将轴分为三个不同的组:

  • 通用轴: 提供有关当前节点的上下文的信息。 可以在顺序 XPath 表达式中的任意位置应用它们。
  • 转发轴: 提供有关流中上下文节点之前节点的信息。 它们只能在位置路径上下文中应用,因为它们要查找“未来”节点。 例如“child”。” 如果“子”位于路径中,则可以成功选择给定路径的子节点。 但是,如果谓词中存在“子节点”,则无法选择当前节点,因为无法向前看其子节点来测试谓词表达式,然后回退读取器以选择节点。
  • 反向轴: 本质上与正向轴相反。 例如,“父级”。如果父节点位于位置路径中,则我们希望返回特定节点的父节点。 同样,由于无法向后移动,因此无法在位置路径或谓词中支持这些轴。

下表显示了 XPathReader 支持的 XPath 轴:

类型 Axes 支持的位置
公共轴 attribute, namespace, self XPath 表达式中的任意位置
前向轴 child、 descendant、 descendant-or-self、 following、 following-同级 XPath 表达式中的任意位置(谓词除外)
反向轴 祖先、祖先或自我、父级、上级、前同级 不支持

XPathReader 不支持某些 XPath 函数,因为它们还需要缓存内存中的 XML 文档的大部分内容,或者能够回溯 XML 分析程序。 完全不支持 count () sum () 等函数,而 local-name () namespace-uri () 等函数仅在未指定参数 ((即仅在上下文节点上请求这些属性) 时才起作用。 下表列出了 XPathReader 中不受支持或部分功能受限的 XPath 函数。

XPath 函数 支持的子集 说明
number last () 不支持 没有缓冲就无法工作
节点集) (计数 不支持 没有缓冲就无法工作
string local-name (node-set?) string local-name () 不能使用 node-set 作为参数
string namespace-uri (node-set?) string namespace-uri () 不能使用 node-set 作为参数
string name (node-set?) string name () 不能使用 node-set 作为参数
节点集) (数字求和 不支持 没有缓冲就无法工作

XPathReader 中对 XPath 的最后一个主要限制是不允许测试元素或文本节点的值。 XPathReader 不支持以下 XPath 表达式:

 /books/book[contains(.,'Frederick Brooks')]

如果书籍元素的字符串包含文本“Frederick Brooks”,则上述查询将选择 该书籍 元素。 为了能够支持此类查询,可能需要缓存文档的大部分内容, 并且 XPathReader 需要能够回退其状态。 但是,支持测试属性、注释或处理指令的值。 XPathReader 支持以下 XPath 表达式:

/books/book[contains(@publisher,'WROX')]

上述 XPath 的子集已足够减少,以便提供内存高效、基于 XPath 的流式 XML 分析器,该分析程序类似于匹配文本流的正则表达式。

XPathReader 的初探

XPathReaderXmlReader 的子类,支持上一部分所述的 XPath 子集。 XPathReader 可用于处理从 URL 加载的文件,也可在 XmlReader 的其他实例上分层。 下表显示了 XPathReader 添加到 XmlReader 的方法。

方法 说明
匹配 (XPathExpression) 测试当前定位读取器的节点是否与 XPathExpression 匹配。
匹配 (字符串) 测试当前定位读取器的节点是否与 XPath 字符串匹配。
匹配 (int) 测试读取器当前所在的节点是否与读取器的 XPathCollection 中指定索引处的 XPath 表达式匹配。
MatchesAny (ArrayList) 测试读取器当前所在的节点是否与列表中的任何 XPathExpression 匹配
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);
      }
   }  
}

与使用 XmlTextReader 进行传统 XML 处理相比,XPathReader 的一个明显优势是,应用程序在处理 XML 流时不必跟踪当前节点上下文。 在上面的示例中,应用程序代码无需担心其显示和打印内容的 title 元素是否是 书籍 元素的子元素,是否通过显式跟踪状态(因为 XPath 已完成此操作)。

谜题的另一部分是 XPathCollection 类。 XPathCollectionXPathReader 应匹配的 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 树,将生成查询树并将其推送到堆栈上。 在 XML 流中遇到节点时,计算要由查询匹配的节点的深度,并将其与 XmlReaderDepth 属性进行比较。 为 XPath 表达式生成 AST 的代码是从System.Xml类的基础代码获取的 。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 的 System.Xml 和 System.Data 命名空间中开发组件、Microsoft XML 核心服务 (MSXML) ,以及 Microsoft 数据访问组件 (MDAC) 。

Howard Hao 是 WebData XML 团队测试中的软件设计工程师,是 XPathReader main开发人员。

请随时在 GotDotNet 上的 Extreme XML 消息板上 发布有关本文的任何问题或评论。