Найти файлы, которые содержат несколько ключевых слов в любом месте файла

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

Но это определенно не то направление, куда я иду. Я хочу что - то , что требует меньше печатать, и , возможно , только один вызов 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}' *и получить тот же эффект, поэтому я бы предпочел решения, которые позволяют это.

arekolek
источник
1
Вам не нужно, чтобы они были gzipдружелюбными, zcatсначала только файлы.
Terdon
@terdon Я редактировал пост, объясняя, почему я упоминаю, что файлы сжаты.
Ареколек
Нет большой разницы между запуском awk один или несколько раз. Я имею в виду, хорошо, небольшие накладные расходы, но я сомневаюсь, что вы даже заметите разницу. Конечно, можно сделать awk / perl любым скриптом, который делает это сам, но он начинает превращаться в полноценную программу, а не в быстрые однострочные. Это то, что вы хотите?
Тердон
@terdon Лично для меня важнее то, насколько сложной будет команда (думаю, мое второе редактирование пришло, когда вы комментировали). Например, grepрешения легко адаптируются, просто добавляя префиксы к grepвызовам z, и мне не нужно обрабатывать имена файлов.
ареколек
Да, но это grep. AFAIK, только grepи catесть стандартные «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;
     }' *

Если вы хотите автоматически обрабатывать сжатые файлы, либо запустите это в цикле с 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), содержащего совпадения для каждого файла.

Всякий раз, когда все шаблоны были замечены в текущем файле (сравнивая if $m_string(отсортированные ключи в %s) равны $p_string), выведите имя файла и перейдите к следующему файлу.

Это не очень быстрое решение, но не слишком медленное. Первой версии потребовалось 4 млн. 58 секунд для поиска трех слов в файлах сжатых журналов объемом 74 МБ (всего без сжатия - 937 МБ). Эта текущая версия занимает 1m13s. Вероятно, возможны дальнейшие оптимизации.

Одна очевидная оптимизация состоит в том, чтобы использовать это вместе с xargs' -Pska ' --max-procsдля параллельного запуска множественного поиска по подмножествам файлов. Чтобы сделать это, вам нужно посчитать количество файлов и разделить на количество ядер / процессоров / потоков, которые есть в вашей системе (и округлить, добавив 1). Например, в моем наборе выборок было найдено 269 файлов, а в моей системе 6 ядер (1090 драм), поэтому:

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 -execили xargsмогут выполнять это значительно быстрее, но этот сценарий имеет преимущество, заключающееся в поддержке любого количества шаблонов для поиска и может работать с несколькими различными типами сжатия.

Если сценарий ограничен проверкой только первых 100 строк каждого файла, он проходит через все из них (в моем примере из 269 файлов размером 74 МБ) за 0,6 секунды. Если это полезно в некоторых случаях, его можно включить в параметр командной строки (например -l 100), но есть риск не найти все подходящие файлы.


Кстати, согласно справочной странице IO::Uncompress::AnyUncompress, поддерживаемые форматы сжатия:

  • zlib RFC 1950 ,
  • выкачать RFC 1951 (опционально),
  • gzip RFC 1952 ,
  • застежка-молния,
  • bzip2,
  • lzop,
  • LZF,
  • LZMA,
  • XZ

Одна последняя (я надеюсь) оптимизация. Используя вместо этого PerlIO::gzipмодуль (упакованный в debian as libperlio-gzip-perl), IO::Uncompress::AnyUncompressя сократил время обработки файлов до 74 МБ до 3,1 секунды . Были также некоторые небольшие улучшения с использованием простого хеша Set::Scalar(что также сэкономило несколько секунд с IO::Uncompress::AnyUncompressверсией).

PerlIO::gzipбыл рекомендован как самый быстрый Perl Gunzip в /programming//a/1539271/137158 (найдено с помощью поиска Google perl fast gzip decompress)

Использование xargs -Pс этим не улучшило это вообще. На самом деле, казалось, что он даже замедлился на 0,1–0,7 секунды. (Я пробовал четыре запуска, и моя система делает другие вещи в фоновом режиме, которые изменят время)

