Rechercher des fichiers contenant plusieurs mots clés n'importe où dans le fichier

16

Je cherche un moyen de répertorier tous les fichiers d'un répertoire contenant l'ensemble complet des mots clés que je recherche, n'importe où dans le fichier.

Ainsi, les mots clés n'ont pas besoin d'apparaître sur la même ligne.

Une façon de procéder serait:

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

Trois mots clés ne sont qu'un exemple, il pourrait tout aussi bien être deux ou quatre, et ainsi de suite.

Une deuxième façon de penser est:

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

Une troisième méthode, apparue dans une autre question , serait:

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

Mais ce n'est certainement pas la direction que je prends ici. Je veux quelque chose qui nécessite moins de frappe, et peut - être juste un appel à grep, awk, perlou similaire.

Par exemple, j'aime comment awkvous permet de faire correspondre des lignes qui contiennent tous les mots clés , comme:

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

Ou, imprimez uniquement les noms de fichiers:

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

Mais je veux trouver des fichiers où les mots-clés peuvent être n'importe où dans le fichier, pas nécessairement sur la même ligne.


Les solutions préférées seraient gzip friendly, par exemple grepa la zgrepvariante qui fonctionne sur les fichiers compressés. Pourquoi je mentionne cela, c'est que certaines solutions peuvent ne pas fonctionner correctement compte tenu de cette contrainte. Par exemple, dans l' awkexemple d'impression de fichiers correspondants, vous ne pouvez pas simplement faire:

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

Vous devez modifier considérablement la commande, en quelque chose comme:

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

Donc, en raison de la contrainte, vous devez appeler awkplusieurs fois, même si vous ne pouvez le faire qu'une seule fois avec des fichiers non compressés. Et certainement, il serait plus agréable de le faire zawk '/pattern/ {print FILENAME; nextfile}' *et d'obtenir le même effet, donc je préférerais des solutions qui permettent cela.

arekolek
la source
1
Vous n'avez pas besoin qu'ils soient gzipconviviaux, juste zcatles fichiers en premier.
terdon
@terdon J'ai édité l'article, expliquant pourquoi je mentionne que les fichiers sont compressés.
arekolek
Il n'y a pas beaucoup de différence entre lancer une ou plusieurs fois awk. Je veux dire, OK, quelques petits frais généraux mais je doute que vous remarquiez même la différence. Il est bien sûr possible de faire en sorte que le script awk / perl le fasse lui-même, mais cela commence à devenir un programme complet et non pas une ligne rapide. Est-ce que c'est ce que tu veux?
terdon
@terdon Personnellement, l'aspect le plus important pour moi est la complexité de la commande (je suppose que ma deuxième édition est venue pendant que vous commentiez). Par exemple, les grepsolutions sont facilement adaptables simplement en préfixant les grepappels avec un z, il n'est pas nécessaire que je gère également les noms de fichiers.
arekolek
Oui, mais c'est grep. AFAIK, uniquement grepet catont des "z-variantes" standard. Je ne pense pas que vous obtiendrez quelque chose de plus simple que d'utiliser une for f in *; do zcat -f $f ...solution. Tout le reste devrait être un programme complet qui vérifie les formats de fichiers avant d'ouvrir ou utilise une bibliothèque pour faire de même.
terdon

Réponses:

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

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

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

Si vous souhaitez gérer automatiquement les fichiers gzippés, exécutez-le en boucle avec zcat(lent et inefficace car vous bifurquerez awkplusieurs fois dans une boucle, une fois pour chaque nom de fichier) ou réécrivez le même algorithme perlet utilisez le IO::Uncompress::AnyUncompressmodule de bibliothèque qui peut décompressez plusieurs types de fichiers compressés (gzip, zip, bzip2, lzop). ou en python, qui a également des modules pour gérer les fichiers compressés.


Voici une perlversion qui utilise IO::Uncompress::AnyUncompresspour autoriser un nombre illimité de modèles et un nombre illimité de noms de fichiers (contenant du texte brut ou du texte compressé).

Tous les arguments avant --sont traités comme des modèles de recherche. Tous les arguments après-- sont traités comme des noms de fichiers. Gestion des options primitive mais efficace pour ce travail. Une meilleure gestion des options (par exemple pour prendre en charge une -ioption pour les recherches non sensibles à la casse) pourrait être obtenue avec les modules Getopt::Stdou Getopt::Long.

Exécutez-le comme ceci:

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

