Jak policzyć wystąpienia tekstu w pliku?

19

Mam plik dziennika posortowany według adresów IP, chcę znaleźć liczbę wystąpień każdego unikalnego adresu IP. Jak mogę to zrobić za pomocą bash? Prawdopodobnie podaje liczbę wystąpień obok adresu IP, takie jak:

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

i tak dalej.

Oto próbka dziennika:

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"
j0h
źródło
1
Czy przez „bash” masz na myśli zwykłą powłokę czy ogólnie wiersz poleceń?
deser
1
Czy masz dostępne oprogramowanie bazy danych?
SpacePhoenix
1
Powiązane
Julien Lopez
Dziennik pochodzi z serwera Appache2, a nie z bazy danych. bash jest tym, co wolałbym, w ogólnym przypadku użycia. Widzę rozwiązania Python i Perl, jeśli są dobre dla kogoś innego, to świetnie. wstępne sortowanie zostało wykonane, sort -Vchoć myślę, że nie było to wymagane. Wysłałem 10 najlepszych osób nadużywających strony logowania do administratora systemu z zaleceniami dotyczącymi banowania odpowiednich podsieci. na przykład One IP trafił na stronę logowania ponad 9000 razy. ten adres IP i jego podsieć klasy D są teraz na czarnej liście. Jestem pewien, że możemy to zautomatyzować, ale to inne pytanie.
j0h

Odpowiedzi:

13

Możesz użyć grepi uniqdo listy adresów, zapętlić je i grepponownie dla liczby:

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

grep -o '^[^ ]*'wypisuje każdy znak od początku ( ^) do pierwszej spacji każdego wiersza, uniqusuwa powtarzające się wiersze, pozostawiając w ten sposób listę adresów IP. Dzięki podstawieniu poleceń forpętla zapętla tę listę, drukując aktualnie przetwarzany adres IP, a następnie „count” i count. Ten ostatni jest obliczany przez grep -c, który zlicza liczbę linii z co najmniej jednym dopasowaniem.

Przykładowy przebieg

$ 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
deser
źródło
13
To rozwiązanie powtarza wielokrotnie plik wejściowy, jeden raz dla każdego adresu IP, co będzie bardzo wolne, jeśli plik jest duży. Inne rozwiązania wykorzystujące plik uniq -club awkczytające go tylko raz,
David
1
@David to prawda, ale to byłby mój pierwszy krok, wiedząc, że grep się liczy. Chyba że wydajność jest mierzalnym problemem ... nie przedwcześnie optymalizować?
D. Ben Knoble
3
Nie nazwałbym tego przedwczesną optymalizacją, biorąc pod uwagę, że bardziej wydajne rozwiązanie jest również prostsze, ale dla każdego z nich.
David
Nawiasem mówiąc, dlaczego jest napisane jako <log grep ...i nie grep ... log?
Santiago
@Santiago Ponieważ jest to lepsze pod wieloma względami, jak wyjaśnia Stéphane Chazelas w U&L .
deser
39

Możesz użyć cuti uniqnarzędzi:

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

Objaśnienie:

  • cut -d ' ' -f1 : wyodrębnij pierwsze pole (adres IP)
  • uniq -c : zgłoś powtarzające się wiersze i wyświetl liczbę wystąpień
Mikael Flora
źródło
6
Można użyć sednp., sed -E 's/ *(\S*) *(\S*)/\2 count: \1/'Aby uzyskać wynik dokładnie taki, jaki chciał OP.
deser
2
To powinna być zaakceptowana odpowiedź, ponieważ ta z deseru musi wielokrotnie czytać plik, więc jest znacznie wolniejsza. I możesz łatwo użyć, sort file | cut .... jeśli nie masz pewności, czy plik jest już posortowany.
Guntram Blohm wspiera Monikę
14

Jeśli nie potrzebujesz konkretnego formatu wyjściowego, polecam już wysłaną odpowiedź opartą na cut+uniq

