Znajdź pliki zawierające wiele słów kluczowych w dowolnym miejscu pliku

16

Szukam sposobu, aby wyświetlić listę wszystkich plików w katalogu zawierającym pełny zestaw słów kluczowych, których szukam, w dowolnym miejscu pliku.

Tak więc słowa kluczowe nie muszą pojawiać się w tym samym wierszu.

Jednym ze sposobów na to byłoby:

grep -l one $(grep -l two $(grep -l three *))

Trzy słowa kluczowe to tylko przykład, równie dobrze mogą to być dwa lub cztery itd.

Drugi sposób, jaki mogę wymyślić, to:

grep -l one * | xargs grep -l two | xargs grep -l three

Trzecią metodą, która pojawiła się w innym pytaniu , byłoby:

find . -type f \
  -exec grep -q one {} \; -a \
  -exec grep -q two {} \; -a \
  -exec grep -q three {} \; -a -print

Ale zdecydowanie nie w tym kierunku idę tutaj. Chcę coś, co wymaga mniej pisać, a może tylko jedno wywołanie grep, awk,perl lub podobny.

Na przykład podoba mi się sposób awkdopasowania linii zawierających wszystkie słowa kluczowe , takie jak:

awk '/one/ && /two/ && /three/' *

Lub wydrukuj tylko nazwy plików:

awk '/one/ && /two/ && /three/ { print FILENAME ; nextfile }' *

Ale chcę znaleźć pliki, w których słowa kluczowe mogą znajdować się w dowolnym miejscu pliku, niekoniecznie w tej samej linii.


Preferowane rozwiązania byłyby przyjazne dla gzip, na przykład grepma zgrepwariant, który działa na skompresowanych plikach. Dlaczego wspominam o tym, że niektóre rozwiązania mogą nie działać dobrze, biorąc pod uwagę to ograniczenie. Na przykład w awkprzykładzie drukowania pasujących plików nie można po prostu:

zcat * | awk '/pattern/ {print FILENAME; nextfile}'

Musisz znacząco zmienić polecenie, na coś takiego:

for f in *; do zcat $f | awk -v F=$f '/pattern/ { print F; nextfile }'; done

Z powodu tego ograniczenia musisz dzwonić awkwiele razy, nawet jeśli możesz to zrobić tylko raz z nieskompresowanymi plikami. I na pewno fajniej byłoby po prostu zrobić zawk '/pattern/ {print FILENAME; nextfile}' *i uzyskać ten sam efekt, więc wolałbym rozwiązania, które na to pozwalają.

arekolek
źródło
1
Nie potrzebujesz ich gzipprzyjaźni, tylko zcatpliki.
terdon
@terdon Zredagowałem post, wyjaśniając, dlaczego wspominam, że pliki są skompresowane.
arekolek
Nie ma dużej różnicy między uruchomieniem awk raz lub wiele razy. Mam na myśli, OK, trochę małych kosztów ogólnych, ale wątpię, byś zauważył różnicę. Oczywiście możliwe jest, aby skrypt awk / perl zrobił to samo, ale zaczyna on pełnić funkcję programu, a nie szybkiego programu uruchamiającego jedną linię. Czy tego chcesz?
terdon
@terdon Osobiście, dla mnie ważniejszym aspektem jest to, jak skomplikowane będzie to polecenie (chyba moja druga edycja przyszła podczas komentowania). Na przykład greprozwiązania można łatwo dostosować, poprzedzając greppołączenia znakiem „a” z, nie muszę też obsługiwać nazw plików.
arekolek
Tak, ale to jest grep. AFAIK, tylko grepi catmają standardowe „warianty Z”. Nie sądzę, że uzyskasz coś prostszego niż użycie for f in *; do zcat -f $f ...rozwiązania. Wszystko inne musiałoby być pełnym programem, który sprawdza formaty plików przed otwarciem lub korzysta z biblioteki, aby zrobić to samo.
terdon

Odpowiedzi:

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

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

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

