La mejor manera de simular "grupo" de bash?


Supongamos que tiene un archivo que contiene direcciones IP, una dirección en cada línea:

10.0.10.1
10.0.10.1
10.0.10.3
10.0.10.2
10.0.10.1

Necesita un script de shell que cuente para cada dirección IP cuántas veces aparece en el archivo. Para la entrada anterior necesita la siguiente salida:

10.0.10.1 3
10.0.10.2 1
10.0.10.3 1

Una forma de hacer esto es:

cat ip_addresses |uniq |while read ip
do
    echo -n $ip" "
    grep -c $ip ip_addresses
done

Sin embargo, está realmente lejos de ser eficiente.

¿Cómo resolverías este problema de manera más eficiente usando bash?

(Una cosa a añadir: Sé que se puede resolver desde perl o awk, estoy interesado en una mejor solución en bash, no en esos idiomas.)

INFORMACIÓN ADICIONAL:

Supongamos que el archivo fuente es de 5 GB y la máquina que ejecuta el algoritmo tiene 4 GB. Así que sort no es una solución eficiente, tampoco lo es leer el archivo más de una vez.

Me gustó la solución tipo hashtable - ¿alguien puede proporcionar mejoras a esa solución?

INFORMACIÓN ADICIONAL #2:

Algunas personas me preguntaron por qué me molestaría en hacerlo en bash cuando es camino más fácil en, por ejemplo, perl. La razón es que en la máquina que tenía que hacer esto perl no estaba disponible para mí. Era una máquina Linux hecha a medida sin la mayoría de las herramientas a las que estoy acostumbrado. Y creo que fue un problema interesante.

Así que por favor, no culpes a la pregunta, solo ignórala si no te gusta. :-)

Author: Luke Girvin, 2008-12-19

14 answers

sort ip_addresses | uniq -c

Esto imprimirá el conteo primero, pero aparte de eso debería ser exactamente lo que quieres.

 330
Author: Joachim Sauer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-19 12:22:35

El método rápido y sucio es el siguiente:

cat ip_addresses | sort -n | uniq -c

Si necesita usar los valores en bash, puede asignar todo el comando a una variable bash y luego recorrer los resultados.

PS

Si se omite el comando sort, no obtendrá los resultados correctos, ya que uniq solo mira líneas idénticas sucesivas.

 41
Author: Francois Wolmarans,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-31 16:43:33

La solución canónica es la mencionada por otro encuestado:

sort | uniq -c

Es más corto y más conciso que lo que se puede escribir en Perl o awk.

Escribes que no quieres usar ordenación, porque el tamaño de los datos es mayor que el tamaño de la memoria principal de la máquina. No subestimes la calidad de implementación del comando Unix sort. Sort se utilizó para manejar volúmenes muy grandes de datos (piense en los datos de facturación originales de AT&T) en máquinas con 128k (eso es 131,072 bytes) de memoria (PDP-11). Cuando sort encuentra más datos que un límite preestablecido (a menudo ajustado cerca del tamaño de la memoria principal de la máquina) ordena los datos que ha leído en la memoria principal y los escribe en un archivo temporal. Luego repite la acción con los siguientes trozos de datos. Finalmente, realiza una ordenación combinada en esos archivos intermedios. Esto permite que sort trabaje con datos muchas veces más grandes que la memoria principal de la máquina.

 19
Author: Diomidis Spinellis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-20 16:02:55

Para resumir múltiples campos, basados en un grupo de campos existentes, use el siguiente ejemplo: (reemplace el $1, $2, $3, $4 según sus requisitos )

cat file

US|A|1000|2000
US|B|1000|2000
US|C|1000|2000
UK|1|1000|2000
UK|1|1000|2000
UK|1|1000|2000

awk 'BEGIN { FS=OFS=SUBSEP="|"}{arr[$1,$2]+=$3+$4 }END {for (i in arr) print i,arr[i]}' file

US|A|3000
US|B|3000
US|C|3000
UK|1|9000
 13
Author: Anonymous,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-03-31 09:51:28
cat ip_addresses | sort | uniq -c | sort -nr | awk '{print $2 " " $1}'

Este comando le dará la salida deseada

 7
Author: zjor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-25 22:28:45

Parece que tienes que usar una gran cantidad de código para simular hashes en bash para obtener un comportamiento lineal o apegarte a las versiones superlineales cuadráticas.

Entre esas versiones, la solución de saua es la mejor (y más simple):

sort -n ip_addresses.txt | uniq -c

Encontré http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-11/0118.html. Pero es feo como el infierno...

 4
Author: Vinko Vrsalovic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:37

Probablemente pueda usar el propio sistema de archivos como una tabla hash. Pseudo-código como sigue:

for every entry in the ip address file; do
  let addr denote the ip address;

  if file "addr" does not exist; then
    create file "addr";
    write a number "0" in the file;
  else 
    read the number from "addr";
    increase the number by 1 and write it back;
  fi
done

