IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  XML | Web development  >

面向 Perl 开发人员的 XML,第 1 部分: XML 加 Perl —— 简单的魔术

使用 XML::Simple 将 XML 集成到 Perl 应用程序中

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Jim Dixon (jddixon@gmail.com), 自由作家, Freelance

2007 年 4 月 06 日

本系列为需要 XML 和 Perl 快速解决方案的开发人员提供了一份指南。在许多情况下,只需要 XML::Simple 这一种工具就能够将 XML 集成到 Perl 应用程序中。第 1 部分讲解在哪里获得 XML::Simple、如何使用它以及接下来要做什么。对在 Perl 中使用 XML 有所体会之后,本系列中的另外两篇文章将帮助您提高技能。

简介

本文是关于 Perl 和 XML 的分三部分的系列文章的第一篇,主要关注 XML::Simple。对于 Perl 程序员,第一次使用 XML 往往是从配置文件接收参数。本文要讲解如何用两行代码读取这样的参数,第一行告诉 Perl 要使用 XML::Simple,第二行将一个变量设置为文件中的一个值。甚至不必提供配置文件的名称:XML::Simple 会进行智能化的猜测。

请访问 面向 Perl 和 PHP 开发人员的 XML
您可以通过该专题来了解更多与 Perl 和 PHP 相关的 XML 技术。

作为一个更复杂的示例,我们要研究一个宠物商店应用程序。在那一节中,将学习如何简便地将 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=>1KeepRoot=>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 解析:树解析和事件驱动的解析。



参考资料

学习

获得产品和技术
  • Perl:获得最新版本并投入使用。

  • 巨大的 CPAN Perl 库(Google 上显示有 1800 万次点击!):访问 Comprehensive Perl Archive Network,这里有关于 Perl 的所有资料。

  • PPM, Perl Package Manager for Windows:可以使用这个工具安装、删除、升级和管理常用的 Perl CPAN 模块(比如 Tk 和 DBI)和 ActivePerl。

  • Grant McLean 的 XML::Simple:尝试将 XML::Simple 模块用作底层 XML 解析模块之上的简单 API 层。

  • XML 规范:这是对 Extensible Markup Language(XML)的完整描述。

  • Document Object Model(DOM)规范:DOM 是一种独立于平台和语言的接口,它使程序和脚本能够动态地访问和更新文档的内容、结构和样式,请通过 DOM 规范了解它的细节。

  • XML 入门(Doug Tidwell,developerWorks,2002 年 8 月):这个教程是 XML 简介,介绍了 XML 是如何开发的、它如何影响电子商务的未来、各种 XML 编程接口和标准以及两个案例研究,演示了公司如何用 XML 解决业务问题。

  • XPath 1.0:XPath 是一种用来在 DOM 树中移动的语言。

  • XSLT 1.0 规范:学习如何对 XML 文档进行转换。

  • Dare to script tree-based XML with Perl: Find out how to work with tree-based document models(Parand Darugar,developerWorks,2000 年 7 月):这篇文章介绍了如何用 Perl 进行基于树的 XML 解析。

  • IBM 试用版软件:用这些试用版软件构建您的下一个开发项目,这些软件可以从 developerWorks 直接下载获得。


讨论


关于作者

Jim Dixon 是一位独立承包商,他最近回到旧金山,在那里推广用 Perl 和 Ruby 实现 Web 2.0。以前,他在一家英美互联网服务提供商担任技术主管有 7 年时间,开发了许多 Java/Java EE 软件。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款