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


16

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

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

一种方法是:

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
您不需要它们gzip友好,只zcat需要文件即可。
terdon

@terdon我已经编辑了帖子,解释了为什么我提到文件被压缩。
arekolek

一次或多次启动awk之间并没有太大区别。我的意思是,好的,有些小开销,但我怀疑您是否会注意到其中的区别。当然,可以通过脚本本身来使awk / perl成为可能,但这开始成为一个功能完善的程序,而不是一成不变的程序。那是你要的吗?
terdon

@terdon就我个人而言,更重要的方面是命令将变得多么复杂(我想我第二次编辑是在您评论时进行的)。例如,grep只需在grep呼叫前面加上z,就可以轻松适应这些解决方案,而无需我也处理文件名。
arekolek

是的,但是那是grep。仅AFAIK,grepcat具有标准的“ z变量”。我认为您不会比使用for f in *; do zcat -f $f ...解决方案更简单。其他任何东西都必须是一个完整的程序,该程序可以在打开文件之前检查文件格式,或者使用库来执行相同的操作。
terdon

Answers:


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

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

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

如果您想自动处理压缩文件,请使用以下命令循环运行zcat(速度慢且效率低,因为您将awk在循环中分叉多次,每个文件名一次),或者重写相同的算法perl并使用IO::Uncompress::AnyUncompress库模块解压缩几种不同类型的压缩文件(gzip,zip,bzip2,lzop)。或在python中,它也具有用于处理压缩文件的模块。


这里的一个perl版本的用途IO::Uncompress::AnyUncompress,以允许任何数量的图案和任意数量的文件名(包含纯文本或压缩的文本)。

之前的所有参数--均视为搜索模式。之后的所有args --均视为文件名。此工作的原始但有效的选项处理。-i使用Getopt::StdGetopt::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未压缩)中搜索三个单词。当前版本需要1m13s。可能还有进一步的优化方法。

一个明显的优化是在同时使用这项功能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[@]}" --

通过这种优化,只花了23秒即可找到所有18个匹配的文件。当然,其他解决方案也可以做到这一点。注意:输出中列出的文件名顺序将有所不同,因此,如果需要,以后可能需要对其进行排序。

正如@arekolek指出的那样,zgrep使用find -execxargs可以很快速地实现多个,但是此脚本的优点是支持搜索任意数量的模式,并且能够处理几种不同类型的压缩。

如果该脚本仅限于检查每个文件的前100行,则它将在0.6秒内遍历所有文件(在我的269个文件的74MB样本中)。如果在某些情况下有用,则可以将其设置为命令行选项(例如-l 100),但是存在无法找到所有匹配文件的风险。


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


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

PerlIO::gzip/programming//a/1539271/137158中被推荐为最快的perl gunzip (通过google搜索找到perl fast gzip decompress

使用xargs -P这个根本没有改善。实际上,它甚至可以将其速度降低0.1到0.7秒。(我尝试了四次运行,并且系统在后台执行了其他操作,这会更改时间)

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

for f in *; do zcat $f | awk -v F=$f '/one/ {a++}; /two/ {b++}; /three/ {c++}; a&&b&&c { print F; nextfile }'; done可以正常工作,但是实际上,它花费的时间是我的grep解决方案的三倍,并且实际上更加复杂。
arekolek

1
OTOH,对于纯文本文件,它将更快。和我建议的用支持读取压缩文件(如perl或python)的语言实现的相同算法将比多次抓取要快。“复杂性”是部分主观的-就我个人而言,我认为单个awk或perl或python脚本要比具有或不具有查找的多次抓取更为复杂。以每个压缩文件的zcat分叉为代价)
cas

我不得不apt-get install libset-scalar-perl使用脚本。但这似乎并没有在任何合理的时间内终止。
arekolek '16

您要搜索的文件有多少个,大小是多少(压缩和未压缩)?数十个或数百个中小型文件还是数千个大文件?
cas

这是压缩文件大小直方图(20到100个文件,最大50MB,但大多数小于5MB)。未压缩的外观相同,但尺寸乘以10
arekolek

11

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

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

与此类似perl

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

3
整齐。请注意,这会将整个文件加载到内存中,这对于大文件可能是个问题。
terdon

我最初对此表示赞同,因为它看起来很有希望。但是我无法将其与压缩文件一起使用。for f in *; do zcat $f | awk -v RS='.' -v F=$f '/one/ && /two/ && /three/ { print F }'; done什么都不输出。
arekolek

@arekolek该循环对我有用。您的文件是否正确压缩了?
jimmij

zcat -f "$f"如果某些文件未压缩,则需要@arekolek 。
terdon

我还对未压缩的文件进行了测试,但awk -v RS='.' '/bfs/&&/none/&&/rgg/{print FILENAME}' greptest/*.txt仍未返回任何结果,而grep -l rgg $(grep -l none $(grep -l bfs greptest/*.txt))返回了预期的结果。
arekolek '16

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 *

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用作第一个参数来轻松地在压缩文件上使用它。

其他解决方案

我还使用两种策略对Python脚本进行了实验:逐行搜索所有关键字,并逐关键字搜索整个文件。就我而言,第二种策略更快。但这比仅使用慢,要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秒)。

中国科学院脚本未能在合理的时间内终止,甚至对用处理的文件数量较少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秒。

当然,我们可能没有优势来搜索已知在文件开头附近出现的关键字。在这种情况下,解决方案NR>100 {exit}时间为63秒(壁挂时间为50秒)。

未压缩的文件

我的grep解决方案和cas' 之间的运行时间没有显着差异awk建议,两者都只需花费一秒钟的时间即可执行。

请注意,FNR == 1 { f1=f2=f3=0; }在这种情况下,必须执行变量初始化才能为每个后续处理的文件重置计数器。因此,如果要更改关键字或添加新关键字,此解决方案需要在三个位置编辑命令。另一方面,grep只需附加| xargs grep -l four或编辑所需的关键字即可。

grep使用命令替换的解决方案的一个缺点是,如果在链中的任何位置(最后一步之前没有匹配的文件),它将挂起。这不会影响xargs变体,因为一旦grep返回非零状态,管道将中止。我已经更新了脚本以供使用,xargs因此不必自己处理,从而简化了脚本。


您的Python解决方案可能会受益于not all(p in text for p in patterns)
iruvar '16

@iruvar感谢您的建议。我已经尝试过(sans not),它在32秒内完成,因此并没有太大的改进,但是它的可读性肯定更高。
arekolek '16

您可以在awk中使用关联数组,而不是f1,f2,f3,使用key = search-pattern,val = count
cas

@arekolek请使用PerlIO::gzip而不是使用我的最新版本IO::Uncompress::AnyUncompress。现在只需要3.1秒,而不是1m13s来处理我的74MB日志文件。
cas

顺便说一句,如果您以前运行过eval $(lesspipe)(例如,在.profile中等),则可以使用less代替,zcat -f并且for循环包装器awk将能够处理任何类型的文件less(gzip,bzip2,xz等)。 less可以检测stdout是否是管道,如果是,则只会将流输出到stdout。
cas

0

另一种选择-一次输入一个单词,xargs以使其grep针对文件运行。xargs通过调用grep返回失败本身可以使自身退出255(请参阅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

这看起来不错,但是我不确定如何使用它。什么是_file?是否会搜索作为参数传递的多个文件并返回包含所有关键字的文件?
arekolek '16

@arekolek,添加了循环版本。至于_,它将作为传递$0给生成的外壳-这将作为命令名称显示在-的输出中ps-我将在此处提交给主服务器
iruvar
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.