我有一个非常大的文本文件(〜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值的特定行?
答案 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有效地查询数据,并进行复杂的查询。如果文件更改,则可以重做导出,也可以使用常规的insert
和update
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
可能无法获得所需的数量,因此您应该将其真正放入循环中。有最近的帖子。
上面的代码假定要找到一个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值。