Java 语言的 XPath API

从 Java 程序中查询 XML

Elliotte Harold (elharo@metalab.unc.edu), 副教授, Polytechnic University

简介: XPath 表达式比繁琐的文档对象模型(DOM)导航代码要容易编写得多。如果需要从 XML 文档中提取信息,最快捷、最简单的办法就是在 Java™ 程序中嵌入 XPath 表达式。Java 5 推出了 javax.xml.xpath 包,这是一个用于 XPath 文档查询的独立于 XML 对象模型的库。

发布日期: 2006 年 9 月 05 日
级别: 中级
访问情况 1261 次浏览
建议: 0 (添加评论)

1 star2 stars3 stars4 stars5 stars 平均分 (共 0 个评分 )

如果要告诉别人买一加仑牛奶,您会怎么说?“请去买一加仑牛奶回来” 还是 “从前门出去,向左转,走三个街区向右转,再走半个街区向右转进入商店。走向四号通道,沿通道走五米向左,拿一瓶一加仑装的牛奶然后到收银台付款。再沿原路回家。” 简直太可笑了。只要在 “请去买一加仑牛奶回来” 的基础上稍加指示,多数成人都能自己买回牛奶来。

查询语言和计算机搜索与此类似。直接说 “找一个 Cryptonomicon 的副本” 要比编写搜索某个数据库的详细逻辑容易得多。由于搜索操作的逻辑非常相似,可以发明一种通用语言让您使用 “找到 Neal Stephenson 的所有著作” 这样的命令,然后编写对特定数据存储执行此类查询的引擎。

XPath

在众多查询语言之中,结构化查询语言(SQL)是一种针对查询特定类型的关系库而设计和优化的语言。其他不那么常见的查询语言还有对象查询语言(OQL)和 XQuery。但本文的主题是 XPath,一种为查询 XML 文档而设计的查询语言。比如,下面这个简单的 XPath 查询可以在文档中找到作者为 Neal Stephenson 的所有图书的标题:

//book[author="Neal Stephenson"]/title

作为对照,查询同样信息的纯 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 显然不如简单的 XPath 表达式通用或者健壮。您愿意编写、调试和维护哪一个?我想答案很明显。

但是虽然有很强的表达能力,XPath 并不是 Java 语言,事实上 XPath 不是一种完整的编程语言。有很多东西用 XPath 表达不出来,甚至有些查询也无法表达。比方说,XPath 不能查找国际标准图书编码(ISBN)检验码不匹配的所有图书,或者找出境外帐户数据库显示欠帐的所有作者。幸运的是,可以把 XPath 结合到 Java 程序中,这样就能发挥两者的优势了:Java 做 Java 所擅长的,XPath 做 XPath 所擅长的。

直到最近,Java 程序执行 XPath 查询所需要的应用程序编程接口(API)还因形形色色的 XPath 引擎而各不相同。Xalan 有一种 API,Saxon 使用另一种,其他引擎则使用其他的 API。这意味着代码往往把您限制到一种产品上。理想情况下,最好能够试验具有不同性能特点的各种引擎,而不会带来不适当的麻烦或者重新编写代码。

于是,Java 5 推出了 javax.xml.xpath 包,提供一个引擎和对象模型独立的 XPath 库。这个包也可用于 Java 1.3 及以后的版本,但需要单独安装 Java API for XML Processing (JAXP) 1.3。Xalan 2.7 和 Saxon 8 以及其他产品包含了这个库的实现。


一个简单的例子

我将举例说明如何使用它。然后再讨论一些细节问题。假设要查询一个图书列表,寻找 Neal Stephenson 的著作。具体来说,这个图书列表的形式如 清单 2 所示:


清单 2. 包含图书信息的 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 是一个抽象工厂。抽象工厂设计模式使得这一种 API 能够支持不同的对象模型,如 DOM、JDOM 和 XOM。为了选择不同的模型,需要向 XPathFactory.newInstance() 方法传递标识对象模型的统一资源标识符(URI)。比如 http://xom.nu/ 可以选择 XOM。但实际上,到目前为止 DOM 是该 API 支持的惟一对象模型。

