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

developerWorks 中国  >  XML | Web development  >

面向 Perl 开发人员的 XML,第 3 部分: 高级操作和写入技术

使用 XSLT、SAX 和 SQL 控制文档转换

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 中级

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

2007 年 4 月 29 日

本系列文章共包括三部分,这是最后一部分,使用 第 2 部分 介绍的解析技术创建能够转换、导航和写入的树结构。您将看到如何把转换后的解析树提供给 SAX 管道,进一步转换之后再写回文本或者 SQL 数据库。最后介绍了如何进行反向处理,即使用数据库内容驱动 SAX 管道。

简介

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

我们首先看看 Rosie 宠物商店的商品目录。业务发生了一项大变化:该宠物商店和 Lizzie 的商店合并了。调查发现两家的商品目录采用了不同的 XML 格式。如前所述,Rosie 采用的格式如 清单 1 所示。而 Lizzie 采用了不同的方案,如 清单 9 所示。

两者有很大不同。

  • 总的来说,Rosie 用子元素表示而 Lizzie 则使用属性表示
  • Lizzie 的库存由 <item> 子元素组成,Rosie 文档中的对应节点则根据宠物类型命名:<dog><cat> 等等。
  • Rosie 对所有的商品都命名,而 Lizzie 没有
  • Rosie 保存了出售价格,Lizzie 则保留了进价
  • Lizzie 的目录中包含宠物的年龄,Rosie 则列出了出生日期('dob')
  • Lizzie 给出了每个商品的位置,而 Rosie 没有
  • Rosie 提供了狗但不包括其他宠物的所有者,而 Lizzie 根本没有所有者

需要将 Lizzie 记录的年龄和 Rosie 的欧洲格式的出生日期转化成统一的格式,但是并不那么简单。首先要把年龄转化成当前格式的日期,然后需要用到 Perl,将日期转化成标准的 YYYY-MM-DD 格式。

经过研究之后决定建立一个 SQL 表规范用数据库管理系统(DBMS)存储数据。创建数据库表的 SQL 语句如 清单 6 所示。一致的意见是,映射到 Lizzie 的 XML 数据表示最方便,列映射到属性。于是决定将两种不同的 XML 文件转化成统一的格式,通过存储到 SQL 数据库将其合并,然后从数据库生成统一的库存文件 XML 文档。





回页首


入门

和本系列的前两期文章一样,运行本文中的代码需要一些新的 Perl 模块。UNIX® 或 Linux™ 用户可以从 cpan 库获得这些模块,Windows® 用户则一般使用 ppm(请参阅参考资料 中的链接)。需要的模块包括:

  • DBI —— 关系数据库接口
  • XML::LibXSLT —— 用于样式表处理




回页首


样式表和 DOM

我们决定使用 XSLT 样式表(rosie.xsl,如 清单 2 所示)将 Rosie 的库存目录转化成公共格式。需要同时解析 XML 文档(清单 1 中的目录)和样式表,然后使用 XSLT 生成新的文档。


清单 1. Rosie 的库存目录
                
<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>

样式表将每个子元素转化成 <item> 元素,创建新的属性类型,使用原来作为元素名的标记作为属性值。下一层的子元素则转化成属性,内容变成属性值(如清单 2 所示)。


清单 2. rosie.xsl 样式表
                
<xsl:stylesheet version="1.0">
 <xsl:template match="/pets">
 <xsl:element name="stock">
 <xsl:for-each select="*">
 <xsl:element name="item">
 <xsl:attribute name="type">
 <xsl:value-of select="name()"/>
 </xsl:attribute>
 <xsl:for-each select="*">
 <xsl:attribute name="{name()}">
 <xsl:apply-templates/>
 </xsl:attribute>
 </xsl:for-each> 
 <xsl:attribute name="cost">0.00</xsl:attribute>
 <xsl:attribute name="location">Rosies</xsl:attribute>
 </xsl:element>
 </xsl:for-each>
 </xsl:element><
 </xsl:template>
