XPath查询XML文档的注意事项

发表于:2007-06-21来源:作者:点击数: 标签:
老鼠与人最好的排列计划 本文的灵感来自一个诸事不顺的周末。我的那一位决定和同事去拉斯维加斯进行一次庆祝旅行,恰好我计划去宜家家居挑一个书柜,这样我在搬到雷得蒙几个月之后终于可以将我的书松绑了。在宜家家居逛了两个小时之后,我发现一个陈列的书柜

   

老鼠与人最好的排列计划

  本文的灵感来自一个诸事不顺的周末。我的那一位决定和同事去拉斯维加斯进行一次庆祝旅行,恰好我计划去宜家家居挑一个书柜,这样我在搬到雷得蒙几个月之后终于可以将我的书松绑了。在宜家家居逛了两个小时之后,我发现一个陈列的书柜与我房间的色调很相配,只是一些必要的配件缺货。最终我订购了这种书柜,空着手先回家了。然而,我已经把书都解开了,它们散落在房间的各个角落。这成为整理我日益增长的书库的最好机会,当然我选择使用 XML 实现此任务。(本文包含一些指向英文站点的链接。)

  不仅是表面那么简单

  我正在建造的 XML 分类的主要目的是建立一个结构来集中存储我所拥有的书的信息,它应该足够灵活,可以进行查询和各种演示,同时也便于携带。下面是该文档的初稿的摘录:

  Books.xml

<?xml version="1.0" encoding="UTF-8" ?>
<bk:books xmlns:bk="urn:xmlns:25hoursaday-com:my-bookshelf" on-loan="yes" >
<bk:book publisher="IDG books" on-loan="Sanjay" >
<bk:title>XML Bible</bk:title>
<bk:author>Elliotte Rusty Harold</bk:author>
</bk:book>
<bk:book publisher="QUE">
<bk:title>XML By Example</bk:title>
<bk:author>Benoit Marchal</bk:author>
</bk:book>
</bk:books>

  我希望能够使用 on-loan 属性来跟踪我是否将书借了出去。根元素上的 on-loan 属性指定至少一本书被借了出去,然后将每个 book 元素上相同的属性指定借书人。现在看来,这可能不是最好的设计,因为它导致在根元素和子元素之间不必要的联系,但是请诸位谅解,这毕竟只是我的初稿。

  设计好简单的格式后,我决定在上面运行一些实际的查询来看看这种格式是否满足我的需要。我尝试在 System.Xml.XmlNode 类中使用 SelectSingleNode Method 的第一个查询如下:

  //*[position() = 1]/@on-loan

  我本来的意思是“选择文档中的所有节点,然后给我第一个节点的 on-loan 属性。”查询返回以下内容:

  on-loan="yes"

  因此,对问题“我是否有书借出?”的答案是 yes。然而,当我模拟如果一本书被借出之后我没有更新根元素上的 on-loan 值会出现什么情况时发生了一件有趣的事情。我从根元素中删除 on-loan 属性,再次运行查询。结果如下:

  on-loan="Sanjay"

  结果是根元素的某个子元素的值。我怀疑有错,所以又在 MSXML 上尝试查询,还是得到相似的结果。进一步研究使我与小组内的一些 XPath 专家进行了很有启发意义的讨论,并进一步阅读了 XPath 建议。我发现与多方共同设计的重要语言一样,有一些古怪、富有个性及不一致的情况,(即在处理 XPath 时,需要避免的陷阱)。

  缩写及它们的真实意思

  XPath 建议列出了一些轴,这些轴包含了一些与当前选择节点(也称为上下文节点)相关的节点。为避免冗长,指定了某些常用轴的一些缩写。下表显示这些缩写和它们等效的轴。

