面向 Perl 开发人员的 XML,第 1 部分: XML 加 Perl —— 简单的
简介
本文是关于 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
<config>
<user>freddy</user>
<passwd>longNails</passwd>
<books>
<book author="Steinbeck" title="Cannery Row"/>
<book author="Faulkner" title="Soldier's Pay"/>
<book author="Steinbeck" title="East of Eden"/>
</books>
</config>
除了构造器之外,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 文件的一部分
<?xml version='1.0'?>
<pets>
<cat>
<name>Madness</name>
<dob>1 February 2004</dob>
<price>150</price>
</cat>
<dog>Maggie</name>
<dob>12 October 2005
<name></dob>
<price>75</price>
<owner>Rosie</owner>
</dog>
<cat>
<name>Little</name>
<dob>23 June 2006</dob>
<price>25</price>
</cat>
</pets>
最初的探索
首先尝试按照清单 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", $amt * (1 $change)
将出生日期转换为年龄有点儿困难。但是,研究一下 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 选项。如果没有另外指定的话,它将使用 <opt> 作为根元素。如果内存中的数据结构具有根元素名称,那么添加 KeepRoot 选项,将这个选项设置为 true 或者 1(在 Perl 中 1 表示真)。清单 15 演示了具体做法。
清单 15. 输出到 XML 文件
$simple->XMLout($data,
KeepRoot => 1,
OutputFile => 'pets.fixed.xml',
XMLDecl => "<?xml version='1.0'?>",
);
完整的解决方案
清单 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", $amt * (1 $change);
}
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 ?
( ", $ageMonths month" . ($ageMonths > 1 ? 's' : '') )
: '');
} elsif ($ageMonths > 1) {
$age = "$ageMonths months";
} elsif ($ageMonths == 1) {
$age = '1 month' . ( $ageDays > 0 ?
( ", $ageDays day" . ($ageDays > 1 ? 's' : '') ) : '');
} 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 => "<?xml version='1.0'?>",
);
尽管还能让这段 Perl 代码更简洁,但是它已经足以说明在 Perl 中处理 XML 是多么容易。尤其是,通过使用分派表,可以按照非常清晰且可维护的方式处理许多不同结构的元素类型。
限制
不幸的是,有些操作无法用 XML::Simple 完成。我将在第 2 部分和第 3 部分中详细讨论这个问题,但是 XML::Simple 有两个主要限制。首先,在输入方面,它将完整的 XML 文件读入内存,所以如果文件非常大或者需要处理 XML 数据流,就不能使用这个模块。第二,它无法处理 XML 混合内容,也就是在一个元素体中同时存在文本和子元素的情况,如清单 17 所示。
清单 17. 混合内容
<example>of <mixed/> content</example>
如何判断文件是否太大了,XML::Simple 处理不了?经验规则是,XML 被读入内存时它会扩大 10 倍。这意味着,如果您的工作站上有几百 MB 的空闲内存,那么 XML::Simple 能够处理的 XML 文件大小最多为几十 MB。
结束语
XML 在计算环境中已经无处不在了,而且越来越深地植入了现代应用程序和操作系统中。Perl 程序员迫切需要掌握使用 XML 的方法。XML::Simple 这样的工具能够轻松地将 XML 文档转换为容易理解的 Perl 数据结构,以及将这些数据结构转换回 XML。这些操作一般只需一行代码。
另一方面,XML 专家也会惊喜地发现 Perl 在转换和响应 XML 内容方面是多么有帮助。
第 2 部分将讲解如何在 Perl 中进行两种主要的 XML 解析:树解析和事件驱动的解析。