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


16

私は、ファイル内のどこにでも、探しているキーワードの完全なセットを含むディレクトリ内のすべてのファイルをリストする方法を探しています。

そのため、キーワードを同じ行に表示する必要はありません。

これを行う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

しかし、それは間違いなく私がここに行く方向ではありません。私はあまりタイピングを必要と何か、そしておそらくちょうど1への呼び出したいgrepawkperlまたは類似の。

たとえば、次のように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}' *同じ効果を実行して取得する方が良いので、これを可能にするソリューションを好むでしょう。


1
gzip友好的である必要はなくzcat、ファイルだけが最初です。
テルドン

@terdon投稿を編集し、ファイルが圧縮されていると言う理由を説明しました。
-arekolek

awkを1回または複数回起動することに大きな違いはありません。つまり、多少のオーバーヘッドがありますが、違いに気付くことさえないでしょう。もちろん、スクリプト自体がこれを行うものであれば何でもawk / perlを作成することは可能ですが、それは本格的なプログラムになり始め、迅速なワンライナーではありません。それはあなたが望むものですか?
テルドン

@terdon個人的には、私にとってより重要な側面は、コマンドがどれほど複雑になるかです(コメントしている間に2回目の編集が行われたと思います)。たとえば、コールのgrepgrepにaを付けるだけでソリューションを簡単に適応zできます。ファイル名も処理する必要はありません。
-arekolek

はい、しかしそれはgrepです。私の知る限りgrepcat標準の「zバリアント」があります。for f in *; do zcat -f $f ...ソリューションを使用するよりも簡単なものは得られないと思います。それ以外は、開く前にファイル形式をチェックするか、ライブラリを使用して同じことを行う完全なプログラムでなければなりません。
テルドン

回答:


13
awk 'FNR == 1 { f1=f2=f3=0; };

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

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

gzip圧縮されたファイルを自動的に処理する場合は、これをループで実行しますzcatawkループ内でファイル名ごとに何度も分岐するため、低速で非効率的です)か、同じアルゴリズムを書き換えperlIO::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一度だけコンパイルします)、map()各ファイルの一致を含むハッシュ(%s)を構築するために使用されます。

現在のファイルにすべてのパターンが表示されたときはいつでも$m_string((のソートされたキー%s)が次と等しいかどうかを比較することにより)$p_string)、ファイル名を出力し、次のファイルにスキップします。

これは特に高速なソリューションではありませんが、不当に遅いというわけではありません。最初のバージョンでは、74MBの圧縮ログファイル(合計937MBの非圧縮)で3つの単語を検索するのに4m58秒かかりました。この現行バージョンには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で述べたように、複数zgrep持つのfind -execかはxargsかなり速くそれを行うことができますが、このスクリプトはを検索するためのパターンの任意の数をサポートするという利点があり、圧縮、いくつかの異なるタイプに対処することができます。

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


ところで、のmanページによると、IO::Uncompress::AnyUncompressサポートされている圧縮形式は次のとおりです。


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

PerlIO::gzip/programming//a/1539271/137158で最速のperl gunzipとして推奨されました(Google検索で発見perl fast gzip decompress

これを使用xargs -Pしてもまったく改善されませんでした。実際、0.1秒から0.7秒の範囲で速度を落とすようにも見えました。(私は4回実行しましたが、システムはバックグラウンドで他の処理を行い、タイミングを変更します)

ただし、このバージョンのスクリプトはgzip圧縮されたファイルと圧縮されていないファイルのみを処理できます。速度と柔軟性:このバージョンでは3.1秒IO::Uncompress::AnyUncompressxargs -Pラッパーあり(または1m13sなしxargs -P)では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;
    }
  }
}

for f in *; do zcat $f | awk -v F=$f '/one/ {a++}; /two/ {b++}; /three/ {c++}; a&&b&&c { print F; nextfile }'; done正常に動作しますが、実際には私のgrepソリューションの3倍の時間がかかり、実際にはより複雑です。
arekolek

1
OTOH、プレーンテキストファイルの場合は高速になります。そして、私が提案した圧縮ファイル(perlやpythonなど)の読み取りをサポートする言語で実装された同じアルゴリズムは、複数のgrepsよりも高速です。「複雑化」は部分的に主観的です-個人的に、単一のawkまたはperlまたはpythonスクリプトは、findの有無にかかわらず複数のgrepsよりも複雑ではないと思います。@ terdonの答えは良いです。すべてのcompresssedファイルのZCATをフォークの費用で)
CAS

