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

16

Estou procurando uma maneira de listar todos os arquivos em um diretório que contenham 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, podem ser duas ou quatro e assim por diante.

Uma segunda maneira em 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 essa definitivamente não é a direção que eu vou aqui. Eu quero algo que exige menos digitação, e possivelmente apenas uma chamada para grep, awk, perlou similar.

Por exemplo, eu gosto de como awkpermite combinar linhas que contêm 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 compatíveis com o gzip, por exemplo, greptem a zgrepvariante que funciona em arquivos compactados. Por que mencionei isso, é que algumas soluções podem não funcionar bem, devido a essa restrição. Por exemplo, no awkexemplo 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, é necessário ligar awkvárias vezes, mesmo que você possa fazê-lo apenas uma vez com arquivos não compactados. E certamente, seria melhor fazer zawk '/pattern/ {print FILENAME; nextfile}' *e obter o mesmo efeito, então eu preferiria soluções que permitam isso.

arekolek
fonte
1
Você não precisa que eles sejam gzipamigáveis, apenas zcatos arquivos primeiro.
terdon
@terdon Eu editei o post, explicando porque mencionei que os arquivos estão compactados.
arekolek
Não há muita diferença entre iniciar o awk uma ou várias vezes. Quero dizer, OK, algumas pequenas despesas gerais, mas duvido que você notaria a diferença. É claro que é possível tornar o awk / perl qualquer script que faça isso por si só, mas isso começa a se tornar um programa completo e não um one-liner rápido. É isso que você quer?
terdon
@terdon Pessoalmente, o aspecto mais importante para mim é o quão complicado será o comando (acho que minha segunda edição ocorreu enquanto você estava comentando). Por exemplo, as grepsoluções são facilmente adaptáveis ​​apenas prefixando as grepchamadas com a z, não há necessidade de eu também lidar com nomes de arquivos.
arekolek
Sim, mas é isso grep. AFAIK, apenas grepe cattem "variantes z" padrão. Acho que você não conseguirá nada mais simples do que usar uma for f in *; do zcat -f $f ...solução. Qualquer outra coisa teria que ser um programa completo que verifique os formatos de arquivo antes de abrir ou use uma biblioteca para fazer o mesmo.
terdon

Respostas:

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

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

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

Se você quiser manipular automaticamente arquivos compactados com gzip, execute-o em um loop com zcat(lento e ineficiente, porque você estará bifurcando awkvárias vezes em um loop, uma vez para cada nome de arquivo) ou reescreva o mesmo algoritmo perle use o IO::Uncompress::AnyUncompressmódulo de biblioteca que pode descompacte 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 perlversão usada IO::Uncompress::AnyUncompresspara permitir qualquer número de padrões e nomes de arquivos (contendo texto sem formatação ou texto compactado).

Todos os argumentos anteriores --são tratados como padrões de pesquisa. Todos os argumentos posteriores --são tratados como nomes de arquivos. Manuseio de opções primitivo, mas eficaz para este trabalho. Melhor manipulação opção (por exemplo, para suportar uma -iopção para pesquisas maiúsculas e minúsculas) poderia ser alcançado com o Getopt::Stdou Getopt::Longos módulos.

Execute-o assim:

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

