Il modo migliore per simulare “group by” da bash?

Supponiamo di avere un file che contiene indirizzi IP, un indirizzo in ogni riga:

10.0.10.1 10.0.10.1 10.0.10.3 10.0.10.2 10.0.10.1 

È necessario uno script di shell che conta per ogni indirizzo IP quante volte appare nel file. Per l’input precedente è necessario il seguente output:

 10.0.10.1 3 10.0.10.2 1 10.0.10.3 1 

Un modo per farlo è:

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

Tuttavia è davvero lontano dall’essere efficiente.

Come risolverebbe questo problema in modo più efficiente usando bash?

(Una cosa da aggiungere: so che può essere risolta da perl o awk, mi interessa una soluzione migliore in bash, non in quelle lingue.)

INFORMAZIONI ADDIZIONALI:

Supponiamo che il file sorgente sia 5 GB e che la macchina che esegue l’algoritmo abbia 4 GB. Quindi ordinare non è una soluzione efficiente, né sta leggendo il file più di una volta.

Mi è piaciuta la soluzione tipo hashtable: chiunque può apportare miglioramenti a tale soluzione?

INFORMAZIONI AGGIUNTIVE # 2:

Alcune persone hanno chiesto perché dovrei preoccuparmi di farlo in bash quando è molto più facile, ad esempio in Perl. Il motivo è che sulla macchina ho dovuto fare questo perl non era disponibile per me. Era una macchina Linux costruita su misura senza la maggior parte degli strumenti a cui ero abituato. E penso che sia stato un problema interessante.

Quindi, per favore, non incolpare la domanda, ignorala se non ti piace. 🙂

 sort ip_addresses | uniq -c 

Questo stamperà il conto per primo, ma a parte questo dovrebbe essere esattamente quello che vuoi.

Il metodo rapido e sporco è il seguente:

cat ip_addresses | sort -n | uniq -c

Se è necessario utilizzare i valori in bash, è ansible assegnare l’intero comando a una variabile bash e quindi scorrere i risultati.

PS

Se il comando sort è omesso, non otterrai i risultati corretti poiché uniq guarda solo linee identiche successive.

La soluzione canonica è quella menzionata da un altro rispondente:

 sort | uniq -c 

È più breve e più conciso di ciò che può essere scritto in Perl o awk.

Si scrive che non si desidera utilizzare l’ordinamento, poiché la dimensione dei dati è maggiore della dimensione della memoria principale della macchina. Non sottovalutare la qualità di implementazione del comando di ordinamento Unix. Ordina è stato utilizzato per gestire grandi volumi di dati (si pensi ai dati di fatturazione originali di AT & T) su macchine con 128k (ovvero 131.072 byte) di memoria (PDP-11). Quando sort incontra più dati di un limite preimpostato (spesso sintonizzato vicino alla dimensione della memoria principale della macchina), ordina i dati letti nella memoria principale e li scrive in un file temporaneo. Ripete quindi l’azione con i prossimi blocchi di dati. Infine, esegue un ordinamento di unione su quei file intermedi. Ciò consente a sort di lavorare su dati molto più grandi della memoria principale della macchina.

per sumre più campi, in base a un gruppo di campi esistenti, utilizza l’esempio seguente: (sostituisci $ 1, $ 2, $ 3, $ 4 in base alle tue esigenze)

 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}' 

questo comando ti darebbe l’output desiderato

Sembra che sia necessario utilizzare una grande quantità di codice per simulare gli hash in bash per ottenere un comportamento lineare o attenersi alle versioni quadrilatere superlineari.

Tra quelle versioni, la soluzione di saua è la migliore (e la più semplice):

 sort -n ip_addresses.txt | uniq -c 

Ho trovato http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-11/0118.html . Ma è brutto da morire …

Probabilmente puoi usare il file system stesso come tabella hash. Pseudo-codice come segue:

 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 

Alla fine, tutto ciò che devi fare è attraversare tutti i file e stampare i nomi e i numeri dei file al loro interno. In alternativa, invece di tenere un conteggio, è ansible aggiungere uno spazio o una nuova riga ogni volta al file e, alla fine, osservare le dimensioni del file in byte.

Soluzione (raggruppa come miqql)

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

Risultato

 3249 googleplus 4211 linkedin 5212 xing 7928 facebook 

In questo caso, mi sento utile anche l’array associativo awk

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

Un gruppo per posta qui

Capisco che stai cercando qualcosa in Bash, ma nel caso in cui qualcun altro potrebbe cercare qualcosa in Python, potresti prendere in considerazione questo:

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

Dato che i valori nel set sono unici per impostazione predefinita e Python è piuttosto bravo in questa roba, potresti vincere qualcosa qui. Non ho testato il codice, quindi potrebbe esserci un bug, ma questo potrebbe portarti lì. E se vuoi contare le occorrenze, usare un dict invece di un set è facile da implementare.

Edit: Sono un pessimo lettore, quindi ho risposto male. Ecco uno snippet con un dettato che conterrebbe le occorrenze.

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

Il dizionario mydict ora contiene un elenco di IP univoci come chiavi e il numero di volte in cui si sono verificati come valori.

Pure bash (niente forchetta!)

C’è un modo, usando una funzione bash . In questo modo è molto veloce in quanto non vi è alcuna fork! …

… Mentre il mazzo di indirizzi IP rimane piccolo !

 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: gli indirizzi IP vengono convertiti in valori interi senza segno a 32 bit, usati come indice per array . Questo usa semplici array bash , non un array associativo (che è più 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 

Sul mio host, farlo è molto più rapido dell'utilizzo di fork, fino a circa 1000 indirizzi, ma impiega circa 1 secondo intero quando cercherò di ordinare 10.000 indirizzi.

L’avrei fatto in questo modo:

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

ma uniq potrebbe funzionare per te.

La maggior parte delle altre soluzioni conta duplicati. Se hai davvero bisogno di raggruppare coppie di valori chiave, prova questo:

Ecco i miei dati di esempio:

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

Questo stamperà le coppie di valori chiave raggruppate per il checksum 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 

L’ordinamento può essere omesso se l’ordine non è significativo

 uniq -c  

o

 echo "$list" | uniq -c 

se la lista dei sorgenti è una variabile