Al final, todo lo que necesita hacer es recorrer todos los archivos e imprimir los nombres y números de archivo en ellos. Alternativamente, en lugar de mantener un recuento, podría agregar un espacio o una nueva línea cada vez al archivo, y al final solo mirar el tamaño del archivo en bytes.

 3
Author: PolyThinker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-20 15:30:34

Solución (grupo por como mysql)

grep -ioh "facebook\|xing\|linkedin\|googleplus" access-log.txt | sort | uniq -c | sort -n

Resultado

3249  googleplus
4211 linkedin
5212 xing
7928 facebook
 2
Author: kairouan2020,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-14 09:08:38

Siento que la matriz asociativa awk también es útil en este caso

$ awk '{count[$1]++}END{for(j in count) print j,count[j]}' ips.txt

Un grupo por correo aquí

 2
Author: SriniV,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-03-31 09:51:06

Entiendo que está buscando algo en Bash, pero en caso de que alguien más pueda estar buscando algo en Python, es posible que desee considerar esto:

mySet = set()
for line in open("ip_address_file.txt"):
     line = line.rstrip()
     mySet.add(line)

Como los valores en el conjunto son únicos por defecto y Python es bastante bueno en estas cosas, podrías ganar algo aquí. No he probado el código, así que podría estar pinchado, pero esto podría llevarte allí. Y si desea contar las ocurrencias, usar un dict en lugar de un conjunto es fácil de implementar.

Editar: Soy un pésimo lector, así que respondí mal. Aquí hay un fragmento con un dictado que contaría las ocurrencias.

mydict = {}
for line in open("ip_address_file.txt"):
    line = line.rstrip()
    if line in mydict:
        mydict[line] += 1
    else:
        mydict[line] = 1

El diccionario mydict ahora contiene una lista de IP únicas como claves y la cantidad de veces que ocurrieron como sus valores.

 1
Author: wzzrd,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-20 17:17:17

Pure bash (¡sin tenedor!)

Hay una manera, usando un bash function . Este camino es muy rápido ya que no hay tenedor!...

... Mientras que un montón de direcciones ip permanecen pequeñas!

countIp () { 
    local -a _ips=(); local _a
    while IFS=. read -a _a ;do
        ((_ips[_a<<24|${_a[1]}<<16|${_a[2]}<<8|${_a[3]}]++))
    done
    for _a in ${!_ips[@]} ;do
        printf "%.16s %4d\n" \
          $(($_a>>24)).$(($_a>>16&255)).$(($_a>>8&255)).$(($_a&255)) ${_ips[_a]}
    done
}

Nota: Las direcciones IP se convierten a un valor entero sin signo de 32 bits, utilizado como índice para array. Este uso simple bash matrices, no matriz asociativa (que es más caro)!

time countIp < ip_addresses 
10.0.10.1    3
10.0.10.2    1
10.0.10.3    1
real    0m0.001s
user    0m0.004s
sys     0m0.000s

time sort ip_addresses | uniq -c
      3 10.0.10.1
      1 10.0.10.2
      1 10.0.10.3
real    0m0.010s
user    0m0.000s
sys     0m0.000s

En mi host, hacerlo es mucho más rápido que usar bifurcaciones, hasta aproximadamente 1'000 direcciones, pero toma aproximadamente 1 segundo entero cuando intentaré ordenar y contar 10'000 direcciones.

 1
Author: F. Hauri,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-18 12:38:08

Lo habría hecho así:

perl -e 'while (<>) {chop; $h{$_}++;} for $k (keys %h) {print "$k $h{$k}\n";}' ip_addresses

Pero uniq podría funcionar para usted.

 0
Author: nicerobot,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-20 00:30:51

La mayoría de las otras soluciones cuentan duplicados. Si realmente necesitas agrupar pares de valores clave, prueba esto:

Aquí están mis datos de ejemplo:

find . | xargs md5sum
fe4ab8e15432161f452e345ff30c68b0 a.txt
30c68b02161e15435ff52e34f4fe4ab8 b.txt
30c68b02161e15435ff52e34f4fe4ab8 c.txt
fe4ab8e15432161f452e345ff30c68b0 d.txt
fe4ab8e15432161f452e345ff30c68b0 e.txt

Esto imprimirá los pares de valores clave agrupados por la suma de comprobación md5.

cat table.txt | awk '{print $1}' | sort | uniq  | xargs -i grep {} table.txt
30c68b02161e15435ff52e34f4fe4ab8 b.txt
30c68b02161e15435ff52e34f4fe4ab8 c.txt
fe4ab8e15432161f452e345ff30c68b0 a.txt
fe4ab8e15432161f452e345ff30c68b0 d.txt
fe4ab8e15432161f452e345ff30c68b0 e.txt
 0
Author: Aron Curzon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-11 21:02:38

Sort puede omitirse si el orden no es significativo

uniq -c <source_file>

O

echo "$list" | uniq -c

Si la lista fuente es una variable

 -7
Author: Sudden Def,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-19 12:28:01