【问题标题】:How can I speed up XML::Twig如何加快 XML::Twig
【发布时间】:2012-12-12 11:54:04
【问题描述】:

我正在使用XML::Twig 解析一个非常大的 XML 文档。我想根据<change></change> 标签把它分成块。

现在我有:

my $xml = XML::Twig->new(twig_handlers => { 'change' => \&parseChange, });
$xml->parsefile($LOGFILE);

sub parseChange {

  my ($xml, $change) = @_;

  my $message = $change->first_child('message');
  my @lines   = $message->children_text('line');

  foreach (@lines) {
    if ($_ =~ /[^a-zA-Z0-9](?i)bug(?-i)[^a-zA-Z0-9]/) {
      print outputData "$_\n";
    }
  }

  outputData->flush();
  $change->purge;
}

现在,当它从 XML 中提取该块时,它正在运行 parseChange 方法。它进展非常缓慢。我测试了它是否可以从带有$/=</change> 的文件中读取 XML 并编写一个函数来返回 XML 标记的内容,并且速度要快得多。

是我遗漏了什么还是我错误地使用了XML::Twig?我是 Perl 新手。

编辑:这是更改文件的示例更改。该文件由很多这样一个接一个地组成,它们之间不应该有任何东西:

<change>
<project>device_common</project>
<commit_hash>523e077fb8fe899680c33539155d935e0624e40a</commit_hash>
<tree_hash>598e7a1bd070f33b1f1f8c926047edde055094cf</tree_hash>      
<parent_hashes>71b1f9be815b72f925e66e866cb7afe9c5cd3239</parent_hashes>      
<author_name>Jean-Baptiste Queru</author_name>      
<author_e-mail>jbq@google.com</author_e-mail>      
<author_date>Fri Apr 22 08:32:04 2011 -0700</author_date>      
<commiter_name>Jean-Baptiste Queru</commiter_name>      
<commiter_email>jbq@google.com</commiter_email>      
<committer_date>Fri Apr 22 08:32:04 2011 -0700</committer_date>      
<subject>chmod the output scripts</subject>      
<message>         
    <line>Change-Id: Iae22c67066ba4160071aa2b30a5a1052b00a9d7f</line>      
</message>      
<target>         
    <line>generate-blob-scripts.sh</line>      
</target>   
</change>

【问题讨论】:

  • 我认为在将 XML 传递给XML::Twig 之前使用正则表达式对其进行预处理不是一个好主意。它使您的代码变得不那么健壮。例如,如果评论中有&lt;/change&gt; 怎么办?此外,XML 解析不太可能会减慢您的脚本速度。您能否提供更多信息:文件的大小以及您正在进行什么样的处理?
  • 我现在没有在任何地方使用正则表达式。一种方法是使用 twig,另一种方法是读入并自己解析。我从整个脚本中提取了这一部分,所以它是唯一正在运行的东西。此外,文件大小为 2.3gb。我正在从 xml 中提取数据并将其中的一些添加到哈希中。
  • 抱歉,说“正则表达式”是个错误。我的意思是,如果您在解析文件之前使用某些规则(例如行分隔符)分解文件,则可能会破坏 XML 的完整性。你的 XML 文件有多大?
  • 文件为 2.3gb。它是来自 git 存储库的 xml 格式的更改日志
  • 更多信息:它在那里解析了至少一个小时,而使用行分隔符的解析器花了大约 15-20 分钟来做同样的事情。

标签: xml perl parsing large-files xml-twig


【解决方案1】:

就目前而言,您的程序正在处理 XML 文档的所有,包括您不感兴趣的 change 元素之外的数据。

如果您将构造函数中的twig_handlers 参数更改为twig_roots,则只会为感兴趣的元素构建树结构,其余的将被忽略。

my $xml = XML::Twig->new(twig_roots => { change => \&parseChange });

【讨论】:

  • 我会试试这个,但无论如何文档应该只是一堆更改。我已经开始运行它了,它看起来和以前的速度差不多。
  • 然后您应该将您的 XML 导入到 SQLite,然后从那里处理并导出它。 XML 不是随机访问数据库格式。