</xsl:stylesheet>

还需要使用样式表添加成本和位置属性,因为这样更方便,只需要一行代码,如果后面在 SAX 处理程序中完成的话可能需要更多工作。

因为需要进行多次这种转换(这里以及下面一节),可以将通用的代码放在 Perl 模块 RosieStock.pm 中(如清单 3 所示)。


清单 3. RosieStock.pm (部分)
                
package RosieStock;
use strict;
use XML::LibXML;
use XML::LibXSLT;
use XML::LibXML::SAX::Generator;

sub new { 
 my $class = shift;
 my $parser = XML::LibXML->new;
 my $xslt = XML::LibXSLT->new;
 my $style = $parser->parse_file('rosie.xsl');
 my $self = { 
 PARSER => $parser,
 STYLESHEET => $xslt->parse_stylesheet($style),
 };
 bless $self, $class;
}
sub parseAndStyle {
 my ($self, $inFile) = @_;
 $self->{DOC} = $self->{PARSER}->parse_file($inFile);
 $self->{STYLED} = $self->{STYLESHEET}->transform($self->{DOC});
 return $self;
}
sub toString {
 my ($self) = shift;
 $self->{STYLESHEET}->output_string( $self->{STYLED} );
}

完成转换只需要清单 4 所示的少量 Perl 代码。这段代码解析库存文件和样式表,应用样式表并显示转换结果(清单 5),以便看到进展情况。


清单 4. part3a.pl
                
#!/usr/bin/perl -w
use strict;
use DBI;
use RosieStock; 

my $fixRosie = RosieStock->new;
print $fixRosie->parseAndStyle('pets.xml')->toString;

可以看到,初始文档中的子元素现在都变成了属性。库存目录中的实际内容没有变,只不过增加了成本和位置属性(如清单 5 所示)。


清单 5. 转换后的 Rosie 库存目录(part3a.out)
                
<?xml version="1.0"?>
<stock>
 <item type="cat" name="Madness" dob="1 February 2004" price="150" 
 cost="0.00" location="Rosies"/>
 <item type="dog" name="Maggie" dob="12 October 2002" price="75" 
 owner="Rosie" cost="0.00" location="Rosies"/>
 <item type="cat" name="Little" dob="23 June 2006" price="25" 
 cost="0.00" location="Rosies"/>
</stock>





回页首


SAX 管道

第 2 部分(请参阅 参考资料)中提到,虽然树解析(因而包括 XSLT 转换)受到 XML 文档必须能装入内存和/或固定长度的限制,但 SAX 能够处理任意大小和长度的 XML 流。而且还能利用两者的优点:解析、导航和转换 XML 文档树,然后随意访问这些树并生成供给 SAX 管道的事件。因为 Rosie 的库存文档现在已经转化成了树形式,马上就可以实现上述操作。我们将获得解析并转换后的树,在存入数据库之前将其提供给 SAX 处理程序。





回页首


遍历 DOM 树

建立的第一个 SAX 管道包括两部分:遍历 DOM 树并生成 SAX 事件的生成器,捕捉这些事件并将其写入数据库的处理程序。生成器是 RosieStock 包中新增加的一个方法,如清单 6 所示。处理程序则向预先存在的数据库中的 PETS.STOCK 表中写入行,如 清单 7 所示。创建和运行 SAX 管道的 Perl 程序如 清单 8 所示。


清单 6. RosieStock.pm(其他部分)
                
sub generate {
 my ($self, $handler) = @_;
 my $generator 
 = XML::LibXML::SAX::Generator->new(Handler=>$handler);
 $generator->generate( $self->{STYLED} );
}
1;

RosieStock.pm 中增加的是 XML::LibXML::SAX::Generator,它遍历 DOM 树,即模块中已经创建的规范化的 XML 文档,并生成 SAX 事件流。事件流传递给处理程序。


