Encontre arquivos que contenham várias palavras-chave em qualquer lugar do arquivo

15

Estou procurando uma maneira de listar todos os arquivos em um diretório que contenha o conjunto completo de palavras-chave que estou procurando, em qualquer lugar do arquivo.

Portanto, as palavras-chave não precisam aparecer na mesma linha.

Uma maneira de fazer isso seria:

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

Três palavras-chave são apenas um exemplo, bem como duas ou quatro e assim por diante.

Uma segunda maneira que posso pensar é:

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

Um terceiro método, que apareceu em outra pergunta , seria:

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

Mas isso definitivamente não é a direção em que estou indo. Eu quero algo que requer menos digitação e, possivelmente, apenas uma chamada para grep , awk , perl ou similar.

Por exemplo, gosto de como awk permite corresponder linhas que contenham todas as palavras-chave , como:

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

Ou imprima apenas os nomes dos arquivos:

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

Mas quero encontrar arquivos em que as palavras-chave possam estar em qualquer lugar do arquivo, não necessariamente na mesma linha.

As soluções preferidas seriam amigáveis com gzip, por exemplo, grep tem a variante zgrep que funciona em arquivos compactados. Por que eu menciono isso, algumas soluções podem não funcionar bem devido a essa restrição. Por exemplo, no exemplo awk de impressão de arquivos correspondentes, você não pode simplesmente fazer:

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

Você precisa alterar significativamente o comando para algo como:

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

Portanto, devido à restrição, você precisa chamar awk muitas vezes, mesmo que você possa fazer isso apenas uma vez com arquivos descompactados. E certamente, seria melhor fazer apenas zawk '/pattern/ {print FILENAME; nextfile}' * e obter o mesmo efeito, então eu preferiria soluções que permitissem isso.

    
por arekolek 30.06.2016 / 11:44

5 respostas

2

De todas as soluções propostas até agora, minha solução original usando o grep é a mais rápida, terminando em 25 segundos. A desvantagem é que é tedioso adicionar e remover palavras-chave. Então eu criei um script (apelidado multi ) que simula o comportamento, mas permite mudar a sintaxe:

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

Agora, escrever multi grep one two three -- * é equivalente à minha proposta original e é executado no mesmo tempo. Também posso usá-lo facilmente em arquivos compactados usando zgrep como o primeiro argumento.

Outras soluções

Eu também experimentei um script Python usando duas estratégias: pesquisar todas as palavras-chave linha por linha e pesquisar em todo o arquivo palavra-chave por palavra-chave. A segunda estratégia foi mais rápida no meu caso. Mas foi mais lento do que usar apenas grep , terminando em 33 segundos. A correspondência de palavras-chave linha por linha terminou em 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)

O script dado por terdon terminou em 54 segundos. Na verdade, levou 39 segundos de tempo de parede, porque meu processador é dual core. O que é interessante, porque meu script Python levou 49 segundos de tempo de parede (e grep foi de 29 segundos).

O script por cas falhou em terminar em tempo razoável, mesmo em um número menor de arquivos processados com grep em 4 segundos, então tive que matá-lo.

Mas sua proposta original awk , embora seja mais lenta que grep , tem vantagem potencial. Em alguns casos, pelo menos na minha experiência, é possível esperar que todas as palavras-chave apareçam em algum lugar na cabeça do arquivo, se elas estiverem no arquivo. Isto dá a esta solução um aumento dramático no desempenho:

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 em um quarto de segundo, ao contrário de 25 segundos.

Naturalmente, talvez não tenhamos a vantagem de procurar palavras-chave que ocorram perto do início dos arquivos. Nesse caso, a solução sem NR>100 {exit} leva 63 segundos (50s de tempo de parede).

Arquivos descompactados

Não há diferença significativa no tempo de execução entre minha grep solution e cas ' awk proposal, ambos levam uma fração de segundo para serem executados.

Observe que a inicialização da variável FNR == 1 { f1=f2=f3=0; } é obrigatória nesse caso para redefinir os contadores de cada arquivo processado subsequente. Como tal, esta solução requer a edição do comando em três locais, se você quiser alterar uma palavra-chave ou adicionar novas. Por outro lado, com grep você pode simplesmente anexar | xargs grep -l four ou editar a palavra-chave desejada.

Uma desvantagem da solução grep que usa a substituição de comandos, é que ela irá travar em qualquer lugar da cadeia, antes da última etapa, não há arquivos correspondentes. Isso não afeta a variante xargs porque o canal será cancelado quando grep retornar um status diferente de zero. Eu atualizei meu script para usar xargs , então eu não tenho que lidar com isso sozinho, tornando o script mais simples.

    
por 01.07.2016 / 13:29
12
awk 'FNR == 1 { f1=f2=f3=0; };

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

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

Se você quiser manipular automaticamente os arquivos compactados com gzip, execute-o em um loop com zcat (lento e ineficiente porque você estará forking awk muitas vezes em um loop, uma vez para cada nome de arquivo) ou reescreva o mesmo algoritmo em perl e use o módulo de biblioteca IO::Uncompress::AnyUncompress que pode descompactar vários tipos diferentes de arquivos compactados (gzip, zip, bzip2, lzop). ou em python, que também possui módulos para manipular arquivos compactados.

Aqui está uma versão perl que usa IO::Uncompress::AnyUncompress para permitir qualquer número de padrões e qualquer número de nomes de arquivos (contendo texto simples ou texto comprimido).

Todos os argumentos antes de -- são tratados como padrões de pesquisa. Todos os argumentos após -- são tratados como nomes de arquivos. Manipulação de opção primitiva, mas eficaz para este trabalho. A melhor manipulação de opções (por exemplo, para suportar uma opção -i para pesquisas sem distinção entre maiúsculas e minúsculas) pode ser obtida com os módulos Getopt::Std ou Getopt::Long .