【解决方案2】:

XML::Twig 包含一种机制,您可以在标签出现时对其进行处理,然后丢弃不再需要的内容以释放内存。

这是取自the documentation 的示例(其中还有很多有用的信息):

my $t= XML::Twig->new( twig_handlers => 
                          { section => \&section,
                            para   => sub { $_->set_tag( 'p'); }
                          },
                       );
  $t->parsefile( 'doc.xml');

  # the handler is called once a section is completely parsed, ie when 
  # the end tag for section is found, it receives the twig itself and
  # the element (including all its sub-elements) as arguments
  sub section 
    { my( $t, $section)= @_;      # arguments for all twig_handlers
      $section->set_tag( 'div');  # change the tag name.4, my favourite method...
      # let's use the attribute nb as a prefix to the title
      my $title= $section->first_child( 'title'); # find the title
      my $nb= $title->att( 'nb'); # get the attribute
      $title->prefix( "$nb - ");  # easy isn't it?
      $section->flush;            # outputs the section and frees memory
    }

这在处理数 GB 文件时可能是必不可少的,因为(同样,根据文档)将整个内容存储在内存中可能会占用文件大小的 10 倍。

编辑:基于您编辑的问题的几个 cmets。在不了解文件结构的情况下,不清楚究竟是什么让您放慢了速度,但这里有一些事情可以尝试:

  • 如果您要编写很多行,刷新输出文件句柄会减慢您的速度。 Perl 专门出于性能原因缓存文件写入,而您正在绕过它。
  • 不使用(?i) 机制,这是一个相当高级的功能,可能会降低性能,为什么不让整个匹配不区分大小写呢? /[^a-z0-9]bug[^a-z0-9]/i 是等价的。您还可以使用/\bbug\b/i 来简化它,它几乎等效,唯一的区别是下划线包含在不匹配的类中。
  • 还可以进行其他一些简化以删除中间步骤。

这个处理程序代码在速度方面与您的相比如何?

sub parseChange
{
    my ($xml, $change) = @_;

    foreach(grep /[^a-z0-9]bug[^a-z0-9]/i, $change->first_child_text('message'))
    {
        print outputData "$_\n";
    }

    $change->purge;
}

【讨论】:

  • 我确实看过这个,虽然我必须承认我对para 行有点困惑。我想这就是我正在做的事情。您可以在我的示例代码中看到我确实定义了一个处理程序。
  • @user1897691,您是否 flushpurge 释放了处理程序中的内存?我不是XML::Twig 方面的专家,但如果您发布处理程序的代码,有人可能会为您提供更多帮助。
  • 好的,我将它添加到我原来的问题中。我相信有人会评论 FileIO 是多么昂贵,但它是在两个版本的代码中完成的,而且我得到的时间不同。 FileIO 并不是一个运行速度比另一个快的原因。
  • @user1897691,更新了一些建议。如果这没有帮助,提供有关您的文件的更多信息可能会:有多少更改标签,您正在搜索的部分中有多少行,您匹配错误行多少次并打印它?等
  • @Borodin,这最初并不清楚,因为 OP 没有显示处理程序子,所以不清楚树正在被清除或刷新。
【解决方案3】:

如果您的 XML 非常大,请使用 XML::SAX。它不必将整个数据集加载到内存中;相反,它顺序加载文件并为每个标签生成回调事件。我成功地使用 XML::SAX 来解析大小超过 1GB 的 XML。以下是您的数据的 XML::SAX 处理程序示例:

#!/usr/bin/env perl
package Change::Extractor;
use 5.010;
use strict;
use warnings qw(all);

use base qw(XML::SAX::Base);

sub new {
    bless { data => '', path => [] }, shift;
}

sub start_element {
    my ($self, $el) = @_;
    $self->{data} = '';
    push @{$self->{path}} => $el->{Name};
}

sub end_element {
    my ($self, $el) = @_;
    if ($self->{path} ~~ [qw[change message line]]) {
        say $self->{data};
    }
    pop @{$self->{path}};
}

sub characters {
    my ($self, $data) = @_;
    $self->{data} .= $data->{Data};
}

1;

