dom4j使用xpath
文档对象模型(DOM)是一种平台和语言无关的接口,用于动态访问和更新XML文档的内容,结构和样式。 DOM定义了用于表示文档的标准接口集,如何组合这些对象的标准模型以及用于访问和操作它们的标准方法集。 DOM是一项W3C建议书,使之成为公认的Web标准。 实现可用于多种语言,包括Perl,C,C ++,Java,Tcl和Python。
正如我将在本文中演示的那样,当基于流的模型(例如SAX)不足时,DOM是XML处理的绝佳选择。 不幸的是,该规范的几个方面,例如与语言无关的界面以及对“一切皆有节点”抽象的使用,使其难以使用并且易于生成脆弱的代码。 在我公司最近对由多个开发人员在过去一年中开发的几个大型DOM项目的审查中,这一点尤其明显。 常见问题及其解决方法将在下面讨论。
探索DOM
DOM规范旨在与任何编程语言一起使用。 因此,它尝试使用一组通用的核心功能,所有语言都可用。 DOM规范还尝试在其接口定义中保持中立。 因此,Perl程序员可以在使用Java时应用其DOM知识,反之亦然。
规范还将文档的每个部分都视为由类型和值组成的节点。 这为处理文档的各个方面提供了一个优雅的概念框架。 例如,以下XML片段
<paragraph align="left">the <it>Italicized</it> portion.</paragraph>
通过以下DOM结构表示:
图1:XML文档的DOM表示

