使用 XML::Simple 将 XML 集成到 Perl 应用程序中
简介: 本系列为需要 XML 和 Perl 快速解决方案的开发人员提供了一份指南。在许多情况下,只需要 XML::Simple 这一种工具就能够将 XML 集成到 Perl 应用程序中。第 1 部分讲解在哪里获得 XML::Simple、如何使用它以及接下来要做什么。对在 Perl 中使用 XML 有所体会之后,本系列中的另外两篇文章将帮助您提高技能。 简介
本文是关于 Perl 和 XML 的分三部分的系列文章的第一篇,主要关注 XML::Simple。对于 Perl 程序员,第一次使用 XML 往往是从配置文件接收参数。本文要讲解如何用两行代码读取这样的参数,第一行告诉 Perl 要使用 XML::Simple,第二行将一个变量设置为文件中的一个值。甚至不必提供配置文件的名称:XML::Simple 会进行智能化的猜测。 作为一个更复杂的示例,我们要研究一个宠物商店应用程序。在那一节中,将学习如何简便地将 XML 文件读入一个层次化的 Perl 数据结构(匿名数组和散列的组合)。本文讲解 Perl 如何简便地转换和重组原 XML 文档中包含的信息,然后以各种形式将信息写回去。
最后,讨论 XML::Simple 的一些限制。这会引出后两篇文章的主题:更高级的解析,使用高级工具对 XML 的形式进行转换,以及对 DOM 和其他内存中形式的 XML 进行串行化。
本文主要针对非常熟悉 Perl 的 Perl 程序员,但是对 XML 专家也有用,可以帮助他们以更程序性的方式操纵 XML 文档。
开始
在开始之前,需要安装 Perl。如果还没有安装 Perl,请参见 参考资料 中的链接。 接下来,需要 XML::Simple。如果使用 UNIX 或 Linux,那么最方便的方法是使用 cpan 从 CPAN 获得它们。首先,使用清单 1 中的命令在机器上安装 cpan。一般来说,应该作为根用户执行这个操作,从而让 Perl 模块可供所有用户使用。
清单 1. 安装 cpan,获得 XML::Simple
$ perl -MCPAN -e shell cpan> ...
cpan> install XML::Simple cpan> quit
首次运行此命令时,要经历很长的对话。这在 清单 1 中做了省略。某些用户会发现,编辑得到的配置文件(/etc/perl/CPAN/Config.pm)很方便。
Windows 用户使用 PPM 执行相似的过程(如果您还没有 PPM,请参见 参考资料)。在这种情况下,安装模块的命令与清单 2 相似。
清单 2. Windows:使用 PPM 获得 XML::Simple $ ppm install XML::Simple cpan 和 ppm 都会在安装期间检查依赖项,并从存储库获取任何缺少的依赖项。如果将 cpan 的先决条件策略设置为 “follow”,那么这是自动的。在安装期间,模块一般会被编译,并产生几页消息。这会花些时间,这是正常的。 另一个先决条件 XML::Simple 将 XML 文档转换为对散列和散列数组的引用。这意味着需要充分了解引用、散列和数组在 Perl 中的交互作用。如果在这方面需要帮助,请参阅 参考资料 中精彩的 Perl 参考教程。 XML::Simple Grant McLean 的 XML::Simple 基本上有两个功能;它将 XML 文本文档转换为 Perl 数据结构(匿名散列和数组的组合),以及将这种数据结构转换回 XML 文本文档。 这些功能尽管有限,但是很有用,我们将在两个层次上说明这一点。首先,您将看到如何从 XML 形式的配置文件中导入数据。然后在更复杂的本地宠物商店示例中,学习如何将复杂的大型 XML 文件读入内存,以传统 XML 工具(比如 XSLT)不可能实现的方式对它进行转换,并将它写回磁盘。 对于大多数情况,XML::Simple 提供了在 Perl 中处理 XML 所需的所有东西。 XML 配置文件 全世界的所有程序员都要面对一个问题:需要将适度复杂的配置信息传递给程序,但是如果用命令行参数传递这些信息,就太麻烦了。所以决定使用配置文件。因为 XML 是这种信息的标准格式,所以决定采用 XML 文件格式,形成的文件像清单 3 这样。我们将使用 XML::Simple 处理这个文件。
清单 3. 配置文件 part1.xml
除了构造器之外,XML::Simple 有两个子例程:XMLin() 和 XMLout()。如您所预料的,第一个子例程读取 XML 文件,返回一个引用。给出适当数据结构的引用,第二个子例程将它转换为 XML 文档,根据参数的不同,产生的 XML 文档采用字符串格式或文件形式。 XML::Simple 有一些合理的默认设置,例如如果没有指定输入文件名,那么 Perl 程序 part1.pl(清单 4)将读取文件 part1.xml。
清单 4. part1.pl
#!/usr/bin/perl -w use strict;
use XML::Simple; use Data::Dumper; print Dumper
(XML::Simple->new()->XMLin());
执行 part1.pl 会产生清单 5 所示的输出。
清单 5. part1.pl 的输出 $VAR1 = {
'passwd' => 'longNails', 'user' => 'freddy',
'books' => {
'book' => [ {
'title' => 'Cannery Row', 'author' => 'Steinbeck' }, {
'title' => 'Soldier\\'s Pay', 'author' => 'Faulkner' }, {
'title' => 'East of Eden', 'author' => 'Steinbeck' } ] } };
XMLin() 返回一个对散列的引用。如果将这个引用赋值给变量 $config,就可以使用 $config->{user} 获得用户名,使用 $config->{passwd} 获得密码。关心简便性的读者会注意到,只用一行代码就可以读取配置文件并返回一个参数:XML::Simple->new->{user}。
显然,在处理 XML::Simple 时要注意几个问题:
首先,它丢弃了根元素的名称。
第二,它将具有相同名称的元素合并成一个匿名数组引用。因此,第一本书的标题是 @{$config->{books}->{book}}[0]->{title},即 “Cannery Row”。 第三,它以同样的方式对待属性和子元素。
可以通过 XMLin() 的选项改变这些行为。关于选项的更多信息,参见 参考资料 和下面的讨论。 一个更复杂的示例:宠物商店 XML::Simple 不仅仅能够对配置文件进行简单的解析。实际上,它可以处理复杂的大型 XML 文件,并将它们转换为整齐的数据结构,这些结构常常更适合进行转换,这在 Perl 中是非常容易的,但是使用比较传统的 XML 转换工具(比如 XSLT)是很难完成的,甚至是不可能的。 假设您在一家宠物商店工作,要在一个 XML 文件中记录关于宠物的信息。这个文档的一部分如清单 6 所示。经理希望做一些修改:
为了节省空间,将所有子元素改为属性 将价格提高 20%
让所有价格显示为同样的形式,都显示两位小数 对列表进行排序 用年龄替换出生日期
由于您对 Perl 有信心,而且意识到 XSLT 无法完成计算,所以决定用 XML::Simple 完成这个工作(见清单 6)。
清单 6. pets.xml 文件的一部分
最初的探索
首先尝试按照清单 7 这样使用 XML::Simple。
清单 7. 最初的尝试 #!/usr/bin/perl -w use strict;
use XML::Simple; use Data::Dumper;
my $simple = XML::Simple->new(); my $data =
$simple->XMLin('pets.xml'); # DEBUG
print Dumper($data) . \"\\n\"; # END
谨慎起见,先使用 Data::Dumper 查看在内存中读入了什么内容,结果如清单 8 所示。
清单 8. 获得的内容 $VAR1 = {
'cat' => {
'Little' => {
'dob' => '23 June 2006', 'price' => '25' },
'Madness' => {
'dob' => '1 February 2004', 'price' => '150' } },
'dog' => {
'owner' => 'Rosie',
'dob' => '12 October 2005', 'name' => 'Maggie', 'price' => '75' } };
结果是让人失望的。猫和狗的表示方式完全不一样:两只猫的信息存储在一个双重嵌套的散列中,以名称作为键;而关于狗的信息存储在一个简单散列中,它的名称只是属性之一。另外,根元素的名称消失了。所以您去阅读文档(参见 参考资料)并发现有一些选项,尤其是 ForceArray=>1 和 KeepRoot=>1。第一个选项使所有嵌套元素都表示为数组。在输入中第二个选项指示保留根元素的名称。正如在后面的输出中看到的,这意味着数据的内存表示会包含根元素的名称。使用这些选项之后,得到了清单 9 中的结果,这对于程序员来说处理起来容易多了,尽管它占用的内存要多一点儿。
清单 9. 添加选项之后的 Data::Dumper 输出,整洁了些,可读性有所提高 $VAR1 = {
'pets' => [ {
'cat' => [
{
'dob' => [ '1 February 2004' ], 'name' => [ 'Madness' ], 'price' => [ '150' ] }, {
'dob' => [ '23 June 2006' ], 'name' => [ 'Little' ], 'price' => [ '25' ] } ],
'dog' => [ {
'owner' => [ 'Rosie' ],
'dob' => [ '12 October 2005' ], 'name' => [ 'Maggie' ], 'price' => [ '75' ] } ] } ] };
对内存中的数据结构进行转换
现在在内存中已经有了一个整齐的结构,非常容易通过程序处理它。为了实现您老板的第一个要求(将子元素转换为属性),需要替换对数组的引用,如清单 10 所示。
清单 10. 对单元素数组的引用 'name' => [ 'Maggie' ]
然后,必须替换简单值的引用,如清单 11 所示。
清单 11. 简单值的引用 'name' => 'Maggie'
经过这一修改,XML::Simple 将输出一个属性 —— 值对,而不是子元素。在需要输出一个类型的多个实例的情况下 —— 在这个示例中,有两只猫和一只狗 —— 需要以匿名散列的匿名数组的形式收集散列。清单 12 演示如何完成这个有点儿技巧性的任务。
清单 12. 将数组转换为散列,从而将元素转换为属性 sub makeNewHash($) { my $hashRef = shift; my %oldHash = %$hashRef; my %newHash = ();
while ( my ($key, $innerRef) = each %oldHash ) { $newHash[$key] = @$innerRef[0]; }
return \\%newHash; } 给出一个描述单个宠物的 XML 引用,这段代码将它转换为一个散列。如果该类型只有一只宠物,那么这样就可以了。可以将这个新散列的引用写回 $data。但是,如果该类型有多只宠物,要写回的就应该是对一个匿名数组的引用,这个数组包含对描述各个宠物的匿名散列的引用。可以查看完整解决方案(清单 16)中的 foldType(),了解这是如何实现的。 其他需求:Perl 的出色之处 老板的其他需求是对列表进行排序、将价格提高 20%、将价格写成两位小数以及用年龄替换出生日期。第一个需求无需处理,因为这是 XML::Simple 输出的默认设置。在 Perl 中,第二个和第三个需求只需一行代码就能够实现。Perl 具有很方便的多态性:在将价格提高 20% 时,价格是数字;但是,如果将它们作为字符串写回,它们会保持您所指定的格式。所以清单 13 同时完成这两项工作,它将字符串转换为数字,处理后再转换回字符串。 清单 13. 提高价格并重新格式化 sprintf \"%6.2f\ 将出生日期转换为年龄有点儿困难。但是,研究一下 CPAN 就会发现,Date::Calc 提供了所需的所有特性(还有许多其他特性)。Decode_Date_EU 将 ‘European’ 格式的日期(比如 13 January 2006)转换为三个元素的数组(YMD),这是这个包使用的标准日期格式。给出两个这样的日期,Delta_YMD($earlier, $later) 会产生相同格式的时间差,这样就可以得到年龄。但糟糕的是,Delta_YMD 有点儿错误:有时候,天或月份会是负数!但是,在 google 上很容易搜索到修复方法。完整解决方案(见 清单 16)中的 deltaYMD 演示了如何处理这个问题。 对猫和狗进行分派
为了使代码更容易扩展,要使用清单 14 所示的分派表。Jason Dominus 的精彩著作 Higher Order Perl 中详细讨论了分派表(参见 参考资料 中的链接)。
清单 14. 分派表 my $DISPATCHER = { 'cat' => sub
{ foldType(shift); }, 'dog' => sub { foldType(shift); },
'hippo' => \\&hippoFunc, }; 分派表可以包含用来处理特定元素的实际代码(匿名子例程),也可以包含对别处定义的命名子例程的引用。可以使用这种结构实现其他语言中 switch-case 结构的效果。 在这个示例中,只有两种元素类型,猫和狗。在真实的 XML 文档中,可能有许多元素类型,而且处于不同的层次上。尽管可以在 Perl 中使用 if ... elsif ... elsif 结构,但是使用一个或多个分派表要清晰得多,而且更容易维护。 将 XML 写回磁盘 XML::Simple 的默认输出通常是很合理的。如果没有为 XMLout() 指定选项,它就会产生一个字符串。如果希望将输出写到文件中,就要加上 OutputFile 选项。如果没有另外指定的话,它将使用 OutputFile => 'pets.fixed.xml', XMLDecl => \"\ ); 完整的解决方案 清单 16 中的 112 行代码就可以完成老板的要求。XML::Simple 的简便性确实让人印象深刻。有 8 行代码用来读写 XML。其他代码的一小半儿用来转换 XML 的结构。 清单 16. 代码的最终版本 #!/usr/bin/perl -w use strict; use XML::Simple; use Date::Calc qw(Add_Delta_YM Decode_Date_EU Delta_Days Delta_YMD); use Data::Dumper; my $simple = XML::Simple->new (ForceArray => 1, KeepRoot => 1); my $data = $simple->XMLin('pets.xml'); my @now = (localtime(time))[5, 4, 3]; $now[0] += 1900; # Perl years start in 1900 $now[1]++; # months are zero-based sub fixPrice($$) { my ($amt, $change) = @_; return sprintf \"%6.2f\} sub deltaYMD($$) { my ($earlier, $later) = @_; # refs to YMD arrays my @delta = Delta_YMD (@$earlier, @$later); while ( $delta[1] < 0 or $delta[2] < 0 ) { if ( $delta[1] < 0 ) { # negative month $delta[0]--; $delta[1] += 12; } if ( $delta[2] < 0 ) { # negative day $delta[1]--; $delta[2] = Delta_Days( Add_Delta_YM (@$earlier, @delta[0,1]), @$later); } } return \\@delta; } sub dob2age($) { my $strDOB = shift; my @dob = Decode_Date_EU($strDOB); my $ageRef = deltaYMD( \\@dob, \\@now ); my ($ageYears, $ageMonths, $ageDays) = @$ageRef; my $age; if ( $ageYears > 1 ) { $age = \"$ageYears years\"; } elsif ($ageYears == 1) { $age = '1 year' . ( $ageMonths > 0 ? ( \ : ''); } elsif ($ageMonths > 1) { $age = \"$ageMonths months\"; } elsif ($ageMonths == 1) { $age = '1 month' . ( $ageDays > 0 ? ( \ } else { $age = \"$ageDays day\" . ($ageDays != 1 ? 's' : ''); } return $age; } sub makeNewHash($) { my $hashRef = shift; my %oldHash = %$hashRef; my %newHash = (); while ( my ($key, $innerRef) = each %oldHash ) { my $value = @$innerRef[0]; if ($key eq 'dob') { $newHash{'age'} = dob2age($value); } else { if ($key eq 'price') { $value = fixPrice($value, 0.20); } $newHash{$key} = $value; } } return \\%newHash; } sub foldType ($) { my $arrayRef = shift; # if single element in array, return simple hash if (@$arrayRef == 1) { return makeNewHash(@$arrayRef[0]); } # if multiple elements, return array of simple hashes else { my @outArray = (); foreach my $hashRef (@$arrayRef) { push @outArray, makeNewHash($hashRef); } return \\@outArray; } } my $dispatcher = { 'cat' => sub { foldType(shift); }, 'dog' => sub { foldType(shift); }, }; my @base = @{$data->{pets}}; my %types = %{$base[0]}; my %newTypes = (); while ( my ($petType, $arrayRef) = each %types ) { my @petArray = @$arrayRef; print \"type $petType has \" . @petArray . \" representatives \\n\"; my $refReturned = &{$dispatcher->{$petType}}( $arrayRef ); $newTypes{$petType} = $refReturned; } $data->{pets} = \\%newTypes; # overwrite existing data $simple->XMLout($data, KeepRoot => 1, OutputFile => 'pets.fixed.xml', XMLDecl => \"\ ); 尽管还能让这段 Perl 代码更简洁,但是它已经足以说明在 Perl 中处理 XML 是多么容易。尤其是,通过使用分派表,可以按照非常清晰且可维护的方式处理许多不同结构的元素类型。 限制 不幸的是,有些操作无法用 XML::Simple 完成。我将在第 2 部分和第 3 部分中详细讨论这个问题,但是 XML::Simple 有两个主要限制。首先,在输入方面,它将完整的 XML 文件读入内存,所以如果文件非常大或者需要处理 XML 数据流,就不能使用这个模块。第二,它无法处理 XML 混合内容,也就是在一个元素体中同时存在文本和子元素的情况,如清单 17 所示。 清单 17. 混合内容 树解析和事件驱动的解析 简介: 本系列文章是为那些需要一种快捷的 XML/Perl 解决方案的人编写的。第 1 部分考察了 XML::Simple,把 XML 集成到 Perl 应用程序中的一种工具。这是本系列第二篇文章,向 Perl 程序员介绍两种主要的 XML 解析技术:树解析和事件驱动的解析。 简介 对于很多 Perl 应用程序来说,首选的 XML 工具是 XML::Simple,这是本系列文章第 1 部分的主题(请参阅 参考资料)。XML::Simple 将 XML 输入文件转化成易于操作的 Perl 数据结构,然后将这种数据结构作为 XML 写回。但是要记住,有些情况下不能使用这种方法。 如果需要在内存中建立 XML 文档的表示,然后进行复杂的或不可预测的搜索或转换,XML::Simple 不是最好的办法,这种情况下应该使用树解析。如果 XML 文档不能全部装入内存或者是长度未知的流,就不能使用 XML::Simple。此时必须使用事件驱动的解析器。多数人认为事件驱动的解析器乍看起来有点奇怪,但是一旦习惯了这种解析方式,SAX 也许会成为您首选的工具。 本文后面将讨论这两种使用 Perl 解析 XML 的高级方法。 入门 清单 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::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\ 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. 树解析器输出(经过整理) 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 为了使用 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 解析器输出 使用 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 解析。 因篇幅问题不能全部显示,请点此查看更多更全内容