【问题标题】:Perl XML::SAX partial parsingPerl XML::SAX 部分解析
【发布时间】:2014-12-23 06:21:21
【问题描述】:

是否可以使用 XML::SAX::Expat 的处理程序类暂停和恢复解析?

文件非常大,我们正在将节点读入内存以渲染表格。我们只想一次渲染一个部分,否则内存不足。所以我们需要停止解析文件,在程序的其他部分做一些事情,然后在下一页继续。

我可以想到几种方法来实现这一点(如下),但它们都感觉像是 hack。有什么我可以使用的原生的吗?

可能的选择:

  • 跟踪 $current_node 计数器并将其传递给处理程序构造函数,每个 我们在解析文件时会在存储数据之前跳过这么多节点。
  • 与上述类似,但使用 tell/seek 跳过每个新调用
  • 预处理将文件拆分为多个大小正确的文件。

前两个效率低下,最后一个杂乱无章。有更好的选择吗?


编辑以解释有关文件结构的更多信息以及替代方法不起作用的原因。

除了一些其他数据外,大部分结构如下。

<DETAILS>
    <DETAIL>
        <ITEM1>...</ITEM1>
        <ITEM2>...</ITEM2>
        ...
    </DETAIL>
    <DETAIL>
        <ITEM1>...</ITEM1>
        <ITEM2>...</ITEM2>
        ...
    </DETAIL>
    ...
</DETAILS>

对于有问题的文件,每个&lt;DETAIL&gt; 节点的大小大约为 240 字节,虽然不多,但我们有超过 180,000 个(这是无法处理的较小文件之一)。 LibXML 在遇到此结构时会失败,因为它会尝试将其全部解析到内存中(我们仅限于 32 位系统,并且 Perl 的内存中还有其他重要的结构)。

更新到最新版本和一些代码调整后,XML::Twig 将解析文档,但我仍然有同样的问题 - 是否可以稍后暂停和恢复?

我不控制整个逻辑流程,所以当主应用程序准备好进入下一页时,它会调用我的对象来获取它。我需要能够输出一大块数据并等待下一个请求。这可能由fork 处理,但我不确定是否需要这样做。


显示程序流程的示例。

这是一种简化(尤其是 while 循环)。实际程序具有复杂的文档页面嵌套结构,其中包含表示页面元素的多个对象。它是通过使用 Web 服务调用来定义的,并且也是数据驱动的,因此我们不能为此硬编码任何假设。

我看不出如何在其中添加回调 - 处理必须在表格之后恢复以发出剩余的页面元素,开始一个新页面,并在恢复表格之前发出该新页面的前几个页面元素。

use strict;
use warnings;

use XML::Twig;

my $table = Table->new('details.xml');

my $table_finished = 0;
while (!$table_finished) {
    # emit some data e.g. page header
    # ...
    # emit the table - 2 data rows per page, for testing
    $table_finished = $table->partial_emit(2);
    # emit some data e.g. page footer
    # ...
}

exit;

package Table;

sub new {
    my ($class, $filename) = @_;

    my $self = {
        '_file' => $filename,
    };

    bless ($self, $class);

    my $sub_ref = $self->can('process_table_row');

    $self->{'_twig'} = XML::Twig->new( 
                twig_handlers => {
                    'DETAIL'    => sub {
                        $sub_ref->($self, @_),
                        },
                });     

    return $self;
}

sub partial_emit {
    my ($this, $rows) = @_;
    $this->{'_rows_emitted'} = 0;
    $this->{'_limit'} = $rows;
    $this->{'_finished'} = 1;

    # we want this to return after parsing part of the file if it is large
    $this->{'_twig'}->parsefile($this->{'_file'});

    # should be zero if we returned early
    return $this->{'_finished'};
}

sub process_table_row {
        my ($this, $twig, $elt) = @_;

        # increase row count
        $this->{'_rows_emitted'}++;

        # handle data - doesn't matter what it does here
        print $elt->text, "\n";

        # we've done as many as we want - how to stop processing and return to main loop?
        if ($this->{'_rows_emitted'} >= $this->{'_limit'}) {
            print "Limit reached\n";
            # Ideally we'd set this, tell Twig to stop for a while, and carry on, but in my test script this causes an infinite loop
            #$this->{'_finished'} = 0;
        }       
}

1;

还有另一个编辑......似乎在调整了我的搜索之后,我偶然发现了我一直想要的东西。 XML::SAX::Expat::Incremental 有一个 parse_more 例程,它完全符合我的需要。我需要等待几天才能测试完整的数据集,但下面的简短测试有效。

Table 类可以这样做:

