---
title: "Web Scraping com Python: Tutorial Completo"
url: "https://python.dev.br/blog/web-scraping-python/"
markdown_url: "https://python.dev.br/blog/web-scraping-python.MD"
description: "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."
date: "2026-03-15"
author: "Equipe Python Brasil"
---

# 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

```python
# Instalar as bibliotecas necessárias
# pip install requests beautifulsoup4 lxml

import requests
from bs4 import BeautifulSoup
```

## Conceitos Básicos

### Fazendo Requisições HTTP

```python
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:

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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:

```python
# 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)

```python
# 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!

> **Scraping em alta escala**: para projetos que precisam raspar milhoes de paginas, <a href="https://golang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go</a> com Colly oferece scraping concorrente extremamente eficiente com goroutines. Python continua ideal para prototipagem e projetos de medio porte com BeautifulSoup e Scrapy.