树的每个Document
, Element
, Text
和Attr
都是DOM::Node
。
设计问题
DOM语言中立性的缺点是不能采用每种编程语言中通常使用的方法和模式。 例如,由于XML节点的属性是一组唯一的名称/值对,因此自然会在Perl中将其表示为哈希。 但是,对于DOM,它们表示为一组节点,并且每个节点的值都通过单独的函数调用进行访问。 程序员必须学会使用许多新的数据结构和访问方法,而不是使用简单的哈希。 这些不便之处加起来是不寻常的编码实践和代码行数的增加。 它们还迫使程序员学习DOM的处事方法,以代替她直观地处理事务的方法。
一切都是节点的抽象,虽然非常优雅,但会导致尴尬的编码情况,例如上面的属性节点示例。 当访问XML标记中包含的值时,也会发生这种情况。 考虑一下XML片段: <tagname>Value</tagname>
。 您可能认为可以通过在tagname
节点上调用getValue
或类似方法来访问文本值。 实际上,该文本被视为tagname
节点下的一个或多个子节点。 因此,为了获得文本值,您需要遍历tagname
的子代,将它们整理为字符串。 这样做有充分的理由: tagname
可能包含其他嵌入式XML标记。 如果tagname
确实包含嵌入式XML标记,则获取其文本值就没有意义了。 然而,在现实世界中,由于缺乏便捷的功能,我们经常看到编码错误。
一切皆有节点的抽象也失去了一些价值,因为存在的节点类型数量众多,并且由于其访问方法中缺乏统一性。 例如, insertData
方法用于设置CharacterData
节点的值,而Attr
(属性)节点的值是通过直接访问value
字段来设置的。 通过为不同的节点提供不同的接口,可以减少模型的均匀性和优雅度,并增加学习曲线。
常见编码问题
对几个大型XML项目的分析揭示了使用DOM时遇到的一些常见问题。 下面介绍了其中一些。
代码膨胀
在我们审查的所有项目中,都存在一个总体问题:它需要很多代码行才能完成简单的事情。 在一个示例中,使用了16行代码来检查属性的值。 但是,具有增强的鲁棒性和错误处理能力的同一任务可以用三行代码来完成。 导致代码行数量增加的原因是DOM API的底层特性,方法和编程模式的不正确应用以及缺乏完整API的知识。 下面介绍了这些问题的具体实例。
遍历DOM
在我们检查的代码中,最常见的任务是遍历或搜索DOM。 这是在文档的config
部分下找到名为“ header”的节点所需的代码的简化版本:
$document_root = $dom_document->getDocumentElement();
my $config_node = $document_root->getFirstChild();
foreach my $node ( $config_node->getChildNodes() ) {
if ( $node->getName() eq "header") {
# do something
}
}
通过获取顶部元素,获取其第一个子config_node
( config_node
),最后通过config_node
检查config_node
的子config_node
,从根目录遍历文档。 不幸的是,这种方法不仅非常冗长,而且还充满了脆弱性和潜在的错误。
例如,代码的第二行使用getFirstChild
方法获取中间节点。 已经存在许多潜在的问题。 根节点的第一个孩子实际上可能不是用户正在搜索的config_node
。 通过盲目跟随第一个孩子,我们已经忽略了标签的实际名称,可能会搜索文档中不正确的部分。 当源XML文档在根节点之后包含空格或回车时,会发生这种情况下的常见错误。 根节点的第一个子节点实际上是DOM::Text
节点,而不是预期的节点。 为了正确地导航到我们想要的节点,我们需要检查document_root
的每个子节点,直到找到不是Text
节点并且具有我们要查找的名称的子节点。
我们也忽略了文档可能具有与我们预期不同的结构的可能性。 例如,如果document_root
没有任何子节点,则config_node
将设置为undef
,示例的第三行将引发错误。 因此,为了正确地浏览文档,我们不仅必须单独检查每个子节点并检查适当的名称,而且在每一步中,我们还必须检查以确保每个方法调用均返回有效值。 编写可处理任意输入的健壮,无错误的代码既需要大量关注细节,又需要许多行代码。
检索标签中的文本值
遍历DOM之后,第二个最常见的任务是检索标签中包含的文本值。 考虑XML片段<sometag>The Value</sometag>
。 导航到sometag
节点后,如何捕获其文本值( The Value
)? 一个直观的实现可能是:
$sometag->getData();
您可能已经猜到了,上面的代码将无法执行所需的操作。 我们无法在sometag
节点上调用getData
或类似函数,因为实际文本存储为一个或多个子节点。 更好的方法是:
$sometag->getFirstChild()->getData();
这里的问题是该值可能实际上没有包含在第一个孩子中。 处理指令或其他嵌入式节点可以在sometag
找到,或者文本值可以包含在多个子节点中,而不仅仅是一个子节点中。 回想一下,空格经常表示为文本节点,因此调用$sometag->getFirstChild()
可能只会使您在标记及其值之间返回回车符。 实际上,我们需要遍历所有子项,检查Text
类型的节点,然后整理它们的值,直到获得完整的值。
getElementsByTagName
DOM接口包括一种用于查找具有给定名称的子节点的方法。 例如,调用:
my @results = $document_root->getElementsByTagName("name");
将从文档中返回名为name
的标签的数组(或NodeList
)。 这肯定比我们上面讨论的遍历方法更方便。 这也是导致一系列常见错误的原因。
问题是getElementsByTagName
递归遍历文档,返回所有匹配的节点。 假设您有一个包含客户信息,公司信息和产品信息的文档。 所有这三个项目都可能在其中包含name
标签。 如果您要调用getElementsByTagName
搜索客户名称并以产品和公司名称结尾,则您的程序可能会出现异常。 在文档的子树上调用函数可以降低风险。 但是,XML的灵活特性使得很难确保要操作的子树具有您期望的结构,并且没有带有正在搜索名称的虚假子节点。
有效使用DOM
鉴于DOM的设计约束所施加的限制,您如何有效地使用规范? 我们提出了一些有关DOM使用的基本原则和准则,并创建了一个函数库来简化生活。
基本原则
如果遵循一些基本原则,您使用DOM的经验将得到显着改善:
- 不要使用DOM遍历文档
- 尽可能使用XPath查找节点或遍历文档
- 使用更高级别的函数库来简化DOM的使用
这些原则直接来自对常见问题的研究。 如上所述,DOM遍历是错误的主要原因。 但是,它也是最常用的功能之一。 我们如何在不使用DOM的情况下遍历文档?
XPath
XPath是一种用于寻址,搜索和匹配文档片段的语言。 这是W3C建议书,使其成为公认的标准,并且已在大多数语言和XML包中实现。 您的DOM包可能会直接或通过附件支持XPath。
XPath提供了一种极好的方法来遍历和搜索文档。 它使用类似于文件系统和URL中使用的路径符号来指定和匹配文档的各个部分。 例如,XPath: /x/y/z
在文档中搜索x的根节点,该根节点位于该节点y之下,该节点y则位于该节点z之下。 该语句返回与指定路径结构匹配的所有节点。
在文档的结构以及节点的值及其属性方面,更复杂的匹配都是可能的。 语句/x/y/*
返回带有父x的任何节点y下的所有节点。 /x/y[@name='a']
匹配具有父x且具有名为name
且值为a
的属性的所有节点y。
对XPath及其用法的完整检查不在本文的讨论范围之内。 请参阅相关信息的链接,一些优秀的教程。 花一点时间来学习XPath,您将获得更轻松地处理XML文档的回报。
功能库
我们检查DOM项目的令人惊讶的方面之一是存在大量的复制和粘贴代码。 一个文件中的代码段将被复制并粘贴到许多其他文件中,以实现类似的功能。 为什么经验丰富的开发人员会采用良好的编程习惯,而他们会采用复制粘贴方法而不是创建帮助程序库? 我们认为这是因为大多数程序员都不是DOM专家,他们会很乐意抓取执行所需功能的第一段代码。 他们对自己的DOM技能没有足够的信心来产生组成帮助程序库的规范函数。
创建和使用帮助程序库来实现通用功能非常容易。 它只需要少量的纪律。 以下是一些基本的帮助程序功能,可以帮助您入门。
getValue
使用XML文档时,最常执行的操作是查找给定节点的值。 如上所述,这可能在遍历文档以找到所需节点以及检索节点的值方面都带来困难。 可以使用XPath简化遍历,并且可以将值的检索编码一次,然后重新使用。 我们已经通过两个较低级别的函数findNode
实现了getValue
函数。 该帮助程序查找并返回与给定XPath表达式匹配的第一个节点,以及getTextContents
,后者以递归方式返回传入节点下的文本节点的串联值,如清单2所示。
sub getTextContents {
my ($node, $strip)= @_;
my $contents;
if (! $node )
{
return;
}
for my $child ($node->getChildNodes()) {
if ( ! is_element_node($child) ) {
$contents .= $child->getData();
}
}
if ($strip) {
$contents =~ s/^\s+//;
$contents =~ s/\s+$//;
}
return $contents;
}
sub findNode {
my ($node, $xpath) = @_;
if (! defined($node) || ! defined($xpath) )
{
return undef;
}
my $match = ($node->xql($xpath))[0];
if (! $match )
{
return undef;
}
return $match;
}
sub getValue {
my ($node, $xpath) = @_;
my $match = findNode( $node, $xpath );
if (! defined($match) )
{
return undef;
}
return getTextContents( $match );
}
通过传入从中开始搜索的节点和指定我们要搜索的节点的XPath语句来调用getValue
。 该函数找到第一个匹配给定XPath的节点,并提取其文本值。
设定值
另一个常见的操作是将节点的值设置为所需值,如清单3所示。
sub setValue {
my ($node, $xpath, $value) = @_;
my $match = findNode( $node, $xpath );
if (! defined($match) )
{
return undef;
}
foreach my $child ( $match->getChildNodes() )
{
$match->removeChild ($child);
}
$match->addText($value);
return $match;
}
此函数采用一个起始节点和一个XPath语句(就像getValue
一样)和一个将匹配节点的值设置为的字符串。 它使用findNode
查找所需的节点,删除其所有子节点(从而删除其中包含的所有文本和其他元素),并将其文本内容设置为传入的字符串。
appendNode
当某些程序查找并修改XML文档中包含的值时,其他程序则通过添加和删除节点来修改文档本身的结构。 这个辅助函数简化了向文档添加节点的过程,如清单4所示。
sub appendNode {
my ($doc, $nodename, $xpath, $value) = @_;
if (! defined($nodename) || ($nodename eq "") ) {
return undef;
}
my $match = findNode( $doc, $xpath );
if (! defined($match) )
{
return undef;
}
my $newnode;
eval {
$newnode = $doc->createElement( $nodename );
};
if ($@ || (! defined($newnode) )) {
return undef;
}
$match->appendChild( $newnode );
if ( defined($value) ) {
$newnode->addText($value);
}
return $newnode;
}
该函数的参数包括DOM文档,要添加的节点的名称,指定要在其下添加节点的XPath语句(即新节点的父节点所在的节点)以及文本值(可选)节点的 新节点将附加到指定的父节点,并且其值设置为传入的字符串。
copySubTree
将文档的一部分复制到另一个位置或文档中虽然不是很常见的操作,但却引起了很多混乱,并导致了各种创造性的复制过程。 如清单5所示,它实际上很容易实现。
sub copySubTree
{
my ($sourcenode, $destnode) = @_;
my $copy_node = $sourcenode->cloneNode(1);
if ( $sourcenode->getOwnerDocument() ne $destnode->getOwnerDocument() )
{
$copy_node->setOwnerDocument( $destnode->getOwnerDocument() );
}
$destnode->appendChild($copy_node);
return $copy_node;
}
此功能获取源节点并将其复制为目标节点下的子节点。 目的节点可能在另一个文档中,在这种情况下,子树将在文档之间复制。
结论
DOM已成为处理XML文档的一种困难且非直观的方式。 实际上,它构成了一个非常有效的基础,可以通过遵循一些简单的原理来构建易于使用的系统。 DOM已经在大多数平台上实现和优化,对于需要在复杂过程中搜索和操作XML文档的应用程序来说,它是一个很好的选择。
翻译自: https://www.ibm.com/developerworks/xml/library/x-domprl/index.html
dom4j使用xpath