带有大文本文件的 Perl "out of memory"

Perl "out of memory" with large text file

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

我在大量文件(>10,000)上使用此脚本 运行,它总是在一个大于 400 MB 的特定文件上出现 "Out of memory!" 消息。问题是当我编写一个只处理一个文件的程序时,代码工作正常。

机器有 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: $!";
}

您在内存中同时保存了文件的两个完整副本,@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 兼容。

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

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

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

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

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 解析器。

逐行处理文件:

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; 
}

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

foreach my $line (<LOG>) {}

因内存不足而失败,而

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

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