(Não listarei arquivos {1..6}.txt.gze {1..6}.txtaqui ... eles contêm apenas algumas ou todas as palavras "um" "dois" "três" "quatro" "cinco" e "seis" para teste. Os arquivos listados na saída acima Contenha 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 sequência que contém as chaves classificadas desse hash. A sequência $patterncontém uma expressão regular pré-compilada também criada a partir do %patternshash.

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

Sempre que todos os padrões tiverem sido vistos no arquivo atual (comparando se $m_string(as chaves classificadas %s) são iguais 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 arquivos de log compactados no valor de 74 MB (totalizando 937 MB descompactados). Esta versão atual leva 1m13s. Provavelmente existem outras otimizações que poderiam ser feitas.

Uma otimização óbvia é usar isso em conjunto com xargso -Paka --max-procspara 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 amostras e meu sistema possui 6 núcleos (um 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[@]}" --

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

Conforme observado por @arekolek, vários zgreps com find -execou xargspodem fazê-lo significativamente mais rápido, mas esse script tem a vantagem de oferecer suporte a qualquer número de padrões a serem pesquisados ​​e é capaz de lidar com vários tipos diferentes de compactação.

Se o script estiver limitado a examinar apenas as primeiras 100 linhas de cada arquivo, ele será executado em todas elas (no meu exemplo de 74MB de 269 arquivos) em 0,6 segundos. Se isso for útil em alguns casos, poderá ser transformado em uma opção de linha de comando (por exemplo -l 100), mas corre o risco de não encontrar todos os arquivos correspondentes.


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


Uma última (espero) otimização. Ao usar o PerlIO::gzipmódulo (empacotado no debian as libperlio-gzip-perl) em vez de IO::Uncompress::AnyUncompressreduzir o tempo para cerca de 3,1 segundos para processar meus 74 MB de arquivos de log. Houve também algumas pequenas melhorias usando um hash simples em vez de Set::Scalar(o que também economizou alguns segundos com a IO::Uncompress::AnyUncompressversão).

PerlIO::gzipfoi recomendado como o gunzip perl mais rápido em /programming//a/1539271/137158 (encontrado em uma pesquisa no google perl fast gzip decompress)

Usar xargs -Pcom isso não melhorou nada. De fato, parecia até abrandar em 0,1 a 0,7 segundos. (Tentei quatro execuções e meu sistema faz outras coisas em segundo plano, o que altera o tempo)

O preço é que esta versão do script pode manipular apenas arquivos compactados e compactados com gzip. Velocidade vs flexibilidade: 3,1 segundos para esta versão vs 23 segundos para a IO::Uncompress::AnyUncompressversão com um xargs -Pinvólucro (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;
    }
  }
}
cas
fonte
for f in *; do zcat $f | awk -v F=$f '/one/ {a++}; /two/ {b++}; /three/ {c++}; a&&b&&c { print F; nextfile }'; donefunciona bem, mas na verdade leva três vezes mais tempo que a minha grepsolução e é realmente mais complicado.
arekolek
1
OTOH, para arquivos de texto simples, seria mais rápido. e o mesmo algoritmo implementado em uma linguagem com suporte para leitura de arquivos compactados (como perl ou python), como sugeri, seria mais rápido que vários greps. "complicação" é parcialmente subjetivo - pessoalmente, acho que um único script awk ou perl ou python é menos complicado do que vários greps com ou sem encontrar .... A resposta de @ terdon é boa e faz isso sem a necessidade do módulo que mencionei (mas ao custo de bifurcar o zcat para cada arquivo compactado)
cas
Eu tive que apt-get install libset-scalar-perlusar o script. Mas parece não terminar em um tempo razoável.
Arekolek
quantos e qual o tamanho (compactado e descompactado) dos arquivos que você está pesquisando? dezenas ou centenas de arquivos de tamanho médio-pequeno ou milhares de arquivos grandes?
cas
Aqui está um histograma do tamanho dos arquivos compactados (20 a 100 arquivos, até 50 MB, mas principalmente abaixo de 5 MB). Olhar descompactado o mesmo, mas com tamanhos multiplicado por 10.
arekolek
11