package main;
use strict;
use warnings qw(all);

use XML::SAX::PurePerl;

my $handler = Change::Extractor->new;
my $parser = XML::SAX::PurePerl->new(Handler => $handler);

$parser->parse_file(\*DATA);

__DATA__
<?xml version="1.0"?>
<change>
  <project>device_common</project>
  <commit_hash>523e077fb8fe899680c33539155d935e0624e40a</commit_hash>
  <tree_hash>598e7a1bd070f33b1f1f8c926047edde055094cf</tree_hash>
  <parent_hashes>71b1f9be815b72f925e66e866cb7afe9c5cd3239</parent_hashes>
  <author_name>Jean-Baptiste Queru</author_name>
  <author_e-mail>jbq@google.com</author_e-mail>
  <author_date>Fri Apr 22 08:32:04 2011 -0700</author_date>
  <commiter_name>Jean-Baptiste Queru</commiter_name>
  <commiter_email>jbq@google.com</commiter_email>
  <committer_date>Fri Apr 22 08:32:04 2011 -0700</committer_date>
  <subject>chmod the output scripts</subject>
  <message>
    <line>Change-Id: Iae22c67066ba4160071aa2b30a5a1052b00a9d7f</line>
  </message>
  <target>
    <line>generate-blob-scripts.sh</line>
  </target>
</change>

输出

Change-Id: Iae22c67066ba4160071aa2b30a5a1052b00a9d7f

【讨论】:

  • 如果这样更快,那么它会成功。但是,除了您在示例中提取的行之外,我还在从 xml 中寻找其他信息。如何在我的规范的某个标签中提取数据?
  • 提供的示例通过if ($self-&gt;{path} ~~ [qw[change message line]]) { ... } 条件检测标签。因此,要获取author_name,请添加条件$self-&gt;{path} ~~ [qw[change author_name]]
【解决方案4】:

不是 XML::Twig 的答案,而是 ...

如果您要从 xml 文件中提取内容,您可能需要考虑 XSLT。使用 xsltproc 和以下 XSL 样式表,我在大约一分钟内从 &lt;change&gt;s 的 1Gb 中得到了包含错误的更改行。我敢肯定,还有很多改进可能。

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" >

  <xsl:output method="text"/>
  <xsl:variable name="lowercase" select="'abcdefghijklmnopqrstuvwxyz'" />
  <xsl:variable name="uppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'" />

  <xsl:template match="/">
    <xsl:apply-templates select="changes/change/message/line"/>
  </xsl:template>

  <xsl:template match="line">
    <xsl:variable name="lower" select="translate(.,$uppercase,$lowercase)" />
    <xsl:if test="contains($lower,'bug')">
      <xsl:value-of select="."/>
      <xsl:text>
</xsl:text>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

如果你的 XML 处理可以做为

  1. 提取为纯文本
  2. 整理扁平化文本
  3. 利润

那么 XSLT 可能是该过程中第一步的工具。

【讨论】:

    【解决方案5】:

    我的要花很长时间。

        my $twig=XML::Twig->new
      (
    twig_handlers =>
       {
        SchoolInfo => \&schoolinfo,
       },
       pretty_print => 'indented',
      );
    
    $twig->parsefile( 'data/SchoolInfos.2018-04-17.xml');
    
    sub schoolinfo {
      my( $twig, $l)= @_;
      my $rec = {
                     name   => $l->field('SchoolName'),
                     refid  => $l->{'att'}->{RefId},
                     phone  => $l->field('SchoolPhoneNumber'),
                    };
    
      for my $node ( $l->findnodes( '//Street' ) )    { $rec->{street} = $node->text; }
      for my $node ( $l->findnodes( '//Town' ) )      { $rec->{city} = $node->text; }
      for my $node ( $l->findnodes( '//PostCode' ) )  { $rec->{postcode} = $node->text; }
      for my $node ( $l->findnodes( '//Latitude' ) )  { $rec->{lat} = $node->text; }
      for my $node ( $l->findnodes( '//Longitude' ) ) { $rec->{lng} = $node->text; }     
    }
    

    这是 pretty_print 的机会吗?否则就很简单了。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多