Jeśli chcesz automatycznie obsługiwać pliki spakowane gzip, uruchom to w pętli za pomocą zcat(powolne i nieefektywne, ponieważ będziesz rozwidlać awkwiele razy w pętli, raz dla każdej nazwy pliku) lub przepisz ten sam algorytm perli użyj IO::Uncompress::AnyUncompressmodułu biblioteki, który może rozpakuj kilka różnych rodzajów skompresowanych plików (gzip, zip, bzip2, lzop). lub w pythonie, który ma również moduły do ​​obsługi skompresowanych plików.


Oto perlwersja, która używaIO::Uncompress::AnyUncompress pozwala na dowolną liczbę wzorców i dowolną liczbę nazw plików (zawierających zwykły tekst lub skompresowany tekst).

Wszystkie argumenty wcześniej --są traktowane jako wzorce wyszukiwania. Wszystkie argumenty później --są traktowane jak nazwy plików. Prymitywna, ale skuteczna obsługa opcji dla tego zadania. Lepszą obsługę opcji (np. W celu obsługi -iopcji wyszukiwania bez rozróżniania wielkości liter) można uzyskać za pomocą Getopt::StdlubGetopt::Long modułów .

Uruchom tak:

$ ./arekolek.pl one two three -- *.gz *.txt
1.txt.gz
4.txt.gz
5.txt.gz
1.txt
4.txt
5.txt

(Nie wymienię plików, {1..6}.txt.gza {1..6}.txttutaj ... zawierają one tylko niektóre lub wszystkie słowa „jeden” „dwa” „trzy” „cztery” „pięć” i „sześć” do testowania. Pliki wymienione w wynikach powyżej Zawierają wszystkie trzy wzorce wyszukiwania. Sprawdź to sam na podstawie własnych danych)

#! /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;
    }
  }
}

Hash %patternszawiera pełny zestaw wzorców, które pliki muszą zawierać co najmniej jeden z każdego elementu, $_pstringto ciąg zawierający posortowane klucze tego hasha. Ciąg $patternzawiera wstępnie skompilowane wyrażenie regularne również zbudowane z %patternsskrótu.

$patternjest porównywany z każdą linią każdego pliku wejściowego (przy użyciu /omodyfikatora do kompilacji $patterntylko raz, ponieważ wiemy, że nigdy się nie zmieni podczas uruchamiania), i map()jest używany do budowania skrótu (% s) zawierającego dopasowania dla każdego pliku.

Ilekroć wszystkie wzory są widoczne w bieżącym pliku (przez porównanie, czy $m_string(posortowane klucze %ssą równe $p_string), wydrukuj nazwę pliku i przejdź do następnego pliku.

Nie jest to szczególnie szybkie rozwiązanie, ale nie jest nieuzasadnione powolne. Pierwsza wersja zajęła 4m58 sekund, aby wyszukać trzy słowa w skompresowanych plikach dziennika o wartości 74 MB (łącznie 937 MB bez kompresji). Ta aktualna wersja zajmuje 1m13s. Prawdopodobnie można dokonać dalszych optymalizacji.

Jednym z oczywistych optymalizacji jest do tego użyć w połączeniu z xargs„s -Paka --max-procsuruchamianie wielu wyszukiwań w podgrupach plików równolegle. Aby to zrobić, musisz policzyć liczbę plików i podzielić przez liczbę rdzeni / cpus / wątków, które ma Twój system (i zaokrąglić w górę, dodając 1). np. w moim zestawie próbek przeszukano 269 plików, a mój system ma 6 rdzeni (AMD 1090T), więc:

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[@]}" --

Dzięki tej optymalizacji znalezienie wszystkich 18 pasujących plików zajęło tylko 23 sekundy. Oczywiście to samo można zrobić z dowolnym innym rozwiązaniem. UWAGA: Kolejność nazw plików wymienionych w danych wyjściowych będzie inna, więc może to wymagać późniejszego posortowania, jeśli to ma znaczenie.

Jak zauważył @arekolek, wiele zgreps z find -execlub xargsmoże to zrobić znacznie szybciej, ale ten skrypt ma tę zaletę, że obsługuje dowolną liczbę wzorców do wyszukiwania i jest w stanie poradzić sobie z kilkoma różnymi typami kompresji.

