¿La mejor manera de simular "agrupar por" desde bash?

231

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á 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:

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

Me gustó la solución similar a una tabla hash: ¿alguien puede proporcionar mejoras a esa solución?

INFORMACIÓN ADICIONAL # 2:

Algunas personas preguntaron por qué me molestaría hacerlo en bash cuando es mucho más fácil, por ejemplo, en Perl. La razón es que en la máquina 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.

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

Zizzencs
fuente
Creo que bash es la herramienta incorrecta para el trabajo. Perl probablemente será una mejor solución.
Francois Wolmarans

Respuestas:

412
sort ip_addresses | uniq -c

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

Joachim Sauer
fuente
71
que luego puede canalizar a "sort -nr" para haber ordenado en orden descendente, de mayor a menor recuento. es decirsort ip_addresses | uniq -c | sort -nr
Brad Parks
15
Y sort ip_addresses | uniq -c | sort -nr | awk '{ print $2, $1 }'para obtener la dirección IP en la primera columna y contar en la segunda.
Raghu Dodda
un ajuste más para ordenar parte:sort -nr -k1,1
Andrzej Martyna
50

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.

PD

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

Francois Wolmarans
fuente
Es muy similar en
términos de
Significado cuadrático O (n ^ 2) ?? Eso dependería del algoritmo de clasificación seguramente, es poco probable que use un tipo bogo como ese.
paxdiablo
Bueno, en el mejor de los casos sería O (n log (n)), que es peor que dos pases (que es lo que obtienes con una implementación trivial basada en hash). Debería haber dicho 'superlineal' en lugar de cuadrático.
Vinko Vrsalovic
Y todavía está en el mismo límite que lo que el OP pidió para mejorar la eficiencia sabio ...
Vinko Vrsalovic
11
uuoc, uso inútil del gato
22

para resumir múltiples campos, en función de un grupo de campos existentes, use el siguiente ejemplo: (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
Anónimo
fuente
2
+1 porque muestra qué hacer cuando no solo se necesita el recuento
user829755
1
+1 porque sorty uniqson más fáciles para hacer recuentos, pero no ayudan cuando necesita calcular / sumar valores de campos. La sintaxis de matriz de awk es muy poderosa y clave para agrupar aquí. ¡Gracias!
odony
1
Una cosa más, tenga en cuenta que la printfunción de awk parece reducir los enteros de 64 bits a 32 bits, por lo que para valores int superiores a 2 ^ 31 es posible que desee usar printfcon el %.0fformato en lugar de printallí
odony
1
Las personas que buscan "agrupar por" con concatenación de cadenas en lugar de sumar números reemplazarían, arr[$1,$2]+=$3+$4por ejemplo, con arr[$1,$2]=(arr[$1,$2] $3 "," $4). I needed this to provide a grouped-by-package list of files (two columns only) and used: arr [$ 1] = (arr [$ 1] $ 2) `con éxito.
Stéphane Gourichon
20

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

sort | uniq -c

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

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 subestimes la calidad de implementación del comando de clasificación Unix. Sort se utilizó 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 la clasificación 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 fragmentos de datos. Finalmente, realiza una ordenación por fusión en esos archivos intermedios. Esto permite que la ordenación funcione en datos muchas veces más grandes que la memoria principal de la máquina.

Diomidis Spinellis
fuente
Bueno, todavía es peor que un recuento de hash, ¿no? ¿Sabe qué algoritmo de clasificación utiliza la clasificación si los datos se ajustan en la memoria? ¿Varía en el caso de datos numéricos (opción -n)?
Vinko Vrsalovic
Depende de cómo se implemente sort (1). Tanto la ordenación GNU (utilizada en distribuciones de Linux) como la ordenación BSD hacen todo lo posible para utilizar el algoritmo más apropiado.
Diomidis Spinellis el
9
cat ip_addresses | sort | uniq -c | sort -nr | awk '{print $2 " " $1}'

este comando le daría la salida deseada

zjor
fuente
4

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

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

Vinko Vrsalovic
fuente
Estoy de acuerdo. Esta es la mejor solución hasta ahora y son posibles soluciones similares en perl y awk. ¿Alguien puede proporcionar una implementación más limpia en bash?
Zizzencs
No que yo sepa. Puede obtener mejores implementaciones en idiomas que admiten hashes, donde lo hace para mi $ ip (@ips) {$ hash {$ ip} = $ hash {$ ip} + 1; } y luego simplemente imprima las claves y los valores.
Vinko Vrsalovic
4

Solución (agrupar por 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
kairouan2020
fuente
3

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 atravesar todos los archivos e imprimir los nombres y números de los archivos en ellos. Alternativamente, en lugar de llevar un recuento, puede agregar un espacio o una nueva línea cada vez al archivo y, al final, simplemente mirar el tamaño del archivo en bytes.

PolyThinker
fuente
3

Siento que una 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 post aquí

SriniV
fuente
Sí, una gran solución awk, pero awk simplemente no estaba disponible en la máquina en la que estaba haciendo esto.
Zizzencs
1

La mayoría de las otras soluciones cuentan duplicados. Si realmente necesita agrupar pares de valores clave, intente 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
Aron Curzon
fuente
1

Puro (¡sin tenedor!)

Hay una manera, usando un la función . ¡Este camino es muy rápido ya que no hay tenedor! ...

... ¡Mientras que las direcciones IP se mantienen 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 usa como índice para la matriz . ¡Esto usa matrices bash simples , no matrices asociativas (lo cual es más costoso)!

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 me tomará aproximadamente 1 segundo entero cuando intente ordenar y contar 10'000 direcciones.

F. Hauri
fuente
0

Lo hubiera hecho así:

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

pero uniq podría funcionar para ti.

nicerobot
fuente
Como dije en la publicación original, Perl no es una opción. Sé que es fácil en perl, no hay problema con eso :-)
Zizzencs
0

Entiendo que está buscando algo en Bash, pero en caso de que alguien más esté 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, puede ganar algo aquí. No he probado el código, por lo que podría tener errores, 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 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 IP únicas como claves y la cantidad de veces que ocurrieron como sus valores.

wzzrd
fuente
Esto no cuenta nada. necesitas un dict que lleva la cuenta.
Doh Mala lectura de la pregunta, lo siento. Originalmente tenía algo sobre el uso de un dict para almacenar la cantidad de veces que ocurría cada dirección IP, pero lo eliminé porque, bueno, no leí muy bien la pregunta. * intenta despertarse correctamente
wzzrd
2
Hay una itertools.groupby()que combinada con sorted()hace exactamente lo que OP pide.
jfs
Es una gran solución en python, que no estaba disponible para esto :-)
Zizzencs
-8

La ordenación puede omitirse si el orden no es significativo

uniq -c <source_file>

o

echo "$list" | uniq -c

si la lista fuente es una variable

Def repentina
fuente
1
Para aclarar más, desde la página de manual de uniq: Nota: 'uniq' no detecta líneas repetidas a menos que sean adyacentes. Es posible que desee ordenar primero la entrada o usar 'sort -u' sin 'uniq'.
convertidor42