web-dev-qa-db-ja.com

Bashで別の大きなファイルからファイルの行を見つける最速の方法

file1.txtfile2.txtの2つのファイルがあります。 file1.txtには約14K行あり、file2.txtには約20億行あります。 file1.txtには1行に1つのフィールドf1がありますが、file2.txtにはf1からf3までの3つのフィールドがあり、|で区切られています。

file2.txt of f1 of file1.txt of f2 of file2.txtからすべての行を検索したいfile2.txt)の値を分割する余分な時間。

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コマンドまたは小さなスクリプトを使用してこの操作を行うより良い、より高速な方法があるかどうか疑問に思います。

23
codeforester

Perlコードの小さな断片が問題を解決しました。これは取られたアプローチです:

  • file1.txtの行をハッシュに保存する
  • file2.txtを1行ずつ読み取り、2番目のフィールドを解析して抽出する
  • 抽出されたフィールドがハッシュにあるかどうかを確認します。もしそうなら、行を印刷します

これがコードです:

#!/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==NR{hash[$1]; next}{for (i in hash) if (match($0,i)) {print; break}}' file1.txt FS='|' file2.txt

これは、file2.txtの各行で14K回ループするため、Perlソリューションよりもはるかに低速でした。 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==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

私は parallel を使用するという提案に従いました。ただし、ブロックサイズが非常に小さい場合でも、fgrep: memory exhaustedエラーで失敗しました。


私を驚かせたのは、fgrepがこれにはまったく不向きだったことです。 22時間後に中止し、約10万回の試合が行われました。 fgrepに、Perlコードと同じように-f fileのコンテンツをハッシュに保持するオプションがあったらいいのにと思います。

私はjoinアプローチをチェックしませんでした-ファイルをソートするための追加のオーバーヘッドが欲しくありませんでした。また、fgrepのパフォーマンスが低いことを考えると、joinがPerlコードよりも優れていたとは思えません。

ご静聴ありがとうございました。

5
codeforester

Perlソリューション。 [以下のを参照してください。]

最初のファイルにはハッシュを使用します。大きなファイルを1行ずつ読み取るときに、正規表現(_||_間の最初のパターンをキャプチャする)またはsplit(2番目のWordを取得する)によってフィールドを抽出し、それがexists。それらはおそらく速度が少し異なります(時間を計ります)。正規表現ではdefinedチェックは必要ありませんが、splitの場合は、短絡する_//_(defined-or)を使用します。

_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ブランチを回避して短絡を使用する方が高速ですが、ごくわずかです。何十億もの行で、これらの微調整は追加されますが、多すぎません。上記のリストコンテキストではなく、小さいファイルを1行ずつ読み取る方が少し高速かもしれません(またはそうでないかもしれません)。ただし、これは気にならないはずです

UpdateSTDOUTへの書き込みは2つの操作を節約し、ファイルに書き込むよりも少し速くなるように繰り返し時間を計測します。このような使用法は、ほとんどのUNIXツールでも一貫しているため、STDOUTに書き込むように変更しました。次に、existsテストは必要ありません。テストを削除すると、操作が不要になります。しかし、私は一貫してそれを使って少し良いランタイムを取得しますと同時に、目的をよりよく伝えます。まとめておきます。コメントを寄せてくれた ikegami に感謝します。

コメントアウトされたバージョンは、他のバージョンより約50%高速です、以下の私のベンチマークで。これらは両方ともdifferentであるので、一方が最初の一致を見つけ、もう一方が2番目のフィールドを見つけるので、両方が与えられます。質問が曖昧であるため、私はこの方法をより一般的な選択として保持しています。


一部の比較(ベンチマーク)[STDOUTへの書き込み用に更新。上記の「更新」を参照]

HåkonHæglandによる回答 には広範な分析があり、ほとんどのソリューションの1回の実行のタイミングを計っています。上記の2つのソリューション、OP自体の回答、および投稿されたfgrepのベンチマークを比較した別のテイクは、高速で、質問や多くの回答で使用されることが期待されています。

以下の方法でテストデータを作成します。大体示されている長さの一握りの行は、2番目のフィールドで一致するように、両方のファイルに対してランダムな単語で作成されます。次に、データサンプルのこの「シード」に一致しない行を埋め込むため、OPで引用されたサイズと一致の比率を模倣します。小さなファイルの14K行には、大きなファイルの1.3M行、126K一致次に、これらのサンプルを繰り返し書き込み、完全なデータファイルをOPとしてビルドします。毎回 List :: Util を使用してshuffle- edします。

以下で比較したすべての実行は、上記のファイルサイズ(チェックにはdiff- ed)に一致する_106_120_を生成するため、一致頻度は十分に近くなります。それらは、my $res = timethese(60 ...)を使用して完全なプログラムを呼び出すことによりベンチマークされます。 v5.16でのcmpthese($res)の結果は

正規表現cfor分割fgrep 
正規表現1.05/s--23%-35%-44%
 cfor 1.36/s 30%--16%-28%
分割1.62/s 54%19%--14%
 fgrep 1.89/s 80%39%17%-

