perl xml
这是有关Perl和XML的由三部分组成的系列文章中的第一篇,该系列关注XML::Simple
。 对于Perl程序员而言,XML的最常见的首次使用是从配置文件中检索参数。 本文向您展示如何用两行代码读取这样的参数,第一行告诉Perl正在使用XML::Simple
,第二行告诉Perl在文件中将变量设置为值。 您甚至不必提供配置文件的名称: XML::Simple
可以做出明智的猜测。
举一个更详尽的例子,您将参观一家宠物店。 在该部分中,您将学习如何以最少的精力将XML文件读入Perl分层数据结构中,该结构是匿名数组和哈希的混合体。 本文说明了Perl如何简洁地转换和重组原始XML文档中包含的信息,然后说明如何以各种形式将其写回。
最后,我讨论了XML::Simple
一些局限性。 这导致了本系列下两篇文章的主题:更高级的解析,使用复杂的工具将XML从一种形式转换为另一种形式以及将XML从DOM和其他内存形式序列化的技术。
本文主要针对很少接触Perl的Perl程序员,但对于有兴趣探索更多编程方法来处理XML文档的XML专家也将很有用。
入门
在开始之前,您需要安装Perl。 如果尚未安装,请参阅参考资料中的链接。
接下来,您将需要XML::Simple
。 如果使用的是UNIX或Linux,最方便的方法是使用cpan
从CPAN中获取这些内容。 您可以使用清单1所示的命令在计算机上安装cpan来开始此过程。通常,您将需要以root用户身份执行此操作,以使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的前提条件策略设置为“跟随”,这是自动的。 这些模块通常在安装过程中进行编译,并生成消息页面。 这可能需要一些时间,不应将其视为引起关注的原因。
另一个前提
XML::Simple
将XML文档转换为对哈希和哈希数组的引用。 这意味着您需要对Perl中的引用,哈希和数组的交互有深入的了解。 如果你在这个方向上需要帮助,请在优秀的Perl参考教程相关主题 。
XML::Simple
基本上,格randint·麦克莱恩(Grant McLean)的XML::Simple
有两个功能; 它将XML文本文档转换为Perl数据结构,匿名哈希和数组的混合,并将此类数据结构转换回XML文本文档。
这种有限的功能非常有用,将在两个级别上进行演示。 首先,您将了解如何以XML格式从配置文件导入数据。 然后,在一个更精致的示例中,在本地的宠物店中,您将学习如何将大而复杂的XML文件读入内存,如何以传统XML工具(如XSLT)难以实现的方式对其进行转换,以及将其写回到磁盘中。
对于许多人来说, XML::Simple
将提供在Perl中处理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::Simple
通常具有合理的默认值,因此例如,如果您未指定输入文件名,则名为part1.pl的Perl程序(如清单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->new->{user}
。
转储明确表明,在处理XML::Simple
必须格外小心。
- 首先,它丢弃根元素的名称。
- 其次,它将具有相同名称的元素折叠为对匿名数组的单个引用。 因此,第一本书的标题为
@{$config->{books}->{book}}[0]->{title}
或“ Cannery Row”。 - 第三,它相同地对待属性和子元素。
您可以通过XMLin()
选项来更改每种行为。 请参阅相关信息和下文中的选项的详细信息的讨论。
一个更复杂的例子:宠物店
XML::Simple
不仅可以经济地解析配置文件,而且还具有很多优点。 实际上,它可以处理大型和复杂的XML文件,并将它们转换为通常非常适合转换的常规数据结构,这在Perl中非常简单,但是使用更常规的XML转换工具(例如XSLT)则很难或不可能。
假设您正在一家宠物店工作,该店将有关宠物的信息保存在XML文件中。 该文档的一小部分如下清单6所示。经理希望进行一些更改:
- 为了节省空间,请将所有子元素更改为属性
- 将价格提高20%
- 让所有价格看起来都一样,因此所有价格都将显示两位小数
- 排序清单
- 用年龄代替出生日期
凭借对Perl的全新信心,并意识到XSLT在计算方面面临挑战-您是否曾经尝试过使用XPath进行转变? -您决定使用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>
<name>Maggie</name>
<dob>12 October 2005</dob>
<price>75</price>
<owner>Rosie</owner>
</dog>
<cat>
<name>Little</name>
<dob>23 June 2006</dob>
<price>25</price>
</cat>
</pets>
初步探索
首次尝试使用XML::Simple
,如清单7所示。
清单7.您勇敢的新Perl
#!/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
将“欧洲”格式的日期(如2006年1月13日)转换为软件包用作标准的3元素数组(YMD)。 给定两个这样的日期, Delta_YMD($earlier, $later)
会以相同的格式产生差异(在您的情况下为年龄)。 不幸的是, Delta_YMD
有点越野车:有时一天或一月都是负数! 但是,稍作谷歌搜索就找到了补丁,一切又恢复了。 完整解决方案中的deltaYMD
(如清单16所示)展示了如何处理此问题。
派遣猫和狗
为了使代码更易于扩展,请使用调度表,如清单14所示。Jason Dominus的绝妙著作“ Higher Order Perl”中对调度表进行了详细讨论(请参阅参考资料中的链接)。
清单14.调度表
my $DISPATCHER = {
'cat' => sub { foldType(shift); },
'dog' => sub { foldType(shift); },
'hippo' => \&hippoFunc,
};
调度程序可以包含用于处理特定元素的实际代码作为匿名子例程,也可以包含对在其他地方定义的命名子例程的引用。 您可以使用其他语言使用switch-case的构造。
在工作示例中,只有两种元素类型,即cat和dog。 在实际的XML文档中,很可能会有许多不同级别的文档。 比起Perl替代方案, if ... elsif ... elsif
构造的一行一行使用一个或多个调度表更加清晰和可维护得多。
将XML写入磁盘
XML::Simple
的输出默认值通常是明智的。 如果您不提供XMLout()
选项,它将生成一个字符串。 如果要写入文件,请添加OutputFile
选项。 如果您不告诉它其他方式,它将使用<opt>
作为根元素。 如果内存中的数据结构具有根元素的名称,请添加一个KeepRoot
选项,将其设置为true,或者在Perl中称为1。清单15为您完成了所有这些工作。
清单15.输出到XML文件
$simple->XMLout($data,
KeepRoot => 1,
OutputFile => 'pets.fixed.xml',
XMLDecl => "<?xml version='1.0'?>",
);
完整的解决方案
清单16中的112行代码完成了老板要求的工作。 XML::Simple
的经济性令人印象深刻。 八行代码读写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读入内存时,它会扩展十倍。 含义是,如果工作站上有几百兆的可用内存, XML::Simple
应该能够处理最大几十兆的XML文件。
摘要
XML已在计算世界中变得越来越普遍,并越来越深入地嵌入到现代应用程序和操作系统中。 对于Perl程序员来说,必须对如何使用它有很好的了解。 诸如XML::Simple
类的工具可以轻松地将XML文档转换为易于理解的Perl数据结构,并将这些数据结构转换回XML。 每个动作通常都是一行代码。
另一方面,XML专家会对Perl在转换和响应XML内容方面的有用性感到惊讶。
第2部分将向您展示如何为Perl开发人员利用XML解析的两个主要流派:树解析和事件驱动解析。
翻译自: https://www.ibm.com/developerworks/web/library/x-xmlperl1/index.html
perl xml