La mejor forma de simular “agrupar por” 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 el siguiente resultado:

 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á muy lejos de ser eficiente.

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

(Una cosa para agregar: 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 5GB y la máquina que ejecuta el algoritmo tiene 4GB. Entonces, ordenar no es una solución eficiente, tampoco 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 N ° 2:

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

Entonces, por favor, no culpes a la pregunta, solo ignórala si no te gusta. 🙂

 sort ip_addresses | uniq -c 

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

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 el comando completo a una variable bash y luego recorrer los resultados.

PD

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

La solución canónica es la que menciona otro encuestado:

 sort | uniq -c 

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

Usted escribe que no desea utilizar la ordenación, porque el tamaño de los datos es mayor que el tamaño de la memoria principal de la máquina. No subestime la calidad de implementación del comando de clasificación Unix. Sort se usó para manejar grandes volúmenes 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 sintonizado cerca del tamaño de la memoria principal de la máquina) clasifica 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 fragmentos de datos. Finalmente, realiza una clasificación de fusión en esos archivos intermedios. Esto permite ordenar para trabajar en datos muchas veces más grandes que la memoria principal de la máquina.

para resumir varios campos, en función de un grupo de campos existentes, use el ejemplo siguiente: (reemplace $ 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 
 cat ip_addresses | sort | uniq -c | sort -nr | awk '{print $2 " " $1}' 

este comando le daría la salida deseada

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 cuadráticas y superlineales.

Entre esas versiones, la solución de saua es la mejor (y la 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 …

Probablemente pueda usar el sistema de archivos como una tabla hash. Pseudocódigo de la siguiente manera:

 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 los archivos en ellos. Alternativamente, en lugar de llevar un conteo, puede agregar un espacio o una línea nueva cada vez al archivo, y al final solo mirar el tamaño del archivo en bytes.

Solución (group by like mysql)

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

Resultado

 3249 googleplus 4211 linkedin 5212 xing 7928 facebook 

Me siento awk array asociativo 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í

Tengo entendido que estás buscando algo en Bash, pero en caso de que alguien más esté buscando algo en Python, es posible que quieras 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 esto, puedes ganar algo aquí. No probé el código, por lo que podría estar bloqueado, 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 lector pésimo, así que respondí mal. Aquí hay un fragmento con un dict 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 direcciones IP únicas como claves y la cantidad de veces que ocurrieron como sus valores.

Bash puro (sin tenedor!)

Hay una manera, usando una función bash . ¡De esta manera es muy rápido ya que no hay tenedor! …

… ¡Mientras que el montón de direcciones IP se quedan 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 en un valor entero sin signo de 32 bits, que se utiliza como índice para la matriz . Esto usa matrices bash simples, no una matriz asociativa (¡que es más cara)!

 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 horquillas, hasta aproximadamente 1 000 direcciones, pero tomar aproximadamente 1 segundo completo cuando intentaré ordenar 10 000 direcciones.

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.

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

Aquí está mi ejemplo de datos:

 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 sum 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 

Ordenar puede omitirse si el orden no es significativo

 uniq -c  

o

 echo "$list" | uniq -c 

si la lista fuente es una variable