行番号でファイルをフィルターする


17

行ごとに1つの非負の整数を持つファイルLとテキストファイルFが与えられた場合、ファイルLに行番号が表示されるFの行のみを保持する高速な方法は何でしょうか。

例:

$ cat L.txt
1
3

$ cat F.txt
Hello World
Hallo Welt
Hola mundo

$ command-in-question -x L.txt F.txt
Hello World
Hola mundo

5億以上のエントリを持つファイルLを処理できるコマンドを探しています。ファイルLは数値順にソートされます。

注:私はaの実装の途中ですcommand-in-questionが、ここでもUnixツールを使用できるかどうか疑問に思いました。


更新:すべての答えをありがとう、私は今日多くを学びました!もう1つ答えを受け入れたいのですが、それは不可能です。


ファイルFには何行ありますか?
Peter.O

1
Fで多くの行として少なくともL.のようにあります
ミク

回答:


8

C意味のあるエラーメッセージを省略:

#include <stdio.h>
#include <stdlib.h>

int main (int argc, char *argv[]) {

    FILE *L;
    FILE *F;

    unsigned int to_print;
    unsigned int current = 0;
    char *line = NULL;
    size_t len = 0;

    if ((L = fopen(argv[1], "r")) == NULL) {
        return 1;
    } else if ((F = fopen(argv[2], "r")) == NULL) {
        fclose(L);
        return 1;
    } else {

        while (fscanf(L, "%u", &to_print) > 0) {
            while (getline(&line, &len, F) != -1 && ++current != to_print);
            if (current == to_print) {
                printf("%s", line);
            }
        }

        free(line);
        fclose(L);
        fclose(F);
        return 0;
    }
}

2
これは、ここで最もパフォーマンスの高い答えです。少なくとも、私のテストではそうです。誰かが興味がある場合は、次のようにコンパイルしましたxsel -bo | cc -xc - -o cselect。そしてそれはちょうど働いた-それは2つのライブラリだけが必要です。
mikeserv

1
ありがとう、これは素晴らしい!気にしないことを願っていますが、コードを小さなツールにまとめました。
ミク

1
@mikuさあ、助けてくれてうれしいです。LINE_MAXバージョンが増えていることに気づいたので、おそらくファイルの非常に大きな行で作業することになります。getline()行サイズ制限を削除するために使用するバージョンでAを更新しました。
FloHimself

@FloHimself、まあ、もう一度ありがとう:)実際、いくつかの入力行はを超えるかもしれないLINE_MAXので、getlineちょうどいいようです。
ミク

10

を使用しますがawk、コンテンツ全体L.txtをメモリに保存せず、不要なハッシュ検索を行います;-)。

list=L.txt file=F.txt
LIST="$list" awk '
  function nextline() {
    if ((getline n < list) <=0) exit
  }
  BEGIN{
    list = ENVIRON["LIST"]
    nextline()
  }
  NR == n {
    print
    nextline()
  }' < "$file"

まさに、ハッシュマップを試してみましたが、メモリを超えるでしょう。ビットセットはより多くのヘッドルームを購入します。しかし、入力がソートされるという事実を使用することにより、この(スペース)問題を完全に取り除くことができます。
ミク

1
@Janis; それは単なる標準の良いコーディング慣行の場合ではありません:リテラルをハードコーディングしないでください-代わりに変数を使用してください...(より柔軟でエラーが少なく、保守が容易です)
-Peter.O

1
@StéphaneChazelas:の事前ループ初期化が必要ですn。そうでなければ(1L.txt
現状のまま)失敗

1
@ Peter.O、おっと、それは私がNR> = nで対処しようとしたことですが、それは間違っていました。今より良いはずです。
ステファンシャゼル

1
@Janis、アイデアは、そのコードをcommand-in-questionスクリプトに埋め込む場合、ファイル名をコードに埋め込むことはできないというものでした。-v list="$opt_x"awkがバックスラッシュ処理を行うため、どちらも機能しません。ここで代わりにENVIRONを使用する理由です。
ステファンシャゼル

10

grep -n | sort | sed | cut

(   export LC_ALL=C
    grep -n ''   | sort -t:  -nmk1,1 ./L - |
    sed /:/d\;n  | cut  -sd: -f2-
)   <./F