Execute assim:

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

(Eu não vou listar os arquivos {1..6}.txt.gz e {1..6}.txt aqui ... eles apenas contêm algumas ou todas as palavras "um" "dois" "três" "quatro" "cinco" e "seis" para Os arquivos listados na saída acima DO contêm todos os três padrões de pesquisa. Teste você mesmo com seus próprios dados)

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

Um hash %patterns contém o conjunto completo de padrões que os arquivos devem conter pelo menos um de cada membro $_pstring é uma string contendo as chaves classificadas desse hash. A string $pattern contém uma expressão regular pré-compilada também criada a partir do %patterns hash.

$pattern é comparado com cada linha de cada arquivo de entrada (usando o modificador /o para compilar $pattern apenas uma vez, como sabemos que nunca irá mudar durante a execução), e map() é usado para construir um hash (% s) contendo as correspondências para cada arquivo.

Sempre que todos os padrões foram vistos no arquivo atual (comparando se $m_string (as chaves classificadas em %s ) é igual a $p_string ), imprima o nome do arquivo e pule para o próximo arquivo.

Esta não é uma solução particularmente rápida, mas não é excessivamente lenta. A primeira versão levou 4m58s para procurar três palavras em 74 MB de arquivos de log compactados (totalizando 937 MB não compactados). Esta versão atual leva 1m13s. Provavelmente há outras otimizações que poderiam ser feitas.

Uma otimização óbvia é usar isso em conjunto com xargs ' -P aka --max-procs para executar várias pesquisas em subconjuntos dos arquivos em paralelo. Para fazer isso, você precisa contar o número de arquivos e dividir pelo número de núcleos / cpus / threads que seu sistema possui (e arredondar para cima adicionando 1). por exemplo. havia 269 arquivos sendo pesquisados no meu conjunto de amostra, e meu sistema tem 6 núcleos (um AMD 1090T), então:

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

Com essa otimização, levou apenas 23 segundos para encontrar todos os 18 arquivos correspondentes. Naturalmente, o mesmo poderia ser feito com qualquer uma das outras soluções. OBSERVAÇÃO: A ordem dos nomes de arquivos listados na saída será diferente, portanto, talvez seja necessário classificá-los posteriormente, se isso for importante.

Como observado por @arekolek, vários zgrep s com find -exec ou xargs podem fazer isso significativamente mais rápido, mas esse script tem a vantagem de suportar qualquer número de padrões para pesquisar e é capaz de lidar com vários tipos diferentes de compressão.

Se o script estiver limitado a examinar apenas as primeiras 100 linhas de cada arquivo, ele será executado em todos eles (em minha amostra de 74 MB de 269 arquivos) em 0,6 segundos. Se isso for útil em alguns casos, pode ser feito em uma opção de linha de comando (por exemplo, -l 100 ), mas há o risco de não encontrar os arquivos correspondentes all .

BTW, de acordo com a página man do IO::Uncompress::AnyUncompress , os formatos de compactação suportados são:

Uma última otimização (espero). Usando o módulo PerlIO::gzip (empacotado em debian como libperlio-gzip-perl ) em vez de IO::Uncompress::AnyUncompress , reduzi o tempo para cerca de 3,1 segundos para processar meus 74 MB de arquivos de log. Também houve algumas pequenas melhorias usando um simples hash em vez de Set::Scalar (que também economizou alguns segundos com a versão IO::Uncompress::AnyUncompress ).

PerlIO::gzip foi recomendado como o gunzip perl mais rápido no link (encontrado com uma pesquisa no google por perl fast gzip decompress )

Usar xargs -P com isso não melhorou nem um pouco. Na verdade, pareceu reduzir a velocidade de 0,1 a 0,7 segundos. (Eu tentei quatro corridas e meu sistema faz outras coisas no fundo que irão alterar o tempo)

O preço é que essa versão do script só pode manipular arquivos compactados e não compactados. Velocidade vs flexibilidade: 3,1 segundos para esta versão versus 23 segundos para a versão IO::Uncompress::AnyUncompress com um wrapper xargs -P (ou 1m13s sem 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;
    }
  }
}
    
por 30.06.2016 / 11:52
11

Defina o separador de registro como . para que awk trate o arquivo inteiro como uma linha:

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

Da mesma forma com perl :

perl -ln00e '/one/&&/two/&&/three/ && print $ARGV' *
    
por 30.06.2016 / 12:09
3

Para arquivos compactados, você pode fazer um loop em cada arquivo e descompactar primeiro. Então, com uma versão ligeiramente modificada das outras respostas, você pode fazer:

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

O script Perl sairá com 0 status (sucesso) se todas as três strings forem encontradas. A abreviação }{ é Perl para END{} . Qualquer coisa após ela será executada após todas as entradas terem sido processadas. Assim, o script sairá com um status de saída diferente de 0, se nem todas as strings forem encontradas. Portanto, o && printf '%s\n' "$f" imprimirá o nome do arquivo somente se todos os três forem encontrados.

Ou para evitar o carregamento do arquivo na memória:

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, se você realmente quiser fazer a coisa toda em um script, você pode fazer:

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

Salve o script acima como foo.pl em algum lugar no seu $PATH , torne-o executável e execute-o assim:

foo.pl one two three *
    
por 30.06.2016 / 12:58
0

Outra opção - insira uma palavra de cada vez em xargs para executar grep no arquivo. xargs pode ser feito para sair assim que uma invocação de grep retornar falha retornando 255 para ele (verifique a documentação de xargs ). É claro que a desova de conchas e bifurcações envolvidas nesta solução provavelmente irá atrasá-la significativamente

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

e fazer um loop

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
    
por 01.07.2016 / 20:29