查找文件中任意位置包含多个关键字的文件

查找文件中任意位置包含多个关键字的文件

我正在寻找一种方法来列出目录中包含我正在寻找的完整关键字集的所有文件,位于文件中的任何位置。

因此,关键字不必出现在同一行。

一种方法是:

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

但这绝对是不是我要去的方向。我想要一些需要更少打字的东西,并且可能只需要一次调用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

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

之前的所有参数--都被视为搜索模式。之后的所有参数--都被视为文件名。这项工作的原始但有效的选项处理。-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在这里......它们只包含部分或全部单词“一”“二”“三”“四”“五”和“六”用于测试。上面输出中列出的文件请使用您自己的数据自行测试所有三种搜索模式)。

#! /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)。

每当在当前文件中看到所有模式时(通过比较$m_string( 中的排序键%s)是否等于$p_string),打印文件名并跳到下一个文件。

这不是一个特别快的解决方案,但也不是不合理的慢。第一个版本花了 4 分 58 秒在 74MB 的压缩日志文件(未压缩时总计 937MB)中搜索三个单词。当前版本需要 1 分 13 秒。可能还可以进行进一步的优化。

xargs一个明显的优化是将其与's结合使用,-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[@]}" --

通过这种优化,只需 23 秒即可找到所有 18 个匹配文件。当然,使用任何其他解决方案也可以完成相同的操作。注意:输出中列出的文件名顺序会有所不同,因此如果重要的话可能需要随后进行排序。

正如 @arekolek 所指出的,多个zgrep带有find -execor 的sxargs可以显着更快地完成,但该脚本的优点是支持任意数量的模式搜索,并且能够处理几种不同类型的压缩。

如果脚本仅限于检查每个文件的前 100 行,那么它会在 0.6 秒内运行完所有文件(在我的 269 个文件的 74MB 样本中)。如果这在某些情况下有用,可以将其制作成命令行选项(例如-l 100),但存在找不到的风险全部匹配文件。


顺便说一句,根据 的手册页IO::Uncompress::AnyUncompress,支持的压缩格式是:


最后(我希望)优化。通过使用PerlIO::gzip模块(在 debian 中打包为libperlio-gzip-perl)而不是IO::Uncompress::AnyUncompress我将时间减少到大约3.1秒用于处理我的 74MB 日志文件。通过使用简单的散列而不是Set::Scalar(这也节省了该版本的几秒钟IO::Uncompress::AnyUncompress),还有一些小的改进。

PerlIO::gzip被推荐为最快的 Perl Gunziphttps://stackoverflow.com/a/1539271/137158(通过谷歌搜索找到perl fast gzip decompress

使用xargs -P它根本没有改善它。事实上,它甚至似乎将速度减慢了 0.1 到 0.7 秒。 (我尝试了四次运行,我的系统在后台执行其他操作,这会改变时间)

代价是这个版本的脚本只能处理 gzip 压缩和未压缩的文件。速度与灵活性:此版本为 3.1 秒,带包装器IO::Uncompress::AnyUncompress的版本为 23 秒xargs -P(或不带包装器的 1 分 13 秒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;
    }
  }
}

答案2

将记录分隔符设置为.,以便awk将整个文件视为一行:

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

类似地perl

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

答案3

对于压缩文件,您可以循环遍历每个文件并首先解压缩。然后,通过对其他答案进行稍微修改的版本,您可以执行以下操作:

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

0如果找到所有三个字符串,Perl 脚本将退出并显示状态(成功)。是}{Perl 的简写END{}。处理完所有输入后,将执行其后的任何内容。因此,如果未找到所有字符串,脚本将以非 0 退出状态退出。因此,&& printf '%s\n' "$f"仅当找到所有三个文件时才会打印文件名。

或者,避免将文件加载到内存中:

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 *

答案4

另一种选择 - 一次输入一个单词,使其针对文件xargs运行。一旦调用返回失败,就可以通过返回它来使其退出(检查文档)。当然,此解决方案中涉及的 shell 和分叉的生成可能会显着减慢速度grepxargsgrep255xargs

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

相关内容