Jeśli naprawdę potrzebujesz podanego formatu wyjściowego, możesz to zrobić w Awk za pomocą jednego przejścia

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

Jest to nieco nieidealne, gdy dane wejściowe są już posortowane, ponieważ niepotrzebnie przechowuje wszystkie adresy IP w pamięci - lepszym, choć bardziej skomplikowanym, sposobem na zrobienie tego w przypadku wstępnie posortowanego (bardziej bezpośrednio równoważnego uniq -c) byłoby:

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

Dawny.

$ 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
steeldriver
źródło
łatwo byłoby zmienić odpowiedź opartą na cut + uniq przy pomocy sed, aby pojawiła się w żądanym formacie.
Peter - przywróć Monikę
@ PeterA.Schneider tak, tak - uważam, że zostało to już wskazane w komentarzach do tej odpowiedzi
steeldriver
Ach tak, rozumiem.
Peter - Przywróć Monikę
8

Oto jedno z możliwych rozwiązań:

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
  • zastąp file.logrzeczywistą nazwą pliku.
  • wyrażenie podstawiania poleceń $(awk '{print $1}' "$IN_FILE" | sort -u)zapewni listę unikatowych wartości pierwszej kolumny.
  • następnie grep -cpoliczy każdą z tych wartości w pliku.

$ 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
pa4080
źródło
1
Wolę printf...
D. Ben Knoble
1
Oznacza to, że musisz przetworzyć cały plik wiele razy. Raz, aby uzyskać listę adresów IP, a następnie jeszcze raz dla każdego adresu IP, który znajdziesz.
terdon
5

Some 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

To ten sam pomysł, co podejście awk Steeldrivera , ale w Perlu. Do -aprzyczyn Perl automatycznie podzielić każdą linię wejścia do tablicy @F, której pierwszy element (IP) to $F[0]. Utworzy więc $k{$F[0]}++skrót %k, którego kluczami są adresy IP, a których wartości to liczba wyświetleń każdego adresu IP. Jest }{to funky perlspeak dla „wykonaj resztę na samym końcu, po przetworzeniu wszystkich danych wejściowych”. Na koniec skrypt będzie iterował klucze skrótu i ​​wypisał bieżący klucz ( $_) wraz z jego wartością ( $k{$_}).

I tak, żeby ludzie nie myśleli, że Perl zmusza cię do napisania skryptu, który wygląda jak tajemnicze bazgroły, jest to to samo w mniej skondensowanej formie:

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
terdon
źródło
4

Może nie tego chce OP; Jeśli jednak wiemy, że długość adresu IP będzie ograniczona do 15 znaków, szybszy sposób wyświetlania liczby unikatowych adresów IP z dużego pliku dziennika można uzyskać za pomocą uniqsamej komendy:

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

Opcje:

-w Nporównuje nie więcej niż Nznaki w wierszach

-c będzie poprzedzać wiersze liczbą wystąpień

Alternatywnie, dla dokładnego sformatowania danych wyjściowych wolę awk(powinien również działać dla adresów 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

Zauważ, że uniqnie wykryje powtarzających się linii w pliku wejściowym, jeśli nie są one sąsiadujące, więc może być konieczne do sortpliku.

Y. Pradhan
źródło
1
Prawdopodobnie wystarczająco dobry w praktyce, ale warty odnotowania w narożnikach. Tylko 6 prawdopodobnie stałych znaków po IP `- - [`. Ale teoretycznie adres może być maksymalnie 8 znaków krótszy niż maksimum, więc zmiana daty może podzielić liczbę takich adresów IP. Jak sugerujesz, nie będzie to działać w przypadku IPv6.
Martin Thornton
Podoba mi się, nie wiedziałem, że Uniq może liczyć!
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))

Wynik:

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
wjandrea
źródło
0
cut -f1 -d- my.log | sort | uniq -c

Objaśnienie: Weź pierwsze pole podziału my.log na myślniki -i posortuj je. uniqwymaga posortowanego wejścia. -ckaże policzyć zdarzenia.

Doktorat
źródło