Raspagem de dados

Raspagem de dados (data scraping), em seu sentido mais amplo, é um conceito aplicado à obtenção de dados de um certo programa a partir de outro de forma a extrair conteúdo de alto valor que sejam, prioritariamente, de fácil interpretação a humanos. Atualmente, a raspagem de dados é um sinônimo para raspagem da web (web scraping), visto que a fonte mais ampla para coleta dados é a web. Então, o ato de “raspar” dados da web equivale a utilizar scripts, programas ou APIs para obter dados relevantes de sites, páginas, blogs, repositórios, ou qualquer outro lugar acessível por conectividade e requisição.

Através da raspagem de dados, podemos, entre outras coisas,

  • coletar preços de ativos do mercado financeiro em tempo real;

  • baixar históricos de sinistros em saúde pública, tais como os registros de casos durante a pandemia da Covid-19;

  • localizar matérias jornalísticas sobre um mesmo tema em diversos canais de comunicação;

  • encontrar o placar final de todos os jogos da NBA nos últimos 5 anos.

Durante o curso, você já lidou implicitamente com a raspagem de dados ao utilizar, por exemplo, a função pandas.read_csv, por exemplo, para realizar a leitura de um arquivo CSV hospedado em um site da internet. Neste capítulo, faremos uma breve introdução à raspagem de dados usando BeautifulSoap, um dos módulos Python mais populares para dissecar páginas da web, de maneira a ampliar a nossa compreensão acerca dessa área de conhecimento extremamente relevante para a ciência de dados.

HTML 5

A maioria das páginas da internet hoje são escritas em uma linguagem chamada HTML (HyperText Markup Language), desenvolvida no início da década de 1990 como a linguagem básica da internet. O consórcio W3C (The World Wide Web Consortium) é quem assegura os padrões abertos para desenvolvedores web. Desde 2014, a versão HTML 5 é a recomendada pela W3C para todos os criadores de sites. Em 2019, a W3C e a WHATWG assinaram um acordo para uniformizar o HTML, concluindo um documento de especificações.

Note

Para saber mais sobre a história do HTML e do legado de Tim Berners-Lee, o inventor da World Wide Web (WWW), leia este texto.

Um documento HTML é estruturado por meio de elementos enclausurados por um par de tags e tem a seguinte aparência:

<!DOCTYPE html>
<html lang="pt-br">
 <head>
  <title>Introdução à Ciência de Dados</title>
 </head>
 <body>
  <h1>Ciência de Dados no século XXI</h1>
  <p>A História começa neste <a href="historia-icd.html">link</a>.</p>
  <!-- comentário -->
 </body>
</html>

No exemplo, <head> e </head> são exemplo de tags de abertura e fechamento para a seção head do documento.

A árvore DOM

Os navegadores da web interpretam o código HTML e transformam em uma “árvore”. Esta árvore caracteriza o modelo de objetos do documento, ou, formalmente, DOM (Document Object Model). Na forma de árvore DOM, o código acima tornar-se-ia algo como:

|- DOCTYPE: html
|- html lang="pt-br"
   |- head
   |  |- #text:
   |  |- title
   |  |  |- #text: Introdução à Ciência de Dados
   |  |- #text:
   |- #text:
   |- body
      |- #text:
      |- h1
      |  |- #text: Ciência de Dados no século XXI
      |- #text:
      |- p
      |  |- #text: A História começa neste 
      |  |- a href="historia-icd.html"
      |     | #text: link
      |  |- #text:
      |- #comment: comentário
      |- #text:

Tags

Existem diversas tags disponíveis em HTML. A seguir listamos as que aparecem no código de exemplo anterior e sua descrição.

Tag

Descrição

<!DOCTYPE>

define o tipo do documento

<html>

define a raiz de um documento HTML

<head>

enclausura os metadados (informações) sobre o documento

<title>

define um título para o documento

<body>

define o corpo do documento

<h1>

define cabeçalho de primeiro nível (seção)

<p>

define um parágrafo

<a>

define um hyperlink (ancoramento)

Note

Um documento de referência para tags HTML está disponível aqui.

Escrevendo HTML no Jupyter Notebook

Podemos escrever sintaxes de código HTML em um Jupyter Notebook e renderizar a saída formatada usando o comando mágico %%html ou IPython.display. Por exemplo:

%%html
<!-- Nada aqui -->
<h2>Brincando com <sup>H</sup><sub>T</sub><sup>M</sup>L!</h2>
<p> Muito legal adicionar <b>negrito</b>, 
<i>itálico</i> emojis como <span> &#128540; </span> e 
um <a href="none.html">link</a> para lugar algum!</p>

Brincando com HTML!

Muito legal adicionar negrito, itálico emojis como 😜 e um link para lugar algum!

from IPython.display import HTML

HTML("<!-- Nada aqui --> \
<h2>Brincando com <sup>H</sup><sub>T</sub><sup>M</sup>L!</h2> \
<p> Muito legal adicionar <b>negrito</b>, \
<i>itálico</i> emojis como <span> &#128540; </span> e \
um <a href='none.html'>link</a> para lugar algum!</p>")

Brincando com HTML!

Muito legal adicionar negrito, itálico emojis como 😜 e um link para lugar algum!

APIs

A raspagem de dados pode ser otimizada através de uma API (Application Program Interface). APIs são mecanismos (interfaces) que usam aplicativos de terceiros para realizar “conexões” e puxar dados. APIs são parecidas com módulos, mas não oferecem meramente um conjunto de funções, mas sim um programa capaz de operar com muitos dados. Embora uma API possa funcionar localmente (offline), sua utilidade para raspagem de dados é melhor exibida quando se conecta a aplicativos da web (online).

Diversas instuições fornecem APIs para que desenvolvedores possam coletar dados. No início deste capítulo, mencionamos algumas aplicações de raspagem de dados. Algumas são possíveis apenas por meio de APIs. Google, Facebook, Twitter, Yahoo e Elsevier são algumas das empresas que fornecem APIs para aplicações em buscas na web, redes sociais, finanças e literatura científica. No Brasil, podemos citar como exemplos relevantes

Note

Veja uma lista de APIs públicas neste site.

Métodos HTTP

Na web, em geral lidamos com o HTTP (HyperText Transfer Protocol), isto é, um protocolo de comunicação entre clientes e servidores. Quando raspamos dados, eles são transferidos por meio de requisições (requests) e respostas. Em geral, os quatro métodos mais comuns para transitar informações entre browsers e servidores web via HTTP são os seguintes:

  • GET, para recuperar informação;

  • POST, para criar informação;

  • PUT, para atualizar informação;

  • DELETE, para deletar informação;

Note

Um padrão de projeto que pode ser usado para criar APIs web e integrar esses 4 métodos é o REST,Representational State Transfer. Saiba mais aqui.

JSON e XML

APIs utilizadas para raspagem de dados comumente retornam a informação em formato XML (eXtensible Markup Language), blocada em tags ou JSON (JavaScript Object Notation), serializada. Embora JSON seja a escolha de APIs mais modernas, é importante ter em mente que muitos provedores de APIs as fornecem com saída XML.

Um dos argumentos em favor de JSON é a economia de caracteres. Por exemplo, a estrutura XML

<user><firstname>Juan</firstname><lastname>Hernandes</lastname><username>Fernandez</username></user>

possui 100 caracteres.

A mesma informação, serializada em JSON,

{"user":{"firstname":"Juan","lastname":"Hernandes","username":"Fernandez"}}

com 75 caracteres, pouparia 25% do espaço.

Web crawlers

Rastreadores da web (web crawlers) são programas que indexam informações da rede a partir de várias fontes. Pelo fato de se comportarem como “espreitadores metódicos”, eles também são conhecidos como bots, aranhas ou escutadores da rede. Eles trabalham de uma forma recursiva ad infinitum puxando conteúdo de páginas e examinando-os.

Crawlers são úteis para coleta de dados, porém baseiam-se em termos de serviço e conduta. Todo site público possui, de alguma forma, termos de serviço geridos por um administrador que declaram o que é permitido ao crawler fazer ou não. Essas permissões (allows) ou restrições (disallows) estão expostas em arquivo chamado robots.txt. Qualquer site relevante possui um arquivo deste associado à sua URL. Para vê-lo, basta adicionar este nome após a URL do endereço. As restrições sobre crawlers esbarram na fronteira da ética, principalmente no que diz respeito à raspagem de dados. Contudo, não discutiremos essas questões aqui.

Abaixo, mostramos o arquivo robots.txt para o site da UFPB.

Para outros exemplos, veja:

Bibliotecas para raspagem de dados

