65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 VTD+XML 索引 XML 文档

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (11投票s)

2008年3月25日

CPOL

12分钟阅读

viewsIcon

85015

downloadIcon

478

介绍了一个简单、高效、人类可读的 XML 索引,名为 VTD+XML。

摘要

传统上,基于 DOM 或 SAX 的企业应用程序在多次访问同一文档时必须重复 CPU 密集型的 XML 解析。本文介绍了一种非常简单、通用的本地 XML 索引,名为 VTD+XML,它消除了这些应用程序重复解析的需要。

使用 VTD+XML 避免重复 XML 解析

正如在“使用 VTD-XML 简化 XML 处理”中所讨论的,迄今为止,XML 应用程序开发的一个基本假设是,在对 XML 文档进行任何操作之前必须对其进行解析。换句话说,XML 应用程序的处理逻辑在没有解析的情况下无法启动。XML 解析通常比 XPath 评估等其他 XML 操作慢得多,并且经常被视为对数据库性能的威胁。当这些应用程序对不经常更改的 XML 数据执行多次只读访问时,能否消除相关的重复解析的开销岂不更好?

通过 VTD-XML 的本地 XML 索引功能,您可以精确地做到这一点。封装各种解析例程的类 VTDGen 现在添加了“readIndex(...)”和“writeIndex(...)”。有两个与索引相关的异常:indexWriteExceptionindexReadException

让我将这些新方法付诸实践,并向您展示如何在应用程序中启用索引功能。考虑以下 XML 文档:

<purchaseOrder orderDate="1999-10-21">
     <item partNum="872-AA">
       <productName>Lawnmower</productName>
       <quantity>1</quantity>
       <USPrice>148.95</USPrice>
     </item>
</purchaseOrder>

下面是一个名为“printPrice.cs”的简单应用程序,它打印元素“USPrice”的内容。请注意,它解析 XML 文件,然后使用 XPath 过滤目标节点。

using com.ximpleware.*;
public class printPrice{
   public static void main(String args[]){
     VTDGen vg = new VTDGen();
     try{
       if (vg.parseFile("po.xml",true)){
         VTDNav vn = vg.getNav();
         AutoPilot ap = new AutoPilot(vn);
         ap.selectXPath("/purchaseOrder/item/USPrice/text()");
         int i=-1; 
         while((i=ap.evalXPath())!=-1){
           System.out.println(" USPrice ==> "+vn.toString(i));
         }
       } 
     }catch(Exception e){ 

     }
   }
}

需要对 C# 代码进行一些更改,以添加 VTD-XML 的新索引功能。首先,您需要读取 XML 文档,对其进行解析,然后写出同一 XML 文档的索引版本。从那时起,您的应用程序就可以直接在索引之上运行 XPath 查询或处理逻辑,从而节省了再次解析 XML 文档的 CPU 周期。以下代码片段(分别命名为“genIndex.cs”和“accessIndex.cs”)展示了如何生成和访问索引。请注意,按顺序执行时,这两个应用程序都产生与“printPrice.cs”相同的输出。

第一个应用程序 (genIndex.cs) 读取“po.xml”并生成“po.vxl”。

using System;
using com.ximpleware;

namespace genIndex
{
    class genIndex
    {
        static void Main(string[] args)
        {
            VTDGen vg = new VTDGen();
            try
            {
                if (vg.parseFile("d:/codeProject/app3/po.xml", true))
                {
                    vg.writeIndex("d:/codeProject/app3/po.vxl");
                }
            }
            catch (VTDException e)
            {
            }
        }
    }
}

第二个应用程序 (accessIndex.java) 加载“po.vxl”并使用 XPath 表达式“/purchaseOrder/item/USPrice/text()”过滤文档。

using System;
using com.ximpleware;

namespace accessIndex
{
    class accessIndex
    {
        static void Main(string[] args)
        {
            VTDGen vg = new VTDGen();
            try
            {
                VTDNav vn = vg.loadIndex("d:/codeProject/app3/po.vxl");
                AutoPilot ap = new AutoPilot(vn);
                ap.selectXPath("/purchaseOrder/item/USPrice/text()");
                int i = -1;
                while ((i = ap.evalXPath()) != -1)
                {
                    Console.WriteLine(" USPrice ==> " + vn.toString(i));
                }
            }
            catch (VTDException e)
            {
            }

        }
    }
}

VTD+XML 30 秒上手