清单 7. Stock2DB.pm
                
package Stock2DB;
use XML::SAX::Base;
@ISA = ('XML::SAX::Base');
use strict;
use DBI;
use constant TYPE0 => 0;
use constant TYPE1 => 1;
use constant TYPE2 => 2;

sub new {
 my $class = shift;
 my $self = { COUNTER => 0, HASHTYPE => TYPE0 };
 return bless {}, $class;
}

my @months = ( 'January', 'February', 'March', 'April', 'May', 'June',
 'July', 'August', 'September', 'October', 'November', 
 'December');
sub _dobFromAge($) { # precision is not an issue!
 my $age = shift;
 my $tob = time() - $age * ((86400) * 365.25);
 my ($day, $month, $year) = (localtime($tob))[ 3, 4, 5 ];
 $year += 1900;
 return "$day $months[$month] $year";
}
# see Perl SAX 2.1 spec for details regarding $data (hash ref)

sub _getVal($$) {
 my ($hash, $name) = @_;
 return (defined $hash->{$name}) ? ''.$hash->{$name} : '';
}
# expect attribute names to appear in hash
sub _getAttrsTYPE1($) {
 my $attrs = shift;
 return (
 _getVal($attrs, 'type'), _getVal($attrs, 'name'), 
 _getVal($attrs, 'cost'), _getVal($attrs, 'price'), 
 _getVal($attrs, 'dob'), _getVal($attrs, 'location'),
 _getVal($attrs, 'age'),
 ); 
}
# expect prefixed attribute names to key secondary hash
sub _getFromSecondary ($$) {
 my ($hash, $name) = @_;
 my $value;
 my $ref = $hash->{ "{}$name" };
 if (defined $ref) {
 $value = $ref->{Value};
 }
 return $value;
}
sub _getAttrsTYPE2($) {
 my $attrs = shift;
 my ($type, $name, $cost, $price, $dob, $location, $age) = (
 _getFromSecondary ($attrs, 'type'), 
 _getFromSecondary ($attrs, 'name'), 
 _getFromSecondary ($attrs, 'cost'), 
 _getFromSecondary ($attrs, 'price'), 
 _getFromSecondary ($attrs, 'dob'), 
 _getFromSecondary ($attrs, 'location'), 
 _getFromSecondary ($attrs, 'age'), 
 ); 
 return ($type, $name, $cost, $price, $dob, $location, $age);
}
sub start_element {
 my ($self, $data) = @_;
 my $elmName = $data->{Name};
 if ($elmName eq 'item') {
 my $attrs = $data->{Attributes};
 my $hashType = $self->{HASHTYPE};
 $hashType = TYPE0 if !defined($hashType);
 if ($hashType == TYPE0) {
 my @keyList = keys %$attrs;
 my $attr0 = $keyList[0];
 if ($attr0 =~ /^{}/) {
 $hashType = TYPE2;
 } else {
 $hashType = TYPE1;
 }
 $self->{HASHTYPE} = $hashType;
 $self->{COUNTER} = 0;
 }
 my ($type, $name, $cost, $price, $dob, $location, $age);
 if ($hashType == TYPE1) {
 ($type, $name, $cost, $price, $dob, $location, $age) = 
 _getAttrsTYPE1( $attrs );
 } else {
 ($type, $name, $cost, $price, $dob, $location, $age) = 
 _getAttrsTYPE2( $attrs );
 }
 # do any necessary fixups
 if (!defined($name) or $name eq '') {
 $name = "$type-" . $self->{COUNTER}++;
 }
 if (!defined($cost) or $cost eq '' or ($cost eq '0.00') ) { 
 $cost = 0.6 * $price;
 }
 if (!defined($price) or $price eq '') {
 $price = 1.667 * $cost;
 } 
 if (!defined($dob) or $dob eq '') {
 $dob = _dobFromAge($age);
 }

 my $insert = 
 'INSERT INTO STOCK (type, name, cost, price, dob, location) '
 . "VALUES( \"$type\", \"$name\", \"$cost\", "
 . "\"$price\", \"$dob\", \"$location\" )";
 
 $self->{DBH}->do ( $insert );
 }
 $self->SUPER::start_element($data);
}
sub start_document {
 my ($self, $data) = @_;
 $self->{DBH} = DBI->connect('DBI:mysql:PETS', 'genny', 'joshsgirl')
 or die "couldn't connect to PETS: " . DBI->errstr;
 $self->SUPER::start_document($data);
}
sub end_document {
 my ($self, $data) = @_;
 $self->SUPER::end_document($data);
 $self->{DBH}->disconnect;
}
1;