Existem muitos ferramentas, bibliotecas e frameworks para raspagem de dados. Alguns exemplos são: _requests, grab, scrapy, restkit, lxml, PDFMiner. Neste capítulo, vamos dar enfoque ao módulo Python BeautifulSoap e seus interpretadores (parsers).

Vantagens de BeautifulSoup

Segundo o site oficial, a BeautifulSoup

  • fornece métodos simples e expressões idiomáticas “Pythônicas” para navegar, pesquisar e modificar uma árvore de análise: um kit de ferramentas para dissecar um documento e extrair o que você precisa.

  • converte automaticamente os documentos recebidos em Unicode e os documentos enviados em UTF-8. Você não tem que pensar em codificações, a menos que o documento não especifique uma codificação e a Beautiful Soup não consiga detectar uma.

  • baseia-se em interpretadores (parsers) Python populares como lxml e html5lib, permitindo que você experimente diferentes estratégias de análise para obter flexibilidade.

Note

A biblioteca BeautifulSoup foi assim denominada em homenagem a um poema de Lewis Carroll de mesmo nome em Alice no País das Maravilhas, em alusão à Sopa de Tartaruga.

Raspando dados do site da UFPB

Neste exemplo, faremos uma raspagem no site da UFPB para coletar a lista de cursos de graduação. Os passos a serem seguidos são:

  1. abrir uma requisição para a URL da PRG/UFPB;

  2. coletar o HTML da página;

  3. extrair o conteúdo da tabela de cursos na árvore DOM;

  4. construir um DataFrame cujas colunas devem conter: nome do curso, sede, modalidade, nome do(a) coordenador(a) e Centro de Ensino que administra o curso.

Entretanto, antes de produzirmos o DataFrame, faremos uma breve explanação sobre outras funcionalidades do módulo BeautifulSoap.

Primeiramente, abriremos uma requisição com urllib.request.

from urllib.request import urlopen
from bs4 import BeautifulSoup

html = urlopen('https://sigaa.ufpb.br/sigaa/public/curso/lista.jsf?nivel=G&aba=p-graduacao')
bs = BeautifulSoup(html.read(),'html.parser')

Neste ponto, criamos o objeto bs que contém a árvore DOM do documento HTML. Podemos acessar as partes do documento diretamente a partir das tags head, body, title etc.

# impressão na tela de head e body
# omitidas por serem grandes. 
# Teste em seu computador!
head = bs.head
body = bs.body
title = bs.title
title
<title> SIGAA- Sistema Integrado de Gestão de Atividades Acadêmicas </title>

Podemos puxar o conteúdo das tags com contents.

# é uma lista
title.contents
[' SIGAA- Sistema Integrado de Gestão de Atividades Acadêmicas ']
# opera sobre str
title.contents[0].strip()
'SIGAA- Sistema Integrado de Gestão de Atividades Acadêmicas'

Podemos navegar na árvore por meio das tags.

head.link
<link class="component" href="/sigaa/a4j/s/3_3_3.Finalorg/richfaces/renderkit/html/css/basic_classes.xcss/DATB/eAF7sqpgb-jyGdIAFrMEaw__.jsf;jsessionid=57EEFAB5ED6AB35AB2C8CBCF201B3551" rel="stylesheet" type="text/css"/>
head.meta
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
body.li
<li><a class="sr-only sr-only-focusable" href="#conteudo">Pular para conteúdo</a></li>
body.span
<span class="icone-contraste"></span>

Navegação na árvore para baixo

Inversamente, podemos acessar elementos “pai” (ou “mãe”) a partir dos filhos com parent.

list(head.link.parent)[:4]
['\n',
 <meta content="IE=edge" http-equiv="X-UA-Compatible"/>,
 '\n',
 <meta content="width=device-width, initial-scale=1" name="viewport"/>]

Para iterar sobre os elementos “pai” (ou “mãe”), use parents.

for h in head.link.parents:
    print(type(h))
<class 'bs4.element.Tag'>
<class 'bs4.element.Tag'>
<class 'bs4.BeautifulSoup'>

Realizando buscas na árvore

Funções de localização bastante úteis são find_all e find. Podemos aplicá-la passando como argumento uma tag

