Shell

查找文件中任意位置包含多個關鍵字的文件

  • July 29, 2016

我正在尋找一種方法來列出目錄中的所有文件,這些文件包含我正在尋找的完整關鍵字集,位於文件中的任何位置。

因此,關鍵字不必出現在同一行。

一種方法是:

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

三個關鍵字只是一個例子,它也可以是兩個或四個,等等。

我能想到的第二種方法是:

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

出現在另一個問題中的第三種方法是:

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

但這絕對不是我要去的方向。我想要一些不需要打字的東西,可能只需要一次呼叫,grep或類似的東西。awk``perl

例如,我喜歡如何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}' *並獲得相同的效果會更好,所以我更喜歡允許這樣做的解決方案。

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

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

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

如果您想自動處理 gzip 壓縮文件,請在循環中執行它zcat(緩慢且低效,因為您將awk在循環中多次分叉,每個文件名一次)或重寫相同的算法perl並使用IO::Uncompress::AnyUncompress庫模組,它可以解壓縮幾種不同類型的壓縮文件(gzip、zip、bzip2、lzop)。或者在 python 中,它也有處理壓縮文件的模組。


這是一個允許任意數量的模式和任意數量的文件名(包含純文字或壓縮文本)的perl版本。IO::Uncompress::AnyUncompress

之前的所有參數--都被視為搜尋模式。之後的所有參數--都被視為文件名。這項工作的原始但有效的選項處理。可以使用or模組實現更好的選項處理(例如,支持-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在這裡……它們只包含部分或全部單詞“一”“二”“三”“四”“五”和“六”用於測試。上面輸出中列出的文件DO 包含所有三種搜尋模式。使用您自己的數據自行測試)

#! /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包含文件必須包含的完整模式集,每個成員中至少有一個 $_pstring是包含該散列的排序鍵的字元串。該字元串$pattern包含一個預編譯的正則表達式,也是從%patterns散列建構的。

$pattern與每個輸入文件的每一行進行比較(使用/o修飾符$pattern只編譯一次,因為我們知道它在執行期間永遠不會改變),並map()用於建構包含每個文件匹配項的雜湊 (%s)。

每當在目前文件中看到所有模式時(通過比較 if $m_string(在 中的排序鍵%s)是否等於$p_string),列印文件名並跳到下一個文件。

這不是一個特別快的解決方案,但也不是不合理的慢。第一個版本花了 4 分 58 秒在 74MB 的壓縮日誌文件中搜尋三個單詞(未壓縮的總共 937MB)。目前版本需要 1 分 13 秒。可能還可以進行進一步的優化。

一個明顯的優化是將它與xargs-Paka結合使用--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[@]}" --

通過該優化,只需 23 秒即可找到所有 18 個匹配文件。當然,任何其他解決方案都可以這樣做。注意:輸出中列出的文件名的順序會有所不同,因此可能需要事後進行排序。

正如@arekolek 所指出的,多個zgreps 使用find -execorxargs可以顯著加快速度,但該腳本的優勢在於支持任意數量的模式進行搜尋,並且能夠處理幾種不同類型的壓縮。

如果腳本僅限於檢查每個文件的前 100 行,它會在 0.6 秒內執行所有文件(在我的 269 個文件的 74MB 範例中)。如果這在某些情況下有用,可以將其設置為命令行選項(例如-l 100),但存在找不到所有匹配文件的風險。


順便說一句,根據手冊頁IO::Uncompress::AnyUncompress,支持的壓縮格式是:


最後一個(我希望)優化。通過使用PerlIO::gzip模組(打包在 debian as 中libperlio-gzip-perl)而不是IO::Uncompress::AnyUncompress我將處理 74MB 日誌文件的時間縮短到大約3.1 秒。Set::Scalar通過使用簡單的雜湊而不是(這也節省了幾秒鐘的IO::Uncompress::AnyUncompress版本)也有一些小的改進。

PerlIO::gzip在https://stackoverflow.com/a/1539271/137158中被推薦為最快的 perl gunzip (通過 google 搜尋找到perl fast gzip decompress

使用xargs -P它根本沒有改善它。事實上,它甚至似乎將其減慢了 0.1 到 0.7 秒。(我嘗試了四次執行,我的系統在後台執行其他操作,這會改變時間)

代價是這個版本的腳本只能處理 gzip 和未壓縮的文件。速度與靈活性:此版本為 3.1 秒,而帶包裝器的版本為 23 秒(或不IO::Uncompress::AnyUncompress帶包裝器的版本為xargs -P1m13s xargs -P)。

#! /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;
   }
 }
}

將記錄分隔符設置為,.以便awk將整個文件視為一行:

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

與以下類似perl

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

引用自:https://unix.stackexchange.com/questions/293058