$self->{'_parser'} = XML::SAX::Expat::Incremental->new( Handler => MyHandler->new($self) );

其中MyHandler 是一个简单的XML::SAX 样式处理程序,现在可以访问Table

调用Table::partial_emit 将执行以下操作:

my $buf;
my $bytes_to_read = 50; # small for testing
while (read($this->{'_fh'}, $buf, $bytes_to_read)) {
    $this->{'_parser'}->parse_more($buf);
    # MyHandler will increment this based on the number of rows (DETAIL nodes) encountered
    if ($this->{'_rows_emitted'} >= $rows) {
        $this->{'_finished'} = 0;
        last;
    }
}

以上可能在极端情况下存在一些错误,但它适用于我的测试。稍后我需要对其进行适当的压力测试,看看它是否可以投入生产。

【问题讨论】:

  • 请澄清“是否可以稍后暂停和恢复?”XML::Twig 将等待您指定的回调返回。如果 later 意味着几微秒,那么没有问题,但如果可能是几年,那么你有一个不同的设计问题。这同样适用于您的 BIG BROTHER 主应用程序。您没有描述任何阻止您等待一两年再回复IT的事情。
  • @Borodin pause 是不正确的词,我不应该使用它。产量控制会更准确。我们希望 twig 在特定数量的节点后返回,恢复正常处理,然后稍后再次调用拥有 twig 的对象,它应该从中断的地方继续读取。稍后我将尝试添加一个简化的示例。

标签: xml windows perl xml-parsing sax


【解决方案1】:

XML::Twig 模块旨在在此类情况下表现良好。

它的副标题是“A perl module for processing large XML documents in tree mode”

【讨论】:

  • 对不起,我忘了提 - 我喜欢 Twig 的风格,并在这个特定的文档上尝试过,但我在一个小时后放弃了等待,而 SAX 在一两分钟内完成了完整的解析(没有构建桌子)。我明天会检查以确保我们拥有最新版本,看看出了什么问题。如果可能的话,我更喜欢 SAX,因为它看起来很轻量级。
  • @NickP:您可能没有正确设置XML::Twig。您应该为要作为 unit 处理的 XML 元素设置回调(树枝处理程序),并确保在回调结束时 flush 树,否则整个树被保存在内存中,XML::Twig 并不比XML::LibXML 或任何其他从 XML 数据构建整个树的模块更好。
  • 除了我同意 Borodin 所说的,XML::Twig 还附带了一个名为 xml_split 的工具。它提供了一些方便的方法将一个大文件拆分为几个较小的文件(按深度、节点数或拆分文件的大小)。如果您选择这样做,这可能会对您有所帮助。
  • 我还应该提到 XML::LibXML::Reader 也可能是一个不错的选择。
【解决方案2】:

经过一番搜索,我发现了一个非常有用的旧线程,它详细说明了我需要什么。

http://www.perlmonks.org/?node_id=420383

我可以将XML::Parser::ExpatNB 用于我需要的行为。 XML::SAX::Expat::Incremental 会在必要时将其封装到 SAX 接口中,但我想我不会打扰。

示例代码如下。它的性能足够好(比XML::Twig 快​​),所以我将使用它。

use strict;
use warnings;

use XML::Parser::Expat;

my $parser = XML::Parser::ExpatNB->new();

$parser->setHandlers('Start' => \&start_element,
                     'End'   => \&end_element,
                     'Char'  => \&char_data);

my $read_size = 64 * 1024; # test to find optimal size
my $file_name = '../details.xml';
my $buf;

open(my $fh, '<', $file_name) or die $!;
binmode($fh);

my $bytes_read;
while ( $bytes_read = read($fh, $buf, $read_size) ) {
    $parser->parse_more($buf);
}
$parser->parse_done();
die "Error: $!" unless defined($bytes_read);
close($fh);

我省略了处理程序,它们是这种方法的典型($_[0] 是包含当前上下文的 XML::Parser::ExpatNB 对象,$_[1] 是数据,例如节点名称或字符数据)。

XML::LibXML::Reader也可以如下图,我之前没完全看懂界面。不过在我的机器上速度较慢,并且所需的节点处理有点复杂(例如,CDATA 不会自动以文本形式返回),所以我暂时避免使用它。

my $reader = XML::LibXML::Reader->new(location => $file_name) or die $!;
while ($reader->read) {
    processNode($reader);
}

【讨论】:

    猜你喜欢
    • 2011-04-13
    • 2011-06-29
    • 1970-01-01
    • 2012-08-27
    • 1970-01-01
    • 2011-04-30
    • 2011-04-16
    • 2013-12-05
    • 2017-08-19
    相关资源
    最近更新 更多