查找所有图书的 XPath 查询非常简单://book[author="Neal Stephenson"]。为了找出这些图书的标题,只要增加一步,表达式就变成了 //book[author="Neal Stephenson"]/title。最后,真正需要的是 title 元素的文本节点孩子。这就要求再增加一步,完整的表达式就是 //book[author="Neal Stephenson"]/title/text()

现在我提供一个简单的程序,它从 Java 语言中执行这个查询,然后把找到的所有图书的标题打印出来。首先,需要将文档加载到一个 DOM Document 对象中。为了简化起见,假设该文档在当前工作目录的 books.xml 文件中。下面的简单代码片段解析文档并建立对应的 Document 对象:


清单 3. 用 JAXP 解析文档
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true); // never forget this!
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse("books.xml");

到目前为止,这仅仅是标准的 JAXP 和 DOM,没有什么新鲜的。

接下来创建 XPathFactory

XPathFactory factory = XPathFactory.newInstance();

然后使用这个工厂创建 XPath 对象:

XPath xpath = factory.newXPath();

XPath 对象编译 XPath 表达式:

PathExpression expr = xpath.compile("//book[author='Neal Stephenson']/title/text()");

直接求值

如果 XPath 表达式只使用一次,可以跳过编译步骤直接对 XPath 对象调用 evaluate() 方法。但是,如果同一个表达式要重复使用多次,编译可能更快一些。

最后,计算 XPath 表达式得到结果。表达式是针对特定的上下文节点计算的,在这个例子中是整个文档。还必须指定返回类型。这里要求返回一个节点集:

Object result = expr.evaluate(doc, XPathConstants.NODESET);

可以将结果强制转化成 DOM NodeList,然后遍历列表得到所有的标题:

        NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
}

清单 4 把上述片段组合到了一个程序中。还要注意,这些方法可能抛出一些检查异常,这些异常必须在 throws 子句中声明,但是我在上面把它们掩盖起来了:


清单 4. 用固定的 XPath 表达式查询 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("//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 数据模型

每当混合使用诸如 XPath 和 Java 这样两种不同的语言时,必定会有某些将两者粘合在一起的明显接缝。并非一切都很合拍。XPath 和 Java 语言没有同样的类型系统。XPath 1.0 只有四种基本数据类型:

  • node-set
  • number
  • boolean
  • string

当然,Java 语言有更多的数据类型,包括用户定义的对象类型。

多数 XPath 表达式,特别是位置路径,都返回节点集。但是还有其他可能。比如,XPath 表达式 count(//book) 返回文档中的图书数量。XPath 表达式 count(//book[@author="Neal Stephenson"]) > 10 返回一个布尔值:如果文档中 Neal Stephenson 的著作超过 10 本则返回 true,否则返回 false。

evaluate() 方法被声明为返回 Object。实际返回什么依赖于 XPath 表达式的结果以及要求的类型。一般来说,XPath 的

  • number 映射为 java.lang.Double
  • string 映射为 java.lang.String
  • boolean 映射为 java.lang.Boolean
  • node-set 映射为 org.w3c.dom.NodeList

XPath 2

前面一直假设您使用的是 XPath 1.0。XPath 2 大大扩展和修改了类型系统。Java XPath API 支持 XPath 2 所需的主要修改是为返回 XPath 2 新数据类型增加常量。

在 Java 中计算 XPath 表达式时,第二个参数指定需要的返回类型。有五种可能,都在 javax.xml.xpath.XPathConstants 类中命名了常量:

  • XPathConstants.NODESET
  • XPathConstants.BOOLEAN
  • XPathConstants.NUMBER
  • XPathConstants.STRING
  • XPathConstants.NODE

最后一个 XPathConstants.NODE 实际上没有匹配的 XPath 类型。只有知道 XPath 表达式只返回一个节点或者只需要一个节点时才使用它。如果 XPath 表达式返回了多个节点并且指定了 XPathConstants.NODE,则 evaluate() 按照文档顺序返回第一个节点。如果 XPath 表达式选择了一个空集并指定了 XPathConstants.NODE,则 evaluate() 返回 null。

如果不能完成要求的转换,evaluate() 将抛出 XPathException


名称空间上下文

若 XML 文档中的元素在名称空间中,查询该文档的 XPath 表达式必须使用相同的名称空间。XPath 表达式不一定要使用相同的前缀,只需要名称空间 URI 相同即可。事实上,如果 XML 文档使用默认名称空间,那么尽管目标文档没有使用前缀,XPath 表达式也必须使用前缀。

但是,Java 程序不是 XML 文档,因此不能用一般的名称空间解析。必须提供一个对象将前缀映射到名称空间 URI。该对象是 javax.xml.namespace.NamespaceContext 接口的实例。比如,假设图书文档放在 http://www.example.com/books 名称空间中,如 清单 5 所示:


清单 5. 使用默认名称空间的 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 全部著作标题的 XPath 表达式就要改为 //pre:book[pre:author="Neal Stephenson"]/pre:title/text()。但是,必须将前缀 pre 映射到 URI http://www.example.com/books。NamespaceContext 接口在 Java 软件开发工具箱(JDK)或 JAXP 中没有默认实现似乎有点笨,但确实如此。不过,自己实现也不难。清单 6 对一个名称空间给出了简单的实现。还需要映射 xml 前缀。


清单 6. 绑定一个名称空间和默认名称空间的简单上下文
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();
}

}


