XML::Twig 在 Perl 中的性能调整和优化
Performance tuning and optimization of XML::Twig in Perl
以下是我为了根据四个条件过滤我的 3 到 5 GB XML 文件而草草写下的代码:
条件如下:
1) 筛选所有子股
2) 有一定来源的股票应该持续存在。否则都应该被过滤。
3) 在股票交易中,有一个 <event>
标签,随后有一个 <subevent>
标签。对于 <event>
标签属性 'code' 应该有值 'abc' 而 <subevent>
标签应该有特定的属性值,如代码所示。
4) 只应保留给定 ref(股票的另一个属性)的最高版本(股票的属性)。其余的应该全部删除(这个是最复杂的条件)
我的代码是:
use strict;
use warnings;
use XML::Twig;
open( my $out, '>:utf8', 'out.xml') or die "cannot create output file out.xml: $!";
my $twig = new XML::Twig(
twig_roots => { '/STOCKEXT/STOCK/STOCK'=> sub { $_->delete() },
'/STOCKEXT/STOCK[@origin != "ASIA"]' => sub { $_->delete; },
'/STOCKEXT/STOCK' => \&trade_handler
},
att_accessors => [ qw/ ref version / ],
pretty_print => 'indented',
);
my %max_version;
$twig->parsefile('1513.xml');
for my $stock ($twig->root->children('STOCK'))
{
my ($ref, $version) = ($trade->ref, $trade->version);
if ($version eq $max_version{$ref} &&
grep {grep {$_->att('code') eq 'abc' and $_->att('narrative') eq 'def'}
$_->children('subevent')} $trade->children('event[@eventtype="ghi"]'))
{
$trade->flush($out);
}
else
{
$trade->purge;
}
}
sub trade_handler
{
my ($twig, $trade) = @_;
{
my ($ref, $version) = ($trade->ref, $trade->version);
unless (exists $max_version{$ref} and $max_version{$ref} >= $version)
{
$max_version{$ref} = $version;
}
}
1;
}
样本XML
<STOCKEXT>
<STOCK origin = "ASIA" ref="12" version="1" >(Filtered out, lower version ref)
<event eventtype="ghi">
<subevent code = "abc" narattive = "def" />
</event>
</STOCK>
<STOCK origin = "ASIA" ref="12" version="2" >(highest version=2 for ref=12)
<event eventtype="ghi">
<subevent code = "abc" narattive = "def" />
</event>
</STOCK>
<STOCK origin = "ASI" ref="13" version="1" >(Fileterd out "ASI" val wrong)
<event eventtype="ghi">
<subevent code = "abc" narattive = "def" />
</event>
</STOCK>
代码运行良好并提供必要的输出。但是它消耗了大量内存,即使我已经尝试实现 "FLUSH" & "PURGE"。任何人都可以帮助提供一些优化技巧。
如果您担心内存占用,您确实希望在 twig 处理程序中使用 flush/purge。
这样它在文件被解析时被调用。
你的清除调用是在你的解析文件之后进行的,这意味着 - 它必须首先加载和解析整个文件。
您也许可以将其中的一部分构建到您的 trade_handler
- 例如您测试最大版本,然后稍后在迭代循环中进行比较。因此,您可以可能 在处理程序中测试该条件:
if ( $max_version{$ref} > $version ) { $trade -> purge; }
但请记住,如果您这样做,您需要重新考虑您的 post-parse foreach
循环,因为您会在进行时丢弃。
虽然我不完全确定你的 grep
是干什么用的。您也可以在 trade_handler 中实现该逻辑,但我不能肯定地说。 (例如,负面测试,如果不需要此元素则清除)。
我认为 - 从根本上讲 - 你应该能够为你的 'for' 循环使用一个处理程序,并在你进行时处理和清除。我不能肯定地说——你可能需要一个两遍的方法,因为需要提前查看版本号。
编辑:这并不能完全满足您的要求,但希望能说明我的建议:
#!/usr/bin/perl
use strict;
use warnings;
use XML::Twig;
open( my $out, '>:utf8', 'out.xml' )
or die "cannot create output file out.xml: $!";
my $twig = new XML::Twig(
twig_roots => {
'/STOCKEXT/STOCK/STOCK' => sub { $_->delete() },
'/STOCKEXT/STOCK[@origin != "ASIA"]' => sub { $_->delete; },
'/STOCKEXT/STOCK' => \&trade_handler
},
att_accessors => [qw/ ref version /],
pretty_print => 'indented',
);
my %max_version;
my %best_version_of;
local $/;
$twig->parse(<DATA>);
foreach my $ref ( keys %best_version_of ) {
$best_version_of{$ref} -> print;
}
#$twig->parsefile('1513.xml');
sub trade_handler {
my ( $twig, $trade ) = @_;
my ( $ref, $version ) = ( $trade->ref, $trade->version );
if ( not exists $max_version{$ref}
or $max_version{$ref} < $version )
{
###something here that replicates your grep test, as I'm not sure I've got it right.
$max_version{$ref} = $version;
$best_version_of{$ref} = $trade;
}
$trade -> purge;
}
__DATA__
<STOCKEXT>
<STOCK origin = "ASIA" ref="12" version="1" >
<event eventtype="ghi">
<subevent code = "abc" narattive = "def" />
</event>
</STOCK>
<STOCK origin = "ASIA" ref="12" version="2" >
<event eventtype="ghi">
<subevent code = "abc" narattive = "def" />
</event>
</STOCK>
<STOCK origin = "ASI" ref="13" version="1" >
<event eventtype="ghi">
<subevent code = "abc" narattive = "def" />
</event>
</STOCK>
</STOCKEXT>
如前所述,这并不能完全满足您的需求 - 它会 'save memory' 只要您能够通过清除它来丢弃很多 XML go along...但是因为直到你到达终点你才知道哪个版本是最高的,所以内存占用是不可避免的,因为你永远不会完全知道你可以安全地丢弃什么直到你到达那里。
因此,也许您需要一种两次通过的方法,首先解析以提取 'highest version numbers' - 就像您正在做的那样,但是边走边清除...然后在您了解它们后重新开始,因为这样您就知道可以在进行时清除或冲洗什么。
您遇到此问题的原因是,在您到达文件末尾之前,您无法知道您是否拥有最新版本。
所以您可能需要改为执行类似的操作?
#!/usr/bin/perl
use strict;
use warnings;
use XML::Twig;
open( my $out, '>:utf8', 'out.xml' )
or die "cannot create output file out.xml: $!";
my $first_pass = new XML::Twig(
twig_roots => {
'/STOCKEXT/STOCK/STOCK' => sub { $_->delete() },
'/STOCKEXT/STOCK[@origin != "ASIA"]' => sub { $_->delete; },
'/STOCKEXT/STOCK' => \&extract_highest_version,
},
att_accessors => [qw/ ref version /],
pretty_print => 'indented',
);
my $main_parse = new XML::Twig(
twig_roots => {
'/STOCKEXT/STOCK/STOCK' => sub { $_->delete() },
'/STOCKEXT/STOCK[@origin != "ASIA"]' => sub { $_->delete; },
'/STOCKEXT/STOCK' => \&trade_handler
},
att_accessors => [qw/ ref version /],
pretty_print => 'indented',
);
my %max_version_of;
$first_pass->parsefile('1513.xml');
$main_parse->parsefile('1513.xml');
sub extract_highest_version {
my ( $twig, $trade ) = @_;
my ( $ref, $version ) = ( $trade->ref, $trade->version );
if ( not exists $max_version_of{$ref}
or $max_version_of{$ref} < $version )
{
$max_version_of{$ref} = $version;
}
$trade->purge;
}
sub trade_handler {
my ( $twig, $trade ) = @_;
my ( $ref, $version ) = ( $trade->ref, $trade->version );
if ( $version >= $max_version_of{$ref}
and ( $trade->first_child('event')->att('eventtype') eq 'ghi' )
and ( $trade->first_child('event')->first_child('subevent')->att('code') eq 'abc' )
and ( $trade->first_child('event')->first_child('subevent') ->att('narattive') eq 'def' )
)
{
$trade->flush;
}
else {
$trade->purge;
}
}
可能会更简洁一些,但重点是 - 你 运行 完成一次 - 然后 purge
随着你的进行,所以内存中只有一个 <STOCK>
在给定的时间。那你就有了'ref'和'highest version.
的关系
然后你第二次解析,因为你知道最高版本是什么,所以你可以 purge/flush 随着你的前进 - 你不必阅读未来。
现在如评论中所述 - 您的第一种方法将整个文件读入内存,因为它需要知道 'highest version'。虽然它是单次通过,但速度更快。
另一种方法是两次传递 - 读取文件两次。速度较慢,但根本不会在内存中保留太多内容。
中途之家可能是:
#!/usr/bin/perl
use strict;
use warnings;
use XML::Twig;
open( my $out, '>:utf8', 'out.xml' )
or die "cannot create output file out.xml: $!";
my $twig = new XML::Twig(
twig_roots => {
'/STOCKEXT/STOCK/STOCK' => sub { $_->delete() },
'/STOCKEXT/STOCK[@origin != "ASIA"]' => sub { $_->delete; },
'/STOCKEXT/STOCK' => \&trade_handler
},
att_accessors => [qw/ ref version /],
pretty_print => 'indented',
);
my %max_version_of;
my %best_version_of;
$twig->parsefile('1513.xml');
print {$out} "<STOCKEXT>\n";
foreach my $ref ( keys %best_version_of ) {
foreach my $trade ( @{ $best_version_of{$ref} } ) {
$trade->print($out);
}
}
print {$out} "</STOCKEXT>\n";
sub trade_handler {
my ( $twig, $trade ) = @_;
my ( $ref, $version ) = ( $trade->ref, $trade->version );
if (( not defined $max_version_of{$ref}
or $version >= $max_version_of{$ref}
)
and ( $trade->first_child('event')->att('eventtype') eq 'ghi' )
and
( $trade->first_child('event')->first_child('subevent')->att('code')
eq 'abc' )
and ( $trade->first_child('event')->first_child('subevent')
->att('narattive') eq 'def' )
)
{
if ( not defined $max_version_of{$ref}
or $version >= $max_version_of{$ref} )
{
#this version is higher, so anything lower is redundant - remove it
@{ $best_version_of{$ref} } = ();
}
push( @{ $best_version_of{$ref} }, $trade );
$max_version_of{$ref} = $version;
}
$trade->purge;
#can omit this, it'll just print how 'big' the hash is getting.
print "Hash size: ". %best_version_of."\n";
}
它所做的仍然是清除,但会慢慢填满 %best_version_of
。尽管它可能仍然占用大量内存 - 这在很大程度上取决于 'keep' 与 'discard' 的比率。
恐怕没有最佳解决方案 - 要弄清楚哪个版本是 'newest' 你必须 运行 遍历文件两次,要么你以内存消耗为代价这样做,或者你这样做是以磁盘 IO 为代价的。
(而且我想我必须提供一个警告 - 这不会产生 'valid' XML,因为根节点 <STOCKEXT>
将被丢弃。把它放在开头最后的 $out
和 </STOCKEXT>
会解决这个问题)。
以下是我为了根据四个条件过滤我的 3 到 5 GB XML 文件而草草写下的代码:
条件如下:
1) 筛选所有子股
2) 有一定来源的股票应该持续存在。否则都应该被过滤。
3) 在股票交易中,有一个 <event>
标签,随后有一个 <subevent>
标签。对于 <event>
标签属性 'code' 应该有值 'abc' 而 <subevent>
标签应该有特定的属性值,如代码所示。
4) 只应保留给定 ref(股票的另一个属性)的最高版本(股票的属性)。其余的应该全部删除(这个是最复杂的条件)
我的代码是:
use strict;
use warnings;
use XML::Twig;
open( my $out, '>:utf8', 'out.xml') or die "cannot create output file out.xml: $!";
my $twig = new XML::Twig(
twig_roots => { '/STOCKEXT/STOCK/STOCK'=> sub { $_->delete() },
'/STOCKEXT/STOCK[@origin != "ASIA"]' => sub { $_->delete; },
'/STOCKEXT/STOCK' => \&trade_handler
},
att_accessors => [ qw/ ref version / ],
pretty_print => 'indented',
);
my %max_version;
$twig->parsefile('1513.xml');
for my $stock ($twig->root->children('STOCK'))
{
my ($ref, $version) = ($trade->ref, $trade->version);
if ($version eq $max_version{$ref} &&
grep {grep {$_->att('code') eq 'abc' and $_->att('narrative') eq 'def'}
$_->children('subevent')} $trade->children('event[@eventtype="ghi"]'))
{
$trade->flush($out);
}
else
{
$trade->purge;
}
}
sub trade_handler
{
my ($twig, $trade) = @_;
{
my ($ref, $version) = ($trade->ref, $trade->version);
unless (exists $max_version{$ref} and $max_version{$ref} >= $version)
{
$max_version{$ref} = $version;
}
}
1;
}
样本XML
<STOCKEXT>
<STOCK origin = "ASIA" ref="12" version="1" >(Filtered out, lower version ref)
<event eventtype="ghi">
<subevent code = "abc" narattive = "def" />
</event>
</STOCK>
<STOCK origin = "ASIA" ref="12" version="2" >(highest version=2 for ref=12)
<event eventtype="ghi">
<subevent code = "abc" narattive = "def" />
</event>
</STOCK>
<STOCK origin = "ASI" ref="13" version="1" >(Fileterd out "ASI" val wrong)
<event eventtype="ghi">
<subevent code = "abc" narattive = "def" />
</event>
</STOCK>
代码运行良好并提供必要的输出。但是它消耗了大量内存,即使我已经尝试实现 "FLUSH" & "PURGE"。任何人都可以帮助提供一些优化技巧。
如果您担心内存占用,您确实希望在 twig 处理程序中使用 flush/purge。 这样它在文件被解析时被调用。
你的清除调用是在你的解析文件之后进行的,这意味着 - 它必须首先加载和解析整个文件。
您也许可以将其中的一部分构建到您的 trade_handler
- 例如您测试最大版本,然后稍后在迭代循环中进行比较。因此,您可以可能 在处理程序中测试该条件:
if ( $max_version{$ref} > $version ) { $trade -> purge; }
但请记住,如果您这样做,您需要重新考虑您的 post-parse foreach
循环,因为您会在进行时丢弃。
虽然我不完全确定你的 grep
是干什么用的。您也可以在 trade_handler 中实现该逻辑,但我不能肯定地说。 (例如,负面测试,如果不需要此元素则清除)。
我认为 - 从根本上讲 - 你应该能够为你的 'for' 循环使用一个处理程序,并在你进行时处理和清除。我不能肯定地说——你可能需要一个两遍的方法,因为需要提前查看版本号。
编辑:这并不能完全满足您的要求,但希望能说明我的建议:
#!/usr/bin/perl
use strict;
use warnings;
use XML::Twig;
open( my $out, '>:utf8', 'out.xml' )
or die "cannot create output file out.xml: $!";
my $twig = new XML::Twig(
twig_roots => {
'/STOCKEXT/STOCK/STOCK' => sub { $_->delete() },
'/STOCKEXT/STOCK[@origin != "ASIA"]' => sub { $_->delete; },
'/STOCKEXT/STOCK' => \&trade_handler
},
att_accessors => [qw/ ref version /],
pretty_print => 'indented',
);
my %max_version;
my %best_version_of;
local $/;
$twig->parse(<DATA>);
foreach my $ref ( keys %best_version_of ) {
$best_version_of{$ref} -> print;
}
#$twig->parsefile('1513.xml');
sub trade_handler {
my ( $twig, $trade ) = @_;
my ( $ref, $version ) = ( $trade->ref, $trade->version );
if ( not exists $max_version{$ref}
or $max_version{$ref} < $version )
{
###something here that replicates your grep test, as I'm not sure I've got it right.
$max_version{$ref} = $version;
$best_version_of{$ref} = $trade;
}
$trade -> purge;
}
__DATA__
<STOCKEXT>
<STOCK origin = "ASIA" ref="12" version="1" >
<event eventtype="ghi">
<subevent code = "abc" narattive = "def" />
</event>
</STOCK>
<STOCK origin = "ASIA" ref="12" version="2" >
<event eventtype="ghi">
<subevent code = "abc" narattive = "def" />
</event>
</STOCK>
<STOCK origin = "ASI" ref="13" version="1" >
<event eventtype="ghi">
<subevent code = "abc" narattive = "def" />
</event>
</STOCK>
</STOCKEXT>
如前所述,这并不能完全满足您的需求 - 它会 'save memory' 只要您能够通过清除它来丢弃很多 XML go along...但是因为直到你到达终点你才知道哪个版本是最高的,所以内存占用是不可避免的,因为你永远不会完全知道你可以安全地丢弃什么直到你到达那里。
因此,也许您需要一种两次通过的方法,首先解析以提取 'highest version numbers' - 就像您正在做的那样,但是边走边清除...然后在您了解它们后重新开始,因为这样您就知道可以在进行时清除或冲洗什么。
您遇到此问题的原因是,在您到达文件末尾之前,您无法知道您是否拥有最新版本。
所以您可能需要改为执行类似的操作?
#!/usr/bin/perl
use strict;
use warnings;
use XML::Twig;
open( my $out, '>:utf8', 'out.xml' )
or die "cannot create output file out.xml: $!";
my $first_pass = new XML::Twig(
twig_roots => {
'/STOCKEXT/STOCK/STOCK' => sub { $_->delete() },
'/STOCKEXT/STOCK[@origin != "ASIA"]' => sub { $_->delete; },
'/STOCKEXT/STOCK' => \&extract_highest_version,
},
att_accessors => [qw/ ref version /],
pretty_print => 'indented',
);
my $main_parse = new XML::Twig(
twig_roots => {
'/STOCKEXT/STOCK/STOCK' => sub { $_->delete() },
'/STOCKEXT/STOCK[@origin != "ASIA"]' => sub { $_->delete; },
'/STOCKEXT/STOCK' => \&trade_handler
},
att_accessors => [qw/ ref version /],
pretty_print => 'indented',
);
my %max_version_of;
$first_pass->parsefile('1513.xml');
$main_parse->parsefile('1513.xml');
sub extract_highest_version {
my ( $twig, $trade ) = @_;
my ( $ref, $version ) = ( $trade->ref, $trade->version );
if ( not exists $max_version_of{$ref}
or $max_version_of{$ref} < $version )
{
$max_version_of{$ref} = $version;
}
$trade->purge;
}
sub trade_handler {
my ( $twig, $trade ) = @_;
my ( $ref, $version ) = ( $trade->ref, $trade->version );
if ( $version >= $max_version_of{$ref}
and ( $trade->first_child('event')->att('eventtype') eq 'ghi' )
and ( $trade->first_child('event')->first_child('subevent')->att('code') eq 'abc' )
and ( $trade->first_child('event')->first_child('subevent') ->att('narattive') eq 'def' )
)
{
$trade->flush;
}
else {
$trade->purge;
}
}
可能会更简洁一些,但重点是 - 你 运行 完成一次 - 然后 purge
随着你的进行,所以内存中只有一个 <STOCK>
在给定的时间。那你就有了'ref'和'highest version.
然后你第二次解析,因为你知道最高版本是什么,所以你可以 purge/flush 随着你的前进 - 你不必阅读未来。
现在如评论中所述 - 您的第一种方法将整个文件读入内存,因为它需要知道 'highest version'。虽然它是单次通过,但速度更快。
另一种方法是两次传递 - 读取文件两次。速度较慢,但根本不会在内存中保留太多内容。
中途之家可能是:
#!/usr/bin/perl
use strict;
use warnings;
use XML::Twig;
open( my $out, '>:utf8', 'out.xml' )
or die "cannot create output file out.xml: $!";
my $twig = new XML::Twig(
twig_roots => {
'/STOCKEXT/STOCK/STOCK' => sub { $_->delete() },
'/STOCKEXT/STOCK[@origin != "ASIA"]' => sub { $_->delete; },
'/STOCKEXT/STOCK' => \&trade_handler
},
att_accessors => [qw/ ref version /],
pretty_print => 'indented',
);
my %max_version_of;
my %best_version_of;
$twig->parsefile('1513.xml');
print {$out} "<STOCKEXT>\n";
foreach my $ref ( keys %best_version_of ) {
foreach my $trade ( @{ $best_version_of{$ref} } ) {
$trade->print($out);
}
}
print {$out} "</STOCKEXT>\n";
sub trade_handler {
my ( $twig, $trade ) = @_;
my ( $ref, $version ) = ( $trade->ref, $trade->version );
if (( not defined $max_version_of{$ref}
or $version >= $max_version_of{$ref}
)
and ( $trade->first_child('event')->att('eventtype') eq 'ghi' )
and
( $trade->first_child('event')->first_child('subevent')->att('code')
eq 'abc' )
and ( $trade->first_child('event')->first_child('subevent')
->att('narattive') eq 'def' )
)
{
if ( not defined $max_version_of{$ref}
or $version >= $max_version_of{$ref} )
{
#this version is higher, so anything lower is redundant - remove it
@{ $best_version_of{$ref} } = ();
}
push( @{ $best_version_of{$ref} }, $trade );
$max_version_of{$ref} = $version;
}
$trade->purge;
#can omit this, it'll just print how 'big' the hash is getting.
print "Hash size: ". %best_version_of."\n";
}
它所做的仍然是清除,但会慢慢填满 %best_version_of
。尽管它可能仍然占用大量内存 - 这在很大程度上取决于 'keep' 与 'discard' 的比率。
恐怕没有最佳解决方案 - 要弄清楚哪个版本是 'newest' 你必须 运行 遍历文件两次,要么你以内存消耗为代价这样做,或者你这样做是以磁盘 IO 为代价的。
(而且我想我必须提供一个警告 - 这不会产生 'valid' XML,因为根节点 <STOCKEXT>
将被丢弃。把它放在开头最后的 $out
和 </STOCKEXT>
会解决这个问题)。