Trouvez des files contenant plusieurs mots-keys dans le file

Je cherche un moyen de listr tous les files dans un directory qui contient l'set des mots-keys que je cherche, n'importe où dans le file.

Ainsi, les mots-keys ne doivent pas apparaître sur la même ligne.

Une façon de le faire serait:

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

Trois mots-keys sont juste un exemple, il pourrait tout aussi bien être deux, ou quatre, et ainsi de suite.

Une deuxième façon dont je peux penser est:

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

Une troisième méthode, qui apparaît dans une autre question , serait la suivante:

 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 vais ici. Je veux quelque chose qui nécessite less de frappe, et peut-être un seul appel à grep , awk , perl ou similaire.

Par exemple, j'aime comment awk vous permet de faire correspondre des lignes qui contiennent tous les mots – keys , comme:

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

Ou, n'imprimez que les noms de files:

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

Mais je veux find des files où les mots-keys peuvent être n'importe où dans le file, pas nécessairement sur la même ligne.


Les solutions préférées seraient gzip friendly, par exemple grep a la variante zgrep qui fonctionne sur les files compressés. Pourquoi je mentionne ceci, c'est que certaines solutions peuvent ne pas fonctionner correctement count tenu de cette contrainte. Par exemple, dans l'exemple awk d'printing de files correspondants, vous ne pouvez pas simplement faire:

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

Vous devez modifier de manière significative la command, à quelque chose comme:

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

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

 awk 'FNR == 1 { f1=f2=f3=0; }; /one/ { f1++ }; /two/ { f2++ }; /three/ { f3++ }; f1 && f2 && f3 { print FILENAME; nextfile; }' * 

Si vous voulez gérer automatiquement les files gzip, exécutez-les en boucle avec zcat (lente et inefficace parce que vous allez vous awk plusieurs fois dans une boucle, une fois pour chaque nom de file) ou réécrivez le même algorithm en perl et utilisez l' IO::Uncompress::AnyUncompress module de bibliothèque qui peut décompresser plusieurs types différents de files compressés (gzip, zip, bzip2, lzop). ou en python, qui a également des modules pour gérer les files compressés.


Voici une version de perl qui utilise IO::Uncompress::AnyUncompress pour autoriser n'importe quel nombre de patterns et n'importe quel nombre de noms de files (contenant du text brut ou du text compressé).

Tous les arguments avant -- sont traités comme des templates de search. Tous les arguments après -- sont traités comme des noms de files. Gestion des options primitive mais efficace pour ce travail. Une meilleure gestion des options (par exemple pour prendre en charge une option -i pour les searchs insensibles à la casse) peut être réalisée avec les Getopt::Std ou 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 {1..6}.txt.gz pas les files {1..6}.txt.gz et {1..6}.txt ici … ils contiennent juste tout ou partie des mots "un" "deux" "trois" "quatre «cinq» et «six» pour les tests.Les files répertoriés dans la sortie ci-dessus DO contiennent tous les trois des templates de search.Tester vous-même avec vos propres données)

 #! /usr/bin/perl use ssortingct; 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_ssortingng=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_ssortingng=join('',sort keys %s); if ($m_ssortingng eq $p_ssortingng) { print "$f\n" ; last; } } } 

Un hachage %patterns contient l'set complet de motifs que les files doivent contenir au less un de chaque membre $_pssortingng est une string contenant les keys sortingées de ce hachage. La string $pattern contient une expression régulière pré-compilée également construite à partir du hash %patterns .

$pattern est comparé à chaque ligne de chaque file d'input (en utilisant le modificateur /o pour comstackr $pattern seulement une fois car on sait qu'il ne changera jamais pendant l'exécution) et map() pour build un hash (% s ) contenant les correspondances pour chaque file.

Chaque fois que tous les templates ont été vus dans le file en cours (en comparant si $m_ssortingng (les keys sortingées dans %s ) est égal à $p_ssortingng ), imprimez le nom du file et passez au file suivant.

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

Une optimization évidente est d'utiliser ceci en conjonction avec --max-procs de --max-procs pour exécuter plusieurs searchs sur des sous-sets de files en parallèle. Pour ce faire, vous devez countr le nombre de files et split par le nombre de cœurs / cpus / threads de votre système (et arrondir en ajoutant 1). Par exemple, 269 files 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 optimization, il a fallu seulement 23 secondes pour find les 18 files correspondants. Bien sûr, la même chose pourrait être faite avec l'une des autres solutions. REMARQUE: l'ordre des noms de files répertoriés dans la sortie sera différent, il peut donc être nécessaire de les sortinger par la suite si cela est important.

Comme l'a noté @arekolek, plusieurs scripts avec find -exec ou xargs peuvent le faire beaucoup plus rapidement, mais ce script a l'avantage de supporter n'importe quel nombre de templates à searchr et est capable de traiter différents types de compression.

Si le script est limité à l'examen des 100 premières lignes de chaque file, il les parcourt tous (dans mon échantillon de 269 files de 74 Mo) en 0,6 seconde. Si cela s'avère utile dans certains cas, il peut être converti en une option de command line (par exemple -l 100 ) mais il risque de ne pas find tous les files correspondants.


BTW, selon la page de manuel pour IO::Uncompress::AnyUncompress , les formats de compression supportés sont:

  • zlib RFC 1950 ,
  • dégonfler le RFC 1951 (facultativement),
  • gzip RFC 1952 ,
  • Zip *: français,
  • bzip2,
  • lzop,
  • lzf,
  • lzma,
  • xz