clearcase/" target="_blank" >cccccc cellSpacing=0 cellPadding=0 width=300 align=center bgColor=#ffffff border=1>
缩写
. self::node()
.. parent::node()
// /descendent-or-self::node()/
@ attribute::


  另一个事实是在每个位置步骤或路径表达式上使用的默认轴是 child:: 轴。因此,/bk:books/bk:book 实际等效于 /child::bk:book/child::bk:book, 这比直接键入要容易得多。

  节点测试用来选择当前轴的主节点类型的所有节点。* 是节点测试,不是步骤的缩写。最后,带有数字的谓词等效于检查上下文节点的位置是否与该数字相同。这意味着查询 /bk:book[1] 等效于 /bk:book[position()=1]。

  有了以上信息,我们可以返回原来出问题的查询,看看为什么会得到意外的结果。//*[position() = 1]/@on-loan 实际是 /descendent-or-self::node()/child::*[position() = 1]/@on-loan 的缩写,它选择文档中的每个节点,检索“每个选定节点的第一个子节点”的 on-loan 属性。明智地使用圆括号可以迅速解决此问题,(//*)[position() = 1]/@on-loan(它是 (/descendent-or-self::node()/child::*)[position() = 1]/@on-loan 的缩写) 是我实际想要的。

  有趣的是,在问题解决之后不久,我意识到实现我要求的更简单和更有效的查询本来可以是:

  /*/@on-loan

  这是一个更好的解决方案,因为它只需要查看文档中的第一个节点。我将保留多个示例,强调为什么一个人应该考虑在某些情况下缩写所表示的内容,以避免令人迷惑不解的结果。

缩写
完整查询
查询结果
//*[1] /descendent-or-self::node()/child::*[position()=1] 选择文档中每个节点的第一个子节点。
(//*)[1] (/descendent-or-self::node()/child::*)[position()=1] 选择文档中第一个节点。


  提高我们的数学技能

  涉及关系或算术运算符和字符串的查询通常导致与直觉不相符的结果。XPath 将涉及关系或算术运算符的表达式中的所有操作数转换为数字。不完全是数字值的字符串将转换为 NaN(不是一个数)。下表显示某些 XPath 表达式、表达式隐式转换成的内容以及表达式的结果。

表达式 隐式转换 结果
'5' + 7 5 + 7 12
'5' + '7' 5 + 7 12
5 + 'a' 5 + NaN NaN
'5' < 7 5 < 7 True
'5' < '7' 5 < 7 True
'5' < 'b' 5 < NaN False
'a' < 'b' NaN < NaN False
'a' > 'b' NaN > NaN False


  必须注意到比较运算符(<、>、<=、>=)不执行字符串值的字典式比较功能。

  另一个有趣的算术定义是虽然定义了一元减号(例如 -6 是有效的 XPath 表达式),但是却未定义一元加号(+6 不是有效的 XPath 表达式)。更令人吃惊的是多重否定可以堆叠在一起,却仍然有效。因此,------6 是有效的 XPath 表达式,等效于值 6。

  XPath 缺乏对科学/指数记数法的支持将使用户犯错,因为支持它的既有流行的查询语言(如 SQL),也有流行的编程语言(如 C++)。

  在节点集合上结合算术和关系运算的表达式还可能导致令人吃惊的结果。节点集合上的算术运算将“集合中第一个节点”的值转换为数字,而关系运算符将判断“节点集合中的任意节点”是否满足条件。下面是一个 XML 文档,用来显示算术运算和关系运算符如何导致不 Associative(结合)的表达式。

   Numbers.xml

<Root>
<Numbers>
<Integer value="4" />
<Integer value="2" />
<Integer value="3" />
</Numbers>
<Numbers>
<Integer value="2" />
<Integer value="3" />
<Integer value="6" />
</Numbers>
</Root>

  下表显示缺乏结合性的算术运算。

表达式 结果 解释
Root/Numbers[Integer/@value > 4 - 1] <Numbers>

<Integer value="4" />

<Integer value="2" />

<Integer value="3" />

</Numbers>

<Numbers>

<Integer value="2" />

<Integer value="3" />

<Integer value="6" />

</Numbers>

选择文档中的所有 <Numbers> 元素,其中“至少一个”<Integer> 元素具有值大于 4 减 1 的 value 属性。
Root/Numbers[ 1 + Integer/@value > 4] <Numbers>

<Integer value="4" />

<Integer value="2" />

<Integer value="3" />

</Numbers>

选择文档中的所有 <Numbers> 元素,其中 1 加上具有值大于 4 的 value 属性的“第一个”<Integer> 元素。



  如果 XPath 是代数结合的,则两种查询将返回同样的结果。

  何时集合不是一个集合?

  虽然节点集合是无序的集合,就象数学(或您喜欢的编程语言)中的集合一样,但是处理它们通常与处理数学意义上的集合不同。XPath 中的某些操作在处理节点集合时使用“第一”语义,而其他操作使用“任意”语义。“第一”语义意味着该操作的节点集合的值从集合中的第一个节点获得,而“任意”语义则意味着节点集合中的操作取决于集合中的任何节点是否满足该条件。标题为“提高数学技能”的小节将介绍使用“任意”和“第一”语义的情况。

  XPath 节点集合与数学集合不同的另一个特征是 XPath 不直接提供机制以执行集合操作(如子集、交集或对称差集)。Michael Kay(XSLT Programmer's Reference 2nd edition 的作者)最早发现如何使用 count() 函数和联合运算符 | 来模拟缺少的集合运算符。下面列出了对上面一节中的 XML 文档执行集合操作的 XSLT 样式表及其输出。

  样式表

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" >

<xsl:output method="text" />

<xsl:variable name="a" select="/Root/Numbers[1]/Integer/@value"/>
<xsl:variable name="b" select="/Root/Numbers[1]/Integer/@value[. > 2]"/>
<xsl:variable name="c" select="/Root/Numbers[1]/Integer/@value[. = 3]"/>

<xsl:template match="/">

SET A: { <xsl:for-each select="$a"> <xsl:value-of select="." />, </xsl:for-each> }
SET B: { <xsl:for-each select="$b"> <xsl:value-of select="." />, </xsl:for-each> }
SET C: { <xsl:for-each select="$c"> <xsl:value-of select="." />, </xsl:for-each> }

a UNION b: { <xsl:for-each select="$a | $b"> <xsl:value-of select="."
/>, </xsl:for-each> }
b UNION c: { <xsl:for-each select="$b | $c"> <xsl:value-of select="."
/>, </xsl:for-each> }
a INTERSECTION b: { <xsl:for-each select="$a[count(.|$b) = count($b)]">
<xsl:value-of select="." />, </xsl:for-each> }
a INTERSECTION c: { <xsl:for-each select="$a[count(.|$c) = count($c)]">
<xsl:value-of select="." />, </xsl:for-each> }
a DIFFERENCE b: { <xsl:for-each select="$a[count(.|$b) != count($b)] |
$b[count(.|$a) != count($a)]"> <xsl:value-of select="." />, </xsl:for-each> }
a DIFFERENCE c: { <xsl:for-each select="$a[count(.|$c) != count($c)] |
$c[count(.|$a) != count($a)]"> <xsl:value-of select="." />, </xsl:for-each> }
a SUBSET OF b: { <xsl:value-of select="count($b | $a) = count($b)"/> }
b SUBSET OF a: { <xsl:value-of select="count($b | $a) = count($a)"/> }

</xsl:template>

</xsl:stylesheet>

  输出

SET A: { 4, 2, 3, }
SET B: { 4, 3, }
SET C: { 3, }

a UNION b: { 4, 2, 3, }
b UNION c: { 4, 3, }
a INTERSECTION b: { 4, 3, }
a INTERSECTION c: { 3, }
a DIFFERENCE b: { 2, }
a DIFFERENCE c: { 4, 2, }
a SUBSET OF b: { false }
b SUBSET OF a: { true }    



  节点集合和数学集合之间差异的最后一点是节点集合通常是有序的。W3C XPath 建议将它们描绘为无序的,但是 XSLT 确实指定了节点集合的顺序。

  标识危机

  在 XPath 中,没有直接确定节点标识或不同节点集合中的等效节点的构造。不直接支持比较,例如由 /bk:books 返回的节点是否与由 /bk:books/bk:book[1]/parent::* 返回的节点相同。在节点集合上使用 = 运算符的比较不将节点集合作为一个整体进行比较,而是使用“任意”语义。从 W3C XPath 建议:

“如果要比较的两个对象都是节点集合,则当且仅当第一个节点集合中有一个节点且第二个节点集合中有一个节点时,该比较才为 true,这样在两个节点的字符串值上进行比较的结果才为 true。”

  为了解释清楚这一点,以下是显示从简介的 XML 分类格式中执行有关节点集合比较操作结果的表格。请注意这些初看起来就象相互矛盾的结果。

表达式 结果 解释
//bk:book = /bk:books/bk:book[1] TRUE 是否 //bk:book 中至少有一个节点与 /bk:books/bk:book[1] 中的另一个节点具有同样的字符串值?
//bk:book != /bk:books/bk:book[1] TRUE 是否 //bk:book 中至少有一个节点与 /bk:books/bk:book[1] 中的另一个节点具有不同的字符串值?
not(//bk:book = /bk:books/bk:book[1]) FALSE 问题“是否//bk:book 中至少有一个节点与 /bk:books/bk:book[1] 中的另一个节点具有同样的字符串值?”的相反答案。

  可以使用 XPath count() 函数模仿节点标识,判断相同长度的两个节点集合的相交部分是否是任意节点集合的同样长度,或者在单一元素节点集合的情况下是否等于 1。例如,以下查询在这种情况下返回 TRUE,因为两个节点都是相同的。

  count(/bk:books | /bk:books/bk:book[1]/parent::*) = 1

  也可以使用 XSLT 中的 generate-id() 函数模仿节点标识。XSLT FAQ 提供使用 generate-id() 的示例。

  我是,故我在

  虽然没有测试节点存在的明确机制,但是它确实在涉及节点集合的一些表达式中隐式发生了。不存在的节点集合表示为空节点集合。在分别涉及字符串和数值操作的情况下,空节点集合隐式转换为空的字符串或 NaN。如果执行查询时没有查看实例文档,从而未确定空节点集合导致发生了(以及没有发生)哪些实例,系列隐式转换可能导致令人混淆的结果。下面是查询的一些示例,涉及空节点集合以及这些隐式转换如何影响它们。

表达式 结果
/NonExistentNode + 5 NaN
/NonExistentNode = 5 False
/NonExistentNode != 5 False
concat(/NonExistentNode, "hello") "hello"
/Root[@nonExistentAttribute] 不返回结果
/Root[@nonExistentAttribute < 5] 不返回结果
/Root[@nonExistentAttribute > 5] 不返回结果


  因为节点可能包含空的字符串,通常最好使用 boolean() 函数,而不是通过检查节点的字符串值来测试节点的存在。例如,以下查询(返回 FALSE)是肯定地告诉您文档中没有 NonExistentNode 的最好方法。   boolean(/NonExistentNode)

  命名空间和 XPath Redux

  在 XPath 中处理命名空间时的缺陷,这个缺陷涉及到即使文档使用默认的命名空间,也必须在表达式中映射前缀和命名空间名称。

  有趣的是对于一个文档,总有至少一个命名空间节点可以使用:XML 命名空间 http://www.w3.org/1998/namespace。例如,看看以下查询:

  /bk:books/namespace::*

  该查询将返回以下内容:

urn:xmlns:25hoursaday-com:my-bookshelf
http://www.w3.org/XML/1998/namespace  

  返回的项是 books.xml 文档根处提供的命名空间节点。

  未涉及的内容

  XML 文档中的某些信息是透明的,或者在某些情况下,对 XPath 是不可见的。XML 文档顶部的 XML 声明就是一种 XPath 看不到的 XML 构造。这意味着不能通过 XPath 来查询 XML 文档的版本、编码或独立状态。

  用于引入在分析 XML 文档的过程中替换的文本的语法构造(例如 CDATA 节和分析的实体)对 XPath 同样是透明的。XPath 将替换文本作为常规文本节点进行处理。

原文转自:http://www.ltesting.net