use*_*390 5 sed awk perl text-processing
我有一个包含 800 亿行的大文件。现在我想提取几行(大约 10000 行),我知道行号,处理它的最快方法是什么。
是否可以使用包含行号的另一个文件来提取这些行?行号文件中的行号并不总是连续的。
比如原文件是:
0.1
0.2
0.3
0.4
...
Run Code Online (Sandbox Code Playgroud)
行号文件:
1
3
4
Run Code Online (Sandbox Code Playgroud)
输出:
0.1
0.3
0.4
Run Code Online (Sandbox Code Playgroud)
这里有一种替代方法和一些基准测试,并在 Weijun Zhou 的回答中进行了补充。
join
假设您有一个data
要从中提取行的文件和一个列出line_numbers
要提取的行数的文件,如果输出的排序顺序不重要,您可以使用:
join <(sort padded_line_numbers) <(nl -w 12 -n rz data) | cut -d ' ' -f 2-
Run Code Online (Sandbox Code Playgroud)
这将对文件的行进行编号data
,将其与padded_line_numbers
第一个字段(默认)上的文件连接起来,并打印出公共行(不包括被切除的连接字段本身)。
join
需要输入文件按字母顺序排序。上述padded_line_numbers
文件必须通过左填充line_numbers
文件的每一行来准备。例如:
while read rownum; do
printf '%.12d\n' "$rownum"
done <line_numbers >padded_line_numbers
Run Code Online (Sandbox Code Playgroud)
选项-w 12 -n rz
和参数指示nl
输出带前导零的 12 位长数字。
如果输出的排序顺序必须与line_numbers
文件的排序顺序匹配,您可以使用:
join -1 2 -2 1 <(nl padded_line_numbers | sort -k 2,2) \
<(nl -w 12 -n rz data) |
sort -k 2,2n |
cut -d ' ' -f 3-
Run Code Online (Sandbox Code Playgroud)
我们对padded_line_numbers
文件进行编号,按第二个字段的字母顺序对结果进行排序,将其与编号的data
文件连接起来,并按 的原始排序顺序对结果进行数字排序padded_line_numbers
。
为了方便起见,这里使用过程替换。如果您不能或不想依赖它,并且您可能不愿意浪费创建常规文件来保存中间结果所需的存储空间,则可以利用命名管道:
mkfifo padded_line_numbers
mkfifo numbered_data
while read rownum; do
printf '%.12d\n' "$rownum"
done <line_numbers | nl | sort -k 2,2 >padded_line_numbers &
nl -w 12 -n rz data >numbered_data &
join -1 2 -2 1 padded_line_numbers numbered_data | sort -k 2,2n | cut -d ' ' -f 3-
Run Code Online (Sandbox Code Playgroud)
由于您问题的特殊性是文件中的行数data
,因此我认为使用相当数量的数据测试替代方法可能很有用。
在我的测试中,我使用了 32 亿行的数据文件。每行只是来自 的 2 个字节的垃圾openssl enc
,使用 和 进行十六进制编码od -An -tx1 -w2
,并使用 删除空格tr -d ' '
:
$ head -n 3 data
c15d
061d
5787
$ wc -l data
3221254963 data
Run Code Online (Sandbox Code Playgroud)
该line_numbers
文件是通过使用shuf
GNU Coreutils 随机选择 1 到 3,221,254,963 之间的 10,000 个数字(不重复)创建的:
shuf -i 1-"$(wc -l <data)" -n 10000 >line_numbers
Run Code Online (Sandbox Code Playgroud)
测试环境是一台笔记本电脑,配备 i7-2670QM Intel 四核处理器、16 GiB 内存、SSD 存储、GNU/Linux、bash
5.0 和 GNU 工具。
我测量的唯一维度是通过time
shell 内置函数执行的时间。
这里我考虑的是:
sed
来自周伟军的回答。awk
给出了解决方案。perl
来自wurtel 的回答。join
如上。perl
似乎是最快的:
$ time perl_script line_numbers data | wc -l
10000
real 14m51.597s
user 14m41.878s
sys 0m9.299s
Run Code Online (Sandbox Code Playgroud)
awk
的性能看起来相当:
$ time awk 'FNR==NR { seen[$0]++ }; FNR!=NR && FNR in seen' line_numbers data | wc -l
10000
real 29m3.808s
user 28m52.616s
sys 0m10.709s
Run Code Online (Sandbox Code Playgroud)
join
似乎也具有可比性:
$ time join <(sort padded_line_numbers) <(nl -w 12 -n rz data) | wc -l
10000
real 28m24.053s
user 27m52.857s
sys 0m28.958s
Run Code Online (Sandbox Code Playgroud)
请注意,上面提到的排序版本与此版本相比几乎没有性能损失。
最后,sed
似乎明显慢了:我在大约九小时后杀死了它:
$ time sed -nf <(sed 's/$/p/' line_numbers) data | wc -l
^C
real 551m12.747s
user 550m53.390s
sys 0m15.624s
Run Code Online (Sandbox Code Playgroud)
我会为此使用 perl 脚本。我想出了这个:
#!/usr/bin/perl
# usage: thisscript linenumberslist.txt contentsfile
unless (open(IN, $ARGV[0])) {
die "Can't open list of line numbers file '$ARGV[0]'\n";
}
my %linenumbers = ();
while (<IN>) {
chomp;
$linenumbers{$_} = 1;
}
unless (open(IN, $ARGV[1])) {
die "Can't open contents file '$ARGV[1]'\n";
}
$. = 0;
while (<IN>) {
print if defined $linenumbers{$.};
}
exit;
Run Code Online (Sandbox Code Playgroud)
首先将我们感兴趣的行号列表读入关联数组,其中行号是键。chomp
删除行尾的换行符,$_
即行本身。
接下来打开数据文件,当行号是行号数组中的现有键时,则打印该行。
这$.
是 perl 的行号计数器,每读取一行就会递增。由于这是跨文件计数的,因此在读取数据文件的任何行之前我将其重置为零。
这可能可以用“perl”风格写得更多,但我更喜欢让它更具可读性。
如果您要提取的行列表非常大,这可能不是最有效的方法,但我发现 perl 在这些事情上通常非常高效。
如果您需要按照列出的顺序(即不按顺序)提取行,那么它会变得更加复杂......