Não é novidade que a internet é rica em informação. Contém textos e mais textos sobre tudo. Mas não é só por meio dessa informação, em forma de texto, que podemos aprender. A internet contém também muita informação crua na forma de dados.
Ao contrário dos textos, a informação crua de dados, conjuntos de números, requer coleta, processamento e análise para se transformar em informação verbal. Quando os dados estão reunidos, e.g. dentro de um único arquivo, não existe problema de coleta. Você baixa esse arquivo e começa a processar e analisar de imediato. Por outro lado, muitos dados estão disponíveis de uma maneira fragmentada e a coleta deles pode ser um desafio.
A corrida de São Silvestre têm resultados disponíveis na internet desde 1998. A partir de 2007, os resultados são centenas de páginas html com tabelas de 10 linhas, uma linha por atleta. Com os dados fragmentados desse jeito, qualquer iniciativa do tipo recorta-cola-edita-repete, algo que muitos usuários de planilha eletrônica fazem todos os dias, é abandonada por ser impraticável, inexequível.
Vou partir para uma solução computacional, afinal, o computador tem que trabalhar para mim, e não eu para ele. Vou trabalhar com técnicas de web scraping para extrair esse resultados de todas as páginas.
Programar é algo que eu gosto, pois apredendo e também exercito o que já sei. Na pior das hipóteses, posso passar horas programando essa solução, mas certamente será menos tempo do que fazer a terefa manualmente, algo repetitivo e que não acrescenta conhecimento algum. Na melhor das hipóteses, posso usar minha rotina para outras páginas e compartilhar com outras pessoas (com você que está lendo essa matéria).
As páginas de resultado têm uma estrutura fixa que é mantida em todas. Os resultados para o masculino de 2013 consistem de cabeçalho e rodapé com informações de situação, botões para mudar de página, e a tabela que quero no meio. Essa têm 10 linhas e 8 colunas que incluem informações como nome, idade e tempo para cumprir a prova.
Eu visitei várias páginas para certificar que a estrutura era a mesma.
Então fui para a página 4 (não me pergunte porque essa) para usar o
inspecionar elemento do Firefox e encontrei a estrutura na qual
(figura abaixo) a tabela é um elemento html <table>
, que tem 8
atributos, como a largura <witdh='98%'>
e a cor de fundo <bgcolor>
.
Carreguei os pacotes necessários e li a página para decodificar com
htmlTreeParse()
. No summary
tem-se o número de ocorrências de cada
tipo de elemento html na página. O campo que me interessa é o table
pois é ele que contém os dados que quero. Mas não existe apenas um
table
(como eu gostaria) e sendo assim, eu tive que ser específico ao
escrever a minha consulta para trazer a tabela alvo ignorando as demais.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
##---------------------------------------------------------------------- ## Pacotes necessários. library(XML) library(lattice) library(latticeExtra) ##---------------------------------------------------------------------- ## Ler uma página. ## Url para a 4 página dos resultados do masculino em 2013. ## NOTE: paste0() é para juntar a linha quebrada evitando ultrapassar a ## margem, coisa que procuro sempre evitar. url <- paste0("http://www.yescom.com.br", "/codigo_comum/classificacao/codigo", "/p_classificacao03_v1.asp", "?evento_yescom_id=1511&tipo=3", "&tipo_do_evento_id=4128", "&PaginaAtual=4", "&faixa=&sexo=M&campo=&pesquisa=") ## Abre a url no navegador. ## browseURL(url) ## Ler (direto da web). pr <- readLines(con=url) str(pr) |
1 2 |
## chr [1:395] "" "<html>" "<head>" ... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
## Outras opções para ler a página. ## library(RCurl) ## system.time(pr <- getURL(url, useragent="curl")) ## system.time(pr <- getURL(url, useragent="wget")) ## Não peça para ver no console. Salve em arquivo. ## cat(con=pr, file="pg0004.html") ## Salva em arquivo. ## download.file(url=url, destfile="pg0004.html") ## Ou baixa direto. ##---------------------------------------------------------------------- ## Examina e decodifica. h <- htmlTreeParse(file=pr, asText=TRUE, useInternalNodes=TRUE, encoding="utf-8") ## Ocorrência de cada typo de elemento. summary(h) |
1 2 3 4 5 6 7 8 9 10 |
## $nameCounts ## ## td tr b table img input span body form head ## 120 37 14 13 6 5 2 1 1 1 ## html link script title ## 1 1 1 1 ## ## $numNodes ## [1] 204 |
Minha primeira tentativa foi tentar a função XML::readHTMLTable()
que
extraí as tabelas (table
) de páginas html. De início eu achei que não
tinha dado muito certo porque ela leu todas as ocorrências de table
deixando em uma lista (tb
). Para estudar o que eu havia conseguido,
pedi classe (class
) e depois dimensão (dim
). Nisso vi que tinha um
data.frame
no elemento 10 com as dimensões certas, 10 por 8. Era a
tabela que eu queria.
1 2 3 4 5 6 7 |
##---------------------------------------------------------------------- ## Lê as tabelas (<table>) de uma página html. tb <- readHTMLTable(h, header=TRUE, stringsAsFactors=FALSE, as.data.frame=TRUE) length(tb) |
1 2 |
## [1] 13 |
1 2 3 4 5 |
## sapply(tb, class) ## sapply(tb, dim) ## A tabela que eu quero. tb[[10]] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
## CLASSIFICAÇÃO NUM ATLETA IDADE FX.ET. ## 1 31º 172 CARLOS OLIVEIRA SANTOS 23 M2024 ## 2 32º 138 RENILSON VITORINO DA SILVA 28 M2529 ## 3 33º 146 ANTONIO PEDRO DA SILVA SALES 38 M3539 ## 4 34º 113 IVANILDO PEREIRA DOS ANJOS 34 M3034 ## 5 35º 159 SEBASTIAO RODRIGUES VIANA 28 M2529 ## 6 36º 162 ADILSON ALVES DOUBERT 29 M2529 ## 7 37º 170 MARILDO JOSE BARDUCO 37 M3539 ## 8 38º 302 JONILSON PINTO PRATES 30 M3034 ## 9 39º 126 ADRIANO PACHECO DA CRUZ 25 M2529 ## 10 40º 14941 VAGNER DA SILVA NORONHA 29 M2529 ## EQUIPE TEMPO TEMPO LÍQUIDO ## 1 ABA 00:48:33 00:48:33 ## 2 MONTEVERGINE 00:48:37 00:48:37 ## 3 POEIRA AUTO PECAS TOP TRAINING 00:48:41 00:48:41 ## 4 CRUZEIRO CAIXA CAFE 3 CORACOES 00:48:51 00:48:51 ## 5 TRT SPORTS ADAUTO DOMINGUES 00:49:03 00:49:03 ## 6 BMF BOVESPA 00:49:04 00:49:04 ## 7 NAO INFORMADO 00:49:07 00:49:07 ## 8 VITORIA RUNNER LAFEX 00:49:09 00:49:08 ## 9 ASSES ADRIANO PACHECO NETTER 00:49:19 00:49:19 ## 10 MPD ENGENHARIA / MIGUEL SARKIS 00:50:00 00:49:25 |
O código até aqui já resolveu 1 problema: ler a tabela de uma página. No
entanto, não vou omitir minhas outras tentativas. Eu procurei um jeito
de especificar apenas a tabela de resultados que eu queria e consegui
filtrando para as tabelas que tinham width='98%'
e bgcolor
. Essas
são informações que vieram do inspecionar elemento que fiz (ver
imagem). Note como fazer para especificar dois atributos.
1 2 3 4 5 6 7 8 |
##---------------------------------------------------------------------- ## Fazer uma consulta mais específica. nc <- getNodeSet(doc=h, path="//table[@width='98%'][@bgcolor]", fun=readHTMLTable, stringsAsFactors=FALSE) tb <- nc[[1]] str(tb) |
1 2 3 4 5 6 7 8 9 10 |
## 'data.frame': 10 obs. of 8 variables: ## $ CLASSIFICAÇÃO: chr "31º" "32º" "33º" "34º" ... ## $ NUM : chr "172" "138" "146" "113" ... ## $ ATLETA : chr "CARLOS OLIVEIRA SANTOS" "RENILSON VITORINO DA SILVA" "ANTONIO PEDRO DA SILVA SALES" "IVANILDO PEREIRA DOS ANJOS" ... ## $ IDADE : chr "23" "28" "38" "34" ... ## $ FX.ET. : chr "M2024" "M2529" "M3539" "M3034" ... ## $ EQUIPE : chr "ABA" "MONTEVERGINE" "POEIRA AUTO PECAS TOP TRAINING" "CRUZEIRO CAIXA CAFE 3 CORACOES" ... ## $ TEMPO : chr "00:48:33" "00:48:37" "00:48:41" "00:48:51" ... ## $ TEMPO LÍQUIDO: chr "00:48:33" "00:48:37" "00:48:41" "00:48:51" ... |
Temos duas soluções para o problema 1, mas ainda resta o problema 2: ler todas as páginas. Resolvi correr um loop no intervalo de páginas. Para deixar meu script mais interessante e praticar um pouco mais, o número total de páginas eu fiz questão de extrair. Usei o inspecionar elemento para chegar à especificação abaixo.
1 2 3 4 5 6 7 8 9 |
##---------------------------------------------------------------------- ## Qual o número total de páginas? ## <span class="FontVermelha">1745</span> pges <- xpathApply(doc=h, path="//span[@class='FontVermelha']", fun=xmlValue) last <- max(as.numeric(unlist(pges))) last |
1 2 |
## [1] 1745 |
Para ler as 1745 páginas, eu precisei preparar os
endereços. Felizmente, o endereço de cada página contém o seu número
(PaginaAtual=
) e eu só precisei alterá-lo. Criei uma função
(getTable
), que ao receber o link (url), executa os passos: decodifica
a página, extraí a tabela e escreve num arquivo texto, acrescentando
novas linhas (append=TRUE
). Eu incluí uma opção de verbose
para ter
algum status de evolução do processo, algo que foi útil enquanto eu
fazia a função. Também incluí um indicador de progresso modo texto no
laço for
que faz a extração dos dados.
O sexo também é parte do endereço (sexo=
) então resolvi ler os
dois. Algo inesperado aconteceu quando eu experimentei a leitura com
poucas páginas. O que difere na url para masculino e feminino, nos
resultados gerais, não é o sexo=M
e sexo=F
que eu esperava mas o
tipo=3
e tipo=4
. Independente do sexo, as urls têm
sexo=M
. Esquisito, não é mesmo? Mas nos resultados por faixa etária o
sexo=F
aparece.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
##---------------------------------------------------------------------- ## Cria todos os endereços que serão lidos. url0 <- paste0("http://www.yescom.com.br", "/codigo_comum/classificacao/codigo", "/p_classificacao03_v1.asp", "?evento_yescom_id=1511&tipo=%d", ## sexo=tipo "&tipo_do_evento_id=4128&PaginaAtual=%d", ## página "&faixa=&sexo=M&campo=&pesquisa=") getTable <- function(url, verbose=TRUE){ t0 <- Sys.time() ## pr <- readLines(con=url) library(RCurl) pr <- getURL(url, useragent="wget") h <- htmlTreeParse(file=pr, asText=TRUE, useInternalNodes=TRUE, encoding="utf-8") nc <- getNodeSet(doc=h, path="//table[@width='98%'][@bgcolor]", fun=readHTMLTable, stringsAsFactors=FALSE) tb <- nc[[1]] if (verbose){ cat("Reading a page...\n") t1 <- Sys.time() tdiff <- t1-t0 units(tdiff) <- "secs" cat("Elapsed: ", c(tdiff), " secs\n") } write.table(x=tb, file=filename, append=TRUE, quote=FALSE, sep=";", fileEncoding="utf-8", row.names=FALSE, col.names=FALSE) invisible(NULL) } filename <- "saoSilvestre2013.csv" if (file.exists(filename)){ file.remove(filename) } pb <- txtProgressBar(min=1, max=last, style=3) for (i in 1:last){ url <- sprintf(fmt=url0, 3, i) getTable(url, verbose=FALSE) setTxtProgressBar(pb, i) } last <- 526 ## Total de páginas do femínino. pb <- txtProgressBar(min=1, max=last, style=3) for (i in 1:last){ url <- sprintf(fmt=url0, 4, i) getTable(url, verbose=FALSE) setTxtProgressBar(pb, i) } file.info(filename) ## Poderia usar um *apply também. ## invisible(lapply(urls, FUN=getTable, verbose=TRUE)) |
O que domina o tempo para concluir a tarefa é a velocidade de conexão com a internet. O processo levou 15 minutos com conexão cabeada.
O objetivo dessa matéria foi mostrar como extrair esses dados de página html. Agora que temos os dados, vamos só fazer um acabamento: ler os dados para certificar a integridade/qualidade e conhecer, mesmo que por cima, um pouco sobre eles.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
##---------------------------------------------------------------------- ## Lê o arquivo texto gerado. dtfr <- read.table("saoSilvestre2013.csv", header=FALSE, sep=";", stringsAsFactors=FALSE) names(dtfr) <- c("class", "num", "atl", "idade", "fxet", "eqp", "tmp", "tmpl") dtfr$tmp <- as.POSIXct(dtfr$tmp, format="%H:%M:%S")- as.POSIXct("00:00:00", format="%H:%M:%S") units(dtfr$tmp) <- "mins" dtfr$tmp <- as.numeric(dtfr$tmp) ## Prepara a variável sexo a partir de faixa etária. dtfr$sexo <- substr(dtfr$fxet, 1, 1) dtfr$sexo <- factor(dtfr$sexo, levels=c("M", "F")) ## Reescreve os níveis sem presença do sexo. dtfr$fxet <- paste0("[", substr(dtfr$fxet, 2, 3), ", ", substr(dtfr$fxet, 4, 5), "]") ## Cria um fator baseado na idade. dtfr$eq <- equal.count(dtfr$idade, number=9, overlap=0.1) ## Diagrama de dispersão do tempo de prova contra a idade. xyplot(tmp~idade|sexo, data=dtfr, layout=c(1, NA), ylab="Tempo para concluir a prova (min)", xlab="Idade do atleta (anos)") |
1 2 3 4 |
densityplot(~tmp|eq, data=dtfr, as.table=TRUE, groups=sexo, strip=strip.custom(strip.levels=TRUE, strip.names=FALSE), xlab="Tempo para concluir a prova (min)", ylab="Densidade") |
1 2 3 4 5 |
ecdfplot(~tmp|eq, data=dtfr, as.table=TRUE, strip=strip.custom(strip.levels=TRUE, strip.names=FALSE), groups=sexo, auto.key=TRUE, xlab="Tempo para concluir a prova (min)", ylab="Distribuição de frequências relativas acumulada") |
Você pode adaptar esse script (webScrapSaoSilvestre.R) para os
resultados de outros anos da São Silvestre. No entanto, se você visitar
os outros links, vai perceber que os resultados em html começaram em
2007 e que mantiveram o padrão estrutural. Antes de 2007, eles eram
divulgados em formato texto de comprimento fixo e cada ano tinha uma
estrutura diferente. Para ler os últimos, você pode usar a read.fwf()
.