Busque archivos que contengan varias palabras clave en cualquier parte del archivo

16

Estoy buscando una manera de enumerar todos los archivos en un directorio que contiene el conjunto completo de palabras clave que estoy buscando, en cualquier parte del archivo.

Por lo tanto, las palabras clave no necesitan aparecer en la misma línea.

Una forma de hacer esto sería:

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

Tres palabras clave es solo un ejemplo, podría ser dos o cuatro, y así sucesivamente.

Una segunda forma en que puedo pensar es:

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

Un tercer método, que apareció en otra pregunta , sería:

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

Pero definitivamente esa no es la dirección a la que voy aquí. Quiero algo que requiere menos escribir, y, posiblemente, sólo una llamada a grep, awk, perlo similar.

Por ejemplo, me gusta cómo le awkpermite unir líneas que contienen todas las palabras clave , como:

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

O imprima solo los nombres de archivo:

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

Pero quiero encontrar archivos donde las palabras clave pueden estar en cualquier parte del archivo, no necesariamente en la misma línea.


Las soluciones preferidas serían compatibles con gzip, por ejemplo, greptiene la zgrepvariante que funciona en archivos comprimidos. Por eso menciono esto, es que algunas soluciones pueden no funcionar bien dada esta restricción. Por ejemplo, en el awkejemplo de imprimir archivos coincidentes, no puede simplemente hacer:

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

Necesita cambiar significativamente el comando, a algo como:

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

Entonces, debido a la restricción, debe llamar awkmuchas veces, aunque solo puede hacerlo una vez con archivos sin comprimir. Y ciertamente, sería mejor hacer zawk '/pattern/ {print FILENAME; nextfile}' *y obtener el mismo efecto, por lo que preferiría soluciones que lo permitan.

arekolek
fuente
1
No necesita que sean gzipamigables, solo zcatlos archivos primero.
terdon
@terdon He editado la publicación, explicando por qué menciono que los archivos están comprimidos.
arekolek
No hay mucha diferencia entre lanzar awk una o muchas veces. Quiero decir, vale, algo de gastos generales, pero dudo que incluso notes la diferencia. Es, por supuesto, posible hacer que awk / perl sea el guión que haga esto por sí mismo, pero eso comienza a convertirse en un programa completo y no en una línea rápida. ¿Es eso lo que quieres?
terdon
@terdon Personalmente, el aspecto más importante para mí es lo complicado que será el comando (supongo que mi segunda edición se produjo mientras comentabas). Por ejemplo, las grepsoluciones son fácilmente adaptables simplemente anteponiendo grepllamadas con un z, no es necesario que yo también maneje los nombres de los archivos.
arekolek
Sí, pero eso es grep. AFAIK, solo grepy cattiene "variantes z" estándar. No creo que pueda obtener nada más simple que usar una for f in *; do zcat -f $f ...solución. Cualquier otra cosa tendría que ser un programa completo que verifique los formatos de archivo antes de abrir o use una biblioteca para hacer lo mismo.
terdon

Respuestas:

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

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

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

Si desea manejar automáticamente los archivos comprimidos, ejecute esto en un bucle con zcat(lento e ineficiente porque se bifurcará awkmuchas veces en un bucle, una vez para cada nombre de archivo) o vuelva a escribir el mismo algoritmo perly use el IO::Uncompress::AnyUncompressmódulo de biblioteca que puede descomprima varios tipos diferentes de archivos comprimidos (gzip, zip, bzip2, lzop). o en python, que también tiene módulos para manejar archivos comprimidos.


Aquí hay una perlversión que utiliza IO::Uncompress::AnyUncompresspara permitir cualquier número de patrones y cualquier número de nombres de archivos (que contengan texto sin formato o texto comprimido).

Todos los argumentos anteriores --se tratan como patrones de búsqueda. Todos los argumentos posteriores --se tratan como nombres de archivo. Manejo de opciones primitivo pero efectivo para este trabajo. Mejor manejo opción (por ejemplo, para soportar una -iopción para las búsquedas de mayúsculas y minúsculas) podría lograrse con el Getopt::Stdo Getopt::Longlos módulos.

Ejecútelo así:

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

