Bash One Liners #2

Em: 26/06/2008 Tags: sed, sort, awk, Bash Referencie do seu blog (Trackback)

Este segundo episódio da série apresenta um one liner que imprime o ranking de meses com mais artigos publicados em um blog (e uma explicação detalhada de como o one liner foi construído, para que o leitor conheça mais as ferramentas que qualquer distribuição Linux nos dá de graça e muitas vezes nem sabemos que existem).

Este script funciona apenas para blogs que tenham uma listagem de artigos mais publicados com a seguinte estrutura geral (esse padrão pode se repetir várias vezes):

mês ano_com_quatro_dígitos (quantidade_de_artigos)

Para este artigo utilizo como exemplo o blog da Thânia Clair, pois se eu utilizar o meu, após publicado o artigo as expressões regulares utilizadas passam a casar com as listagens deste artigo, e todos os exemplos passam a apresentar resultados diferentes dos que eu obtive antes da publicação.

Eis o one liner:

w3m -dump http://www.thaniaclair.com | awk '/[0-9][0-9][0-9][0-9] \([0-9]+\)$/ { print $4, $2, $3 }' | sed 's/[\(|\)]//g' | sort -gr

Eis o resultado:

23 Janeiro 2008
18 Dezembro 2007
16 Fevereiro 2008
13 Novembro 2007
6 Abril 2008
5 Março 2008
2 Maio 2008
1 Junho 2008

Não parece muito simples de entender, né? Não se preocupe, vou apresentar aqui, passo a passo, como cheguei a essa solução. Considero esse um exemplo significativo de one liner, pois demonstra a filosofia que tenho seguido para criar minhas soluções: escolher a ferramenta mais adequada para a tarefa. Note que utilizei 4 programas para chegar a esse resultado, em vez de uma única linguagem de programação, ao contrário do que nos ensinam em cursos Brasil afora.

Estes 4 programas têm propósitos bem específicos, e por isso atendem tão bem ao domínio do meu problema:

  • w3m: navegador web modo texto, com suporte a screen scraping
  • awk: linguagem criada para processamento de dados estruturados
  • sed: linguagem criada para transformação de texto
  • sort: programa criado para ordenar linhas de um arquivo

Como sugestão, proponho ao leitor realizar a mesma tarefa com uma linguagem de programação de propósito geral, como Perl, PHP, Python ou Ruby, e depois me contar em quantas linhas resolveu o mesmo problema (ouso chutar que Ruby e Perl gastariam menos linhas). Só pra não perder a deixa: tente em Java também!

Obviamente, não tenho nada contra nenhuma dessas linguagens, estou apenas reforçando a minha fisolofia da melhor ferramenta para o trabalho.

Agora vamos à solução.

Passo 1 - Imprimir o conteúdo do site

Para imprimir o conteúdo de um site na saída padrão a opção mais simples que eu conheço é utilizar ou o lynx ou o w3m com a opção -dump habilitada. O conteúdo é impresso em um formato estruturado, mas todas as tags são removidas. O seguinte comando:

w3m -dump http://www.thaniaclair.com

Imprimiria um documento que terminaria assim (estou suprimindo caracteres especiais que só fazem sentido quando lidos em um terminal):

  ruby (6)
  tirinhas (21)
  web (1)

Arquivos

  Junho 2008 (1)
  Maio 2008 (2)
  Abril 2008 (6)
  Março 2008 (5)
  Fevereiro 2008 (16)
  Janeiro 2008 (23)
  Dezembro 2007 (18)
  Novembro 2007 (13)

Nosso próximo passo é filtrar esse documento, para trabalhar apenas com as linhas que nos interessam. Nesse ponto entra o awk.

Passo 2 - Selecionar as linhas que interessam com awk

As linhas que nos interessam são todas aquelas que exibem a quantidade de artigos publicados durante cada mês de vida do blog. Essas linhas seguem o mesmo padrão: o nome de um mês, seguido de um espaço em branco, seguido de um ano, seguido de um espaço em branco, seguido pelo número de artigos entre parênteses.

A função do awk, nesse momento, é varrer todo o documento impresso pelo w3m, e desse documento excluir todas as linhas que não se enquadrem nesse padrão. Nossa declaração (na forma de uma expressão regular) é a seguinte: imprima qualquer linha que termine com 4 números (o ano), seguido por um espaço em branco, seguido por um ou mais números (a quantidade de artigos) cercados por parênteses, caso tudo isso esteja no fim da linha.

awk '/[0-9][0-9][0-9][0-9] \([0-9]+\)$/ { print $0 }'

O awk, então, imprimiria:

