Nem todo site oferece uma API. Quando o dado que você precisa está visível na tela mas não existe endpoint público para buscá-lo, web scraping vira a alternativa: um programa abre a página, lê o HTML e extrai só o que interessa. Em Python, a combinação mais direta para isso são três bibliotecas — requests para baixar a página, BeautifulSoup para navegar no HTML e pandas para salvar o resultado. Este guia percorre os cinco passos do fluxo completo, com código que roda, os erros que aparecem na prática e os limites legais que você precisa respeitar antes de apertar o play.
Antes de começar
Você vai precisar de Python 3.9 ou superior instalado e de familiaridade básica com HTML — o suficiente para reconhecer uma tag, um atributo class e um id. Não é necessário saber JavaScript nem CSS a fundo, mas saber abrir o inspetor do navegador ajuda muito, e é justamente o que faremos no passo 2.
Trabalhe sempre dentro de um ambiente virtual (python -m venv .venv). Isso evita que as dependências deste projeto conflitem com outros projetos na sua máquina.
Passo 1: instale as bibliotecas
São três pacotes. O requests faz as requisições HTTP, o beautifulsoup4 interpreta o HTML e o pandas cuida da parte final, de organizar e exportar os dados.
python -m venv .venv
source .venv/bin/activate # no Windows: .venv\Scripts\activate
pip install requests beautifulsoup4 pandasRepare que o nome do pacote é beautifulsoup4, com o número, mas na hora de importar no código ele vira bs4. É uma das confusões mais comuns de quem está começando.
Passo 2: leia o HTML antes de escrever qualquer código
Esse passo não tem código, e é o que mais economiza tempo. Abra a página no navegador, clique com o botão direito sobre o dado que você quer extrair e escolha "Inspecionar". O inspetor vai destacar exatamente a tag que contém aquele conteúdo.
O que você procura é um padrão: normalmente cada item de uma listagem está dentro de um mesmo tipo de container, com a mesma classe CSS. Se cada produto de um catálogo está em uma <div class="produto-card">, essa classe é a sua âncora — é por ela que o BeautifulSoup vai encontrar todos os itens de uma vez. Anote a tag e a classe antes de continuar.
Se o conteúdo aparece no navegador mas não no HTML retornado pelo requests, a página provavelmente é renderizada por JavaScript. Nesse caso requests e BeautifulSoup não bastam — você precisaria de uma ferramenta que execute JavaScript, como Playwright ou Selenium.
Passo 3: faça a requisição com tratamento de erro
Uma requisição sem timeout e sem verificação de status é a receita para um script que trava ou que processa silenciosamente uma página de erro achando que é a página certa.
import requests
from bs4 import BeautifulSoup
URL = "https://exemplo.com/produtos"
HEADERS = {
"User-Agent": "Mozilla/5.0 (compatible; MeuScraper/1.0; +https://meusite.com/bot)"
}
try:
resposta = requests.get(URL, headers=HEADERS, timeout=10)
resposta.raise_for_status()
except requests.exceptions.Timeout:
print("A requisicao demorou demais.")
raise
except requests.exceptions.HTTPError as erro:
print(f"Servidor respondeu com erro: {erro}")
raise
soup = BeautifulSoup(resposta.text, "html.parser")Três detalhes importam aqui. O timeout=10 impede que o script fique pendurado indefinidamente se o servidor não responder. O raise_for_status() transforma respostas 4xx e 5xx em exceção, em vez de deixar você processar uma página de erro como se fosse conteúdo válido. E o User-Agent identifica quem está fazendo a requisição — muitos servidores rejeitam o agente padrão do requests, e um identificador honesto, com forma de contato, é considerado boa prática.
Passo 4: extraia os dados
O BeautifulSoup tem dois métodos centrais: find() devolve a primeira ocorrência que casar com o critério, e find_all() devolve uma lista com todas. Como class é palavra reservada em Python, o parâmetro se chama class_, com underscore no final.
produtos = []
for card in soup.find_all("div", class_="produto-card"):
titulo = card.find("h2", class_="titulo")
preco = card.find("span", class_="preco")
link = card.find("a", href=True)
produtos.append({
"titulo": titulo.get_text(strip=True) if titulo else None,
"preco": preco.get_text(strip=True) if preco else None,
"url": link["href"] if link else None,
})
print(f"{len(produtos)} produtos encontrados")A verificação if titulo else None não é preciosismo. Quando um único card da listagem vem sem preço — porque o produto está esgotado, por exemplo — find() devolve None, e chamar .get_text() em None derruba o script inteiro no meio da execução. Verificar antes de acessar é o que separa um scraper que roda uma vez de um que roda todo dia.
O get_text(strip=True) remove espaços e quebras de linha que o HTML costuma deixar em volta do texto. Sem ele você acaba com valores cheios de \n e espaçamento irregular no CSV final.
Passo 5: salve em CSV
Com a lista de dicionários pronta, o pandas resolve a exportação em duas linhas.
import pandas as pd
df = pd.DataFrame(produtos)
df.to_csv("produtos.csv", index=False, encoding="utf-8-sig")
print(df.head())O index=False evita uma coluna extra com a numeração das linhas, que quase nunca é útil. Já o encoding="utf-8-sig" é o detalhe que resolve um problema clássico no Brasil: sem ele, o Excel abre o arquivo e mostra acentuação quebrada, com "informação" virando algo ilegível. O sufixo sig adiciona um marcador no início do arquivo que faz o Excel reconhecer o UTF-8 corretamente.
Coletando várias páginas
Listagens costumam ser paginadas. O padrão é montar a URL com o número da página e percorrer em loop — sempre com uma pausa entre as requisições.
import time
todos = []
for pagina in range(1, 11):
url = f"https://exemplo.com/produtos?page={pagina}"
resposta = requests.get(url, headers=HEADERS, timeout=10)
if resposta.status_code != 200:
print(f"Parando na pagina {pagina}: status {resposta.status_code}")
break
soup = BeautifulSoup(resposta.text, "html.parser")
cards = soup.find_all("div", class_="produto-card")
if not cards:
print(f"Pagina {pagina} vazia, encerrando.")
break
todos.extend(cards)
time.sleep(2) # respeite o servidorO time.sleep(2) entre requisições não é opcional na prática. Disparar centenas de requisições em sequência sem pausa sobrecarrega o servidor do outro lado, e é a forma mais rápida de ter seu IP bloqueado — além de ser, simplesmente, má educação técnica. Dois segundos é um ponto de partida razoável; sites menores merecem intervalos maiores.
As duas condições de parada também importam: verificar o status e verificar se a página voltou vazia evita que o loop continue rodando contra páginas inexistentes depois que a listagem acabou.
Erros comuns
- AttributeError: 'NoneType' object has no attribute 'get_text' — o find() não encontrou o elemento. Verifique se a classe mudou ou se o item específico não tem aquele campo.
- Status 403 ou 429 — o servidor está recusando ou limitando suas requisições. Aumente o intervalo entre elas e revise o User-Agent.
- O HTML baixado não contém o conteúdo que você vê no navegador — a página usa JavaScript para renderizar. requests não executa JavaScript.
- Acentuação quebrada no CSV — use encoding="utf-8-sig" na exportação.
- O scraper funcionava e parou do nada — sites mudam de layout. Scrapers são frágeis por natureza e precisam de manutenção.
O lado legal: o que você precisa checar
Scraping não é ilegal por si só, mas isso não significa que qualquer coleta seja permitida. Três pontos merecem atenção antes de rodar um scraper contra um site que não é seu.
O primeiro é o arquivo robots.txt, disponível na raiz de praticamente qualquer site (exemplo.com/robots.txt). Ele indica quais caminhos o site pede que robôs não acessem. Tecnicamente é uma convenção, não um mecanismo de bloqueio — mas ignorá-lo deliberadamente enfraquece bastante qualquer argumento de boa-fé.
O segundo são os Termos de Uso do site. Muitos proíbem expressamente coleta automatizada. Descumprir os termos é uma questão contratual, e plataformas grandes costumam levar isso a sério — redes sociais, em particular, historicamente agem contra scraping em massa de seus dados.
O terceiro ponto é o mais sensível no Brasil: se os dados coletados incluem informação sobre pessoas identificáveis — nome, e-mail, telefone, CPF —, você entra no escopo da LGPD (Lei 13.709/2018). Isso vale mesmo que o dado esteja publicamente visível: dado público não é dado livre. Coletar exige base legal, finalidade definida e cuidado no armazenamento. Em caso de dúvida, consulte um profissional da área jurídica — este texto é informativo e não substitui orientação legal.
Uma regra prática que evita a maior parte dos problemas: colete apenas dados agregados ou não pessoais, respeite o robots.txt, mantenha um intervalo generoso entre requisições e identifique seu bot com uma forma de contato. Se o volume que você precisa é grande a ponto de preocupar o servidor do outro lado, o caminho certo provavelmente não é scraping.
Quando não usar scraping
Se o site oferece uma API pública, use a API. Ela é mais estável — não quebra quando mudam o CSS —, costuma ser mais rápida, devolve dados já estruturados e deixa explícito que aquele uso é autorizado. Scraping é a solução para quando não existe alternativa, não a primeira opção.
Vale também considerar o custo de manutenção. Um scraper é código acoplado ao layout de um site que você não controla e que pode mudar sem aviso. Se o dado é crítico para o seu produto, depender de scraping significa aceitar que ele pode parar de funcionar em qualquer terça-feira. Para muitos casos isso é aceitável; para outros, vale procurar a fonte oficial ou negociar acesso direto.
Perguntas frequentes
Não existe lei que proíba scraping em si, mas a atividade esbarra em outras normas dependendo do que é coletado e como. Se os dados incluem informação de pessoas identificáveis, a LGPD se aplica mesmo que o dado esteja público. Além disso, os Termos de Uso do site podem proibir coleta automatizada, o que é uma questão contratual. Avalie caso a caso e busque orientação jurídica quando o uso for comercial ou envolver dados pessoais.