級別: 中級
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 來說第一步是找到一個引用元素的句柄??墒窃谶x擇元素之前需要理解當前上下文。上下文就是在 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
元素)。注意,有時候您得到了預期的結果,但是很可能比您打算 要求的多。
還有更多需要注意的地方?,F在給出的表達式僅僅返回實際的元素??赡苓€要獲得這些節點的值。如果需要元素的值(假設元素包含文本數據),需要使用 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 技術的權威圖書。
|