【问题标题】:Multiple patterns match in a big file and track the counts of each pattern matched in perl多个模式匹配一​​个大文件并跟踪 perl 中匹配的每个模式的计数
【发布时间】:2017-11-28 12:18:05
【问题描述】:

我有一个@pat_array,它有用户给定的模式并包含特殊字符。比如Error 78?(Not available) 77% 等。

我需要匹配 1GB+ 日志文件中的模式,从特定的行号到末尾。我还需要记录每个模式被找到的次数。下面的代码可以工作,但耗时太长:3 种模式接近 2 分钟。

我正在考虑一种方法来避免额外的 for 循环,同时匹配模式并一次性完成。 (这里对于 3 种模式,我将 $_ 与 pat_array 中的三种不同模式进行匹配,如您所见)。

my @pat_array = split( '@@@', $InListOfPatterns );
my $num_pat   = @pat_array;

my @match_count;
for ( my $i = 0; $i < $num_pat; $i = $i + 1 ) {
    $match_count[$i] = 0;
}

open LOG_READ, '<', "$InLogFilePath" || die "can not open file :$!";

while ( <LOG_READ> ) {
    chomp;

    if ( $. > $InStartLineNumber ) {

        for ( my $j = 0; $j < $num_pat; $j = $j + 1 ) {

            if ( $_ =~ m/\Q$pat_array[$j]\E/ ) {
                $match_count[$j] = ( $match_count[$j] + 1 );
            }
        }
    }
}

close(LOG_READ);

