Знайдіть файли, які містять кілька ключових слів у будь-якому місці файлу


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}' *та отримати той же ефект, тому я віддаю перевагу рішенням, які це дозволяють.


1
Вам не потрібно, щоб вони були gzipдружніми, лише zcatфайли спочатку.
terdon

@terdon Я редагував публікацію, пояснюючи, чому я згадую, що файли стискаються.
arekolek

Існує не велика різниця між запуском awk раз або багато разів. Я маю на увазі, гаразд, невеликі накладні, але я сумніваюся, ви навіть помітили різницю. Звичайно, можна зробити awk / perl будь-яким сценарієм, який він робить сам, але це стає повноцінною програмою, а не швидким одноразовим. Це те, чого ти хочеш?
terdon

@terdon Особисто для мене важливішим аспектом є те, наскільки складною буде команда (я думаю, моя друга редакція виникла, коли ви коментували). Наприклад, grepрішення легко адаптуються лише за допомогою префіксації grepвикликів з а z, мені також не потрібно обробляти імена файлів.
arekolek

Так, але це 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;
     }' *

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

Це не особливо швидке рішення, але не є надмірно повільним. Перша версія зайняла 4m58s для пошуку трьох слів у стислих файлах журналу вартістю 74 Мб (загалом 937 Мб нестиснуто). Ця поточна версія займає 1м13. Можливо, є додаткові оптимізації, які можна зробити.

Однією з очевидних оптимізацій є використання цього в поєднанні з xargs‘s -Paka --max-procsдля паралельного запуску декількох пошукових запитів у підмножинах файлів. Для цього вам потрібно порахувати кількість файлів і розділити на кількість ядер / cpus / потоків у вашій системі (і закріпити, додавши 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 -execабо xargsможуть зробити це значно швидше, але цей сценарій має перевагу в підтримці будь-якої кількості шаблонів для пошуку і здатний вирішувати кілька різних типів стиснення.

Якщо сценарій обмежується вивченням лише перших 100 рядків кожного файлу, він проходить через усі вони (у моїй вибірці з 749 МБ з 269 файлів) за 0,6 секунди. Якщо це корисно в деяких випадках, його можна перетворити на параметр командного рядка (наприклад -l 100), але це ризик знайти не всі файли, що відповідають.


BTW, відповідно до сторінки man для IO::Uncompress::AnyUncompress, підтримуються формати стиснення:

  • zlib RFC 1950 ,
  • спуску RFC 1951 (необов'язково),
  • gzip RFC 1952 ,
  • блискавка,
  • bzip2,
  • lzop,
  • lzf,
  • lzma,
  • xz

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

PerlIO::gzipрекомендовано як найшвидший perzi gunzip в /programming//a/1539271/137158 (знайдено за допомогою пошуку Google perl fast gzip decompress)

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

Ціна полягає в тому, що ця версія сценарію може обробляти лише gzipped та нестиснені файли. Швидкість проти гнучкості: 3,1 секунди для цієї версії проти 23 секунд для IO::Uncompress::AnyUncompressверсії з 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;
    }
  }
}

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рішення, і насправді є складнішим.
arekolek

1
OTOH, для текстових файлів із звичайним текстом було б швидше. і той самий алгоритм, реалізований мовою із підтримкою для читання стислих файлів (наприклад, perl чи python), як я запропонував, був би швидшим, ніж декілька файлів. "ускладнення" є частково суб'єктивним - особисто я думаю, що один скрипт "awk" або "perl" або "python" є менш складним, ніж декілька грепсів з або без пошуку .... @ відповідь тердона є хорошою, і це робиться, не потребуючи модуля, про який я згадав (але ціною роздвоєння zcat за кожен стислий файл)
cas

Довелося apt-get install libset-scalar-perlвикористовувати сценарій. Але це, здається, не припиняється в розумні терміни.
arekolek

скільки і який розмір (стислий і нестиснений) - це файли, які ви шукаєте? десятки чи сотні файлів малого та середнього розміру чи тисячі великих?
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' *

3
Акуратний. Зауважте, що це завантажить весь файл у пам'ять, і це може бути проблемою для великих файлів.
тердон

Я спочатку це схвалив, бо це виглядало перспективно. Але я не можу змусити його працювати з gzipped файлами. for f in *; do zcat $f | awk -v RS='.' -v F=$f '/one/ && /two/ && /three/ { print F }'; doneвиводить нічого.
arekolek

@arekolek Цей цикл працює для мене. Чи правильно завантажено файли?
jimmij

@arekolek вам потрібен, zcat -f "$f"якщо деякі файли не стиснуті.
тердон

Я перевіряв його також на нестиснених файлах і awk -v RS='.' '/bfs/&&/none/&&/rgg/{print FILENAME}' greptest/*.txtдосі не повертає результатів, тоді як grep -l rgg $(grep -l none $(grep -l bfs greptest/*.txt))повертає очікувані результати.
arekolek

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тому мені не доведеться самостійно впоратися з цим, зробивши сценарій більш простим.


Ваше рішення Python може спричинити користь від натискання циклу на рівень C за допомогоюnot all(p in text for p in patterns)
iruvar

@iruvar Дякую за пропозицію. Я спробував це (sans not), і він закінчився за 32 секунди, так що не так багато покращення, але, безумовно, більш читабельним.
arekolek

ви можете використовувати асоціативний масив, а не f1, f2, f3 in awk, з key = search-pattern, val = count
cas

@arekolek див. мою останню версію, використовуючи, PerlIO::gzipа не IO::Uncompress::AnyUncompress. Зараз на обробку моїх 74 МБ файлів журналів потрібно лише 3,1 секунди замість 1м13с.
cas

BTW, якщо ви раніше запускалися eval $(lesspipe)(наприклад, у вашому .profileтощо), ви можете використовувати lessзамість цього, zcat -fі ваша forпетля навколо циклу awkзможе обробити будь-який файл, який lessможе (gzip, bzip2, xz тощо) .... менше може виявити, чи 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

@arekolek, додав версію циклу. А що стосується _, це передається як $0породжена оболонка - це відображатиметься як ім'я команди на виході ps- я б
відправив
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.