Il modo migliore per simulare "raggruppa per" da bash?


231

Supponiamo di avere un file che contiene indirizzi IP, un indirizzo in ciascuna 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 conti per ciascun 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 lungi dall'essere efficiente.

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

(Una cosa da aggiungere: so che può essere risolto da perl o awk, sono interessato a 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 l'ordinamento non è una soluzione efficiente, né legge il file più di una volta.

Mi è piaciuta la soluzione simile a hashtable: chiunque può fornire miglioramenti a tale soluzione?

INFORMAZIONI SUPPLEMENTARI # 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 su misura senza la maggior parte degli strumenti a cui sono abituato. E penso che sia stato un problema interessante.

Quindi, per favore, non dare la colpa alla domanda, ignorala se non ti piace. :-)


Penso che bash sia lo strumento sbagliato per il lavoro. Perl sarà probabilmente una soluzione migliore.
Francois Wolmarans,

Risposte:


412
sort ip_addresses | uniq -c

Questo stamperà prima il conteggio, ma a parte questo dovrebbe essere esattamente quello che vuoi.


71
che puoi quindi reindirizzare a "sort -nr" per ordinarli in ordine decrescente, dal conteggio più alto a quello più basso. vale a diresort ip_addresses | uniq -c | sort -nr
Brad Parks

15
E sort ip_addresses | uniq -c | sort -nr | awk '{ print $2, $1 }'per ottenere l'indirizzo IP nella prima colonna e contare nella seconda.
Raghu Dodda,

ancora una modifica per la parte sort -nr -k1,1
ordinata

50

Il metodo rapido e sporco è il seguente:

cat ip_addresses | sort -n | uniq -c

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

PS

Se il comando di ordinamento viene omesso, non si otterranno i risultati corretti poiché uniq esamina solo le linee identiche successive.


È molto simile dal punto di vista dell'efficienza, hai ancora un comportamento quadratico
Vinko Vrsalovic,

Quadratico che significa O (n ^ 2) ?? Dipenderebbe sicuramente dall'algoritmo di ordinamento, è improbabile che usi un bogo-sort come quello.
paxdiablo,

Bene, nel migliore dei casi sarebbe O (n log (n)), che è peggio di due passaggi (che è quello che ottieni con una banale implementazione basata su hash). Avrei dovuto dire "superlineare" anziché quadratico.
Vinko Vrsalovic,

Ed è ancora nello stesso limite che ciò che l'OP ha chiesto di migliorare l'efficienza in termini di ...
Vinko Vrsalovic,

11
uuoc, uso inutile di cat

22

per riassumere più campi, in base a un gruppo di campi esistenti, utilizzare l'esempio seguente: (sostituire $ 1, $ 2, $ 3, $ 4 in base alle proprie 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

2
+1 perché mostra cosa fare quando non è necessario solo il conteggio
user829755

1
+1 perché sorte uniqsono i più facili da eseguire conteggi, ma non aiutano quando è necessario calcolare / sommare i valori dei campi. La sintassi dell'array di awk è molto potente e la chiave per raggruppare qui. Grazie!
odony

1
un'altra cosa, attenzione che la printfunzione di awk sembra ridimensionare 64 bit interi a 32 bit, quindi per valori int superiori a 2 ^ 31 potresti voler usare printfcon il %.0fformato invece di print
odony

1
Le persone che cercano "raggruppa per" con concatenazione di stringhe anziché aggiunta di numeri sostituiranno arr[$1,$2]+=$3+$4con es. 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 successo.
Stéphane Gourichon,

20

La soluzione canonica è quella citata da un altro intervistato:

sort | uniq -c

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

Scrivi che non vuoi usare l'ordinamento, perché la dimensione dei dati è maggiore della dimensione della memoria principale della macchina. Non sottovalutare la qualità dell'implementazione del comando di ordinamento Unix. L'ordinamento è 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 l'ordinamento incontra più dati di un limite preimpostato (spesso regolato vicino alla dimensione della memoria principale della macchina) ordina i dati che ha letto nella memoria principale e li scrive in un file temporaneo. Quindi ripete l'azione con i successivi blocchi di dati. Infine, esegue un ordinamento di unione su quei file intermedi. Ciò consente all'ordinamento di lavorare su dati molte volte più grandi della memoria principale della macchina.


