使用 XPath 查询 XML 文档时要了解和避免的事项

 

Dare Obasanjo
Microsoft Corporation

2002 年 6 月 30 日

老鼠和男人的最佳计划

这篇文章的灵感来自一个周末,在那里我期望的工作实际上已经推出。我重要的另一个决定与同事一起去拉斯维加斯庆祝之旅,这与我去宜家并拿书柜的计划相吻合,这样我终于可以打开我的书包,因为几个月前搬到雷德蒙德。 在宜家周围闲逛了两个小时后,我发现了一个书柜,适合我客厅的配色方案,结果发现一些必要的件缺货了。 我最终点了书柜,空手回家。 不幸的是,我已经在家里拆开了我的书包,它们散落在我的客厅里。 这成为编录新兴库的绝佳机会,当然,我选择使用 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中使用 SelectSingleNode 方法 尝试的第一个查询 。XmlNode 类如下所示:

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

我的意思是“选择文档中的所有节点,然后给我第一个节点的租借属性。该查询返回了以下内容:

   on-loan="yes" 

那么,“我有书出来吗?yes 然而,当我模拟如果我的一本书被借出时未能更新根元素的贷款值,会发生什么时,发生了一些有趣的事情。我从根元素中删除 on-loan 了 属性,并再次运行查询。 结果如下所示:

   on-loan="Sanjay"

此结果是根元素的子元素之一上的值。 怀疑存在 bug,我也在 MSXML 上尝试了此操作,并获得了类似的结果。 进一步的调查使我与我的团队中的一些 XPath 专家进行了启发性的讨论,并进一步阅读 了 XPath 建议。 我发现,与多方设计的任何不平凡的语言一样,处理 XPath 时,有许多古怪、特有、不一致和普通的缺陷需要避免。

缩写及其真正含义

XPath 建议列出了一些轴,这些 包含与当前所选节点相关的节点 (也称为上下文节点) 。 为了减少详细程度,指定了某些常用轴的缩写。 下表显示了这些缩写及其等效轴。

缩写
. self::node ()
.. parent::node ()
// /descendent-or-self::node () /
@ 属性::

此外,每个 位置步骤 或路径表达式上使用的默认轴都是 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 表达式、它们隐式转换为的内容以及表达式的结果。

Expression 隐式转换 结果
'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 是等效于值 6 的有效 XPath 表达式。

XPath 缺乏对 科学/指数表示法 的支持往往会让人大失所望,因为 SQL 等常用查询语言和 C++ 等常用编程语言都支持它。

结合节点集上的算术运算和关系运算的表达式也可能导致令人惊讶的结果。 节点集上的算术运算将 集中第一个节点 的值转换为数字,而关系运算符则评估 节点集中的任何节点 是否满足条件。 下面的 XML 文档将用于说明算术运算和关系运算符如何导致非 关联表达式。

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>

下表显示了算术运算缺乏关联性。

Expression 结果 说明
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>

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

<Integer value="4" />

<Integer value="2" />

<Integer value="3" />

</Numbers>

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

如果 XPath 是代数关联的,则这两个查询将返回相同的结果。

何时不是集?

尽管节点集是无序集合,就像数学 (集或你最喜欢的编程语言) 一样,但在数学意义上,它们通常与集不同。 XPath 中的某些操作在处理节点集时使用 第一个语义 ,而其他操作使用 任何语义。 第一个语义意味着为该操作设置的节点值是从集中的第一个节点获取的,而任何语义都意味着对节点集的操作取决于集中的任何节点是否满足条件。 标题为 改进我们的数学技能 的部分介绍了使用任何和第一个语义的情况。

XPath 节点集与数学集不同的另一个特征是 XPath 不直接提供用于执行集运算的机制,例如子集、交集或对称差。 Michael Kay 是 XSLT 程序员参考第 2 版的作者,最初发现如何使用 count () 函数和 union 运算符|的组合来模拟缺少的集运算符。 下面是一个 XSLT 样式表,用于对上一部分中的 XML 文档及其输出执行设置操作。

样式 表

<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>

OUTPUT

  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 目录格式对节点集执行比较操作的结果。 请注意,在首次查看时,这些看起来是相互矛盾的结果。

Expression 结果 说明
//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 常见问题解答包含 使用 generate-id () 的示例

我存在, 因此我是

虽然没有用于测试节点是否存在的显式机制,但它确实在涉及节点集的多个表达式中隐式发生。 不存在的节点集表示为空节点集。 在分别涉及字符串和数值运算的情况下,空节点集将隐式转换为空字符串或 NaN。 如果在执行查询时不查看实例文档来确定哪些由于空节点集而发生,哪些未执行,则这一系列隐式转换可能会导致结果混乱。 下面是涉及空节点集的一些查询示例,以及这些隐式转换如何影响它们。

Expression 结果
/NonExistentNode + 5 NaN
/NonExistentNode = 5 False
/NonExistentNode != 5 False
concat (/NonExistentNode,“hello”) "hello"
/Root[@nonExistentAttribute] 未返回任何结果
/Root[@nonExistentAttribute < 5] 未返回任何结果
/Root[@nonExistentAttribute > 5] 未返回任何结果

由于节点可能包含空字符串,因此通常最好使用 布尔值 () 函数测试节点是否存在,而不是检查节点的字符串值。 例如,以下返回 FALSE 的查询是确定文档中没有 NonExistentNode 的最好方法。

   boolean(/NonExistentNode) 

命名空间和 XPath Redux

处理 XPath 中的命名空间时的主要缺陷在 我的最后一列中 进行了介绍,涉及在表达式中的前缀和命名空间名称之间创建映射,即使文档使用默认命名空间也是如此。

需要注意的一个有趣的事情是,文档始终至少有一个命名空间节点可用; http://www.w3.org/1998/namespace 这是 XML 命名空间。 例如,查看以下查询:

/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 将替换文本视为常规文本节点。

致谢

在上述 XPath 怪癖和特长名单中有许多贡献者,包括 Julia Jia、Karthik Ravindran、Martin Gudgin、Michael Brundage 和 Michael Rys。 本文的某些方面灵感来自菲利普·瓦德勒和迈克尔·凯撰写的电子邮件。

Dare Obasanjo 是 Microsoft WebData 团队的成员,除其他事项外,该团队还开发 .NET Framework、Microsoft XML Core Services (MSXML) 的 System.Xml 和 System.Data 命名空间中的组件,以及 Microsoft 数据访问组件 (MDAC) 。

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