(Je ne répertorierai pas les fichiers {1..6}.txt.gzet{1..6}.txt ici ... ils contiennent juste une partie ou la totalité des mots "un" "deux" "trois" "quatre" "cinq" et "six" pour les tests. Les fichiers répertoriés dans la sortie ci-dessus Contenez les trois modèles de recherche. Testez-le vous-même avec vos propres données)

#! /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 hachage %patternscontient l'ensemble complet de modèles que les fichiers doivent contenir au moins un de chaque membre $_pstringest une chaîne contenant les clés triées de ce hachage. La chaîne $patterncontient une expression régulière précompilée également construite à partir du %patternshachage.

$patternest comparé à chaque ligne de chaque fichier d'entrée (en utilisant le /omodificateur pour compiler $patternune seule fois car nous savons qu'il ne changera jamais pendant l'exécution), et map()est utilisé pour créer un hachage (% s) contenant les correspondances pour chaque fichier.

Chaque fois que tous les modèles ont été vus dans le fichier actuel (en comparant si $m_string(les clés triées dans %s) sont égales à $p_string), imprimez le nom de fichier et passez au fichier suivant.

Ce n'est pas une solution particulièrement rapide, mais ce n'est pas excessivement lent. La première version a pris 4 min 58 s pour rechercher trois mots dans 74 Mo de fichiers journaux compressés (totalisant 937 Mo non compressés). Cette version actuelle prend 1m13s. Il y a probablement d'autres optimisations qui pourraient être faites.

Une optimisation évidente consiste à l'utiliser en conjonction avec xargs's -Paka --max-procspour exécuter plusieurs recherches sur des sous-ensembles de fichiers en parallèle. Pour ce faire, vous devez compter le nombre de fichiers et diviser par le nombre de cœurs / cpus / threads de votre système (et arrondir en ajoutant 1). Par exemple, 269 fichiers ont été recherchés dans mon jeu d'échantillons et mon système a 6 cœurs (un AMD 1090T), donc:

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

Avec cette optimisation, il n'a fallu que 23 secondes pour trouver les 18 fichiers correspondants. Bien sûr, la même chose pourrait être faite avec n'importe quelle autre solution. REMARQUE: l'ordre des noms de fichiers répertoriés dans la sortie sera différent, il peut donc être nécessaire de les trier ultérieurement si cela est important.

Comme indiqué par @arekolek, plusieurs zgreps avec find -execou xargspeuvent le faire beaucoup plus rapidement, mais ce script a l'avantage de prendre en charge un certain nombre de modèles à rechercher et est capable de gérer plusieurs types de compression différents.

Si le script se limite à examiner uniquement les 100 premières lignes de chaque fichier, il les traverse toutes (dans mon échantillon de 74 Mo de 269 fichiers) en 0,6 seconde. Si cela est utile dans certains cas, il pourrait être transformé en une option de ligne de commande (par exemple -l 100) mais il a le risque de ne pas trouver tous les fichiers correspondants.


BTW, selon la page de manuel de IO::Uncompress::AnyUncompress, les formats de compression pris en charge sont:


Une dernière (j'espère) optimisation. En utilisant le PerlIO::gzipmodule (empaqueté dans debian as libperlio-gzip-perl) au lieu de IO::Uncompress::AnyUncompressj'ai réduit le temps à environ 3,1 secondes pour traiter mes 74 Mo de fichiers journaux. Il y a également eu quelques petites améliorations en utilisant un hachage simple plutôt que Set::Scalar(ce qui a également sauvé quelques secondes avec leIO::Uncompress::AnyUncompress version).

PerlIO::gzipa été recommandé comme le gunzip perl le plus rapide dans /programming//a/1539271/137158 (trouvé avec une recherche google pour perl fast gzip decompress)

Utiliser xargs -Pavec cela ne l'a pas amélioré du tout. En fait, il semblait même le ralentir de 0,1 à 0,7 seconde. (J'ai essayé quatre runs et mon système fait d'autres choses en arrière-plan qui modifieront le timing)

Le prix est que cette version du script ne peut gérer que les fichiers compressés et compressés. Vitesse vs flexibilité: 3,1 secondes pour cette version vs 23 secondes pour la IO::Uncompress::AnyUncompressversion avec xargs -Pwrapper (ou 1m13 sans 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
la source
for f in *; do zcat $f | awk -v F=$f '/one/ {a++}; /two/ {b++}; /three/ {c++}; a&&b&&c { print F; nextfile }'; donefonctionne bien, mais en effet, prend 3 fois plus de temps que ma grepsolution, et est en fait plus compliqué.
arekolek
1
OTOH, pour les fichiers en texte brut, ce serait plus rapide. et le même algorithme implémenté dans un langage prenant en charge la lecture de fichiers compressés (comme perl ou python) comme je l'ai suggéré serait plus rapide que plusieurs greps. "complication" est partiellement subjective - personnellement, je pense qu'un seul script awk ou perl ou python est moins compliqué que plusieurs greps avec ou sans trouver .... La réponse de @ terdon est bonne, et le fait sans avoir besoin du module que j'ai mentionné (mais au prix de bifurquer zcat pour chaque fichier compressé)
cas
J'ai dû apt-get install libset-scalar-perlutiliser le script. Mais cela ne semble pas se terminer dans un délai raisonnable.
arekolek
combien et quelle taille (compressé et non compressé) les fichiers que vous recherchez? des dizaines ou des centaines de fichiers de petite ou moyenne taille ou des milliers de gros fichiers?
cas
Voici un histogramme de la taille des fichiers compressés (20 à 100 fichiers, jusqu'à 50 Mo mais principalement en dessous de 5 Mo). Les fichiers non compressés sont identiques, mais avec des tailles multipliées par 10.
arekolek
11

Définissez le séparateur d'enregistrements sur .afin que awkle fichier entier soit traité comme une seule ligne:

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

De même avec perl:

perl -ln00e '/one/&&/two/&&/three/ && print $ARGV' *
jimmij
la source
3
Soigné. Notez que cela chargera tout le fichier en mémoire et cela pourrait être un problème pour les fichiers volumineux.
terdon
J'ai d'abord voté pour cela, car cela semblait prometteur. Mais je ne peux pas le faire fonctionner avec des fichiers compressés. for f in *; do zcat $f | awk -v RS='.' -v F=$f '/one/ && /two/ && /three/ { print F }'; donene produit rien.
arekolek
@arekolek Cette boucle fonctionne pour moi. Vos fichiers sont-ils correctement compressés?
jimmij
@arekolek dont vous avez besoin zcat -f "$f"si certains fichiers ne sont pas compressés.
terdon
Je l'ai également testé sur des fichiers non compressés et awk -v RS='.' '/bfs/&&/none/&&/rgg/{print FILENAME}' greptest/*.txtne renvoie toujours aucun résultat, tandis que grep -l rgg $(grep -l none $(grep -l bfs greptest/*.txt))renvoie les résultats attendus.
arekolek
3

Pour les fichiers compressés, vous pouvez parcourir chaque fichier en boucle et décompresser en premier. Ensuite, avec une version légèrement modifiée des autres réponses, vous pouvez faire:

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

Le script Perl se terminera avec le 0statut (succès) si les trois chaînes ont été trouvées. C'est l' }{abréviation de Perl END{}. Tout ce qui suit sera exécuté une fois toutes les entrées traitées. Le script se terminera donc avec un état de sortie différent de 0 si toutes les chaînes n'ont pas été trouvées. Par conséquent, le && printf '%s\n' "$f"affichera le nom du fichier uniquement si les trois ont été trouvés.

Ou, pour éviter de charger le fichier en mémoire:

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

Enfin, si vous voulez vraiment tout faire dans un script, vous pouvez faire:

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

Enregistrez le script ci-dessus comme foo.plquelque part dans votre $PATH, rendez-le exécutable et exécutez-le comme ceci:

foo.pl one two three *
terdon
la source
2

De toutes les solutions proposées jusqu'à présent, ma solution originale utilisant grep est la plus rapide, terminant en 25 secondes. Son inconvénient est qu'il est fastidieux d'ajouter et de supprimer des mots clés. J'ai donc trouvé un script (doublé multi) qui simule le comportement, mais permet de changer la syntaxe:

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

Alors maintenant, l'écriture multi grep one two three -- *est équivalente à ma proposition d'origine et s'exécute en même temps. Je peux également l'utiliser facilement sur des fichiers compressés en utilisant zgrepcomme premier argument à la place.

Autres solutions

J'ai également expérimenté un script Python en utilisant deux stratégies: rechercher tous les mots clés ligne par ligne et rechercher dans le fichier entier mot clé par mot clé. La deuxième stratégie a été plus rapide dans mon cas. Mais c'était plus lent que la simple utilisation grep, terminant en 33 secondes. La correspondance des mots clés ligne par ligne s'est terminée en 60 secondes.

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

Le script donné par terdon s'est terminé en 54 secondes. En fait, cela a pris 39 secondes de temps de mur, car mon processeur est dual core. Ce qui est intéressant, car mon script Python a pris 49 secondes de temps de mur (et grepétait de 29 secondes).

Le script par cas n'a pas pu se terminer dans un délai raisonnable, même sur un plus petit nombre de fichiers traités en grepmoins de 4 secondes, j'ai donc dû le tuer.

Mais sa awkproposition originale , même si elle est plus lente qu'actuellement grep, a un avantage potentiel. Dans certains cas, du moins d'après mon expérience, il est possible de s'attendre à ce que tous les mots clés apparaissent tous quelque part dans la tête du fichier s'ils le sont. Cela donne à cette solution une amélioration spectaculaire des performances:

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

Termine en un quart de seconde, contre 25 secondes.

Bien sûr, il se peut que nous n'ayons pas l'avantage de rechercher des mots clés connus pour se produire vers le début des fichiers. Dans ce cas, la solution sans NR>100 {exit}prend 63 secondes (50 secondes de temps de paroi).

Fichiers non compressés

Il n'y a pas de différence significative dans le temps d'exécution entre ma grepsolution et la awkproposition de CAS , les deux prennent une fraction de seconde à exécuter.

Notez que l'initialisation des variables FNR == 1 { f1=f2=f3=0; }est obligatoire dans ce cas pour réinitialiser les compteurs pour chaque fichier traité suivant. En tant que telle, cette solution nécessite de modifier la commande à trois endroits si vous souhaitez modifier un mot clé ou en ajouter de nouveaux. D'un autre côté, avec grepvous pouvez simplement ajouter| xargs grep -l four ou modifier le mot-clé que vous souhaitez.

Un inconvénient de la grepsolution qui utilise la substitution de commandes est qu'elle se bloque si, n'importe où dans la chaîne, avant la dernière étape, il n'y a pas de fichiers correspondants. Cela n'affecte pas la xargsvariante car le canal sera abandonné une fois qu'il grepretournera un état non nul. J'ai mis à jour mon script pour l'utiliser xargs, je n'ai donc pas à gérer cela moi-même, ce qui rend le script plus simple.

arekolek
la source
Votre solution Python pourrait tirer avantage de pousser la boucle vers la couche C avecnot all(p in text for p in patterns)
iruvar
@iruvar Merci pour la suggestion. Je l'ai essayé (sans not) et il s'est terminé en 32 secondes, donc pas beaucoup d'amélioration, mais c'est certainement plus lisible.
arekolek
vous pouvez utiliser un tableau associatif plutôt que f1, f2, f3 dans awk, avec key = search-pattern, val = count
cas
@arekolek voir ma dernière version en utilisant PerlIO::gzipplutôt que IO::Uncompress::AnyUncompress. prend désormais seulement 3,1 secondes au lieu de 1m13 pour traiter mes 74 Mo de fichiers journaux.
cas
BTW, si vous avez déjà exécuté eval $(lesspipe)(par exemple dans votre .profile, etc.), vous pouvez utiliser à la lessplace de zcat -fet votre forwrapper de boucle awkpourra traiter tout type de fichier qui lesspeut (gzip, bzip2, xz, et plus) .... less peut détecter si stdout est un pipe et ne fera que sortir un flux vers stdout s'il l'est.
cas
0

Une autre option - alimentez les mots un par un xargspour qu'il s'exécute grepsur le fichier. xargspeut lui-même être amené à sortir dès qu'une invocation de grepretours échoue en y retournant 255(consultez la xargsdocumentation). Bien sûr, le frai des obus et des fourches impliqués dans cette solution va probablement le ralentir considérablement

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

et pour boucler

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
la source
Cela a l'air bien, mais je ne sais pas comment l'utiliser. Qu'est-ce que _et file? Est-ce que cette recherche dans plusieurs fichiers est passée en argument et renvoie des fichiers contenant tous les mots clés?
arekolek
@arekolek, a ajouté une version en boucle. Et quant à _, il est passé en tant que $0à la coquille engendrée - cela apparaîtrait comme le nom de la commande dans la sortie de ps- je m'en remettrais au maître ici
iruvar