使用映射存储绑定和增加 setter 方法实现名称空间上下文的重用也不难。

创建 NamespaceContext 对象后,在编译表达式之前将其安装到 XPath 对象上。以后就可以像以前一样是用这些前缀查询了。比如:


清单 7. 使用名称空间的 XPath 查询
  XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
xpath.setNamespaceContext(new PersonalNamespaceContext());
XPathExpression expr
= xpath.compile("//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());
}



函数求解器

有时候,在 Java 语言中定义用于 XPath 表达式的扩展函数很有用。这些函数可以执行用纯 XPath 很难或者无法执行的任务。不过必须是真正的函数,而不是随意的方法。就是说不能有副作用。(XPath 函数可以按照任意的顺序求值任意多次。)

通过 Java XPath API 访问的扩展函数必须实现 javax.xml.xpath.XPathFunction 接口。这个接口只声明了一个方法 evaluate:

public Object evaluate(List args) throws XPathFunctionException

该方法必须返回 Java 语言能够转换到 XPath 的五种类型之一:

  • String
  • Double
  • Boolean
  • Nodelist
  • Node

比如,清单 8 显示了一个扩展函数,它检查 ISBN 的校验和并返回 Boolean。这个校验和的基本规则是前九位数的每一位乘上它的位置(即第一位数乘上 1,第二位数乘上 2,依次类推)。将这些数加起来然后取除以 11 的余数。如果余数是 10,那么最后一位数就是 X。


清单 8. 检查 ISBN 的 XPath 扩展函数
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;

}

}


下一步让这个扩展函数能够在 Java 程序中使用。为此,需要在编译表达式之前向 XPath 对象安装 javax.xml.xpath.XPathFunctionResolver。函数求解器将函数的 XPath 名称和名称空间 URI 映射到实现该函数的 Java 类。清单 9 是一个简单的函数求解器,将扩展函数 valid-isbn 和名称空间 http://www.example.org/books 映射到 清单 8 中的类。比如,XPath 表达式 //book[not(pre:valid-isbn(isbn))] 可以找到 ISBN 校验和不匹配的所有图书。


清单 9. 识别 valid-isbn 扩展函数的上下文
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;
}

}


