在 Bash 中从另一个较大文件中查找文件行的最快方法
Posted
技术标签:
【中文标题】在 Bash 中从另一个较大文件中查找文件行的最快方法【英文标题】:Fastest way to find lines of a file from another larger file in Bash 【发布时间】:2017-07-03 12:17:01 【问题描述】:我有两个文件,file1.txt
和 file2.txt
。 file1.txt
有大约 14K 行,file2.txt
有大约 20 亿行。 file1.txt
每行有一个字段 f1
,而 file2.txt
有 3 个字段,f1
到 f3
,由 |
分隔。
我想查找来自file2.txt
的所有行,其中file1.txt
中的f1
与file2.txt
中的f2
匹配(或者如果我们不想花费额外时间拆分@ 的值,则可以在该行的任何位置) 987654338@).
file1.txt(约 14K 行,未排序):
foo1
foo2
...
bar1
bar2
...
file2.txt(大约 20 亿行,未排序):
date1|foo1|number1
date2|foo2|number2
...
date1|bar1|number1
date2|bar2|number2
...
预期输出:
date1|foo1|number1
date2|foo2|number2
...
date1|bar1|number1
date2|bar2|number2
...
这是我尝试过的,它似乎需要几个小时才能运行:
fgrep -F -f file1.txt file2.txt > file.matched
我想知道是否有更好更快的方法来使用常见的 Unix 命令或使用小脚本来执行此操作。
【问题讨论】:
您可以根据您的实际输入告诉我们以下解决方案的时间安排。 数据是否以某种方式排序?是否有基于C
的解决方案?
查看Fastest possible grep接受的解决方案
我有点困惑。您说您可以完全匹配第 2 个字段或在线上的任何位置,但是第一个解决方案(完全匹配第 2 个字段)通常显然更具限制性。例如,给定单词foo1
,第一个解决方案将不匹配foo1|date|number1
,而第二个解决方案(匹配行中的任何位置)将接受它作为匹配项。那么您实际上将使用哪种方法来解决您的问题?
@codeforester 如果您可以将file1.txt
上传到例如pastebin.com 并提供指向该文件的链接,那就太好了。然后我们可以尝试在至少一些真实数据上测试代码:)
【参考方案1】:
Perl 解决方案。 [参见下面的注意。]
对第一个文件使用哈希。当您逐行阅读大文件时,通过正则表达式(捕获||
之间的第一个模式)或split
(获取第二个单词)提取字段并打印exists
。它们的速度可能略有不同(时间)。正则表达式中不需要defined
检查,而split
使用//
(定义或)会短路。
use warnings;
use strict;
# If 'prog smallfile bigfile' is the preferred use
die "Usage: $0 smallfile bigfile\n" if @ARGV != 2;
my ($smallfile, $bigfile) = @ARGV;
open my $fh, '<', $smallfile or die "Can't open $smallfile: $!";
my %word = map chomp; $_ => 1 <$fh>;
open $fh, '<', $bigfile or die "Can't open $bigfile: $!";
while (<$fh>)
exists $word (/\|([^|]+)/)[0] && print;
# Or
#exists $word (split /\|/)[1] // '' && print;
close $fh;
避免if
分支并使用短路会更快,但很少。在数十亿行中,这些调整加起来,但又不会太多。逐行读取小文件可能会(也可能不会)快一点,而不是像上面那样在列表上下文中,但这应该不引起注意。
更新 写入STDOUT
可以节省两个操作,我反复计时以比写入文件快一点。这种用法也与大多数 UNIX 工具一致,所以我改写为STDOUT
。接下来,不需要exists
测试,删除它可以避免操作。但是,我始终可以通过它获得更好的运行时,同时它也更好地传达了目的。总而言之,我把它留在里面。感谢ikegami cmets。
注意 注释掉的版本比另一个版本快 50%,根据我下面的基准。这些都是给出的,因为它们是不同的,一个找到第一个匹配项,另一个找到第二个字段。我把它作为一个更通用的选择保持这种方式,因为这个问题是模棱两可的。
一些比较(基准)[为写入STDOUT
而更新,请参阅上面的“更新”]
answer by HåkonHægland 中有广泛的分析,为大多数解决方案的运行计时。这是另一种方法,对上述两种解决方案、OP 自己的答案和发布的fgrep
进行基准测试,预计会很快并在问题和许多答案中使用。
我通过以下方式构建测试数据。几行大致如图所示的长度是用随机词组成的,对于两个文件,以便在第二个字段中匹配。然后我用不匹配的行填充数据样本的“种子”,以便模拟 OP 引用的大小和匹配之间的比率:对于小文件中的 14K 行,有 1.3M 行在大文件中,产生 126K 个匹配。然后重复编写这些样本以构建完整的数据文件作为 OP,每次都是 List::Util::shuffle
-ed。
下面比较的所有运行都会为上述文件大小生成106_120
匹配项(diff
-ed 进行检查),因此匹配频率足够接近。
它们通过使用my $res = timethese(60 ...)
调用完整程序来进行基准测试。 v5.16 上cmpthese($res)
的结果是
优化的 C 程序 fgrep
排在首位这一事实并不令人惊讶。 “regex”落后于“split”可能是由于启动引擎以进行少量匹配的开销,很多次。考虑到不断发展的正则表达式引擎优化,这可能因 Perl 版本而异。我包括了@codeforester ("cfor") 的答案,因为它被认为是最快的,并且它的24%
落后于非常相似的 "split" 可能是由于分散的小效率低下(请参阅此答案下方的评论)。†
这并没有太大的不同,但硬件和软件以及数据细节之间肯定存在差异。我在不同的 Perls 和机器上运行它,显着的区别是 在某些情况下 fgrep
确实快了一个数量级。
OP 的很慢fgrep
的体验令人惊讶。考虑到他们引用的运行时间,比上面的慢一个数量级,我猜有一个旧系统要“责备”。
尽管这完全是基于 I/O 的,但将它放在多个内核上会带来并发优势,而且我希望能有很好的加速,最高可达几倍。
† 唉,评论被删除了(?)。简而言之:不需要使用标量(成本)、if
分支、defined
、printf
而不是print
(慢!)。这些对于 20 亿行的运行时很重要。
【讨论】:
如果您将exists $word (/\|([^|]+)/)[0]
更改为$word (/\|([^|]+)/)[0]
,则可减少操作。没有理由不打印到 STDOUT,这会删除另外两个操作。
@ikegami 感谢 cmets - 他们涉及我不确定的事情。 (1) 如果没有exists
,它将获取(并返回)该值。我认为exists
(以某种方式)避免了这种情况(虽然它是一个函数调用)? (2) 打印“输出”和进行 shell 重定向是否没有开销?我过去曾比较(定时)这些,但从未发现直接写入文件更快,但我不明白。
@ikegami 无论如何,完整的比较:1.61/s(使用exists
,写入文件),1.62/s(使用exists
,写入标准输出,最好),1.56/ s(不带exists
,写入文件),1.57/s(不带exists
,写入标准输出)。看来(在这个系统的这个基准测试中)exists
有帮助(一点点),而直接写入比写入标准输出慢(我确实不了解这个)。
@zdim 我更新了我的测试套件以包含您使用短代码if
并打印到STDOUT
而不是打印到文件的最后更新,即我使用了这个:exists $word (/\|([^|]+)/)[0] && print
.然而,时间上几乎没有区别。对于我回答中的小案例,我仍然有 0.61 秒的运行时间,而对于大案例,运行时间仍然在 6.55 秒左右。请注意,脚本的输出仍使用 shell 重定向 (zdim.pl > out.txt
) 保存到文件中。
@HåkonHægland 好的,这很有道理——谢谢!正如文中解释的那样,它必须明显更好(但不是那么多)。虽然基于正则表达式的引擎确实落后于split
,但我想是因为启动正则表达式引擎对于所需的超级简单直接匹配来说是一个很大的开销,与split
所做的简单而简单的工作相比(打破空格并选择一个元素返回)。【参考方案2】:
我试图对这里介绍的一些方法进行比较。
首先,我创建了一个 Perl 脚本来生成输入文件 file1.txt
和 file2.txt
。为了比较一些解决方案,我确保来自file1.txt
的单词只能出现在file2.txt
的第二个字段中。为了能够使用@GeorgeVasiliou 提出的join
解决方案,我对file1.txt
和file2.txt
进行了排序。目前,我仅根据 75 个随机单词(取自 https://www.randomlists.com/random-words)生成输入文件。 file1.txt
中仅使用了这 75 个单词中的 5 个,其余 70 个单词用于填写 file2.txt
中的字段。可能有必要大幅增加字数以获得实际结果(根据 OP,原始 file1.txt
包含 14000 个字)。在下面的测试中,我使用了 file2.txt
和 1000000(100 万)行。该脚本还会生成@BOC的grep解决方案所需的文件regexp1.txt
。
gen_input_files.pl:
#! /usr/bin/env perl
use feature qw(say);
use strict;
use warnings;
use Data::Printer;
use Getopt::Long;
GetOptions ("num_lines=i" => \my $nlines )
or die("Error in command line arguments\n");
# Generated random words from site: https://www.randomlists.com/random-words
my $word_filename = 'words.txt'; # 75 random words
my $num_match_words = 5;
my $num_file2_lines = $nlines || 1_000_000;
my $file2_words_per_line = 3;
my $file2_match_field_no = 2;
my $file1_filename = 'file1.txt';
my $file2_filename = 'file2.txt';
my $file1_regex_fn = 'regexp1.txt';
say "generating $num_file2_lines lines..";
my ( $words1, $words2 ) = get_words( $word_filename, $num_match_words );
write_file1( $file1_filename, $words2 );
write_file2(
$file2_filename, $words1, $words2, $num_file2_lines,
$file2_words_per_line, $file2_match_field_no
);
write_BOC_regexp_file( $file1_regex_fn, $words2 );
sub write_BOC_regexp_file
my ( $fn, $words ) = @_;
open( my $fh, '>', $fn ) or die "Could not open file '$fn': $!";
print $fh '\\|' . (join "|", @$words) . '\\|';
close $fh;
sub write_file2
my ( $fn, $words1, $words2, $nlines, $words_per_line, $field_no ) = @_;
my $nwords1 = scalar @$words1;
my $nwords2 = scalar @$words2;
my @lines;
for (1..$nlines)
my @words_line;
my $key;
for (1..$words_per_line)
my $word;
if ( $_ != $field_no )
my $index = int (rand $nwords1);
$word = @ $words1 [$index];
else
my $index = int (rand($nwords1 + $nwords2) );
if ( $index < $nwords2 )
$word = @ $words2 [$index];
else
$word = @ $words1 [$index - $nwords2];
$key = $word;
push @words_line, $word;
push @lines, [$key, (join "|", @words_line)];
@lines = map $_->[1] sort $a->[0] cmp $b->[0] @lines;
open( my $fh, '>', $fn ) or die "Could not open file '$fn': $!";
print $fh (join "\n", @lines);
close $fh;
sub write_file1
my ( $fn, $words ) = @_;
open( my $fh, '>', $fn ) or die "Could not open file '$fn': $!";
print $fh (join "\n", sort @$words);
close $fh;
sub get_words
my ( $fn, $N ) = @_;
open( my $fh, '<', $fn ) or die "Could not open file '$fn': $!";
my @words = map chomp $_; $_ <$fh>;
close $fh;
my @words1 = @words[$N..$#words];
my @words2 = @words[0..($N - 1)];
return ( \@words1, \@words2 );
接下来,我创建了一个包含所有测试用例的子文件夹solutions
:
$ tree solutions/
solutions/
├── BOC1
│ ├── out.txt
│ └── run.sh
├── BOC2
│ ├── out.txt
│ └── run.sh
├── codeforester
│ ├── out.txt
│ ├── run.pl
│ └── run.sh
[...]
这里的文件 out.txt
是每个解决方案的 greps 的输出。脚本run.sh
运行给定测试用例的解决方案。
关于不同解决方案的说明
BOC1
:@BOC 提出的第一个解决方案
grep -E -f regexp1.txt file2.txt
BOC2
:@BOC 建议的第二种解决方案:
LC_ALL=C grep -E -f regexp1.txt file2.txt
codeforester
:@codeforester 接受的 Perl 解决方案(参见 source)
codeforester_orig
:@codeforested 提出的原始解决方案:
fgrep -f file1.txt file2.txt
dawg
:@dawg 提出的使用字典和分割线的 Python 解决方案(参见source)
gregory1
:@gregory 建议的使用 Gnu Parallel 的解决方案
parallel -k --pipepart -a file2.txt --block "$block_size" fgrep -F -f file1.txt
请参阅下面有关如何选择$block_size
的说明。
hakon1
:@HåkonHægland 提供的 Perl 解决方案(请参阅 source)。此解决方案需要在第一次运行代码时编译 c 扩展。当file1.txt
或file2.txt
发生变化时,它不需要重新编译。注意:在初始运行时用于编译 c-extension 的时间不包括在下面显示的运行时间中。
ikegami
:使用汇编正则表达式和使用@ikegami 给出的grep -P
的解决方案。注意:组装的正则表达式被写入单独的文件regexp_ikegami.txt
,因此生成正则表达式的运行时间不包括在下面的比较中。这是使用的代码:
regexp=$(< "regexp_ikegami.txt")
grep -P "$regexp" file2.txt
inian1
:@Inian 使用match()
的第一个解决方案
awk 'FNR==NR
hash[$1]; next
for (i in hash) if (match($0,i)) print; break
' file1.txt FS='|' file2.txt
inian2
:@Inian 使用 index()
的第二个解决方案
awk 'FNR==NR
hash[$1]; next
for (i in hash) if (index($0,i)) print; break
' file1.txt FS='|' file2.txt
inian3
:@Inian 仅检查 $2
字段的第三个解决方案:
awk 'FNR==NR
hash[$1]; next
$2 in hash' file1.txt FS='|' file2.txt
inian4
:@Inian 的第 4 次灵魂(与 codeforester_orig
和 LC_ALL
基本相同):
LC_ALL=C fgrep -f file1.txt file2.txt
inian5
:@Inian 的第 5 个解决方案(与 inian1
相同,但使用 LC_ALL
):
LC_ALL=C awk 'FNR==NR
hash[$1]; next
for (i in hash) if (match($0,i)) print; break
' file1.txt FS='|' file2.txt
inian6
:与inian3
相同,但使用LC_ALL=C
。感谢@GeorgeVasiliou 的建议。
jjoao
:由@JJoao 提议的编译的 flex 生成的 C 代码(参见 source )。注意:每次file1.txt
更改时都必须重新编译可执行文件。用于编译可执行文件的时间不包括在下面显示的运行时间中。
oliv
:@oliv 提供的 Python 脚本(见 source)
Vasiliou
:按照@GeorgeVasiliou 的建议使用join
:
join --nocheck-order -11 -22 -t'|' -o 2.1 2.2 2.3 file1.txt file2.txt
Vasiliou2
:与Vasiliou
相同,但使用LC_ALL=C
。
zdim
:使用@zdim 提供的Perl 脚本(参见source)。注意:这里使用正则表达式搜索版本(而不是分割线解决方案)。
zdim2
:与zdim
相同,只是它使用split
函数而不是正则表达式搜索file2.txt
中的字段。
注意事项
我对 Gnu 并行进行了一些试验(请参阅上面的 gregory1
解决方案)以确定我的 CPU 的最佳块大小。我有 4 个内核,目前看来最佳选择是将文件 (file2.txt
) 分成 4 个大小相等的块,并在 4 个处理器中的每一个上运行一个作业。这里可能需要更多测试。因此,对于file2.txt
为20M 的第一个测试用例,我将$block_size
设置为5M(参见上面的gregory1
解决方案),而对于下面介绍的更现实的情况,file2.txt
为268M,$block_size
为67M被使用了。
解决方案BOC1
、BOC2
、codeforester_orig
、inian1
、inian4
、inian5
和gregory1
都使用了松散匹配。这意味着来自file1.txt
的单词不必在file2.txt
的字段#2 中完全匹配。线上任何地方的匹配都被接受。由于这种行为使得将它们与其他方法进行比较变得更加困难,因此还引入了一些修改的方法。名为BOC1B
和BOC2B
的前两种方法使用了修改后的regexp1.txt
文件。原始regexp1.txt
中的行在\|foo1|foo2|...|fooN\|
形式上,它将匹配任何字段边界处的单词。修改后的文件regexp1b.txt
使用^[^|]*\|foo1|foo2|...|fooN\|
的形式将匹配锚定到字段#2。
然后其余的修改方法codeforester_origB
、inian1B
、inian4B
、inian5B
和gregory1B
使用了修改后的file1.txt
。修改后的文件 file1b.txt
不是每行一个 literal 单词,而是在表单上的每行使用一个 regex:
^[^|]*\|word1\|
^[^|]*\|word2\|
^[^|]*\|word3\|
[...]
此外,对于这些方法,fgrep -f
已替换为 grep -E -f
。
运行测试
这是用于运行所有测试的脚本。它使用 Bash time
命令记录每个脚本花费的时间。请注意,time
命令返回三个不同的时间调用real
、user
和sys
。首先我使用了user
+ sys
,但是在使用Gnu并行命令时意识到这是不正确的,所以下面报告的时间现在是time
返回的real
部分。有关time
返回的不同时间的更多信息,请参阅this question。
第一个测试使用包含 5 行的 file1.txt
和包含 1000000
行的 file2.txt
运行。这是run_all.pl
脚本的前52 行,脚本的其余部分可用here。
run_all.pl
#! /usr/bin/env perl
use feature qw(say);
use strict;
use warnings;
use Cwd;
use Getopt::Long;
use Data::Printer;
use FGB::Common;
use List::Util qw(max shuffle);
use Number::Bytes::Human qw(format_bytes);
use Sys::Info;
GetOptions (
"verbose" => \my $verbose,
"check" => \my $check,
"single-case=s" => \my $case,
"expected=i" => \my $expected_no_lines,
) or die("Error in command line arguments\n");
my $test_dir = 'solutions';
my $output_file = 'out.txt';
my $wc_expected = $expected_no_lines; # expected number of output lines
my $tests = get_test_names( $test_dir, $case );
my $file2_size = get_file2_size();
my $num_cpus = Sys::Info->new()->device( CPU => () )->count;
chdir $test_dir;
my $cmd = 'run.sh';
my @times;
for my $case (@$tests)
my $savedir = getcwd();
chdir $case;
say "Running '$case'..";
my $arg = get_cmd_args( $case, $file2_size, $num_cpus );
my $output = `bash -c " time -p $cmd $arg; 2>&1"`;
my ($user, $sys, $real ) = get_run_times( $output );
print_timings( $user, $sys, $real ) if $verbose;
check_output_is_ok( $output_file, $wc_expected, $verbose, $check );
print "\n" if $verbose;
push @times, $real;
#push @times, $user + $sys; # this is wrong when using Gnu parallel
chdir $savedir;
say "Done.\n";
print_summary( $tests, \@times );
结果
这是运行测试的输出:
$ run_all.pl --verbose
Running 'inian3'..
..finished in 0.45 seconds ( user: 0.44, sys: 0.00 )
..no of output lines: 66711
Running 'inian2'..
..finished in 0.73 seconds ( user: 0.73, sys: 0.00 )
..no of output lines: 66711
Running 'Vasiliou'..
..finished in 0.09 seconds ( user: 0.08, sys: 0.00 )
..no of output lines: 66711
Running 'codeforester_orig'..
..finished in 0.05 seconds ( user: 0.05, sys: 0.00 )
..no of output lines: 66711
Running 'codeforester'..
..finished in 0.45 seconds ( user: 0.44, sys: 0.01 )
..no of output lines: 66711
[...]
总结
[@Vasiliou 获得的结果显示在中间一栏。]
|Vasiliou
My Benchmark |Results | Details
-------------------------------|---------|----------------------
inian4 : 0.04s |0.22s | LC_ALL fgrep -f [loose]
codeforester_orig : 0.05s | | fgrep -f [loose]
Vasiliou2 : 0.06s |0.16s | [LC_ALL join [requires sorted files]]
BOC1 : 0.06s | | grep -E [loose]
BOC2 : 0.07s |15s | LC_ALL grep -E [loose]
BOC2B : 0.07s | | LC_ALL grep -E [strict]
inian4B : 0.08s | | LC_ALL grep -E -f [strict]
Vasiliou : 0.08s |0.23s | [join [requires sorted files]]
gregory1B : 0.08s | | [parallel + grep -E -f [strict]]
ikegami : 0.1s | | grep -P
gregory1 : 0.11s |0.5s | [parallel + fgrep -f [loose]]
hakon1 : 0.14s | | [perl + c]
BOC1B : 0.14s | | grep -E [strict]
jjoao : 0.21s | | [compiled flex generated c code]
inian6 : 0.26s |0.7s | [LC_ALL awk + split+dict]
codeforester_origB : 0.28s | | grep -E -f [strict]
dawg : 0.35s | | [python + split+dict]
inian3 : 0.44s |1.1s | [awk + split+dict]
zdim2 : 0.4s | | [perl + split+dict]
codeforester : 0.45s | | [perl + split+dict]
oliv : 0.5s | | [python + compiled regex + re.search()]
zdim : 0.61s | | [perl + regexp+dict]
inian2 : 0.73s |1.7s | [awk + index($0,i)]
inian5 : 18.12s | | [LC_ALL awk + match($0,i) [loose]]
inian1 : 19.46s | | [awk + match($0,i) [loose]]
inian5B : 42.27s | | [LC_ALL awk + match($0,i) [strict]]
inian1B : 85.67s | | [awk + match($0,i) [strict]]
Vasiliou Results : 2 X CPU Intel 2 Duo T6570 @ 2.10GHz - 2Gb RAM-Debian Testing 64bit- kernel 4.9.0.1 - no cpu freq scaling.
更真实的测试用例
然后我创建了一个更现实的案例,file1.txt
有 100 个字,file2.txt
有 1000 万行(268Mb 文件大小)。我使用shuf -n1000 /usr/share/dict/american-english > words.txt
从/usr/share/dict/american-english
的字典中提取了1000个随机单词,然后将其中的100个单词提取到file1.txt
中,然后以与上述第一个测试用例相同的方式构造file2.txt
。请注意,字典文件是 UTF-8 编码的,我从 words.txt
中删除了所有非 ASCII 字符。
然后我在没有前一个案例中最慢的三种方法的情况下运行测试。 IE。 inian1
、inian2
和 inian5
被排除在外。以下是新结果:
gregory1 : 0.86s | [parallel + fgrep -f [loose]]
Vasiliou2 : 0.94s | [LC_ALL join [requires sorted files]]
inian4B : 1.12s | LC_ALL grep -E -f [strict]
BOC2B : 1.13s | LC_ALL grep -E [strict]
BOC2 : 1.15s | LC_ALL grep -E [loose]
BOC1 : 1.18s | grep -E [loose]
ikegami : 1.33s | grep -P
Vasiliou : 1.37s | [join [requires sorted files]]
hakon1 : 1.44s | [perl + c]
inian4 : 2.18s | LC_ALL fgrep -f [loose]
codeforester_orig : 2.2s | fgrep -f [loose]
inian6 : 2.82s | [LC_ALL awk + split+dict]
jjoao : 3.09s | [compiled flex generated c code]
dawg : 3.54s | [python + split+dict]
zdim2 : 4.21s | [perl + split+dict]
codeforester : 4.67s | [perl + split+dict]
inian3 : 5.52s | [awk + split+dict]
zdim : 6.55s | [perl + regexp+dict]
gregory1B : 45.36s | [parallel + grep -E -f [strict]]
oliv : 60.35s | [python + compiled regex + re.search()]
BOC1B : 74.71s | grep -E [strict]
codeforester_origB : 75.52s | grep -E -f [strict]
注意
基于grep
的解决方案正在寻找整行的匹配项,因此在这种情况下,它们包含一些错误匹配项:codeforester_orig
、BOC1
、BOC2
、gregory1
、inian4
方法, 和oliv
从 10,000,000 行中提取了 1,087,609 行,而其他方法从 file2.txt
中提取了正确的 997,993 行。
注意事项
我在 Ubuntu 16.10 笔记本电脑(Intel Core i7-7500U CPU @ 2.70GHz)上对此进行了测试
整个基准研究可通过here获得。
【讨论】:
很好的测试。我很惊讶加入解决方案可以取得良好的效果。另请注意,加入解决方案应在工作前进行排序 - 请重新检查。此外,我认为您错过了 codeforester 接受的解决方案(perl 代码),并且很奇怪 zdim perl 解决方案比 grep 慢得多,而 OP 则相反(我们倾向于相信他:)) 确保您的输入包含您不想匹配的内容。例如,如果您只想在 file2 的第二个字段上进行精确的字符串匹配,则在 file1 中包含foo
和 f.*o
加上在 file2 中的 a|foobar|b
和 foo|a|b
等等。编写与您想要的东西相匹配的工具总是微不足道的,但很难排除您不想要的东西,因此真正测试解决方案的唯一方法是努力创建包含您可以想象的东西的测试用例可能匹配但不应该匹配(通常是部分匹配、正则表达式或错误部分匹配)。
我希望您在 file1 中包含正则表达式元字符,这样我们就可以过滤掉使用正则表达式而不是字符串比较的解决方案。您还知道 file1 中的任何单词是否与 file2 的 field2 中的单词部分匹配(例如 foo
与 foobar
)?您应该让其中一些再次过滤掉非解决方案。最后 - 您是否进行了第 3 次迭代计时以消除缓存的影响?
您基准测试的解决方案中有一半甚至不起作用!以下无法处理file1.txt
foo1
file2.txt
date1|foo12|number5
:BOC1、BOC2、codeforester_orig、gregory1、inian2、inian4、oliv
@jjoao 这个话题收集了很多信息,每次我在 SE 中看到一个 grep 问题时,我都会在这里指出人们......这些带有基准的解决方案应该是一个“类似 grep”的 wiki!【参考方案3】:
您是否尝试过Awk
可以加快速度:
awk 'FNR==NRhash[$1]; nextfor (i in hash) if (match($0,i)) print; break' file1.txt FS='|' file2.txt
(或)在Awk
中使用index()
函数,正如来自Benjamin W. 的cmets 所建议的,如下
awk 'FNR==NRhash[$1]; nextfor (i in hash) if (index($0,i)) print; break' file1.txt FS='|' file2.txt
(或)Ed Morton 在 cmets 中建议的更直接的正则表达式匹配,
awk 'FNR==NRhash[$1]; nextfor (i in hash) if ($0~i) print; break' file1.txt FS='|' file2.txt
就是你所需要的。我猜这会更快,但不能完全确定包含数百万个条目的文件。这里的问题在于沿线任何地方的可能性匹配。如果在任何特定列中都相同(例如,单独说 $2
),则可以采用更快的方法
awk 'FNR==NRhash[$1]; next$2 in hash' file1.txt FS='|' file2.txt
您还可以通过在系统中设置locale
来加快速度。从这个精彩的Stéphane Chazelas's answer 中解释这个主题,您可以通过将语言环境LC_ALL=C
设置为正在运行的命令本地 来加快速度。
在任何基于GNU
的系统上,locale
的默认值
$ locale
LANG=en_US.UTF-8
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=
使用一个变量LC_ALL
,您可以一次将所有LC_
类型变量设置为指定的语言环境
$ LC_ALL=C locale
LANG=en_US.UTF-8
LC_CTYPE="C"
LC_NUMERIC="C"
LC_TIME="C"
LC_COLLATE="C"
LC_MONETARY="C"
LC_MESSAGES="C"
LC_PAPER="C"
LC_NAME="C"
LC_ADDRESS="C"
LC_TELEPHONE="C"
LC_MEASUREMENT="C"
LC_IDENTIFICATION="C"
LC_ALL=C
那么这有什么影响呢?
简单地说,当使用locale C
时,它将默认使用服务器的基本Unix/Linux 语言ASCII
。基本上,当您 grep
某事时,默认情况下您的语言环境将被国际化并设置为 UTF-8
,它可以表示 Unicode 字符集中的每个字符,以帮助显示世界上的任何书写系统,目前超过 @987654344 @ 唯一字符,而对于 ASCII
,每个字符都以单字节序列编码,其字符集包含不超过 128
唯一字符。
因此,当在UTF-8
字符集编码的文件上使用grep
时,它需要将每个字符与十万个唯一字符中的任何一个匹配,但只是128
in ASCII
,所以使用你的fgrep
作为
LC_ALL=C fgrep -F -f file1.txt file2.txt
同样,同样可以适应Awk
,因为它使用regex
匹配match($0,i)
调用,设置C
语言环境可以加快字符串匹配。
LC_ALL=C awk 'FNR==NRhash[$1]; nextfor (i in hash) if (match($0,i)) print; break' file1.txt FS='|' file2.txt
【讨论】:
关于语言环境的说明非常好。我不知道它是否有效(我想是的),但只是考虑一下它是值得的。 @codeforester:说实话,这个问题没有明确的答案。人们只能做代码高尔夫(使用最少资源的其他答案中最好的),我尽我所能来解决它,因为 OP 你对 downvoting 的反馈在这里不是一个健康的方法,因为没有 100% 的解决方案。感谢人们花时间回答您的问题。 @Inianawk 'FNR==NRhash[$1]; next$2 in hash' file1.txt FS='|' file2.txt
是 field2 上完整字符串匹配的 THE awk 解决方案。你在哪里写了match(i,$0)
ITYM match($0,i)
,但那是不必要地调用一个函数(并填充 RSTART 和 RLENGTH)而不是 $0~i
。 match() 和 index() 版本无论如何都没有用,因为 awk 将 file2 的每一行拆分为字段,因此对于整行上的正则表达式或字符串部分匹配,不能比 grep
或 grep -F
更快.
@EdMorton:一如既往地纳入您宝贵的 cmets。
@codeforester:感谢您奖励我,希望答案对您很有帮助。【参考方案4】:
一小段 Perl 代码解决了这个问题。这是采取的方法:
将file1.txt
的行存储在哈希中
逐行读取file2.txt
,解析提取第二个字段
检查提取的字段是否在哈希中;如果是,打印该行
代码如下:
#!/usr/bin/perl -w
use strict;
if (scalar(@ARGV) != 2)
printf STDERR "Usage: fgrep.pl smallfile bigfile\n";
exit(2);
my ($small_file, $big_file) = ($ARGV[0], $ARGV[1]);
my ($small_fp, $big_fp, %small_hash, $field);
open($small_fp, "<", $small_file) || die "Can't open $small_file: " . $!;
open($big_fp, "<", $big_file) || die "Can't open $big_file: " . $!;
# store contents of small file in a hash
while (<$small_fp>)
chomp;
$small_hash$_ = undef;
close($small_fp);
# loop through big file and find matches
while (<$big_fp>)
# no need for chomp
$field = (split(/\|/, $_))[1];
if (defined($field) && exists($small_hash$field))
printf("%s", $_);
close($big_fp);
exit(0);
我用 file1.txt 中的 14K 行和 file2.txt 中的 1.3M 行运行上述脚本。它在大约 13 秒内完成,产生了 126K 匹配。这是相同的time
输出:
real 0m11.694s
user 0m11.507s
sys 0m0.174s
我运行了@Inian 的awk
代码:
awk 'FNR==NRhash[$1]; nextfor (i in hash) if (match($0,i)) print; break' file1.txt FS='|' file2.txt
它比 Perl 解决方案慢得多,因为它为 file2.txt 中的每一行循环 14K 次——这真的很昂贵。它在处理file2.txt
的 592K 记录并产生 40K 匹配行后中止。这是花了多长时间:
awk: illegal primary in regular expression 24/Nov/2016||592989 at 592989
input record number 675280, file file2.txt
source line number 1
real 55m5.539s
user 54m53.080s
sys 0m5.095s
使用@Inian 的另一个awk
解决方案,消除了循环问题:
time awk -F '|' 'FNR==NRhash[$1]; next$2 in hash' file1.txt FS='|' file2.txt > awk1.out
real 0m39.966s
user 0m37.916s
sys 0m0.743s
time LC_ALL=C awk -F '|' 'FNR==NRhash[$1]; next$2 in hash' file1.txt FS='|' file2.txt > awk.out
real 0m41.057s
user 0m38.475s
sys 0m0.904s
awk
在这里非常令人印象深刻,因为我们不必编写整个程序来完成它。
我也运行了@oliv 的 Python 代码。完成这项工作大约需要 15 个小时,看起来它产生了正确的结果。构建一个巨大的正则表达式不如使用哈希查找高效。这里是time
输出:
real 895m14.862s
user 806m59.219s
sys 1m12.147s
我尝试按照建议使用parallel。但是,即使块大小非常小,它也会因fgrep: memory exhausted
错误而失败。
令我惊讶的是fgrep
完全不适合这个。我在 22 小时后中止了它,它产生了大约 10 万次匹配。 我希望 fgrep
可以选择强制将 -f file
的内容保存在哈希中,就像 Perl 代码所做的那样。
我没有检查join
方法 - 我不希望对文件进行排序的额外开销。另外,鉴于fgrep
的性能不佳,我认为join
不会比Perl 代码做得更好。
感谢大家的关注和回复。
【讨论】:
您的“fgrep:内存耗尽”令人惊讶;我知道您使用的是 MBP,所以我假设您使用的是 BSD grep 版本。您可以尝试带有 -F 标志的 gnu grep 吗? (可以使用自制软件安装并使用 gfgrep 运行)。 我相信 agrep 在使用 -f 时会使用模式的哈希值。您可能想尝试一下;它也可以使用自制软件安装。 1)defined($field)
应该是不必要的。如果有必要,请考虑关闭“未初始化”警告。 2) exists($small_hash$field)
可以用(稍微)更快的$small_hash$field
替换,如果你指定1
而不是undef
作为哈希值。 3) printf("%s", $_);
是print;
的慢版本。
我意识到我没有测试@Inian优化的awk
解决方案的错误。刚刚更新了我的答案以包含它。【参考方案5】:
假设: 1. 您只想在本地工作站上运行此搜索。 2. 你有多个核心/cpu 来利用并行搜索。
parallel --pipepart -a file2.txt --block 10M fgrep -F -f file1.txt
根据上下文进行一些进一步的调整: A. 使用 LANG=C 禁用 NLS(这在另一个答案中已经提到) B. 使用 -m 标志设置最大匹配数。
注意:我猜 file2 约为 4GB,10M 块大小还可以,但您可能需要优化块大小以获得最快的运行速度。
【讨论】:
如果文件较大:gnu.org/software/parallel/…【参考方案6】:这个 Perl 脚本 (a
) 生成一个正则表达式模式:
#!/usr/bin/perl
use strict;
use warnings;
use Regexp::Assemble qw( );
chomp( my @ids = <> );
my $ra = Regexp::Assemble->new();
$ra->add(quotemeta($_)) for @ids;
print("^[^|]*\\|(?:" . (re::regexp_pattern($ra->re()))[0] . ")\\|");
它的使用方法如下:
$ LC_ALL=C grep -P "$( a file1.txt )" file2.txt
date1|foo1|number1
date2|foo2|number2
date1|bar1|number1
date2|bar2|number2
注意该脚本使用 Regexp::Assemble,因此您可能需要安装它。
sudo su
cpan Regexp::Assemble
注意事项:
与称为 BOC1、BOC2、codeforester_orig、gregory1、inian2、inian4 和 oliv 的解决方案不同,我的解决方案可以正确处理
file1.txt
foo1
file2.txt
date1|foo12|number5
我的应该比@BOC 的类似solution 更好,因为该模式经过优化以减少回溯。 (如果file2.txt
中的字段超过三个,我的也可以,而链接的解决方案可能会失败。)
我不知道它与拆分+字典的解决方案相比如何。
【讨论】:
我将 OP 的问题解释为:“(file2.txt 的)第二个字段应该等于 file1.txt 中的单词之一”。如果是这样,我们有一个匹配。所以你是对的,许多解决方案都会给出错误的结果。我在回答中也提到了:对于我回答中的大测试用例,很多方法确实给出了错误的匹配。 也许我们应该将file1.txt
更改为提供错误匹配的方法,例如^[^|]*\|foo1\|
?然后他们应该根据 OP 的假定规范给出正确的结果(假设他们的命令更改为 grep -E -f
而不是使用 fgrep
)。并且方法之间的比较会变得不那么混乱:)
@Håkon Hægland,很好奇为什么您在最近添加其他内容时没有将其添加到您的基准测试中。是因为你没有 R::A 吗?我很好奇它与 BOC 的比较,即 R::A 的分组有多大帮助。【参考方案7】:
这是使用Inline::C
来加快在大文件中搜索匹配字段的Perl 解决方案:
use strict;
use warnings;
use Inline C => './search.c';
my $smallfile = 'file1.txt';
my $bigfile = 'file2.txt';
open my $fh, '<', $smallfile or die "Can't open $smallfile: $!";
my %word = map chomp; $_ => 1 <$fh>;
search( $bigfile, \%word );
search()
子例程在纯 C 中实现,使用 perlapi
在小文件字典 %words
中查找键:
search.c:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#define BLOCK_SIZE 8192 /* how much to read from file each time */
static char read_buf[BLOCK_SIZE + 1];
/* reads a block from file, returns -1 on error, 0 on EOF,
else returns chars read, pointer to buf, and pointer to end of buf */
size_t read_block( int fd, char **ret_buf, char **end_buf )
int ret;
char *buf = read_buf;
size_t len = BLOCK_SIZE;
while (len != 0 && (ret = read(fd, buf, len)) != 0)
if (ret == -1)
if (errno == EINTR)
continue;
perror( "read" );
return ret;
len -= ret;
buf += ret;
*end_buf = buf;
*ret_buf = read_buf;
return (size_t) (*end_buf - *ret_buf);
/* updates the line buffer with the char pointed to by cur,
also updates cur
*/
int update_line_buffer( char **cur, char **line, size_t *llen, size_t max_line_len )
if ( *llen > max_line_len )
fprintf( stderr, "Too long line. Maximimum allowed line length is %ld\n",
max_line_len );
return 0;
**line = **cur;
(*line)++;
(*llen)++;
(*cur)++;
return 1;
/* search for first pipe on a line (or next line if this is empty),
assume line ptr points to beginning of line buffer.
return 1 on success
Return 0 if pipe could not be found for some reason, or if
line buffer length was exceeded */
int search_field_start(
int fd, char **cur, char **end_buf, char **line, size_t *llen, size_t max_line_len
)
char *line_start = *line;
while (1)
if ( *cur >= *end_buf )
size_t res = read_block( fd, cur, end_buf );
if (res <= 0) return 0;
if ( **cur == '|' ) break;
/* Currently we just ignore malformed lines ( lines that do not have a pipe,
and empty lines in the input */
if ( **cur == '\n' )
*line = line_start;
*llen = 0;
(*cur)++;
else
if (! update_line_buffer( cur, line, llen, max_line_len ) ) return 0;
return 1;
/* assume cur points at starting pipe of field
return -1 on read error,
return 0 if field len was too large for buffer or line buffer length exceed,
else return 1
and field, and length of field
*/
int copy_field(
int fd, char **cur, char **end_buf, char *field,
size_t *flen, char **line, size_t *llen, size_t max_field_len, size_t max_line_len
)
*flen = 0;
while( 1 )
if (! update_line_buffer( cur, line, llen, max_line_len ) ) return 0;
if ( *cur >= *end_buf )
size_t res = read_block( fd, cur, end_buf );
if (res <= 0) return -1;
if ( **cur == '|' ) break;
if ( *flen > max_field_len )
printf( "Field width too large. Maximum allowed field width: %ld\n",
max_field_len );
return 0;
*field++ = **cur;
(*flen)++;
/* It is really not necessary to null-terminate the field
since we return length of field and also field could
contain internal null characters as well
*/
//*field = '\0';
return 1;
/* search to beginning of next line,
return 0 on error,
else return 1 */
int search_eol(
int fd, char **cur, char **end_buf, char **line, size_t *llen, size_t max_line_len)
while (1)
if ( *cur >= *end_buf )
size_t res = read_block( fd, cur, end_buf );
if (res <= 0) return 0;
if ( !update_line_buffer( cur, line, llen, max_line_len ) ) return 0;
if ( *(*cur-1) == '\n' )
break;
//**line = '\0'; // not necessary
return 1;
#define MAX_FIELD_LEN 80 /* max number of characters allowed in a field */
#define MAX_LINE_LEN 80 /* max number of characters allowed on a line */
/*
Get next field ( i.e. field #2 on a line). Fields are
separated by pipes '|' in the input file.
Also get the line of the field.
Return 0 on error,
on success: Move internal pointer to beginning of next line
return 1 and the field.
*/
size_t get_field_and_line_fast(
int fd, char *field, size_t *flen, char *line, size_t *llen
)
static char *cur = NULL;
static char *end_buf = NULL;
size_t res;
if (cur == NULL)
res = read_block( fd, &cur, &end_buf );
if ( res <= 0 ) return 0;
*llen = 0;
if ( !search_field_start( fd, &cur, &end_buf, &line, llen, MAX_LINE_LEN )) return 0;
if ( (res = copy_field(
fd, &cur, &end_buf, field, flen, &line, llen, MAX_FIELD_LEN, MAX_LINE_LEN
) ) <= 0)
return 0;
if ( !search_eol( fd, &cur, &end_buf, &line, llen, MAX_LINE_LEN ) ) return 0;
return 1;
void search( char *filename, SV *href)
if( !SvROK( href ) || ( SvTYPE( SvRV( href ) ) != SVt_PVHV ) )
croak( "Not a hash reference" );
int fd = open (filename, O_RDONLY);
if (fd == -1)
croak( "Could not open file '%s'", filename );
char field[MAX_FIELD_LEN+1];
char line[MAX_LINE_LEN+1];
size_t flen, llen;
HV *hash = (HV *)SvRV( href );
while ( get_field_and_line_fast( fd, field, &flen, line, &llen ) )
if( hv_exists( hash, field, flen ) )
fwrite( line, sizeof(char), llen, stdout);
if (close(fd) == -1)
croak( "Close failed" );
测试表明,它比此处介绍的最快的纯 Perl 解决方案(请参阅我的 other answer 中的方法 zdim2
)快大约 3 倍。
【讨论】:
【参考方案8】:这是一个使用集合的 Python 解决方案——在概念上大致相当于 Perl 仅键散列或 awk 数组。
#!/usr/bin/python
import sys
with open(sys.argv[1]) as f:
tgt=e.rstrip() for e in f
with open(sys.argv[2]) as f:
for line in f:
cells=line.split("|")
if cells[1] in tgt:
print line.rstrip()
当我在类似大小的文件上运行它时,它会在大约 8 秒内运行。
速度与:
$ awk 'FNR==NRarr[$1]; next $2 in arrprint $0' FS="|" /tmp/f1 /tmp/f2
这里的 Python 和 awk 解决方案都是全字符串匹配;不是部分正则表达式样式匹配。
由于 awk 解决方案速度快且符合 POSIX,因此这是更好的答案。
【讨论】:
【参考方案9】:虽然这个帖子已经结束,但是两个文件之间的所有类似 grep 的方法都收集在这个帖子中,为什么不添加这个 awk 替代方案,类似于(甚至改进)赢得赏金的 Inian 的 awk 解决方案:
awk 'NR==FNRa[$0]=1;nexta[$2]' patterns.txt FS="|" datafile.txt >matches.txt # For matches restricted on Field2 of datafile
这相当于 Inian awk $2 in hash
解决方案,但它可能更快,因为我们不要求 awk 检查整个哈希数组是否包含 file2 的 $2 - 我们只检查 a[$2]有没有价值。
在通过创建哈希数组读取第一个模式文件 appart 时,我们还分配了一个值。
如果$2
的数据文件之前已经在模式文件中找到,那么a[$2]
将有一个值,因此将被打印,因为它不为空。
如果a[$2]
的数据文件没有返回值(null),这将被转换为 false => 不打印。
扩展匹配数据文件的三个字段中的任何一个:
awk 'NR==FNRa[$0]=1;next(a[$1] || a[$2] || a[$3])' patterns.txt FS="|" datafile.txt >matches.txt. #Printed if any of the three fields of datafile match pattern.
在这两种情况下,在 awk 前面应用 LC_ALL=C 似乎可以加快速度。
PS1:当然,这个解决方案也有所有 awk 解决方案的缺陷。不是模式匹配。是两个文件之间的直接/固定匹配,就像这里的大多数解决方案一样。
PS2:在我使用小基准文件Håkon Hægland 的机器基准测试中,与awk 'FNR==NRhash[$1]; next$2 in hash' file1.txt FS='|' file2.txt
相比,我获得了大约20% 的性能提升
【讨论】:
@HåkonHægland - 你介意用你的小文件和大文件对这个解决方案进行基准测试吗? 嗨,乔治!感谢您的输入。我的第一印象是$2 in a
应该等同于a[$2]
。但也许我错了,我不知道 awk 内部工作的所有细节。但我相信两者都需要哈希查找..因此速度相同
我现在对其进行了测试,在我的比较答案中,速度似乎与inian3
相同。所以这可能表明这些方法确实是等价的。你怎么看?
@HåkonHægland 你好!好的,很高兴知道。所以我们不能声称这种方法更快。在我的旧机器上,它的结果似乎比 inian3 好。我会重新检查。您是否也尝试过使用 C localle?【参考方案10】:
你可以试试join
吗?文件必须排序...
$ cat d.txt
bar1
bar2
foo1
foo2
$ cat e.txt
date1|bar1|number1
date2|bar2|number2
date3|bar3|number3
date1|foo1|number1
date2|foo2|number2
date3|foo3|number3
$ join --nocheck-order -11 -22 -t'|' -o 2.1 2.2 2.3 d.txt e.txt
date1|bar1|number1
date2|bar2|number2
date1|foo1|number1
date2|foo2|number2
小更新: 通过在 join 前使用 LC_ALL=C,事情确实加快了,这可以在 Håkon Hægland 的基准测试中看到
PS1:我怀疑 join 是否比 grep -f 更快 ...
【讨论】:
@codeforester 显然,此解决方案仅适用于您以前版本的问题:文件 1 中的模式以匹配文件 2 的字段 2。即使我们看到计时结果也会很有趣仅比较 seconf 字段。【参考方案11】:一种可能的方式是使用python
:
$ cat test.py
import sys,re
with open(sys.argv[1], "r") as f1:
patterns = f1.read().splitlines() # read pattern from file1 without the trailing newline
m = re.compile("|".join(patterns)) # create the regex
with open(sys.argv[2], "r") as f2:
for line in f2:
if m.search(line) :
print line, # print line from file2 if this one matches the regex
并像这样使用它:
python test.py file1.txt file2.txt
【讨论】:
为什么我们需要从模式数组创建巨大的正则表达式? 我在一个 130 万行的 file2.txt 上运行了这个。它已经运行了 3.5 多个小时,到目前为止已经产生了 86K 的结果(预期的 126K 匹配)。绝对不是最优的。【参考方案12】:您也可以为此使用 Perl:
请注意,这会占用内存,而您的机器/服务器最好有一些。
样本数据:
%_STATION@gaurav * /root/ga/pl> head file1.txt file2.txt
==> file1.txt <==
foo1
foo2
...
bar1
bar2
...
==> file2.txt <==
date1|foo1|number1
date2|foo2|number2
date3|foo3|number3
...
date1|bar1|number1
date2|bar2|number2
date3|bar3|number3
%_STATION@gaurav * /root/ga/study/pl>
脚本输出:脚本将在名为output_comp
的文件中生成最终输出。
%_STATION@gaurav * /root/ga/pl> ./comp.pl file1.txt file2.txt ; cat output_comp
date1|bar1|number1
date2|bar2|number2
date2|foo2|number2
date1|foo1|number1
%_STATION@gaurav * /root/ga/pl>
脚本:
%_STATION@gaurav * /root/ga/pl> cat comp.pl
#!/usr/bin/perl
use strict ;
use warnings ;
use Data::Dumper ;
my ($file1,$file2) = @ARGV ;
my $output = "output_comp" ;
my %hash ; # This will store main comparison data.
my %tmp ; # This will store already selected results, to be skipped.
(scalar @ARGV != 2 ? (print "Need 2 files!\n") : ()) ? exit 1 : () ;
# Read all files at once and use their name as the key.
for (@ARGV)
open FH, "<$_" or die "Cannot open $_\n" ;
while (my $line = <FH>) chomp $line ;$hash$_$line = "$line"
close FH ;
# Now we churn through the data and compare to generate
# the sorted output in the output file.
open FH, ">>$output" or die "Cannot open outfile!\n" ;
foreach my $k1 (keys %$hash$file1)
foreach my $k2 (keys %$hash$file2)
if ($k1 =~ m/^.+?$k2.+?$/)
if (!defined $tmp"$hash$file2$k2")
print FH "$hash$file2$k2\n" ;
$tmp"$hash$file2$k2" = 1 ;
close FH ;
%_STATION@gaurav * /root/ga/pl>
谢谢。
【讨论】:
你几乎是在正确的轨道上!无需将第二个(更大的)文件加载到散列中。请在下面查看我的解决方案。 是的,你是对的。本可以这样做,实际上应该这样做,但无论如何,这需要多少时间? 我没有运行它,因为我知道由于内存消耗它根本不是最优的。之前的fgrep
命令让我的MPB粉丝疯了,所以没有勇气尝试这个!【参考方案13】:
恕我直言,grep 是一个为巨大的 file2.txt 高度优化的好工具,但可能不适合搜索这么多模式。我建议将 file1.txt 的所有字符串组合成一个巨大的正则表达式,如 \|bar1|bar2|foo1|foo2\|
echo '\|'$(paste -s -d '|' file1.txt)'\|' > regexp1.txt
grep -E -f regexp1.txt file2.txt > file.matched
当然 LANG=C 可能会有所帮助。 请提供反馈或发送您的文件,以便我测试自己。
【讨论】:
【参考方案14】:我会使用 SQLite3 :) 也许是内存数据库或其他什么。导入文件并使用 SQL 查询。
【讨论】:
这可能是个好主意。我想要一个简单的命令行解决方案。【参考方案15】:使用 flex:
1:构建弹性处理器:
$ awk 'NR==1 printf "%%%%\n\n.*\\|(%s",$0
printf "|%s",$0
END print ")\\|.*\\n ECHO;\n.*\\n ;\n%%\n" ' file1.txt > a.fl
2:编译
$ flex -Ca -F a.fl ; cc -O lex.yy.c -lfl
3:然后运行
$ a.out < file2.txt > out
编译(cc ...)是一个缓慢的过程;这种方法只为案例付费 稳定文件1.txt
(在我的机器上)在这种方法中运行搜索“100 in 10_000_000”测试所需的时间比LC_ALL=C fgrep...
快3倍
【讨论】:
嗨 JJoao。感谢您的输入!我更新了我的答案以包含此方法。 @HåkonHægland,谢谢;这种方法和其他方法之间的比较对我来说仍然很困难(对于非常大的表达式,flex 并且编译通常需要很多时间)。你能给我最后一次“跑步”的时间吗? (a.out < file2.txt > out
)
@HåkonHægland,很抱歉给您带来的所有麻烦。我对 38 s 感到惊讶——这就是我喜欢基准测试的原因!我要去研究它。
@HåkonHægland,再次抱歉:我生成了错误的弹性代码。更新了——这次我相信会快 10 倍。
谢谢,我已更新我的答案以使用新的 a.fl
文件。现在看起来好多了:)【参考方案16】:
设置语言等可能会有所帮助。
否则我想不出一个神奇的解决方案来逃避你的基本问题: 数据不是结构化的,因此您将进行搜索 这归结为 file1 中的行数乘以 file2 中的行数。
将十亿行放入数据库中,并以智能的方式对其进行索引,这是我能想到的唯一加速方法。不过,该索引必须非常智能......
简单的解决方案是:有足够的内存来容纳所有内容。 否则,您对此无能为力....
【讨论】:
以上是关于在 Bash 中从另一个较大文件中查找文件行的最快方法的主要内容,如果未能解决你的问题,请参考以下文章
如何在eclipse中从另一个java项目中读取项目的属性文件
如何在 ASP.NET MVC 3 的一个解决方案中从另一个项目获取文件路径?
在bash中从一个文件中读取文件名称,并检查当前目录是否保护这些文件