web-dev-qa-db-ja.com

別のファイルに保存されている行番号に基づいて大きなファイルから行を抽出する高速な方法

800億行の大きなファイルがあります。ここで、行番号がわかっている数行(約10000)を抽出したいと思います。これを処理する最速の方法は何ですか。

行番号を含む別のファイルを使用してそれらの行を抽出することは可能ですか?行番号のファイルの行番号は常に連続しているとは限りません。

たとえば、元のファイルは次のとおりです。

0.1
0.2
0.3
0.4
...

行番号ファイル:

1
3
4

出力:

0.1
0.3
0.4
4
user2842390

これに代替の方法と少しのベンチマークを加え、それに追加します Weijun Zhouの答え

join

行を抽出するdataファイルと、抽出する行の数をリストするline_numbersファイルがあると想定します。出力のソート順が重要でない場合は、 :

join <(sort padded_line_numbers) <(nl -w 12 -n rz data) | cut -d ' ' -f 2-

これにより、dataファイルの行に番号が付けられ、最初のフィールド(デフォルト)でpadded_line_numbersファイルと結合し、共通の行を出力します(結合フィールド自体を除いて、切り取られます) )。

joinでは、入力ファイルをアルファベット順にソートする必要があります。前述のpadded_line_numbersファイルは、line_numbersファイルの各行を左パディングして準備する必要があります。例えば。:

while read rownum; do
    printf '%.12d\n' "$rownum"
done <line_numbers >padded_line_numbers

-w 12 -n rzオプションと引数は、先頭に0を付けた12桁の長さの数値を出力するようにnlに指示します。

出力の並べ替え順序がline_numbersファイルの並べ替え順序と一致する必要がある場合は、以下を使用できます。

join -1 2 -2 1 <(nl padded_line_numbers | sort -k 2,2) \
    <(nl -w 12 -n rz data) |
    sort -k 2,2n |
    cut -d ' ' -f 3-

padded_line_numbersファイルに番号を付け、2番目のフィールドでアルファベット順に結果を並べ替え、番号付きのdataファイルと結合し、padded_line_numbersの元の並べ替え順序で結果を数値で並べ替えます。

ここでは、プロセスの置換が便宜上使用されています。それに依存できない、または依存したくない場合は、通常のファイルを作成して中間結果を保持するために必要なストレージを無駄にしたくない場合は、名前付きパイプを利用できます。

mkfifo padded_line_numbers
mkfifo numbered_data

while read rownum; do
    printf '%.12d\n' "$rownum"
done <line_numbers | nl | sort -k 2,2 >padded_line_numbers &

nl -w 12 -n rz data >numbered_data &

join -1 2 -2 1 padded_line_numbers numbered_data | sort -k 2,2n | cut -d ' ' -f 3-

ベンチマーク

質問の特異性はdataファイルの行数なので、同等の量のデータを使用して別のアプローチをテストすることは有用だと思いました。

私のテストでは、32億行のデータファイルを使用しました。各行はopenssl encからの2バイトのゴミであり、od -An -tx1 -w2を使用して16進エンコードされ、tr -d ' 'でスペースが削除されます。

$ head -n 3 data
c15d
061d
5787

$ wc -l data
3221254963 data

line_numbersファイルは、shuf from GNU Coreutils:を使用して、繰り返しなしで、1〜3,221,254,963の間で10,000の数値をランダムに選択することによって作成されました。

shuf -i 1-"$(wc -l <data)" -n 10000 >line_numbers

テスト環境は、i7-2670QM Intelクアッドコアプロセッサ、16 GiBメモリ、SSDストレージ、GNU/Linux、bash 5.0およびGNUツール。
私が測定した唯一の次元は、time Shellビルトインを使用した実行時間です。

ここで私は検討しています:

Perlが最速のようです:

$ time Perl_script line_numbers data | wc -l
10000

real    14m51.597s
user    14m41.878s
sys     0m9.299s

awkのパフォーマンスは同等に見えます。

$ time awk 'FNR==NR { seen[$0]++ }; FNR!=NR && FNR in seen' line_numbers data | wc -l
10000

real    29m3.808s
user    28m52.616s
sys     0m10.709s

joinも比較できるようです。

$ time join <(sort padded_line_numbers) <(nl -w 12 -n rz data) | wc -l
10000

real    28m24.053s
user    27m52.857s
sys     0m28.958s

上記のソートされたバージョンは、このバージョンと比べてパフォーマンスがほぼ低下しないことに注意してください。

最後に、sedはかなり遅いようです:約9時間後に私はそれを殺しました:

$ time sed -nf <(sed 's/$/p/' line_numbers) data | wc -l
^C

real    551m12.747s
user    550m53.390s
sys     0m15.624s
3
fra-san

これにはPerlスクリプトを使用します。私はこれを思いつきました:

#!/usr/bin/Perl

# usage: thisscript linenumberslist.txt contentsfile

unless (open(IN, $ARGV[0])) {
        die "Can't open list of line numbers file '$ARGV[0]'\n";
}
my %linenumbers = ();
while (<IN>) {
        chomp;
        $linenumbers{$_} = 1;
}

