Desenvolvimento

22 ago, 2016

Extraindo dados da Wikipédia usando curl, grep, cut e outros comandos shell

Publicidade

Neste artigo, vou mostrar como fui capaz de extrair e processar algumas informações da Wikipédia usando apenas uma combinação de utilitários bash comuns como curl e grep.

A necessidade

Se você é um amante de esportes como eu, acho que o seu coração foi recentemente aquecido pelos Jogos Olímpicos Rio 2016. Meu esporte favorito nos jogos é o judô e, com as competições estão em alta, eu queria saber quem eram os melhores “judocas olímpicos” de todos os tempos em número de medalhas conquistadas durante os jogos (não importa o tipo de medalha).

Tentei encontrar a resposta no Google por um tempo, mas não foi fácil encontrar um resultado atualizado, então eu decidi fazer uma pesquisa rápida e tentar chegar a uma conclusão por minha conta. Eu tenho que dizer que foi um pouco mais difícil do que eu esperava, mas foi definitivamente divertido…

O conjunto de dados

A primeira coisa que eu precisava era de uma fonte de dados confiável e atualizada, que incluísse a lista de todos os ganhadores de medalhas olímpicas de judô da história. Isso foi fácil de encontrar na Wikipedia: lista de medalhistas olímpicos em judô.

De qualquer forma, os dados na Wikipédia estão estruturados de maneira a serem lidos facilmente por seres humanos, e não para serem processados por uma máquina. Eu também não encontrei nenhuma maneira de ter os mesmos dados na página em um formato CSV ou JSON, então a única opção viável foi extrair, eu mesmo, os dados a partir da página web.

No começo, eu pensei em criar um comando rápido e sujo em JavaScript, além de usar alguma biblioteca como cheerio para extrair os dados diretamente a partir do código HTML da página, mas isso soou como algo muito trabalho para o objetivo simples que eu tinha em mente.

Então, eu dei uma outra olhada rápida na Wikipédia para descobrir se havia qualquer outro formato melhor para extrair as informações. Indo para a opção de edição da página, percebi que analisar o wikitext (fonte) da página seria muito mais fácil e eu poderia até usar uma expressão regular para extrair as informações relevantes a partir de lá.

Nessa fase, eu me perguntava se havia uma maneira de obter apenas o wikitext de uma página específica da Wikipédia. Acontece que é possível e é muito fácil: basta acrescentar o parâmetro de consulta ?action=raw na url!

Então, digitar https://en.wikipedia.org/wiki/List_of_Olympic_medalists_in_judo?action=raw nos dará nosso conjunto de dados de partida que será parecido com este:

==Men==
===Extra Lightweight===
*60 kg
{| {{MedalistTable|type=Games}}
|-
|rowspan=2|[[Judo at the 1980 Summer Olympics|1980 Moscow]]<br>{{DetailsLink|Judo at the 1980 Summer Olympics – Men's 60 kg}}
|rowspan=2|{{flagIOCmedalist|[[Thierry Rey]]|FRA|1980 Summer}}
|rowspan=2|{{flagIOCmedalist|[[José Rodríguez (judoka)|José Rodríguez]]|CUB|1980 Summer}}
|{{flagIOCmedalist|[[Tibor Kincses (judoka)|Tibor Kincses]]|HUN|1980 Summer}}
|-
|{{flagIOCmedalist|[[Aramby Emizh]]|URS|1980 Summer}}
|-
|rowspan=2|[[Judo at the 1984 Summer Olympics|1984 Los Angeles]]<br>{{DetailsLink|Judo at the 1984 Summer Olympics – Men's 60 kg}}
|rowspan=2|{{flagIOCmedalist|[[Shinji Hosokawa]]|JPN|1984 Summer}}
|rowspan=2|{{flagIOCmedalist|[[Kim Jae-Yup]]|KOR|1984 Summer}}
|{{flagIOCmedalist|[[Neil Eckersley]]|GBR|1984 Summer}}
|-
|{{flagIOCmedalist|[[Edward Liddie]]|USA|1984 Summer}}
|-
...

Note que, por razões de brevidade, a partir de agora eu vou usar para indicar que há uma grande quantidade de dados que foram retirados do exemplo.

Nós podemos facilmente obter esses dados em nosso bash shell com curl:

curl -sS "https://en.wikipedia.org/
wiki/List_of_Olympic_medalists_in_judo?action=raw"

No caso de você não saber, as opções -sS permitem retirar a saída de progresso do download e apenas imprimir os dados transferidos (ou qualquer erro possível) no console.

Agora que temos o nosso conjunto de dados, para calcular o resultado, precisamos:

  • Extrair todos os nomes de atletas;
  • Contar as ocorrências de cada um deles;
  • Ordenar inversamente pela contagem resultante.

Vamos fazer isso com uma combinação de comandos bash, ligados entre si usando o operador pipe (|). Pipes permitem que você use a saída de um programa como a entrada de outro, efetivamente criando um fluxo de dados que são transformados passo a passo em pequenas operações fáceis de compreender.

Extraindo dados com grep

O conjunto de dados mostrado antes é apenas o código wikitext necessário para renderizar as tabelas de atletas que ganharam medalhas por categoria e ano. Assim, podemos facilmente assumir que todos os atletas listados na página são interessantes para nós.

Como você pode ver, todo atleta é referenciado no código usando o template flagIOCmedalist, e cada entrada se parece com:

{{flagIOCmedalist|[[NAME]]|COUNTRY|OLYMPIC GAME}}

Assim, podemos facilmente extrair todos os nomes de atletas com um regex como o seguinte:

flagIOCmedalist\|\[\[(.+)\]\]

Essa expressão regular se comporta como descrito na imagem a seguir:

flagIOCmedalist_matches_the_characters_flagIOCmedalist

Se você quer entender todos os detalhes desse regex, pode brincar com ele em regex101.

No shell, podemos aplicar um regex para um conjunto de dados de entrada usando o grep. Nós precisamos concatenar a saída do curl para grep usando o operador pipe (|):

curl ... |  grep -Eoi "flagIOCmedalist\|\[\[(.+)\]\]"

Com o grep, estamos usando as opções -Eoi que nos permitem:

  • -E: usar uma expressão regular POSIX extended (ERE);
  • -o: imprimir apenas as partes combinadas (não-vazio) de uma linha de correspondência;
  • -i: combinações case insensitive (o que não é realmente necessário para o nosso problema, mas poderia nos ajudar para combinar com qualquer variação de template de referência como flagiocmedalist e FLAGIOCMEDALIST).

O comando anterior terá uma saída como esta:

flagIOCmedalist|[[Thierry Rey]]  
flagIOCmedalist|[[José Rodríguez (judoka)|José Rodríguez]]  
flagIOCmedalist|[[Tibor Kincses (judoka)|Tibor Kincses]]  
flagIOCmedalist|[[Aramby Emizh]]  
...

O que, naturalmente, é feio. Nós realmente queremos ter apenas o nome de cada atleta e não toda a sintaxe que envolve o template.

Vamos tentar limpar os dados um pouco mais.

Limpando os dados com cut

cut é outro utilitário de linha de comando interessante que pode ser usado de um monte de maneiras diferentes para extrair substrings (ou colunas) de texto.

Usando cut podemos remover todo o texto desnecessário em 3 passos:

  1. remover o prefixo flagIOCmedalist | [[;
  2. remover o sufixo ]];
  3. limpar os nomes com a nota de desambiguação “(judo)”. Por exemplo, de José Rodríguez (judoca) | José Rodríguez queremos manter apenas José Rodríguez.

Substring com cut

A completar a primeira etapa (removendo o prefixo flagIOCmedalist|[[), podemos usar o seguinte comando:

... | cut -c"19-"

A opção -c “19-“ significa: “tomar o substring que começa a partir do número de caracteres 19 até o fim da cadeia”. Caractere 19 é o 20° personagem considerando que as strings são “0-indexed”.

Você pode usar a opção -c para extrair qualquer substring genérica. Por exemplo -c “4-8” irá extrair a substring a partir do quinto para o nono caractere. Se você deixar a faixa aberta à direita, isso levará toda a string restante até o fim.

Esse filtro será aplicado a cada linha e irá modificar a corrente de dados como se segue:

Thierry Rey]]  
José Rodríguez (judoka)|José Rodríguez]]  
Tibor Kincses (judoka)|Tibor Kincses]]  
Aramby Emizh]]  
...

Strings intensas com cut

Agora nós gostaríamos de remover o sufixo ]].

A fim de alcançar esse objetivo, podemos utilizar:

... cut -d \] -f 1

Dessa vez, estamos usando as opções -d e -f.

Quando usamos d, podemos especificar um caractere a ser usado como delimitador (], nesse caso); dessa forma, cut não irá dividir a string de índice de caractere, mas em pedaços (ou colunas), gerando um novo pedaço cada vez que o delimitador é encontrado ao longo da linha.

A opção -f pode ser combinada com -d para selecionar um ou mais blocos; nesse caso, queremos selecionar o primeiro pedaço.

Como podemos esperar, este será o estado dos nossos dados após este comando ser executado no pipeline:

Thierry Rey  
José Rodríguez (judoka)|José Rodríguez  
Tibor Kincses (judoka)|Tibor Kincses  
Aramby Emizh  
...

Os dados parecem quase limpos, precisamos apenas nos livrar das notas de desambiguação ocasionais. Esses casos podem ser distinguidos pelo caractere | (pipe) que separa a definição de desambiguação do texto que deve ser processado pelo motor wikitext em código HTML. Nesses casos, nós queremos manter apenas a parte da string de caracteres após o caractere pipe. Mais uma vez, podemos usar cut com as opções -d e -f:

