web-dev-qa-db-ja.com

多くの大きなファイルで重複する行を見つける方法は?

30k以下のファイルがあります。各ファイルには〜100k行が含まれます。行にはスペースが含まれていません。個々のファイル内の行は並べ替えられ、複製されません。

私の目標:2つ以上のファイルにまたがるすべてのall重複行、および重複したエントリを含むファイルの名前も検索したい。

簡単な解決策はこれです:

cat *.words | sort | uniq -c | grep -v -F '1 '

そして私は走るでしょう:

grep 'duplicated entry' *.words

より効率的な方法がわかりますか?

8
Lars Schneider

すべての入力ファイルは既に並べ替えられているため、実際の並べ替え手順をバイパスし、_マージファイルをまとめてsort -mを使用するだけです。

一部のUnixシステム(私の知る限りonly Linux)では、これで十分な場合があります

sort -m *.words | uniq -d >dupes.txt

重複する行をファイルdupes.txtに書き込みます。

これらの行がどのファイルからのものかを見つけるには、次のようにします

grep -Fx -f dupes.txt *.words

これは、grepdupes.txt-f dupes.txt)の行を固定文字列パターン-F)として扱うように指示します。 grepでは、行全体が最初から最後まで完全に一致する必要があります(-x)。ファイル名と行を端末に出力します。

Linux以外のUnices(またはmoreファイル)

一部のUnixシステムでは、30000のファイル名が、単一のユーティリティに渡すには長すぎる文字列に拡張されます(つまり、sort -m *.wordsは、OpenBSDシステムではArgument list too longで失敗します)。 Linuxでさえ、ファイルの数がはるかに多い場合、これについて文句を言うでしょう。

だまし絵を見つける

これは、一般的なケース(これはmany 30000ファイル以上でも機能します)では、ソートを「チャンク」する必要があることを意味します。

rm -f tmpfile
find . -type f -name '*.words' -print0 |
xargs -0 sh -c '
    if [ -f tmpfile ]; then
        sort -o tmpfile -m tmpfile "$@"
    else
        sort -o tmpfile -m "$@"
    fi' sh 

または、tmpfileなしでxargsを作成します。

rm -f tmpfile
find . -type f -name '*.words' -exec sh -c '
    if [ -f tmpfile ]; then
        sort -o tmpfile -m tmpfile "$@"
    else
        sort -o tmpfile -m "$@"
    fi' sh {} +

これにより、名前が*.wordsと一致する現在のディレクトリ(またはその下)のすべてのファイルが検索されます。一度にこれらの名前の適切なサイズのチャンク(サイズはxargs/findによって決定されます)の場合、それらをまとめて、ソートされたtmpfileファイルにマージします。 tmpfileがすでに存在する場合(最初のチャンクを除くすべて)、このファイルは現在のチャンク内の他のファイルともマージされます。ファイル名の長さとコマンドラインの最大許容長に応じて、内部スクリプトを10回以上実行する必要があります(find/xargsはこれを実行します自動的に)。

「内部」shスクリプト、

if [ -f tmpfile ]; then
    sort -o tmpfile -m tmpfile "$@"
else
    sort -o tmpfile -m "$@"
fi

sort -o tmpfileを使用してtmpfileに出力します(これがtmpfileへの入力であってもsortを上書きしません)および-mを使用してマージ。両方のブランチで、"$@"は、findまたはxargsからスクリプトに渡される個別に引用されたファイル名のリストに展開されます。

次に、tmpfileに対してuniq -dを実行して、重複するすべての行を取得します。

uniq -d tmpfile >dupes.txt

