Самый быстрый способ найти строки файла из другого более крупного файла в Bash

У меня есть два файла, file1.txt и file2.txt. file1.txt имеет около 14K строк, а file2.txt - около 2 миллиардов. file1.txt имеет одно поле f1 на строку, а file2.txt имеет 3 поля, от f1 до f3, разделенных | ,

Я хочу найти все строки из file2.txt где f1 из file1.txt соответствует f2 из file2.txt (или в любом месте строки, если мы не хотим тратить дополнительное время на разбиение значений file2.txt).

file1.txt (около 14K строк, не отсортировано):

foo1
foo2
...
bar1
bar2
...

file2.txt (около 2 миллиардов строк, не отсортировано):

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 или с помощью небольшого сценария.

Ответ 1

Небольшая часть кода 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);

Я запустил выше script с 14K строк в файлах file1.txt и 1.3M в файле file2.txt. Он завершился примерно за 13 секунд, производя 126K матчей. Вот вывод time для этого же:

real    0m11.694s
user    0m11.507s
sys 0m0.174s

Я запускал код @Inian awk:

awk 'FNR==NR{hash[$1]; next}{for (i in hash) if (match($0,i)) {print; break}}' file1.txt FS='|' file2.txt

Это было медленнее, чем решение Perl, поскольку для каждой строки в файле file2.txt циклически выполняется 14K раз, что очень дорого. Он прервался после обработки 592K записей file2.txt и создания 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==NR{hash[$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==NR{hash[$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

Я попытался использовать предложение . Однако с ошибкой fgrep: memory exhausted она не удалась, даже с очень маленькими размерами блока.


Меня удивило, что fgrep был совершенно непригоден для этого. Я прервал его после 22 часов, и он произвел около 100 тысяч матчей. Я хочу, чтобы fgrep имел возможность принудительно сохранять содержимое -f file в хэше, как и код Perl.

Я не проверял подход join - мне не нужны дополнительные накладные расходы на сортировку файлов. Кроме того, учитывая fgrep низкую производительность, я не верю, что join сделал бы лучше, чем код Perl.

Спасибо всем за внимание и ответы.

Ответ 2

Решение 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 тест не нужен, и удаление его избавляет от операции. Тем не менее, я постоянно чувствую, что время выполнения улучшается, а также лучше передает цель. В целом я оставляю это. Спасибо икегами за комментарии.

Примечание. Моя версия ниже закомментированной версии примерно на 50% быстрее, чем другая. Оба даны, потому что они разные, один находит первое совпадение, а второй - второе поле. Я оставляю это как более общий выбор, поскольку вопрос по этому вопросу неоднозначен.


Некоторые сравнения (тест) [Обновлен для записи в STDOUT, см. "Обновление" выше]

В ответе HåkonHægland содержится подробный анализ, который определил время для запуска большинства решений. Вот еще один fgrep, который сравнивает два вышеупомянутых решения: собственный ответ OP и опубликованный fgrep который, как ожидается, будет быстрым и используется в вопросе и во многих ответах.

Я строю тестовые данные следующим образом. Несколько строк длиной примерно так, как показано, сделаны со случайными словами для обоих файлов, чтобы соответствовать во втором поле. Затем я добавляю это "начальное число" для выборок данных со строками, которые не будут совпадать, поэтому для имитации соотношений между размерами и совпадениями, указанными OP: для 14K строк в маленьком файле в большом файле 1,3M строк, что дает 126K совпадений. Затем эти образцы многократно записываются для построения полных файлов данных в виде OP, каждый раз shuffle -ed, используя List :: Util.

При всех сравнениях, приведенных ниже, 106_120 совпадений для файлов указанных выше размеров (для проверки diff -ed), поэтому частота совпадения достаточно близка. Они сравниваются, вызывая полные программы, используя my $res = timethese(60...). Результатом cmpthese($res) на v5.16 являются

        Rate regex  cfor split fgrep
regex 1.05/s    --  -23%  -35%  -44%
cfor  1.36/s   30%    --  -16%  -28%
split 1.62/s   54%   19%    --  -14%
fgrep 1.89/s   80%   39%   17%    --

Тот факт, что оптимизированная C-программа fgrep выходит на первое место, неудивителен. Отставание "regex" от "split" может быть связано с многократными издержками запуска двигателя для небольших матчей. Это может варьироваться в зависимости от версий Perl, учитывая эволюцию движка регулярных выражений. Я включил ответ @codeforester ("cfor"), так как он был объявлен самым быстрым, и его отставание в 20% от очень похожего "разделения", вероятно, связано с рассеянной небольшой неэффективностью (см. Комментарий ниже этого ответа).

Это не сильно отличается, хотя есть определенные различия в аппаратном и программном обеспечении, а также в деталях данных. Я запускал это на разных Perls и машинах, и заметное различие заключается в том, что в некоторых случаях fgrep действительно был на порядок быстрее.

Опыт OP очень медленного fgrep удивляет. Учитывая их указанное время выполнения, на порядок медленнее, чем указано выше, я предполагаю, что в этом виновата старая система.

Несмотря на то, что это полностью основано на вводе/выводе, параллелизм имеет преимущества от установки его на несколько ядер, и я ожидаю хорошего ускорения, вплоть до нескольких факторов.


Увы, комментарий был удален (?). Короче говоря: ненужное использование скаляра (затраты), ветки if, defined printf вместо print (медленно!). Это имеет значение для эффективности на 2 миллиарда строк.

Ответ 3

Я попытался сделать сравнение между некоторыми из методов, представленных здесь.

Сначала я создал скрипт Perl для генерации входных файлов file1.txt и file2.txt. Чтобы сравнить некоторые решения, я позаботился о том, чтобы слова из file1.txt могли появляться только во втором поле file2.txt. Также, чтобы иметь возможность использовать решение join представленное @GeorgeVasiliou, я отсортировал file1.txt и file2.txt. В настоящее время я сгенерировал входные файлы, основанные только на 75 случайных словах (взяты из https://www.randomlists.com/random-words). Только 5 из этих 75 слов были использованы в file1.txt остальные 70 слов были использованы для заполнения полей в file2.txt. Возможно, потребуется существенно увеличить количество слов, чтобы получить реалистичные результаты (согласно ОП исходный file1.txt содержал 14000 слов). В тестах ниже я использовал file2.txt с 1000000 (1 миллион) строк. Сценарий также генерирует файл regexp1.txt необходимый для решения grep @BOC.

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: Принято решение Perl от @codeforester (см. источник)

  • codeforester_orig: оригинальное решение, представленное @codeforested:

    fgrep -f file1.txt file2.txt
    
  • dawg: решение Python с использованием словаря и разделенной строки, предложенное @dawg (см. источник)

  • gregory1: решение с использованием Gnu Parallel, предложенное @gregory

    parallel -k --pipepart -a file2.txt --block "$block_size" fgrep -F -f file1.txt
    

    См. Примечание ниже о том, как выбрать $block_size.

  • hakon1: решение Perl, предоставленное @HåkonHægland (см. источник). Это решение требует компиляции c -E xtension при первом запуске кода. Не требует перекомпиляции при изменении file1.txt или file2.txt. Примечание. Время, использованное для компиляции c -E при первом запуске, не учитывается в представленном ниже времени выполнения.

  • ikegami: решение с использованием собранного регулярного выражения и использования grep -P как указано @ikegami. Примечание. Собранное регулярное выражение записано в отдельный файл 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: 4-й заголовок @Inian (в основном такой же, как codeforester_orig с LC_ALL):

    LC_ALL=C fgrep -f file1.txt file2.txt
    
  • inian5: 5-е решение от @Inian (аналогично 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: скомпилированный сгенерированный на гибком языке код C, предложенный @JJoao (см. источник). Примечание. Перекомпиляция исполняемого файла должна выполняться каждый раз, file1.txt изменяется file1.txt. Время, используемое для компиляции исполняемого файла, не включено в время выполнения, представленное ниже.

  • oliv: скрипт Python, предоставляемый @oliv (см. источник)

  • Vasiliou: Использование join как предложено @GeorgeVasiliou:

    join --nocheck-order -11 -22 -t'|' -o 2.1 2.2 2.3 file1.txt file2.txt
    
  • Vasiliou2: То же, что Vasiliou, но с LC_ALL=C.

  • zdim: Использование Perl-скрипта, предоставленного @zdim (см. источник). Примечание. При этом используется поисковая версия регулярного выражения (вместо решения с разделением строк).

  • zdim2: То же, что и zdim за исключением того, что он использует функцию split вместо регулярного выражения для поиска поля в file2.txt.

Заметки

  1. Я немного поэкспериментировал с параллелью Gnu (см. Решение gregory1 выше), чтобы определить оптимальный размер блока для моего процессора. У меня 4 ядра, и в настоящее время кажется, что оптимальный выбор - file2.txt файл (file2.txt) на 4 file2.txt равного размера и запустить одно задание на каждом из 4 процессоров. Здесь может потребоваться дополнительное тестирование. Таким образом, для первого тестового случая, где file2.txt равен 20M, я установил $block_size на 5M (см. Решение gregory1 выше), тогда как для более реалистичного случая, представленного ниже, где file2.txt равен 268M, был использован $block_size равный 67M.

  2. Решения BOC1, BOC2, codeforester_orig, inian1, inian4, inian5 и gregory1 все использовали свободное соответствие. Это означает, что слова из file1.txt не обязательно должны совпадать в поле # 2 file2.txt. Матч в любом месте на линии был принят. Поскольку такое поведение затрудняло сравнение их с другими методами, также были введены некоторые модифицированные методы. Первые два метода, называемые BOC1B и BOC2B использовали модифицированный файл regexp1.txt. Строки в исходном regexp1.txt где на форме \|foo1|foo2|...|fooN\| который будет соответствовать словам на любой границе поля. Измененный файл regexp1b.txt совпадение к полю № 2 исключительно с помощью формы ^[^|]*\|foo1|foo2|...|fooN\| вместо.

    Затем остальные модифицированные методы codeforester_origB, inian1B, inian4B, inian5B и gregory1B использовали модифицированный file1.txt. Вместо буквального слова в строке измененный файл file1b.txt использовал одно регулярное выражение в строке в форме:

     ^[^|]*\|word1\|
     ^[^|]*\|word2\|
     ^[^|]*\|word3\|
     [...]
    

    кроме того, для этих методов fgrep -f был заменен на grep -E -f.

Запуск тестов

Вот скрипт, используемый для запуска всех тестов. Он использует команду Bash time для записи времени, потраченного на каждый скрипт. Обратите внимание, что команда time возвращает три разных времени вызова real, user и sys. Сначала я использовал user + sys, но понял, что это неправильно при использовании параллельной команды Gnu, поэтому время, указанное ниже, теперь является real частью, возвращаемой time. Посмотрите этот вопрос для получения дополнительной информации о разном времени, возвращаемом time.

Первый тест выполняется с file1.txt содержащим 5 строк, и file2.txt содержащим 1000000 строк. Вот первые 52 строки скрипта run_all.pl, остальная часть скрипта доступна здесь.

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 имеющий 10 миллионов строк (размер файла 268 МБ). Я извлек 1000 случайных слов из словаря в /usr/share/dict/american-English используя shuf -n1000/usr/share/dict/american-English > words.txt затем извлек 100 из этих слов в file1.txt а затем сконструировал file2.txt же, как описано выше для первого контрольного примера. Обратите внимание, что файл словаря был в кодировке UTF-8, и я удалил все не-ASCII-символы из words.txt.

Затем я запускаю тест без трех самых медленных методов из предыдущего случая. Т.е. inian1, inian2 и inian5 были 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 извлекли 1 087 609 строк из 10 000 000 строк, тогда как другие методы извлекли правильные 997 993 строки из file2.txt.

Заметки

  • Я протестировал это на своем ноутбуке Ubuntu 16.10 (процессор Intel Core i7-7500U @2,70 ГГц)

  • Полное исследование доступно здесь.

Ответ 4

Пробовал ли вы Awk, чтобы немного ускорить работу:

awk 'FNR==NR{hash[$1]; next}{for (i in hash) if (match($0,i)) {print; break}}' file1.txt FS='|' file2.txt

(или) с помощью функции index() в Awk, как это было предложено в комментариях Benjamin W. ниже

awk 'FNR==NR{hash[$1]; next}{for (i in hash) if (index($0,i)) {print; break}}' file1.txt FS='|' file2.txt

(или) более прямое соответствие регулярному выражению, предложенное Эд Мортон в комментариях,

awk 'FNR==NR{hash[$1]; next}{for (i in hash) if ($0~i) {print; break}}' file1.txt FS='|' file2.txt

- это все, что вам нужно. Я предполагаю, что это будет быстрее, но не совсем точно в файлах с миллионами + записей. Здесь проблема заключается в возможности совпадения в любом месте вдоль линии. Если бы то же самое было в любом конкретном столбце (например, только $2), более быстрый подход мог бы быть

awk 'FNR==NR{hash[$1]; next}$2 in hash' file1.txt FS='|' file2.txt

Также вы можете ускорить игру, используя locale, установленный в вашей системе. Перефразируя из этого замечательного Stéphane Chazelas ответ по этому вопросу, вы можете быстро ускорить работу, установив передачу локали 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, который может представлять каждый символ в наборе символов Юникода, чтобы отображать любую из мировых систем написания, в настоящее время более чем 110,000 уникальные символы, тогда как при ASCII каждый символ кодируется в однобайтовой последовательности, а его набор символов содержит не более 128 уникальных символов.

Таким образом, это означает, что при использовании grep в файле, закодированном в наборе символов UTF-8, ему необходимо сопоставить каждый символ с любым из сотен тысяч уникальных символов, но просто 128 в ASCII, поэтому используйте fgrep как

LC_ALL=C fgrep -F -f file1.txt file2.txt

Кроме того, то же самое можно адаптировать к Awk, так как он использует совпадение regex с вызовом match($0,i), установка языкового стандарта C может ускорить соответствие строки.

LC_ALL=C awk 'FNR==NR{hash[$1]; next}{for (i in hash) if (match($0,i)) {print; break}}' file1.txt FS='|' file2.txt

Ответ 5

Предположения: 1. Вы хотите запустить этот поиск только на своей локальной рабочей станции. 2. У вас есть несколько ядер /cpus, чтобы воспользоваться параллельным поиском.

parallel --pipepart -a file2.txt --block 10M fgrep -F -f file1.txt

Некоторые дополнительные настройки в зависимости от контекста: A. Отключить NLS с LANG = C (это уже упоминается в другом ответе) B. Установите максимальное количество совпадений с флагом -m.

Примечание. Я предполагаю, что файл2 составляет ~ 4 ГБ, а размер блока 10М в порядке, но вам может понадобиться оптимизировать размер блока, чтобы получить быстрый запуск.

Ответ 6

Этот Perl script (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

Обратите внимание, что script использует Regexp:: Assemble, поэтому вам может потребоваться установить его.

sudo su
cpan Regexp::Assemble

Примечания:

  • В отличие от решений, названных BOC1, BOC2, codeforester_orig, gregory1, inian2, inian4 и oliv, мое решение корректно обрабатывает

    file1.txt
    foo1
    
    file2.txt
    date1|foo12|number5
    
  • Шахта должна быть лучше, чем аналогичное решение от @BOC, потому что шаблон оптимизирован для уменьшения обратного трассировки. (Mine также работает, если в file2.txt имеется более трех полей, тогда как связанное решение может выйти из строя.)

  • Я не знаю, как он сравнивается с решениями с расщепленным словарем.

Ответ 7

Вот решение Perl, которое использует Inline::C, чтобы ускорить поиск совпадающих полей в большом файле:

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" );

}

Тесты показывают, что он примерно в 3 раза быстрее, чем самое быстрое решение Perl (см. метод zdim2 в моем другом ответе), представленном здесь.

Ответ 8

Вот решение Python с использованием наборов - примерно эквивалентно только ключу хэша или awk-ключа Perl в концепции.

#!/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==NR{arr[$1]; next} $2 in arr{print $0}' FS="|" /tmp/f1 /tmp/f2 

Оба решения Python и awk здесь имеют только полное совпадение строк; а не частичное соответствие стиля регулярного выражения.

Поскольку решение awk выполняется быстро и совместимо с POSIX, это лучший ответ.

Ответ 9

Можете ли вы попробовать 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

Небольшое обновление:
Используя LC_ALL = C перед соединением, все действительно ускоряется, как можно увидеть в тесте Håkon Hægland

PS1: У меня есть сомнения, если соединение может быть быстрее grep -f...

Ответ 10

Несмотря на то, что этот поток завершен, но все grep-подобные методы между двумя файлами собраны в этом сообщении, почему бы не добавить эту альтернативу awk, подобную (или даже улучшенную) для выигрышного решения Inian awk:

awk 'NR==FNR{a[$0]=1;next}a[$2]' patterns.txt FS="|" datafile.txt >matches.txt # For matches restricted on Field2 of datafile

Это эквивалентно решению Inian awk $2 in hash, но оно может быть еще быстрее из-за того, что мы не просим awk проверить, содержит ли весь массив хэшей $2 файла2 - мы просто проверяем, есть ли [$ 2] имеет значение или нет.

При чтении первого файла файла шаблонов от создания хэш-массива мы также назначаем значение.

Если $2 файла данных был найден ранее в файле шаблонов, тогда a[$2] будет иметь значение и, следовательно, будет напечатано, потому что оно не равно null.

если a[$2] файла данных не возвращает значение (null), это переводится в false = > no printing.

Расширение для соответствия любому из трех полей файла данных:

awk 'NR==FNR{a[$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.

В обоих случаях применение LC_ALL = C перед awk, похоже, ускоряет процесс.

PS1: Offcourse это решение имеет также ловушки всех решений awk. Не соответствует шаблону. Является прямым/фиксированным соответствием между двумя файлами, как и большинство решений.

PS2: В моем бедном тесте машины, использующем небольшие тестовые файлы Håkon Hægland, я получаю на 20% лучшую производительность по сравнению с awk 'FNR==NR{hash[$1]; next}$2 in hash' file1.txt FS='|' file2.txt

Ответ 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

Ответ 12

Вы также можете использовать Perl для этого:

Обратите внимание, что это будет память hog, и ваш компьютер/сервер лучше имеет некоторые.

Примеры данных

%[email protected] * /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
%[email protected] * /root/ga/study/pl>

Script Результат: Script будет выдавать final в файле с именем output_comp.

%[email protected] * /root/ga/pl> ./comp.pl  file1.txt file2.txt ; cat output_comp
date1|bar1|number1
date2|bar2|number2
date2|foo2|number2
date1|foo1|number1
%[email protected] * /root/ga/pl>

Script:

%[email protected] * /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  ;
%[email protected] * /root/ga/pl>

Спасибо.

Ответ 13

IMHO, grep - хороший инструмент, очень оптимизированный для огромного файла file2.txt, но, возможно, не для так много шаблонов для поиска. Я предлагаю объединить все строки файла file1.txt в одно огромное regexp, например \| 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...) - медленный процесс; этот подход будет оплачиваться только для случаев стабильного файла file1.txt

(В моей машине). Время, затрачиваемое на поиск теста "100 в 10_000_000" при этом подходе, в 3 раза быстрее, чем LC_ALL=C fgrep...

Ответ 16

язык настроек и т.д. немного помогает, возможно.

В противном случае я не могу придумать волшебное решение, чтобы избежать вашей основной проблемы: данные не структурированы, поэтому у вас будет поиск который сводится к количеству строк в файле1, умноженному на количество строк в файле2.

помещает миллиард строк в базу данных и индексирует их по-умному, это единственная скорость, о которой я могу думать. этот индекс должен быть очень умным, хотя......

Решение SImple: иметь достаточное количество памяти, чтобы соответствовать всем. иначе ничего больше вы не можете сделать об этом....