Armas secretas: AWK

AWK é uma “arma secreta“: faz parte daquele conjunto de ferramentas que sempre estiveram ao seu lado (surge em 1977 e quase toda distribuição Linux a inclui), você a ignora durante toda a sua vida e em um belo dia resolve experimentá-la. É o momento em que aquele sentimento de “ENTÃO QUER DIZER QUE SOFRI TODOS ESTES ANOS A TOA???” aparece. :)

O que é AWK?

Trata-se de uma linguagem de programação cujo nome vêm das iniciais dos seus criadores (Alfred Aho, Peter Weinberger e Brian Kernighan – galerinha bruta) e nos permite escrever programas que tenham como fonte de dados arquivos texto. O mais impressionante é que você consegue resultados incríveis com pouquíssimas linhas de código.

É uma excelente ferramenta quando desejamos extrair inteligência de arquivos texto, especialmente logs. Sei que esta descrição é muito ruim, sendo assim, que tal irmos direto ao código?

Exemplo real: log de acesso do /dev/All

Sempre que alguém acessa o /dev/All é incluída uma linha em um arquivo de log nos informando a data, hora, IP do visitante e página visitada tal como no exemplo a seguir:

03/06/2015 10:20:00 186.213.73.169 http://www.devall.com.br
03/06/2015 10:20:01 34.13.3.69 http://www.devall.com.br
03/06/2015 10:20:20 186.213.73.169 http://www.devall.com.br
04/06/2015 10:20 186.213.73.169 http://www.devall.com.br/blog/info/3

Se quisermos saber quantos acessos tivemos por dia fica fácil pensar em um programa: dado que o caractere de espaço separa estes quatro campos basta escrever um parser simples na minha linguagem favorita e voilá: crio um acumulador para cada dia lendo o arquivo do início ao fim.

Mas e se eu te disser que consigo o mesmo efeito com apenas uma linha de AWK?

{contador[$1]++} END {for (j in contador) print j, contador[j]}

E como eu uso isto?

cat acessos.log | awk '{contador[$1]++} END {for (j in contador) print j, contador[j]}'

Gerando a saída:

03/06/2015 3
04/06/2015 1

Parece até mágica: agora vamos dissecar este código.

Entendendo o AWK

A sintaxe da linguagem é composta por dois elementos fundamentais: uma condição referente à linha corrente do arquivo sendo lido e o que deve ser executado caso esta condição seja satisfeita. Podemos resumir a sintaxe ao código abaixo:

condição {ações a serem executadas}

No nosso código temos claramente duas condições. Vamos à primeira:

{contador[$1]++}

Não há uma condição estabelecida, sendo assim o bloco entre chaves sempre será executado a cada linha lida. O código é tão simples que assusta: criamos uma tabela de hash cuja chave corresponda ao primeiro campo do arquivo.

Campo? Sim: por padrão AWK interpreta o caractere de espaço como um separador de campos. “$1” representa o primeiro campo (data), “$2” o segundo (hora) e por aí vai. Ao aplicar o operador “++” em meu hash, AWK automaticamente o interpreta como um valor numérico e o incrementa.

Ao final da leitura do arquivo temos uma tabela de hash tal como a exposta a seguir:

 ["03/06/2015" => 3, "04/06/2015" => 1] 

Note que não precisamos declarar nossas variáveis antes de usá-las, o que nos gera um código muito mais simples. Agora vamos à segunda condição:

 END { for (j in contador) print j, contador[j]} 

AWK possuí algumas variáveis e condições embutidas. Dentre estas encontra-se “END”: uma condição que é satisfeita apenas quando chegamos ao final do arquivo. E o que fazemos quando isto ocorre? Geramos um relatório de saída ao nosso usuário final.

Apenas iteramos sobre as chaves presentes em nossa tabela de hash usando a estrutura de controle “for” e, para cada uma destas, iremos imprimir seu valor, seguido do número de acessos que fomos incrementando a cada linha do arquivo.

Outro exemplo: validando arquivos

Alguns anos atrás precisei interagir com um sistema que salvava notas fiscais em arquivos textuais similares ao exposto a seguir:

0100|Bolacha Mabel|3.40
0100|Leite Itambé|2.00
0200|5,40

O caractere “|” era usado para separar os campos e havia diferentes tipos de registros dentro do mesmo arquivo. Aqueles que começavam com “0100” representavam um item da nota fiscal (nome do item e valor pago respectivamente), e “0200” o valor total da nota. Uma validação simples deste arquivo é verificar se o valor total bate com o somatório do valor pago em cada item.

O validador em AWK abaixo ilustra melhor a linguagem. Iremos salvá-lo em um arquivo chamado “prog.awk”.