Bene, è ancora peggio di un conteggio di hash, no? Sai quale algoritmo di ordinamento utilizza l'ordinamento se i dati si adattano alla memoria? Varia nel caso di dati numerici (opzione -n)?
Vinko Vrsalovic,

Dipende da come viene implementato l'ordinamento (1). Sia l'ordinamento GNU (utilizzato su distribuzioni Linux) sia l'ordinamento BSD fanno di tutto per utilizzare l'algoritmo più appropriato.
Diomidis Spinellis,

9
cat ip_addresses | sort | uniq -c | sort -nr | awk '{print $2 " " $1}'

questo comando ti darebbe l'output desiderato


4

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

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


Sono d'accordo. Questa è la soluzione migliore finora e soluzioni simili sono possibili in perl e awk. Qualcuno può fornire un'implementazione più pulita in bash?
Zizzencs,

Non che io sappia. Puoi ottenere implementazioni migliori nelle lingue che supportano gli hash, dove lo fai per il mio $ ip (@ips) {$ hash {$ ip} = $ hash {$ ip} + 1; } e quindi stampa le chiavi e i valori.
Vinko Vrsalovic,

4

Soluzione (raggruppa per like mysql)

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

Risultato

3249  googleplus
4211 linkedin
5212 xing
7928 facebook

3

Probabilmente puoi usare il file system stesso come una 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 in essi contenuti. In alternativa, invece di mantenere un conteggio, è possibile aggiungere uno spazio o una nuova riga ogni volta al file, e alla fine basta guardare la dimensione del file in byte.


3

Penso che anche in questo caso sia utile un array associativo

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

Un gruppo per posta qui


Sì, ottima soluzione Awk, ma Awk non era disponibile sulla macchina su cui stavo facendo questo.
Zizzencs,

1

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

1

Puro (niente forchetta!)

C'è un modo, usando a la funzione . In questo modo è molto veloce in quanto non c'è forcella! ...

... Mentre un sacco di indirizzi IP rimangono piccoli !

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 un valore intero senza segno a 32 bit, utilizzato come indice per l' array . Questo usa semplici array bash , non array associativi (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ù veloce dell'uso delle forcelle, fino a circa 1'000 indirizzi, ma impiegherò circa 1 secondo intero quando proverò a ordinare 10'000 indirizzi.


0

Lo avrei fatto così:

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

ma uniq potrebbe funzionare per te.


Come ho detto nel post originale, il perl non è un'opzione. So che è facile in perl, nessun problema con questo :-)
Zizzencs,

0

Capisco che stai cercando qualcosa in Bash, ma nel caso in cui qualcun altro stia cercando qualcosa in Python, potresti voler considerare 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 è abbastanza bravo in queste cose, potresti vincere qualcosa qui. Non ho testato il codice, quindi potrebbe essere stato corretto, ma questo potrebbe portarti lì. E se vuoi contare le occorrenze, usare un dict invece di un set è facile da implementare.

Modifica: sono un lettore schifoso, quindi ho risposto male. Ecco uno snippet con un dict che conta 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.


questo non conta nulla. hai bisogno di un dict che mantenga il punteggio.

Doh. Cattiva lettura della domanda, scusa. Inizialmente avevo qualcosa in più sull'uso di un dict per memorizzare il numero di volte in cui si è verificato ciascun indirizzo IP, ma l'ho rimosso, perché, beh, non ho letto molto bene la domanda. * prova a svegliarsi correttamente
wzzrd

2
C'è un itertools.groupby()che combinato con sorted()fa esattamente ciò che chiede OP.
jfs,

È un'ottima soluzione in Python, che non era disponibile per questo :-)
Zizzencs,

-8

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

uniq -c <source_file>

o

echo "$list" | uniq -c

se l'elenco delle fonti è una variabile


1
Per chiarire ulteriormente, dalla pagina man uniq: Nota: 'uniq' non rileva le linee ripetute a meno che non siano adiacenti. Potresti prima ordinare l'input o usare 'sort -u' senza 'uniq'.
converter42
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.