body.find_all('td')[10:15]
[<td>Presencial</td>,
 <td>SILDIVANE VALCACIA SILVA</td>,
 <td>
 <a href="portal.jsf?id=1626776&amp;lc=pt_BR" title="Visualizar Página do Curso">
 <img src="/sigaa/img/view.gif;jsessionid=57EEFAB5ED6AB35AB2C8CBCF201B3551"/>
 </a>
 </td>,
 <td class="subFormulario" colspan="5">
 										CCA - CENTRO DE CIÊNCIAS AGRÁRIAS (CCA)
 										</td>,
 <td>
 							AGRONOMIA								
 							</td>]

uma lista de tags

body.find_all(['li','ul'])
[<ul id="menu-acessibilidade">
 <li><a class="sr-only sr-only-focusable" href="#conteudo">Pular para conteúdo</a></li>
 <li class="separador"></li>
 <li><a class="toggleable" href="#" id="acessibilidade-contraste" title="Contraste"><span class="icone-contraste"></span><span class="sr-only">Auto Contraste</span></a></li>
 <li class="separador"></li>
 <li>
 <a href="/sigaa/logon/" style="font-size: 15px; font-weight: bold;" title="Acessar o SIGAA">Entrar</a>
 </li>
 </ul>,
 <li><a class="sr-only sr-only-focusable" href="#conteudo">Pular para conteúdo</a></li>,
 <li class="separador"></li>,
 <li><a class="toggleable" href="#" id="acessibilidade-contraste" title="Contraste"><span class="icone-contraste"></span><span class="sr-only">Auto Contraste</span></a></li>,
 <li class="separador"></li>,
 <li>
 <a href="/sigaa/logon/" style="font-size: 15px; font-weight: bold;" title="Acessar o SIGAA">Entrar</a>
 </li>,
 <ul>
 </ul>]

ou tag e classe.

Note

Classes dizem respeito ao estilo de formatação do arquivo HTML, que segue regras da linguagem CSS.

# busca <table data> com classe "subFormulario"
body.find_all('td',class_="subFormulario")[:3]
[<td class="subFormulario" colspan="5">
 										CBIOTEC - CENTRO DE BIOTECNOLOGIA
 										</td>,
 <td class="subFormulario" colspan="5">
 										CCA - CENTRO DE CIÊNCIAS AGRÁRIAS (CCA)
 										</td>,
 <td class="subFormulario" colspan="5">
 										CCAE - CENTRO DE CIÊNCIAS APLICADAS E EDUCAÇÃO (CCAE)
 										</td>]

Podemos também realizar buscas específicas por expressões regulares. Para isso, basta usar o módulo re e funções como re.compile.

import re

body.find_all(string=re.compile('GRAD'))
['Consulta de Cursos -\n\tGRADUAÇÃO',
 'Através desta página você pode consultar os cursos de\n\tGRADUAÇÃO ofereridos pela UFPB. ']
body.find_all(string=re.compile('CENTRO'))
['\n\t\t\t\t\t\t\t\t\t\tCBIOTEC - CENTRO DE BIOTECNOLOGIA\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCCA - CENTRO DE CIÊNCIAS AGRÁRIAS (CCA)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCCAE - CENTRO DE CIÊNCIAS APLICADAS E EDUCAÇÃO (CCAE)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCCS - CENTRO DE CIÊNCIAS DA SAÚDE (CCS)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCCEN - CENTRO DE CIÊNCIAS EXATAS E DA NATUREZA (CCEN)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCCHLA. - CENTRO DE CIÊNCIAS HUMANAS, LETRAS E ARTES (CCHLA)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCCHSA - CENTRO DE CIÊNCIAS HUMANAS SOCIAIS E AGRÁRIAS (CCHSA)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCCJ - CENTRO DE CIÊNCIAS JURÍDICAS (CCJ)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCCM - CENTRO DE CIENCIAS MEDICAS (CCM)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCCSA - CENTRO DE CIÊNCIAS SOCIAIS E APLICADAS (CCSA)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCCTA - CENTRO DE COMUNICAÇÃO, TURISMO E ARTES (CCTA)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCE - CENTRO DE EDUCAÇÃO (CE)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCEAR - CENTRO DE ENERGIAS E ALTERNATIVAS E RENOVÁVEIS (CEAR)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCI - CENTRO DE INFORMÁTICA (CI)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCT - CENTRO DE TECNOLOGIA (CT)\n\t\t\t\t\t\t\t\t\t\t',
 '\n\t\t\t\t\t\t\t\t\t\tCTDR - CENTRO DE TECNOLOGIA E DESENVOLVIMENTO REGIONAL (CTDR)\n\t\t\t\t\t\t\t\t\t\t']