处理程序是 Stock2DB 的一个实例,我们的第一个 SAX 处理程序。它扩展了 XML::SAX::Base,并依赖这个包处理它不能处理(即忽略)的任何事件。

这里需要处理的事件只有文档的开始和结束以及元素的开始事件。start_document 事件中要打开将写入数据的数据库。end_document 事件中则关闭数据库。元素开始的时候,需要查看其名称。所关心的只有 <item> 元素。遇到该元素后需要从作为参数传递的散列表中提取属性,并使用它们建立传递给 DBI 执行的 SQL INSERT 语句。

Perl SAX 绑定中的一些变化带来了困难。为了处理属性值的两个存储变量,代码需要检测类型(这里称为 TYPE1 或 TYPE2)然后提取适当的属性值。这里的属性按名称存储为 $data->{Attributes}

主要是因为 XML::LibXML::SAX::Generator 包太过陈旧。以后如果使用最新的 XML::SAX::ExpatXS,需要使用存储属性的第二种方式,这种方式中,将使用属性名(如 type)构造指向另外一个散列表的键 {}type ,其中包含属性的其他信息(值、前缀等)。

这些处理都放在 part3b.pl 中,如清单 8 所示。脚本首先创建一个干净的数据库表。然后解析和转换目录文件 pets.xml。接下来遍历创建的 DOM 树。SAX 管道的后端是一个处理程序,可以作为 Stock2DB 的实例创建。因而可以实例化管道的前端部分,即遍历 DOM 树的生成器。调用它的时候就会把树的内容写入刚刚创建的数据库表中。


清单 8. part3b.pl
                
#!/usr/bin/perl -w
use strict;
use DBI;
use RosieStock; 
use Stock2DB;

# create a clean database table 
my $dbh = DBI->connect('DBI:mysql:PETS', 'genny', 'joshsgirl')
 or die "couldn't connect to PETS: " . DBI->errstr;

$dbh->do('USE PETS');
$dbh->do('DROP TABLE IF EXISTS PETS.STOCK');
my $create = 'CREATE TABLE STOCK ( '
 . 'id INT NOT NULL AUTO_INCREMENT, '
 . 'type VARCHAR(64) NOT NULL, '
 . 'name VARCHAR(64), '
 . 'cost DECIMAL(4,2), '
 . 'price DECIMAL(4,2) NOT NULL, '
 . 'dob VARCHAR(32), ' # should be DATE, YYYY-MM-DD
 . 'location VARCHAR(64), ' 
 . 'PRIMARY KEY (id) ) ';

$dbh->do($create) 
 or die "can't create table PETS.STOCK: " . $dbh->errstr;
$dbh->disconnect;

my $fixRosie = RosieStock->new;
$fixRosie->parseAndStyle('pets.xml');
my $dbWriter = Stock2DB->new;
$fixRosie->generate( $dbWriter );

那么到现在得到什么结果了?数据库中只包含来自 Rosie 最初的库存目录中转化成标准格式的项。

仔细观察 清单 7 中的代码,就会发现 Stock2DB 也寻找缺少的或者为零的成本并根据价格估算出来,同样,还通过某个固定标记来根据成本计算缺少的价格。