「DRY」の原則(「Do n't Repeat Yourself」)が好きな場合は、内部スクリプトを次のように書くことができます。

if [ -f tmpfile ]; then
    t=tmpfile
else
    t=/dev/null
fi

sort -o tmpfile -m "$t" "$@"

または

t=tmpfile
[ ! -f "$t" ] && t=/dev/null
sort -o tmpfile -m "$t" "$@"

彼らはどこから来ましたか?

上記と同じ理由で、grep -Fx -f dupes.txt *.wordsを使用してこれらの重複の原因を特定することはできないため、代わりにfindを使用します。

find . -type f -name '*.words' \
    -exec grep -Fx -f dupes.txt {} +

実行する「複雑な」処理はないため、-execから直接grepを呼び出すことができます。 -execオプションはユーティリティコマンドを受け取り、見つかった名前を{}に配置します。 +が最後にある場合、findは、ユーティリティの各呼び出しで現在のシェルがサポートするのと同じ数の引数を{}の代わりに配置します。

完全にであるためには、どちらかを使用することができます

find . -type f -name '*.words' \
    -exec grep -H -Fx -f dupes.txt {} +

または

find . -type f -name '*.words' \
    -exec grep -Fx -f dupes.txt /dev/null {} +

ファイル名が常にgrepからの出力に含まれるようにしてください。

最初のバリエーションでは、grep -Hを使用して、常に一致するファイル名を出力します。最後のバリエーションでは、コマンドラインで1つ以上のファイルが指定されている場合、grepには一致するファイルの名前が含まれるという事実を使用します。

grepからfindに送信されたファイル名の最後のチャンクには、実際には単一のファイル名しか含まれていない可能性があるため、これは重要です。この場合、grepは結果にそれを記載しません。


ボーナス素材:

find + xargs + shコマンドを分析する:

find . -type f -name '*.words' -print0 |
xargs -0 sh -c '
    if [ -f tmpfile ]; then
        sort -o tmpfile -m tmpfile "$@"
    else
        sort -o tmpfile -m "$@"
    fi' sh 

find . -type f -name '*.words'は、現在のディレクトリ(またはその下)からパス名のリストを生成します。各パス名は通常のファイル-type f)のパス名であり、ファイル名コンポーネントが*.wordsに一致する終了。 currentディレクトリのみを検索する場合は、-maxdepth 1の後ろ、.の前に-type fを追加できます。

-print0は、見つかったすべてのパス名が\0nul)文字を区切り文字として出力されることを保証します。これは、Unixパスでは無効な文字であり、改行文字(または他の奇妙なもの)が含まれている場合でもパス名を処理できます。

findは、その出力をxargsにパイプします。

xargs -0\0で区切られたパス名のリストを読み取り、指定されたユーティリティをこれらのチャンクで繰り返し実行します。これにより、シェルがあまりに文句を言わないように十分な引数でユーティリティが実行されます。 findからの入力がなくなるまで、長い引数リスト。

xargsによって呼び出されるユーティリティはshであり、スクリプトはコマンドラインで-cフラグを使用して文字列として指定されます。

以下の引数を使用してsh -c '...some script...'を呼び出すと、引数は$@最初の引数を除くでスクリプトで使用できるようになり、$0(これは、たとえばtopで見つけることができる「コマンド名」です。これが、実際のスクリプトの終わりの後に最初の引数として文字列shを挿入する理由です。文字列shダミー引数であり、任意の単一の単語にすることができます(_またはsh-findを好む人もいます)。

12
Kusalananda

個々のファイル内の行はソートされ、複製されません。

つまり、おそらくsort -m

 -m, --merge
        merge already sorted files; do not sort

これを行う他の明白な代替策は、配列の行を収集してそれらを数える単純なawkです。しかし、 @ dave_thompson_085 がコメントしているように、これらの300万行(または固有の行は多数あります)を格納するにはかなりの量のメモリが必要になるため、うまく機能しない可能性があります。

8
ilkkachu

最適化sort + uniqソリューション:

sort --parallel=30000 *.words | uniq -d
  • --parallel=N-同時に実行するソートの数をNに変更します
  • -d, --repeated-グループごとに1つずつ、重複する行のみを印刷します
3
RomanPerekhrest

Awkを使用すると、1つの短いコマンドですべてのファイルのすべての繰り返し行を取得できます。

$ awk '_[$0]++' *.words

ただし、行が3回以上存在する場合は、行が繰り返されます。
最初の重複のみを取得する解決策があります:

$ awk '_[$0]++==1' *.words

(リピートが少ない場合)かなり高速ですが、すべての行をメモリに保持するために大量のメモリを消費します。たぶん、実際のファイルと繰り返しに応じて、3つまたは4つのファイルを最初に試してください。

$ awk '_[$0]++==1' [123]*.words

それ以外の場合は、次のことができます。

$ sort -m *.words | uniq -d

Uniqの繰り返し行を印刷します。

3
Isaac