XPath 表達(dá)式比繁瑣的文檔對(duì)象模型(DOM)導(dǎo)航代碼要容易編寫(xiě)得多。如果需要從 XML 文檔中提取信息,最快捷、最簡(jiǎn)單的辦法就是在 Java™ 程序中嵌入 XPath 表達(dá)式。Java 5 推出了 javax.xml.xpath 包,這是一個(gè)用于 XPath 文檔查詢(xún)的獨(dú)立于 XML 對(duì)象模型的庫(kù)。
如果要告訴別人買(mǎi)一加侖牛奶,您會(huì)怎么說(shuō)?“請(qǐng)去買(mǎi)一加侖牛奶回來(lái)” 還是 “從前門(mén)出去,向左轉(zhuǎn),走三個(gè)街區(qū)向右轉(zhuǎn),再走半個(gè)街區(qū)向右轉(zhuǎn)進(jìn)入商店。走向四號(hào)通道,沿通道走五米向左,拿一瓶一加侖裝的牛奶然后到收銀臺(tái)付款。再沿原路回家。” 簡(jiǎn)直太可笑了。只要在 “請(qǐng)去買(mǎi)一加侖牛奶回來(lái)” 的基礎(chǔ)上稍加指示,多數(shù)成人都能自己買(mǎi)回牛奶來(lái)。
查詢(xún)語(yǔ)言和計(jì)算機(jī)搜索與此類(lèi)似。直接說(shuō) “找一個(gè) Cryptonomicon 的副本” 要比編寫(xiě)搜索某個(gè)數(shù)據(jù)庫(kù)的詳細(xì)邏輯容易得多。由于搜索操作的邏輯非常相似,可以發(fā)明一種通用語(yǔ)言讓您使用 “找到 Neal Stephenson 的所有著作” 這樣的命令,然后編寫(xiě)對(duì)特定數(shù)據(jù)存儲(chǔ)執(zhí)行此類(lèi)查詢(xún)的引擎。
XPath
在眾多查詢(xún)語(yǔ)言之中,結(jié)構(gòu)化查詢(xún)語(yǔ)言(SQL)是一種針對(duì)查詢(xún)特定類(lèi)型的關(guān)系庫(kù)而設(shè)計(jì)和優(yōu)化的語(yǔ)言。其他不那么常見(jiàn)的查詢(xún)語(yǔ)言還有對(duì)象查詢(xún)語(yǔ)言(OQL)和 XQuery。但本文的主題是 XPath,一種為查詢(xún) XML 文檔而設(shè)計(jì)的查詢(xún)語(yǔ)言。比如,下面這個(gè)簡(jiǎn)單的 XPath 查詢(xún)可以在文檔中找到作者為 Neal Stephenson 的所有圖書(shū)的標(biāo)題:
//book[author="Neal Stephenson"]/title
|
作為對(duì)照,查詢(xún)同樣信息的純 DOM 搜索代碼如 清單 1 所示:
清單 1. 找到 Neal Stephenson 所有著作 title 元素的 DOM 代碼
ArrayList result = new ArrayList();
NodeList books = doc.getElementsByTagName("book");
for (int i = 0; i < books.getLength(); i++) {
Element book = (Element) books.item(i);
NodeList authors = book.getElementsByTagName("author");
boolean stephenson = false;
for (int j = 0; j < authors.getLength(); j++) {
Element author = (Element) authors.item(j);
NodeList children = author.getChildNodes();
StringBuffer sb = new StringBuffer();
for (int k = 0; k < children.getLength(); k++) {
Node child = children.item(k);
// really should to do this recursively
if (child.getNodeType() == Node.TEXT_NODE) {
sb.append(child.getNodeValue());
}
}
if (sb.toString().equals("Neal Stephenson")) {
stephenson = true;
break;
}
}
if (stephenson) {
NodeList titles = book.getElementsByTagName("title");
for (int j = 0; j < titles.getLength(); j++) {
result.add(titles.item(j));
}
}
}
|
不論您是否相信,清單 1 中的 DOM 顯然不如簡(jiǎn)單的 XPath 表達(dá)式通用或者健壯。您愿意編寫(xiě)、調(diào)試和維護(hù)哪一個(gè)?我想答案很明顯。
但是雖然有很強(qiáng)的表達(dá)能力,XPath 并不是 Java 語(yǔ)言,事實(shí)上 XPath 不是一種完整的編程語(yǔ)言。有很多東西用 XPath 表達(dá)不出來(lái),甚至有些查詢(xún)也無(wú)法表達(dá)。比方說(shuō),XPath 不能查找國(guó)際標(biāo)準(zhǔn)圖書(shū)編碼(ISBN)檢驗(yàn)碼不匹配的所有圖書(shū),或者找出境外帳戶(hù)數(shù)據(jù)庫(kù)顯示欠帳的所有作者。幸運(yùn)的是,可以把 XPath 結(jié)合到 Java 程序中,這樣就能發(fā)揮兩者的優(yōu)勢(shì)了:Java 做 Java 所擅長(zhǎng)的,XPath 做 XPath 所擅長(zhǎng)的。
直到最近,Java 程序執(zhí)行 XPath 查詢(xún)所需要的應(yīng)用程序編程接口(API)還因形形色色的 XPath 引擎而各不相同。Xalan 有一種 API,Saxon 使用另一種,其他引擎則使用其他的 API。這意味著代碼往往把您限制到一種產(chǎn)品上。理想情況下,最好能夠試驗(yàn)具有不同性能特點(diǎn)的各種引擎,而不會(huì)帶來(lái)不適當(dāng)?shù)穆闊┗蛘咧匦戮帉?xiě)代碼。
于是,Java 5 推出了 javax.xml.xpath
包,提供一個(gè)引擎和對(duì)象模型獨(dú)立的 XPath 庫(kù)。這個(gè)包也可用于 Java 1.3 及以后的版本,但需要單獨(dú)安裝 Java API for XML Processing (JAXP) 1.3。Xalan 2.7 和 Saxon 8 以及其他產(chǎn)品包含了這個(gè)庫(kù)的實(shí)現(xiàn)。
一個(gè)簡(jiǎn)單的例子
我將舉例說(shuō)明如何使用它。然后再討論一些細(xì)節(jié)問(wèn)題。假設(shè)要查詢(xún)一個(gè)圖書(shū)列表,尋找 Neal Stephenson 的著作。具體來(lái)說(shuō),這個(gè)圖書(shū)列表的形式如 清單 2 所示:
清單 2. 包含圖書(shū)信息的 XML 文檔
<inventory>
<book year="2000">
<title>Snow Crash</title>
<author>Neal Stephenson</author>
<publisher>Spectra</publisher>
<isbn>0553380958</isbn>
<price>14.95</price>
</book>
<book year="2005">
<title>Burning Tower</title>
<author>Larry Niven</author>
<author>Jerry Pournelle</author>
<publisher>Pocket</publisher>
<isbn>0743416910</isbn>
<price>5.99</price>
<book>
<book year="1995">
<title>Zodiac</title>
<author>Neal Stephenson<author>
<publisher>Spectra</publisher>
<isbn>0553573862</isbn>
<price>7.50</price>
<book>
<!-- more books... -->
</inventory>
|
|
抽象工廠
XPathFactory 是一個(gè)抽象工廠。抽象工廠設(shè)計(jì)模式使得這一種 API 能夠支持不同的對(duì)象模型,如 DOM、JDOM 和 XOM。為了選擇不同的模型,需要向 XPathFactory.newInstance() 方法傳遞標(biāo)識(shí)對(duì)象模型的統(tǒng)一資源標(biāo)識(shí)符(URI)。比如 http://xom.nu/ 可以選擇 XOM。但實(shí)際上,到目前為止 DOM 是該 API 支持的惟一對(duì)象模型。
|
|
查找所有圖書(shū)的 XPath 查詢(xún)非常簡(jiǎn)單://book[author="Neal Stephenson"]
。為了找出這些圖書(shū)的標(biāo)題,只要增加一步,表達(dá)式就變成了 //book[author="Neal Stephenson"]/title
。最后,真正需要的是 title
元素的文本節(jié)點(diǎn)孩子。這就要求再增加一步,完整的表達(dá)式就是 //book[author="Neal Stephenson"]/title/text()
。
現(xiàn)在我提供一個(gè)簡(jiǎn)單的程序,它從 Java 語(yǔ)言中執(zhí)行這個(gè)查詢(xún),然后把找到的所有圖書(shū)的標(biāo)題打印出來(lái)。首先,需要將文檔加載到一個(gè) DOM Document
對(duì)象中。為了簡(jiǎn)化起見(jiàn),假設(shè)該文檔在當(dāng)前工作目錄的 books.xml 文件中。下面的簡(jiǎn)單代碼片段解析文檔并建立對(duì)應(yīng)的 Document
對(duì)象:
清單 3. 用 JAXP 解析文檔
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true); // never forget this!
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse("books.xml");
|
到目前為止,這僅僅是標(biāo)準(zhǔn)的 JAXP 和 DOM,沒(méi)有什么新鮮的。
接下來(lái)創(chuàng)建 XPathFactory
:
XPathFactory factory = XPathFactory.newInstance();
|
然后使用這個(gè)工廠創(chuàng)建 XPath
對(duì)象:
XPath xpath = factory.newXPath();
|
XPath
對(duì)象編譯 XPath 表達(dá)式:
PathExpression expr = xpath.compile("http://book[author='Neal Stephenson']/title/text()");
|
|
直接求值
如果 XPath 表達(dá)式只使用一次,可以跳過(guò)編譯步驟直接對(duì) XPath 對(duì)象調(diào)用 evaluate() 方法。但是,如果同一個(gè)表達(dá)式要重復(fù)使用多次,編譯可能更快一些。
|
|
最后,計(jì)算 XPath 表達(dá)式得到結(jié)果。表達(dá)式是針對(duì)特定的上下文節(jié)點(diǎn)計(jì)算的,在這個(gè)例子中是整個(gè)文檔。還必須指定返回類(lèi)型。這里要求返回一個(gè)節(jié)點(diǎn)集:
Object result = expr.evaluate(doc, XPathConstants.NODESET);
|
可以將結(jié)果強(qiáng)制轉(zhuǎn)化成 DOM NodeList
,然后遍歷列表得到所有的標(biāo)題:
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
}
|
清單 4
把上述片段組合到了一個(gè)程序中。還要注意,這些方法可能拋出一些檢查異常,這些異常必須在 throws
子句中聲明,但是我在上面把它們掩蓋起來(lái)了:
清單 4. 用固定的 XPath 表達(dá)式查詢(xún) XML 文檔的完整程序
import Java.io.IOException;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import javax.xml.parsers.*;
import javax.xml.xpath.*;
public class XPathExample {
public static void main(String[] args)
throws ParserConfigurationException, SAXException,
IOException, XPathExpressionException {
DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(true); // never forget this!
DocumentBuilder builder = domFactory.newDocumentBuilder();
Document doc = builder.parse("books.xml");
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
XPathExpression expr
= xpath.compile("http://book[author='Neal Stephenson']/title/text()");
Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
}
}
}
|
XPath 數(shù)據(jù)模型
每當(dāng)混合使用諸如 XPath 和 Java 這樣兩種不同的語(yǔ)言時(shí),必定會(huì)有某些將兩者粘合在一起的明顯接縫。并非一切都很合拍。XPath 和 Java 語(yǔ)言沒(méi)有同樣的類(lèi)型系統(tǒng)。XPath 1.0 只有四種基本數(shù)據(jù)類(lèi)型:
- node-set
- number
- boolean
- string
當(dāng)然,Java 語(yǔ)言有更多的數(shù)據(jù)類(lèi)型,包括用戶(hù)定義的對(duì)象類(lèi)型。
多數(shù) XPath 表達(dá)式,特別是位置路徑,都返回節(jié)點(diǎn)集。但是還有其他可能。比如,XPath 表達(dá)式 count(//book)
返回文檔中的圖書(shū)數(shù)量。XPath 表達(dá)式 count(//book[@author="Neal Stephenson"]) > 10
返回一個(gè)布爾值:如果文檔中 Neal Stephenson 的著作超過(guò) 10 本則返回 true,否則返回 false。
evaluate()
方法被聲明為返回 Object
。實(shí)際返回什么依賴(lài)于 XPath 表達(dá)式的結(jié)果以及要求的類(lèi)型。一般來(lái)說(shuō),XPath 的
- number 映射為
Java.lang.Double
- string 映射為
Java.lang.String
- boolean 映射為
Java.lang.Boolean
- node-set 映射為
org.w3c.dom.NodeList
|
XPath 2
前面一直假設(shè)您使用的是 XPath 1.0。XPath 2 大大擴(kuò)展和修改了類(lèi)型系統(tǒng)。Java XPath API 支持 XPath 2 所需的主要修改是為返回 XPath 2 新數(shù)據(jù)類(lèi)型增加常量。
|
|
在 Java 中計(jì)算 XPath 表達(dá)式時(shí),第二個(gè)參數(shù)指定需要的返回類(lèi)型。有五種可能,都在 javax.xml.xpath.XPathConstants
類(lèi)中命名了常量:
-
XPathConstants.NODESET
-
XPathConstants.BOOLEAN
-
XPathConstants.NUMBER
-
XPathConstants.STRING
-
XPathConstants.NODE
最后一個(gè) XPathConstants.NODE
實(shí)際上沒(méi)有匹配的 XPath 類(lèi)型。只有知道 XPath 表達(dá)式只返回一個(gè)節(jié)點(diǎn)或者只需要一個(gè)節(jié)點(diǎn)時(shí)才使用它。如果 XPath 表達(dá)式返回了多個(gè)節(jié)點(diǎn)并且指定了 XPathConstants.NODE
,則 evaluate()
按照文檔順序返回第一個(gè)節(jié)點(diǎn)。如果 XPath 表達(dá)式選擇了一個(gè)空集并指定了 XPathConstants.NODE
,則 evaluate()
返回 null。
如果不能完成要求的轉(zhuǎn)換,evaluate()
將拋出 XPathException
。
名稱(chēng)空間上下文
若 XML 文檔中的元素在名稱(chēng)空間中,查詢(xún)?cè)撐臋n的 XPath 表達(dá)式必須使用相同的名稱(chēng)空間。XPath 表達(dá)式不一定要使用相同的前綴,只需要名稱(chēng)空間 URI 相同即可。事實(shí)上,如果 XML 文檔使用默認(rèn)名稱(chēng)空間,那么盡管目標(biāo)文檔沒(méi)有使用前綴,XPath 表達(dá)式也必須使用前綴。
但是,Java 程序不是 XML 文檔,因此不能用一般的名稱(chēng)空間解析。必須提供一個(gè)對(duì)象將前綴映射到名稱(chēng)空間 URI。該對(duì)象是 javax.xml.namespace.NamespaceContext
接口的實(shí)例。比如,假設(shè)圖書(shū)文檔放在 http://www.example.com/books 名稱(chēng)空間中,如 清單 5 所示:
清單 5. 使用默認(rèn)名稱(chēng)空間的 XML 文檔
<inventory xmlns="http://www.example.com/books">
<book year="2000">
<title>Snow Crash</title>
<author>Neal Stephenson</author>
<publisher>Spectra</publisher>
<isbn>0553380958</isbn>
<price>14.95<price>
</book>
<!-- more books... -->
<inventory>
|
查找 Neal Stephenson 全部著作標(biāo)題的 XPath 表達(dá)式就要改為 //pre:book[pre:author="Neal Stephenson"]/pre:title/text()
。但是,必須將前綴 pre
映射到 URI http://www.example.com/books。NamespaceContext
接口在 Java 軟件開(kāi)發(fā)工具箱(JDK)或 JAXP 中沒(méi)有默認(rèn)實(shí)現(xiàn)似乎有點(diǎn)笨,但確實(shí)如此。不過(guò),自己實(shí)現(xiàn)也不難。清單 6 對(duì)一個(gè)名稱(chēng)空間給出了簡(jiǎn)單的實(shí)現(xiàn)。還需要映射 xml
前綴。
清單 6. 綁定一個(gè)名稱(chēng)空間和默認(rèn)名稱(chēng)空間的簡(jiǎn)單上下文
import Java.util.Iterator;
import javax.xml.*;
import javax.xml.namespace.NamespaceContext;
public class PersonalNamespaceContext implements NamespaceContext {
public String getNamespaceURI(String prefix) {
if (prefix == null) throw new NullPointerException("Null prefix");
else if ("pre".equals(prefix)) return "http://www.example.org/books";
else if ("xml".equals(prefix)) return XMLConstants.XML_NS_URI;
return XMLConstants.NULL_NS_URI;
}
// This method isn't necessary for XPath processing.
public String getPrefix(String uri) {
throw new UnsupportedOperationException();
}
// This method isn't necessary for XPath processing either.
public Iterator getPrefixes(String uri) {
throw new UnsupportedOperationException();
}
}
|
使用映射存儲(chǔ)綁定和增加 setter 方法實(shí)現(xiàn)名稱(chēng)空間上下文的重用也不難。
創(chuàng)建 NamespaceContext
對(duì)象后,在編譯表達(dá)式之前將其安裝到 XPath
對(duì)象上。以后就可以像以前一樣是用這些前綴查詢(xún)了。比如:
清單 7. 使用名稱(chēng)空間的 XPath 查詢(xún)
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
xpath.setNamespaceContext(new PersonalNamespaceContext());
XPathExpression expr
= xpath.compile("http://pre:book[pre:author='Neal Stephenson']/pre:title/text()");
Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
}
|
函數(shù)求解器
有時(shí)候,在 Java 語(yǔ)言中定義用于 XPath 表達(dá)式的擴(kuò)展函數(shù)很有用。這些函數(shù)可以執(zhí)行用純 XPath 很難或者無(wú)法執(zhí)行的任務(wù)。不過(guò)必須是真正的函數(shù),而不是隨意的方法。就是說(shuō)不能有副作用。(XPath 函數(shù)可以按照任意的順序求值任意多次。)
通過(guò) Java XPath API 訪問(wèn)的擴(kuò)展函數(shù)必須實(shí)現(xiàn) javax.xml.xpath.XPathFunction
接口。這個(gè)接口只聲明了一個(gè)方法 evaluate:
public Object evaluate(List args) throws XPathFunctionException
|
該方法必須返回 Java 語(yǔ)言能夠轉(zhuǎn)換到 XPath 的五種類(lèi)型之一:
-
String
-
Double
-
Boolean
-
Nodelist
-
Node
比如,清單 8 顯示了一個(gè)擴(kuò)展函數(shù),它檢查 ISBN 的校驗(yàn)和并返回 Boolean
。這個(gè)校驗(yàn)和的基本規(guī)則是前九位數(shù)的每一位乘上它的位置(即第一位數(shù)乘上 1,第二位數(shù)乘上 2,依次類(lèi)推)。將這些數(shù)加起來(lái)然后取除以 11 的余數(shù)。如果余數(shù)是 10,那么最后一位數(shù)就是 X。
清單 8. 檢查 ISBN 的 XPath 擴(kuò)展函數(shù)
import Java.util.List;
import javax.xml.xpath.*;
import org.w3c.dom.*;
public class ISBNValidator implements XPathFunction {
// This class could easily be implemented as a Singleton.
public Object evaluate(List args) throws XPathFunctionException {
if (args.size() != 1) {
throw new XPathFunctionException("Wrong number of arguments to valid-isbn()");
}
String isbn;
Object o = args.get(0);
// perform conversions
if (o instanceof String) isbn = (String) args.get(0);
else if (o instanceof Boolean) isbn = o.toString();
else if (o instanceof Double) isbn = o.toString();
else if (o instanceof NodeList) {
NodeList list = (NodeList) o;
Node node = list.item(0);
// getTextContent is available in Java 5 and DOM 3.
// In Java 1.4 and DOM 2, you'd need to recursively
// accumulate the content.
isbn= node.getTextContent();
}
else {
throw new XPathFunctionException("Could not convert argument type");
}
char[] data = isbn.toCharArray();
if (data.length != 10) return Boolean.FALSE;
int checksum = 0;
for (int i = 0; i < 9; i++) {
checksum += (i+1) * (data[i]-'0');
}
int checkdigit = checksum % 11;
if (checkdigit + '0' == data[9] || (data[9] == 'X' && checkdigit == 10)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
}
|
下一步讓這個(gè)擴(kuò)展函數(shù)能夠在 Java 程序中使用。為此,需要在編譯表達(dá)式之前向 XPath 對(duì)象安裝 javax.xml.xpath.XPathFunctionResolver
。函數(shù)求解器將函數(shù)的 XPath 名稱(chēng)和名稱(chēng)空間 URI 映射到實(shí)現(xiàn)該函數(shù)的 Java 類(lèi)。清單 9 是一個(gè)簡(jiǎn)單的函數(shù)求解器,將擴(kuò)展函數(shù) valid-isbn
和名稱(chēng)空間 http://www.example.org/books 映射到 清單 8 中的類(lèi)。比如,XPath 表達(dá)式 //book[not(pre:valid-isbn(isbn))]
可以找到 ISBN 校驗(yàn)和不匹配的所有圖書(shū)。
清單 9. 識(shí)別 valid-isbn 擴(kuò)展函數(shù)的上下文
iimport javax.xml.namespace.QName;
import javax.xml.xpath.*;
public class ISBNFunctionContext implements XPathFunctionResolver {
private static final QName name
= new QName("http://www.example.org/books", "valid-isbn");
public XPathFunction resolveFunction(QName name, int arity) {
if (name.equals(ISBNFunctionContext.name) && arity == 1) {
return new ISBNValidator();
}
return null;
}
}
|
由于擴(kuò)展函數(shù)必須有名稱(chēng)空間,所以計(jì)算包含擴(kuò)展函數(shù)的表達(dá)式時(shí)必須使用 NamespaceResolver
,即便查詢(xún)的文檔沒(méi)有使用任何名稱(chēng)空間。由于 XPathFunctionResolver
、XPathFunction
和 NamespaceResolver
都是接口,如果方便的話可以將它們放在所有的類(lèi)中。
結(jié)束語(yǔ)
用 SQL 和 XPath 這樣的聲明性語(yǔ)言編寫(xiě)查詢(xún),要比使用 Java 和 C 這樣的命令式語(yǔ)言容易得多。但是,用 Java 和 C 這樣的圖靈完整語(yǔ)言編寫(xiě)復(fù)雜的邏輯,又比 SQL 和 XPath 這樣的聲明性語(yǔ)言容易得多。所幸的是,通過(guò)使用 Java Database Connectivity (JDBC) 和 javax.xml.xpath
之類(lèi)的 API 可以將兩者結(jié)合起來(lái)。隨著世界上越來(lái)越多的數(shù)據(jù)轉(zhuǎn)向 XML,javax.xml.xpath
將與 Java.sql
一樣變得越來(lái)越重要。
posted on 2006-09-18 13:34
周銳 閱讀(932)
評(píng)論(0) 編輯 收藏 所屬分類(lèi):
Java 、
XML 、
XSLT