回页首


将 XML 写入数据库

下一步是用 Lizzie 的目录格式加载数据库,如清单 9 所示。


清单 9. Lizzie 宠物商店的库存记录
                
<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>

有了 Stock2DB.pm(清单 7)提供的功能之后,将 Lizzie 的库存目录加载到数据库中只需要实例化一个 Stock2DB 的实例,它会把传递来的任何内容写入数据库,然后启动解析器就行了(如清单 10 所示)。


清单 10. part3c.pl
                
#!/usr/bin/perl -w
#use strict;
use XML::SAX::ParserFactory;
use Stock2DB;
$XML::SAX::ParserPackage = "XML::SAX::ExpatXS";

my $parser = XML::SAX::ParserFactory->parser(Handler => Stock2DB->new);
eval { $parser->parse_file('pets2.xml') };
die "can't parse Lizzie's stock file: $@" if $@;

现在两个库存文件都加载到了数据库中。查询数据库将看到清单 11 所示的结果。请注意,所有的宠物都有了名称,缺少的成本和价格也加上了,年龄变成了适当的出生日期,所有的项目都有一个位置。


清单 11. 合并的数据库
                
mysql > select * from PETS.STOCK;
+----+--------+----------+--------+--------+------------------+-----------+
| id | type | name | cost | price | dob | location |
+----+--------+----------+--------+--------+------------------+-----------+
| 1 | cat | Madness | 90.00 | 150.00 | 1 February 2004 | Rosies |
| 2 | dog | Maggie | 45.00 | 75.00 | 12 October 2002 | Rosies |
| 3 | cat | Little | 15.00 | 25.00 | 23 June 2006 | Rosies |
| 4 | iguana | iguana-0 | 124.42 | 207.40 | 16 November 2005 | stockroom |
| 5 | pig | pig-1 | 15.00 | 25.00 | 18 May 2006 | floor |
| 6 | parrot | parrot-2 | 700.00 | 999.99 | 16 November 2000 | cage |
| 7 | pig | pig-3 | 117.50 | 195.87 | 5 September 2003 | floor |
+----+--------+----------+--------+--------+------------------+-----------+
7 rows in set (0.00 sec)
mysql>





回页首


数据库驱动 SAX 管道

最后一项任务是将刚刚创建的数据库转存为 XML 格式。现在的日期形式不太适合 SQL 数据库,使用 Date::Calc 将其转化成 MySQL 所喜欢的 YYYY-MM-DD 形式,这项工作就留给读者来完成了。

现在要使用 XML::Generator::DBI 驱动 SAX 管道。请注意,必须为 XML 文档中的根元素和子元素提供名称,因为这些没有存储在 SQL 表中。此外,默认情况下 XML::Generator::DBI 将列值输出为子元素。为了在 XML 文档中将其变为属性,需要将 AsAttributes 选项设置为 1。清单 12 完成了这项任务,生成的 XML 结果如 清单 13 所示。


清单 12. part3d.pl
                
#!/usr/bin/perl -w
use strict;
use DBI;
use XML::Generator::DBI;
use XML::SAX::Writer;

my $dbh = DBI->connect('DBI:mysql:PETS', 'genny', 'joshsgirl')
 or die "couldn't connect to PETS: " . DBI->errstr;
my $select = qq ( SELECT * FROM STOCK );
my $writer = XML::SAX::Writer->new(Output => 'inventory.xml');
my $generator = XML::Generator::DBI->new(
 dbh => $dbh,
 Handler => $writer, 
 
 );
$generator->execute($select, undef,
 AsAttributes => 1,
 QueryElement => undef,
 RootElement => 'stock',
 RowElement => 'item',
);
$dbh->disconnect;

为了便于阅读对清单 13 中的结果作了调整。生成器弄乱了属性的顺序。如果顺序很重要 —— 尽管这里无所谓,需要进一步的处理来解决这个问题。


