级别: 中级
Brett McLaughlin, 作家,编辑, O'Reilly Media, Inc.
2005 年 12 月 22 日
通
常人们不把 XPath 看作是一种数据绑定 API。除了作为其他规范的一部分以外,XPath 甚至还没有引起 XML
世界的过多关注。但是只要真正理解了 XPath 是什么以及如何使用它,特别是在 Java™
编程环境中,它就会成为一种强大的数据绑定工具,常常优于传统的数据绑定 API 如 JAXB 或 JaxMe。Brett McLaughlin
的 “实用数据绑定” 专栏首先用两期文章探讨了作为数据绑定工具的 XPath。
到目前为止,本专栏一直专注于数据绑定的传统定义和用法:将 XML 文档转换成 Java 表示并用于通常的 Java 方法中(比如 getName()
和 setAddress()
)。
然后将 Java 对象再转换成 XML 表示,通常序列化(保存)到磁盘。我还讨论过另一种方向的转换,即把 Java 对象转换成 XML,
然后使用转换的 XML(可以通过网络连接发送出去,或者作为应用程序中另一个消费 XML
的组件的输入)。这都是非常有效和实用的数据绑定的用例,但是还没有包括所有的可能性。本文和下一期文章将介绍另一种方法,即使用 XPath
技术进行数据绑定。
如果您使用数据绑定很长时间了,通常会提供一个约束集(比如 XML 模式或 DTD),用 API
生成表示该约束集的类。然后使用这些类加载 XML 数据,将其中的数据序列化为 XML。这是一种完善的解决方案,但是并非惟一的不使用 SAX 或
DOM 等底层 API 从 XML 文档中读取数据的方法。XPath,您可能听说过这种规范,就是另一种方法。即使您没有 听说过 XPath,也可能在某些时候已经用到过它。通过本文,您将了解 XPath 是什么,知道如何使用它,看看它为什么像一种数据绑定 API,甚至可以漂亮地完成其他数据绑定任务很难实现的工作。
总是为他人作嫁衣……
XPath 是最常用而不为人知的 XML 技术之一。它实际上是最 常用的 XML 技术 —— XML 转换必不可少的组成部分。在将来的 Web 中它也起着重要的作用,因为它对 XLink 和(特别是) XPointer 至关重要。甚至 XForms 也暗中要用到它。
那么人们为什么不把 XPath 看作一种独立的技术呢?很大程度上是因为其他语言中独立处理 XPath 的 API 还刚刚出现。但是如果使用过 XSLT 或 XPointer,或者处理过更先进的 XML Schema,那么您可能已经走到前面了。
XPath 在 XSLT 中的作用
如果使用过 XSLT,几乎可以肯定曾经看到过 XPath 而且至少在一定程度上看着面熟,即使您并没有意识到。看一看 清单 1,这是一个非常简单的 XSL 样式表的一部分。
清单 1. XSL 样式表中的 XPath
<xsl:template match="address"> <h1>Addresses</h1> <hr /> <table> <tr><th>Street</th><th>City</th><th>State</th><th>Zip Code</th></tr> <xsl:apply-templates select="address" /> </table> </xsl:template> <xsl:template match="address" /> <tr> <td><xsl:value-of select="street" /></td> <td><xsl:value-of select="city" /></td> <td><xsl:value-of select="state" /></td> <td><xsl:value-of select="zipCode" /></td> </tr> </xsl:template>
|
比方说,当您看到 select="address"
或者 select="zipCode"
的时候,您看到的就是应用中的 XPath。包围在引号中的文本就是 XPath 表达式,尽管非常简单,要让样式表工作却是不可或缺的。
实际上,即使编写最简单的 XML 转换,如果样式表中没有 select
会怎么样?根本不可能!这是因为 XPath 是 XML 转换从而也是 XSLT 的完整的一部分。如果您认为自己是一位 XSL 专家,您可能比自己认为的更熟悉 XPath。
XPointer 中的 XPath
XPointer 是另一种大量使用 XPath 的 API,尽管不像 XSL 那么常用,但是也逐渐展露头角。清单 2 展示了一篇文档中指向另一篇文档的链接。
清单 2. XPointer 上下文中 XPath 的用法
<link xmlns:xlink="http://www.w3.org/2000/xlink" xlink:type="simple" xlink:href="cd.xml#xpointer( /cds/cd[@title='August and Everything After'])">
|
|
要考虑的一些问题
注意指向 XML 文档的一部分的 XPath 代码例子。XPath 表达式没有涉及到元素或属性,而是直接包含对这些 XML 结构名称的引用。这应该让您想起数据绑定。
|
|
这是一段稍微复杂的 XPath。它选择了根元素 cds
中的 cd
元素,该元素有一个 title
属性等于 August and Everything After
,这些内容都在 cd.xml 文档中。讨论这些似乎有点为时过早,但是先不要担心语法。关键是要看到 XPointer 和 XSL 一样大量使用了 XPath,事实上没有它 XPointer 就无法存在。再说一次,XPath 是用于选择数据的重要组件。
在 XForms 中使用 XPath
XForms 是一种相对较新的 XML 技术,比不上 XSL 普及,甚至还比不上 XPointer 或 XLink。但是仍然值得提一下,因为它在 input
元素的 ref
属性中使用 XPath 表达式。可以设置一个 input 并将其绑定到 XML 文档中的特定元素(或属性),如 清单 3 所示。
清单 3. XForms 组件使用 XPath 引用绑定到表单的 XML 文档
<input ref="xhtml:html/body/xhtml:p[@id='greentea']/ xhtml:description" />
|
清单 3 中的 XForms 语句将输入控件绑定到 XHTML 文档的特定元素。在这里 XPath 同样是指定具体元素的关键。后面将提到它允许使用一些非常高级的选择条件。
在很大程度上 XForms 是一种尚未得到支持的技术,但是当使用 XForms 的时候,现在学习的 XPath 是一种很好的工具。将这些关于 XSL、XLink 和 XInclude 的知识结合起来就够了!
选择内容:基础
现在您已经确信 XPath 非常有用而且无处不在了,下面开始学习语法吧。如果刚接触 XPath,本课程将帮助您开始学习和领会 XPath 的结构。如果是一位 XSL 或 XLink 和 XPointer 老手,也可了解为什么 要构造那些奇怪的路径。您可能已经知道获取感兴趣的数据的其他方法,也许是更好的方法。
首先要认识到称 XPath 为一种语言有点过于郑重其事。它实际上是选择和处理 XML 文档中的内容的一种语法。即使在 XPath 中使用的函数和运算符也都与选择有关。在 XPath 中不会创建变量,比方说不可能运行 一个 XPath 程序。没有这些东西。如果您开始认识到 XPath 是一种巧妙的、有益的选择 XML 元素和属性的方法,然后使用选择的值,您就已经领先于大多数 XML 开发人员了。XPath 不是要对 XML 做 什么事情,而是要从 XML 中取得一些内容。
选择元素
|
设置上下文
设置上下文不是 XPath 的工作。事实上,根本不能 用 XPath 设置上下文。相反,要用其他使用 XPath 在文档中导航的 API 来设置上下文。比如,XSLT 根据应用的模板设置上下文,XForms(一般来说)每次都从根上下文开始。因此理解上下文更多的是需要了解使用 XPath 的 API,而不是 XPath 本身。
|
|
对于 XPath 来说第一步是找到一个引用元素的句柄。可是在选择元素之前需要理解当前上下文。上下文就是在 XML 文档中的位置。比方说,您可能在根元素上,那根元素就是上下文。当然也可能在第一个 person
元素的第二个 address
元素上。在开始移动和选择内容之前,需要知道上下文。
只要理解了上下文,就可以掌握 XPath 语法了。以 清单 4 中的 XHTML 文档为例。
清单 4. Head First Lounge 的 XHTML
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" /> <title>Head First Lounge Elixirs</title> <link type="text/css" rel="stylesheet" href="../lounge.css" /> </head> <body> <h1>Our Elixirs</h1> <h2>Green Tea Cooler</h2> <p class="greentea"> <img src="../images/green.jpg" alt="Green Tea Cooler" /> Chock full of vitamins and minerals, this elixir combines the healthful benefits of green tea with a twist of chamomile blossoms and ginger root. </p> <h2>Raspberry Ice Concentration</h2> <p class="raspberry"> <img src="../images/lightblue.jpg" alt="Raspberry Ice Concentration" /> Combining raspberry juice with lemon grass, citrus peel and rosehips, this icy drink will make your mind feel clear and crisp. </p>
<h2>Blueberry Bliss Elixir</h2> <p class="blueberry"> <img src="../images/blue.jpg" alt="Blueberry Bliss Elixir" /> Blueberries and cherry essence mixed into a base of elderflower herb tea will put you in a relaxed state of bliss in no time. </p> <h2>Cranberry Antioxidant Blast</h2> <p> <img src="../images/red.jpg" alt="Cranberry Antioxidant Blast" /> Wake up to the flavors of cranberry and hibiscus in this vitamin C rich elixir. </p> <p> <a href="../lounge.html">Back to the Lounge</a> </p> </body> </html>
|
如果在 清单 4 中的上下文是 html
元素,就可以直接使用名称选择一个子元素。比如选择 body
元素可使用 XPath 表达式 body
。如果想访问嵌套在 body
中的 h1
元素则使用 body/h1
。如果认为这有点类似于目录路径,那么就对了。选择元素使用 元素名/子元素名
这样的形式。
不过在前进之前要认识到 XPath 表达式可能返回多个 元素。结果元素集称为节点集。(事实上,用 XPath 选择的所有实体 —— 元素、属性和值 —— 都称为节点。)比如下面的路径:
清单 4 中 body
有五个不同的 p
子元素,因此那个表达式返回包含五个节点的节点集(正如您所期望的,不 只是第一个 p
元素)。注意,有时候您得到了预期的结果,但是很可能比您打算 要求的多。
还有更多需要注意的地方。现在给出的表达式仅仅返回实际的元素。可能还要获得这些节点的值。如果需要元素的值(假设元素包含文本数据),需要使用 text()
函数。要获得第一个 h1
的文本,应使用 body/h1/text()
,对于清单 4 将返回 Our Elixirs
。
选择属性
当然可以选择的不仅仅是元素。前面已经提到还可以选择属性。选择属性的时候要在属性名前使用 @
符号,其他方面和元素都一样。
再回到 清单 4 这个例子,可以使用 head/meta/@http-equiv
返回 meta
元素 http-equiv
属性的值。类似地,head/link/@type
返回 link
元素的 type
属性。
必须知道,选择属性的时候您得到的是它的值,而不像元素那样返回节点。因此对于属性选择器,返回值(虽然不确切但是很有用)是一个值,对于一个元素则是一个节点集(包含一个节点)。
在文档中移动
目前为止看到的都是理想的情况,特别是使用文档的根作为上下文比较简单。但是显然情况并非总是如此。这正是目录结构的真正有用的地方。要从当前上下文移动到上一级,只要使用 ..
运算符。比方说,假设上下文是 body
元素,现在需要获得网页的标题(包含在文档 head
下的 title
元素中),可以使用 ../head/title/text()
。如果您开始感觉到就像是改变 UNIX 或 Mac OS X 终端上的目录,这就对了!
如果上下文是 body
中第二个 p
的 img
元素,要得到标题该怎么办呢?可以使用 ../../../head/title/text()
。但是数这些 ../
很快就变得单调乏味了。因此能直接跳到根元素就好了,再想一想 UNIX,使用 /
返回到根元素就毫不奇怪了。同样的选择器更简单的形式为 /html/head/title/text()
。虽然没有缩短,但是更清楚了。有了 ../
和 /
之后,基本上就能移动到需要的任何地方了。
多重选择
考虑到使用一个 XPath 表达式可以选择多个节点,事情就更有趣了。您可以使用对应多个节点的简单表达式,比如 /html/body/h2
来进行多重选择。但是也可使用通配符 进一步精化这些选择。XPath 中有三个统配符:
-
*
与任何元素匹配,无论元素名是什么
-
node()
与所有的节点类型匹配(元素、文本节点、注释、属性,等等)
-
@*
与所有的属性节点匹配,无论属性名是什么
比如,可以使用 /html/body/*
选择 body
元素的所有直接子元素。也可用 /html/head/meta/@*
选择 meta
标记的所有属性。
所有这些情况下,要记住您得到的是一个节点集,因此不应该满足于处理某个值就结束(除非您已经选择了一个属性,稍后要讲到)。但是,只要使用方法、函数或模板处理多个节点,这些技术就很重要。
更有趣的东西
基本的东西很好,但有时候还需要点特别的让您身边的同事眼花缭乱的东西,或者需要一点特殊的 功能。这种情况下这些基础可能就不够了。虽然很难巨细无遗地介绍 XPath,但下面给出一些高级技巧,可以帮助您在 XPath 应用程序中取得需要的节点(或节点集)。
更大的一般性
到目前为止,看到的路径都是选择一个节点,并假设您知道该节点在何处。比方说,您知道 title
、img
或 p
在文档中什么位置,只需要导航到这些元素。但有时候可能希望打破这些结构,仅仅撷取某种元素而不管其位置(或者无论给定的起始上下文的位置)。为此可使用后代选择器。
后代选择器用双斜线 //
表示。使用它告诉 XPath 选择所有指定的节点,无论嵌套得多深。比如这个简单的特定于 XHTML 的 XPath:
XPath 选择嵌套在 XHTML 的 body
元素中的所有table
元素,不论直接嵌套在 body
中(如 /html/body/table
),还是嵌套了多层(如 /html/body/table/tr/td/table
)。这种情况下,嵌套的 table
和顶层 table
同时被选中。
|
不是很合 XML 的风格
在标准 XML 术语中,属性不属于 元素,而是与元素关联,有时候也说在元素上。但是,XPath 不能处理元素及其属性之间的关系。因此只能尽自己最大的努力:将属性看作是属于它们所在的元素。因此选择 p 的 class 属性要使用 p/@class 。要访问属性所在的元素,可使用 @class/.. 。实际上是从属性上移 一层而选择元素。从技术上说这不是很好的 XML,但却是完全正确的 XPath。
|
|
当与属性结合使用的时候就变得很有趣了(使用 @
)。比方说,假设要选择具有 id
属性的所有元素。可使用 //@id
,首先跳回根元素然后选择文档中的所有 id
属性。但是实际上要访问的是元素而不是属性,因此需要从属性上移一层到包含这些属性的元素:
您应该尝试结合这些不同的方法,会看到一些很有意思的结果。
匹配条件
假设有一个条件希望作为匹配的基础,比如需要 class
属性值为 greentea
的 p
元素。如果知道如何使用方括号,这一点在 XPath 中很容易做到。下面是一个例子:
/html/body/p[class="greentea"]
|
方括号用于指定条件,可使用 =
甚至 <
和 >
。也许要用一个范围更广的表达式:
明白了吗?甚至还能更进一步,选择属于 greentea
类的所有 元素而不论其元素类型是什么:
//@class[.="greentea"]/..
|
这个表达式可能看起来有点奇怪,但很容易解释。首先,//
意味着从根元素开始选择所有的元素(与后面的选择器及条件匹配)而不论其嵌套的位置。然后 @class
选择文档中所有的 class
属性。后面的 [.="greentea"]
有点神秘。="greentea"
部分容易理解,它用 greentea
匹配等式左侧的值。在这里就是 .
,这个标志还没有见到过。但是再想一想目录,..
选择父节点(或者父目录),.
则选择当前节点。因此 //@class[.="greentea"]
选择值为 greentea
的所有 class
属性。然后再移动到这些属性所在的元素:
//@class[.="greentea"]/..
|
现在看起来有点不可思议,但是应该熟悉这类奇怪的表达式。当需要访问特定的元素、属性或节点集时它们很有用。
计算
随着越来越多的使用属性选择器,最终将与处理节点一样经常地使用值(来自属性)。如果处理过非常典型的 XML,一定会遇到数字。XML 文档常常把数字值放在属性中(有时候在元素中)。因此这一节讨论 /people/person[firstname = "John"]/@born
和 /people/person/numChildren/text()
这类表达式的结果(当然,使用元素表示子女的个数不够典型,不过就用下面的例子吧)。
这时候您会发现 XPath 的计算能力很有用。可以像其他编程语言中那样使用 +
、-
和 *
。此外 div
用于除法,mod
则表示求模(两数相除的余数)。比如,假设 XML 文档中包含用四位数表示的出生年份,现在需要改成两位数的形式,首先用下面的表达式取得实际的出生年份:
/people/person/birthdate/@year
|
这样就得到了出生年份(可能是 1976 或者 1945)。然后只需要减去 1900:
(/people/person/birthdate/@year) - 1900
|
当然这种方法很有限,新千年出生的孩子和那些历史人物都会造成这一公式的失败。因此应该用 mod
:
(/people/person/birthdate/@year) mod 100
|
(顺便说一下,应该告诉那些研究 Y2K 的人您省略了年份的前两位数。)
字符串技巧
最后,XPath 还提供了一些很棒的字符串处理功能。XML 基本上都是文本,属性值和元素中的数据通常是文本,因此不用奇怪 XPath 支持某些字符串操作。下面仅仅是 XPath 提供的处理字符串的少数函数:
-
string()
将数据转换成字符串格式,如果还不是字符串的话。
-
starts-with(full-string, start-string)
返回一个 Boolean 值,检查 full-string
是否以 start-string
开始。
-
contains(full-string, contains-string)
返回一个 boolean 值,检查 full-string
是否包含 contains-string
。
-
string-length(string)
返回 string
的长度。
-
normalize-space(string)
去掉 string
两端和内部的空格。
大部分函数的功能都很明确。starts-with("McLaughlin", "Mc")
返回 true
,contains("McLaughlin", "augh")
同样如此。string-length("Brett")
显然返回 5
,normalize-space(" Brett McLaughlin ")
则返回 "Brett McLaughlin"
。是不是很简单?当然这些可用于所有 XPath 表达式的返回值,比如 /html/body/p[class='greentea']
。因此要获取 清单 4 中 p
的文本可用 normalize-space()
:
normalize-space(/html/body/p[class='greentea'])
|
更妙的是可用 string()
取出多个元素的文本。比如,如果希望取得 清单 4 所示 XHTML 中所有p
元素的所有 文本,可用:
normalize-space(string(//p))
|
分析最后一个例子可以了解很多内容:
-
//p
选择文档中所有的 p
元素,无论其位于何处。
-
string(//p)
获取这些元素的内容组成一个大字符串。但是字符串中包含很多无用的空白,因此还要进一步处理。
-
normalize-space(string(//p))
规范化内容中的空白字符,得到您希望的文本。
这是一篇关于数据绑定的文章吗?
现
在您可能已经忘记正在阅读的是一篇关于数据绑定的文章。但是先不要忙于跟过去几年中您读过的其他 X* 文档和 API
比较。您读到的实际上是另一种数据绑定方法。考虑这样一种观点,数据绑定是在 Java 代码中保留 XML
文档逻辑(或者语义,如果您喜欢的话)意义。如果 XML 文档中的 address
元素包含 street
、city
和 state
子元素,想要用 getAddress().getStreet()
这样的形式得到这些元素的值,比如:
Address address = getAddress(); System.out.println(address.getCity() + ", " + address.getState());
|
这就是某种基本的数据绑定形式。但是在 XPath 中也能做到同样的事!可以利用 address/street/text()
或者 /person[last-name="Gosling"]/address[@type="work"]/city
这样的 XPath 表达式来得到街道名。初看起来与前一个例子不同,但是它仍然合理地使用了 XML 数据,您要的是 person、address 和 street 而不是第一个子元素、第二个文本节点 或者属性。这一点至关重要。XPath 按照逻辑而不是结构来处理 XML。基本上数据绑定也是如此,按照逻辑处理数据,而不用担心其结构。
为了避免误导,需要指出使用 XPath 仍然需要对结构有所了解。比如,@
运算符只用于属性,因此必须知道 type
(地址的一部分)用元素还是属性表示。在传统的数据绑定中,只需要调用 getAddress().getType()
而不需要管结构的层次。但是,这点小小的代价是值得的,因为不需要处理大量的生成类、额外的类路径问题、等待封送和解除封送处理以及传统数据绑定的其他缺点。
只
剩下这个等式中需要在 Java 语言中增加的部分:接受 XML 文档和 XPath 表达式,以 Java
友好的方式获得表达式的结果。这些内容将在第 2 部分介绍,很快就要把 XPath 与 Java 编程中其他以 XML
为中心的工具一起使用了。很多情况下您会发现,与使用生成类和 JAXB 这样的 API 的工具相比,XPath 是一种更好的 数据绑定工具。
参考资料
|
|
关于作者
|
|
|
Brett
McLaughlin 从 Logo 时代就开始使用计算机。(还记得那个小三角吗?)近年来他已经成为 Java 技术和 XML
社区最知名的作家和程序员之一。他曾经在 Nextel Communications 实现过复杂的企业系统,在 Lutris
Technologies 编写应用程序服务器,最近在 O'Reilly Media, Inc. 继续撰写和编辑这方面的图书。在他的新书 Head Rush Ajax 中,Brett 与畅销书作家 Eric 及 Beth Freeman 为 Ajax 带来了获奖的创新方法 Head First。最近的著作 Java 1.5 Tiger: A Developer's Notebook 是关于这一 Java 技术新版本的第一本书籍,经典著作 Java and XML 仍然是在 Java 语言中使用 XML 技术的权威图书。
|