Defina o separador de registros como .para awktratar 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' *
jimmij
fonte
3
Arrumado. Observe que isso carregará o arquivo inteiro na memória e isso pode ser um problema para arquivos grandes.
terdon
Eu inicialmente votei isso porque parecia promissor. Mas não consigo fazê-lo funcionar com arquivos compactados em gzip. for f in *; do zcat $f | awk -v RS='.' -v F=$f '/one/ && /two/ && /three/ { print F }'; donenão produz nada.
arekolek
@arekolek Esse loop funciona para mim. Seus arquivos estão compactados corretamente?
jimmij
@arekolek que você precisa zcat -f "$f"se alguns dos arquivos não estiverem compactados.
terdon
Também testei em arquivos não compactados e awk -v RS='.' '/bfs/&&/none/&&/rgg/{print FILENAME}' greptest/*.txtainda não retorna resultados, enquanto grep -l rgg $(grep -l none $(grep -l bfs greptest/*.txt))retorna os resultados esperados.
Arekolek
3

Para arquivos compactados, você pode fazer um loop sobre cada arquivo e descomprimir primeiro. Em seguida, com uma versão ligeiramente modificada das outras respostas, você pode:

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 0status (sucesso) se todas as três cadeias forem encontradas. O }{é Perl, abreviação de END{}. Qualquer coisa a seguir será executada após todas as entradas terem sido processadas. Portanto, o script sairá com um status de saída diferente de 0 se nem todas as seqüências de caracteres foram encontradas. Portanto, && printf '%s\n' "$f"ele imprimirá o nome do arquivo apenas se todos os três forem encontrados.

Ou, para evitar carregar o 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

Por fim, se você realmente deseja fazer a coisa toda em um script, pode:

#!/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.plem algum lugar no seu $PATH, torne-o executável e execute-o assim:

foo.pl one two three *
terdon
fonte
2

De todas as soluções propostas até agora, minha solução original usando grep é a mais rápida, terminando em 25 segundos. A desvantagem é que é entediante adicionar e remover palavras-chave. Então, eu vim com um script (apelidado multi) que simula o comportamento, mas permite alterar 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"

Então, agora, escrever multi grep one two three -- *é equivalente à minha proposta original e é executado ao mesmo tempo. Também posso usá-lo facilmente em arquivos compactados usando zgrepo primeiro argumento.

Outras soluções

Também experimentei um script Python usando duas estratégias: pesquisar todas as palavras-chave linha por linha e pesquisar no arquivo inteiro palavra-chave por palavra-chave. A segunda estratégia foi mais rápida no meu caso. Mas foi mais lento do que apenas usar 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, demorou 39 segundos em tempo de espera, porque meu processador é dual core. O que é interessante, porque meu script Python levou 49 segundos de tempo de exibição (e greptinha 29 segundos).

O script cas não conseguiu terminar em um tempo razoável, mesmo em um número menor de arquivos que foram processados ​​em grepmenos de 4 segundos, então tive que eliminá-lo.

Mas sua awkproposta original , embora seja mais lenta do grepque é, tem uma vantagem potencial. Em alguns casos, pelo menos na minha experiência, é possível esperar que todas as palavras-chave apareçam em algum lugar do cabeçalho do arquivo, caso estejam no arquivo. Isso dá a esta solução um aumento drástico 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, em oposição a 25 segundos.

Obviamente, talvez não tenhamos a vantagem de procurar por palavras-chave conhecidas por ocorrerem no início dos arquivos. Nesse caso, a solução sem NR>100 {exit}demora 63 segundos (50s de tempo na parede).

Arquivos não compactados

Não há diferença significativa no tempo de execução entre minha grepsolução e a awkproposta do caso, ambas levam uma fração de segundo para serem executadas.

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

Uma desvantagem da grepsolução que usa substituição de comando é que ela travará se em qualquer lugar da cadeia, antes da última etapa, não houver arquivos correspondentes. Isso não afeta a xargsvariante porque o tubo será abortado uma vez que grepretorne um status diferente de zero. Atualizei meu script para uso, xargspara que eu não precise lidar com isso sozinho, tornando o script mais simples.

arekolek
fonte
A sua solução pitão pode beneficiar de empurrar o laço para baixo para a camada C comnot all(p in text for p in patterns)
Iruvar
@iruvar Obrigado pela sugestão. Eu tentei (sans not) e terminei em 32 segundos, então não há muita melhoria, mas é certamente mais legível.
Arekolek
você poderia usar uma matriz associativa em vez de f1, f2, f3 no awk, com key = padrão de pesquisa, val = count
caso
@arekolek veja minha versão mais recente usando em PerlIO::gzipvez de IO::Uncompress::AnyUncompress. agora leva apenas 3,1 segundos em vez de 1m13s para processar meus 74MB de arquivos de log.
22416
BTW, se você executou anteriormente eval $(lesspipe)(por exemplo, no seu .profile, etc), você pode usar em lessvez de zcat -fe seu forwrapper de loop awkpoderá processar qualquer tipo de arquivo que lesspossa (gzip, bzip2, xz e mais) .... less pode detectar se stdout é um pipe e apenas enviará um fluxo para stdout, se for.
22416
0

Outra opção - alimente as palavras uma de cada vez para xargsque elas sejam executadas grepno arquivo. xargsele próprio pode sair assim que uma invocação de grepretornos falhar retornando 255a ele (consulte a xargsdocumentação). É claro que a criação de conchas e bifurcações envolvidas nesta solução provavelmente diminuirá significativamente

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

e para enrolá-lo

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
fonte
Parece bom, mas não sei como usar isso. O que é _e file? Essa pesquisa em vários arquivos passou como argumento e retornará arquivos que contêm todas as palavras-chave?
Arekolek
@arekolek, adicionou uma versão em loop. E, quanto a _isso, está sendo passado como o $0para o shell gerado - isso apareceria como o nome do comando na saída de ps- eu adiaria para o mestre aqui
Iruvar