PERL:跳到一个巨大的文本文件中的行

时间:2018-08-12 18:57:27

标签: perl bigdata

我有一个非常大的文本文件(〜4 GB)。 它具有以下结构:

S=1
3 lines of metadata of block where S=1
a number of lines of data of this block
S=2
3 lines of metadata of block where S=2
a number of lines of data of this block
S=4
3 lines of metadata of block where S=4
a number of lines of data of this block
etc.

我正在编写一个PERL程序,该程序可以读取另一个文件, 该文件的foreach行(其中必须包含数字), 在大文件中搜索该数字减去1的S值 然后分析属于该S值的块的数据行。

问题是,文本文件很大,所以用a处理每行

foreach $line {...} loop

非常慢。随着S = value的严格增加,是否有任何方法可以跳转到所需S值的特定行?

5 个答案:

答案 0 :(得分:9)

  

是否有任何方法可以跳转到所需S值的特定行?

是的,如果文件没有更改,则创建一个索引。这要求完整读取文件一次,并使用tell记录所有S=#行的位置。 Store it in a DBM file,键为数字,值为文件中的字节位置。然后,您可以使用seek to jump to that point in the file and read from there

但是,如果要这样做,最好将数据导出到适当的数据库中,例如SQLite。编写程序以将数据插入数据库并添加普通的SQL索引。这可能比编写索引要简单。然后,您可以使用常规SQL有效地查询数据,并进行复杂的查询。如果文件更改,则可以重做导出,也可以使用常规的insertupdate SQL更新数据库。相对于一堆自定义索引和搜索代码,对于任何了解SQL的人来说,这将很容易。

答案 1 :(得分:2)

如果文本块的长度相同(以字节或字符为单位),则可以计算所需的S值在文件中的位置,然后seek在文件中进行读取。否则,原则上您需要读取行以找到S值。

但是,如果仅找到几个S值,则可以估计所需的位置,然后seek在那里估计,那么read足以捕获S值。然后分析所读取的内容以查看距离有多远,然后再次seek或使用<>读取行以获取S值。

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

use Fcntl qw(:seek);

my ($file, $s_target) = @ARGV;
die "Usage: $0 filename\n" if not $file or not -f $file;
$s_target //= 5;  #/ default, S=5

open my $fh, '<', $file or die $!; 

my $est_text_len = 1024;
my $jump_by      = $est_text_len * $s_target;  # to seek forward in file

my ($buff, $found);

seek $fh, $jump_by, SEEK_CUR;  # get in the vicinity

while (1) {

    my $rd = read $fh, $buff, $est_text_len;
    warn "error reading: $!" if not defined $rd;
    last if $rd == 0;

    while ($buff =~ /S=([0-9]+)/g) {
        my $s_val = $1;

        # Analyze $s_val and $buff:
        # (1) if overshot $s_target adjust $jump_by and seek back
        # (2) if in front of $s_target read with <> to get to it
        # (3) if $s_target is in $buff extract needed text

        if ($s_val == $s_target) {
            say "--> Found S=$s_val at pos ", pos $buff, " in buffer";
            seek $fh, - $est_text_len + pos($buff) + 1, SEEK_CUR;
            while (<$fh>) {
                last if /S=[0-9]+/;  # next block
                print $_;
            }
            $found = 1;
            last;
        }
    }   
    last if $found;
}

用您的样本进行测试,放大和清理(在文本中更改S=n,与条件相同!),将$est_text_len$jump_by设置为100和20。

这是草图。完整的实现需要协商过度和寻求不足,如代码注释中所述。如果文本块大小变化不大,则可以在两次查找中获得所需的S值,然后使用<>进行读取,或像示例中一样使用regex。

一些评论

  • 上面概述的“分析”需要仔细进行。首先,一个缓冲区可能包含多条S值行。另外,请注意,如果S值不在缓冲区中,代码将继续读取。

  • 距离足够近并且在$s_target前面读<>即可到达行。

  • read可能无法获得所需的数量,因此您应该将其真正放入循环中。有最近的帖子。

  • read更改为sysread以提高效率。在这种情况下,请使用sysseek,并且不要与<>(已缓冲)混合使用。

  • 上面的代码假定要找到一个S值;调整更多。绝对假设S值已排序。

这显然比读取行要复杂得多,但是它的运行速度却要快得多,文件非常大,只有几个S值可以找到。如果有很多值,则可能无济于事。


问题中指出的foreach (<$fh>)将导致首先读取整个文件(以建立foreach通过的列表);改用while (<$fh>)


如果文件没有更改(或者需要多次搜索同一文件),则可以先对其进行处理一次,以建立S值精确位置的索引。感谢Danny_ds发表评论。

答案 2 :(得分:2)

我知道操作员已经接受了答案,但是对我有用的一种方法是,根据更改“记录分隔符”($ /),将文件插入数组。

如果您执行这样的操作(未经测试,但是应该关闭):

$/ = "S=";
my @records=<fh>;
print $records[4];

输出应该是完整的第五条记录(数组从0开始,但是数据从1开始),从记录编号(5)单独一行开始(您可能需要稍后将其删除),在该记录中其余所有行之后。

它非常简单,快速,尽管它是一个记忆猪...

答案 3 :(得分:1)

二进制排序列表的搜索是O(log N)操作。使用seek这样的事情:

open my $fh, '>>+', $big_file;
$target = 123_456_789;

$low = 0;
$high = -s $big_file;

while ($high - $low > 0.01 * -s $big_file) {
    $mid = ($low + $high) / 2;
    seek $fh, $mid, 0;
    while (<$fh>) {
        if (/^S=(\d+)/) {
            if ($1 < $target) { $low = $mid; }
            else              { $high = $mid }
            last;
        }
    }
}

seek $fh, $low, 0;
while (<$fh>) {
    # now you are searching through the 1% of the file that contains
    # your target S
}

答案 4 :(得分:0)

对第二个文件中的数字进行排序。现在,您可以按顺序处理大文件,并根据需要处理每个S值。