Une dernière optimization (j'espère). En utilisant le module PerlIO::gzip ( libperlio-gzip-perl debian sous le nom libperlio-gzip-perl ) au lieu de IO::Uncompress::AnyUncompress j'ai eu le time d'environ 3,1 secondes pour le traitement de mes 74 Mo de files journaux. Il y avait aussi quelques petites améliorations en utilisant un hash simple plutôt que Set::Scalar (qui a également sauvé quelques secondes avec la version IO::Uncompress::AnyUncompress ).

PerlIO::gzip été recommandé comme le plus rapide perz gunzip dans https://stackoverflow.com/a/1539271/137158 (trouvé avec une search google perl fast gzip decompress )

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

Le prix est que cette version du script ne peut gérer que les files gzip et non compressés. Vitesse vs flexibilité: 3,1 secondes pour cette version vs 23 secondes pour la version IO::Uncompress::AnyUncompress avec une enveloppe xargs -P (ou 1m13s sans xargs -P ).

 #! /usr/bin/perl use ssortingct; 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_ssortingng=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_ssortingng=join('',sort keys %s); if ($m_ssortingng eq $p_ssortingng) { print "$f\n" ; close(F); last; } } } 

Définissez le séparateur d'logging sur . de sorte que awk traitera le file entier comme une seule ligne:

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

De même avec perl :

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

Pour les files compressés, vous pouvez parcourir chaque file 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 quittera avec l'état 0 (succès) si les trois strings ont été trouvées. Le }{ est raccourci Perl pour END{} . Tout ce qui suit le sera exécuté après que toutes les inputs aient été traitées. Ainsi, le script quittera avec un statut de sortie non-0 si toutes les strings n'ont pas été trouvées. Par conséquent, le & & && printf '%s\n' "$f" n'imprimera le nom du file que si tous les trois ont été trouvés.

Ou, pour éviter de charger le file dans la memory:

 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 faire le tout dans un script, vous pourriez faire:

 #!/usr/bin/env perl use ssortingct; use warnings; ## Get the target ssortingngs and file names. The first three ## arguments are assumed to be the ssortingngs, 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); } 

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

 foo.pl one two three * 

De toutes les solutions proposées jusqu'ici, ma solution originale utilisant grep est la plus rapide, finissant en 25 secondes. L'inconvénient est qu'il est fastidieux d'append et de supprimer des mots-keys. Je suis donc venu avec 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, en écrivant multi grep one two three -- * est équivalent à ma proposition originale et fonctionne dans le même time. Je peux aussi l'utiliser facilement sur des files compressés en utilisant zgrep comme premier argument à la place.

Autres solutions

J'ai également expérimenté un script Python en utilisant deux stratégies: la search de tous les mots-keys ligne par ligne et la search dans le file entier mot-key par mot-key. La deuxième stratégie était plus rapide dans mon cas. Mais c'était plus lent que d'utiliser grep , finissant en 33 secondes. Ligne par mot key correspondant terminé 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 terminé en 54 secondes. En fait, il a fallu 39 secondes de time de mur, parce que mon processeur est dual core. Ce qui est intéressant, car mon script Python a pris 49 secondes de time de mur (et grep était de 29 secondes).

Le script de cas ne s'est pas terminé dans un timeout raisonnable, même sur un plus petit nombre de files qui ont été traités avec grep dans les 4 secondes, alors j'ai dû le tuer.

Mais sa proposition d' awk originale, même si elle est plus lente que grep , a un avantage potentiel. Dans certains cas, au less d'après mon expérience, il est possible de s'attendre à ce que tous les mots-keys apparaissent tous quelque part dans la tête du file s'ils se trouvent dans le file. Cela donne à cette solution un coup de pouce spectaculaire dans la performance:

 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 

Finit dans un quart de seconde, par opposition à 25 secondes.

Bien sûr, nous ne pouvons pas avoir l'avantage de searchr des mots-keys connus pour se produire au début des files. Dans ce cas, la solution sans NR>100 {exit} prend 63 secondes (50s de time de mur).

Fichiers non compressés

Il n'y a pas de différence significative dans le time de fonctionnement entre ma solution de grep et la proposition de cas ' awk , les deux prennent une fraction de seconde à exécuter.

Notez que l'initialisation de la variable FNR == 1 { f1=f2=f3=0; } FNR == 1 { f1=f2=f3=0; } est obligatoire dans ce cas pour réinitialiser les counturs pour chaque file traité ultérieur. En tant que telle, cette solution nécessite d'éditer la command à trois endroits si vous souhaitez modifier un mot-key ou en append de nouveaux. D'autre part, avec grep vous pouvez simplement append | xargs grep -l four | xargs grep -l four ou modifiez le mot-key que vous voulez.

Un inconvénient de la solution grep qui utilise la substitution de commands, c'est qu'elle se bloquera si quelque part dans la string, avant la dernière étape, il n'y a pas de files correspondants. Cela n'affecte pas la variante xargs car le canal sera abandonné une fois que grep aura returnné un état non nul. J'ai mis à jour mon script pour utiliser xargs donc je n'ai pas à le gérer moi-même, ce qui rend le script plus simple.

Une autre option – nourrir les mots un à la fois à xargs pour qu'il exécute grep contre le file. xargs peut se faire quitter dès qu'une invocation de grep renvoie un échec en lui returnnant 255 (consultez la documentation de xargs ). Bien sûr, le frai des coquilles et du fourrage impliqué dans cette solution va probablement le ralentir de manière significative

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

et de le 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