Цена в том, что эта версия скрипта может обрабатывать только сжатые и несжатые файлы. Скорость против гибкости: 3,1 секунды для этой версии против 23 секунд для IO::Uncompress::AnyUncompressверсии с xargs -Pоберткой (или 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работает нормально, но на самом деле занимает в 3 раза больше времени, чем мое grepрешение, и на самом деле более сложный.
ареколек
1
OTOH, для простых текстовых файлов это было бы быстрее. и тот же алгоритм, реализованный на языке с поддержкой чтения сжатых файлов (например, perl или python), как я предположил, будет быстрее, чем несколько greps. «усложнение» частично субъективно - лично я думаю, что один скрипт на awk, perl или python менее сложен, чем несколько greps с или без find .... Ответ @ terdon хорош, и делает это без необходимости модуля, который я упомянул (но по цене разветвления zcat для каждого сжатого файла)
cas
Мне пришлось apt-get install libset-scalar-perlиспользовать сценарий. Но это, кажется, не заканчивается в любое разумное время.
ареколек
сколько и какого размера (сжатых и несжатых) файлы вы ищете? десятки или сотни файлов малого и среднего размера или тысячи больших?
Cas
Вот гистограмма размеров сжатых файлов (от 20 до 100 файлов, до 50 МБ, но в основном ниже 5 МБ). Несжатые выглядят так же, но с размерами, умноженными на 10.
arekolek
11

Установите разделитель записей .так, awkчтобы весь файл обрабатывался как одна строка:

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

Аналогично с perl:

perl -ln00e '/one/&&/two/&&/three/ && print $ARGV' *
jimmij
источник
3
Ухоженная. Обратите внимание, что это загрузит весь файл в память, хотя это может быть проблемой для больших файлов.
Terdon
Я изначально проголосовал за это, потому что это выглядело многообещающе. Но я не могу заставить его работать с сжатыми файлами. for f in *; do zcat $f | awk -v RS='.' -v F=$f '/one/ && /two/ && /three/ { print F }'; doneничего не выводит.
Ареколек
@arekolek Этот цикл работает для меня. Ваши файлы правильно распакованы?
Джимми
@arekolek вам нужно, zcat -f "$f"если некоторые файлы не сжаты.
Terdon
Я также проверил это на несжатых файлах и awk -v RS='.' '/bfs/&&/none/&&/rgg/{print FILENAME}' greptest/*.txtдо сих пор не возвращает результатов, в то время как grep -l rgg $(grep -l none $(grep -l bfs greptest/*.txt))возвращает ожидаемые результаты.
ареколек
3

Для сжатых файлов вы можете зациклить каждый файл и распаковать в первую очередь. Затем, с немного измененной версией других ответов, вы можете сделать:

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

Сценарий Perl завершится со 0статусом (успех), если все три строки были найдены. }{Является 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 секунд).

Сценарий КАН не удалось завершить в разумные сроки, даже в меньшем количестве файлов , которые были обработаны с grep4 секунды, так что я должен был убить его.

Но его оригинальное 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решением и awkпредложением cas нет существенной разницы во времени выполнения , обе выполняются за доли секунды.

Обратите внимание, что инициализация переменной FNR == 1 { f1=f2=f3=0; }обязательна в этом случае для сброса счетчиков для каждого последующего обработанного файла. Таким образом, это решение требует редактирования команды в трех местах, если вы хотите изменить ключевое слово или добавить новые. С другой стороны, grepвы можете просто добавить | xargs grep -l fourили изменить ключевое слово, которое вы хотите.

Недостатком grepрешения, использующего подстановку команд, является то, что оно будет зависать, если где-либо в цепочке, перед последним шагом, не найдены подходящие файлы. Это не влияет на xargsвариант, потому что канал будет прерван, как только grepвернет ненулевой статус. Я обновил свой сценарий, чтобы использовать его, xargsпоэтому мне не нужно обрабатывать его самостоятельно, что делает сценарий проще.

arekolek
источник
Ваше решение на Python может получить выгоду от перемещения цикла до уровня C с помощьюnot all(p in text for p in patterns)
iruvar
@iruvar Спасибо за предложение. Я попробовал (без not), и он закончился за 32 секунды, так что не так много улучшений, но это, безусловно, более читабельно.
ареколек
Вы можете использовать ассоциативный массив вместо f1, f2, f3 в awk, с ключом = шаблон поиска, val = count
cas
@arekolek см. мою последнюю версию, PerlIO::gzipа не IO::Uncompress::AnyUncompress. теперь требуется всего 3,1 секунды вместо 1 м13 с для обработки моих 74 МБ файлов журнала.
Cas
Кстати, если вы ранее запускали eval $(lesspipe)(например, в вашем .profileи т. Д.), Вы можете использовать lessвместо этого, zcat -fи ваша forобертка цикла awkбудет в состоянии обработать любой тип файла, который lessможет (gzip, bzip2, xz и другие) .... less может определить, является ли 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
Iruvar
источник
Это выглядит красиво, но я не уверен, как это использовать. Что есть _и file? Будет ли этот поиск в нескольких файлах передаваться в качестве аргумента и возвращать файлы, содержащие все ключевые слова?
ареколек
@arekolek, добавлена ​​версия цикла. А что касается того _, что он передается как $0порожденная оболочка - это будет отображаться как имя команды в выходных данных ps- я бы отложил это до мастера здесь
iruvar