これは、任意のサイズの入力で非常に迅速に動作するはずです(一部のタイミングテストが以下に含まれています)。方法に関する注意:

  • export LC_ALL=C
    • 次の操作のポイントは、linenoのファイル./Fとインラインでスタックされたファイル全体を取得することなので、./L実際に心配する必要がある文字はASCII [0-9]数字と:コロンだけです。
    • そのため、UTF-8が関係する場合よりも、128個の候補のセットでこれらの11文字を見つけることを心配する方が簡単です。
  • grep -n ''
    • これにより、文字列LINENO:がstdin-またはの各行の先頭に挿入され<./Fます。
  • sort -t: -nmk1,1 ./L -
    • sort代わりに、すべての入力ファイルをソートするため無視して、(正しくは)彼らは事前にソートされている前提と-mしてそれらをerges -numericallyソート順、基本的にすべての可能越えて何も無視して-k1,1目が発生して-t:、とにかくコロン文字を。
    • これにはいくつかの一時スペースが必要になる場合がありますが(シーケンスの間隔によって異なります)、適切な並べ替えに比べてそれほど必要ではなく、バックトラッキングがゼロであるため非常に高速です。
    • sortは、linenoのin ./Lがの対応する行の直前にある単一のストリームを出力し./Fます。./Lの行は短いため、常に最初に表示されます。
  • sed /:/d\;n
    • 現在の行が/:/コロンに一致する場合、d出力からそれを選択します。そうでない場合は、現在のn行とext行を自動印刷します。
    • そのため、コロンと次の行に一致しない連続した行のペアのみに、または、次の行から次の行にのみsedプルーニングsortの出力が行われます。./L
  • cut -sd: -f2-
    • cut -s出力から、少なくとも1つの-d:区切り文字列を含まない入力行の出力を抑制します./L。したがって、の行は完全に削除されます。
    • これらの行については、:コロンで区切られた最初の-fフィールドがcut離れているため、すべてのgrep'挿入されたlineno'も同様です。

小入力テスト

seq 5 | sed -ne'2,3!w /tmp/L
        s/.*/a-z &\& 0-9/p' >/tmp/F

... 5行のサンプル入力を生成します。その後...

(   export LC_ALL=C; </tmp/F \
    grep -n ''   | sort -t:  -nmk1,1 ./L - |
    sed /:/d\;n  | cut  -sd: -f2-
)|  head - /tmp[FL]

...プリント...

==> standard input <==
a-z 1& 0-9
a-z 4& 0-9
a-z 5& 0-9

==> /tmp/F <==
a-z 1& 0-9
a-z 2& 0-9
a-z 3& 0-9
a-z 4& 0-9
a-z 5& 0-9

==> /tmp/L <==
1
4
5

より大きな時限テスト

かなり大きなファイルをいくつか作成しました。

seq 5000000 | tee /tmp/F |
sort -R | head -n1500000 |
sort -n >/tmp/L

... /tmp/F5mil 行を挿入し、1.5mil行をランダムに選択し/tmp/Lます。私はそれから:

time \
(   export LC_ALL=C
    grep -n ''   | sort -t:  -nmk1,1 ./L - |
    sed /:/d\;n  | cut  -sd: -f2-
)   <./F |wc - l

印刷した:

1500000
grep -n '' \
    0.82s user 0.05s system 73% cpu 1.185 total
sort -t: -nmk1,1 /tmp/L - \
    0.92s user 0.11s system 86% cpu 1.185 total
sed /:/d\;n \
    1.02s user 0.14s system 98% cpu 1.185 total
cut -sd: -f2- \
    0.79s user 0.17s system 80% cpu 1.184 total
wc -l \
    0.05s user 0.07s system 10% cpu 1.183 total

(そこにバックスラッシュを追加しました)

ここで現在提供されているソリューションの中で、これはすべての中で最速ですが、私のマシンで上記で生成されたデータセットと比較した場合のものです。他の人の中で、2位争いに近づいたのは1人だけで、それがperl ここにあります

これは決して元のソリューションが提供するものではありません-他の人から提供されたアドバイスやインスピレーションのおかげで、実行時間の3分の1が短縮されました。遅いソリューションについては投稿履歴を参照してください(しかし、なぜですか?)

また、システムのマルチCPUアーキテクチャとそのパイプライン内の各プロセスの同時実行に対応していなければ、他の回答がより適切に競合する可能性があることに注意してください。それらはすべて同時に動作します-それぞれ独自のプロセッサコア上で-データをやり取りし、全体の小さな部分を実行します。超カッコイイ。

しかし、最速のソリューションは...

しかし、それは最速のソリューションではありません。ここで提供される最速のソリューションは、Cプログラムです。私はそれを呼んだcselect。Xクリップボードにコピーした後、次のようにコンパイルしました。

xsel -bo | cc -xc - -o cselect

私はそれから:

time \
    ./cselect /tmp/L /tmp/F |