由于扩展函数必须有名称空间,所以计算包含扩展函数的表达式时必须使用 NamespaceResolver,即便查询的文档没有使用任何名称空间。由于 XPathFunctionResolverXPathFunctionNamespaceResolver 都是接口,如果方便的话可以将它们放在所有的类中。


结束语

用 SQL 和 XPath 这样的声明性语言编写查询,要比使用 Java 和 C 这样的命令式语言容易得多。但是,用 Java 和 C 这样的图灵完整语言编写复杂的逻辑,又比 SQL 和 XPath 这样的声明性语言容易得多。所幸的是,通过使用 Java Database Connectivity (JDBC) 和 javax.xml.xpath 之类的 API 可以将两者结合起来。随着世界上越来越多的数据转向 XML,javax.xml.xpath 将与 java.sql 一样变得越来越重要。


参考资料

学习

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文

     

     

  • Get started with XPath 2.0”(Benoît Marchal,developerWorks,2006 年 5 月):XPath 2.0 提供了更强大的功能和更高的效率,了解如何利用这一新的数据模型编写更复杂的请求。

     

     

  • Working with JAXP namespace contexts”:Norm Walsh 为名称空间上下文辩解。

     

     

  • XML in a Nutshell(Elliotte Rusty Harold 和 W. Scott Means,O'Reilly,2005 年):关于 XPath 1.0 以及 DOM 和 JAXP 的完整参考和简明指南。

     

     

  • developerWorks Java 技术专区:提供了关于 Java 编程方方面面的数百篇文章。

     

     

  • IBM XML 1.1 认证:了解如何才能成为一名 IBM 认证的 XML 1.1 及相关技术的开发人员。

     

     

  • XML:developerWorks XML 专区提供了各种技术文章和技巧、教程、标准和 IBM 红皮书。

     

     

  • developerWorks 技术事件和网络广播:随时关注技术的最新进展。

     

     

获得产品和技术

  • JAXP Project:从 java.net 下载 JAXP 1.3 for Java 1.3 和 1.4。

     

     

  • Xalan 2:看看来自 Apache Project 的支持本文中所述 XPath API 的 XSLT 引擎。

     

     

  • SAXON 8:试一试 Michael Kay 的 XSLT 引擎,它也支持本文所讨论的 XPath API。

     

     

  • IBM 试用软件:用这些试用软件开发您的下一个项目,这些软件可直接从 developerWorks 下载。

     

     

讨论

关于作者

Elliotte Harold 出生在新奥尔良,现在他还定期回老家喝一碗美味的秋葵汤。不过目前,他与妻子 Beth 定居在纽约临近布鲁克林的 Prospect Heights,与他们住在一起的还有猫咪 Charm(取自夸克)和 Marjorie(取自他岳母的名字)。他是 Polytechnic 大学的计算机科学副教授,讲授 Java 和面向对象编程。他的 Cafe au Lait 网站是 Internet 上最受欢迎的独立 Java 站点之一,姊妹站点 Cafe con Leche 是最受欢迎的 XML 站点之一。他的最新著作是 Java I/O, 2nd edition。他目前在研究处理 XML 的 XOM API、Jaxen XPath 引擎和 Jester 测试覆盖工具。他将在 9 月份在波士顿举行的 Software Development Best Practices 大会上发表关于 Java 的演讲。

建议



0 条评论 | 添加评论举报不良信息


快来添加第一条评论

添加评论



商标  |  My developerWorks 使用条款与条件

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML, Java technology, Open source
ArticleID=157553
ArticleTitle=Java 语言的 XPath API
publish-date=09052006
author1-email=elharo@metalab.unc.edu
author1-email-cc=dwxed@us.ibm.com
url=http://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html

标签

Help
使用搜索文本框在 My developerWorks 中查找包含该标签的所有内容。热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。

posted on 2010-12-03 16:48  aurawing  阅读(392)  评论(0编辑  收藏  举报