Como contar ocorrências de texto em um arquivo?

19

Eu tenho um arquivo de log classificado por endereços IP, quero encontrar o número de ocorrências de cada endereço IP exclusivo. Como posso fazer isso com o bash? Possivelmente listando o número de ocorrências ao lado de um ip, como:

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

e assim por diante.

Aqui está uma amostra do log:

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
fonte
1
Com "bash", você quer dizer o shell simples ou a linha de comando em geral?
dessert
1
Você tem algum software de banco de dados disponível para uso?
SpacePhoenix em 29/03
1
Related
Julien Lopez
O log é de um servidor appache2, não realmente um banco de dados. bash é o que eu preferiria, em um caso de uso geral. Vejo as soluções python e perl, se elas são boas para outra pessoa, isso é ótimo. a classificação inicial foi feita, sort -Vembora eu ache que isso não foi necessário. Enviei os 10 principais abusadores da página de login ao administrador do sistema com recomendações para banir as respectivas sub-redes. por exemplo, um IP atingiu a página de login mais de 9000 vezes. esse IP e sua sub-rede classe D agora estão na lista negra. Tenho certeza de que poderíamos automatizar isso, embora essa seja uma pergunta diferente.
j0h 31/03

Respostas:

13

Você pode usar grepe, uniqpara a lista de endereços, fazer um loop sobre eles e grepnovamente para a contagem:

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

grep -o '^[^ ]*'gera todos os caracteres do início ( ^) até o primeiro espaço de cada linha, uniqremove linhas repetidas, deixando assim uma lista de endereços IP. Graças à substituição de comando, o forloop percorre esta lista imprimindo o IP atualmente processado, seguido de "count" e a contagem. O último é calculado por grep -c, que conta o número de linhas com pelo menos uma correspondência.

Exemplo de execução

$ 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
sobremesa
fonte
13
Essa solução repete o arquivo de entrada repetidamente, uma vez para cada endereço IP, o que será muito lento se o arquivo for grande. As outras soluções que usam uniq -cou awkprecisam apenas ler o arquivo uma vez,
David
1
@ David, isso é verdade, mas esta teria sido a minha primeira tentativa também, sabendo que o grep conta. A menos que o desempenho seja um problema mensurável ... não otimize prematuramente?
D. Ben Knoble 29/03
3
Eu não chamaria isso de otimização prematura, já que a solução mais eficiente também é mais simples, mas cada uma é sua.
David
A propósito, por que está escrito como <log grep ...e não grep ... log?
Santiago
@Santiago Porque isso é melhor em muitos aspectos, como Stéphane Chazelas explica aqui no U&L .
dessert
39

Você pode usar cute uniqferramentas:

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

Explicação:

  • cut -d ' ' -f1 : extrair primeiro campo (endereço IP)
  • uniq -c : relata linhas repetidas e exibe o número de ocorrências
Mikael Flora
fonte
6
Pode-se usar sed, por exemplo, sed -E 's/ *(\S*) *(\S*)/\2 count: \1/'para obter a saída exatamente como o OP queria.
dessert
2
Essa deve ser a resposta aceita, já que a sobremesa precisa ler o arquivo repetidamente, por isso é muito mais lenta. E você pode usá-lo facilmente sort file | cut .... caso não tenha certeza se o arquivo já está classificado.
Guntram Blohm apoia Monica em
14

Se você não precisar especificamente do formato de saída especificado, recomendo a resposta já postada com base em cut+uniq

Se você realmente precisa do formato de saída fornecido, uma maneira de passagem única no Awk seria

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

Isso não é ideal quando a entrada já está classificada, pois armazena desnecessariamente todos os IPs na memória - uma maneira melhor, embora mais complicada, de fazê-lo no caso pré-classificado (mais diretamente equivalente a uniq -c) seria:

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
chave de aço
fonte
seria fácil alterar a resposta baseada em cut + uniq com sed para aparecer no formato exigido.
Peter - Restabelece Monica
@ PeterA.Schneider sim - acredito que já foi mencionado nos comentários a essa resposta
steeldriver
Ah, entendi.
Peter - Restabelece Monica
8

Aqui está uma solução possível:

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
  • substitua file.logpelo nome do arquivo real.
  • a expressão de substituição de comando $(awk '{print $1}' "$IN_FILE" | sort -u)fornecerá uma lista dos valores exclusivos da primeira coluna.
  • então grep -ccontará cada um desses valores no arquivo.

$ 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
fonte
1
Prefiro printf...
D. Ben Knoble 29/03
1
Isso significa que você precisa processar o arquivo inteiro várias vezes. Uma vez para obter a lista de IPs e mais uma vez para cada um dos IPs encontrados.
terdon 29/03
5

Alguns 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

Essa é a mesma idéia da abordagem awk do Steeldriver , mas no Perl. O -aperl faz com que automaticamente divida cada linha de entrada na matriz @F, cujo primeiro elemento (o IP) é $F[0]. Portanto, $k{$F[0]}++criará o hash %k, cujas chaves são os IPs e cujos valores são o número de vezes que cada IP foi visto. O }{é funky perlspeak para "faça o resto no final, depois de processar todas as entradas". Portanto, no final, o script irá percorrer as chaves do hash e imprimir a chave atual ( $_) junto com seu valor ( $k{$_}).

E, para que as pessoas não pensem que o perl obriga a escrever scripts que pareçam rabiscos enigmáticos, é a mesma coisa de uma forma menos condensada:

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
fonte
4

Talvez não seja isso que o OP queira; no entanto, se soubermos que o tamanho do endereço IP será limitado a 15 caracteres, uma maneira mais rápida de exibir as contagens com IPs exclusivos a partir de um grande arquivo de log pode ser obtida usando uniqapenas o 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] ...

Opções:

-w Ncompara não mais que Ncaracteres em linhas

-c prefixará as linhas pelo número de ocorrências

Como alternativa, para saída formatada exata, eu prefiro awk(também deve funcionar para endereços 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

Observe que uniqnão detectará linhas repetidas no arquivo de entrada se elas não estiverem adjacentes; portanto, pode ser necessário para sorto arquivo.

Y. Pradhan
fonte
1
Provavelmente bom o suficiente na prática, mas vale a pena notar os casos extremos. Apenas 6 caracteres provavelmente constantes após o IP `- - [`. Mas, em teoria, o endereço pode ter até 8 caracteres a menos que o máximo, portanto, uma mudança de data pode dividir a contagem para esse IP. E, como você sugere, isso não funcionará para o IPv6.
Martin Thornton
Gostei, não sabia que o uniq podia contar!
j0h 31/03
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))

Saída:

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
fonte
0
cut -f1 -d- my.log | sort | uniq -c

Explicação: Pegue o primeiro campo de my.log dividido em hífens -e classifique-o. uniqprecisa de entrada classificada. -cdiz para contar ocorrências.

Doutorado
fonte