【问题讨论】:

  • 您的问题更适合Code Review。你已经有了工作代码并想要改进它。无论如何,请edit 并包含几行完整的日志以进行测试,并向我们展示模式列表。如果您不想透露该信息,请编写一个新的完整程序,其中包含显示相同问题的示例数据,并将其包含在内。这称为minimal reproducible example
  • 当你帮我解决新的代码/想法时。“当”的意思是
  • for ( my $i = 0; $i &lt; $num_pat; $i = $i + 1 ) { ... } 通常写成for my $i ( 0; $i ... $num_pat-1 ) { ... }for my $i ( 0; $i ... $#pat_array ) { ... }
  • “但我在想一种方法来吃面糊,所以一天天变胖一点”

标签: perl file-handling


【解决方案1】:

此答案将教您如何分析您的脚本,以了解其缓慢的原因和位置。它将使用 Perl 分析器 Devel::NYTProf,您必须从 CPAN 安装它。

在您自己执行此操作之前,请观看作者的this talk 了解分析。重要的是要知道这应该只在极少数情况下进行。你就是这样。

首先,使用以下命令创建测试输入,使用the JSON API of baconipsum.com

$ curl -s \
  'https://baconipsum.com/api/?type=meat-and-filler&format=text&sentences=100' \
  | perl -nE 'for $i ( 1 .. 10_000 ) { say for map lc, split /\. / }' >log.txt

此文件大小约为 70MB,有 100 万行。这足以进行测试。

$ ls -lah
-rw-rw-r--  1 simbabque simbabque 69M Nov 28 13:36 log.txt
$ tail log.txt
Nisi magna pig pastrami, in chicken elit meatball
Consequat laborum rump kevin beef ham hock proident tempor ex strip steak
Shankle kielbasa in nulla
Consectetur picanha pork belly, drumstick tail tempor alcatra pariatur eiusmod
Tongue tail meatloaf cupim ut do sed, cillum kevin id ex dolore t-bone
Ut cow nulla brisket ball tip ipsum ham strip steak culpa cillum
Doner chicken sint duis in, andouille labore eiusmod
Bacon tempor nostrud, short loin occaecat cow nulla ipsum strip steak pastrami corned beef turducken
Ball tip labore chicken pancetta cupim
Ham leberkas pastrami, exercitation id porchetta tri-tip beef voluptate shoulder ipsum meatloaf sunt ea.

接下来,我们准备您的脚本。我进行了一些更改以使其更现代 Perl,例如三参数 open 和词法文件句柄。

$ cat patterns.pl
use strict;
use warnings;
use Data::Dumper;

my $InListOfPatterns = 'bacon@@@loin@@@steak';
my $InStartLineNumber = 2;

my @pat_array = split( '@@@', $InListOfPatterns );
my $num_pat = @pat_array;
my @match_count;
for ( my $i = 0; $i < $num_pat; $i = $i + 1 ) {
    $match_count[$i] = 0;
}

open my $fh,'<','log.txt' or die "can not open file :$!";
while (<$fh>) {
    chomp;
    if ( $. > $InStartLineNumber ) {
        for ( my $j = 0; $j < $num_pat; $j = $j + 1 ) {
            if ( $_ =~ m/\Q$pat_array[$j]\E/ ) {
                $match_count[$j] = ( $match_count[$j] + 1 );
            }
        }
    }
}

print Dumper \@match_count;

这在我的机器上大约需要 6 秒,并且会在最后打印每个模式的匹配数。

现在让我们看看我们如何使用Devel::NYTProf 对此进行分析。你只需要运行这个命令。 -d 标志告诉 Perl 使用调试器接口,:NYTProf 表示使用 Devel::NYTProf 调试器。

$ perl -d:NYTProf patterns.pl 
$VAR1 = [
          20000,
          300000,
          90000
        ];

现在您的目录中有一个名为 nytprof.out 的文件。

$ nytprofhtml --no-flame --open
Reading nytprof.out
Processing nytprof.out data
Writing line reports to nytprof directory
 100% ... 

它将在您现有的浏览器中打开一个浏览器窗口或一个新选项卡,并显示如下内容:

我们想转到 patterns.plline 报告。红线是 NYTProf 认为非常慢的线。

最明显的是第 17 行中的 chomp。即使在被丢弃的行上也会调用它。当然,在我们的示例中它只跳过了一行,但在您的情况下可能会更多。将 chomp 移到 if 之后。

我们还可以看到,最重要的时间花在if 上。作为chorboa says in his answer on your cross-posted Perlmonks question,您可以使用带有命名捕获组的单一模式。我将分两步演示这一点,以便您了解他为什么这样做。

use strict;
use warnings;
use Data::Dumper;

my $InListOfPatterns  = 'bacon@@@loin@@@steak';
my $InStartLineNumber = 2;

my @pat_array = split( '@@@', $InListOfPatterns );

open my $fh, '<', 'log.txt' or die "can not open file :$!";
my %matched;
while (<$fh>) {
    if ( $. > $InStartLineNumber ) {
        chomp;

        # these two are inside the loop, which is bad
        my $i;
        my $regex = join '|', map +( $i++, "(?<m$i>$_)" )[1], map quotemeta, @pat_array;

        $matched{ (grep defined $-{$_}[0], keys %-)[0] }++ if /$regex/;
    }
}

print Dumper \%matched;

让我们重新运行分析器,并检查结果。现在需要更长的时间,因为它在循环内部执行了更复杂的操作。这很糟糕。

它在第 18 行的那个循环中花费了将近 2 秒,为近 100 万行中的每一行重新编译相同的模式。

所以你显然想把它移出循环,就像 choroba 在他的帖子中所说的那样。

use strict;
use warnings;
use Data::Dumper;

my $InListOfPatterns  = 'bacon@@@loin@@@steak';
my $InStartLineNumber = 2;

my @pat_array = split( '@@@', $InListOfPatterns );
my $i;
my $regex = join '|', map +( $i++, "(?<m$i>$_)" )[1], map quotemeta, @pat_array;

open my $fh, '<', 'log.txt' or die "can not open file :$!";
my %matched;
while (<$fh>) {
    if ( $. > $InStartLineNumber ) {
        chomp;

        $matched{ (grep defined $-{$_}[0], keys %-)[0] }++ if /$regex/;
    }
}

print Dumper \%matched;

如果我们通过分析器重新运行它,它会报告这个。

模式生成的调用现在只调用一次。这明显更快。

不幸的是,总体胜率不是很高,至少在这个文件大小的情况下。原始代码中 for 循环的时间为 12.4 秒 + 1.1 秒,现在为 12.2。只有 100 万行,这 1.3 秒并不重要。但是你的文件可能会有更多的行,并且总体上会变得更快,特别是如果你添加更多可能的模式。

如果我们将模式增加到五个,那么新实现将是 23.6s,而原来的实现是 1.78s + 23.6s。那是1.78s的差异。

在循环中使用一个匹配而不是三个匹配的好处非常明显,但是要找出匹配哪个模式的捕获组是有代价的,并且每次创建查找哈希的命名捕获组更加昂贵。

如果我们将其与解决方案 in Sobrique's answer 进行比较,我们得到 3.69 秒,而原来的 1.78 秒 + 23.6 秒。这种差异现在几乎是一个数量级,非常显着。要获得带有序号的模式,您必须在循环之外编写一两行附加代码,这可以忽略不计。


请注意,所有测量值因机器而异,并且还会受到同时运行的其他进程的影响。在您的计算机上,它们可能完全不同。基准测试很难,而且通常不是很准确。

【讨论】:

    【解决方案2】:

    之所以需要这么长时间,是因为循环嵌套。您正在阅读模式列表,然后为每一行尝试每个模式。

    这是非常低效的。

    这样的事情会做同样的事情:

    use Data::Dumper;
    
    my @pat_array = split( '@@@', $InListOfPatterns );
    
    my $match_regex = join '|', map { quotemeta } @pat_array;
       $match_regex = qr/($match_regex)/; 
    
    print "Using match regex of: ", $match_regex,"\n";
    
    my %count_of;    
    open my $log_read, '<', $InLogFilePath or die "can not open file :$!";
    while (<$log_read>) {
       next unless $. > $InStartLineNumber; 
       chomp;
       m/$match_regex/ && $count_of{$1}++; 
    }
    close($log_read);
    
    print Dumper \%count_of;
    

    它遵循单个捕获正则表达式,每行运行一次,捕获%count_of 哈希中的匹配项。但它适用于 捕获的结果 而不是正则表达式(与您的示例不同)。考虑到我们quotemeta(它与\Q\E 类似地给出文字模式),这不应该是重要的。

    另外 - 你的 open 行有一个错误 - || 的优先级太高,所以它不起作用。您的代码功能如下:

    open LOG_READ, '<', ("$InLogFilePath" || die "can not open file :$!");
    

    这意味着die 将在 - 且仅当 - $InLogFilePath 为假(未定义或为空)时发生。

    改用or,你就没有这个问题。或者添加括号。

    【讨论】:

    • 这很有趣。您的解决方案比 Perl Monks 上的 choroba 解决方案快 8 倍。但是你不允许像他那样使用模式的索引。
    • 那些\b 不起作用,因为它假定模式以单词字符开头和结尾,但至少有一个模式 ((Not available) 77%) 以非单词开头和结尾。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-01-07
    • 1970-01-01
    • 2020-08-04
    • 1970-01-01
    • 1970-01-01
    • 2011-10-20
    • 2013-03-09
    相关资源
    最近更新 更多