... | cut -d \| -f 2

Dessa vez, estamos usando o caractere pipe como delimitador e pegando apenas o segundo pedaço da string.

Isso pode parecer óbvio nessa fase, mas o que acontece em todas as linhas onde não há nenhuma nota de desambiguação (e nenhuma barra vertical)? Nesses casos, teremos apenas um pedaço que contém a linha completa, então o que é que vamos obter usando a opção -f 2?

Felizmente, nesses casos, o cut é inteligente o suficiente para assumir que a string não está combinando com nosso padrão e retorna a linha inteira, então os nossos dados não são destruídos.

O resultado final depois desse comando é:

Thierry Rey  
José Rodríguez  
Tibor Kincses  
Aramby Emizh  
...

Nossos dados estão finalmente limpos!

Contando e ordenando com uniq e sort

Agora que temos a nossa lista limpa de nomes, precisamos contar as ocorrências de cada atleta e classificar.

A fim de remover duplicatas (e contar as ocorrências), podemos usar o comando uniq. Esse comando, quando alimentado com algum texto, emite o próprio texto com linhas adjacentes idênticas transformando-as em apenas uma.

Isso significa que, antes de podermos usar uniq, precisamos ter todos os nomes em ordem alfabética, para que todos os atletas com várias ocorrências tenham seu nome repetido em várias linhas, um após o outro.

Isso pode ser alcançado com o comando sort, que, como você pode facilmente adivinhar, apenas classifica todas as linhas recebidas como entrada em ordem alfabética.

Só para dar um exemplo prático, depois de usar o comando sort, seus dados serão algo como:

...
Paweł Nastula  
Peter Seisenbacher  
Peter Seisenbacher  
Priscilla Gneto  
Qin Dongya  
Radomir Kovačević  
Rafael Silva  
Rafael Silva  
Rafaela Silva  
Ramaz Kharshiladze  
...

Agora nós podemos canalizar o comando uniq -c para nosso fluxo de processamento de dados a fim de remover duplicatas. A opção -c imprime em frente de cada linha o número de ocorrências consecutivas originalmente encontradas para essa linha.

Depois desse comando, teremos algo como isto:

...
1 Paweł Nastula  
2 Peter Seisenbacher  
1 Priscilla Gneto  
1 Qin Dongya  
1 Radomir Kovačević  
2 Rafael Silva  
1 Rafaela Silva  
1 Ramaz Kharshiladze  
...

Note que Rafael Silva e Rafaela Silva são dois atletas diferentes, não é um erro de digitação! ?

Estamos quase terminando. Agora só precisamos ordenar os nossos dados mais uma vez, mas desta vez na ordem inversa. Para isso, podemos usar o comando sort, mais uma vez, desta vez como se segue:

sort -nr

Quando a opção -n especifica que a primeira parte de cada linha é um número (de modo que o comando pode distinguir eficazmente e classificar linhas começando, por exemplo, com 1 e 10). A opção -r, em vez disso, indica que queremos classificar na ordem reversa (maior primeiro).

Isso conclui nossa pipeline de comandos!

Combinando tudo junto

Combinando todos os comandos juntos, nosso pipeline final será o seguinte:

curl -sS "https://en.wikipedia.org/wiki/List_of_Olympic_medalists_in_judo?action=raw" |\  
 grep -Eoi "flagIOCmedalist\|\[\[(.+)\]\]" |\
 cut -c"19-" |\
 cut -d \] -f 1 |\
 cut -d \| -f 2 |\
 sort |\
 uniq -c |\
 sort -nr

Agora que você entendeu cada parte, não deve ser muito enigmático olhar tudo junto.

Se executá-lo, podemos finalmente descobrir quem são os melhores judocas olímpicos até a Rio 2016:

… Tambores …

Com 4 medalhas:

Com 3 medalhas:

Muitas franceses lá, uh! É uma pena que nenhum atleta italiano esteja lá ainda! ?

Conclusão

Espero que este artigo tenha mostrado que o unix shell é uma ferramenta muito poderosa. Se você aprender seus comandos básicos e as opções mais comuns, vai ser capaz de completar um monte de tarefas diretamente a partir da linha de comando por apenas combiná-las com sabedoria. Você não vai precisar abrir um editor e escrever um longo script e, em seguida, ter um intérprete como Node.js para executá-lo – por vezes, a linha de comando é tudo o que você precisa!

Se você quiser fazer mais algumas experiências relativas a esse assunto, eu posso propor-lhe uma agradável variação deste artigo como o exercício:

“Você pode calcular o ranking das nações com o maior número de medalhas?”.

Se assim for, por favor, escreva a sua solução nos comentários ?

Até a próxima vez!

***

Luciano Mammino faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://loige.co/extracting-data-from-wikipedia-using-curl-grep-cut-and-other-bash-commands/