从 java 程序中查询 xml
elliotte harold (elharo@metalab.unc.edu), 副教授, polytechnic university
简介: xpath
表达式比繁琐的文档对象模型(dom)导航代码要容易编写得多。如果需要从 xml 文档中提取信息,最快捷、最简单的办法就是在 java™ 程序中嵌入 xpath 表达式。java 5 推出了
javax.xml.xpath 包,这是一个用于
xpath 文档查询的独立于 xml 对象模型的库。
如果要告诉别人买一加仑牛奶,您会怎么说?“请去买一加仑牛奶回来” 还是
“从前门出去,向左转,走三个街区向右转,再走半个街区向右转进入商店。走向四号通道,沿通道走五米向左,拿一瓶一加仑装的牛奶然后到收银台付款。再沿原路回家。”
简直太可笑了。只要在 “请去买一加仑牛奶回来” 的基础上稍加指示,多数成人都能自己买回牛奶来。
查询语言和计算机搜索与此类似。直接说 “找一个 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
element book = (element)
books.item(i);
nodelist authors =
book.getelementsbytagname("author");
boolean stephenson = false;
for (int j = 0; j
element author = (element)
authors.item(j);
nodelist children = author.getchildnodes();
stringbuffer sb = new
stringbuffer();
for (int k = 0; 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
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 文档
snow crash
neal
stephenson
spectra
0553380958
14.95
burning
tower
larry
niven
jerry
pournelle
0743416910
5.99
zodiac
neal
stephenson
spectra
0553573862
7.50
抽象工厂
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 语言中执行这个查询,然后把找到的所有图书的标题打印出来。首先,需要将文档加载到一个
domdocument 对象中。为了简化起见,假设该文档在当前工作目录的 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
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
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 文档
snow crash
清单 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
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
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,即便查询的文档没有使用任何名称空间。由于 xpathfunctionresolver、xpathfunction 和 namespaceresolver 都是接口,如果方便的话可以将它们放在所有的类中。
结束语
用 sql 和
xpath 这样的声明性语言编写查询,要比使用
java 和 c 这样的命令式语言容易得多。但是,用 java 和 c 这样的图灵完整语言编写复杂的逻辑,又比
sql 和 xpath 这样的声明性语言容易得多。所幸的是,通过使用 java database
connectivity (jdbc) 和javax.xml.xpath 之类的
api 可以将两者结合起来。随着世界上越来越多的数据转向
xml,javax.xml.xpath 将与 java.sql 一样变得越来越重要。
======================================================
在最后,我邀请大家参加新浪APP,就是新浪免费送大家的一个空间,支持PHP+MySql,免费二级域名,免费域名绑定 这个是我邀请的地址,您通过这个链接注册即为我的好友,并获赠云豆500个,价值5元哦!短网址是http://t.cn/SXOiLh我创建的小站每天访客已经达到2000+了,每天挂广告赚50+元哦,呵呵,饭钱不愁了,\(^o^)/