Trova file che contengono più parole chiave in qualsiasi punto del file


16

Sto cercando un modo per elencare tutti i file in una directory che contiene l'intero set di parole chiave che sto cercando, ovunque nel file.

Pertanto, le parole chiave non devono apparire sulla stessa riga.

Un modo per farlo sarebbe:

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

Tre parole chiave sono solo un esempio, potrebbero anche essere due o quattro e così via.

Un secondo modo a cui riesco a pensare è:

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

Un terzo metodo, apparso in un'altra domanda , sarebbe:

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

Ma sicuramente non è la direzione che sto andando qui. Voglio qualcosa che richiede meno di battitura, e, eventualmente, una sola chiamata a grep, awk, perlo simili.

Ad esempio, mi piace come awkti consente di abbinare le linee che contengono tutte le parole chiave , come:

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

Oppure, stampa solo i nomi dei file:

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

Ma voglio trovare file in cui le parole chiave potrebbero trovarsi ovunque nel file, non necessariamente sulla stessa riga.


Le soluzioni preferite sarebbero gzip friendly, ad esempio grepha la zgrepvariante che funziona su file compressi. Perché menziono questo, è che alcune soluzioni potrebbero non funzionare bene dato questo vincolo. Ad esempio, awknell'esempio di stampa di file corrispondenti, non puoi semplicemente fare:

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

È necessario modificare in modo significativo il comando, in qualcosa del tipo:

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

Quindi, a causa del vincolo, è necessario chiamare awkpiù volte, anche se è possibile farlo solo una volta con file non compressi. E certamente, sarebbe meglio semplicemente fare zawk '/pattern/ {print FILENAME; nextfile}' *e ottenere lo stesso effetto, quindi preferirei soluzioni che lo consentano.


1
Non hai bisogno che siano gzipamichevoli, solo zcati file per primi.
terdon

@terdon Ho modificato il post, spiegando perché menziono che i file sono compressi.
Arekolek,

Non c'è molta differenza tra l'avvio di awk una o più volte. Voglio dire, OK, qualche piccolo overhead ma dubito che noteresti anche la differenza. Ovviamente, è possibile rendere awk / perl qualunque script faccia questo da solo, ma questo inizia a diventare un programma completo e non un rapido one-liner. È questo che vuoi?
terdon

@terdon Personalmente, l'aspetto più importante per me è quanto complicato sarà il comando (immagino che la mia seconda modifica sia arrivata mentre stavi commentando). Ad esempio, le grepsoluzioni sono facilmente adattabili semplicemente aggiungendo il prefisso alle grepchiamate con un z, non è necessario che io gestisca anche i nomi dei file.
Arekolek,

Sì, ma quello è grep. AFAIK, solo grepe cathanno "varianti z" standard. Non credo che otterrai qualcosa di più semplice dell'uso di una for f in *; do zcat -f $f ...soluzione. Qualsiasi altra cosa dovrebbe essere un programma completo che controlla i formati di file prima di aprire o utilizza una libreria per fare lo stesso.
terdon

Risposte:


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

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

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

Se si desidera gestire automaticamente i file compressi con gzip, eseguirlo in un ciclo con zcat(lento e inefficiente perché si biforcerà awkpiù volte in un ciclo, una volta per ogni nome file) o riscrivere lo stesso algoritmo perle utilizzare il IO::Uncompress::AnyUncompressmodulo libreria che può decomprimere diversi tipi di file compressi (gzip, zip, bzip2, lzop). o in python, che ha anche moduli per la gestione di file compressi.


Ecco una perlversione che utilizza IO::Uncompress::AnyUncompressper consentire un numero qualsiasi di motivi e qualsiasi numero di nomi di file (contenenti sia testo normale che testo compresso).

