博客园  :: 首页  :: 联系 :: 订阅 订阅  :: 管理

XPath 2.0

Posted on 2007-02-13 18:17  ╁蓝驿┲→  阅读(422)  评论(0编辑  收藏  举报
 

XPath 2.0 简介

发布日期: 1/11/2005 | 更新日期: 1/11/2005
*

两年之前,本专栏开始第一次连载时,我曾撰写过有关 XPath 版本 1.0 的文章(请参阅 The XML Files:Addressing Infosets with XPath 以快速回顾)。正如最初的规范中所述:XPath 是一种用于寻址 XML 文档内容的语言。”通过使用 XPath 表达式,开发人员可以轻而易举地标识出 XML 文档中的节点,以进行进一步的处理。这使得利用简单的声明表达式替换复杂的遍历算法成为可能。例如,以下表达式将标识出所有为 Invoice 根元素子代且 Sku 属性值为 123 的 LineItem 元素的子元素:

/Invoice//LineItem[@Sku='123']/*
            

利用传统的 XML API 编写同样的逻辑,将是一项冗长乏味且充满艰辛的工作。因此,XPath 通常会被作为如今各种 API 实现(如 DOM 和 XPathNavigator 等等)中的层次服务而受到支持:

// C# DOM Code
            XmlNodeList nodes =
            doc.SelectNodes("/Invoice//LineItem[@Sku='123']/*");
            for (int i=0; i<nodes.Count; i++) {
            ... // process selection here
            }
            

XPath 还经常用于 XSLT 1.0 中,以寻址输入文档,如下所示:

<xsl:apply-templates
            select="/Invoice//LineItem[@Sku='123']/*"/>
            

总体上来说,XPath 对于 XML 开发人员而言非常重要,并迅速成为他们编程工具包中不可或缺的一部分。本专栏的其余部分假设读者熟悉 XPath 1.0(请参阅 http://www.w3.org/TR/xpath)。

XPath 1.0 限制

尽管 XPath 1.0 简化了许多一般的编程任务,但是,开发人员开始想要得到更多好处。XPath 1.0 规范在几个领域中受到了限制或混淆,需要进行一番整改。开发人员一直致力于改进 W3C,并向该语言添加了一些更为有意义的功能,其中多数功能支持对 W3C XML 规范的其他引申(如 XML 架构、XML Query 1.0 以及 XSLT 2.0)。

自从 XPath 1.0 发布以来,XML 架构就成为 W3C 推荐项之一,并讯速定位为其他数个进行中的工作的“官方”类型系统,例如 XQuery 和其他与 Web 服务有关的工作。因为 XML 架构是 XML 全局中不可或缺的组成部分,所以 W3C 强烈希望实现类型化 XPath(能够选择类型 double 的所有元素不是非常好吗?)。而且,最近有关 XQuery 1.0 和 XSLT 2.0 的工作揭示了大量的共同基础,即两种语言在其中均可以共享相同的数据模型和表达式语法的领域。这个最小公分母就变成了 XPath 2.0。

XPath 2.0

有关 XPath 2.0 (http://www.w3.org/TR/XPath20) 的工作很好地修复了 XPath 1.0 中的问题并达到了下列要求(位于 http://www.w3.org/TR/xpath20req 的 XPath 2.0 Requirements 文档中对此进行了概述):

应当保持向后兼容性

必须提升易用性

必须改善字符串操纵和匹配

必须支持 XML 系列标准(XSLT 2.0 和 XQuery 1.0)

必须支持 XML 架构(简单类型和复杂类型)

并不绝对要求向后兼容性的原因在于,它不如可能会形成阻碍的其他要求重要,虽然 W3C 已经尽全力来全面达到该要求。

提升易用性和简化常见用例(例如使用字符串)是非常清楚的目标,但是最后两个要求带来了更为明显的变化。在本专栏的剩余部分中,我将重点介绍这一更为显著的方面,以帮助您对即将到来的事物作好准备。

如果您在阅读本专栏时,希望试验某些示例表达式,请下载 Michael Kay 的 SAXON 7.2 XSLT 处理器,该处理器包含了一个相当完整的参考实现(请参阅 http://saxon.sourceforge.net/saxon7.2/),除了没有包含完整的 XML 架构支持。截至本文撰写之日止,任何 Microsoft XML 处理器(或 API)均不支持 XPath 2.0,但是预计它们的 XQuery 1.0 和 XSLT 2.0 实现支持它。虽然没有确定特定的发布日期,但是将其与该规范的发布之日(或许会在来年)相结合会比较有意义。

一个控制所有的数据模型

可在 XPath 2.0 的数据模型中找到其最具意义的变化。可在 XQuery 1.0 和 XPath 2.0 数据模型规范中找到 XPath 2.0 数据模型的详细信息,而且,该信息由 XPath 2.0、XSLT 2.0 和 XQuery 1.0 共享。XPath 2.0 数据模型基于具备支持 XML 架构的必要扩展的 Infoset。该数据模型定义了七个构成 XML 文档结构的节点类型:文档(根)、元素、文本、属性、命名空间、处理指令以及注释节点。

这些节点类型类似于 XPath 1.0 数据模型中的节点类型,但是,元素和属性类型已受到扩展,以在验证出现后包括 XML 架构类型信息。因此产生的“类型化”数据模型被引用为后架构验证 Infoset (PSVI)。

除基本的节点类型外,XPath 1.0 还定义了四个原子类型,以协助处理在元素和属性中发现的文本值:节点集、字符串、数字以及布尔值。这个简单的类型系统使得在必要时,可以将文本值作为特定类型来处理。例如,您应该考虑以下价格元素版本:

<price>10</price>
            <price>10.0</price>
            <price>10.00</price>
            

在纯文本环境中测试特定值非常麻烦,因为您必须检查所有可能的词法格式:

price = '10' || price = '10.0' || price = '10.00'
            

但是,如果配备了数值类型,您就可以使用以下表达式以自动将文本强制转换为数值:

number(price) = 10
            

这样可使您无需处理文档中可能用到的各种词法格式。

在 XPath 2.0 中,原子类型系统基于该同一原则,但是它已受到扩展,以包含所有在 XML 架构数据类型规范中定义的数据类型(请参阅 http://www.w3.org/TR/xmlschema-2/)。图 1 显示了 XML 架构数据类型层次结构,其中的派生通过限制出现。除有 19 种基元数据类型和更多的派生类型外,XPath 2.0 还定义了两个新的持续时间子类型,即 yearMonthDuration 和 dayTimeDuration,以简化持续时间值的使用。所有这些类型都可以在 XPath 2.0 中使用。

和以往一样,XPath 2.0 类型系统可以使开发人员避免遇到过多的词法细节,允许他们主要处理值空间,但是从现在开始,有更多的值空间需要进行选择。您可以通过数据函数访问节点的类型化值,并可以利用 XQuery 1.0 和 XPath 2.0 函数和运算符规范中定义的各种构造函数来构造类型化值(或 cast 值)。例如,以下表达式测试了是否任何 birthdate 元素的值(或其中 xs 被绑定至架构命名空间的类型 xs:date)都表示 1972 年 01 月 01 日之前的日期:

data(birthdate) < xs:date('1972-01-01')
            

除能够使用强类型化值外,PSVI 可以使您采用任何元素或属性以及在运行时访问其类型信息,正如今天您可以访问基本名称信息一般。可将元素绑定至简单类型或复杂类型,而始终将属性绑定至简单类型。有几个新的利于类型检查和强制的运算符:instance of、cast as 和 treat as。

运算符 instance of 可以用于验证操作数是否为特定类型,如下面的相应表达式所示:

LineItem/node()[. instance of element of type xsd:double]
            LineItem/*[. instance of xsd:double]
            

该表达式将标识出类型为 xsd:double 的 LineItem 元素的所有子元素。运算符 cast as 可以将一种数据类型显式转换为另一种数据类型。该运算符也许不能用于用户定义的类型。另一方面,运算符 treat as 也许可以用于用户定义的类型,以检查提供表达式的值是否是提供数据类型的实例。如果是,则 treat 表达式会返回该表达式的值,否则它会生成一个错误。Treat 通常用于保证某值的动态类型是该值的静态类型的特定子类型。XQuery 1.0 和 XPath 2.0 函数和运算符规范为类型间的强制转换定义了许多的强制规则。这或许是该规范中最为复杂和令人不知所措的部分。

在数据模型中,另一个主要更改针对节点集或节点集不足的地方。在 XPath 1.0 中,某些表达式返回节点集,而另一些返回原子值。在 XPath 2.0 中,返回的就全是序列了。

序列

序列,顾名思义:就是一个包含零或包含多个项的排序系列。一个项目可能是一个原子值,或七个节点类型之一。在将某个节点放入序列时,该节点会维护其标识,并将该标识重复到相同序列中。

序列与 XPath 1.0 节点集非常不同,后者是未排序的,并仅包含没有重复的节点。尽管节点集是未排序的,但是它们仍然由 XSLT 1.0 按文档顺序进行处理。为了有助于维护向后兼容性,XPath 2.0 路径表达式始终以不带重复项的文档顺序生成序列(该保证不适用于其它表达式类型)。在 XSLT 2.0 中,只需根据该序列的顺序来处理序列即可。正如您将在稍后所见,序列可以让您以更大的灵活性来控制处理。

XPath 2.0 中的每个值都是序列。使用逗号分隔符在括号内编写序列,如下所示:

("Nathan", 1.32e0, true(), xs:date('2001-05-24'))
            

该序列包含四个类型的值:分别是 xs:string、xs:double、xs:Boolean 以及 xs:date。以下序列依次包含 Sku 元素列表、Price 元素列表以及 Description 元素列表:

(//Sku, //Price, //Description)
            

空序列写为:()。实际上,原子值被认为是长度为 1 的序列(指单元素序列)。单元素序列可编写为 ("Nathan") 或 "Nathan"。二者之间没有不同。

序列的另一个重要特征是,它们都是平面的,即序列可能不包含其他序列。例如,以下三个序列完全相同:

(1, 2, 3, 4)
            ((1, 2), (3, 4))
            (((1), (2, 3), (4)))
            

因为整个语言围绕序列而转,所以有各种运算符和函数可与它们协同使用。

集操作

我已经介绍了一个序列运算符,即逗号 (",")。逗号表示串联运算符,可将两个序列串联在一起,以维持排序。有其他几个基于集的可用于序列的运算符:pipe ("|")、intersect 以及 except。

运算符 | 返回两个序列的并集,移除重复项(如在 XPath 1.0 中一样)。例如,假设 $node1、$node2、$node3 以及 $node4 全部都引用相互不同的节点,以下示例说明了一个并集:

($node1, $node2, $node3) | ($node2, $node3, $node4) ->
            ($node1, $node2, $node3, $node4)
            

运算符返回两个序列的交集,移除重复项:

(($node1, $node2, $node3) intersect ($node2, $node3, $node4)->
            ($node2, $node3)
            

运算符返回每个出现在第一个序列但不出现在第二个序列的节点,移除重复项:

($node1, $node2, $node3) except ($node2, $node3, $node4) ->
            ($node1)
            

除这些集操作外,还有几个其他利于使用序列的函数。有关运算符的详细信息,请参阅 http://www.w3.org/TR/xquery-operators

序列处理

XPath 2.0 为基本列表操纵提供了预期函数:insert、remove、item-at、index-of 以及 subsequence。前两个可用于从序列添加和移除项目,如下所示:

insert((1, 3, 4), 2, 2) -> (1, 2, 3, 4)
            remove((1, 2, 3), 2)    -> (1, 3)
            

函数 item-at 根据提供索引返回项目(相当于使用一个简单的定位谓词 [index]),而 index-of 函数返回与提供值匹配的项目的位置:

index-of((10, 20, 30), 20) -> 2
            

Subsequence 返回由提供的开始和结束位置标识的连续子集(作为另一个序列)。

有几种方法可用于检查序列的长度。您可以使用 exists、empty 和 count。Exists 在序列不为空时返回真,否则返回假。Empty 在序列为空时返回真,否则返回假。Count 返回序列内的项目数,正如它在 XPath 1.0 中所做一样。

empty(())         -> true
            exists((1, 2, 3)) -> true
            count((1, 2, 3)   -> 3
            

还有几种方法可用于执行有关序列的数学运算:sum、avg、min 以及 max:

sum(1, 2, 3) -> 6
            avg(1, 2, 3) -> 2
            min(1, 2, 3) -> 1
            max(1, 2, 3) -> 3
            

我喜欢的与序列有关的函数是 distinct-values。该函数返回一个新序列,该序列只包含带有唯一值的项(它移除了带有重复值的项目,如 SQL 中的 SELECT DISTINCT)。它假设序列包含所有节点或所有值,否则会出现错误。以下表达式将标识出文档中所有的唯一 Sku 元素:

distinct-values(//Sku)
            

这比 XPath 1.0 中相应的表达式便利:

//Sku[not(preceding::Sku = .)]
            

还有一个 distinct-nodes 函数,它根据标识而不是值来移除重复的节点。

序列和谓词

在 XPath 1.0 中,可以通过圆括号将谓词应用到特定的节点集。考察以下表达式:

//Price[1]
            

第一眼看过去,它似乎像是以文档顺序返回 Price 元素的表达式,但是,实际上,它将返回每个为其父元素的第一个子元素的 Price 元素(这是由于运算符优先级所致)。您可以通过在希望筛选的节点集外使用圆括号来修复这个问题:

  (//Price)[1]
            

该同一模型适用于序列。您可以使用谓词来筛选出不需要的项目。将谓词应用到序列,将返回一个新序列,其中只包含满足谓词表达式的项目。例如,以下谓词将测试项目的位置(等同于 item-at):

(10, 20, 30)[2] -> (20)
            

该示例使用稍许复杂的谓词来检查子元素的数量以及 id 属性的值:

$seq[count(*) > 1 and @id < 1000]
            

一般而言,您在 XPath 1.0 中学到的有关谓词的知识,也可应用于 XPath 2.0 中的序列上。

序列强制

如果您在需要另一个类型的上下文中使用序列,则会试图执行隐式强制。将序列强制为字符串、数字以及布尔值的规则几乎与 XPath 1.0 中的规则相同。例如,如果要提供一个其中要求字符串的序列,则第一项的值将会被强制为字符串并进行使用。用于计算节点的字符串值的规则如下所示:对于文本/属性而言,它只是值,而对于元素而言,它是子代文本节点的串联。如果要提供一个其中要求数字的序列,则它首先会被强制为字符串,然后再强制为 double。唯一稍许不同的规则是强制为布尔值的规则。如果序列为空,则它会被强制为假。包含单个布尔值的序列被认为是假,否则会将至少包含一个节点的非空序列认为是真。

用于在字符串、数字以及布尔值类型之间显式强制的函数与 XPath 1.0 中的一样,并且有其他几个函数可用于各种 XML 架构数据类型(如xsd:date、xsd:unsignedInteger 等)的强制,如本专栏之前所述。

比较

XPath 1.0 最令人讨厌的地方之一是它对节点集比较的计算方式。例如,考察以下示例表达式:

Catalog/Prices/Price = 9.95
            

如果至少(在 Catalog/Prices 之下)存在一个带有数值为 9.95 的 Price 元素,则该表达式将计算为真。当在布尔值表达式中也如此使用序列时,如果其任何成员的比较为真,则结果为真,否则就是所谓的存在量化。有趣地是,这适用于任何比较运算符(<、<=、>、>= 等),甚至是不等于 (!=) 运算符,如以下代码行所示:

Catalog/Prices/Price != 9.95
            

如果至少(在 Catalog/Prices 之下)存在一个带有不等于 9.95 的数值的 Price 元素,则该表达式将计算为真。因此,如果不是或许,就是非常有可能这两个比较都将为同一节点集返回真。无需多说,这就是导致多数混淆的原因。

通过使用布尔值而不是函数,可以测试序列中的所有项,否则就是所谓的通用量化。例如,为了确定所有的 Price 元素都具有值 9.95,请使用以下逻辑而不是上一个表达式:

not(Catalog/Prices/Price != 9.95)
            

为了验证单个 Price 元素具有等于 9.95 的数值,您必须使用以下逻辑而不是第一个表达式:

not(Catalog/Prices/Price = 9.95)
            

在 XPath 2.0 中,它们通过辨别不同的比较表达式,以及使得在存在量化和通用量化之间进行显式选择成为可能,来试图消除尽可能多的混淆。使用标准的 =、!=、<、<=、> 以及 >= 运算符的一般比较表达式,会保持我介绍过的 XPath 1.0 的默认行为(存在量化或“for any”语义),以保留向后兼容性。它们还添加了几个新函数,即 sequence-deep-equal 和 sequence-node-equal,用于分别根据值或节点标识来比较两个序列。

XPath 2.0 为简单的值比较定义了单独的机制。值比较使用以下运算符:eq、ne、lt、le、gt 以及 ge(分别代表 =、!=、<、<=、> 以及>=),但是对于比较如原子值之类的单个项,XPath 2.0 提供了一些附加的比较,以用于处理单个节点。例如,is 和 isnot 运算符测试节点标识:

Price[1] is Price[1]  -> true
            Price[1] isnot Sku[1] -> true
            

您还可以使用 << 和 >> 运算符来测试节点排序。如果在文档顺序中,第一个操作数节点位于第二个操作数节点之前,则 << 运算符返回真。而 >> 运算符刚好相反。因为值和节点表达式期望单元素,所以“for any”对“for all”语义完全不起作用。

显式量化

XPath 2.0 还使得您可以通过新的表达式类型编写显式量化表达式。您可以表达,在将约束表达式(满足条件)应用到提供序列的节点时,是希望使用存在(一些)量化还是通用(每个)量化:

some $item in //LineItem satisfies
            (($item/Price * $item/Quantity) > 100)
            every $item in //LineItem satisfies
            (($item/Price * $item/Quantity) > 100)
            

如果至少有一个 LineItem 元素的总价 (Price * Quantity) 大于 100,则第一个表达式返回真。如果每个 LineItem 元素的总价都大于 100,则第二个表达式返回真。

因为约束表达式可以是任何内容,所以该模型提供了比 =、!=、< 以及其他运算符更大的灵活性。而且,它对于表达式涉及到的序列数量没有限制,如下面的检查提供序列的笛卡尔坐标的示例所示:

some $x in (1, 2, 3), $y in (2, 3, 4)
            satisfies $x + $y = 4
            

无疑,XPath 2.0 要比 XPath 1.0 更易于使用且具有的灵活性更大。

其他语言增强

现在,在单个步骤中就可以提供多个节点测试,并增强了 XPath 2.0 中位置步骤的方法:

LineItems/(Sku|Price)/text()
            

在 XPath 1.0 中,需要联合利用以下两个表达式来获得同样的结果:

LineItems/Sku/text() | LineItems/Price/text()
            

XPath 2.0 还提供了“for”表达式以用于迭代目的。这一类型的表达式可用于从多个源生成新序列(例如联接),并且该表达式类似于显式量化表达式。例如,以下表达式将生成一个新序列,该序列包含每个 LineItem 元素的总价:

for $i in //LineItem return ($i/Price * $i/Quantity)
            

这会非常有用,因为您可以将新序列传递至另一个函数中,以便将来进行处理。例如,以下表达式将计算出发票总金额:

sum(for $i in //LineItem return ($i/Price * $i/Quantity))
            

这在 XPath 1.0 中是根本不可能的,而在 XSLT 1.0 中,过程会非常冗长,令人厌烦(要求递归和临时的结果树片段)。For 表达式可以用于更为复杂的转换。以下示例将生成一个销售人员及其售出产品的列表:

for $sp in distinct-values(//Salesperson)
            return ($sp,
            for $item in //LineItem[Salesperson = $sp]
            return $item/Description)
            

生成的序列将包含 Salesperson 元素序列,每个序列后他们所售出的 LineItem 元素序列。

XPath 2.0 还提供了一个内置的条件表达式 (if)。该语法包含了传统的 if/then/else 关键字。以下表达式将根据最初价格的量级计算出折扣价格:

if ($Price > 100)
            then ($Price * .90)
            else ($Price * .95)
            

这些新的语言构造可用于任何希望使用 XPath 表达式的地方。

改善的字符串支持

除这些元素外,XPath 2.0 还利用几个新函数增强了自身的字符串处理支持:upper-case、lower-case、string-pad、matches、replace 以及 tokenize。在没有 upper-case 和 lower-case 之前,对字符串使用大字或小写字母的唯一方式是通过冗长且低效的转换函数进行。在没有 string-pad 函数之前,对于以下示例,您必须将递归重新排序:

upper-case('Michael') -> 'MICHAEL'
            string-pad('-', 7)    -> '-------'
            

最后三个函数可将正则表达式引入表格。函数 matches 允许您测试提供的输入字符串是否与提供的正则表达式相匹配。以下示例将根据 U.S. 社会安全编号的标准格式检查 SSNumber 元素:

matches(SSNumber, '\d{3}-\d{2}-\d{4}')
            

函数 replace 使您可以替换模式匹配的子字符串,而函数 tokenize 有利于将字符串分解为由提供的模式分隔的子字符串。正则表达式处理会将语言的灵活性提升至一个全新的级别。

敝帚自珍

XPath 2.0 为 XML 规范系列的中级阶段。其他规范(如 XQuery 1.0 和 XSLT 2.0)将其用作官方寻址语言,可能还有其他更多规范会这样使用它。XPath 2.0 在提升功能并尽可能维持向后兼容性的同时,也提高了使用性。它还添加了 XML 架构支持,承诺为在强类型化环境中工作的开发人员提供附加值。

此时,不应忘记的事实是,也有人反对使用 XPath 2.0。许多 XML 开发人员(尤其是那些深入研究 XSLT 的人)对 XPath 2.0 的日趋复杂有些晕头转向。他们最大的抱怨是,使用它时必须要求支持 XML 架构方可进行工作,而不是使其成为附加的可选层。旷日持久的 XPath 2.0 争论最近达到了白热化状态,有点类似于激烈的政治讨论了。

频繁使用数据库或 Web 服务的开发人员会毋庸置疑地找到与其价值相配的益处。但是,我确实同情那些希望获得全新且改进的 XPath 而无需强制使用强类型的、刻苦学习 XSLT 的人。最终结果会怎样,这很难说。如果您对此事有自己的看法或希望与人分享任何有关该语言的评论,请发送至 public-qt-comments@w3.org 的公共 W3C XPath/XQuery 邮件列表。

将您的疑问和评论发送至 Aaron xmlfiles@microsoft.com

Aaron Skonnard 是 DevelopMentor 的一位讲师/研究员,他在那开发有关 XML 和 Web 服务的全部课程。Aaron 与人合著有 Essential XML Quick Reference (Addison-Wesley, 2001) 和 Essential XML (Addison-Wesley, 2000)。您可以通过 http://Skonnard.com 与他联系。

 



©2007 Microsoft Corporation. 版权所有.  保留所有权利 |商标 |隐私权声明
Microsoft