清单 13. part3d 输出:inventory.xml
                
<stock>
 <item cost="90.00" dob="1 February 2004" location="Rosies" 
 name="Madness" price="150.00" id="1" type="cat"/>
 <item cost="45.00" dob="12 October 2002" location="Rosies" 
 name="Maggie" price="75.00" id="2" type="dog"/>
 <item cost="15.00" dob="23 June 2006" location="Rosies" 
 name="Little" price="25.00" id="3" type="cat"/>
 <item cost="124.42" dob="16 November 2005" location="stockroom" 
 name="iguana-0" price="207.40" id="4" type="iguana"/>
 <item cost="15.00" dob="18 May 2006" location="floor" 
 name="pig-1" price="25.00" id="5" type="pig"/>
 <item cost="700.00" dob="16 November 2000" location="cage" 
 name="parrot-2" price="999.99" id="6" type="parrot"/>
 <item cost="117.50" dob="5 September 2003" location="floor" 
 name="pig-3" price="195.87" id="7" type="pig"/>
</stock>

虽然设置 XML::Generator::DBI 非常简单,运行 清单 12 中的脚本还是出现了一些讨厌的警告信息。通过稍微修改模块可以解决,这些不足将在下一个版本中得到修复(现在是 1.0 版)。这期间您也会得到作者提供的 diff 补丁文件。





回页首


结束语

本系列文章 从一开始就指出,只要一个步骤就能实现多数 XML 文档和易于处理的 Perl 数据结构之间的来回转换(使用 XML::Simple)。

第 2 部分 介绍了更强大的 XML 解析工具:DOM 风格的树解析器和基于事件的 SAX 解析器。您学习了 XML::SAX::Base 以及如何使用它创建源文件、处理程序和 SAX 事件。

最后,第 3 部分将这些内容结合起来,并增加了关系数据库的内容。我们建立了解析树、对其进行转换、遍历转换后的树、生成 SAX 事件并提供给数据库。然后使用 SAX 管道解析第二个 XML 文档、转换并添加到同一个数据库中,从而完成了两个库存目录的合并。最后又使用保存合并数据的数据库生成另一个 SAX 事件流,用另一个管道进行转换并创建最终的统一文档。



参考资料

学习

获得产品和技术
  • Document Object Model 规范:详细了解这种平台和语言中立的接口,通过使用它,程序和脚本可以动态地访问和更新文档的内容、结构和样式。

  • Perl SAX 2.1 binding:这篇文章描述了 Perl 模块使用的 SAX 版本。

  • Perl:下载使用最新的 Perl 版本。

  • 庞大的 CPAN Perl 库(Google 搜索到 1800 万个页面!):Comprehensive Perl Archive Network 提供了本文所述全部模块的链接。

  • PPM, Perl Package Manager for Windows:下载允许安装、删除、更新以及使用 ActivePerl 管理常见 Perl CPAN 模块(如 Tk 和 DBI)所使用的工具。

  • Grant McLean 的 XML::Simple :XML::Simple 模块用于底层 XML 解析模块之上的一个简单的 API 层。

  • XML 规范:详细描述了可扩展标记语言(XML)。

  • XML 入门(Doug Tidwell,developerWorks,2002 年 11 月):这篇教程更好地介绍了 XML,包括 XML 的由来、它如何塑造了电子商务的未来、各种 XML 编程接口和标准,以及说明企业如何利用 XML 解决业务问题的两个案例。

  • XPath 1.0:了解这种用于导航 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 软件。




对本文的评价










回页首


UNIX 是 The Open Group 在美国和其他国家的商标。 Linux 是 Linus Torvalds 在美国和/或其他国家的商标。 Microsoft、Windows 和 Windows 徽标是 Microsoft Corporation 在美国和/或其他国家的商标。 其他公司、产品或服务名称可能是其他公司的商标或者服务标志。 其他公司、产品或服务的名称可能是其他公司的商标或服务标志。

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