Jeśli skrypt ogranicza się do zbadania tylko pierwszych 100 wierszy każdego pliku, przechodzi przez wszystkie (w mojej 74 MB próbce 269 plików) w 0,6 sekundy. Jeśli jest to przydatne w niektórych przypadkach, można je przekształcić w opcję wiersza poleceń (np. -l 100), Ale istnieje ryzyko , że nie uda się znaleźć wszystkich pasujących plików.


BTW, według strony IO::Uncompress::AnyUncompresspodręcznika, obsługiwane formaty kompresji to:

  • zlib RFC 1950 ,
  • spuścić powietrze z RFC 1951 (opcjonalnie),
  • gzip RFC 1952 ,
  • zamek błyskawiczny,
  • bzip2,
  • lzop,
  • lzf,
  • lzma,
  • xz

Ostatnia (mam nadzieję) optymalizacja. Korzystając z PerlIO::gzipmodułu (spakowanego w debian as libperlio-gzip-perl) zamiast zużywać IO::Uncompress::AnyUncompressczas do około 3,1 sekundy na przetworzenie moich 74 MB plików dziennika. Wprowadzono również niewielkie ulepszenia, używając prostego skrótu zamiast Set::Scalar(co również pozwoliło zaoszczędzić kilka sekund w przypadku IO::Uncompress::AnyUncompresswersji).

PerlIO::gzipbył zalecany jako najszybszy perl gunzip w /programming//a/1539271/137158 (znaleziony przy wyszukiwaniu w Google perl fast gzip decompress)

Używanie xargs -Pz tym wcale go nie poprawiło. W rzeczywistości wydawało się, że nawet spowalnia to od 0,1 do 0,7 sekundy. (Próbowałem czterech uruchomień, a mój system wykonuje inne czynności w tle, które zmienią czas)

Cena jest taka, że ​​ta wersja skryptu obsługuje tylko pliki spakowane i nieskompresowane. Szybkość vs elastyczność: 3,1 sekundy dla tej wersji vs 23 sekund dla IO::Uncompress::AnyUncompresswersji z xargs -Potoką (lub 1m13s bez 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;
    }
  }
}
cas
źródło
for f in *; do zcat $f | awk -v F=$f '/one/ {a++}; /two/ {b++}; /three/ {c++}; a&&b&&c { print F; nextfile }'; donedziała dobrze, ale w rzeczywistości zajmuje 3 razy więcej czasu niż moje greprozwiązanie i jest bardziej skomplikowane.
arekolek
1
OTOH, dla zwykłych plików tekstowych byłoby to szybsze. i ten sam algorytm zaimplementowany w języku z obsługą odczytu skompresowanych plików (takich jak Perl lub Python), jak sugerowałem, byłby szybszy niż wielokrotne greps. „komplikacja” jest częściowo subiektywna - osobiście uważam, że pojedynczy skrypt awk, perl lub python jest mniej skomplikowany niż wielokrotne greps z lub bez find .... Odpowiedź terdona jest dobra i nie wymaga modułu, o którym wspomniałem (ale kosztem rozwidlenia Zcat dla każdego skompresowanego pliku)
cas
Musiałem apt-get install libset-scalar-perlużyć skryptu. Ale wydaje się, że nie kończy się w rozsądnym czasie.
arekolek
ile i jaki rozmiar (skompresowany i nieskompresowany) to pliki, których szukasz? dziesiątki czy setki małych i średnich plików czy tysiące dużych?
cas
Oto histogram rozmiarów skompresowanych plików (od 20 do 100 plików, do 50 MB, ale głównie poniżej 5 MB). Nieskompresowane wyglądają tak samo, ale przy rozmiarach pomnożonych przez 10.
arekolek
11

Ustaw separator rekordów na .tak, awkaby traktował cały plik jako jedną linię:

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

Podobnie z perl:

perl -ln00e '/one/&&/two/&&/three/ && print $ARGV' *
jimmij
źródło
3
Schludny. Pamiętaj, że spowoduje to załadowanie całego pliku do pamięci i może to stanowić problem w przypadku dużych plików.
terdon
Początkowo głosowałem za tym, ponieważ wyglądało to obiecująco. Ale nie mogę zmusić go do pracy z plikami spakowanymi gzip. for f in *; do zcat $f | awk -v RS='.' -v F=$f '/one/ && /two/ && /three/ { print F }'; donenic nie produkuje.
arekolek
@arekolek Ta pętla działa dla mnie. Czy twoje pliki są poprawnie skompresowane?
jimmij
@arekolek potrzebujesz, zcat -f "$f"jeśli niektóre pliki nie są skompresowane.
terdon
Przetestowałem to również na nieskompresowanych plikach i awk -v RS='.' '/bfs/&&/none/&&/rgg/{print FILENAME}' greptest/*.txtnadal nie zwraca żadnych wyników, a grep -l rgg $(grep -l none $(grep -l bfs greptest/*.txt))zwraca oczekiwane wyniki.
arekolek
3

W przypadku plików skompresowanych można zapętlić każdy plik i najpierw rozpakować. Następnie, używając nieco zmodyfikowanej wersji innych odpowiedzi, możesz:

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

Skrypt Perla zakończy działanie ze 0statusem (sukces), jeśli wszystkie trzy ciągi zostaną znalezione. Jest }{to skrót od Perla END{}. Wszystko po nim zostanie wykonane po przetworzeniu wszystkich danych wejściowych. Skrypt zakończy działanie ze statusem wyjścia innym niż 0, jeśli nie zostaną znalezione wszystkie ciągi. Dlatego && printf '%s\n' "$f"wydrukuje nazwę pliku tylko wtedy, gdy wszystkie trzy zostaną znalezione.

Lub, aby uniknąć ładowania pliku do pamięci:

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

Wreszcie, jeśli naprawdę chcesz zrobić wszystko w skrypcie, możesz:

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

Zapisz skrypt powyżej jako foo.plgdzieś w swoim $PATH, ustaw go jako wykonywalny i uruchom go w następujący sposób:

foo.pl one two three *
terdon
źródło
2

Ze wszystkich proponowanych do tej pory rozwiązań moje oryginalne rozwiązanie wykorzystujące grep jest najszybsze i kończy się w 25 sekund. Wadą jest to, że żmudne jest dodawanie i usuwanie słów kluczowych. Wymyśliłem więc skrypt (dubbingowany multi), który symuluje zachowanie, ale pozwala zmienić składnię:

#!/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"

Więc teraz pisanie multi grep one two three -- *jest równoważne mojej oryginalnej propozycji i działa w tym samym czasie. Mogę też łatwo użyć go w skompresowanych plikach, używając zgrepjako pierwszego argumentu.

Inne rozwiązania

Eksperymentowałem również ze skryptem Python, stosując dwie strategie: wyszukiwanie wszystkich słów kluczowych wiersz po wierszu i wyszukiwanie w całym pliku słów kluczowych według słowa kluczowego. Druga strategia była szybsza w moim przypadku. Ale było wolniejsze niż zwykłe używanie grep, kończąc w 33 sekundy. Dopasowywanie słów kluczowych wiersz po wierszu zakończone w 60 sekund.

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

Skrypt podany przez terdon wykończone w 54 sekund. Właściwie zajęło to 39 sekund czasu na ścianie, ponieważ mój procesor jest dwurdzeniowy. Co jest interesujące, ponieważ mój skrypt w języku Python zajął 49 sekund czasu na ścianie (igrep wynosił 29 sekund).

Scenariusz cas nie udało się zakończyć w rozsądnym czasie, nawet na mniejszą liczbę plików, które zostały przetworzone zgrep poniżej 4 sekund, więc musiałem go zabić.

Ale jego oryginalna awkpropozycja, choć wolniejsza niż grepobecnie, ma potencjalną przewagę. W niektórych przypadkach, przynajmniej z mojego doświadczenia, można oczekiwać, że wszystkie słowa kluczowe powinny pojawić się gdzieś w nagłówku pliku, jeśli w ogóle są w pliku. Daje to rozwiązaniu znaczny wzrost wydajności:

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

Kończy się za kwadrans, w przeciwieństwie do 25 sekund.

Oczywiście możemy nie mieć przewagi w wyszukiwaniu słów kluczowych, o których wiadomo, że występują na początku plików. W takim przypadku rozwiązanie bezNR>100 {exit} zajmuje 63 sekundy (50 sekund czasu na ścianie).

Pliki nieskompresowane

Nie ma znaczącej różnicy w czasie działania między moim greprozwiązaniem a cas 'awk propozycją , wykonanie obu zajmuje ułamek sekundy.

Zauważ, że inicjalizacja zmiennej FNR == 1 { f1=f2=f3=0; }jest w takim przypadku obowiązkowa, aby zresetować liczniki dla każdego kolejnego przetwarzanego pliku. Jako takie, to rozwiązanie wymaga edycji polecenia w trzech miejscach, jeśli chcesz zmienić słowo kluczowe lub dodać nowe. Z drugiej strony, grepmożesz po prostu dołączyć| xargs grep -l four lub edytować słowo kluczowe.

Wadą greprozwiązania wykorzystującego zastępowanie poleceń jest to, że zawiesi się ono, jeśli w dowolnym miejscu w łańcuchu, przed ostatnim krokiem nie będzie pasujących plików. Nie wpływa to na xargswariant, ponieważ rura zostanie przerwana, gdy grepzwróci status niezerowy. Zaktualizowałem skrypt, aby go używać, xargswięc nie muszę sobie z tym radzić, dzięki czemu skrypt jest prostszy.

arekolek
źródło
Twoje rozwiązanie w Pythonie może not all(p in text for p in patterns)
odnieść
@iruvar Dzięki za sugestię. Próbowałem (sans not) i zakończyło się w 32 sekundy, więc nie tak wiele ulepszeń, ale z pewnością jest bardziej czytelne.
arekolek
możesz użyć tablicy asocjacyjnej zamiast f1, f2, f3 w awk, z kluczem = wzorzec wyszukiwania, val = liczba
cas
@arekolek zobacz moją najnowszą wersję za pomocą PerlIO::gzipzamiast IO::Uncompress::AnyUncompress. teraz przetworzenie moich 74 MB plików dziennika zajmuje tylko 3,1 sekundy zamiast 1m13s.
cas
BTW, jeśli wcześniej działałeś eval $(lesspipe)(np. W swoim .profileitp.), Możesz użyć lesszamiast tego, zcat -fa foropakowanie pętli awkbędzie w stanie przetworzyć dowolny rodzaj pliku, który lessmoże (gzip, bzip2, xz i więcej) .... less może wykryć, czy stdout jest potokiem, i po prostu wyśle ​​strumień do stdout, jeśli jest.
cas
0

Inna opcja - podaj słowa pojedynczo xargs, aby działały grepna pliku. xargsmoże zostać zmuszony do wyjścia, gdy tylko wywołanie grepniepowodzenia zakończy się niepowodzeniem przez powrót 255do niego (sprawdź xargsdokumentację). Oczywiście spawnowanie pocisków i rozwidlanie zaangażowane w to rozwiązanie prawdopodobnie znacznie go spowolnią

printf '%s\n' one two three | xargs -n 1 sh -c 'grep -q $2 $1 || exit 255' _ file

i zapętlić

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
źródło
Wygląda to ładnie, ale nie jestem pewien, jak tego użyć. Co to jest _i file? Czy to wyszukiwanie w wielu plikach zostanie przekazane jako argument i zwróci pliki zawierające wszystkie słowa kluczowe?
arekolek
@arekolek, dodał wersję pętli. A co do _tego, jest przekazywany jako $0spawnowana powłoka - pojawi się jako nazwa polecenia w danych wyjściowych ps- Chciałbym odłożyć się tutaj do mistrza
iruvar