¿Cómo contar las apariciones de texto en un archivo?


19

Tengo un archivo de registro ordenado por direcciones IP, quiero encontrar el número de ocurrencias de cada dirección IP única. ¿Cómo puedo hacer esto con bash? Posiblemente enumere el número de ocurrencias junto a una ip, como:

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

y así.

Aquí hay una muestra del registro:

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
Con "bash", ¿te refieres al shell simple o la línea de comando en general?
postre

1
¿Tiene algún software de base de datos disponible para usar?
SpacePhoenix


El registro es de un servidor appache2, no realmente una base de datos. bash es lo que preferiría, en un caso de uso general. Veo las soluciones de Python y Perl, si son buenas para otra persona, eso es genial. la clasificación inicial se realizó sort -Vaunque creo que no fue necesario. Envié los 10 principales abusadores de la página de inicio de sesión al administrador del sistema con recomendaciones para prohibir las subredes respectivas. por ejemplo, una IP golpeó la página de inicio de sesión más de 9000 veces. esa IP, y su subred de clase D ahora está en la lista negra. Estoy seguro de que podríamos automatizar esto, aunque esa es una pregunta diferente.
j0h

Respuestas:


13

Puede usar grepy uniqpara la lista de direcciones, recorrerlas y grepnuevamente para el recuento:

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

grep -o '^[^ ]*'genera cada carácter desde el principio ( ^) hasta el primer espacio de cada línea, uniqelimina las líneas repetidas y le deja una lista de direcciones IP. Gracias a la sustitución de comandos, el forbucle recorre esta lista imprimiendo la IP procesada actualmente seguida de "conteo" y el conteo. Este último se calcula por grep -c, que cuenta el número de líneas con al menos una coincidencia.

Ejecución de ejemplo

$ 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
Esta solución itera sobre el archivo de entrada repetidamente, una vez para cada dirección IP, lo que será muy lento si el archivo es grande. Las otras soluciones que usan uniq -co awksolo necesitan leer el archivo una vez,
David

1
@David, esto es cierto, pero también habría sido mi primer intento, sabiendo que grep cuenta. A menos que el rendimiento sea un problema medible ... ¿no se optimiza prematuramente?
D. Ben Knoble

3
No lo llamaría una optimización prematura, dado que la solución más eficiente también es más simple, pero para cada uno la suya.
David

Por cierto, ¿por qué está escrito como <log grep ...y no grep ... log?
Santiago

@Santiago Porque eso es mejor en muchos sentidos, como Stéphane Chazelas explica aquí en U&L .
postre

39

Puedes usar cuty uniqherramientas:

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

Explicacion:

  • cut -d ' ' -f1 : extraer el primer campo (dirección IP)
  • uniq -c : informa líneas repetidas y muestra el número de ocurrencias

66
Se podría usar sed, por ejemplo, sed -E 's/ *(\S*) *(\S*)/\2 count: \1/'para obtener la salida exactamente como OP quería.
postre

2
Esta debería ser la respuesta aceptada, ya que la de postre necesita leer el archivo repetidamente, por lo que es mucho más lenta. Y puede usarlo fácilmente sort file | cut .... en caso de que no esté seguro de si el archivo ya está ordenado.
Guntram Blohm apoya a Monica el

14

Si no requiere específicamente el formato de salida dado, recomendaría la respuesta basada en + ya publicadacutuniq

Si realmente necesita el formato de salida dado, una forma de paso único para hacerlo en Awk sería

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

Esto es algo no ideal cuando la entrada ya está ordenada, ya que almacena innecesariamente todas las IP en la memoria; una forma mejor, aunque más complicada, de hacerlo en el caso previamente ordenado (más directamente equivalente a uniq -c) sería:

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

sería fácil cambiar la respuesta basada en cortar + uniq con sed para que aparezca en el formato solicitado.
Peter - Restablece a Monica el

@ PeterA.Schneider sí lo haría, creo que eso ya se señaló en los comentarios a esa respuesta
steeldriver


8

Aquí hay una posible solución:

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
  • reemplazar file.logcon el nombre real del archivo.
  • La expresión de sustitución de comando $(awk '{print $1}' "$IN_FILE" | sort -u)proporcionará una lista de los valores únicos de la primera columna.
  • luego grep -ccontará cada uno de estos valores dentro del archivo.

$ 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
Prefiero printf...
D. Ben Knoble

1
Esto significa que debe procesar todo el archivo varias veces. Una vez para obtener la lista de IP y luego una vez más para cada una de las IP que encuentre.
Terdon

5

Algunos 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

Esta es la misma idea que el enfoque awk de Steeldriver , pero en Perl. Las -acausas perl para dividir automáticamente cada línea de entrada en la matriz @F, cuyo primer elemento (la IP) es $F[0]. Entonces, $k{$F[0]}++creará el hash %k, cuyas claves son las IP y cuyos valores son la cantidad de veces que se vio cada IP. El }{es funky perlspeak para "hacer el resto al final, después de procesar todas las entradas". Entonces, al final, el script iterará sobre las claves del hash e imprimirá la clave actual ( $_) junto con su valor ( $k{$_}).

Y, para que la gente no piense que Perl te obliga a escribir guiones que parecen garabatos crípticos, esto es lo mismo en una forma menos condensada:

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

Quizás esto no sea lo que quiere el OP; sin embargo, si sabemos que la longitud de la dirección IP se limitará a 15 caracteres, se puede lograr una forma más rápida de mostrar los recuentos con direcciones IP únicas de un archivo de registro enorme utilizando uniqsolo el comando:

$ 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] ...

Opciones:

-w Nno compara más que Ncaracteres en líneas

-c prefijará líneas por el número de ocurrencias

Alternativamente, para la salida con formato exacto que prefiero awk(también debería funcionar para direcciones 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

Tenga en cuenta que uniqno detectará líneas repetidas en el archivo de entrada si no son adyacentes, por lo que puede ser necesario para sortel archivo.


1
Probablemente lo suficientemente bueno en la práctica, pero vale la pena señalar los casos de esquina. Probablemente solo 6 caracteres constantes después de la IP `- - [`. Pero, en teoría, la dirección podría tener hasta 8 caracteres menos que el máximo, por lo que un cambio de fecha podría dividir el recuento de dicha IP. Y como insinúa, esto no funcionará para IPv6.
Martin Thornton

Me gusta, ¡no sabía que uniq podía contar!
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))

Salida:

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

Explicación: Tome el primer campo de división my.log en guiones -y ordénelo . uniqnecesita entrada ordenada. -cle dice que cuente las ocurrencias.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.