Curso

Você encontra um exemplo finalizado e funcionando do script que vamos escrever aqui.
O que é web scraping?
Certo, então o que exatamente é web scraping? Como o nome sugere, é um método de "raspar" ou extrair dados de páginas da web. Tudo o que você vê na internet pelo navegador, incluindo este tutorial, pode ser coletado para o seu disco local.
Há muitos usos para web scraping. Em qualquer análise de dados, o primeiro passo é a aquisição de dados. A internet é um repositório imenso da história e do conhecimento da humanidade, e você tem meios de extrair o que quiser e usar essas informações como preferir.
Neste tutorial, vamos usar Python e o pacote BeautifulSoup 4 para obter informações de um subreddit. Nosso foco é o subreddit datascience. Queremos coletar os primeiros 1000 posts do subreddit e exportá-los para um arquivo CSV. Também queremos saber quem postou, além de quantos likes e comentários cada post tem.
O que vamos abordar no tutorial:
- Obter páginas da web usando
requests - Analisar páginas no navegador em busca de informações
- Extrair informações do HTML bruto com BeautifulSoup
Observação: vamos usar a versão antiga do site do Reddit porque é mais leve para carregar e, portanto, menos pesada para a sua máquina.
Pré-requisitos
Este tutorial parte do pressuposto de que você sabe o seguinte:
- Executar scripts Python no seu computador
- Noções básicas da estrutura de HTML
Você pode aprender as habilidades acima no curso para iniciantes em Python da DataCamp. Dito isso, os conceitos usados aqui são bem básicos e dá para acompanhar com pouquíssimo conhecimento prévio de Python.
Agora que resolvemos isso, podemos avançar para a primeira parte de criar nosso scraper. Na verdade, a primeira parte de qualquer script Python: imports.
No nosso scraper, vamos usar os seguintes pacotes:
requestsbeautifulsoup4
Você pode instalar esses pacotes com pip, assim:
pip install package_name
Depois de baixar os pacotes, importe-os no seu código.
import requests
import csv
import time
from bs4 import BeautifulSoup4
Usaremos o módulo nativo csv do Python para gravar os resultados em um arquivo CSV. Você pode ter notado algo curioso no trecho acima. Baixamos um pacote chamado beautifulsoup4, mas importamos de um módulo chamado bs4. Isso é permitido em Python e, embora muitos não recomendem por estilo, não é exatamente proibido.
Nota importante sobre políticas de plataforma
Antes de usar as técnicas de scraping que vou mostrar nos próximos passos, verifique os termos de uso atuais do Reddit ou de qualquer outro site que você planeje extrair. As políticas podem mudar com o tempo. O User Agreement do Reddit de setembro de 2024 proíbe scraping sem consentimento prévio por escrito (veja a seção 7). Sempre garanta que você está em conformidade com as diretrizes mais recentes do site antes de prosseguir.
Primeiros passos para rastrear o Reddit
Com o ambiente pronto, precisamos da URL da página que queremos raspar. Para este tutorial, usaremos o subreddit "datascience" do Reddit. Antes de começar a escrever o script, há um trabalho de campo a fazer. Abra um navegador e vá até o subreddit em questão.
Note que usaremos a versão antiga do subreddit no nosso scraper. A versão mais nova esconde algumas informações importantes na "parte de baixo" da página. É possível extrair isso do site novo também, mas, para simplificar, vamos usar a versão antiga, que deixa tudo mais explícito.
Ao abrir o link, você se depara com um excesso de informações. O que exatamente precisamos nisso tudo? Ao investigar os links, você verá que os posts do Reddit são de dois tipos:
- Inbound
- Outbound
Links inbound apontam para conteúdo no próprio Reddit; outbound é o oposto. Isso importa porque queremos apenas os links inbound. Posts que contêm texto escrito pelos usuários, não apenas links para outros sites.
Sabemos o que queremos; como extrair? Se você olhar o título dos posts, verá que eles são seguidos por um texto entre colchetes. Os posts do nosso interesse são seguidos por "(self.datascience)". Logicamente, podemos supor que "self" se refere ao diretório raiz do Reddit e ".datascience" ao subreddit.
Ótimo, temos uma forma de identificar quais posts são inbound e quais são outbound. Agora precisamos identificar isso no DOM. Dá para localizar esses identificadores no DOM pesquisando por tags, classes ou ids? Claro!
No navegador, clique com o botão direito em qualquer link "self.datascience" e selecione "inspecionar elemento". A maioria dos navegadores modernos tem essa ótima ferramenta que permite aos desenvolvedores percorrer as partes relevantes do código-fonte de forma dinâmica. No Safari, ative as ferramentas de desenvolvedor nas preferências. Você verá um painel rolado até a parte do código responsável pelo identificador "(self.datascience)".
No painel, você verá que o identificador é carregado por uma tag âncora.
<a href="/r/datascience">self.datascience</a>
Mas isso está dentro de uma tag span.
<span class="domain">
"("
<a href="/r/datascience">self.datascience</a>
")"
</span>
Essa tag span contém o texto que vemos na página, ou seja, "(self.datascience)". Ela está marcada com a classe "domain". Você pode conferir os outros links para ver se seguem o mesmo padrão e, de fato, seguem.
Obtendo a página com BeautifulSoup
Sabemos o que queremos na página, ótimo — mas como usar Python para ler o conteúdo? Funciona basicamente como um humano lendo no navegador.
Primeiro, precisamos solicitar a página usando a biblioteca requests.
url = "https://old.reddit.com/r/datascience"
# Headers para simular uma visita de navegador
headers = {'User-Agent': 'Mozilla/5.0'}
# Retorna um objeto requests.models.Response
page = requests.get(url, headers=headers)
Agora temos um objeto Response que contém o texto bruto da página. Por enquanto, não dá para fazer muito com isso. É uma string enorme com todo o código-fonte HTML. Para dar sentido a isso, precisamos do BeautifulSoup 4.
Os headers permitem simular uma visita de navegador. Como a resposta para um bot pode ser diferente da resposta para um navegador — e nosso referencial é o navegador — é melhor obter a resposta de navegador.
O BeautifulSoup nos permite encontrar tags específicas, pesquisando por qualquer combinação de classes, ids ou nomes de tag. Ele cria uma árvore de sintaxe, mas os detalhes não são relevantes aqui (e estão fora do escopo deste tutorial).
Então vamos criar essa árvore.
soup = BeautifulSoup(page.text, 'html.parser')
O soup é um objeto BeautifulSoup criado a partir do código-fonte em forma de string. Lembre-se de especificar o parser HTML, porque o BeautifulSoup também pode criar soup a partir de XML.
Encontrando nossas tags
Sabemos que tags queremos (os spans com a classe "domain") e temos o soup. O próximo passo é percorrer o soup e encontrar todas as instâncias dessas tags. Você pode até rir de como isso é simples com BeautifulSoup.
domains = soup.find_all("span", class_="domain")
O que fizemos? Chamamos o método find_all no objeto soup, que procura em si mesmo por tags âncora com os parâmetros passados como segundo argumento. Só passamos uma restrição (classe precisa ser "domain"), mas daria para combinar com outras coisas, ou até usar um id.
Usamos "class_=" porque "class" é uma palavra reservada do Python para definir classes e afins
Se você quiser passar mais de um parâmetro, basta tornar o segundo argumento um dicionário com os atributos desejados, assim:
soup.find_all("span", {"class": "domain", "height", "100px"})
Temos todas as tags span na lista "domains", mas o que queremos são os domínios "(self.datascience)".
for domain in domains:
if domain != "(self.datascience)":
continue
print(domain.text)
Agora você deve ver uma lista com os tipos de posts da página, excluindo os que não são "(self.datascience)". Basicamente, temos tudo o que queremos. Agora temos referências a todos os posts inbound.
Encontrando nossas informações
Legal estarmos imprimindo algumas linhas de "(self.datascience)", mas e agora? Volte ao nosso objetivo inicial: obter o título do post, autor, likes e número de comentários.
Para isso, voltamos ao navegador. Se você abrir o inspetor no nosso identificador novamente, verá que nosso span está dentro de uma div... que por sua vez está dentro de outra div, e assim por diante. Suba os níveis até chegar na div pai de todo o post.
<div class=" thing id-t3_8qccuv even link self" ...>
...
<div class="entry unvoted">
<div class="top-matter">
<p class="title">
...
<span class="domain">
...
</p>
</div>
</div>
</div>
Como você pode ver, o pai comum está 4 níveis acima na estrutura do DOM. As reticências representam ruído desnecessário. Agora precisamos de uma referência à div do post para cada item da nossa lista domains. Podemos encontrar o pai de qualquer elemento no soup usando os métodos do BeautifulSoup.
for domain in soup.find_all("span", class_="domain"):
if domain != "(self.datascience)":
continue
parent_div = domain.parent.parent.parent.parent
print(parent_div.text)
Executando o script, será impresso todo o texto dos posts inbound. Mas você pode achar que esse código está feio. E estaria certo. Nunca é seguro depender apenas da integridade estrutural do DOM. É um jeitinho que exige manutenção constante e é o equivalente a uma bomba-relógio no código.
Em vez disso, vamos olhar para a div pai de cada post e ver se conseguimos separar links inbound e outbound a partir do próprio pai. Cada div pai tem um atributo chamado "data-domain", cujo valor é exatamente o que queremos! Todos os posts inbound têm o data-domain definido como "self.datascience".
Como mencionamos, podemos buscar tags com uma combinação de atributos no BeautifulSoup. Por sorte, o Reddit classificou a div pai de cada post com "thing".
attrs = {'class': 'thing', 'data-domain': 'self.datascience'}
for post in soup.find_all('div', attrs=attrs):
print(post.attrs['data-domain'])
A variável "attrs" é um dicionário com os atributos e valores que queremos buscar. Essa abordagem é muito mais segura para encontrar os posts, pois envolve menos partes móveis. Como já estamos adicionando "self.datascience" como argumento na busca, não precisamos usar o if para pular iterações — estamos garantidos a receber apenas posts com o atributo data-domain "self.datascience".
Agora que temos os posts certos, falta apenas extrair as informações dos filhos. E isso é tão simples quanto parece.
Extraindo nossas informações do Reddit
Para cada post, precisamos de 4 informações:
- Título
- Autor
- Likes
- Comentários
Analisando a estrutura da div do post, encontramos tudo isso.
O título
Este é o mais simples. Em cada div do post, existe uma tag de parágrafo aninhada sob algumas divs. É particularmente fácil de achar porque tem a classe "title".
title = post.find('p', class_="title").text
O objeto post está dentro do nosso laço for de antes. Ele também é um objeto BeautifulSoup, mas contém apenas a estrutura do DOM dentro da div do post. O método "find" retorna um único objeto (diferente de "find_all", que retorna uma lista). Depois de encontrar a tag, queremos apenas a string do título, então usamos a propriedade text para lê-la. Também podemos ler outros atributos com "object.attrs['attribute']".
O autor
Também é simples: abra o inspetor no nome do autor abaixo do título. Você verá que o nome está em uma âncora com a classe "author".
author = post.find('a', class_='author').text
Os comentários
Exige um passinho extra, mas continua simples. Podemos encontrar os comentários do mesmo jeito que encontramos título e autor.
comments = post.find('a', class_='comments').text
Ao rodar, você verá algo como "49 comments". Ok, mas seria melhor obter apenas o número. Para isso, usamos um pouco mais de Python.
Se usarmos o str.split(), ele retorna uma lista com os elementos separados por espaço. No nosso caso, teremos ["49", "comments"]. Ótimo! Agora basta pegar o primeiro elemento e armazenar. É só encadear as funções na linha:
comments = post.find('a', class_='comments').text.split()[0]
Mas ainda não acabou, porque às vezes não vem um número; vem "comment". Isso acontece quando não há comentários no post. Como sabemos disso, basta verificar se o resultado é "comment" e substituir por "0".
if comments == "comment":
comments = 0
Os likes
Encontrar o número de likes é moleza e segue a mesma lógica.
likes = post.find("div", attrs={"class": "score likes"}).text
Aqui, você notará que usamos a combinação de duas classes. Isso porque existem várias outras divs com a classe "score" ou "likes", mas apenas uma com a combinação "score" e "likes".
Se o número de likes for 0, recebemos 0. Mas algo estranho acontece se o post for tão novo que ainda não tem likes: recebemos "•". Vamos trocar por "None" para não causar confusão no resultado final.
if likes == "•":
likes = "None"
Gravando nossos resultados em CSV
Até aqui, temos um loop que extrai título, autor, likes e comentários de cada post da página. O Python tem um ótimo módulo nativo para ler e escrever arquivos CSV chamado "csv" — no melhor estilo pythônico: simples. Tudo o que precisamos é adicionar uma linha ao final do bloco do loop para anexar os detalhes do post a um CSV.
counter = 1
for post in posts:
...
post_line = [counter, title, author, likes, comments]
with open('output.csv', 'a') as f:
writer = csv.writer(f)
writer.writerow(post_line)
counter += 1
Bem direto, né? A post_line cria uma lista com os elementos que serão separados por vírgula. A variável counter serve para acompanhar quantos posts já registramos.
Indo para a próxima página
Como estamos contando os posts salvos, basta colocar toda a nossa lógica dentro de outro loop que solicite novas páginas até atingir uma certa quantidade de posts.
counter = 1
while (counter <= 100):
for post in posts:
# Obtendo título, autor, ...
# Gravando no CSV e incrementando counter...
next_button = soup.find("span", class_="next-button")
next_page_link = next_button.find("a").attrs['href']
time.sleep(2)
page = requests.get(next_page_link, headers=headers)
soup = BeautifulSoup(page.text, 'html.parser')
Fizemos várias coisas aí, não? Vamos linha a linha. Primeiro, movemos a variável counter para cima, para que o while possa usá-la.
Depois, já sabemos o que o for faz, mas e o resto? A variável "next_button" encontra e guarda o botão "next". Em seguida, buscamos a âncora dentro dele e obtemos o atributo "href", que guardamos em "next_page_link".
Usamos esse link para solicitar a próxima página, salvar em "page" e criar um novo soup com o BeautifulSoup.
O trecho acima para após 100, mas você pode ajustar para qualquer quantidade de posts que quiser.
Fazendo scraping com responsabilidade
A linha de que não falamos acima é "time.sleep(2)". Ela merece uma seção própria. Qualquer servidor tem recursos finitos, então cabe a nós não sobrecarregá-lo. Além de poder gerar banimento fazer centenas de requisições em segundos, também não é legal.
É bom lembrar que é gentil da parte do site permitir que você faça scraping. Se quisessem, poderiam detectar bots nas primeiras 10 a 20 requisições, ou até te identificar pelo objeto de requisição que o Python envia. Já que eles permitem sem você precisar rotacionar IP, faça sua parte desacelerando o bot.
Você encontra a política de rastreamento de um site no arquivo robots.txt. Geralmente fica na raiz (ex.: http://www.reddit.com/robots.txt).
E depois?
Bem, o que você quiser. Como acabamos de mostrar, tudo o que você vê na web pode ser raspado e armazenado localmente. Dá para usar as informações que coletamos para vários fins. Só com as quatro peças de informação, já conseguimos tirar muitas conclusões.
Aprofundando a análise, podemos descobrir que tipos de posts recebem mais likes, quais palavras contêm, quem publica. Ou, ao contrário, dá para criar um "medidor de controvérsia" analisando a razão entre likes e comentários.
Considere um post que recebe muitos comentários, mas poucos likes. Provavelmente ele toca em algum ponto sensível, mas não necessariamente em algo que as pessoas curtam.
As possibilidades são muitas, e cabe a você decidir como usar a informação.
Se você curtiu este tutorial, confira também nosso guia sobre scraping da Amazon com Python e nossa coleção de cursos de Python.