(No enumeraré archivos {1..6}.txt.gzy {1..6}.txtaquí ... solo contienen algunas o todas las palabras "uno" "dos" "tres" "cuatro" "cinco" y "seis" para probar. Los archivos enumerados en el resultado anterior SÍ contiene los tres patrones de búsqueda. Pruébelo usted mismo con sus propios datos)

#! /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 el conjunto completo de patrones que los archivos deben contener, al menos uno de cada miembro $_pstringes una cadena que contiene las claves ordenadas de ese hash. La cadena $patterncontiene una expresión regular precompilada también construida a partir del %patternshash.

$patternse compara con cada línea de cada archivo de entrada (usando el /omodificador para compilar $patternsolo una vez, ya que sabemos que nunca cambiará durante la ejecución), y map()se usa para construir un hash (% s) que contiene las coincidencias para cada archivo.

Siempre que se hayan visto todos los patrones en el archivo actual (al comparar si $m_string(las claves ordenadas en %s) son iguales a $p_string), imprima el nombre del archivo y pase al siguiente archivo.

Esta no es una solución particularmente rápida, pero no es irrazonablemente lenta. La primera versión tardó 4m58s en buscar tres palabras en 74MB de archivos de registro comprimidos (un total de 937MB sin comprimir). Esta versión actual dura 1m13s. Probablemente hay más optimizaciones que podrían hacerse.

Una optimización obvia es usar esto junto con xargs' -Paka' --max-procspara ejecutar múltiples búsquedas en subconjuntos de archivos en paralelo. Para hacer eso, debe contar la cantidad de archivos y dividir por la cantidad de núcleos / cpus / hilos que tiene su sistema (y redondear agregando 1). por ejemplo, se buscaron 269 archivos en mi conjunto de muestras, y mi sistema tiene 6 núcleos (un AMD 1090T), por lo que:

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 esa optimización, tomó solo 23 segundos encontrar los 18 archivos coincidentes. Por supuesto, lo mismo podría hacerse con cualquiera de las otras soluciones. NOTA: El orden de los nombres de archivo enumerados en la salida será diferente, por lo que puede ser necesario ordenarlos después si eso es importante.

Como señaló @arekolek, múltiples zgreps con find -execo xargspueden hacerlo significativamente más rápido, pero este script tiene la ventaja de admitir cualquier número de patrones para buscar, y es capaz de manejar varios tipos diferentes de compresión.

Si el script se limita a examinar solo las primeras 100 líneas de cada archivo, se ejecuta a través de todas ellas (en mi muestra de 74MB de 269 archivos) en 0.6 segundos. Si esto es útil en algunos casos, podría convertirse en una opción de línea de comando (por ejemplo -l 100), pero tiene el riesgo de no encontrar todos los archivos coincidentes.


Por cierto, de acuerdo con la página del manual para IO::Uncompress::AnyUncompress, los formatos de compresión admitidos son:


Una última (espero) optimización. Al usar el PerlIO::gzipmódulo (empaquetado en Debian como libperlio-gzip-perl) en lugar de hacerlo IO::Uncompress::AnyUncompress, obtuve el tiempo de espera de aproximadamente 3,1 segundos para procesar mis 74 MB de archivos de registro. También hubo algunas pequeñas mejoras al usar un hash simple en lugar de Set::Scalar(que también ahorró unos segundos con la IO::Uncompress::AnyUncompressversión).

PerlIO::gzipfue recomendado como el gunzip perl más rápido en /programming//a/1539271/137158 (encontrado con una búsqueda en google para perl fast gzip decompress)

Usar xargs -Pcon esto no lo mejoró en absoluto. De hecho, incluso pareció ralentizarlo entre 0.1 y 0.7 segundos. (Intenté cuatro ejecuciones y mi sistema hace otras cosas en segundo plano que alterarán el tiempo)

