提高Perl搜索文件脚本的性能

时间:2013-08-29 11:33:05

标签: regex perl perl-data-structures

我最近注意到,我在Perl中编写的用于子10MB文件的快速脚本已被修改,重新任务并在40MB +文本文件中使用,在批处理环境中存在严重的性能问题。

当遇到大型文本文件时,每次运行的作业已经运行了大约12个小时,我想知道如何提高代码的性能?我应该将文件粘贴到内存中,如果我这样做,将会破坏作业对文件中行号的依赖。任何建设性的想法都会非常感激,我知道这个工作循环遍历文件太多次但是如何减少它?

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

my $filename = "$ARGV[0]"; # This is needed for regular batch use 
my $cancfile = "$ARGV[1]"; # This is needed for regular batch use 
my @num =();
open(FILE, "<", "$filename") || error("Cannot open file ($!)");
while (<FILE>)
{
    push (@num, $.) if (/^P\|/)
}
close FILE;

my $start;
my $end;

my $loop = scalar(@num);
my $counter =1;
my $test;

open (OUTCANC, ">>$cancfile") || error ("Could not open file: ($!)");

#Lets print out the letters minus the CANCEL letters
for ( 1 .. $loop )
{
    $start = shift(@num) if ( ! $start );
    $end = shift(@num);
    my $next = $end;
    $end--;
    my $exclude = "FALSE";

    open(FILE, "<", "$filename") || error("Cannot open file ($!)");
    while (<FILE>)
    {
        my $line = $_;
        $test = $. if ( eof );
        if ( $. == $start && $line =~ /^P\|[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]\|1I\|IR\|/)
        {
            print OUTCANC "$line";
            $exclude = "TRUECANC";
            next;
        }
        if ( $. >= $start && $. <= $end && $exclude =~ "TRUECANC")
        {
            print OUTCANC "$line";
        } elsif ( $. >= $start && $. <= $end && $exclude =~ "FALSE"){
            print $_;
        }
    }
    close FILE;
    $end = ++$test if ( $end < $start );
    $start = $next if ($next);
}


#Lets print the last letter in the file

my $exclude = "FALSE";

open(FILE, "<", "$filename") || error("Cannot open file ($!)");
while (<FILE>)
{
    my $line = $_;
    if ( $. == $start && $line =~ /^P\|[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]\|1I\|IR\|/)
    {
        $exclude = "TRUECANC";
        next;
    }
    if ( $. >= $start && $. <= $end && $exclude =~ "TRUECANC")
    {
        print OUTCANC "$line";
    } elsif ( $. >= $start && $. <= $end && $exclude =~ "FALSE"){
        print $_;
    }
}
close FILE;
close OUTCANC;


#----------------------------------------------------------------

sub message
{
    my $m = shift or return;
    print("$m\n");
}

sub error
{
    my $e = shift || 'unknown error';
    print("$0: $e\n");
    exit 0;
}

1 个答案:

答案 0 :(得分:2)

有些东西可以加快脚本速度,比如删除不必要的正则表达式使用。

  • /^P\|/相当于"P|" eq substr $_, 0, 2
  • $foo =~ "BAR"可以是-1 != index $foo, "BAR"

然后有一些重复的代码。将其分解为sub不会提高性能本身,但可以更容易推断脚本的行为。

有很多不必要的字符串,例如"$filename" - $filename就足够了。

但最糟糕的罪犯就是这样:

for ( 1 .. $loop ) {
  ...
  open FILE, "<", $filename or ...
  while (<FILE>) {
    ...
  }
  ...
}

您只需要在一次中读取该文件,最好是在数组中读取。你可以循环索引:

for ( 1 .. $loop ) {
  ...
  for my $i (0 .. $#file_contents) {
    my $line = $file_contents[$i];
    ... # swap $. for $i, but avoid off-by-one error
  }
  ...
}

磁盘IO ,因此请尽可能缓存!

我还看到您使用$exclude变量作为布尔值,其值为FALSETRUECANC。为什么不01,所以您可以直接在条件中使用它?

您可以在if / elsif中分解常见测试:

if    (FOO && BAR) { THING_A }
elsif (FOO && BAZ) { THING_B }

应该是

if (FOO) {
    if    (BAR) { THING_A }
    elsif (BAZ) { THING_B }
}

$. == $start && $line =~ /^P\|.../测试可能很愚蠢,因为$start只包含以P|开头的行数 - 所以这里的正则表达式就足够了。

修改

如果我已正确理解了脚本,那么以下内容应该会显着提高性能:

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

my ($filename, $cancfile) = @ARGV;
open my $fh, "<", $filename or die "$0: Couldn't open $filename: $!";

my (@num, @lines);
while (<$fh>)
{
    push @lines, $_;
    push @num, $#lines if "P|" eq substr $_, 0, 2;
}

open my $outcanc, ">>", $cancfile or die "$0: Couldn't open $cancfile: $!";

for my $i ( 0 .. $#num )
{
    my $start = $num[$i];
    my $end   = ($num[$i+1] // @lines) - 1;
    # pre v5.10:
    # my $end = (defined $num[$i+1] ? $num[$i+1] : @lines) - 1

    if ($lines[$start] =~ /^P[|][0-9]{9}[|]1I[|]IR[|]/) {
        print {$outcanc} @lines[$start .. $end];
    } else {
        print STDOUT     @lines[$start .. $end];
    }
}

清理脚本。该文件缓存在一个数组中。只迭代实际需要的数组部分 - 我们从前面的 O(n·m)开始 O(n)

对于你未来的脚本:证明循环和变异变量的行为并非不可能,但是单调乏味且烦人。意识到

for (1 .. @num) {
  $start = shift @num unless $next;  # aka "do this only in the first iteration"
  $next = shift @num:
  $end = $next - 1:
  while (<FH>) {
    ...
    $test = $. if eof
    ...
  }
  $end = ++test if $end < $start;
  $start = $next if $next;
}

实际上是关于绕过第二个undef中可能的shift需要一些时间。我们可以在循环之后选择行号,而不是在内循环中测试eof,因此我们不需要$test。然后我们得到:

$start = shift @num;
for my $i (1 .. @num) {
  $end = $num[$i] - 1:

  while (<FH>) { ... }

  $end = $. + 1 if $end < $start;  # $end < $start only true if not defined $num[$i]
  $start = $num[$i] if $num[$i];
}

$i向下翻译为1后,我们将界外问题仅限于一点:

for my $i (0 .. $#num) {
  $start = $num[$i];
  $end = $num[$i+1] - 1; # HERE: $end = -1 if $i == $#num

  while (<FH>) { ... }
}
$end = $. + 1 if $end < $start;

用数组替换文件读取后(注意,数组索引和行号之间存在差异),我们看到如果我们将该迭代拉入{{{},则可以避免最终文件读取循环。 1}}循环,因为我们知道总共有多少行。所以说,我们做了

for

希望我的清理代码确实等同于原始代码。