私がしなければならなかったapt-get install libset-scalar-perlスクリプトを使用します。しかし、それは妥当な時間で終了するようには見えません。
arekolek

検索するファイルの数とサイズ(圧縮および非圧縮)は?数十または数百の中小サイズのファイルまたは数千の大きなファイルですか?
cas

ここにありますサイズのヒストグラム圧縮ファイル(20〜100ファイル、50メガバイトまでが、ほとんど5メガバイト以下)のが。非圧縮の外観は同じですが、サイズに10を掛けたものです
。– arekolek

11

ファイル全体を1行として扱う.ようにレコード区切り文字を設定しawkます。

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

同様にperl

perl -ln00e '/one/&&/two/&&/three/ && print $ARGV' *

3
きちんとした。ただし、これによりファイル全体がメモリにロードされ、大きなファイルでは問題になる可能性があることに注意してください。
テルドン

有望そうに見えたので、最初はこれを支持しました。しかし、gzip圧縮されたファイルで動作させることはできません。for f in *; do zcat $f | awk -v RS='.' -v F=$f '/one/ && /two/ && /three/ { print F }'; done何も出力しません。
arekolek

@arekolekそのループは私のために動作します。ファイルは適切にgzip圧縮されていますか?
-jimmij

@arekolekはzcat -f "$f"、一部のファイルが圧縮されていない場合に必要です。
テルドン

非圧縮ファイルでもテストしましたawk -v RS='.' '/bfs/&&/none/&&/rgg/{print FILENAME}' greptest/*.txtが、結果はgrep -l rgg $(grep -l none $(grep -l bfs greptest/*.txt))返されませんが、期待される結果が返されます。
arekolek

3

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

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

03つの文字列がすべて見つかった場合、Perlスクリプトはステータス(成功)で終了します。}{以下のためのPerlの省略形ですEND{}。それに続くものはすべて、すべての入力が処理された後に実行されます。そのため、すべての文字列が見つからなかった場合、スクリプトは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 *

2

これまでに提案されたすべてのソリューションの中で、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代わりに最初の引数として使用することで、圧縮ファイルでも簡単に使用できます。

その他の解決策

また、2つの戦略を使用してPythonスクリプトを試しました。1行ずつすべてのキーワードを検索し、キーワードごとにファイル全体を検索します。私の場合、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秒かかっていたためです(grep29秒でした)。

CAによってスクリプトがさえて処理されたファイルの数が少ない上、合理的な時間内に終了することができなかったgrep私はそれを殺すために持っていたので、4秒未満。

しかし、彼の最初の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ので、自分でこれを処理する必要がなく、スクリプトが簡単になりました。


あなたのPythonソリューションは、ループをCレイヤーにプッシュすることで恩恵を受けるかもしれませんnot all(p in text for p in patterns)
-iruvar

@iruvar提案をありがとう。私はそれを試してみましたが(sans not)、32秒で終了したので、それほど改善されていませんが、確かに読みやすくなっています。
-arekolek

key = search-pattern、val = count
cas

使用して、私の最新版を参照してください@arekolek PerlIO::gzipのではなくIO::Uncompress::AnyUncompress。74MBのログファイルを処理するのに、1分13秒ではなく3.1秒しかかかりません。
cas

ところで、以前に実行したことがある場合eval $(lesspipe)(たとえば.profile、など)、less代わりに使用できzcat -fforループラッパーはawkあらゆる種類のファイルless(gzip、bzip2、xz など)を処理できます。 lessは、stdoutがパイプであるかどうかを検出でき、パイプがあればstdoutにストリームを出力します。
cas

0

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

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

これは良さそうに見えますが、これの使い方がわかりません。何です_file?これは、引数として渡された複数のファイルを検索し、すべてのキーワードを含むファイルを返しますか?
arekolek

@arekolek、ループバージョンを追加しました。そして、について_$0、生成されたシェルにとして渡されています-これは、コマンド名として出力に表示されますps-私はここでマスターに
先送りし
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.