El precio es que esta versión del script solo puede manejar archivos comprimidos y descomprimidos. Velocidad frente a flexibilidad: 3.1 segundos para esta versión frente a 23 segundos para la IO::Uncompress::AnyUncompressversión con xargs -Penvoltura (o 1m13s sin 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
fuente
for f in *; do zcat $f | awk -v F=$f '/one/ {a++}; /two/ {b++}; /three/ {c++}; a&&b&&c { print F; nextfile }'; donefunciona bien, pero de hecho, toma 3 veces más que mi grepsolución, y en realidad es más complicado.
arekolek
1
OTOH, para archivos de texto sin formato sería más rápido. y el mismo algoritmo implementado en un lenguaje con soporte para leer archivos comprimidos (como perl o python) como sugerí, sería más rápido que varios greps. "complicación" es parcialmente subjetiva: personalmente, creo que un solo script awk o perl o python es menos complicado que múltiples greps con o sin find ... La respuesta de @ terdon es buena, y lo hace sin necesidad del módulo que mencioné (pero a costa de bifurcar zcat para cada archivo comprimido)
cas
Tuve apt-get install libset-scalar-perlque usar el guión. Pero no parece terminar en un tiempo razonable.
arekolek 01 de
¿Cuántos y de qué tamaño (comprimido y sin comprimir) son los archivos que está buscando? ¿docenas o cientos de archivos de tamaño pequeño a mediano o miles de archivos grandes?
cas
Aquí hay un histograma del tamaño de los archivos comprimidos (de 20 a 100 archivos, hasta 50 MB, pero en su mayoría por debajo de 5 MB). Sin comprimir se ve igual, pero con tamaños multiplicados por 10.
arekolek
11

Establezca el separador de registros para .que awktrate el archivo completo como una línea:

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

De manera similar con perl:

perl -ln00e '/one/&&/two/&&/three/ && print $ARGV' *
jimmij
fuente
3
Ordenado. Tenga en cuenta que esto cargará todo el archivo en la memoria y eso podría ser un problema para archivos grandes.
terdon
Inicialmente voté esto porque parecía prometedor. Pero no puedo hacer que funcione con archivos comprimidos. for f in *; do zcat $f | awk -v RS='.' -v F=$f '/one/ && /two/ && /three/ { print F }'; doneno produce nada
arekolek
@arekolek Ese ciclo funciona para mí. ¿Están sus archivos correctamente comprimidos?
jimmij
@arekolek que necesita zcat -f "$f"si algunos de los archivos no están comprimidos.
terdon
También lo probé en archivos sin comprimir y awk -v RS='.' '/bfs/&&/none/&&/rgg/{print FILENAME}' greptest/*.txtaún no devuelve resultados, mientras que grep -l rgg $(grep -l none $(grep -l bfs greptest/*.txt))devuelve los resultados esperados.
arekolek 01 de
3

Para archivos comprimidos, puede recorrer cada archivo y descomprimir primero. Luego, con una versión ligeramente modificada de las otras respuestas, puede hacer:

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

El script de Perl se cerrará con 0estado (éxito) si se encuentran las tres cadenas. La }{es la abreviatura de Perl END{}. Todo lo que sigue se ejecutará después de que se haya procesado toda la entrada. Por lo tanto, el script saldrá con un estado de salida distinto de 0 si no se encuentran todas las cadenas. Por lo tanto, && printf '%s\n' "$f"imprimirá el nombre del archivo solo si se encontraron los tres.

O, para evitar cargar el archivo en la 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

Finalmente, si realmente quieres hacer todo en un script, puedes hacer:

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

Guarde el script anterior como foo.plen algún lugar de su $PATH, hágalo ejecutable y ejecútelo así:

foo.pl one two three *
terdon
fuente
2

De todas las soluciones propuestas hasta ahora, mi solución original usando grep es la más rápida, terminando en 25 segundos. Su inconveniente es que es tedioso agregar y eliminar palabras clave. Así que se me ocurrió una secuencia de comandos (doblada multi) que simula el comportamiento, pero permite cambiar la sintaxis:

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

Ahora, escribir multi grep one two three -- *es equivalente a mi propuesta original y se ejecuta al mismo tiempo. También puedo usarlo fácilmente en archivos comprimidos usando zgrepcomo primer argumento en su lugar.

Otras soluciones

También experimenté con un script de Python usando dos estrategias: buscar todas las palabras clave línea por línea y buscar en todo el archivo palabra clave por palabra clave. La segunda estrategia fue más rápida en mi caso. Pero fue más lento que solo usar grep, terminando en 33 segundos. La coincidencia de palabras clave línea por línea finalizó en 60 segundos.

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

El guión dado por terdon terminó en 54 segundos. En realidad, me llevó 39 segundos de tiempo en la pared, porque mi procesador es de doble núcleo. Lo cual es interesante, porque mi secuencia de comandos de Python tomó 49 segundos de tiempo de pared (y grepfue de 29 segundos).

El script de cas no pudo finalizar en un tiempo razonable, incluso en un número menor de archivos procesados ​​con grepmenos de 4 segundos, por lo que tuve que matarlo.

Pero su awkpropuesta original , aunque es más lenta de lo grepque es, tiene una ventaja potencial. En algunos casos, al menos en mi experiencia, es posible esperar que todas las palabras clave aparezcan en algún lugar del encabezado del archivo si están en el archivo. Esto le da a esta solución un impulso dramático en el rendimiento:

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 en un cuarto de segundo, en lugar de 25 segundos.

Por supuesto, es posible que no tengamos la ventaja de buscar palabras clave que se sabe que ocurren cerca del comienzo de los archivos. En tal caso, la solución sin NR>100 {exit}toma 63 segundos (50 segundos de tiempo de pared).

Archivos sin comprimir

No hay una diferencia significativa en el tiempo de ejecución entre mi grepsolución y la awkpropuesta de cas , ambas tardan una fracción de segundo en ejecutarse.

Tenga en cuenta que la inicialización de la variable FNR == 1 { f1=f2=f3=0; }es obligatoria en tal caso para restablecer los contadores para cada archivo procesado posterior. Como tal, esta solución requiere editar el comando en tres lugares si desea cambiar una palabra clave o agregar otras nuevas. Por otro lado, con grepsolo puede agregar | xargs grep -l fouro editar la palabra clave que desee.

Una desventaja de la grepsolución que utiliza la sustitución de comandos es que se bloqueará si en algún lugar de la cadena, antes del último paso, no hay archivos coincidentes. Esto no afecta la xargsvariante porque la tubería se abortará una vez que grepdevuelva un estado distinto de cero. He actualizado mi script para usarlo, xargsasí que no tengo que manejar esto yo mismo, simplificando el script.

arekolek
fuente
Su solución Python puede beneficiarse al empujar el bucle hacia la capa C connot all(p in text for p in patterns)
iruvar
@iruvar Gracias por la sugerencia. Lo probé (sin not) y terminó en 32 segundos, por lo que no es una gran mejora, pero ciertamente es más legible.
arekolek 01 de
podría usar una matriz asociativa en lugar de f1, f2, f3 en awk, con key = search-pattern, val = count
cas
@arekolek ve mi última versión usando en PerlIO::gziplugar de IO::Uncompress::AnyUncompress. ahora solo toma 3.1 segundos en lugar de 1m13s para procesar mis 74MB de archivos de registro.
cas
Por cierto, si ha ejecutado previamente eval $(lesspipe)(por ejemplo, en su .profile, etc.), puede usar en lesslugar de zcat -fy su forenvoltura de bucle awkpodrá procesar cualquier tipo de archivo que lesspueda (gzip, bzip2, xz y más) ... less puede detectar si stdout es una tubería y solo generará una secuencia en stdout si lo es.
cas
0

Otra opción: alimentar palabras de una en una para xargs para que se ejecute grepcontra el archivo. xargsse puede hacer que salga tan pronto como una invocación de grepdevolución devuelva el error 255(consulte la xargsdocumentación). Por supuesto, el desove de las conchas y las bifurcaciones involucradas en esta solución probablemente disminuirá significativamente

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

y para enrollarlo

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
fuente
Esto se ve bien, pero no estoy seguro de cómo usar esto. ¿Qué es _y file? ¿Esta búsqueda en múltiples archivos se pasará como argumento y devolverá archivos que contienen todas las palabras clave?
arekolek 01 de
@arekolek, agregó una versión en bucle. Y en cuanto a _, se pasa como $0al shell generado - esto se mostraría como el nombre del comando en la salida de ps- Me
referiría