Vincent.Chan‘s Blog

常用链接

统计

积分与排名

网站

最新评论

实用数据绑定: 使用 XPath 作为数据绑定工具,第 1 部分:使用 XPath 选择 XML 文档部分

级别: 中级

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 选择的所有实体 —— 元素、属性和值 —— 都称为节点。)比如下面的路径:

												body/p
										

清单 4body 有五个不同的 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 中第二个 pimg 元素,要得到标题该怎么办呢?可以使用 ../../../head/title/text()。但是数这些 ../ 很快就变得单调乏味了。因此能直接跳到根元素就好了,再想一想 UNIX,使用 / 返回到根元素就毫不奇怪了。同样的选择器更简单的形式为 /html/head/title/text()。虽然没有缩短,但是更清楚了。有了 ..// 之后,基本上就能移动到需要的任何地方了。

多重选择

考虑到使用一个 XPath 表达式可以选择多个节点,事情就更有趣了。您可以使用对应多个节点的简单表达式,比如 /html/body/h2 来进行多重选择。但是也可使用通配符 进一步精化这些选择。XPath 中有三个统配符:

  • * 与任何元素匹配,无论元素名是什么
  • node() 与所有的节点类型匹配(元素、文本节点、注释、属性,等等)
  • @* 与所有的属性节点匹配,无论属性名是什么

比如,可以使用 /html/body/* 选择 body 元素的所有直接子元素。也可用 /html/head/meta/@* 选择 meta 标记的所有属性。

所有这些情况下,要记住您得到的是一个节点集,因此不应该满足于处理某个值就结束(除非您已经选择了一个属性,稍后要讲到)。但是,只要使用方法、函数或模板处理多个节点,这些技术就很重要。





回页首


更有趣的东西

基本的东西很好,但有时候还需要点特别的让您身边的同事眼花缭乱的东西,或者需要一点特殊的 功能。这种情况下这些基础可能就不够了。虽然很难巨细无遗地介绍 XPath,但下面给出一些高级技巧,可以帮助您在 XPath 应用程序中取得需要的节点(或节点集)。

更大的一般性

到目前为止,看到的路径都是选择一个节点,并假设您知道该节点在何处。比方说,您知道 titleimgp 在文档中什么位置,只需要导航到这些元素。但有时候可能希望打破这些结构,仅仅撷取某种元素而不管其位置(或者无论给定的起始上下文的位置)。为此可使用后代选择器

后代选择器用双斜线 // 表示。使用它告诉 XPath 选择所有指定的节点,无论嵌套得多深。比如这个简单的特定于 XHTML 的 XPath:

												/html/body//table
										

XPath 选择嵌套在 XHTML 的 body 元素中的所有table 元素,不论直接嵌套在 body 中(如 /html/body/table),还是嵌套了多层(如 /html/body/table/tr/td/table)。这种情况下,嵌套的 table 和顶层 table 同时被选中。

不是很合 XML 的风格

在标准 XML 术语中,属性不属于 元素,而是与元素关联,有时候也说在元素上。但是,XPath 不能处理元素及其属性之间的关系。因此只能尽自己最大的努力:将属性看作是属于它们所在的元素。因此选择 pclass 属性要使用 p/@class。要访问属性所在的元素,可使用 @class/..。实际上是从属性上移 一层而选择元素。从技术上说这不是很好的 XML,但却是完全正确的 XPath。

当与属性结合使用的时候就变得很有趣了(使用 @)。比方说,假设要选择具有 id 属性的所有元素。可使用 //@id,首先跳回根元素然后选择文档中的所有 id 属性。但是实际上要访问的是元素而不是属性,因此需要从属性上移一层到包含这些属性的元素:

												//@id/..
										

您应该尝试结合这些不同的方法,会看到一些很有意思的结果。

匹配条件

假设有一个条件希望作为匹配的基础,比如需要 class 属性值为 greenteap 元素。如果知道如何使用方括号,这一点在 XPath 中很容易做到。下面是一个例子:

												
														
/html/body/p[class="greentea"]

方括号用于指定条件,可使用 = 甚至 <>。也许要用一个范围更广的表达式:

												
														
//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") 返回 truecontains("McLaughlin", "augh") 同样如此。string-length("Brett") 显然返回 5normalize-space(" Brett McLaughlin ") 则返回 "Brett McLaughlin"。是不是很简单?当然这些可用于所有 XPath 表达式的返回值,比如 /html/body/p[class='greentea']。因此要获取 清单 4p 的文本可用 normalize-space()

												
														
normalize-space(/html/body/p[class='greentea'])

更妙的是可用 string() 取出多个元素的文本。比如,如果希望取得 清单 4 所示 XHTML 中所有p 元素的所有 文本,可用:

												normalize-space(string(//p))
										

分析最后一个例子可以了解很多内容:

  1. //p 选择文档中所有的 p 元素,无论其位于何处。
  2. string(//p) 获取这些元素的内容组成一个大字符串。但是字符串中包含很多无用的空白,因此还要进一步处理。
  3. normalize-space(string(//p)) 规范化内容中的空白字符,得到您希望的文本。




回页首


这是一篇关于数据绑定的文章吗?

现 在您可能已经忘记正在阅读的是一篇关于数据绑定的文章。但是先不要忙于跟过去几年中您读过的其他 X* 文档和 API 比较。您读到的实际上是另一种数据绑定方法。考虑这样一种观点,数据绑定是在 Java 代码中保留 XML 文档逻辑(或者语义,如果您喜欢的话)意义。如果 XML 文档中的 address 元素包含 streetcitystate 子元素,想要用 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 数据,您要的是 personaddressstreet 而不是第一个子元素第二个文本节点 或者属性。这一点至关重要。XPath 按照逻辑而不是结构来处理 XML。基本上数据绑定也是如此,按照逻辑处理数据,而不用担心其结构。

为了避免误导,需要指出使用 XPath 仍然需要对结构有所了解。比如,@ 运算符只用于属性,因此必须知道 type(地址的一部分)用元素还是属性表示。在传统的数据绑定中,只需要调用 getAddress().getType() 而不需要管结构的层次。但是,这点小小的代价是值得的,因为不需要处理大量的生成类、额外的类路径问题、等待封送和解除封送处理以及传统数据绑定的其他缺点。

只 剩下这个等式中需要在 Java 语言中增加的部分:接受 XML 文档和 XPath 表达式,以 Java 友好的方式获得表达式的结果。这些内容将在第 2 部分介绍,很快就要把 XPath 与 Java 编程中其他以 XML 为中心的工具一起使用了。很多情况下您会发现,与使用生成类和 JAXB 这样的 API 的工具相比,XPath 是一种更好的 数据绑定工具。





回页首


参考资料





回页首


关于作者

Brett McLaughlin 的照片

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 技术的权威图书。


posted on 2006-03-18 20:47 Vincent.Chen 阅读(585) 评论(0)  编辑  收藏 所属分类: AJAX


只有注册用户登录后才能发表评论。


网站导航: