老鼠与人最好的排列计划
本文的灵感来自一个诸事不顺的周末我的那一位决定和同事去拉斯维加斯进行一次庆祝旅行恰好我计划去宜家家居挑一个书柜这样我在搬到雷得蒙几个月之后终于可以将我的书松绑了在宜家家居逛了两个小时之后我发现一个陈列的书柜与我房间的色调很相配只是一些必要的配件缺货最终我订购了这种书柜空着手先回家了然而我已经把书都解开了它们散落在房间的各个角落这成为整理我日益增长的书库的最好机会当然我选择使用 XML 实现此任务(本文包含一些指向英文站点的链接)
不仅是表面那么简单
我正在建造的 XML 分类的主要目的是建立一个结构来集中存储我所拥有的书的信息它应该足够灵活可以进行查询和各种演示同时也便于携带下面是该文档的初稿的摘录
Booksxml
<?xml version= encoding=UTF ?>
<bk:books xmlns:bk=urn:xmlns:hoursadaycom:mybookshelf onloan=yes >
<bk:book publisher=IDG books onloan=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>
我希望能够使用 onloan 属性来跟蹤我是否将书借了出去根元素上的 onloan 属性指定至少一本书被借了出去然后将每个 book 元素上相同的属性指定借书人现在看来这可能不是最好的设计因为它导致在根元素和子元素之间不必要的联系但是请诸位谅解这毕竟只是我的初稿
设计好简单的格式后我决定在上面运行一些实际的查询来看看这种格式是否满足我的需要我尝试在 SystemXmlXmlNode 类中使用 SelectSingleNode Method 的第一个查询如下
//*[position() = ]/@onloan
我本来的意思是选择文档中的所有节点然后给我第一个节点的 onloan 属性查询返回以下内容
onloan=yes
因此对问题我是否有书借出?的答案是 yes然而当我模拟如果一本书被借出之后我没有更新根元素上的 onloan 值会出现什么情况时发生了一件有趣的事情我从根元素中删除 onloan 属性再次运行查询结果如下
onloan=Sanjay
结果是根元素的某个子元素的值我怀疑有错所以又在 MSXML 上尝试查询还是得到相似的结果进一步研究使我与小组内的一些 XPath 专家进行了很有启发意义的讨论并进一步阅读了 XPath 建议我发现与多方共同设计的重要语言一样有一些古怪富有个性及不一致的情况(即在处理 XPath 时需要避免的陷阱)
缩写及它们的真实意思
XPath 建议列出了一些轴这些轴包含了一些与当前选择节点(也称为上下文节点)相关的节点为避免冗长指定了某些常用轴的一些缩写下表显示这些缩写和它们等效的轴
缩写轴self::node()parent::node()///descendentorself::node()/@attribute::
另一个事实是在每个位置步骤或路径表达式上使用的默认轴是 child:: 轴因此/bk:books/bk:book 实际等效于 /child::bk:book/child::bk:book 这比直接键入要容易得多
节点测试用来选择当前轴的主节点类型的所有节点* 是节点测试不是步骤的缩写最后带有数字的谓词等效于检查上下文节点的位置是否与该数字相同这意味着查询 /bk:book[] 等效于 /bk:book[position()=]
有了以上信息我们可以返回原来出问题的查询看看为什么会得到意外的结果//*[position() = ]/@onloan 实际是 /descendentorself::node()/child::*[position() = ]/@onloan 的缩写它选择文档中的每个节点检索每个选定节点的第一个子节点的 onloan 属性明智地使用圆括号可以迅速解决此问题(//*)[position() = ]/@onloan(它是 (/descendentorself::node()/child::*)[position() = ]/@onloan 的缩写) 是我实际想要的
有趣的是在问题解决之后不久我意识到实现我要求的更简单和更有效的查询本来可以是
/*/@onloan
这是一个更好的解决方案因为它只需要查看文档中的第一个节点我将保留多个示例强调为什么一个人应该考虑在某些情况下缩写所表示的内容以避免令人迷惑不解的结果
缩写完整查询查询结果//*[]/descendentorself::node()/child::*[position()=]选择文档中每个节点的第一个子节点(//*)[](/descendentorself::node()/child::*)[position()=]选择文档中第一个节点
提高我们的数学技能
涉及关系或算术运算符和字符串的查询通常导致与直觉不相符的结果XPath 将涉及关系或算术运算符的表达式中的所有操作数转换为数字不完全是数字值的字符串将转换为 NaN(不是一个数)下表显示某些 XPath 表达式表达式隐式转换成的内容以及表达式的结果
表达式隐式转换结果 + + + + + a + NaNNaN < < True < < True < b < NaNFalsea < b NaN < NaNFalsea > bNaN > NaNFalse
必须注意到比较运算符(<><=>=)不执行字符串值的字典式比较功能
另一个有趣的算术定义是虽然定义了一元减号(例如 是有效的 XPath 表达式)但是却未定义一元加号(+ 不是有效的 XPath 表达式)更令人吃惊的是多重否定可以堆叠在一起却仍然有效因此 是有效的 XPath 表达式等效于值
XPath 缺乏对科学/指数记数法的支持将使用户犯错因为支持它的既有流行的查询语言(如 SQL)也有流行的编程语言(如 C++)
在节点集合上结合算术和关系运算的表达式还可能导致令人吃惊的结果节点集合上的算术运算将集合中第一个节点的值转换为数字而关系运算符将判断节点集合中的任意节点是否满足条件下面是一个 XML 文档用来显示算术运算和关系运算符如何导致不 Associative(结合)的表达式
NumbersXML
<Root>
<Numbers>
<Integer value= />
<Integer value= />
<Integer value= />
</Numbers>
<Numbers>
<Integer value= />
<Integer value= />
<Integer value= />
</Numbers>
</Root>
下表显示缺乏结合性的算术运算
表达式结果解释Root/Numbers[Integer/@value >
]<Numbers>
<Integer value= />
<Integer value= />
<Integer value= />
</Numbers>
<Numbers>
<Integer value= />
<Integer value= />
<Integer value= />
</Numbers>
选择文档中的所有 <Numbers> 元素
其中
至少一个
<Integer> 元素具有值大于
减
的 value 属性
Root/Numbers[
+ Integer/@value >
]<Numbers>
<Integer value= />
<Integer value= />
<Integer value= />
</Numbers>
选择文档中的所有 <Numbers> 元素
其中
加上具有值大于
的 value 属性的
第一个
<Integer> 元素
如果 XPath 是代数结合的则两种查询将返回同样的结果
何时集合不是一个集合?
虽然节点集合是无序的集合就象数学(或您喜欢的编程语言)中的集合一样但是处理它们通常与处理数学意义上的集合不同XPath 中的某些操作在处理节点集合时使用第一语义而其他操作使用任意语义第一语义意味着该操作的节点集合的值从集合中的第一个节点获得而任意语义则意味着节点集合中的操作取决于集合中的任何节点是否满足该条件标题为提高数学技能的小节将介绍使用任意和第一语义的情况
XPath 节点集合与数学集合不同的另一个特征是 XPath 不直接提供机制以执行集合操作(如子集交集或对称差集)Michael Kay(XSLT Programmers Reference nd edition 的作者)最早发现如何使用 count() 函数和联合运算符 | 来模拟缺少的集合运算符下面列出了对上面一节中的 XML 文档执行集合操作的 XSLT 样式表及其输出
样式表
<xsl:stylesheet xmlns:xsl= version= >
<xsl:output method=text />
<xsl:variable name=a select=/Root/Numbers[]/Integer/@value/>
<xsl:variable name=b select=/Root/Numbers[]/Integer/@value[ > ]/>
<xsl:variable name=c select=/Root/Numbers[]/Integer/@value[ = ]/>
<xsl:template match=/>
SET A: { <xsl:foreach select=$a> <xsl:valueof select= /> </xsl:foreach> }
SET B: { <xsl:foreach select=$b> <xsl:valueof select= /> </xsl:foreach> }
SET C: { <xsl:foreach select=$c> <xsl:valueof select= /> </xsl:foreach> }
a UNION b: { <xsl:foreach select=$a | $b> <xsl:valueof select=
/> </xsl:foreach> }
b UNION c: { <xsl:foreach select=$b | $c> <xsl:valueof select=
/> </xsl:foreach> }
a INTERSECTION b: { <xsl:foreach select=$a[count(|$b) = count($b)]>
<xsl:valueof select= /> </xsl:foreach> }
a INTERSECTION c: { <xsl:foreach select=$a[count(|$c) = count($c)]>
<xsl:valueof select= /> </xsl:foreach> }
a DIFFERENCE b: { <xsl:foreach select=$a[count(|$b) != count($b)] |
$b[count(|$a) != count($a)]> <xsl:valueof select= /> </xsl:foreach> }
a DIFFERENCE c: { <xsl:foreach select=$a[count(|$c) != count($c)] |
$c[count(|$a) != count($a)]> <xsl:valueof select= /> </xsl:foreach> }
a SUBSET OF b: { <xsl:valueof select=count($b | $a) = count($b)/> }
b SUBSET OF a: { <xsl:valueof select=count($b | $a) = count($a)/> }
</xsl:template>
</xsl:stylesheet>
输出
SET A: { }
SET B: { }
SET C: { }
a UNION b: { }
b UNION c: { }
a INTERSECTION b: { }
a INTERSECTION c: { }
a DIFFERENCE b: { }
a DIFFERENCE c: { }
a SUBSET OF b: { false }
b SUBSET OF a: { true }
节点集合和数学集合之间差异的最后一点是节点集合通常是有序的WC XPath 建议将它们描绘为无序的但是 XSLT 确实指定了节点集合的顺序
标识危机
在 XPath 中没有直接确定节点标识或不同节点集合中的等效节点的构造不直接支持比较例如由 /bk:books 返回的节点是否与由 /bk:books/bk:book[]/parent::* 返回的节点相同在节点集合上使用 = 运算符的比较不将节点集合作为一个整体进行比较而是使用任意语义从 WC XPath 建议
如果要比较的两个对象都是节点集合则当且仅当第一个节点集合中有一个节点且第二个节点集合中有一个节点时该比较才为 true这样在两个节点的字符串值上进行比较的结果才为 true
为了解释清楚这一点以下是显示从简介的 XML 分类格式中执行有关节点集合比较操作结果的表格请注意这些初看起来就象相互矛盾的结果
表达式结果解释//bk:book = /bk:books/bk:book[
] TRUE是否 //bk:book 中至少有一个节点与 /bk:books/bk:book[
] 中的另一个节点具有同样的字符串值?//bk:book != /bk:books/bk:book[
] TRUE是否 //bk:book 中至少有一个节点与 /bk:books/bk:book[
] 中的另一个节点具有不同的字符串值?not(//bk:book = /bk:books/bk:book[
])FALSE问题
是否//bk:book 中至少有一个节点与 /bk:books/bk:book[
] 中的另一个节点具有同样的字符串值?
的相反答案
可以使用 XPath count() 函数模仿节点标识判断相同长度的两个节点集合的相交部分是否是任意节点集合的同样长度或者在单一元素节点集合的情况下是否等于 例如以下查询在这种情况下返回 TRUE因为两个节点都是相同的
count(/bk:books | /bk:books/bk:book[]/parent::*) =
也可以使用 XSLT 中的 generateid() 函数模仿节点标识XSLT FAQ 提供使用 generateid() 的示例
我是故我在
虽然没有测试节点存在的明确机制但是它确实在涉及节点集合的一些表达式中隐式发生了不存在的节点集合表示为空节点集合在分别涉及字符串和数值操作的情况下空节点集合隐式转换为空的字符串或 NaN如果执行查询时没有查看实例文档从而未确定空节点集合导致发生了(以及没有发生)哪些实例系列隐式转换可能导致令人混淆的结果下面是查询的一些示例涉及空节点集合以及这些隐式转换如何影响它们
表达式结果/NonExistentNode + NaN/NonExistentNode = False/NonExistentNode != Falseconcat(/NonExistentNode hello)hello/Root[@nonExistentAttribute]不返回结果/Root[@nonExistentAttribute < ]不返回结果/Root[@nonExistentAttribute > ]不返回结果
因为节点可能包含空的字符串通常最好使用 boolean() 函数而不是通过检查节点的字符串值来测试节点的存在例如以下查询(返回 FALSE)是肯定地告诉您文档中没有 NonExistentNode 的最好方法 boolean(/NonExistentNode)
命名空间和 XPath Redux
在 XPath 中处理命名空间时的缺陷这个缺陷涉及到即使文档使用默认的命名空间也必须在表达式中映射前缀和命名空间名称
有趣的是对于一个文档总有至少一个命名空间节点可以使用XML 命名空间 例如看看以下查询
/bk:books/namespace::*
该查询将返回以下内容
urn:xmlns:hoursadaycom:mybookshelf
返回的项是 booksxml 文档根处提供的命名空间节点
未涉及的内容
XML 文档中的某些信息是透明的或者在某些情况下对 XPath 是不可见的XML 文档顶部的 XML 声明就是一种 XPath 看不到的 XML 构造这意味着不能通过 XPath 来查询 XML 文档的版本编码或独立状态
用于引入在分析 XML 文档的过程中替换的文本的语法构造(例如 CDATA 节和分析的实体)对 XPath 同样是透明的XPath 将替换文本作为常规文本节点进行处理