unless (open(IN, $ARGV[1])) {
        die "Can't open contents file '$ARGV[1]'\n";
}
$. = 0;
while (<IN>) {
        print if defined $linenumbers{$.};
}

exit;

これは最初に、関心のある行番号のリストを連想配列に読み込みます。ここで、行番号はキーです。 chompは、行末の改行を削除します$_は行そのものです。

次に、データファイルが開かれ、行番号が行番号の配列の既存のキーである場合、その行が出力されます。

$.は、Perlの行番号カウンターです。これは、読み込まれる行ごとに増加します。これはファイル全体でカウントされるため、データファイルの行を読み取る前にゼロにリセットします。

これはおそらく「Perl」スタイルではるかに多く書くことができるでしょうが、私はそれをもう少し読みやすくすることを好みます。

抽出したい行のリストが非常に大きい場合、これは最も効率的な方法ではないかもしれませんが、Perlはこれらの点で驚くほど効率的であることがよくあります。

リストされている順序で、つまり順次ではなく行を抽出する必要がある場合は、はるかに複雑になります...

2
wurtel

1つのライナー、sedを使用:

sed -nf <(sed 's/$/p/' linenumberfile) contentfile

linenumberfileで元の順序を維持するには、次のようにします

sed -nf <(sed 's/$/p/' linenumberfile) contentfile | paste <(nl linenumberfile | sort -n -k 2,2) - | sort -n -k 1,1 | cut -f 3-

説明

sed 's/$/p/' linenumberfile

指定された行を出力するsedスクリプトを生成します。次に、スクリプトを別のsedに送り(-nを使用してパターンスペースのデフォルトの印刷を抑制する)、実際の印刷を行います。 sedはコンテンツファイルを1行ずつ処理するため、出力はコンテンツファイルと同じ順序になります。これはワンパスプロセスであることに注意してください。したがって、速度は許容範囲内であると期待します。

プロセスを加速するには、p{p;b}に変更し、生成されたqスクリプトの最後にsedを追加します。

行番号ファイルにあるとおりに行の順序を保持するには、nlを使用して「行番号」を行番号ファイルに追加します。したがって、行番号ファイル

4
5
2

なるだろう

1 4
2 5
3 2

最初の列は、行番号ファイルの元の順序を記録しています。

「行番号」を含むファイルはsortedおよびpastedでsedの出力になり、

3 2    content_of_line2
1 4    content_of_line4
2 5    content_of_line5

次に、最初の列をキーとしてsortedされ、最終的に取得されます

1 4    content_of_line4
2 5    content_of_line5
3 2    content_of_line2

最後に、cutを使用して、2つの余分な列を削除します。

ベンチマーク

sedは数行の場合に最適ですが、Perlは質問で指定されている10000行の方法です。

$ cat /proc/cpuinfo | grep -A 4 -m 1 processor
processor   : 0
vendor_id   : GenuineIntel
cpu family  : 6
model       : 60
model name  : Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz

$ wc -l linenumber
10 linenumber

$ wc -l content
8982457 content

$ file content
content: ASCII text

$ time bash -c "sed -nf <(sed 's/$/p/' linenumber) content > /dev/null"    
real    0m0.791s
user    0m0.661s
sys     0m0.133s

$ time bash -c "awk 'FNR==NR { seen[$0]++ }; FNR!=NR && FNR in seen' linenumber content > /dev/null"
real    0m3.061s
user    0m2.908s
sys     0m0.152s

$ time bash -c "./ln.pl linenumber content > /dev/null"
real    0m1.706s
user    0m1.582s
sys     0m0.124s

$ ./genlinenumber.py 100 > linenumber
$ wc -l linenumber
100 linenumber

$ time bash -c "sed -nf <(sed 's/$/p/' linenumber) content > /dev/null"
real    0m3.326s
user    0m3.164s
sys     0m0.164s

$ time bash -c "awk 'FNR==NR { seen[$0]++ }; FNR!=NR && FNR in seen' linenumber content > /dev/null"
real    0m3.055s
user    0m2.890s
sys     0m0.164s

$ time bash -c "./ln.pl linenumber content > /dev/null"
real    0m1.769s
user    0m1.604s
sys     0m0.165s

行の順序を保持する必要がある場合、時間は無視できるため、最初の|の後のコマンドを引き続き使用できます。

$ ./genlinenumber.py 10000 > linenumber
$ wc -l linenumber
10000 linenumber

$ time bash -c "./ln.pl linenumber content > extract"
real    0m1.933s
user    0m1.791s
sys     0m0.141s

$ time bash -c "paste <(nl linenumber | sort -n -k 2,2) extract | sort -n -k 1,1 | cut -f 3- > /dev/null"
real    0m0.018s
user    0m0.012s
sys     0m0.005s
2
Weijun Zhou
micha@linux-micha: /tmp
$ cat numbers.txt
1
2
4
5

micha@linux-micha: /tmp
$ cat sentences.txt
alpha
bravo
charlie
delta
echo
foxtrott

micha@linux-micha: /tmp
$ awk 'FNR==NR { seen[$0]++ }; FNR!=NR && FNR in seen' numbers.txt sentences.txt
alpha
bravo
delta
echo
1
Micha