Come contare le occorrenze di testo in un file?


19

Ho un file di registro ordinato per indirizzi IP, voglio trovare il numero di occorrenze di ciascun indirizzo IP univoco. Come posso farlo con bash? Possibilmente elencando il numero di occorrenze accanto a un IP, come ad esempio:

5.135.134.16 count: 5
13.57.220.172: count 30
18.206.226 count:2

e così via.

Ecco un esempio del registro:

5.135.134.16 - - [23/Mar/2019:08:42:54 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
5.135.134.16 - - [23/Mar/2019:08:42:55 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
5.135.134.16 - - [23/Mar/2019:08:42:55 -0400] "POST /wp-login.php HTTP/1.1" 200 3836 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
5.135.134.16 - - [23/Mar/2019:08:42:55 -0400] "POST /wp-login.php HTTP/1.1" 200 3988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
5.135.134.16 - - [23/Mar/2019:08:42:56 -0400] "POST /xmlrpc.php HTTP/1.1" 200 413 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:05 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:06 -0400] "POST /wp-login.php HTTP/1.1" 200 3985 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:07 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:08 -0400] "POST /wp-login.php HTTP/1.1" 200 3833 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:09 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:11 -0400] "POST /wp-login.php HTTP/1.1" 200 3836 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:12 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:15 -0400] "POST /wp-login.php HTTP/1.1" 200 3837 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.220.172 - - [23/Mar/2019:11:01:17 -0400] "POST /xmlrpc.php HTTP/1.1" 200 413 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
13.57.233.99 - - [23/Mar/2019:04:17:45 -0400] "GET / HTTP/1.1" 200 25160 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36"
18.206.226.75 - - [23/Mar/2019:21:58:07 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "https://www.google.com/url?3a622303df89920683e4421b2cf28977" "Mozilla/5.0 (Windows NT 6.2; rv:33.0) Gecko/20100101 Firefox/33.0"
18.206.226.75 - - [23/Mar/2019:21:58:07 -0400] "POST /wp-login.php HTTP/1.1" 200 3988 "https://www.google.com/url?3a622303df89920683e4421b2cf28977" "Mozilla/5.0 (Windows NT 6.2; rv:33.0) Gecko/20100101 Firefox/33.0"
18.213.10.181 - - [23/Mar/2019:14:45:42 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
18.213.10.181 - - [23/Mar/2019:14:45:42 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"
18.213.10.181 - - [23/Mar/2019:14:45:42 -0400] "GET /wp-login.php HTTP/1.1" 200 2988 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0"

1
Con "bash", intendi la shell semplice o la riga di comando in generale?
dessert

1
Hai qualche software di database disponibile da usare?
SpacePhoenix


Il registro proviene da un server appache2, non proprio da un database. bash è ciò che preferirei, in un caso d'uso generale. Vedo le soluzioni Python e Perl, se sono buone per qualcun altro, è fantastico. l'ordinamento iniziale è stato fatto sort -Vanche se penso che non fosse necessario. Ho inviato i primi 10 abusatori della pagina di accesso all'amministratore di sistema con consigli per vietare le rispettive sottoreti. ad esempio, un IP ha colpito la pagina di accesso oltre 9000 volte. quell'IP e la sua sottorete di classe D sono ora nella lista nera. Sono sicuro che potremmo automatizzare questo, sebbene questa sia una domanda diversa.
j0h

Risposte:


13

È possibile utilizzare grepe uniqper l'elenco di indirizzi, scorrere su di essi e grepancora per il conteggio:

for i in $(<log grep -o '^[^ ]*' | uniq); do
  printf '%s count %d\n' "$i" $(<log grep -c "$i")
done

grep -o '^[^ ]*'genera ogni carattere dall'inizio ( ^) fino al primo spazio di ogni riga, uniqrimuove le righe ripetute, lasciandoti così con un elenco di indirizzi IP. Grazie alla sostituzione dei comandi, il forloop scorre su questo elenco stampando l'IP attualmente elaborato seguito da "count" e il conteggio. Quest'ultimo è calcolato da grep -c, che conta il numero di righe con almeno una corrispondenza.

Esempio di esecuzione

$ for i in $(<log grep -o '^[^ ]*'|uniq);do printf '%s count %d\n' "$i" $(<log grep -c "$i");done
5.135.134.16 count 5
13.57.220.172 count 9
13.57.233.99 count 1
18.206.226.75 count 2
18.213.10.181 count 3

13
Questa soluzione scorre ripetutamente sul file di input, una volta per ciascun indirizzo IP, che sarà molto lento se il file è grande. Le altre soluzioni che usano uniq -co awkdevono solo leggere il file una volta,
David

1
@David questo è vero, ma questo sarebbe stato anche il mio primo tentativo, sapendo che il grep conta. A meno che le prestazioni non siano misurabili in modo misurabile ... non ottimizzare prematuramente?
D. Ben Knoble

3
Non la definirei un'ottimizzazione prematura, dato che la soluzione più efficiente è anche più semplice, ma a ciascuno la propria.
David

A proposito, perché è scritto come <log grep ...e non grep ... log?
Santiago,

@Santiago Perché è meglio sotto molti aspetti, come spiega Stéphane Chazelas qui su U&L .
dessert

39

È possibile utilizzare cute uniqstrumenti:

cut -d ' ' -f1 test.txt  | uniq -c
      5 5.135.134.16
      9 13.57.220.172
      1 13.57.233.99
      2 18.206.226.75
      3 18.213.10.181

Spiegazione :

  • cut -d ' ' -f1 : estrai primo campo (indirizzo ip)
  • uniq -c : riporta le righe ripetute e visualizza il numero di occorrenze

6
Si potrebbe usare sed, ad esempio, sed -E 's/ *(\S*) *(\S*)/\2 count: \1/'per ottenere l'output esattamente come desiderato da OP.
dessert

2
Questa dovrebbe essere la risposta accettata, poiché quella di dessert deve leggere il file ripetutamente, quindi è molto più lenta. E puoi facilmente usarlo sort file | cut .... nel caso in cui non sei sicuro che il file sia già ordinato.
Guntram Blohm supporta Monica il

14

Se non si richiede specificamente il formato di output specificato, si consiglia la risposta basata su + già pubblicatacutuniq

Se hai davvero bisogno del formato di output dato, sarebbe un modo single pass per farlo in Awk

awk '{c[$1]++} END{for(i in c) print i, "count: " c[i]}' log

Questo è in qualche modo non ideale quando l'input è già ordinato poiché memorizza inutilmente tutti gli IP in memoria - un modo migliore, sebbene più complicato, di farlo nel caso preordinato (più direttamente equivalente uniq -c) sarebbe:

awk '
  NR==1 {last=$1} 
  $1 != last {print last, "count: " c[last]; last = $1} 
  {c[$1]++} 
  END {print last, "count: " c[last]}
'

Ex.

$ awk 'NR==1 {last=$1} $1 != last {print last, "count: " c[last]; last = $1} {c[$1]++} END{print last, "count: " c[last]}' log
5.135.134.16 count: 5
13.57.220.172 count: 9
13.57.233.99 count: 1
18.206.226.75 count: 2
18.213.10.181 count: 3

sarebbe facile cambiare la risposta basata su cut + uniq con sed per apparire nel formato richiesto.
Peter - Ripristina Monica il

@ PeterA.Schneider sì - credo che fosse già stato sottolineato nei commenti a quella risposta
steeldriver

Ah, sì, capisco.
Peter - Ripristina Monica il

8

Ecco una possibile soluzione:

IN_FILE="file.log"
for IP in $(awk '{print $1}' "$IN_FILE" | sort -u)
do
    echo -en "${IP}\tcount: "
    grep -c "$IP" "$IN_FILE"
done
  • sostituire file.logcon il nome file effettivo.
  • l'espressione di sostituzione del comando $(awk '{print $1}' "$IN_FILE" | sort -u)fornirà un elenco dei valori univoci della prima colonna.
  • quindi grep -cconterà ciascuno di questi valori all'interno del file.

$ IN_FILE="file.log"; for IP in $(awk '{print $1}' "$IN_FILE" | sort -u); do echo -en "${IP}\tcount: "; grep -c "$IP" "$IN_FILE"; done
13.57.220.172   count: 9
13.57.233.99    count: 1
18.206.226.75   count: 2
18.213.10.181   count: 3
5.135.134.16    count: 5

1
Preferisco printf...
D. Ben Knoble

1
Ciò significa che è necessario elaborare l'intero file più volte. Una volta per ottenere l'elenco degli IP e poi ancora una volta per ciascuno degli IP che trovi.
terdon,

5

Qualche Perl:

$ perl -lae '$k{$F[0]}++; }{ print "$_ count: $k{$_}" for keys(%k)' log 
13.57.233.99 count: 1
18.206.226.75 count: 2
13.57.220.172 count: 9
5.135.134.16 count: 5
18.213.10.181 count: 3

Questa è la stessa idea dell'approccio imbarazzante di Steeldriver , ma in Perl. Le -acause perl dividono automaticamente ciascuna linea di input nell'array @F, il cui primo elemento (IP) è $F[0]. Quindi, $k{$F[0]}++creerà l'hash %k, le cui chiavi sono gli IP e i cui valori sono il numero di volte in cui ogni IP è stato visto. È }{perlspeak funky per "fare il resto alla fine, dopo aver elaborato tutti gli input". Quindi, alla fine, lo script ripeterà le chiavi dell'hash e stamperà la chiave corrente ( $_) insieme al suo valore ( $k{$_}).

E, proprio così la gente non pensa che Perl ti costringa a scrivere sceneggiature che sembrano scarabocchi criptici, questa è la stessa cosa in una forma meno condensata:

perl -e '
  while (my $line=<STDIN>){
    @fields = split(/ /, $line);
    $ip = $fields[0];
    $counts{$ip}++;
  }
  foreach $ip (keys(%counts)){
    print "$ip count: $counts{$ip}\n"
  }' < log

4

Forse questo non è ciò che l'OP vuole; tuttavia, se sappiamo che la lunghezza dell'indirizzo IP sarà limitata a 15 caratteri, è possibile ottenere un modo più rapido per visualizzare i conteggi con IP univoci da un enorme file di registro utilizzando uniqsolo il comando:

$ uniq -w 15 -c log

5 5.135.134.16 - - [23/Mar/2019:08:42:54 -0400] ...
9 13.57.220.172 - - [23/Mar/2019:11:01:05 -0400] ...
1 13.57.233.99 - - [23/Mar/2019:04:17:45 -0400] ...
2 18.206.226.75 - - [23/Mar/2019:21:58:07 -0400] ...
3 18.213.10.181 - - [23/Mar/2019:14:45:42 -0400] ...

Opzioni:

-w Nconfronta non più di Ncaratteri in righe

-c anteporrà le righe in base al numero di occorrenze

In alternativa, per l'output formattato esatto preferisco awk(dovrebbe funzionare anche per gli indirizzi IPV6), ymmv.

$ awk 'NF { print $1 }' log | sort -h | uniq -c | awk '{printf "%s count: %d\n", $2,$1 }'

5.135.134.16 count: 5
13.57.220.172 count: 9
13.57.233.99 count: 1
18.206.226.75 count: 2
18.213.10.181 count: 3

Si noti che uniqnon rileverà le righe ripetute nel file di input se non sono adiacenti, quindi potrebbe essere necessario per sortil file.


1
Probabilmente abbastanza buono in pratica, ma vale la pena notare i casi d'angolo. Solo 6 caratteri probabilmente costanti dopo l'IP `- - [`. Ma in teoria l'indirizzo potrebbe essere più breve di 8 caratteri rispetto al massimo, quindi un cambio di data potrebbe dividere il conteggio per tale IP. E come suggerisci, questo non funzionerà per IPv6.
Martin Thornton,

Mi piace, non sapevo che uniq potesse contare!
j0h

1

FWIW, Python 3:

from collections import Counter

with open('sample.log') as file:
    counts = Counter(line.split()[0] for line in file)

for ip_address, count in counts.items():
    print('%-15s  count: %d' % (ip_address, count))

Produzione:

13.57.233.99     count: 1
18.213.10.181    count: 3
5.135.134.16     count: 5
18.206.226.75    count: 2
13.57.220.172    count: 9

0
cut -f1 -d- my.log | sort | uniq -c

Spiegazione: prendere il primo campo di divisione my.log sui trattini -e ordinarlo. uniqnecessita di input ordinati. -cgli dice di contare le occorrenze.

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.