BEGIN {FS = "|"}
$1 == "0100" { total += $3}
$1 == "0200" && $2 != total { print "Valor inválido. Total = ", $2, "Somatório = ", total}
$1 == "0200" && $2 == total {print "Arquivo OK"}

A primeira condição é executada quando o programa é iniciado, o que é representado pela condição “BEGIN”. Nesta linha definimos que o caractere separador de campos será “|” mudando o valor de uma variável embutida da linguagem que é o FS (Field Separator).

A segunda linha verifica se o campo 1 contém o texto “0100”. Se for o caso, iremos criar uma variável chamada “total” na qual iremos incrementar o valor presente no terceiro campo. (nota importante: evite o caractere vírgula ao lidar com números de pontos flutuante com AWK).

A terceira linha verifica se o arquivo é inválido. Primeiro checamos se o valor do primeiro campo é “0200” e se o valor do segundo campo desta linha é diferente ao que calculamos na variável “total”. Se for, iremos imprimir o erro para o usuário.

A quarta linha valida o arquivo fazendo o oposto do que foi feito na terceira.

E como executamos este programa no Linux?

 cat notafiscal.txt | awk -f prog.awk 

Mais recursos sobre AWK

O objetivo por trás deste post foi apenas mostrar com o que se parece a linguagem de programação para despertar sua curiosidade sobre o assunto. Claro que não cabiam aqui todos os detalhes a seu respeito, mas você os encontrará nos links abaixo:

AWK Community Portal – http://awk.info – o nome já diz tudo: é o portal da comunidade mundial com muitos links e textos interessantes a respeito.

AWK User Guide – http://www.math.utah.edu/docs/info/gawk_toc.html – é o texto que estou usando para me aprofundar na linguagem. Excelente leitura!

JAWK – http://jawk.sourceforge.net/ – Se você usa a JVM vai gostar de conhecer este projeto: é uma implementação completa do AWK para Java. Muito bom como linguagem embarcada!

AWK.net – https://awkdotnet.codeplex.com/ – Curte .net? Encontrei esta implementação, mas não cheguei a experimentá-la.

Notas finais

Minhas experiências recentes com AWK têm sido extremamente satisfatórias: em um de nossos projetos em que precisamos lidar com arquivos de log conseguimos um excelente ganho de produtividade conhecendo muito pouco a linguagem.

Espero que com este post mais pessoas incluam AWK em sua caixa de ferramentas, pois é algo que vale muito à pena aprender.

 

4 thoughts on “Armas secretas: AWK

  1. Muito bacana, Henrique. O natural muitas vezes é pensarmos em usar nossa linguagem de propósito geral preferidas para resolver qualquer problema. Parece doer menos que aprender outra linguagem mais apropriada, ou uma DSL específica para o problema em questão, como é o caso de AWK e processamento de arquivos texto. Com pouco tempo o investimento que fazemos para incluir essas novas ferramentas no nosso arsenal se paga, e com juros.

    Responda

    Kico (Henrique Lobo Weissmann) Reply:

    Sem dúvida: nos salvou um projeto aqui cuja primeira opção era implementar uma DSL em Groovy! :)

    Responda

  2. Muito legal Kico!
    Eu ja usei o AWK, porém de modo mais basico.
    A grande boa dele, na minha opinião, é a integração direta com o bash, como foi feito com o comando cat!

    Abç

    Responda

  3. AWK é uma linguagem bem poderosa se vc consegue dividir o seu problema em uma série de registros contendo um ou mais campos, veja como da pra fazer coisas divertidas com 15 linhas apenas

    por ser uma linguagem orientada a fluxo de dados, vc tem os blocos BEGIN, END e vc pode ter blocos para executar em dadas condições ( ou sempre ). o GAWK atualmente suporta arrays multi-dimensionais porem eu não curto muito não — se vc precisa de algo assim vc precisa talvez de um programa de verdade, testes, etc.

    uma coisa que eu gosto de usar o awk é para dividir um arquivo de acordo com o conteudo. imagine que vc tem algo como


    01/01/2011 abobora 1 2 3 45 7
    01/01/2011 verde 84 4 3 5 46 -09090

    e vc quer um arquivo com todos os ‘abobora’ e outros com todos os ‘verde’ vc pode fazer algo como

    awk '{
    arquivo = $2 ".log" ; # para concatenar de strings basta colocar o conteudo lado-a-lado
    $2 = "";
    print $0 > arquivo # um redirecionamento semelhante ao do shell
    }' todos.log

    Responda

Leave a Reply

Your email address will not be published. Required fields are marked *