wc -l

...そして結果は...

1500000
./cselect /tmp/L /tmp/F  \
    0.50s user 0.05s system 99% cpu 0.551 total
wc -l \
    0.05s user 0.05s system 19% cpu 0.551 total

1
あなたが、それはかなり速く(ほぼ同じ速いマルチコアシステム上の鉱山として)行うことができますsed -ne'/:/!{n;p;}' | cut -d: -f2-代わりにsed -ne'/:/!N;/\n/s/[^:]*://p'
ステファンChazelas

@StéphaneChazelas- seds を切り替えると、より良い結果が得られる場合があります- sed私が使用しているのは家宝ですsed- 結果にalias値が表示されtimeます。ちなみに、私の家宝のパッケージは、musl libc(TREに基づく正規表現の実装)に対して静的にコンパイルされています。GNUに切り替えてsed-なしで実行するcutと、完了時間(2.8秒)に1秒が追加され、3分の1以上の時間がかかります。そして、それは私のシステム上であなたよりもわずか0.3秒速いです。
mikeserv

1
sort -mn対照的に、sort -nmk1,1あなたはここで(テストされていない)分割を行う必要はありませんと良いかもしれない
ステファンChazelas

@StéphaneChazelas-ええ、私は同じことを考えて、あらゆる方法で試しました。-n行の最初の数値文字列を実行するだけの仕様ですので、わかりました、-mnまたは-nm、何らかの理由で、完了時間で2秒未満に落ちたのは、すべてのオプションをそのまま追加したときだけでした。それは奇妙です-そして、それが昨日私がそもそもタックしなかった理由です-m-私は自分が何であるかを知っていましたが、何らかの自動最適化のものとしてうまくいくように見えました。興味深いことに、家宝にsortは...に-zのみ適用される文字列長のオプションがあります-[cm]
。– mikeserv

-n、行の最初の数値文字列ではありません。数がようにそれはちょうどラインを考慮しabc 123、それができないように0になり少ないとより効率的-t: -k1,1
ステファンChazelas

9

私が使用したいawk

awk 'NR==FNR {a[$1]; next}; FNR in a' L.txt F.txt

更新:パフォーマンスを測定しました。比較は非常に高速であり、ハッシュテーブルを構築するのに必要な労力を過大に補償するため、このバージョンは非常に大きなデータセットを使用する場合(前述の要件の場合と同様)にさらに拡張できるようです。


1
@ミク; はい、それはすてきなコンパクトなソリューションです。しかし、注意点。すべてawkのがこのような巨大なデータセットを処理できるわけではありません。-私はGNU awkを使用していますが、問題はありません。5億行のデータを使用したテストには7分かかりました。
ジャニス

1
(比較して)これはむしろ遅いです real 16m3.468s- - 。user 15m48.447s sys 0m10.725s3.3 GBのRAMを使用Lして、50,000,000行で1/10のサイズをテストしました 。そして、F5億行- ステファンChazelas' awkのANSERのための時間:real 2m11.637s- user 2m2.748s- sys 0m6.424s-私は高速のボックスを使用していないんだけど、比較が興味深いです。
-Peter.O

@ Peter.O; データをありがとう!(私自身のテストケースでは)5億行が連想配列に格納されていることを考えると、より遅い速度が予想されました。(だから、私はステファンの提案に対して上記の「(+1)」とコメントしました。)-この簡潔なソリューションがまだ1秒あたり100万行を処理していることに驚きましたが!私はそれがこのコードパターンを(それが単純だから!)実行可能なオプションにし、特に極端なデータサイズが少ない場合に有効だと思います。
ジャニス

それは間違いなく実行可能なソリューションです。私が使用したテストデータ(5mil行/1.5mil L)では、4秒強で完了しました。Stephaneの答えよりもわずか1秒遅れています。テストセットを生成するために使用されるコードは私の答えにありますが、ほとんどは単にseq出力であり、Lの同じより小さなランダムに選択されたサブセットです。
mikeserv

1
データファイルのサイズを5億行、キーファイルのサイズを5,000万およびそれ以上にして、さらにパフォーマンスを測定しました。5億行、注目すべき観察結果。小さいキーファイルでは、時間は4分(ステファン)対8分(ジャニス)で、大きいキーファイルでは19分(ステファン)対12分(ジャニス)です。
ジャニス

3

完璧を期すために、perlがawkよりも高速である可能性を期待して、StéphaneChazelasの回答の優れたawkスクリプトと、kosの回答のperlスクリプトをマージできます。(元の質問に一致するように引数の順序を変更しました)。