通过将 XML 解析与应用程序逻辑解耦,上面示例的关键是索引文件“po.vxl”,该文件符合 VTD+XML 规范。什么是 VTD+XML?由于 VTD-XML 对 XML 信息集的内部表示本质上是持久的,因此 VTD+XML 顾名思义,只是一种二进制打包格式,它将 VTD 记录、LC 条目和 XML 组合到一个文件中。详细的技术规范可以在此处找到。

一个简单示例

本节深入探讨规范的细节,通过逐字节手动组合 VTD+XML 索引。为简单起见,本示例选择索引一个简单的 XML 文档,该文档包含一个没有子元素的根元素,其解析表示没有位置缓存条目。此示例还假定小端字节序(如 C# 中)和 UTF-8 文档编码(默认字符集)。命名空间感知设置为 false。

相应索引文件的前四个字节字是 0x01028000,包含:

  • 第一个字节中的 VTD+XML 版本号 (0x01)
  • 第二个字节中的字符编码格式 (0x02)
  • 命名空间感知、LC 条目在最后一级的字长、平台的字节序以及 VTD 版本,均在第三个字节 (0x80) 的各个位字段中编码。
  • 文档深度(0x00,因为根元素没有子元素)

第二个四个字节字的值为 0x00040001,包含:

  • 上半部分 16 位中的 VTD-XML 实现支持的 LC 级别数(大端字节序中的 0x0004)
  • 下半部分 16 位中的根元素索引值(大端字节序中的 0x0001)

接下来的四个四个字节字保留为零。

所有后续 32 位或 64 位字的字节序都取决于平台,并在 VTD+XML 规范的第三个字节中指定。接下来的八字节字表示 XML 文档的大小(以字节为单位),在本例中等于七 (0x0700000000000000)。紧随其后 (0x3C726F6F742F3E00) 是 XML 的字节内容,通过在末尾填充零向上取整为八个字节的整数倍。

VTD+XML 索引的剩余部分由多个相邻的段组成,每个段包含一个八字节字(0x0000000000000002 表示 VTD 记录或 LC 条目数),后跟 VTD 记录或 LC 条目的实际内容。第一个八字节字 (0x020000000000000000) 表示有两个 VTD 记录,它们是 0x000000000000F0DF 和 0x0100000004000000。

其余三个八字节字的值均为零,表示第一、第二和第三级的查找缓存(location caches)在 VTD+XML 索引中没有条目。

作为最终输出,“”的 VTD+XML 索引长度为 88 字节,其十六进制表示如下:

图 1. “”的字节屏幕截图

优点和局限性

由于 VTD+XML 直接组合了 VTD 和 XML,因此它继承了 VTD-XML 解析的所有优点。与现有的 XML 索引(例如各种纯二进制 XML 索引模型化标签化、有序树等)相比,VTD+XML 具有许多独特的技术优势:

  • 通用性 - 在 VTD+XML 之前,大多数本地 XML 索引仅针对 XPath 查询的特定类型(例如,轴)进行了优化。如果输入查询与索引类型略有不同,则查询执行仍必须 resorting to expensive parsing. 由于这种限制,许多本地 XML 数据库现在要求用户创建多个索引,每个索引对应一种输入查询类型,以便用户可以从这些索引中获益。问题是,XML 数据库应用程序通常服务于许多不可预测且复杂的查询类型,这常常使索引的优势微不足道。相比之下,VTD+XML 是第一个完全消除了 XML 解析成本并可预测地加速任何类型 XPath 查询的索引。它也与命名空间(namespaces)配合得非常好。
  • 人类可读 - VTD+XML 也是第一个人类可读的 XML 索引。您可以实际在文本编辑器中打开它来检查 XML 文本。图 2 显示了“po.vxl”在“VI”中的样子。VTD+XML 的人类可读性不仅仅是一个不错的特性,它还提供了比纯二进制索引方案的明显优势。在其他条件相同的情况下,将 XML 保留其原始格式可以避免与二进制格式相互转换的处理成本。此外,如果您的应用程序只想修改 XML 有效负载,例如从中插入从另一个 SOAP 消息中提取的 XML 片段,那么将 XML 转换为二进制格式有什么意义?在面向服务的异构环境中,以原始格式维护 XML 可以自动保留开放性和互操作性。在我看来,XML 的唯一无损等价物就是 XML 本身,不多不少。
  • 图 2. VTD+XML 索引的自然人类可读性
  • 卓越的索引性能 - 生成 VTD+XML 索引的性能与 VTD-XML 的解析性能相同,因为从两个不同角度来看,两者本质上是相同的操作。在 1.7GHz Pentium 机器上,可以预期持续的索引性能为 50 MB/s-70 MB/s。
  • 易于使用 - 通常,在现有 VTD-XML 代码中添加几行(如前一个示例中所示的 loadIndex(...)writeIndex(...))即可在您的应用程序中启用 VTD+XML。
  • 紧凑 - VTD+XML 的大小通常比相应 XML 文档的大小大 30%-50%。这同样符合 VTD-XML 处理模型的内存使用情况。
  • 平台中立 - 就像 XML 一样,VTD+XML 被设计为平台中立的,因为它明确包含有关生成索引的平台的字节序(Endian-ness)的信息。C 或 C# 版 VTD-XML 代码的用户可以自动识别并利用 Java 版生成的索引。

同时,VTD+XML 的用户需要注意以下限制:

  • 文档大小上限 - VTD+XML 支持的最大 XML 文档大小为 2GB(无命名空间支持)。有命名空间时,VTD+XML 支持的最大大小为 1GB。
  • 不支持外部实体 - VTD-XML 目前支持 XML 1.0 中定义的五个内置实体引用(<、>、&、' 和 ")。

涉及 XML 内容更新的案例

有些人可能会问:如果后续的 XML 操作涉及改变偏移量的内容更新怎么办?一般来说,这些用例通常需要更新 XML 文档并重新索引。而且,对于大型 XML 文档,您可能会认为重新索引的成本相当可观。然而,实际上有一些变通方法,它们都旨在降低甚至消除重新索引的成本。

第一个变通方法

与其为单个大型 XML 文档创建 VTD+XML 索引,不如将 XML 文档拆分成多个较小的文档,每个文档都使用 VTD+XML 进行索引。从那时起,您只需要为那些“已更新”的 XML 片段重新生成 VTD+XML 索引,这些片段通常小得多,因此重新索引的成本也较低。

VTD+XML 的编辑功能允许您修改 XML 内容,而无需重新生成索引。下面的代码利用 VTDNav 类的新 overWrite(...) 方法将“<root>good</root>”的文本节点从“good”更改为“bad”。如果新内容比旧内容短或等长,则“overWrite(...)”方法会用空格填充非重叠部分,并返回 true。否则,原始内容不变,“overWrite(...)”返回 false

using System;
using System.Text;
using com.ximpleware;

namespace template
{
    class template
    {
        static void Main(string[] args)
        {
            VTDGen vg = new VTDGen();
            Encoding eg = System.Text.Encoding.GetEncoding("utf-8");
            if (vg.parseFile("d:/codeProject/app3/temp1.xml", true))
            {
                VTDNav vn = vg.getNav();
                int i = vn.getText();
                //print "good"
                Console.WriteLine("text ---> " + vn.toString(i));
                if (vn.overWrite(i,eg.GetBytes("bad")))
                {
                    //overwrite, if successful, returns true
                    //print "bad" here
                    Console.WriteLine("text ---> " + vn.toString(i));
                }
            }
        }
    }
}

尽管简单,但这种“编辑”功能实际上具有意想不到的性能影响。考虑数据库表设计,其中您指定了列宽。您现在可以将相同的技术应用于 XML 组合:通过预先将一些额外的空格序列化到文本节点或属性值中,您可以对这些节点进行“原地”更新,并且无需重新生成索引。您甚至可以在 XML 文档中预先序列化虚拟元素,这些元素包含文本节点或属性值,其初始值完全是空格。这些虚拟元素作为模板,以应对未来的内容更新,如下面的示例所示。

模板

<purchaseorder orderdate="" />
 <purchaseorder orderdate="" />
    <purchaseOrder orderDate=" ">
      <item partNum=" " >
         <productName> </productName>
         <quantity> </quantity>
         <USPrice> </USPrice>
      </item>
    </purchaseOrder>
    <quantity /></quantity />
    <usprice /> </usprice />
 </purchaseorder />
</purchaseorder />

“嵌入”数据后

<purchaseOrder orderDate="1999-10-21">
   <item partNum="872-AA" >
      <productName>Lawnmower </productName>
      <quantity>1 </quantity>
      <USPrice> 100 </USPrice>
   </item>
</purchaseOrder>

同样,XML 内容删除的概念也值得重新思考。与其物理删除 XML 元素,不如通过使其对应用程序“不可见”来禁用 XML 元素,以达到相同的目的。好处:您又避免了重新索引的需要。请注意,这有利于 XML 作为松散编码数据格式的优势。下面是一个将元素的“enable”属性值设置为使其“不可见”的示例。

以前

<purchaseorder orderdate="1999-10-21"" />
  <purchaseOrder orderDate="1999-10-21">
      <item partNum="872-AA" enable="1">
         <productName>Lawnmower</productName>
         <quantity>1</quantity>
         <USPrice>148.95</USPrice>
      </item>
  </purchaseOrder>
</purchaseorder />

操作后

<purchaseOrder orderDate="1999-10-21">
   <item partNum="872-AA" enable='0'>
     <productName>Lawnmower</productName>
     <quantity>1</quantity>
     <USPrice>148.95</USPrice>
   </item>
</purchaseOrder>

应用程序场景

有两种不同的观点可以理解 VTD+XML 作为解决实际问题的实用解决方案。第一种是本地 XML 索引的传统观点。或者,您可以将 VTD+XML 视为一种向后兼容 XML 的二进制数据格式。

本地 XML 索引

从这个角度来看,您只需将 VTD+XML 作为本地 XML 数据存储的基础,以满足 XML/SOA 应用程序的后端数据需求。通过将其作为 BLOB(二进制大对象)保存在更传统的数据库表中,您可以获得额外的功能,如并发、数据完整性和复制。VTD+XML 比尴尬的基于分片(shredding)的 XML 到关系数据映射要好得多,在纯 XML/SOA 环境中非常适用。拥有大量 XBRL(可扩展商业报告语言)文档或那些大型 GML(地理标记语言)文件?VTD+XML 应该为您提供前所未有的强大功能。

二进制增强 XML

VTD+XML 还自然地扩展了 XML 的核心功能,将处理效率提升到一个全新的水平。换句话说,作为一种线上传输格式(wire format),XML 现在拥有了一切:它不仅易于学习、人类可读、可互操作且设计为松散编码,而且在性能方面,它还远远领先于 CORBA、DCOM 和 RMI。应用于 XML 流水线(pipelining)时,VTD+XML 可能会消除流水线每个阶段的重复解析——这是现有 XML 流水线规范(例如 XProc 和 XML 流水线定义语言)都未解决的问题。如果您的 DOM 应用程序通过 ESB(企业服务总线)传输大型文档花费太长时间,那么每秒传输 100MB 的速度听起来如何?

性能比较

本节主要强调了索引加载的性能。对于 VTD+XML,性能数字是索引加载到内存然后运行单个 XPath 的延迟。对于 DOM,性能是 DOM 解析加上运行单个 XPath(Xalan 和 Jaxen)的延迟。结果已标准化,因此显示的是比率。可以看到,当启用索引加载时,性能提升可能非常显著。

图 3. XPath “/*/*/*[position() mod 2=0]”的相对延迟比较

图 4. XPath “/purchaseOrder/items/item[USPrice<100]”的相对延迟比较

图 5. XPath “//item/comment”的相对延迟比较

结论

本文介绍了 VTD+XML 的最新索引功能以及最新的基准测试数据,展示了它实现的效率水平。在 VTD+XML 之前,使用 DOM 或 SAX 编写的 XML/SOA 应用程序会产生 XML 解析、XPath 评估和可选的内容更新的开销。这些开销占应用程序总 CPU 周期的 80%-90% 或更多的情况并不少见。VTD+XML 消除了这些开销,因为没有多少开销可以优化了。使用 VTD+XML 作为解析器可将 XML 解析开销降低 5 倍-10 倍。接下来,VTD+XML 的增量更新独特地消除了更新 XML 的往返开销。此外,本文展示了 VTD+XML 创新的非阻塞、无状态 XPath 引擎,其性能远超 Jaxen 和 Xalan。通过添加索引功能,XML 解析现在已成为“可选”的。换句话说,阻碍 SOA 成功的障碍已悄然消失。但这只是另一个起点。可能不难看出,如果 VTD+XML 坚持过度对象分配(excessive object allocation)如 DOM,那么它的任何好处都不会存在。在 XML 处理的上下文中,XML 信息集的纯面向对象建模(例如,字符串和节点对象)似乎从一开始就不太合适。与其他任何事物一样,面向对象也有其弱点。问题(例如,DOM 和 SAX 的问题)出现在当人们为了选择而选择面向对象,并且停止质疑其合理性时。对我来说,知道何时不使用对象与知道何时使用对象同等重要,甚至更重要。源于这些弱点、约束和限制,VTD+XML 努力成为解决这些问题的简单、合理的答案。

© . All rights reserved.