 | 级别: 中级 Jim Dixon (jddixon@gmail.com), 自由作家, Freelance
2007 年 4 月 29 日 本系列文章共包括三部分,这是最后一部分,使用 第 2 部分 介绍的解析技术创建能够转换、导航和写入的树结构。您将看到如何把转换后的解析树提供给 SAX 管道,进一步转换之后再写回文本或者 SQL 数据库。最后介绍了如何进行反向处理,即使用数据库内容驱动 SAX 管道。
简介
我们首先看看 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 事件流,用另一个管道进行转换并创建最终的统一文档。
参考资料 学习
获得产品和技术
讨论
关于作者  | |  | Jim Dixon 是一位独立承包商,他最近回到旧金山,在那里推广用 Perl 和 Ruby 实现 Web 2.0。以前,他在一家英美互联网服务提供商担任技术主管有 7 年时间,开发了许多 Java/Java EE 软件。 |
对本文的评价
|  |