最適化されたCプログラムfgrepが上位にあることは驚くに値しません。 「regex」の「split」に対する遅れは、ほとんど一致しない場合にエンジンを起動するオーバーヘッドが原因である可能性があります(many回。進化する正規表現エンジンの最適化を考えると、これはPerlのバージョンによって異なる場合があります。 @codeforester( "cfor")の回答が含まれています。これは、最速であると主張されており、その_20%_は非常に類似した "split "は、散在する小さな非効率性が原因である可能性があります(この回答の下のコメントを参照してください)。

ハードウェアとソフトウェア、そしてデータの詳細には確かにばらつきがありますが、これは大きな違いはありません。私はこれをさまざまなPerlとマシンで実行しましたが、顕著な違いは、場合によってはfgrepの方が桁違いに速いことです。

OPの非常に遅いfgrepの経験は意外です。それらの引用された実行時間を考えると、上記よりも桁違いに遅いため、古いシステムには「責任がある」と思います。

これは完全にI/Oベースですが、複数のコアに配置することによる同時実行性の利点があり、数倍までの高速化が期待されます。


 残念ながら、コメントは削除されました(?)。つまり、ifの代わりに、スカラー(コスト)、definedブランチ、printfprintの不要なスカラー(コスト)の使用(遅い!)。これらは20億回線の効率にとって重要です。

18
zdim

少しスピードアップできるAwkを試してみましたか?

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

(または) Benjamin W。 からのコメントで示唆されているように、Awkindex()関数を使用する

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

(または)コメントの Ed Morton で提案されている、より直接的な正規表現一致、

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

は、あなたが必要とすることすべてです。 100万以上のエントリを持つファイルでは、これはより高速になりますが正確にはわかりません。ここでの問題は、ライン上のどこにでも一致する可能性があることです。同じことが特定の列にあった場合(例:$2のみ)、より高速なアプローチが可能です。

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

また、システムのlocaleセットで遊んで、速度を上げることもできます。この素晴らしい StéphaneChazelasの答え から言い換えると、ロケールLC_ALL=Cをコマンドlocally実行されています。

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=

1つの変数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文字セットのすべての文字を表すことができ、現在、世界中の書記体系を表示するのに役立ちます。 110,000を超える一意の文字を超えるのに対し、ASCIIでは、各文字は1バイトシーケンスでエンコードされ、その文字セットは128を超えない一意の文字で構成されます。

したがって、これに変換されます。UTF-8文字セットでエンコードされたファイルでgrepを使用する場合、各文字を10万の一意の文字のいずれかに一致させる必要がありますが、128ASCIIにあるので、fgrep

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

また、Awkmatch($0,i)呼び出しでregex一致を使用するため、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
9
Inian

前提条件:1.この検索をローカルワークステーションだけで実行したいとします。 2.並列検索を利用するために複数のコア/ CPUがある。

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

コンテキストに応じてさらにいくつかの微調整:A. LANG = CでNLSを無効にします(これは別の回答ですでに説明されています)B. -mフラグで一致の最大数を設定します。

注:file2は〜4GBで、10Mのブロックサイズは問題ないと思いますが、実行を最速にするためにブロックサイズを最適化する必要がある場合があります。

6
gregory

_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()サブルーチンは、 perlapi を使用して純粋なCで実装され、小さなファイルディクショナリ_%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倍速いことを示しています。

4
Håkon Hægland

次の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に4つ以上のフィールドがある場合、鉱山は機能します。)

  • Split + dictionaryソリューションと比較する方法はわかりません。

4
ikegami

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

Pythonとここのawkソリューションは両方とも完全な文字列の一致のみであり、部分的な正規表現スタイルの一致ではありません。

Awkソリューションは高速でPOSIXに準拠しているので、それがより良い答えです。

3
dawg

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をjoinの前で使用することにより、- HåkonHægland のベンチマークでわかるように、物事は本当にスピードアップします

PS1:結合がgrep -fよりも高速であるかどうか、私には疑問があります...

2
George Vasiliou

このスレッドは終了しましたが、2つのファイル間のすべての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ソリューションと同等ですが、ハッシュ配列全体に$ 2のfile2が含まれているかどうかをawkに要求しないため、さらに高速になる可能性があります。a[$ 2]が値かどうか。

ハッシュ配列の作成から最初のパターンファイルappartを読み取るときに、値も割り当てます。

データファイルの$2が以前にパターンファイルで見つかった場合、a[$2]には値があり、nullではないため出力されます。

データファイルのa[$2]が値を返さない場合(null)、これはfalseに変換されます=>印刷されません。

データファイルの3つのフィールドのいずれかに一致する拡張子:

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.

どちらの場合も、awkの前にLC_ALL = Cを適用すると、処理が速くなるようです。

PS1:このソリューションにはもちろん、すべてのawkソリューションの落とし穴があります。パターンマッチングではありません。ここにあるほとんどのソリューションのように、2つのファイル間の直接/固定マッチングです。

PS2: HåkonHægland の小さなベンチマークファイルを使用した貧弱なマシンベンチマークでは、awk 'FNR==NR{hash[$1]; next}$2 in hash' file1.txt FS='|' file2.txtに比べてパフォーマンスが約20%向上しています。

2
George Vasiliou

私はSQLite3を使用します:)おそらくメモリ内データベースか何か。ファイルをインポートし、SQLクエリを使用します。

1
user933161

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 in 10_000_000"テストを実行するのにかかる時間はLC_ALL=C fgrep...より3倍高速です。

1
JJoao

これには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>

ありがとう。

1
User9102d82

私見、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が役立つかもしれません。自分でテストできるように、フィードバックを送信するか、ファイルを送信してください。

1
BOC

可能な方法は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
1
oliv

言語などを設定することは、おそらく少し役に立ちます。

そうでなければ、私はあなたの基本的な問題から逃れるための魔法の解決策を考えることができません:データは構造化されていないので、file1の行数にfile2の行数を掛けた結果になる検索があります。

数十億行をデータベースに入れ、それをスマートな方法でインデックス付けすることは、私が考えることができる唯一のスピードアップです。ただし、そのインデックスは非常にスマートでなければなりません。

簡単な解決策は、すべてを収めるのに十分なメモリがあることです。そうでなければ、これについてあなたができることはこれ以上ありません...

0
rens