Se o resultado a ser localizado for único, podemos usar find.

body.find(string=re.compile('Copyright'))
'\n\t\t\tSIGAA - 21.11.0 -| STI - Superintendência de Tecnologia da Informação | Copyright © 2006-2021 - UFPB\n\t\t'

Funções customizadas

Agora, implementaremos algumas funções customizadas para extrair o cabeçalho e o conteúdo da tabela de cursos do site da UFPB. Essas funções varrem a árvore DOM e coletam apenas as informações de interesse, transformando-as para listas.

# extrai cabeçalhos
def get_table_head(t):
    '''Lê objeto tabela e extrai header para lista'''
    res = []
    thead = t.find('thead')
    th = thead.find_all('th')
    for f in th:
        res.append(f.getText().strip())
    return res

t_header = get_table_head(body.div)

t_header
['Nome', 'Sede', 'Modalidade', 'Coordenador', '']
# extrai linhas
def get_table_body(t):
    res = []
    tbody = t.find('tbody')
    tr = tbody.find_all('tr')
    for row in tr:
        this_row = []
        row_fields = row.find_all('td')
        for f in row_fields:
            this_row.append(f.getText().strip())
            res.append(this_row)
    return res

r = get_table_body(body.div)  

Limpeza dos dados extraídos

Finalmente, construiremos o DataFrame a partir das listas anteriores. Porém, precisamos realizar procedimento de preenchimento de dados e limpeza.

Note que a tabela original não traz os nomes dos cursos organizados por linha. Então, precisamos de uma coluna com o nome de cada Centro de Ensino organizado por curso.

import pandas as pd

# cria DataFrame
df = pd.DataFrame(r,columns=t_header).drop_duplicates().reset_index(drop=True)
mask = df['Nome'].str.find('CENTRO') != -1
centros = df['Nome'].loc[mask]
idx = centros.index.values

# preenchimento
vals = []
for k in range(1,len(idx)):
    for i in range(max(idx)+1):
        if i >= idx[k-1] and i < idx[k]:
            vals.append(centros.iloc[k-1])
# extra
dx = len(df) - max(idx)
vals.extend(dx*[vals[-1]])


# limpa e renomeia
df['Centro'] = vals
df = df.drop(idx).rename(columns={"Nome": "Curso"}).reset_index(drop=True)
df
Curso Sede Modalidade Coordenador Centro
0 BIOTECNOLOGIA João Pessoa Presencial SILDIVANE VALCACIA SILVA CBIOTEC - CENTRO DE BIOTECNOLOGIA
1 AGRONOMIA Areia Presencial FABIO MIELEZRSKI CCA - CENTRO DE CIÊNCIAS AGRÁRIAS (CCA)
2 CIÊNCIAS BIOLÓGICAS- AREIA Areia Presencial LAIS ANGELICA DE ANDRADE PINHEIRO BORGES CCA - CENTRO DE CIÊNCIAS AGRÁRIAS (CCA)
3 MEDICINA VETERINÁRIA Areia Presencial GISELE DE CASTRO MENEZES CCA - CENTRO DE CIÊNCIAS AGRÁRIAS (CCA)
4 QUÍMICA Areia Presencial DAYSE DAS NEVES MOREIRA CCA - CENTRO DE CIÊNCIAS AGRÁRIAS (CCA)
... ... ... ... ... ... ...
115 QUÍMICA INDUSTRIAL João Pessoa Presencial RAUL ROSENHAIM CT - CENTRO DE TECNOLOGIA (CT)
116 ADMINISTRAÇÃO PÚBLICA João Pessoa A Distância JULIANA FERNANDES MOREIRA CT - CENTRO DE TECNOLOGIA (CT)
117 GASTRONOMIA João Pessoa Presencial SAMARA DE MACÊDO MORAIS CT - CENTRO DE TECNOLOGIA (CT)
118 TECNOLOGIA DE ALIMENTOS João Pessoa Presencial ISMAEL IVAN ROCKENBACH CT - CENTRO DE TECNOLOGIA (CT)
119 TECNOLOGIA EM PRODUÇÃO SUCROALCOOLEIRA João Pessoa Presencial SOLANGE MARIA DE VASCONCELOS CT - CENTRO DE TECNOLOGIA (CT)

120 rows × 6 columns