Tutti gli argomenti precedenti --sono trattati come schemi di ricerca. Tutti gli argomenti successivi --vengono trattati come nomi di file. Gestione delle opzioni primitiva ma efficace per questo lavoro. Una migliore gestione delle opzioni (ad esempio per supportare -iun'opzione per ricerche senza distinzione tra maiuscole e minuscole) potrebbe essere ottenuta con i moduli Getopt::Stdo Getopt::Long.

Eseguilo così:

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

(Non elencherò i file {1..6}.txt.gze {1..6}.txtqui ... contengono solo alcune o tutte le parole "uno" "due" "tre" "quattro" "cinque" e "sei" per il test. I file elencati nell'output sopra Contiene tutti e tre i modelli di ricerca. Provalo tu stesso con i tuoi dati)

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

Un hash %patternscontiene il set completo di pattern che i file devono contenere almeno uno di ogni membro $_pstringè una stringa contenente le chiavi ordinate di tale hash. La stringa $patterncontiene un'espressione regolare precompilata creata anche %patternsdall'hash.

$patternviene confrontato con ciascuna riga di ciascun file di input (utilizzando il /omodificatore per compilare $patternuna sola volta poiché sappiamo che non cambierà mai durante l'esecuzione) e map()viene utilizzato per creare un hash (% s) contenente le corrispondenze per ciascun file.

Ogni volta che tutti i motivi sono stati visti nel file corrente (confrontando se $m_string(le chiavi ordinate %s) è uguale a $p_string), stampa il nome del file e salta al file successivo.

Questa non è una soluzione particolarmente veloce, ma non è irragionevolmente lenta. La prima versione impiegava 4 m 58 per cercare tre parole in 74 MB di file di registro compressi (per un totale di 937 MB non compressi). Questa versione corrente richiede 1m13s. Probabilmente ci sono ulteriori ottimizzazioni che potrebbero essere fatte.

Un'ottimizzazione ovvia è quella di utilizzare questo in combinazione con xargs'il -Paka --max-procsper eseguire ricerche multiple su sottoinsiemi dei file in parallelo. Per fare ciò, devi contare il numero di file e dividerlo per il numero di core / cpus / thread che il tuo sistema ha (e arrotondare aggiungendo 1). ad esempio, nel mio set di campioni sono stati cercati 269 file e il mio sistema ha 6 core (un AMD 1090T), quindi:

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

Con tale ottimizzazione, ci sono voluti solo 23 secondi per trovare tutti i 18 file corrispondenti. Naturalmente, lo stesso potrebbe essere fatto con una qualsiasi delle altre soluzioni. NOTA: l'ordine dei nomi dei file elencati nell'output sarà diverso, quindi potrebbe essere necessario ordinarli in seguito, se ciò è importante.

Come notato da @arekolek, più zgreps con find -execo xargspossono farlo in modo significativamente più veloce, ma questo script ha il vantaggio di supportare qualsiasi numero di pattern da cercare ed è in grado di gestire diversi tipi di compressione.

Se lo script si limita a esaminare solo le prime 100 righe di ciascun file, le esegue tutte (nel mio esempio di 74 MB di 269 file) in 0,6 secondi. Se questo è utile in alcuni casi, potrebbe essere trasformato in un'opzione della riga di comando (ad es. -l 100) Ma ha il rischio di non trovare tutti i file corrispondenti.


A proposito, secondo la pagina man di IO::Uncompress::AnyUncompress, i formati di compressione supportati sono:


Un'ultima (spero) ottimizzazione. Usando il PerlIO::gzipmodulo (impacchettato in debian as libperlio-gzip-perl) invece di IO::Uncompress::AnyUncompressho ottenuto il tempo a circa 3,1 secondi per l'elaborazione dei miei 74 MB di file di registro. Ci sono stati anche alcuni piccoli miglioramenti usando un semplice hash anziché Set::Scalar(che ha anche salvato alcuni secondi con la IO::Uncompress::AnyUncompressversione).

PerlIO::gzipè stato raccomandato come il più veloce gunzip perl in /programming//a/1539271/137158 (trovato con una ricerca su Google per perl fast gzip decompress)

L'uso xargs -Pcon questo non lo ha migliorato affatto. In effetti, sembrava addirittura rallentarlo da 0,1 a 0,7 secondi. (Ho provato quattro esecuzioni e il mio sistema fa altre cose in background che alterano i tempi)

Il prezzo è che questa versione dello script può gestire solo file compressi con gzip e non compressi. Velocità vs flessibilità: 3,1 secondi per questa versione vs 23 secondi per la IO::Uncompress::AnyUncompressversione con un xargs -Pwrapper (o 1m13s senza 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 }'; donefunziona bene, ma in effetti richiede 3 volte più della mia grepsoluzione ed è in realtà più complicato.
Arekolek,

1
OTOH, per i file di testo semplice sarebbe più veloce. e lo stesso algoritmo implementato in una lingua con supporto per la lettura di file compressi (come perl o python) come ho suggerito sarebbe più veloce di più greps. La "complicazione" è in parte soggettiva - personalmente, penso che un singolo script awk o perl o python sia meno complicato di più greps con o senza trovare .... La risposta di @ terdon è buona, e lo fa senza bisogno del modulo che ho citato (ma a scapito del fork di zcat per ogni file compresso)
cas

Ho dovuto apt-get install libset-scalar-perlusare la sceneggiatura. Ma non sembra terminare in un tempo ragionevole.
Arekolek,

quante e quali dimensioni (compresse e non compresse) sono i file che stai cercando? dozzine o centinaia di file di dimensioni medio-piccole o migliaia di file di grandi dimensioni?
Cas

Ecco un istogramma delle dimensioni dei file compressi (da 20 a 100 file, fino a 50 MB ma per lo più al di sotto di 5 MB). Non compresso sembrano uguali, ma con dimensioni moltiplicate per 10.
arekolek,

11

Impostare il separatore record su in .modo che awktratterà l'intero file come una riga:

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

Allo stesso modo con perl:

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

3
Neat. Si noti che questo caricherà l'intero file in memoria e che potrebbe essere un problema per file di grandi dimensioni.
terdon

Inizialmente ho votato a favore, perché sembrava promettente. Ma non riesco a farlo funzionare con i file compressi con gzip. for f in *; do zcat $f | awk -v RS='.' -v F=$f '/one/ && /two/ && /three/ { print F }'; donenon genera nulla.
Arekolek,

@arekolek Quel loop funziona per me. I tuoi file sono stati compressi correttamente?
Jimmij,

@arekolek è necessario zcat -f "$f"se alcuni dei file non sono compressi.
terdon

L'ho provato anche su file non compressi e awk -v RS='.' '/bfs/&&/none/&&/rgg/{print FILENAME}' greptest/*.txtnon restituisce ancora risultati, mentre grep -l rgg $(grep -l none $(grep -l bfs greptest/*.txt))restituisce i risultati previsti.
Arekolek,

3

Per i file compressi, è possibile eseguire il ciclo su ciascun file e decomprimere prima. Quindi, con una versione leggermente modificata delle altre risposte, puoi fare:

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

Lo script Perl uscirà con 0stato (esito positivo) se tutte e tre le stringhe fossero state trovate. È la }{scorciatoia del Perl per END{}. Tutto ciò che segue verrà eseguito dopo che tutti gli input sono stati elaborati. Quindi lo script uscirà con uno stato di uscita diverso da 0 se non sono state trovate tutte le stringhe. Pertanto, && printf '%s\n' "$f"verrà stampato il nome del file solo se tutti e tre sono stati trovati.

Oppure, per evitare di caricare il file in memoria:

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

Infine, se vuoi davvero fare tutto in uno script, puoi fare:

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

Salva lo script sopra come foo.plda qualche parte nel tuo $PATH, rendilo eseguibile ed eseguilo in questo modo:

foo.pl one two three *

2

Di tutte le soluzioni proposte finora, la mia soluzione originale che utilizza grep è la più veloce, finendo in 25 secondi. Lo svantaggio è che è noioso aggiungere e rimuovere parole chiave. Quindi ho ideato uno script (soprannominato multi) che simula il comportamento, ma consente di modificare la sintassi:

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

Quindi ora scrivere multi grep one two three -- *è equivalente alla mia proposta originale e funziona allo stesso tempo. Posso anche usarlo facilmente su file compressi usando invece zgrepcome primo argomento.

Altre soluzioni

Ho anche sperimentato uno script Python usando due strategie: la ricerca di tutte le parole chiave riga per riga e la ricerca in tutto il file parola chiave per parola chiave. La seconda strategia è stata più veloce nel mio caso. Ma è stato più lento del solo utilizzo grep, terminando in 33 secondi. La corrispondenza delle parole chiave riga per riga è terminata in 60 secondi.

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

La sceneggiatura di Terdon è terminata in 54 secondi. In realtà ci sono voluti 39 secondi di tempo sul muro, perché il mio processore è dual core. Il che è interessante, perché il mio script Python ha impiegato 49 secondi di wall time (ed grepera 29 secondi).

Lo script per cas non è riuscito a terminare in tempi ragionevoli, anche su un numero inferiore di file che sono stati elaborati con grepmeno di 4 secondi, quindi ho dovuto ucciderlo.

Ma la sua awkproposta originale , sebbene sia più lenta di grepquanto non sia, ha un potenziale vantaggio. In alcuni casi, almeno nella mia esperienza, è possibile aspettarsi che tutte le parole chiave debbano apparire tutte da qualche parte nella testa del file se sono nel file. Ciò offre a questa soluzione un notevole incremento delle prestazioni:

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

Termina in un quarto di secondo, invece di 25 secondi.

Naturalmente, potremmo non avere il vantaggio di cercare parole chiave che si verificano prima dell'inizio dei file. In tal caso, la soluzione senza NR>100 {exit}richiede 63 secondi (50 secondi di tempo a muro).

File non compressi

Non c'è alcuna differenza significativa nel tempo di esecuzione tra la mia grepsoluzione e la awkproposta di cas , entrambi richiedono una frazione di secondo per essere eseguiti.

Si noti che l'inizializzazione della variabile FNR == 1 { f1=f2=f3=0; }è obbligatoria in tal caso per ripristinare i contatori per ogni file elaborato successivo. Pertanto, questa soluzione richiede la modifica del comando in tre punti se si desidera modificare una parola chiave o aggiungerne di nuove. D'altra parte, con grepte puoi semplicemente aggiungere | xargs grep -l fouro modificare la parola chiave che desideri.

Uno svantaggio della grepsoluzione che utilizza la sostituzione dei comandi è che si bloccherà se in qualsiasi punto della catena, prima dell'ultimo passaggio, non ci sono file corrispondenti. Ciò non influisce sulla xargsvariante perché la pipe verrà interrotta una volta greprestituito uno stato diverso da zero. Ho aggiornato il mio script per usarlo, xargsquindi non devo gestirlo da solo, rendendo lo script più semplice.


La tua soluzione Python potrebbe trarre vantaggio dal passaggio del loop al livello C connot all(p in text for p in patterns)
iruvar,

@iruvar Grazie per il suggerimento. L'ho provato (sans not) ed è finito in 32 secondi, quindi non c'è molto miglioramento, ma è sicuramente più leggibile.
Arekolek,

potresti usare un array associativo piuttosto che f1, f2, f3 in awk, con key = pattern di ricerca, val = count
cas

@arekolek guarda la mia ultima versione usando PerlIO::gzipinvece di IO::Uncompress::AnyUncompress. ora sono necessari solo 3,1 secondi anziché 1m13s per elaborare i miei 74 MB di file di registro.
Cas

A proposito, se hai eseguito in precedenza eval $(lesspipe)(ad esempio nel tuo .profile, ecc.), Puoi utilizzare al lessposto di zcat -fe il tuo forwrapper di loop awksarà in grado di elaborare qualsiasi tipo di file che lesspuò (gzip, bzip2, xz e altro) .... less è in grado di rilevare se stdout è una pipe e, se lo è, emetterà un flusso su stdout.
cas

0

Un'altra opzione: inserisci le parole una alla volta xargs inserisci le per consentire l'esecuzione grepsul file. xargspuò essere costretto a uscire non appena viene invocata una grepmancata restituzione restituendola 255(consultare la xargsdocumentazione). Ovviamente la generazione di conchiglie e il forking coinvolti in questa soluzione probabilmente rallenterà in modo significativo

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

e per fare il giro

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

Sembra carino, ma non sono sicuro di come usarlo. Che cos'è _e file? Questa ricerca verrà eseguita in più file passati come argomento e restituirà file che contengono tutte le parole chiave?
Arekolek,

@arekolek, aggiunta una versione in loop. E per quanto riguarda _, viene passato come $0alla shell generata - questo apparirebbe come il nome del comando nell'output di ps- Vorrei differire al master qui
Iruvar
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.