Comment compter les occurrences de texte dans un fichier?


19

J'ai un fichier journal trié par adresses IP, je veux trouver le nombre d'occurrences de chaque adresse IP unique. Comment puis-je faire cela avec bash? Énumérant éventuellement le nombre d'occurrences à côté d'une adresse IP, comme:

5.135.134.16 count: 5
13.57.220.172: count 30
18.206.226 count:2

etc.

Voici un exemple du journal:

5.135.134.16 - - [23/Mar/2019:08:42:54 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
5.135.134.16 - - [23/Mar/2019:08:42:55 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
5.135.134.16 - - [23/Mar/2019:08:42:55 -0400] "POST /wp-login.php HTTP/1.1" 200 3836 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
5.135.134.16 - - [23/Mar/2019:08:42:55 -0400] "POST /wp-login.php HTTP/1.1" 200 3988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
5.135.134.16 - - [23/Mar/2019:08:42:56 -0400] "POST /xmlrpc.php HTTP/1.1" 200 413 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:05 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:06 -0400] "POST /wp-login.php HTTP/1.1" 200 3985 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:07 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:08 -0400] "POST /wp-login.php HTTP/1.1" 200 3833 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:09 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:11 -0400] "POST /wp-login.php HTTP/1.1" 200 3836 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:12 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:15 -0400] "POST /wp-login.php HTTP/1.1" 200 3837 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:17 -0400] "POST /xmlrpc.php HTTP/1.1" 200 413 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.233.99 - - [23/Mar/2019:04:17:45 -0400] "GET / HTTP/1.1" 200 25160 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36"
18.206.226.75 - - [23/Mar/2019:21:58:07 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "https://www.google.com/url?3a622303df89920683e4421b2cf28977" "Mozilla/5.0 (Windows NT 6.2; rv:33.0) Gecko/20100101 Firefox/33.0"
18.206.226.75 - - [23/Mar/2019:21:58:07 -0400] "POST /wp-login.php HTTP/1.1" 200 3988 "https://www.google.com/url?3a622303df89920683e4421b2cf28977" "Mozilla/5.0 (Windows NT 6.2; rv:33.0) Gecko/20100101 Firefox/33.0"
18.213.10.181 - - [23/Mar/2019:14:45:42 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
18.213.10.181 - - [23/Mar/2019:14:45:42 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
18.213.10.181 - - [23/Mar/2019:14:45:42 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"

1
Avec «bash», voulez-vous dire le shell ordinaire ou la ligne de commande en général?
dessert

1
Avez-vous un logiciel de base de données à utiliser?
SpacePhoenix


Le journal provient d'un serveur appache2, pas vraiment d'une base de données. bash est ce que je préférerais, dans un cas d'utilisation général. Je vois les solutions python et perl, si elles sont bonnes pour quelqu'un d'autre, c'est super. le tri initial a été fait sort -Vmais je pense que ce n'était pas nécessaire. J'ai envoyé les 10 principaux abuseurs de la page de connexion à l'administrateur du système avec des recommandations pour interdire les sous-réseaux respectifs. par exemple, une adresse IP a atteint la page de connexion plus de 9 000 fois. cette IP et son sous-réseau de classe D sont désormais sur liste noire. Je suis sûr que nous pourrions automatiser cela, bien que ce soit une question différente.
J0H

Réponses:


13

Vous pouvez utiliser grepet uniqpour la liste des adresses, les parcourir en boucle et grepencore pour le décompte:

for i in $(<log grep -o '^[^ ]*' | uniq); do
  printf '%s count %d\n' "$i" $(<log grep -c "$i")
done

grep -o '^[^ ]*'renvoie chaque caractère du début ( ^) jusqu'au premier espace de chaque ligne, uniqsupprime les lignes répétées, vous laissant ainsi une liste d'adresses IP. Grâce à la substitution de commande, la forboucle fait une boucle sur cette liste en imprimant l'IP en cours de traitement suivi de «count» et du count. Ce dernier est calculé par grep -c, qui compte le nombre de lignes avec au moins une correspondance.

Exemple d'exécution

$ for i in $(<log grep -o '^[^ ]*'|uniq);do printf '%s count %d\n' "$i" $(<log grep -c "$i");done
5.135.134.16 count 5
13.57.220.172 count 9
13.57.233.99 count 1
18.206.226.75 count 2
18.213.10.181 count 3

13
Cette solution parcourt le fichier d'entrée à plusieurs reprises, une fois pour chaque adresse IP, ce qui sera très lent si le fichier est volumineux. Les autres solutions utilisant uniq -cou awkne devant lire le fichier qu'une seule fois,
David

1
@David c'est vrai, mais cela aurait été mon premier essai aussi, sachant que grep compte. À moins que les performances ne soient un problème mesurable ... ne pas optimiser prématurément?
D. Ben Knoble

3
Je n'appellerais pas cela une optimisation prématurée, étant donné que la solution la plus efficace est également plus simple, mais à chacun la sienne.
David

Soit dit en passant, pourquoi est-il écrit comme <log grep ...et non grep ... log?
Santiago

@Santiago Parce que c'est mieux à bien des égards, comme l'explique Stéphane Chazelas ici sur U&L .
dessert le

39

Vous pouvez utiliser cutet uniqoutils:

cut -d ' ' -f1 test.txt  | uniq -c
      5 5.135.134.16
      9 13.57.220.172
      1 13.57.233.99
      2 18.206.226.75
      3 18.213.10.181

Explication:

  • cut -d ' ' -f1 : extraire le premier champ (adresse IP)
  • uniq -c : signaler les lignes répétées et afficher le nombre d'occurrences

6
On pourrait utiliser sed, par exemple, sed -E 's/ *(\S*) *(\S*)/\2 count: \1/'pour obtenir la sortie exactement comme OP le voulait.
dessert

2
Cela devrait être la réponse acceptée, car celle du dessert doit lire le fichier à plusieurs reprises, ce qui est beaucoup plus lent. Et vous pouvez facilement l'utiliser sort file | cut .... au cas où vous ne seriez pas sûr que le fichier est déjà trié.
Guntram Blohm soutient Monica

14

Si vous n'avez pas spécifiquement besoin du format de sortie donné, je recommanderais la réponse déjà publiée cut+ uniqbasée

Si vous avez vraiment besoin du format de sortie donné, une façon de le faire en un seul passage dans Awk serait

awk '{c[$1]++} END{for(i in c) print i, "count: " c[i]}' log

Ceci n'est pas idéal lorsque l'entrée est déjà triée car elle stocke inutilement toutes les adresses IP en mémoire - une meilleure façon, bien que plus compliquée, de le faire dans le cas pré-trié (plus directement équivalent à uniq -c) serait:

awk '
  NR==1 {last=$1} 
  $1 != last {print last, "count: " c[last]; last = $1} 
  {c[$1]++} 
  END {print last, "count: " c[last]}
'

Ex.

$ awk 'NR==1 {last=$1} $1 != last {print last, "count: " c[last]; last = $1} {c[$1]++} END{print last, "count: " c[last]}' log
5.135.134.16 count: 5
13.57.220.172 count: 9
13.57.233.99 count: 1
18.206.226.75 count: 2
18.213.10.181 count: 3

il serait facile de changer la réponse basée sur cut + uniq avec sed pour qu'elle apparaisse dans le format demandé.
Peter - Rétablir Monica

@ PeterA.Schneider oui, je le ferais - je crois que cela a déjà été souligné dans les commentaires de cette réponse
steeldriver

Ah, oui, je vois.
Peter - Réintègre Monica

8

Voici une solution possible:

IN_FILE="file.log"
for IP in $(awk '{print $1}' "$IN_FILE" | sort -u)
do
    echo -en "${IP}\tcount: "
    grep -c "$IP" "$IN_FILE"
done
  • remplacer file.logpar le nom de fichier réel.
  • l'expression de substitution de commande $(awk '{print $1}' "$IN_FILE" | sort -u)fournira une liste des valeurs uniques de la première colonne.
  • puis grep -ccomptera chacune de ces valeurs dans le fichier.

$ IN_FILE="file.log"; for IP in $(awk '{print $1}' "$IN_FILE" | sort -u); do echo -en "${IP}\tcount: "; grep -c "$IP" "$IN_FILE"; done
13.57.220.172   count: 9
13.57.233.99    count: 1
18.206.226.75   count: 2
18.213.10.181   count: 3
5.135.134.16    count: 5

1
Préférez printf...
D. Ben Knoble

1
Cela signifie que vous devez traiter l'intégralité du fichier plusieurs fois. Une fois pour obtenir la liste des adresses IP, puis une fois de plus pour chacune des adresses IP que vous trouvez.
terdon

5

Quelques Perl:

$ perl -lae '$k{$F[0]}++; }{ print "$_ count: $k{$_}" for keys(%k)' log 
13.57.233.99 count: 1
18.206.226.75 count: 2
13.57.220.172 count: 9
5.135.134.16 count: 5
18.213.10.181 count: 3

C'est la même idée que l'approche awk de Steeldriver , mais en Perl. Les -aPerl de diviser automatiquement chaque ligne d'entrée dans le réseau @F, dont le premier élément (IP) est $F[0]. Donc, $k{$F[0]}++va créer le hachage %k, dont les clés sont les IP et dont les valeurs sont le nombre de fois que chaque IP a été vue. Le }{perlspeak est génial pour "faire le reste à la toute fin, après avoir traité toutes les entrées". Ainsi, à la fin, le script itérera sur les clés du hachage et affichera la clé actuelle ( $_) ainsi que sa valeur ( $k{$_}).

Et, juste pour que les gens ne pensent pas que perl vous oblige à écrire un script qui ressemble à des gribouillages cryptiques, c'est la même chose sous une forme moins condensée:

perl -e '
  while (my $line=<STDIN>){
    @fields = split(/ /, $line);
    $ip = $fields[0];
    $counts{$ip}++;
  }
  foreach $ip (keys(%counts)){
    print "$ip count: $counts{$ip}\n"
  }' < log

4

Ce n'est peut-être pas ce que souhaite le PO; cependant, si nous savons que la longueur de l'adresse IP sera limitée à 15 caractères, un moyen plus rapide d'afficher les décomptes avec des adresses IP uniques à partir d'un énorme fichier journal peut être obtenu en utilisant la uniqcommande seule:

$ uniq -w 15 -c log

5 5.135.134.16 - - [23/Mar/2019:08:42:54 -0400] ...
9 13.57.220.172 - - [23/Mar/2019:11:01:05 -0400] ...
1 13.57.233.99 - - [23/Mar/2019:04:17:45 -0400] ...
2 18.206.226.75 - - [23/Mar/2019:21:58:07 -0400] ...
3 18.213.10.181 - - [23/Mar/2019:14:45:42 -0400] ...

Options:

-w Nne compare pas plus de Ncaractères que les lignes

-c préfixera les lignes par le nombre d'occurrences

Alternativement, pour une sortie formatée exacte, je préfère awk(devrait également fonctionner pour les adresses IPV6), ymmv.

$ awk 'NF { print $1 }' log | sort -h | uniq -c | awk '{printf "%s count: %d\n", $2,$1 }'

5.135.134.16 count: 5
13.57.220.172 count: 9
13.57.233.99 count: 1
18.206.226.75 count: 2
18.213.10.181 count: 3

Notez que uniqne détectera pas les lignes répétées dans le fichier d'entrée si elles ne sont pas adjacentes, il peut donc être nécessaire au sortfichier.


1
Probablement assez bon dans la pratique, mais il convient de noter les cas d'angle. Seulement 6 caractères probablement constants après l'IP `- - [`. Mais en théorie, l'adresse pourrait être jusqu'à 8 caractères plus courte que le maximum, donc un changement de date pourrait diviser le nombre pour une telle IP. Et comme vous l'indiquez, cela ne fonctionnera pas pour IPv6.
Martin Thornton

J'aime ça, je ne savais pas que uniq pouvait compter!
J0H

1

FWIW, Python 3:

from collections import Counter

with open('sample.log') as file:
    counts = Counter(line.split()[0] for line in file)

for ip_address, count in counts.items():
    print('%-15s  count: %d' % (ip_address, count))

Production:

13.57.233.99     count: 1
18.213.10.181    count: 3
5.135.134.16     count: 5
18.206.226.75    count: 2
13.57.220.172    count: 9

0
cut -f1 -d- my.log | sort | uniq -c

Explication: prenez le premier champ de fractionnement my.log sur les tirets -et triez-le. uniqnécessite une entrée triée. -clui dit de compter les occurrences.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.