如何在文件中搜索包含 Perl 关键字的最后一块连续行

hw_*_*_sw 3 perl

想象一个像下面这样的文本文件,其中 <some random text> 可以是任何东西或什么没有,这意味着 KEYWORD 可以出现在行中的任何地方,单独或与其他文本一起出现:

 1 <some random text>
 2 <some random text>KEYWORD<some random text>
 3 <some random text>KEYWORD<some random text>
 4 <some random text>
 5 <some random text>
 6 <some random text>KEYWORD<some random text>
 7 <some random text>
 8 <some random text>KEYWORD<some random text>
 9 <some random text>KEYWORD<some random text>
10 <some random text>KEYWORD<some random text>
11 <some random text>
12 <some random text>KEYWORD<some random text>
13 <some random text>KEYWORD<some random text>
14 <some random text>
15 <some random text>KEYWORD<some random text>
16 <some random text>
Run Code Online (Sandbox Code Playgroud)

如何获取包含关键字2 个或更多连续行的最后一次出现(示例中的第 12 行和第 13 行)?明确地说,我对第 (8, 9, 10) 行感兴趣,因为虽然它们包含关键字并且是连续的,但它们不是最后一行,也不是第 15 行,因为虽然它包含关键字并且是最后一行带有关键字,它不是 2 个或更多连续行的一部分。

zdi*_*dim 6

记录这些带有模式的行序列,始终保留最后一组,一旦文件输出,您将拥有最后一组。

直截了当的方式

use warnings;
use strict;
use feature 'say';

die "Usage: $0 file(s)\n"  if not @ARGV;

my $threshold = 2;

my (@buf, $cnt, @res);

while (<>) {
    if (not /KEYWORD/) {
        $cnt = 0  if $cnt;
        @buf = () if @buf;
        next 
    }   

    ++$cnt;
    push @buf, $_; 

    if ($cnt >= $threshold) {
        @res = @buf;  # excessive copying; refine if a problem
    }
}
print for @res;
Run Code Online (Sandbox Code Playgroud)

(删除@ARGV检查以允许STDIN输入,它<>在没有给出文件的情况下读取。)

笔记

  • 行进入缓冲区,直到满足阈值条件(重复次数),并且计数器增加。在没有图案的线上,这些被重置

  • 这只是一次在这里(只需要两个重复的行),所以它会更容易为以后处理的行复制到一个标量,以保存它,但使用任何阈值数组作品

  • 一旦满足条件,就复制缓冲区。虽然需要对与阈值匹配的第一行执行此操作,但要覆盖@res之前的行,以下重复行不需要复制整个数组 - 可以在阈值通过后添加该行。

    这需要额外的踢踏舞;这是一种方法(经过最低限度的测试)

    while (<>) {
        if (not /KEYWORD/) {
            $cnt = 0  if $cnt;
            @buf = () if @buf;
            next
        }
        ++$cnt;
    
        if ($cnt < $threshold) {
            push @buf, $_;
        }
        elsif ($cnt == $threshold) {
            @res = (@buf, $_);
        }
        else {
            push @res, $_
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    现在缓冲区在第一次添加到大于阈值的计数中时被复制,但在没有额外缓冲区副本的情况下添加了以下行。(如果这样的行序列非常少见,或者文件很小,这不会有明显的影响。)

如果您需要知道文件中的位置,这些将保存行号$.以及行。

如果文件可能很大——这是唯一可以用它做的事情——我们可以使用相同的代码,但从文件末尾向后。一个模块是File::ReadBackwards


为了说明增益,这里有一个程序通过向后读取文件来做同样的事情

use warnings;
use strict;
use feature 'say';

use File::ReadBackwards;

my (@buf, $cnt, @res);
my $threshold = 2;

my $bw = File::ReadBackwards->new(shift) or die $!;     
#print $bw->readline until $bw->eof; exit;  # test

while ( my $line = $bw->readline ) {     
    if (not $line =~ /KEYWORD/) {    
        last if @res >= $threshold;
        $cnt = 0  if $cnt;
        @buf = () if @buf;
        next 
    }   
    ++$cnt;

    if ($cnt  < $threshold) { 
        push @buf, $line;
    }   
    elsif ($cnt == $threshold) { 
        @res = (@buf, $line);
    }   
    else { 
        push @res, $line;
    }
}    
print for reverse @res;
Run Code Online (Sandbox Code Playgroud)

这将产生与从头开始读取的程序相同的输出。

我附加了 20 万次测试文件,文件大小为 111 Mb。第一个程序(按照注释进行调整)将 ~1.85 sec放在上面(运行几次的平均值),而上面的程序则输入0.02 sec.

因此,对于足够大的文件,节省是甜蜜的;从后面阅读的小开销是完全看不见的。但是,在此过程中无法进行其他处理。此外,目标必须是可搜索的(文件),并且支持的操作很少;一方面,我们没有得到行号。


这是针对整个程序、启动和所有程序,time在调用程序时在命令行上测量,并在几次运行中取平均值。

当我只对代码本身计时,使用Time::HiRes时,处理文件的运行时是

  • 在在第二程序第四(第4次)的小数位,例如0.0003 sec

  • 在第一个程序中,它当然是静止的1.8881 sec或类似的