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.

8 min de leitura Equipe Python Brasil

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

  1. Respeite o robots.txt do site
  2. Adicione delays entre requisições (mínimo 1 segundo)
  3. Identifique-se com um User-Agent adequado
  4. Não sobrecarregue o servidor
  5. Verifique os termos de uso do site
  6. Prefira APIs quando disponíveis
  7. Cache os resultados para evitar requisições repetidas
  8. 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:

  1. Selenium ou Playwright para sites com JavaScript
  2. Scrapy para projetos de scraping em larga escala
  3. APIs como alternativa mais estável ao scraping
  4. 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!

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português