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> 会解决这个问题)。