web-dev-qa-db-ja.com

ファイル内の任意の場所に複数のキーワードを含むファイルを検索します

ファイル内の任意の場所にある、探しているキーワードの完全なセットを含むディレクトリ内のすべてのファイルを一覧表示する方法を探しています。

したがって、キーワードが同じ行に表示される必要はありません。

これを行う1つの方法は次のとおりです。

grep -l one $(grep -l two $(grep -l three *))

3つのキーワードは単なる例であり、2つまたは4つなども同様です。

私が考えることができる2番目の方法は次のとおりです。

grep -l one * | xargs grep -l two | xargs grep -l three

別の質問 に現れる3番目の方法は、次のようになります。

find . -type f \
  -exec grep -q one {} \; -a \
  -exec grep -q two {} \; -a \
  -exec grep -q three {} \; -a -print

しかし、これは間違いなく私がここに向かっている方向ではありません。入力が少なくて済み、grepawkPerlなどへの呼び出しが1回だけ必要なものが必要です。

たとえば、次のように awkを使用すると、すべてのキーワードを含む行を一致させることができます が好きです。

awk '/one/ && /two/ && /three/' *

または、ファイル名のみを印刷します。

awk '/one/ && /two/ && /three/ { print FILENAME ; nextfile }' *

しかし、キーワードが同じ行にある必要はなく、ファイル内のどこにでもある可能性があるファイルを見つけたいのです。


推奨されるソリューションはgzip対応です。たとえば、grepには、圧縮ファイルで機能するzgrepバリアントがあります。私がこれに言及する理由は、この制約が与えられた場合、一部のソリューションはうまく機能しない可能性があることです。たとえば、一致するファイルを印刷するawkの例では、次のことはできません。

zcat * | awk '/pattern/ {print FILENAME; nextfile}'

コマンドを次のように大幅に変更する必要があります。

for f in *; do zcat $f | awk -v F=$f '/pattern/ { print F; nextfile }'; done

そのため、制約があるため、圧縮されていないファイルでは一度しか実行できなかったとしても、awkを何度も呼び出す必要があります。そしてもちろん、zawk '/pattern/ {print FILENAME; nextfile}' *と同じ効果が得られるので、これを可能にするソリューションを選びます。

16
arekolek

これまでに提案されたすべてのソリューションの中で、grepを使用した私のオリジナルのソリューションは最速で、25秒で終了します。欠点は、キーワードを追加したり削除したりするのが面倒なことです。そこで、動作をシミュレートしながら、構文を変更できるスクリプト(multiと呼ばれる)を思い付きました。

#!/bin/bash

# Usage: multi [z]grep PATTERNS -- FILES

command=$1

# first two arguments constitute the first command
command_head="$1 -le '$2'"
shift 2

# arguments before double-dash are keywords to be piped with xargs
while (("$#")) && [ "$1" != -- ] ; do
  command_tail+="| xargs $command -le '$1' "
  shift
done
shift

# remaining arguments are files
eval "$command_head $@ $command_tail"

したがって、multi grep one two three -- *を書くことは、私の最初の提案と同等であり、同時に実行されます。代わりにzgrepを最初の引数として使用することで、圧縮ファイルでも簡単に使用できます。

その他の解決策

また、Pythonスクリプトを使用して、すべてのキーワードを行ごとに検索することと、ファイル全体をキーワードごとに検索することの2つの戦略を使用してスクリプトを試しました。2番目の戦略は、私の場合より高速でしたが、より低速でしたgrepを使用するだけではなく、33秒で終了します。行ごとのキーワードマッチングは60秒で終了します。

#!/usr/bin/python3

import gzip, sys

i = sys.argv.index('--')
patterns = sys.argv[1:i]
files = sys.argv[i+1:]

for f in files:
  with (gzip.open if f.endswith('.gz') else open)(f, 'rt') as s:
    txt = s.read()
    if all(p in txt for p in patterns):
      print(f)

terdonによって与えられたスクリプト は54秒で終了しました。私のプロセッサーはデュアルコアなので、実際には39秒のウォールタイムがかかりました。私のPythonスクリプトは49秒のウォールタイムを必要としました(そしてgrepは29秒でした)。

cas by cas は、4秒未満のgrepで処理されたファイルの数が少ない場合でも、適切な時間で終了できなかったため、強制終了する必要がありました。