#!/usr/bin/env perl
use strict;

die "Usage: $0 l f\n" if $#ARGV+1 != 2;
open(L,$ARGV[0]) or die "$ARGV[0]: $!";
open(F,$ARGV[1]) or die "$ARGV[1]: $!";

while(my $number = <L>){
    #chop $number;
    while (<F>) {
        if($. == $number){
            print;
            last;
        }
    }
}

これはawk。それは私のものと同じくらい速いです-私はちょうど今3回両方をテストし、そのたびに私のものは1.8ミリ秒で私の5milラインテストセットを処理し、毎回1.9 ...秒でテストしました。気になればテストセットのgenコードは私の答えにありますが、ポイントは非常に良いということです。さらに、出力は正しいです-私はまだawk仕事をすることができません...それでも、私たちの答えはどちらもFloHimself'sによって恥ずかしさを感じています。
mikeserv

@mikeserv、異なるが必要awkです。サンプルでは、​​gawkで1.4秒(Janisでは4秒)、mawkでは0.9秒、このperlソリューションでは1.7秒、​​kos 'では2.3秒、GNU sedでは4.5秒、GNU sedでは1.4秒( GNU sed)および私の提案された改善(およびCソリューションの0.5秒)。
ステファンシャゼラス

@mikeserv、ああ!もちろん、あなたのアプローチでは、ロケールによって違いが生じます。UFT-8からCに切り替えると、ここでは4.5秒から2.3秒に減少します。
StéphaneChazelas 15年

3

そのための簡単なPerlスクリプトを作成しました。

Usage: script.pl inputfile_f inputfile_f

#!/usr/bin/env perl

$number_arguments = $#ARGV + 1;
if ($number_arguments != 2) {
    die "Usage: script.pl inputfile_f inputfile_l\n";
}

open($f, '<', $ARGV[0])
    or die "$ARGV[0]: Not found\n";
open($l, '<', $ARGV[1])
    or die "$ARGV[1]: Not found\n";

@line_numbers = <$l>;

while ($line = <$f>) {
    $count_f ++;
    if ($count_f == @line_numbers[$count_l]) {
        print $line;
        $count_l ++;
    }
}
  • 荷重 F.txt
  • 荷重 L.txt
  • の各行をL.txt配列に格納します
  • F.txt行ごとに読み取り、現在の行番号と現在の配列インデックスを追跡します。増加F.txt現在の行番号。F.txt現在の行番号が現在の配列インデックスの配列の内容と一致する場合、現在の行を出力し、インデックスを増やします

コストと複雑さに関する考慮事項

割り当てにかかるコスト、比較にかかるコスト、および行を印刷するコストを考慮して、N 1を入力行数として、F.txtN 2を行数として指定するL.txtと、whileループは最大でN 1回実行されます。 2N 1 + N 2の割り当て(明らかにN 1 > N 2であると仮定)、2N 1の比較、およびN 2の印刷につながります。各操作のコストに等しいとすると、whileループを実行するための総コストは4N 1 + 2N 2になり、O(N)のスクリプトが複雑になります。

1000万行の入力ファイルでテストします

F.txtランダムな50文字の行を含む1,000万行のファイルと、L.txt1〜10000000の数字を含む1000万行のファイルを使用する(最悪のシナリオ):

~/tmp$ for ((i=0; i<3; i++)); do time ./script.pl F.txt L.txt > output; done

real    0m15.628s
user    0m13.396s
sys 0m2.180s

real    0m16.001s
user    0m13.376s
sys 0m2.436s

real    0m16.153s
user    0m13.564s
sys 0m2.304s

2

このperlソリューションは、他のawkまたはperlソリューションよりも20%程度高速ですが、明らかにCのソリューションほど高速ではありません。

perl -e '
  open L, shift or die $!;
  open F, shift or die $!;
  exit if ! ($n = <L>);
  while (1) {
    $_ = <F>;
    next if $. != $n;
    print;
    exit if ! ($n = <L>);
  }
' -- L F

0
cat <<! >L.txt
1
3
!

cat <<! >F.txt
Hello World
Hallo Welt
Hola mundo
!

cmd(){
 L=$1 F=$2
 cat -n $F |
 join $L - |
 sed 's/[^ ]* //'
}

cmd L.txt F.txt
Hello World
Hola mundo

L.txtはソートされているため、結合を使用できます。F.txtの各行に番号を付け、2つのファイルを結合して、行番号を削除します。大きな中間ファイルは必要ありません。