Junho 2008 (1)
Maio 2008 (2)
Abril 2008 (6)
Março 2008 (5)
Fevereiro 2008 (16)
Janeiro 2008 (23)
Dezembro 2007 (18)
Novembro 2007 (13)

Ótimo! Apenas as linhas que nos interessam. Mas como vamos ordenar esses dados? Como sabemos de antemão que o programa que irá ordená-los será o sort (e que o sort ordena as linhas lendo-as da esquerda para a direita), temos que posicionar a quantidade de artigos do mês no início da linha, e não no final. Nosso texto precisa, portanto, de transformação. Mas essa transformação é trivial para o awk. Vamos ver porquê.

No momento em que o awk encontra uma linha que atende ao padrão que especificamos ele define um conjunto de variáveis iniciadas por $. $0 corresponde à própria linha; para definir as demais variáveis $ o awk utiliza (por padrão) o espaço em branco como caracter divisor de dados, e atribui cada dado a uma variável, da esquerda para direita, de 1 até N (N = quantidade de dados). No nosso exemplo, nossas linhas geram as seguintes variáveis:

  • $1: um caracter especial gerado pelo w3m que eu decidi ignorar
  • $2: o mês
  • $3: o ano
  • $4: a quantidade de artigos publicados, cercados por parênteses

Nosso programa awk anterior, portanto, deve ser modificado da seguinte forma:

awk '/[0-9][0-9][0-9][0-9] \([0-9]+\)$/ { print $4, $2, $3 }'

Nossa saída, agora, seria:

(1) Junho 2008
(2) Maio 2008
(6) Abril 2008
(5) Março 2008
(16) Fevereiro 2008
(23) Janeiro 2008
(18) Dezembro 2007
(13) Novembro 2007

Ótimo! Agora nossos dados estão quase prontos para serem passados ao sort. O problema são os parênteses cercando a quantidade de artigos. Se passarmos os dados dessa forma para o sort ele não realizará a ordenação corretamente. Precisamos, portanto, realizar uma simples substituição em todas as linhas: eliminar os 2 parênteses. Poderíamos tentar fazer isso diretamente no awk, mas certamente o código-fonte perderia legibilidade, e talvez fosse obrigado a deixar de ser um one liner (e tirar todo o propósito deste artigo!). A saída, portanto, é escolher a melhor ferramenta para o trabalho: sed.

Passo 3: Eliminar caracteres desnecessários

Este simples comando sed irá substituir os parênteses de abertura e fechamento por uma string vazia em todas as linhas do texto que chegar como entrada:

sed 's/[\(|\)]//g'

Nossa entrada, que era assim (apenas as 2 primeiras linhas):

(1) Junho 2008
(2) Maio 2008

Sairá do sed assim:

1 Junho 2008
2 Maio 2008

Com essa saída o sort é capaz de fazer a tarefa dele: ordenar as linhas.

Passo 4: Ordenar as linhas

Por padrão o sort ordena as linhas da mesma forma que um dicionário ordena seus verbetes. Assim, por exemplo, 10 apareceria antes de 9. Para instruir o sort a ordenar as linhas de entrada como se elas fossem dados numéricos, utilizamos a opção -g, e para inverter a ordenação (colocando o maior número no início e o menor no fim), utilizamos a opção -r. Nosso último filtro, portanto, é o seguinte:

sort -gr

E quando aplicado à saída do programa sed:

1 Junho 2008
2 Maio 2008
6 Abril 2008
5 Março 2008
16 Fevereiro 2008
23 Janeiro 2008
18 Dezembro 2007
13 Novembro 2007

Nos devolve:

23 Janeiro 2008
18 Dezembro 2007
16 Fevereiro 2008
13 Novembro 2007
6 Abril 2008
5 Março 2008
2 Maio 2008
1 Junho 2008

Problema resolvido!

Comentários adicionais

É importante ressaltar que se, ao fim do passo 1, a quantidade de artigos publicados no mês estivesse aparecendo na primeira coluna das linhas, não seria necessário utilizar o awk no passo 2, pois para filtrar linhas de um arquivo basta utilizar o programa grep.

Também seria possível, após o passo 4, fazer com que a quantidade de artigos publicados no mês voltasse a ser a terceira coluna de cada linha se adicionássemos mais uma chamada ao awk:

awk '{ print $2, $3, $1 }'

E se quiséssemos restaurar os parênteses, então o programa acima se tornaria:

awk '{ print $2, $3, "("$1")" }'

Agora você já deve entender o que esses dois programas fazem.

Uma resposta para “Bash One Liners #2”

  1. Bash One Liners #3 | Caio Moritz Ronchi disse:

    […] Bash One Liners #2 apresentei uma combinação de programas Linux para construirmos um ranking dos meses com mais […]

Escreva um comentário (utilize o formato Markdown)