しかし、彼の最初のawk提案は、そのままではgrepより遅いにもかかわらず、潜在的な利点があります。場合によっては、少なくとも私の経験では、すべてのキーワードがファイル内にある場合、すべてがファイルの先頭のどこかにあると期待することができます。これにより、このソリューションのパフォーマンスが劇的に向上します。

for f in *; do
  zcat $f | awk -v F=$f \
    'NR>100 {exit} /one/ {a++} /two/ {b++} /three/ {c++} a&&b&&c {print F; exit}'
done

25秒ではなく、1/4秒で終了します。

もちろん、ファイルの先頭近くにあることがわかっているキーワードを検索する利点はありません。そのような場合、NR>100 {exit}なしのソリューションには63秒かかります(実時間の50秒)。

非圧縮ファイル

私のgrepソリューションとcas 'awkプロポーザルの間の実行時間に大きな違いはありません。どちらも実行に数秒かかります。

変数の初期化FNR == 1 { f1=f2=f3=0; }は、後続のすべての処理済みファイルのカウンターをリセットする場合に必須です。そのため、このソリューションでは、キーワードを変更したり、新しいキーワードを追加したりする場合に、3つの場所でコマンドを編集する必要があります。一方、grepを使用すると、| xargs grep -l fourを追加するか、必要なキーワードを編集できます。

コマンド置換を使用するgrepソリューションの欠点は、チェーンのどこかに、最後のステップの前に一致するファイルがない場合にハングすることです。 xargsがゼロ以外のステータスを返すとパイプが中止されるため、これはgrepバリアントには影響しません。 xargsを使用するようにスクリプトを更新したので、これを自分で処理する必要がないため、スクリプトが簡単になりました。

2
arekolek
_awk 'FNR == 1 { f1=f2=f3=0; };

     /one/   { f1++ };
     /two/   { f2++ };
     /three/ { f3++ };

     f1 && f2 && f3 {
       print FILENAME;
       nextfile;
     }' *
_

Gzip圧縮されたファイルを自動的に処理する場合は、zcatを使用してループでこれを実行します(ループ内でawkを複数回フォークするため、ファイル名ごとに1回)、低速で非効率的です。同じアルゴリズムをPerlで書き換え、_IO::Uncompress::AnyUncompress_ライブラリモジュールを使用して、さまざまな種類の圧縮ファイル(gzip、Zip、bzip2、lzop)を解凍できます。または、圧縮ファイルを処理するためのモジュールもあるpythonで。


以下は、Perlバージョンで、_IO::Uncompress::AnyUncompress_を使用して、任意の数のパターンと任意の数のファイル名(プレーンテキストまたは圧縮テキストを含む)を許可します。

_--_の前のすべての引数は、検索パターンとして扱われます。 _--_の後のすべての引数は、ファイル名として扱われます。このジョブの原始的で効果的なオプション処理。 _-i_または_Getopt::Std_モジュールを使用すると、より良いオプション処理(たとえば、大文字と小文字を区別しない検索で_Getopt::Long_オプションをサポートする)を実現できます。

次のように実行します。

_$ ./arekolek.pl one two three -- *.gz *.txt
1.txt.gz
4.txt.gz
5.txt.gz
1.txt
4.txt
5.txt
_

(ここでは、ファイル__{1..6}.txt.gz_および_{1..6}.txt_はリストしません...「1」「2」「3」「4」「5」および「6」という単語の一部またはすべてが含まれているだけですテスト用。上記の出力にリストされているファイルには、3つの検索パターンがすべて含まれています。独自のデータでテストしてください)

_#! /usr/bin/Perl

use strict;
use warnings;
use IO::Uncompress::AnyUncompress qw(anyuncompress $AnyUncompressError) ;

my %patterns=();
my @filenames=();
my $fileargs=0;

# all args before '--' are search patterns, all args after '--' are
# filenames
foreach (@ARGV) {
  if ($_ eq '--') { $fileargs++ ; next };

  if ($fileargs) {
    Push @filenames, $_;
  } else {
    $patterns{$_}=1;
  };
};

my $pattern=join('|',keys %patterns);
$pattern=qr($pattern);
my $p_string=join('',sort keys %patterns);

foreach my $f (@filenames) {
  #my $lc=0;
  my %s = ();
  my $z = new IO::Uncompress::AnyUncompress($f)
    or die "IO::Uncompress::AnyUncompress failed: $AnyUncompressError\n";

  while ($_ = $z->getline) {
    #last if ($lc++ > 100);
    my @matches=( m/($pattern)/og);
    next unless (@matches);

    map { $s{$_}=1 } @matches;
    my $m_string=join('',sort keys %s);

    if ($m_string eq $p_string) {
      print "$f\n" ;
      last;
    }
  }
}
_

