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

are*_*lek 17 shell grep find awk text-processing

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

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

一种方法是:

grep -l one $(grep -l two $(grep -l three *))
Run Code Online (Sandbox Code Playgroud)

三个关键字只是一个例子,也可以是两个或四个,依此类推。

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

grep -l one * | xargs grep -l two | xargs grep -l three
Run Code Online (Sandbox Code Playgroud)

出现在另一个问题中的第三种方法是:

find . -type f \
  -exec grep -q one {} \; -a \
  -exec grep -q two {} \; -a \
  -exec grep -q three {} \; -a -print
Run Code Online (Sandbox Code Playgroud)

但这绝对不是我来这里的方向。我想要的东西,需要更少的输入,可能只需一个电话来grepawkperl或类似的。

例如,我喜欢如何awk让您匹配包含所有关键字的行,例如:

awk '/one/ && /two/ && /three/' *
Run Code Online (Sandbox Code Playgroud)

或者,只打印文件名:

awk '/one/ && /two/ && /three/ { print FILENAME ; nextfile }' *
Run Code Online (Sandbox Code Playgroud)

但我想找到关键字可能在文件中任何位置的文件,不一定在同一行。


首选的解决方案是 gzip 友好的,例如grep具有zgrep适用于压缩文件的变体。我之所以提到这一点,是因为考虑到这种限制,某些解决方案可能效果不佳。比如在awk打印匹配文件的例子中,你不能只做:

zcat * | awk '/pattern/ {print FILENAME; nextfile}'
Run Code Online (Sandbox Code Playgroud)

您需要显着更改命令,例如:

for f in *; do zcat $f | awk -v F=$f '/pattern/ { print F; nextfile }'; done
Run Code Online (Sandbox Code Playgroud)

因此,由于限制,您需要awk多次调用,即使您只能使用未压缩的文件调用一次。当然,这样做zawk '/pattern/ {print FILENAME; nextfile}' *并获得相同的效果会更好,所以我更喜欢允许这样做的解决方案。

cas*_*cas 13

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

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

     f1 && f2 && f3 {
       print FILENAME;
       nextfile;
     }' *
Run Code Online (Sandbox Code Playgroud)

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


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

之前的所有参数--都被视为搜索模式。后面的所有参数--都被视为文件名。这项工作的原始但有效的选项处理。-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
Run Code Online (Sandbox Code Playgroud)

(我不会列出文件{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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

散列%patterns包含完整的一组模式,文件必须包含每个成员$_pstring中的至少一个 是一个包含该散列的排序键的字符串。该字符串$pattern包含一个预编译的正则表达式,也是从%patterns散列构建的。

$pattern与每个输入文件的每一行进行比较(使用/o修饰符$pattern只编译一次,因为我们知道它在运行期间不会改变),并map()用于构建包含每个文件匹配项的哈希 (%s)。

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

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

一个明显的优化是将其与xargs's -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[@]}" --
Run Code Online (Sandbox Code Playgroud)

通过这种优化,找到所有 18 个匹配文件只需要 23 秒。当然,对于任何其他解决方案也可以这样做。注意:输出中列出的文件名的顺序会有所不同,因此如果这很重要,可能需要在之后进行排序。

正如@arekolek 所指出的,zgrep使用find -execor 的多个sxargs可以显着加快速度,但该脚本的优点是支持任意数量的模式进行搜索,并且能够处理多种不同类型的压缩。

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


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


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

PerlIO::gzip/sf/answers/107749001/ 中被推荐为最快的 perl gunzip (通过谷歌搜索找到perl fast gzip decompress

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

代价是这个版本的脚本只能处理 gzip 和未压缩的文件。速度与灵活性:此版本为 3.1 秒,IO::Uncompress::AnyUncompress而带有xargs -P包装器的版本为 23 秒(或不带 1m13s xargs -P)。

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

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

     f1 && f2 && f3 {
       print FILENAME;
       nextfile;
     }' *
Run Code Online (Sandbox Code Playgroud)


jim*_*mij 12

集合记录分隔符.,这样awk就会把整个文件作为一个行:

awk -v RS='.' '/one/&&/two/&&/three/{print FILENAME}' *
Run Code Online (Sandbox Code Playgroud)

类似的perl

perl -ln00e '/one/&&/two/&&/three/ && print $ARGV' *
Run Code Online (Sandbox Code Playgroud)

  • 整洁的。请注意,这会将整个文件加载到内存中,这对于大文件来说可能是一个问题。 (3认同)

are*_*lek 3

在迄今为止提出的所有解决方案中,我最初使用 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"
Run Code Online (Sandbox Code Playgroud)

所以现在写multi grep one two three -- *就相当于我原来的提案,同时运行。zgrep我还可以通过使用作为第一个参数来轻松地在压缩文件上使用它。

其他解决方案

我还尝试了使用两种策略的 Python 脚本:逐行搜索所有关键字,以及逐个关键字搜索整个文件。就我而言,第二种策略更快。但它比仅使用 慢grep,在 33 秒内完成。逐行关键字匹配在 60 秒内完成。

#!/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"
Run Code Online (Sandbox Code Playgroud)

terdon给出的脚本在54秒内完成。实际上花了 39 秒的实际时间,因为我的处理器是双核的。这很有趣,因为我的 Python 脚本花了 49 秒的实际时间(实际上grep是 29 秒)。

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
Run Code Online (Sandbox Code Playgroud)

在四分之一秒内完成,而不是 25 秒。

当然,我们可能不具备搜索已知出现在文件开头附近的关键字的优势。在这种情况下,解决方案无需NR>100 {exit}花费 63 秒(50 秒的挂接时间)。

未压缩的文件

grep我的解决方案和 cas 的提案之间的运行时间没有显着差异awk,执行时间都只需要几分之一秒。

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

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