首页 > 解决方案 > 如何在文件中搜索包含 Perl 中关键字的最后一个连续行块

问题描述

想象一个像下面这样的文本文件,其中 <some random text> 可以是 any 或nothing,这意味着 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>

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

标签: perl

解决方案


记录这些带有图案的线条序列,始终保留最后一组,一旦文件出来,您将拥有最后一组。

直截了当的方法

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;

(删除@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, $_
        }
    }
    

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

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

如果一个文件可能很大——这是唯一要做的事情——我们可以使用相同的代码,但从文件末尾向后退。一个模块是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;

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

我将测试文件附加了 200k 次,文件大小为 111 Mb。第一个程序(在注释中进行了调整)使用 ~ 1.85 sec(几次运行的平均值),而上面的程序进入0.02 sec. †</sup>

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


†</sup> 这适用于整个程序、启动和所有程序,time在调用程序时在命令行上测量,并在几次运行中取平均值。

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

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

  • 在第一个程序中,它当然还是1.8881 sec这样的


推荐阅读