ハッシュ_%patterns_には、ファイルが各メンバーの少なくとも1つを含まなければならないパターンの完全なセットが含まれます_$_pstring_は、そのハッシュのソートされたキーを含む文字列です。文字列_$pattern_には、_%patterns_ハッシュから構築された、事前にコンパイルされた正規表現が含まれています。

_$pattern_は各入力ファイルの各行と比較され(_/o_修飾子を使用して_$pattern_を1回だけコンパイルして、実行中に変更されないことがわかっているため)、およびmap()は、各ファイルの一致を含むハッシュ(%s)を構築するために使用されます。

現在のファイルですべてのパターンが確認された場合は常に(_$m_string_(_%s_のソート済みキー)が_$p_string_と等しいかどうかを比較して)、ファイル名を出力し、次のファイルにスキップします。

これは特に高速なソリューションではありませんが、不当に遅いわけではありません。最初のバージョンでは、74MB相当の圧縮ログファイル(合計937MB非圧縮)で3ワードを検索するのに4分58秒かかりました。この現在のバージョンには1分13秒かかります。おそらく、さらに最適化を行うことができます。

明らかな最適化の1つは、xargsの_-P_別名_--max-procs_と組み合わせてこれを使用して、ファイルのサブセットに対して複数の検索を並行して実行することです。そのためには、ファイルの数をカウントし、システムのコア/ CPU /スレッドの数で除算する必要があります(1を追加して切り上げます)。例えば私のサンプルセットでは269個のファイルが検索されており、私のシステムには6つのコア(AMD 1090T)があるため、次のようになります。

_patterns=(one two three)
searchpath='/var/log/Apache2/'
cores=6
filecount=$(find "$searchpath" -type f -name 'access.*' | wc -l)
filespercore=$((filecount / cores + 1))

find "$searchpath" -type f -print0 | 
  xargs -0r -n "$filespercore" -P "$cores" ./arekolek.pl "${patterns[@]}" --
_

その最適化により、18個の一致するファイルすべてを見つけるのに23秒しかかかりませんでした。もちろん、他のどのソリューションでも同じことができます。注:出力にリストされるファイル名の順序は異なるため、問題がある場合は後でソートする必要があります。

@arekolekで指摘されているように、_find -exec_またはzgrepを指定した複数のxargssを使用すると、処理が大幅に速くなりますが、このスクリプトには、検索するパターンをいくつでもサポートできるという利点があります。複数の異なるタイプの圧縮を処理することができます。

スクリプトが各ファイルの最初の100行のみを検査するように制限されている場合、スクリプトはそれらすべてを(私の74MBの269ファイルのサンプルで)0.6秒で実行します。これが役立つ場合は、コマンドラインオプション(_-l 100_など)にすることもできますが、all一致するファイルが見つからないというリスクがあります。


ところで、_IO::Uncompress::AnyUncompress_のマニュアルページによると、サポートされている圧縮形式は次のとおりです。

  • zlib RFC 195
  • deflate RFC 1951 (オプション)、
  • gzip RFC 1952
  • ジップ、
  • bzip2、
  • lzop、
  • lzf、
  • lzma、
  • xz

最後の(私は願っています)最適化。 _PerlIO::gzip_の代わりに_libperlio-gzip-Perl_モジュール(debianに_IO::Uncompress::AnyUncompress_としてパッケージ化)を使用することで、処理時間を約3.1秒に短縮しました74MBのログファイル。 _Set::Scalar_ではなく単純なハッシュを使用することにより、いくつかの小さな改善も行われました(これは、_IO::Uncompress::AnyUncompress_バージョンでも数秒節約されました)。

_PerlIO::gzip_は https://stackoverflow.com/a/1539271/137158 (_Perl fast gzip decompress_のグーグル検索で見つかりました)の最速のPerl gunzipとして推奨されました

これで_xargs -P_を使用しても、まったく改善されませんでした。実際には、0.1秒から0.7秒の範囲で速度が低下するように見えました。 (私は4つの実行を試みました、そして私のシステムはタイミングを変えるバックグラウンドで他のものをします)

