Web Scraping com Python: Tutorial Completo
Aprenda web scraping com Python usando requests e BeautifulSoup. Tutorial prático com exemplos reais de extração de dados de sites e boas práticas.
Web scraping é a técnica de extrair dados de páginas web automaticamente. Python é a linguagem mais popular para isso, graças a bibliotecas como requests e BeautifulSoup. Neste tutorial, você vai aprender desde o básico até projetos práticos completos.
Configuração Inicial
# Instalar as bibliotecas necessárias
# pip install requests beautifulsoup4 lxml
import requests
from bs4 import BeautifulSoup
Conceitos Básicos
Fazendo Requisições HTTP
import requests
# GET simples
response = requests.get("https://httpbin.org/get")
print(f"Status: {response.status_code}")
print(f"Tipo do conteúdo: {response.headers['content-type']}")
print(f"Tamanho: {len(response.text)} caracteres")
# Com headers personalizados (boa prática!)
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36",
"Accept-Language": "pt-BR,pt;q=0.9,en;q=0.8",
}
response = requests.get("https://httpbin.org/headers", headers=headers)
print(response.json())
# Com parâmetros de query
params = {"q": "python", "page": 1}
response = requests.get("https://httpbin.org/get", params=params)
print(f"URL final: {response.url}")
Entendendo HTML
Antes de fazer scraping, é essencial entender a estrutura básica do HTML:
html_exemplo = """
<html>
<head><title>Minha Página</title></head>
<body>
<h1 class="titulo">Produtos em Destaque</h1>
<div class="produto" id="prod-1">
<span class="nome">Notebook Dell</span>
<span class="preco">R$ 3.500,00</span>
<a href="/produto/1">Ver detalhes</a>
</div>
<div class="produto" id="prod-2">
<span class="nome">Mouse Logitech</span>
<span class="preco">R$ 89,90</span>
<a href="/produto/2">Ver detalhes</a>
</div>
</body>
</html>
"""
# Parseando com BeautifulSoup
soup = BeautifulSoup(html_exemplo, "lxml")
# Acessando elementos
titulo = soup.find("h1")
print(f"Título: {titulo.text}")
# Encontrar todos os produtos
produtos = soup.find_all("div", class_="produto")
for prod in produtos:
nome = prod.find("span", class_="nome").text
preco = prod.find("span", class_="preco").text
link = prod.find("a")["href"]
print(f" {nome} - {preco} ({link})")
Métodos de Busca do BeautifulSoup
from bs4 import BeautifulSoup
html = """
<html>
<body>
<div id="conteudo">
<h2 class="titulo destaque">Artigos Recentes</h2>
<ul>
<li class="artigo"><a href="/art/1">Python para Iniciantes</a></li>
<li class="artigo"><a href="/art/2">Django Tutorial</a></li>
<li class="artigo"><a href="/art/3">FastAPI na Prática</a></li>
</ul>
<p>Total: <span data-count="3">3 artigos</span></p>
</div>
<div id="sidebar">
<h3>Tags Populares</h3>
<span class="tag">python</span>
<span class="tag">django</span>
<span class="tag">flask</span>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html, "lxml")
# find() - primeiro elemento que corresponde
primeiro_artigo = soup.find("li", class_="artigo")
print(f"Primeiro: {primeiro_artigo.text}")
# find_all() - todos os elementos
todos_artigos = soup.find_all("li", class_="artigo")
print(f"Total de artigos: {len(todos_artigos)}")
# Busca por ID
conteudo = soup.find(id="conteudo")
print(f"Conteúdo: {conteudo.h2.text}")
# Busca por atributo
span_count = soup.find("span", attrs={"data-count": "3"})
print(f"Contagem: {span_count.text}")
# CSS Selectors (muito poderoso!)
tags = soup.select(".tag")
print(f"Tags: {[t.text for t in tags]}")
links = soup.select("#conteudo ul li a")
print(f"Links: {[a['href'] for a in links]}")
# select_one() - primeiro resultado do seletor CSS
titulo = soup.select_one("h2.titulo.destaque")
print(f"Título: {titulo.text}")
Projeto Prático: Scraping de Cotações
import requests
from bs4 import BeautifulSoup
from dataclasses import dataclass
from typing import Optional
import json
from datetime import datetime
@dataclass
class Cotacao:
moeda: str
nome: str
compra: float
venda: float
variacao: float
data: str
class ScraperCotacoes:
"""Extrai cotações de moedas usando API pública."""
BASE_URL = "https://economia.awesomeapi.com.br/json/last"
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "PythonDevBR-Scraper/1.0",
})
def buscar_cotacoes(self, moedas=None):
"""Busca cotações das moedas especificadas."""
if moedas is None:
moedas = ["USD-BRL", "EUR-BRL", "GBP-BRL", "BTC-BRL"]
moedas_str = ",".join(moedas)
url = f"{self.BASE_URL}/{moedas_str}"
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
dados = response.json()
cotacoes = []
for chave, info in dados.items():
cotacao = Cotacao(
moeda=f"{info['code']}/{info['codein']}",
nome=info["name"],
compra=float(info["bid"]),
venda=float(info["ask"]),
variacao=float(info["pctChange"]),
data=info["create_date"],
)
cotacoes.append(cotacao)
return cotacoes
except requests.RequestException as e:
print(f"Erro ao buscar cotações: {e}")
return []
def exibir_cotacoes(self, cotacoes):
"""Exibe cotações formatadas."""
print(f"\n{'=' * 60}")
print(f" COTAÇÕES - {datetime.now().strftime('%d/%m/%Y %H:%M')}")
print(f"{'=' * 60}")
for c in cotacoes:
seta = "+" if c.variacao >= 0 else ""
print(f"\n {c.moeda} ({c.nome})")
print(f" Compra: R$ {c.compra:,.4f}")
print(f" Venda: R$ {c.venda:,.4f}")
print(f" Variação: {seta}{c.variacao:.2f}%")
print(f"\n{'=' * 60}")
def salvar_json(self, cotacoes, arquivo="cotacoes.json"):
"""Salva cotações em arquivo JSON."""
dados = {
"data_consulta": datetime.now().isoformat(),
"cotacoes": [
{
"moeda": c.moeda,
"nome": c.nome,
"compra": c.compra,
"venda": c.venda,
"variacao": c.variacao,
}
for c in cotacoes
]
}
with open(arquivo, "w", encoding="utf-8") as f:
json.dump(dados, f, ensure_ascii=False, indent=2)
print(f"Cotações salvas em {arquivo}")
# Uso
scraper = ScraperCotacoes()
cotacoes = scraper.buscar_cotacoes()
scraper.exibir_cotacoes(cotacoes)
scraper.salvar_json(cotacoes)
Projeto Prático: Scraping de Notícias
import requests
from bs4 import BeautifulSoup
from dataclasses import dataclass, field
from typing import Optional
import csv
import time
@dataclass
class Artigo:
titulo: str
link: str
resumo: str = ""
autor: str = ""
data: str = ""
tags: list = field(default_factory=list)
class ScraperNoticias:
"""Scraper genérico para sites de notícias."""
def __init__(self, base_url):
self.base_url = base_url
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
"Accept-Language": "pt-BR,pt;q=0.9",
})
self.artigos = []
def buscar_pagina(self, url):
"""Faz a requisição e retorna o BeautifulSoup."""
try:
response = self.session.get(url, timeout=15)
response.raise_for_status()
return BeautifulSoup(response.text, "lxml")
except requests.RequestException as e:
print(f"Erro ao acessar {url}: {e}")
return None
def extrair_artigos(self, soup, seletores):
"""Extrai artigos usando seletores CSS configuráveis."""
containers = soup.select(seletores["container"])
for container in containers:
try:
titulo_elem = container.select_one(seletores.get("titulo", "h2"))
link_elem = container.select_one(seletores.get("link", "a"))
resumo_elem = container.select_one(seletores.get("resumo", "p"))
if not titulo_elem:
continue
titulo = titulo_elem.get_text(strip=True)
link = ""
if link_elem and link_elem.get("href"):
href = link_elem["href"]
if href.startswith("/"):
link = self.base_url + href
else:
link = href
resumo = resumo_elem.get_text(strip=True) if resumo_elem else ""
artigo = Artigo(
titulo=titulo,
link=link,
resumo=resumo[:200],
)
self.artigos.append(artigo)
except Exception as e:
print(f"Erro ao extrair artigo: {e}")
continue
return self.artigos
def salvar_csv(self, arquivo="artigos.csv"):
"""Salva artigos em CSV."""
if not self.artigos:
print("Nenhum artigo para salvar.")
return
with open(arquivo, "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(["Título", "Link", "Resumo", "Data"])
for artigo in self.artigos:
writer.writerow([
artigo.titulo,
artigo.link,
artigo.resumo,
artigo.data,
])
print(f"{len(self.artigos)} artigos salvos em {arquivo}")
def exibir(self):
"""Exibe artigos formatados."""
for i, artigo in enumerate(self.artigos, 1):
print(f"\n{i}. {artigo.titulo}")
if artigo.link:
print(f" Link: {artigo.link}")
if artigo.resumo:
print(f" {artigo.resumo[:100]}...")
# Exemplo de uso
scraper = ScraperNoticias("https://example.com")
# soup = scraper.buscar_pagina("https://example.com/noticias")
# if soup:
# seletores = {
# "container": "article.post",
# "titulo": "h2.title",
# "link": "a.read-more",
# "resumo": "p.excerpt",
# }
# artigos = scraper.extrair_artigos(soup, seletores)
# scraper.exibir()
# scraper.salvar_csv()
Lidando com Paginação
import requests
from bs4 import BeautifulSoup
import time
def scraping_com_paginacao(url_base, total_paginas=5):
"""Exemplo de scraping com múltiplas páginas."""
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 PythonDevBR-Bot/1.0",
})
todos_resultados = []
for pagina in range(1, total_paginas + 1):
url = f"{url_base}?page={pagina}"
print(f"Buscando página {pagina}...")
try:
response = session.get(url, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, "lxml")
# Extrair dados da página
itens = soup.select(".item") # Ajuste o seletor
for item in itens:
titulo = item.select_one("h3")
if titulo:
todos_resultados.append(titulo.get_text(strip=True))
print(f" Encontrados: {len(itens)} itens")
# IMPORTANTE: respeitar o servidor!
time.sleep(1) # Esperar 1 segundo entre requisições
except requests.RequestException as e:
print(f" Erro na página {pagina}: {e}")
continue
print(f"\nTotal coletado: {len(todos_resultados)} itens")
return todos_resultados
Tratamento de Erros e Robustez
import requests
from bs4 import BeautifulSoup
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ScraperRobusto:
"""Scraper com tratamento de erros e retry."""
def __init__(self, max_retries=3, delay_entre_requisicoes=1):
self.max_retries = max_retries
self.delay = delay_entre_requisicoes
self.session = requests.Session()
# Configurar retry automático
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry_strategy = Retry(
total=max_retries,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
self.session.headers.update({
"User-Agent": "Mozilla/5.0 PythonDevBR/1.0",
})
def buscar(self, url):
"""Busca uma URL com tratamento de erros."""
try:
response = self.session.get(url, timeout=15)
response.raise_for_status()
time.sleep(self.delay)
return BeautifulSoup(response.text, "lxml")
except requests.Timeout:
logger.error(f"Timeout ao acessar {url}")
except requests.ConnectionError:
logger.error(f"Erro de conexão: {url}")
except requests.HTTPError as e:
logger.error(f"Erro HTTP {e.response.status_code}: {url}")
except Exception as e:
logger.error(f"Erro inesperado: {e}")
return None
def extrair_texto_seguro(self, elemento, seletor, default=""):
"""Extrai texto de forma segura, sem gerar exceções."""
if elemento is None:
return default
encontrado = elemento.select_one(seletor)
return encontrado.get_text(strip=True) if encontrado else default
def extrair_atributo_seguro(self, elemento, seletor, atributo, default=""):
"""Extrai atributo de forma segura."""
if elemento is None:
return default
encontrado = elemento.select_one(seletor)
if encontrado and encontrado.get(atributo):
return encontrado[atributo]
return default
# Uso
scraper = ScraperRobusto(max_retries=3, delay_entre_requisicoes=2)
soup = scraper.buscar("https://example.com")
if soup:
titulo = scraper.extrair_texto_seguro(soup, "h1", "Sem título")
print(f"Título: {titulo}")
Boas Práticas e Ética
Web scraping é uma ferramenta poderosa, mas deve ser usada com responsabilidade:
# 1. SEMPRE verifique o robots.txt do site
# https://www.exemplo.com/robots.txt
import requests
def verificar_robots(url):
"""Verifica se o scraping é permitido pelo robots.txt."""
from urllib.parse import urlparse
from urllib.robotparser import RobotFileParser
parsed = urlparse(url)
robots_url = f"{parsed.scheme}://{parsed.netloc}/robots.txt"
rp = RobotFileParser()
rp.set_url(robots_url)
try:
rp.read()
permitido = rp.can_fetch("*", url)
print(f"Scraping em {url}: {'Permitido' if permitido else 'BLOQUEADO'}")
return permitido
except Exception:
print("Não foi possível verificar robots.txt")
return False
verificar_robots("https://www.google.com/search?q=python")
Regras importantes
- Respeite o robots.txt do site
- Adicione delays entre requisições (mínimo 1 segundo)
- Identifique-se com um User-Agent adequado
- Não sobrecarregue o servidor
- Verifique os termos de uso do site
- Prefira APIs quando disponíveis
- Cache os resultados para evitar requisições repetidas
- Respeite a legislação brasileira (LGPD)
# Exemplo de cache simples
import json
import os
from datetime import datetime, timedelta
class CacheScraping:
"""Cache simples para resultados de scraping."""
def __init__(self, pasta_cache=".cache", validade_horas=24):
self.pasta = pasta_cache
self.validade = timedelta(hours=validade_horas)
os.makedirs(pasta_cache, exist_ok=True)
def _arquivo_cache(self, chave):
# Gera nome de arquivo seguro
import hashlib
hash_chave = hashlib.md5(chave.encode()).hexdigest()
return os.path.join(self.pasta, f"{hash_chave}.json")
def obter(self, chave):
"""Retorna dados do cache se válidos."""
arquivo = self._arquivo_cache(chave)
if not os.path.exists(arquivo):
return None
with open(arquivo, "r") as f:
dados = json.load(f)
salvo_em = datetime.fromisoformat(dados["salvo_em"])
if datetime.now() - salvo_em > self.validade:
return None # Cache expirado
return dados["conteudo"]
def salvar(self, chave, conteudo):
"""Salva dados no cache."""
arquivo = self._arquivo_cache(chave)
dados = {
"salvo_em": datetime.now().isoformat(),
"chave": chave,
"conteudo": conteudo,
}
with open(arquivo, "w") as f:
json.dump(dados, f, ensure_ascii=False)
# Uso
cache = CacheScraping(validade_horas=12)
url = "https://example.com/dados"
dados = cache.obter(url)
if dados is None:
print("Cache vazio, fazendo requisição...")
# dados = scraper.buscar(url)
# cache.salvar(url, dados)
else:
print("Dados carregados do cache!")
Conclusão
Web scraping é uma habilidade fundamental para qualquer desenvolvedor Python. Com requests e BeautifulSoup, você pode extrair dados de praticamente qualquer site de forma eficiente.
Para ir além, explore:
- Selenium ou Playwright para sites com JavaScript
- Scrapy para projetos de scraping em larga escala
- APIs como alternativa mais estável ao scraping
- Async scraping com aiohttp para maior performance
Lembre-se sempre de ser ético e responsável ao fazer web scraping. Respeite os termos de uso dos sites, use delays entre requisições e prefira APIs quando disponíveis. Bom scraping!
Equipe Python Brasil
Contribuidor do Python Brasil — Aprenda Python em Português