面向 Perl 开发人员的 XML,第 2 部分: 使用到 Perl 的高级 XML 解析技术
树解析和事件驱动的解析
本系列文章是为那些需要一种快捷的 XML/Perl 解决方案的人编写的。第 1 部分考察了 XML::Simple,把 XML 集成到 Perl 应用程序中的一种工具。这是本系列第二篇文章,向 Perl 程序员介绍两种主要的 XML 解析技术:树解析和事件驱动的解析。
Jim Dixon (jddixon@gmail.com), 自由作家, Freelance
Jim Dixon 是一位独立承包商,他最近回到旧金山,在那里推广用 Perl 和 Ruby 实现 Web 2.0。以前,他在一家英美互联网服务提供商担任技术主管有 7 年时间,开发了许多 Java/Java EE 软件。
简介
对于很多 Perl 应用程序来说,首选的 XML 工具是 XML::Simple
,这是本系列文章第 1 部分的主题(请参阅 参考资料)。XML::Simple
将 XML 输入文件转化成易于操作的 Perl 数据结构,然后将这种数据结构作为 XML 写回。但是要记住,有些情况下不能使用这种方法。
请访问 面向 Perl 和 PHP 开发人员的 XML:您可以通过该专题来了解更多与 Perl 和 PHP 相关的 XML 技术。
如果需要在内存中建立 XML 文档的表示,然后进行复杂的或不可预测的搜索或转换,XML::Simple
不是最好的办法,这种情况下应该使用树解析。如果 XML 文档不能全部装入内存或者是长度未知的流,就不能使用 XML::Simple
。此时必须使用事件驱动的解析器。多数人认为事件驱动的解析器乍看起来有点奇怪,但是一旦习惯了这种解析方式,SAX 也许会成为您首选的工具。
本文后面将讨论这两种使用 Perl 解析 XML 的高级方法。
入门
理解本文需要一些开放源码的 Perl 模块。通常有两种方法获得:如果操作系统是 Windows,请使用 ppm,如果操作系统是 UNIX® 或 Linux™,则使用 CPAN(请参阅 参考资料 中的链接)。如果不熟悉这些存储库,本系列的第 1 部分提供了相关介绍。
清单 1 说明了如何在 UNIX/Linux 环境中获得这些模块。当然最好作为 root 登录,这样的话系统上的所有帐户都能使用这些模块。这些模块有一些依赖项,有些可能系统上没有。正确配置 cpan(follow=yes)可以让这些依赖项自动安装。
清单 1. 从 CPAN 获取本文用到的模块
$ perl -MCPAN -e shell cpan> install XML::LibXML XML::SAX::Base XML::SAX::ExpatXS XML::SAX::Writer cpan> quit
在 Windows 下更简单,如清单 2 所示。同样,最好用 admin 账户进行安装。
清单 2. 使用 PPM 获得模块
$ppm install XML::LibXML XML::SAX::Base XML::SAX::ExpatXS XML::SAX::Writer
树解析
多数程序员可能发现把 XML 看成一种树结构更方便。经过数年努力,这种 XML 观点被规范化为文档对象模型(DOM),2002 年发布了 DOM Level 3。
DOM 把 XML 文档表示成双链接节点组成的树,每一层上的第一个孩子链接到父节点和所有的兄弟节点。大部分函数都在树上定义,主流编程语言都有实现。
虽然可以沿着链接遍历树,但从节约程序员时间上来说,使用 XPath 协议通常效率更高。这是一种用于遍历节点、检索节点组等的子语言。
关于 DOM 规范本身以及其他更容易阅读的 DOM 规范、XPath 及相关协议的介绍性文章,请参阅 参考资料。
很多 Perl 模块能够把 XML 文档解析成 DOM 树。其中 Petr Pajas 的 XML::LibXML
是最好的一个(请参阅 参考资料)。它包装了 Gnome 项目的 libxml2,后者是一种功能完善的包,其中包括 DOM 解析器、XPath 的部分实现和 SAX2 实现(后面 讨论)。
清单 3 是本系列第 1 部分中使用的 XML 文件(请参阅 参考资料),当时我们用 XML::Simple
解析它,将其表示成 Perl 数据结构并修改,然后使用 XML::Simple
再转换成文本形式的 XML。
清单 3. Rosie 的宠物商店,pets.xml
<?xml version='1.0'?> <pets> <cat> <name>Madness</name> <dob>1 February 2004</dob> <price>150</price> </cat> <dog> <name>Maggie</name> <dob>12 October 2002</dob> <price>75</price> <owner>Rosie</owner> </dog> <cat> <name>Little</name> <dob>23 June 2006</dob> <price>25</price> </cat> </pets>
使用 XML::LibXML
解析它非常简单(如 清单 4 所示,程序输出见 清单 5)。一个简单的 $parser->parse_file
就能创建 DOM 模型的 XML 树结构。这里定义了一个简单的 Perl 子例程向树中的一个节点增加子元素,然后用它构造表示单个新宠物的子树。接下来我们使用子例程 addPet()
在目录中添加两只新宠物:一只沙鼠和一只仓鼠。
清单 4. XML::LibXML
解析 Rosie 的库存文件
#!/usr/bin/perl -w use strict; use XML::LibXML; my $parser = XML::LibXML->new; my $doc = $parser->parse_file('pets.xml') or die "can't parse Rosie's stock file: $@"; my $root = $doc->documentElement(); sub addSubElm($$$) { my ($pet, $name, $body) = @_; my $subElm = $pet->addNewChild('', $name); $subElm->addChild( $doc->createTextNode($body) ); } sub addPet($$$$) { my ($type, $name, $dob, $price) = @_; # addNewChild is non-compliant; could use addSibling instead my $pet = $root->addNewChild('', $type); addSubElm ( $pet, 'name', $name ); addSubElm ( $pet, 'dob', $dob ); addSubElm ( $pet, 'price', $price ); } addPet('gerbil', 'nasty', '15 February 2006', '5'); addPet('hamster', 'boris', '5 July 2006', '7.00'); my @nodeList = $doc->getElementsByTagName('price'); foreach my $priceNode (@nodeList) { my $curPrice = $priceNode->textContent; my $newPrice = sprintf "%6.2f", $curPrice * 1.2; my $parent = $priceNode->parentNode; my $newPriceNode = XML::LibXML::Element->new('price'); $newPriceNode->addChild ( $doc->createTextNode( $newPrice ) ); $parent->replaceChild ( $newPriceNode, $priceNode ); } print $doc->toString(1); # pretty print
为了帮助您进一步掌握 DOM,下面我们将得到树中的价格节点引用列表并加价 20%。由于可以用多个文本节点表示元素中的文本(价格),最简单的办法就是从节点中取得价格,增加后改变格式,然后整体替换原来的节点,而不是就地修改。这种方法当然要比第 1 部分中的 Perl 代码复杂得多。
清单 5. 树解析器输出(经过整理)
<?xml version="1.0"?> <pets> <cat> <name>Madness</name> <dob>1 February 2004</dob> <price>180.00</price> </cat> <dog> <name>Maggie</name> <dob>12 October 2002</dob> <price> 90.00</price> <owner>Rosie</owner> </dog> <cat> <name>Little</name> <dob>23 June 2006</dob> <price> 30.00</price> </cat> <gerbil> <name>nasty</name><dob>15 February 2006</dob><price> 6.00</price> </gerbil> <hamster> <name>boris</name><dob>5 July 2006</dob><price> 8.40</price> </hamster> </pets>
使用更常规的树解析器处理 XML 时通常会出现这种情况。XML 文本格式的源代码被转换成了 DOM 树。要遍历树,可遍历节点、沿着链接从一个节点到另一个节点,或者使用类似 XPath 命令获得多组节点引用。然后再通过这些引用编辑节点。最后再把树写回磁盘或者以优美的格式打印。
对于小的、简单的树,从工程代价上来说使用 XML::Simple
通常更低廉。但是,如果 XML 文档非常复杂,由于能够使用 getElementsByTagName
这类方法,XML::LibXML
更合适。虽然这种方法运行起来可能比手工编码 Perl 和 XML::Simple
慢,但不需要编写,也不需要调试。
基于事件的解析:SAX
Simple API for XML (SAX) 采用了完全不同的解析方法,这种方法在一开始开销更大一些。SAX 把文档看作一系列事件,要求您告诉它如何响应每种事件。比如 start_document
、end_document
、start_element
、end_element
和 characters
。参考资料 中的 Perl SAX 2.1 Binding 提供了完整的清单。对于任何文档,Perl 程序员都必须使用一组处理程序方法,分别对应每种事件。
虽然看起来非常罗嗦,有不少重复,但实际上也是一种机会,后面您将看到。
虽然 XML::LibXML
提供了 SAX 接口,但仍然是一种 DOM 解析器,因此要把整个文档都读入内存然后再提供面向事件的接口。虽然可能有用,但是不能处理超出内存容量的 XML 文档或者 XML 流,比如 Jabber/XMPP。因此我们将使用 XML::SAX::ExpatXS
。该模块包装了 James Clark 的古老的 expat 解析器,可靠而且速度快。
假设有一家新开的宠物店,就像是本系列第 1 部分中的那个例子一样。清单 6 显示了商店库存目录中的一部分。
清单 6. Lizzie 的 Petatorium,pets2.xml
<stock> <item type="iguana" cost="124.42" location="stockroom" age="1"/> <item type="pig" cost="15" location="floor" age="0.5"/> <item type="parrot" cost="700" location="cage" age="6"/> <item type="pig" cost="117.50" location="floor" age="3.2"/> </stock>
为了使用 SAX2 解析,需要一些代码处理解析器生成的事件。最简单的事件处理程序就是输出每个事件中的一些文本的复写器。清单 7 中的代码解析新建的 XML。
清单 7. SAX 解析 pets2.xml
#!/usr/bin/perl -w #use strict; use XML::SAX::ParserFactory; use XML::SAX::Writer; my $writer = XML::SAX::Writer->new; $XML::SAX::ParserPackage = "XML::SAX::ExpatXS"; my $parser = XML::SAX::ParserFactory->parser(Handler => $writer); eval { $parser->parse_file('pets2.xml') }; die "can't parse Lizzie's stock file: $@" if $@;
XML 生成的结果如清单 8 所示。
清单 8. SAX 解析器输出
<?xml version='1.0'?><stock> <item cost='124.42' location='stockroom' type='iguana' age='1' /> <item cost='15' location='floor' type='pig' age='0.5' /> <item cost='700' location='cage' type='parrot' age='6' /> <item cost='117.50' location='floor' type='pig' age='3.2' /> </stock>
使用 ExpatXS 需要注意以下几点:
- 要保证所有的工具要么是 SAX 要么是 SAX2,但不要混合使用。如果 清单 7 中使用
XML::Handler::YAWriter
而不是XML::SAX::Writer
,就看不到任何错误消息,但输出也变成了一堆杂烩。由于 ExpatXS 是一种 SAX2 解析器,因此也必须使用 SAX2 复写器。 - 为了检查解析器错误,可以用
eval
把解析包装起来,然后测试$@
而不是$!
。 - 使用之前必须设置处理程序。必须知道,虽然程序员将 SAX 解析器看成是从左到右处理的管道(后面 还要进一步解释),但初始化必须从右向左进行。就是说对于管道 P > W,需要按照相反的顺序初始化,先 W 后 P。
驱动器和筛选器
SAX 的天才在这里表现了出来。SAX 定义了一个事件流:解析器生成一系列事件,将每个事件传递给一个处理程序。设想一个能够从不同角度观察的抽象模块。和解析器一样,它能生成 SAX 事件。但同时也是一个处理程序,在承担解析器角色的同时,也能通过开关帽子处理任何标准 SAX 事件,并把事件传递给下一个处理程序。就是说它定义了一组默认的方法仅仅用于传递事件。处理这些方法的模块是 XML::SAX::Base
。
要定义任何可能的 SAX 事件处理程序,程序员必须扩展 XML::SAX::Base
并重写任何需要的方法。其他事件不需要处理。这些事件处理程序可以链接在一起,从而能够建立像 UNIX 命令行那样的管道。这些处理程序有定义好的接口以及定义明确的内容:XML。
此外在管道的两端都已采用同样的方法扩展。第一步,生成器是 SAX2 解析器,使用 XML 文档生成事件。事实上,生成器可以是任何生成 SAX 事件的程序。比方说,可以编写一个模块读取数据库表并输出 SAX 事件流。(也存在这样的事件,比如 XML::Generator::DBI
。)
通常管道的另一端使用 SAX 事件而输出文档。XML::SAX::Writer
就完成这项工作。不过处理程序也很容易写入数据库(XML::SAX::DBI
)。
主要有两方面的好处。首先它鼓励开发简单转换事件流的 SAX 处理程序。这一点已经实现,现在有数百个开放源码的 Perl 模块实现了 SAX 2.1 绑定(请参阅 参考资料)。其次,它意味着设计人员可以集中精力定义仅提供必需功能的处理程序,与其他现有的处理程序结合起来完成工作。两者都是用低廉的机器资源替代高昂的程序员工作时间。
XML::SAX::Base
的详细讨论
使用 Kip Hampton 的 XML::SAX::Base
设计处理程序只需要两个简单步骤。首先处理程序必须扩展基类。其次,程序员必须重写必要的基本方法。然后就可以放弃事件或者调用重写了基类的方法。处理程序必须调用超类中的方法而是重写模块中的方法(如清单 9 所示)。
清单 9. 使用 XML::SAX::Base
package XyzHandler; use base qw(XML::SAX::Base); # extend it sub start_element { # override methods as necessary my $self = shift; my $data = shift; # parameter is a reference to a hash # transform/extract/copy data $self->SUPER::start_element($data); }
结束语
本系列文章分为三部分,这是第 2 部分,简要介绍了复杂的 XML 解析世界。
本文首先说明了如何将 XML 文档转化成内存中的对象树。一开始大多数程序员发现这种方法更易用,从很多角度来说,数据能合适地装入内存也确实很方便。
然后介绍了 SAX 和基于事件的解析,如果 XML 文档非常大或者是一个无终止的流,必须采用这种方法。结果开发出来的处理这种情形的工具本身形成了一种完全不同的编程风格,非常丰富:SAX 管道。
本系列的下一篇文章将说明在更复杂的应用程序中如何使用这些方法,DOM 和 SAX 解析。