来源:http://d23xapp2.cn.ibm.com/developerworks/education/xml/xmljava/tutorial/xmljava-1-1.html
第五章 解析器高级功能
概览
我们已经讨论了使用一个 xml 解析器来处理 xml 文档的基础。在本章节,我们将探讨一些高级概念。
首先,我们将从头构建一棵 dom 树。换而言之,我们将不需要一个 xml 源文件来创建一个 document 对象。
然后,我们将向您显示如何使用解析器来处理包含在一个字符串的 xml 文档。
接着,我们将向您显示如何操作一棵 dom 树。我们将对我们示例的 xml 文档操作并对其诗句排序。
最后,我们将展示如何利用如 dom 和 sax 标准的接口使得解析器的更改十分容易。我们将向您展示两个使用不同 xml 解析器的示例应用。而其中 dom 和 sax 代码没有改变。
从头构建一棵 dom 树
有时您想要从头构建一棵 dom 树。要完成这个任务,您创建一个 document 对象,然后对其添加不同的 node 对象。
您可运行 java dombuilder 来看一个从头构建一棵 dom 树的示例应用。该应用重新创建了通过对 sonnet.xml 最初解析而构建出的 dom 树(但不再创建空格符)。
我们首先创建一个 documentimpl 类的实例。此类实现 dom 定义的 document 接口。
document doc = (document)class.
forname("com.ibm.xml.dom.documentimpl").
newinstance();
dombuilder.java (代码请参考附录2)
这段代码不使用一个 xml 文档来构建一个 dom 树。当树被构建完后,该代码将树的内容输出到标准输出。
…
<address>
<name>
<title>mrs.</title>
<first-name>mary</first-name>
<last-name>mcgoon</last-name>
</name>
<street>1401 main street</street>
<city>anytown</city>
<state>nc</state>
<zip>34829</zip>
</address>
<address>
<name>
…
添加 node 到我们的 document
现在我们有自己的 document 对象了,我们开始创建 node。我们第一个要创建的 node 是一个 <sonnet> 元素。我们将创建所有的 node,然后将每个节点添加到其对应的父母。
注意我们使用 setattribute 方法来对<sonnet>元素设置其 type 属性的值。
element root = doc.
createelement("sonnet");
root.setattribute("type",
h"shakespearean");
构建您的文档结构
当我们开始构建我们的 dom 树时,我们将需要构建我们文档的结构。要完成它,我们将需要恰当地使用 appendchild 方法。我们将创建 <author> 元素,然后创建在其下的其它元素,接着使用 appendchild 方法将所有这些元素添加到正确的父母。
注意 createelement 是属于 document 类的一个方法。我们的 document 对象拥有我们在此创建的所有元素。
最终,注意到我们为所有的元素内容创建 text 节点。text 节点是元素的子女,而 text 节点的父母则被添加到对应的父母下。
element author =
doc.createelement("author");
element lastname = doc.
createelement("last-name");
lastname.appendchild(doc.
createtextnode("shakespeare"));
author.appendchild(lastname);
完成我们的 dom 树
一旦我们对 <sonnet> 元素添加了所有内容,我们需要将它添加到 document 对象。我们最后一次调用 appendchild 方法,这次是将子元素添加到 document 对象上。
记住一个 xml 文档只能有一个根(root)元素;如果您要向document添加多个根元素appendchild 将抛出一个异常。
当我们构建好 dom 树后,我们构建一个 dombuilder 对象,然后调用它的 printdomtree 方法来打印 dom 树。
element line14 = doc.
createelement("line");
line14.appendchild(doc.
createtextnode("as any …"));
text.appendchild(line14);
root.appendchild(text);
doc.appendchild(root);
dombuilder db = new dombuilder();
db.printdomtree(doc);
使用 dom 对象来避免解析
您可以想象一个 dom document 对象作为一个 xml 文档的编译形式。如果您正使用 xml 来在不同方之间传递数据,您将可以通过接受和发送 dom 对象而不是 xml 源数据来节约时间。
这是最常见的原因您为何需要从头来构建一个 dom 树。
最坏情况下,您需要在您发送数据前从一棵 dom 树创建出 xml 源数据,然后在您接受 xml 数据时创建出一棵 dom 树。直接使用 dom 对象将节约大量时间。
一个警告:要注意一个 dom 对象可能比 xml 源数据要大很多。如果您要在一个十分缓慢的连接线路上传递数据,发送较小的 xml 源数据而重新解析数据要比传递大数据更有效。
解析一个 xml 字符串
很有可能您需要解析一个 xml 字符串。ibm 的 xml4j 解析器支持这个功能,尽管您需要将您的字符串转换成一个 inputsource 对象。
第一步是从您的字符串中创建一个 stringreader 对象。一旦完成此步,您可以从 stringreader 创建一个 inputsource 对象。
您可运行 java parsestring 来查看代码的运行结果。在示例应用中,xml 字符串是写死的(hardcoded);有许多种方法让您从一个用户或其它机器获得 xml 输入。有了这个技术,您就不再需要将 xml 文档输出到一个文件系统来解析它了。
parsestring ps = new parsestring();
stringreader sr =
new stringreader("<?xml version=\"1.0\"?>
<a>alphabravo…");
inputsource isrc = new inputsource(sr);
ps.parseandprint(isrc);
parsestring.java (参附录2)
这段代码展示了如何解析一个包含 xml 文档的字符串。
排列在一棵 dom 树中的 node
为了介绍您如何改变一棵 dom 树的结构,我们将修改我们的 dom 示例来排列十四行诗的 <line> 元素。有多种 dom 方法可以用来在 dom 树中搬移节点。
要查看代码的结果,运行 java domsorter sonnet.xml。它并不会改进诗的韵律,但它真确地排列了 <line> 元素。
要开始排列工作,我们将使用 getelementsbytagname 方法来提取在文档中的所有 <line> 元素。该方法节约了我们编写代码来遍历整个树的开销。
if (doc != null)
{
sortlines(doc);
printdomtree(doc);
}
…
public void sortlines(document doc)
{
nodelist thelines =
doc.getdocumentelement().
getelementsbytagname("line");
…
domsorter.java (参附录2)
这段代码在 xml 文档中查找所有的 <line> 元素,然后排序。它展示了如何操作一个 dom 树。
提取我们的 <line> 文本
为了简化代码,我们创建一个 helper 功能,gettextfromline,用来提取包含在一对 <line> 元素中的文本。它简单地找到 <line> 元素的第一个子女,如果其是一个 text 节点就返回其文本。
该方法返回一个 java string 因此我们的排序过程可以使用 string.compareto 方法来决定排序的次序。
该段代码实际上应该检查 <line> 所有的子女,因为它们可能包含实体(entity)引用 (例如实体 &miss; 可能替代了文本 "mistress")。我们将把这个改进作为读者的一个练习。
public string gettextfromline(node
lineelement)
{
stringbuffer returnstring =
new stringbuffer();
if (lineelement.getnodename().
equals("line"))
{
nodelist kids = lineelement.
getchildnodes();
if (kids != null)
if (kids.item(0).getnodetype() ==
node.text_node)
returnstring.append(kids.item(0).
getnodevalue());
}
else
returnstring.setlength(0);
return new string(returnstring);
}
文本排序
现在我们有能力从一个给定的 <line> 元素获取文本,我们可以开始排列数据了。由于我们只有 14 个元素,我们将使用冒泡排序。
冒泡排序算法是比较两个相邻数据值,然后如果它们次序不对就交换它们。要完成交换,我们使用 getparentnode 和 insertbefore 方法。
getparentnode 返回任意 node 的父母;我们使用这个方法来获得当前 <line> 的父母 (文档的一个 <lines> 元素使用 sonnet dtd)。
insertbefore(nodea, nodeb) 在 nodeb 前插入 nodea 到 dom 树中。insertbefore 最重要的特性是如果 nodea 已经存在于 dom 树中了,它在插入 nodeb 前将删除该节点。
public void sortlines(document doc)
{
nodelist thelines =
doc.getdocumentelement().
getelementsbytagname("line");
if (thelines != null)
{
int len = thelines.getlength();
for (int i=0; i < len; i++)
for (int j=0; j < (len-1-i); j++)
if (gettextfromline(
thelines.item(j)).
compareto(gettextfromline(
thelines.item(j+1))) > 0)
thelines.item(j).
getparentnode().insertbefore(
thelines.item(j+1),
thelines.item(j));
}
}
用来操作树的有用的 dom 方法
除insertbefore 之外,还有其它的 dom 方法可用来操作树。
parentnode.appendchild(newchild)
将一个节点作为给定父母节点的最后子女添加。调用 parentnode.insertbefore(newchild, null) 完成同样功能。
parentnode.replacechild(newchild, oldchild)
将 oldchild 用 newchild 来取代。节点 oldchild 必须是 parentnode 的子女。
parentnode.removechild(oldchild)
将 oldchild 从 parentnode 下删除。
parentnode.appendchild(newchild);
…
parentnode.insertbefore(newchild,
oldchild);
…
parentnode.replacechild(newchild,
oldchild);
…
parentnode.removechild(oldchild);
…
关于树操作还需注意的事项
如果您需要删除一个给定节点的所有子女,这要比看上去难多了。这两段在左边的示例代码看起来可以完成任务。但是,在第二个才能完成。第一个示例代码由于 kid 的实例数据在 removechild(kid) 被调用后就改变了而无法完成任务。
换而言之,for 循环删除了 kid,第一个子女,然后检查 kid.getnextsibling 是否为 null 。由于 kid 刚被删除,它不再有任何同胞了,因此 kid.getnextsibling 是 null。 for 循环只会运行一次。不论 node 有一个或几千个子女,第一段示例代码只是删除第一个子女。要使用第二段示例代码来删除所有的子节点。
/** doesnt work **/
for (node kid = node.getfirstchild();
kid != null;
kid = kid.getnextsibling())
node.removechild(kid);
/** does work **/
while (node.haschildnodes())
node.removechild(node.getfirstchild());
使用另一个dom 解析器
尽管我们想象不出一个您为何要改换解析器的理由,您可以使用非 xml4j 的解析器来解析您的 xml 文档。如果您查看t domtwo.java 的代码,您将看到更换到 sun 的 xml 解析器只需要两个修改。
首先,我们必须载入(import) sun 公司的类。这很简单。我们要修改的只是创建 parser 对象的代码。如您所见,sun 的解析器构建过程比较复杂,但余下的代码不用修改了。所有的 dom 代码不需要任何的修改。
最终,在 domtwo 的不同之处是命令行格式。出于某些原因,sun 的解析器不能用常用的方法来解析文件名。如果您运行 java domtwo file:///d:/sonnet.xml (当然根据您的系统修改 file uri),您将获得 domone 的相同结果。
import com.sun.xml.parser.parser;
import
com.sun.xml.tree.xmldocumentbuilder;
…
xmldocumentbuilder builder =
new xmldocumentbuilder();
parser parser =
new com.sun.xml.parser.parser();
parser.setdocumenthandler(builder);
builder.setparser(parser);
parser.parse(uri);
doc = builder.getdocument();
domtwo.java (参见附录2)
这段代码等同于 domone.java,但它使用 sun 公司的 xml 解析器而不是 ibm 的。它展示了 dom 接口的可移植性。
使用一个不同的 sax 解析器
我们也编写了 saxtwo.java 来展示如何使用 sun 公司的 sax 解析器。与 domtwo 类似,我们有两个修改之处。第一个是载入(import) sun 的 resolver 类而不是 ibm 的 saxparser 类。
我们需要修改创建 parser 对象的代码,然后我们需要根据我们输入的 uri 创建一个 inputsource 对象。我们要修改的只是创建 parser 的代码需要被包含在 try 代码段中以捕获当我们创建 parser 对象时可能产生的异常。
import com.sun.xml.parser.resolver;
…
try
{
parser parser =
parserfactory.makeparser();
parser.setdocumenthandler(this);
parser.seterrorhandler(this);
parser.parse(resolver.
createinputsource(new file(uri)));
}
saxtwo.java (见附录2)
这段代码等同于 saxone.java,但它使用 sun 公司的 xml 解析器而不是 ibm 的。它展示了 sax 接口的可移植性。
总结
在本章节,我们介绍了一些使用 xml 解析器的高级编程技巧。我们展现了如何直接生成 dom 树,如何解析字符串而不是文件,如何在一棵 xml 树中移动元素以及如何改变解析器而不用影响 dom 和 sax 的代码。
希望您喜欢本教程!
这就是本教程的所有内容了。 我们讨论了 xml 应用的基本架构,而且我们也介绍了您如何处理 xml 文档。以后的教程将介绍构建 xml 应用更多的细节,包括:
使用可视工具来构建 xml 应用
将一个 xml 文档从一种形式转换到另一种
为最终用户或其他进程创建接口,及对后端存储数据的接口
要获得更多信息
如果您想了解更多的 xml 知识,可访问 developerworks 的 xml 专区。这个站点有示例代码、其它的教程、关于 xml 标准的信息以及其它内容。
最后,我们很愿意听取您的意见!我们设计 developerworks 是成为开发人员的资源。如果您有任何评价、建议或抱怨,请让我们知道。
谢谢,—doug tidwell 或 developerworks 中国站点!
