【问题标题】:Out of memory processing large files with Perl, Sed, AWK使用 Perl、Sed、AWK 处理大文件时内存不足
【发布时间】:2012-08-17 17:55:25
【问题描述】:

我正在使用以下内容提取 XML 标记之间的内容:-

perl -lne 'BEGIN{undef $/} while (/<tagname>(.*?)<\/tagname>/sg){print $1}' input.txt > output.txt

不幸的是,我遇到了out of memory 问题,我知道我可以拆分文件并分别处理 concat 但我想知道是否还有其他方法,是对上述内容进行修改还是使用 awk 或 sed 之类的方法?

input.txt 文件大小在 17GB 和 70GB 之间变化。

编辑:

输入文件可以是任何 XML 文件,需要注意的是它不包含换行符,例如:-

&lt;body&gt;&lt;a&gt;&lt;/a&gt;&lt;b&gt;&lt;/b&gt;&lt;c&gt;&lt;/c&gt;&lt;/body&gt;&lt;foo&gt;&lt;/foo&gt;&lt;bar&gt;&lt;z&gt;&lt;/z&gt;&lt;/bar&gt;

【问题讨论】:

  • 请给出输入文件的摘录
  • 输入文件可以是任何XML文件。我可能应该指出的一点是它没有换行符。

标签: xml perl sed awk


【解决方案1】:

这个单行将整个文件作为一个巨大的“行”读入内存。当然,在内存中塞入 17GB 甚至更多的内存会出现问题!逐行读取和处理文件或使用read 来获取合适大小的块。

在这种情况下,搜索&lt;tagname&gt;,注意它在行中的位置并从那里开始搜索结束标记。如果您没有找到它,请将当前行/块填充到缓冲区中并重复,直到您在文件中的其他行上找到它。找到后,打印出此缓冲区并将其清空。重复直到文件结束。

请注意,如果您使用任意大小的块,则必须通过从块的末尾切割不完整的标签并将其填充到“待处理”缓冲区中来考虑标签被边界分割的可能性。

【讨论】:

  • 输入文件不包含换行符。 :(
  • 建议的算法是一个不错的起点。然而,它可能需要一些调整才能变得健壮:如果输入文件中存在错误(从不关闭的标签)或标签相距很远(例如 &lt;body&gt; 标签包含整个文件)。
  • 这种方式可能对您有用,但请注意您不是在处理 XML。例如,如果标签具有属性,那么您将无法匹配它们。因此,如果您决定采用正则表达式方式,至少要清楚地记录您可以处理的 XML 子集。这样,如果您的输入发生变化,您就有更好的机会找出问题所在。
【解决方案2】:

使用像XML::LibXML::Reader 这样的拉式解析器应该可以解析大文件。这是一个例子:

#!/usr/bin/perl
use warnings;
use strict;

use XML::LibXML::Reader;

my $reader = XML::LibXML::Reader->new(location => 'input.txt') or die;

while ($reader->read) {
    if ($reader->nodePath =~ m{/tagname$}                    # We are at <tagname> or </tagname>.
        and $reader->nodeType == XML_READER_TYPE_ELEMENT) {  # Only the start tag is interesting.
        print $reader->readInnerXml;
    }
}

【讨论】:

  • +1 对 XML 数据使用 XML 解析器。正则表达式是错误的工具。
【解决方案3】:

为了从文件中读取较小的块,您可以将输入记录分隔符设置为结束标记:

BEGIN { $/ = "</tagname>"; }

这是一个例子:

代码:

perl -lnwe 'BEGIN { $/ = "</tagname>"; } print;'

输入:

<tagname>foo</tagname><tagname>bar</tagname><tagname>baz</tagname><tagname>baf</tagname>

输出:

<tagname>foo
<tagname>bar
<tagname>baz
<tagname>baf

您会注意到缺少结束标记,这是因为您使用的-l 选项还包含chomp,它删除了输入记录分隔符。如果您不希望这种行为,只需删除 -l 选项并在您的打印语句中插入换行符。

注意:

我会说这有点像 hack,但它确实匹配您已经使用的内容,即区分大小写、精确标签。

您可以做的就是在其中使用您的正则表达式:

perl -lnwe 'BEGIN { $/ = "</tagname>"; } 
    while (/<tagname>(.*?)<\/tagname>/sg) { print $1 }' input.txt > output.txt

或者,可能,使用 XML 解析器来解析块。

如果其他人建议的 XML 解析器不适用于如此大的文件,这可能是一种读取较小数据块的方法,而不会有将标签切成两半的风险。

【讨论】:

    【解决方案4】:

    您还可以使用 awk 来破坏一个大的单行文件。 Sed 在尝试加载整行时会因内存不足而崩溃,但在 awk(如 perl 中)中,您可以定义要用作“换行符”的内容,从而绕过问题。

    对于 perl,上面已经有一个示例,这里是 awk 示例:

    cat big-one-line-file |  awk 'BEGIN { RS=">" } ; {print $0">"}'
    

    请注意,在文件末尾,如果文件不以“>”结尾,则会显示一个额外的 >。您可以通过任何方式将其删除(例如清理后 sed: sed '$ s/&gt;$//' )或调整脚本。

    由于我也遇到了这个问题,为了帮助其他人,我会添加更多示例来帮助测试。

    您可以使用 dd 测试脚本以提取文件的一小部分并捕获更大的“记录分隔符”,例如作品或标签。示例:

    dd if=big-one-line-file.xml bs=8192 count=10  | awk ' BEGIN { RS="<tag 123>" } ; NR>1 {print "<tag 123>"$0}  ; NR==1 {print $0}  ' 
    

    提取 big-one-line-file.xml 的前 80kB 并在 "" 中拆分文件。为避免文件开头的多余(和错误)“”,请区别对待(即:不要触摸它)

    使用 dd 选项 skip={# of blocks to reach near the file size} 提取文件的末尾而不是顶部(尾部将失败,因为它总是只有一行)。我使用了 skip=100000000 并开始删除零,直到出现某些内容并调整块号。

    【讨论】:

      【解决方案5】:

      我会对您的输入文件应用过滤器以引入换行符。也许在每个&lt;/tagname&gt; 之后?然后你就可以在你的perl 命令中去掉BEGIN{undef $/} 并通过处理“合理”的记录来避免内存问题。

      【讨论】:

        【解决方案6】:

        不清楚您输入的文件是否是格式良好的 XML。您给出的示例不是 XML(没有根元素)。但是,如果数据是 XML,您可以使用 xml_grep 附带的工具 XML::Twigxml_grep -r tagname --text_only mybig.xml 这将适用于任何大小的文件,前提是每个匹配的元素都可以放入内存中。

        如果这太慢了,你可能可以通过直接使用 XML::Parser 来获得一些速度,代码编写起来并不复杂。不过,不必编写它更容易;--)

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-03-23
          • 2021-08-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多