【问题标题】:Perl "out of memory" with large text file带有大文本文件的 Perl“内存不足”
【发布时间】:2015-04-04 11:00:05
【问题描述】:

我在最新版本的 Strawberry Perl for Windows 下的以下代码存在问题:我想读入目录中的所有文本文件并处理它们的内容。我目前看不到逐行处理它们的方法,因为我想对文件内容进行的一些更改跨越换行符。处理主要涉及删除大块文件(在下面的示例代码中,它只是一行,但理想情况下我会运行几个类似的正则表达式,每个都从文件中删除内容)

我在大量文件 (>10,000) 上运行此脚本,但它总是以“内存不足!”而崩溃。大于 400 MB 的特定文件上的消息。问题是,当我编写一个只处理一个文件的程序时,代码可以正常工作。

这台机器有 8 GB RAM,所以我认为物理 RAM 不是问题。

我阅读了有关记忆问题的其他帖子,但没有找到任何可以帮助我实现目标的内容。

谁能建议我需要更改哪些内容才能使程序正常运行,即提高内存效率或以某种方式回避问题?

use strict;
use warnings;
use Path::Iterator::Rule;
use utf8;

use open ':std', ':encoding(utf-8)';

my $doc_rule = Path::Iterator::Rule->new;
$doc_rule->name('*.txt'); # only process text files
$doc_rule->max_depth(3); # don't recurse deeper than 3 levels
my $doc_it = $doc_rule->iter("C:\Temp\");
while ( my $file = $doc_it->() ) { # go through all documents found
    print "Stripping $file\n";

    # read in file
    open (FH, "<", $file) or die "Can't open $file for read: $!";
    my @lines;
    while (<FH>) { push (@lines, $_) }; # slurp entire file
    close FH or die "Cannot close $file: $!";

    my $lines = join("", @lines); # put entire file into one string

    $lines =~ s/<DOCUMENT>\n<TYPE>EX-.*?\n<\/DOCUMENT>//gs; #perform the processing

    # write out file
    open (FH, ">", $file) or die "Can't open $file for write: $!";
    print FH $lines; # dump entire file
    close FH or die "Cannot close $file: $!";
}

【问题讨论】:

  • 不确定您是否遇到过这个问题,但这是堆内存分析和使用内存映射文件而不是使用文件的一个很好的答案。 stackoverflow.com/questions/9733146/…
  • 而不是进行多行搜索和替换,为什么不只是:1)逐行读取文件,直到到达起始分隔符 2)检查后续行是否符合您想要的条件;如果是这样,请不要在输出文件中打印任何内容,直到到达结束分隔符;如果没有,请打印出来。
  • 你可以试试File::Sip
  • 看起来你正在处理 XML 数据,你应该使用XML::Twig
  • 这不是一个解决方案(这就是我将其作为评论发布的原因),但是,对于它的价值,您可以以my @lines = &lt;FH&gt;; 的形式一次性读取文件

标签: windows string perl memory


【解决方案1】:

在 Windows Server 2013 上使用 Perl 5.10.1 处理一个有点大 (1.2G) 的文件时,我注意到

foreach my $line (<LOG>) {}

由于内存不足而失败,而

while (my $line = <LOG>) {}

在一个简单的脚本中工作,该脚本只运行一些正则表达式并打印我感兴趣的行。

【讨论】:

  • 那是因为第一个示例中的 是在列表上下文中评估的,即整个文件被吞入。在您的第二个示例中, 在标量上下文中评估,一次只有一行已获取。
【解决方案2】:

逐行处理文件:

while ( my $file = $doc_it->() ) { # go through all documents found
    print "Stripping $file\n";

    open (my $infh, "<", $file) or die "Can't open $file for read: $!";
    open (my $outfh, ">", $file . ".tmp") or die "Can't open $file.tmp for write: $!";

    while (<$infh>) {
       if ( /<DOCUMENT>/ ) {
           # append the next line to test for TYPE
           $_ .= <$infh>;
           if (/<TYPE>EX-/) {
              # document type is excluded, now loop through 
              # $infh until the closing tag is found.
              while (<$infh>) { last if m|</DOCUMENT>|; }

              # jump back to the <$infh> loop to resume
              # processing on the next line after </DOCUMENT>
              next;
           }
           # if we've made it this far, the document was not excluded
           # fall through to print both lines
       }
       print $outfh $_;
    }

    close $outfh or die "Cannot close $file: $!";
    close $infh or die "Cannot close $file: $!";
    unlink $file;
    rename $file.'.tmp', $file; 
}

【讨论】:

  • 我在答案末尾暗示但懒得写的解决方案。 +1
  • 确实如此。现在有点短了:)
  • 你测试过这个吗?你有一个多余的next,但更重要的是,在你的内部while 循环之后,$_ 中的内容远不清楚。看起来您将打印结束 &lt;/DOCUMENT&gt; 标记,而 OP 的代码将其删除。
  • 是的,它已经过测试。 next 跳回到 while 的顶部,避免在成功匹配 TYPE 后打印。但是,如果您看不到这一点,那么也许有些 cmets 是正常的。
【解决方案3】:

使用正则表达式处理 XML 容易出错且效率低下,正如将整个文件作为字符串吞咽的代码所示。要处理 XML,您应该使用 XML 解析器。特别是,您需要一个 SAX 解析器,它可以一次处理一个 XML,而不是一个读取整个文件的 DOM 解析器。

我将按原样回答您的问题,因为了解如何逐行工作是有价值的。

如果可以避免,请不要将整个文件读入内存。一行一行地工作。您的任务似乎是出于某种原因从 XML 文件中删除几行。 &lt;DOCUMENT&gt;\n&lt;TYPE&gt;EX-&lt;\/DOCUMENT&gt; 之间的所有内容。我们可以通过保留一些状态来逐行执行此操作。

use autodie;

open (my $infh, "<", $file);
open (my $outfh, ">", "$file.tmp");

my $in_document = 0;
my $in_type_ex  = 0;
while( my $line = <$infh> ) {
    if( $line =~ m{<DOCUMENT>\n}i ) {
        $in_document = 1;
        next;
    } 
    elsif( $line =~ m{</DOCUMENT>}i ) {
        $in_document = 0;
        next;
    }
    elsif( $line =~ m{<TYPE>EX-}i ) {
        $in_type_ex = 1;
        next;
    }
    elsif( $in_document and $in_type_ex ) {
        next;
    }
    else {
        print $outfh $line;
    }
}

rename "$file.tmp", $file;

使用临时文件可以让您在构建替换文件时读取该文件。

当然,如果 XML 文档的格式不正确(我在正则表达式中添加了 /i 标志以允许使用小写标记),那么这将失败,您应该真正使用 SAX XML 解析器。

【讨论】:

  • 也许有点简化了。原代码中的.*s修饰符的影响。
  • 实际上,他们应该使用 SAX 解析器。
  • 当然,但这是一个有趣的小练习。
  • 我已经更新了答案以适应@tjd 的评论。
【解决方案4】:

您同时在内存中保留文件的两个完整副本,@lines$lines。您可以考虑:

open (my $FH, "<", $file) or die "Can't open $file for read: $!";
$FH->input_record_separator(undef); # slurp entire file
my $lines = <$FH>;
close $FH or die "Cannot close $file: $!";

在足够过时的 Perl 版本上,您可能需要明确地use IO::Handle

另请注意:我已从裸词版本切换到词法文件句柄。我想你并没有努力与 Perl v4 兼容。

当然,如果将内存需求减少一半还不够,您可以随时遍历文件...

【讨论】:

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