実際、上記はすべての空白を単一のスペースに置き換えることによりデータ行を破壊します。行をそのままにするには、データに表示されない文字(「|」など)を区切り文字として選択する必要があります。cmdは

cmd(){
 L=$1 F=$2
 cat -n $F |
 sed 's/^ *//;s/\t/|/' |
 join -t'|' $L - |
 sed 's/[^|]*|//'
}

最初のsedは、「cat -n」出力から先頭のスペースを削除し、タブを置き換えます。2番目のsedは行番号と「|」を削除します。


これは大きなファイルでは動作しないのではないかと心配しています。10行未満が必要です。私は同じ考えを持っていて試しましたjoin L.txt <(nl F.txt )が、大きなファイルでは動作しません。ちなみに、新しいユーザーからこのような明確で適切な形式の回答が得られることはあまりありません。
テルドン

@terdon、はい、数値でソートされた入力では動作しないjoin/ commできないという残念です。
ステファンシャゼル

@terdon:私はあなたのリード(現在削除されています)をフォローアップし、試しましたjoin -t' ' <(<L.txt awk '{printf("%010s\n",$0)}') <(<F.txt awk '{printf("%010s %s\n",NR,$0)}') | cut -d' ' -f2--遅かったです!-そして、適切な0が埋め込まれたキーで準備されたファイルをフィードしてもjoin -t' ' L.txt F.txt | cut -d' ' -f2- 、それはまだ遅かった(準備時間を含まない)- awk@Janisによる回答よりも遅かった彼と@StéphaneChazelasの答え
Peter.O

@ Peter.Oええ。はawksの1つを回避する同様のアプローチを試みましたが、それを機能させるだけでなく価値がある方法を見つけることができませんでした。
テルドン

@terdon及びその他のための実際の時間join+のawk printf プロセスsubstiturionがあったreal 20m11.663s user 19m35.093s sys 0m10.513s ステファンChazelas'対real 2m11.637s user 2m2.748s sys 0m6.424s 使用L5000万線、F5億ライン。
Peter.O

0

完全を期すために、join解決策の別の試み:

sed -r 's/^/00000000000000/;s/[0-9]*([0-9]{15})/\1/' /tmp/L | join <( nl -w15 -nrz /tmp/F ) - | cut -d' ' -f2-

これは、数値が常に15桁の長さになるように、結合する行番号列を先行ゼロ付きの固定長としてフォーマットすることで機能します。これにより、通常の数値のソート順ではなく結合の問題が回避されます。これは、列が事実上辞書ソートになることを余儀なくされたためです。 nlこの形式の行番号をF.txtに追加するために使用されます。残念ながらsed、L.txtの番号を再フォーマットするために使用する必要があります。

このアプローチは、@ mikeservのメソッドを使用して生成されたテストデータで正常に機能するようです。しかし、それでもまだ非常に遅いです-私のマシンではcソリューションは60倍高速です。時間の約2/3が費やされsed、1/3 が費やされjoinます。おそらく、より良いsed式があります...


わかりました-しかし、なぜすべてのゼロを追加するのですか?この感覚をつかもうとしています。また、はnl非常に優れていますが、テストされていない入力では堅牢に使用できません。とてもクールなものの1つは、論理ページの -d区切り文字です。デフォルトでは、文字列のみで構成される:\` (ただし、最後の墓を除く)行が 1、2、3、または3回連続して入力されている場合、カウントは少し狂ってしまいます。それで実験-それはかなりきれいです。特に、nl`が1つの区切り文字列を含む行を読み取り、その後さらに3または2の行を読み取ったときに何が起こるかを見てください
-mikeserv

0

受け入れられた答えはCであるため、ここにPythonソリューションを投げても問題ないと考えました:

# Read mask
with open('L.txt', 'r') as f:
    mask = [int(line_num) for line_num in f.read().splitlines()]

# Filter input file
filtered_lines = []
with open('F.txt', 'r') as f:
    for i, line in enumerate(f.read().splitlines()):
        if (i+1) in mask:
            filtered_lines.append(line)

# Write newly filtered file
with open('F_filtered.txt', 'w') as f:
    for line in filtered_lines:
        f.write('%s\n' % line)

numpyのような外部ライブラリを使用する場合、ソリューションはさらにエレガントになります。

import numpy as np

with open('L.txt', 'r') as f:
    mask = np.array([int(line_num)-1 for line_num in f.read().splitlines()])

with open('F.txt', 'r') as f:
    lines = np.array(f.read().splitlines())
filtered_lines = lines[mask]

with open('F_filtered.txt', 'w') as f:
    for line in filtered_lines:
        f.write('%s\n' % line)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.