価格は、このバージョンのスクリプトはgzip圧縮された非圧縮ファイルのみを処理できることです。速度と柔軟性:このバージョンでは3.1秒、_IO::Uncompress::AnyUncompress_ラッパーを使用した_xargs -P_バージョン(または_xargs -P_を使用しない1m13s)では23秒。

_#! /usr/bin/Perl

use strict;
use warnings;
use PerlIO::gzip;

my %patterns=();
my @filenames=();
my $fileargs=0;

# all args before '--' are search patterns, all args after '--' are
# filenames
foreach (@ARGV) {
  if ($_ eq '--') { $fileargs++ ; next };

  if ($fileargs) {
    Push @filenames, $_;
  } else {
    $patterns{$_}=1;
  };
};

my $pattern=join('|',keys %patterns);
$pattern=qr($pattern);
my $p_string=join('',sort keys %patterns);

foreach my $f (@filenames) {
  open(F, "<:gzip(autopop)", $f) or die "couldn't open $f: $!\n";
  #my $lc=0;
  my %s = ();
  while (<F>) {
    #last if ($lc++ > 100);
    my @matches=(m/($pattern)/ogi);
    next unless (@matches);

    map { $s{$_}=1 } @matches;
    my $m_string=join('',sort keys %s);

    if ($m_string eq $p_string) {
      print "$f\n" ;
      close(F);
      last;
    }
  }
}
_
13
cas

レコードセパレーターを.に設定して、awkがファイル全体を1行として扱うようにします。

awk -v RS='.' '/one/&&/two/&&/three/{print FILENAME}' *

同様にPerlを使用:

Perl -ln00e '/one/&&/two/&&/three/ && print $ARGV' *
11
jimmij

圧縮ファイルの場合、各ファイルをループして最初に解凍できます。次に、他の回答を少し変更したバージョンで、次のことができます。

for f in *; do 
    zcat -f "$f" | Perl -ln00e '/one/&&/two/&&/three/ && exit(0); }{ exit(1)' && 
        printf '%s\n' "$f"
done

3つの文字列がすべて見つかった場合、Perlスクリプトは0ステータス(成功)で終了します。 }{は、END{}のPerl省略形です。それ以降のすべては、すべての入力が処理された後に実行されます。したがって、すべての文字列が見つからなかった場合、スクリプトは0以外の終了ステータスで終了します。したがって、&& printf '%s\n' "$f"は、3つすべてが見つかった場合にのみファイル名を出力します。

または、ファイルをメモリに読み込まないようにするには:

for f in *; do 
    zcat -f "$f" 2>/dev/null | 
        Perl -lne '$k++ if /one/; $l++ if /two/; $m++ if /three/;  
                   exit(0) if $k && $l && $m; }{ exit(1)' && 
    printf '%s\n' "$f"
done

最後に、スクリプトですべてを行いたい場合は、次のようにできます。

#!/usr/bin/env Perl

use strict;
use warnings;

## Get the target strings and file names. The first three
## arguments are assumed to be the strings, the rest are
## taken as target files.
my ($str1, $str2, $str3, @files) = @ARGV;

FILE:foreach my $file (@files) {
    my $fh;
    my ($k,$l,$m)=(0,0,0);
    ## only process regular files
    next unless -f $file ;
    ## Open the file in the right mode
    $file=~/.gz$/ ? open($fh,"-|", "zcat $file") : open($fh, $file);
    ## Read through each line
    while (<$fh>) {
        $k++ if /$str1/;
        $l++ if /$str2/;
        $m++ if /$str3/;
        ## If all 3 have been found
        if ($k && $l && $m){
            ## Print the file name
            print "$file\n";
            ## Move to the net file
            next FILE;
        }
    }
    close($fh);
}

上記のスクリプトをfoo.plとして$PATHのどこかに保存し、実行可能にして、次のように実行します。

foo.pl one two three *
3
terdon

別のオプション-単語を1つずつxargsにフィードして、ファイルに対してgrepを実行します。 xargsは、255を返すことにより、grepの呼び出しが失敗を返すとすぐに、それ自体を終了させることができます(xargsのドキュメントを確認してください)。もちろん、このソリューションに含まれるシェルとフォークの生成は、かなり遅くなる可能性があります

printf '%s\n' one two three | xargs -n 1 sh -c 'grep -q $2 $1 || exit 255' _ file

そしてそれをループする

for f in *; do
    if printf '%s\n' one two three | xargs -n 1 sh -c 'grep -q $2 $1 || exit 255' _ "$f"
    then
         printf '%s\n' "$f"
    fi
done
0
iruvar