Pular para o conteúdo
Backend

Web Scraping de Odds com Python: Tutorial

Dá pra comparar odds de dezenas de casas de apostas em segundos usando Python. Nesse tutorial eu mostro como montar um scraper completo, do zero, com código pronto

TL;DR

Web Scraping de Odds com Python: Tutorial. Dá pra comparar odds de dezenas de casas de apostas em segundos usando Python. Nesse tutorial eu mostro como montar um scraper completo, do zero, com código pronto pra rodar.

Por que coletar odds de várias casas (line shopping)

Se você aposta em uma casa só, tá deixando dinheiro na mesa. Simples assim. Cada casa de apostas tem uma margem diferente, e as odds variam bastante entre elas pro mesmo evento. Isso se chama line shopping — comparar as linhas pra sempre pegar a melhor odd disponível.

Eu comecei a levar apostas a sério quando entendi que a diferença entre uma odd de 1.85 e 1.95 no mesmo resultado parece pequena, mas ao longo de centenas de apostas, isso vira uma diferença enorme no seu bankroll. Estamos falando de algo entre 3% e 8% a mais de retorno no longo prazo.

O problema é que fazer isso manualmente é inviável. Você não vai abrir 15 sites diferentes, procurar o mesmo jogo em cada um e anotar as odds numa planilha. Por isso a gente automatiza com Python.

O que o line shopping resolve

  • Garante que você sempre aposta na melhor odd disponível
  • Aumenta o ROI de longo prazo em 3-8% dependendo do esporte
  • Identifica oportunidades de arbitragem automaticamente
  • Detecta movimentações de linha que indicam onde o dinheiro smart tá entrando

Setup do ambiente Python

Antes de começar a codar, vamos preparar o ambiente. Eu uso Python 3.11+ pra tudo relacionado a scraping. Se você ainda tá no 3.8 ou 3.9, vale atualizar porque as bibliotecas mais novas já pedem versões mais recentes.

bash
# Criar ambiente virtual
python -m venv odds-env
source odds-env/bin/activate  # Linux/Mac
# odds-env\Scripts\activate  # Windows

# Instalar dependências
pip install requests beautifulsoup4 selenium pandas sqlalchemy
pip install webdriver-manager  # gerencia ChromeDriver automaticamente
pip install python-dotenv      # pra variáveis de ambiente

A estrutura de pastas que eu uso é bem direta. Nada de overengineering — a gente quer algo funcional, não um monolito corporativo.

bash
odds-scraper/
├── .env                 # chaves de API
├── config.py            # configurações gerais
├── collectors/
│   ├── odds_api.py      # coleta via API
│   └── scraper.py       # coleta via scraping
├── storage/
│   └── database.py      # SQLite / PostgreSQL
├── analysis/
│   └── compare.py       # comparação de odds
└── main.py              # orquestrador
  1. Instale Python 3.11+ e crie um ambiente virtual isolado
  2. Instale as dependências: requests, beautifulsoup4, selenium, pandas, sqlalchemy
  3. Crie a estrutura de pastas conforme o exemplo acima
  4. Configure o arquivo .env com suas chaves de API
  5. Teste rodando python main.py pra garantir que tudo importa corretamente

Método 1: APIs oficiais (The Odds API, OddsPortal)

A forma mais limpa de coletar odds é via API. Sem quebrar termos de serviço, sem lidar com CAPTCHAs, sem dor de cabeça. A The Odds API é a minha favorita porque tem um plano gratuito que dá pra começar tranquilamente.

O plano free dá 500 requests por mês. Parece pouco, mas se você focar nos esportes certos e nos horários certos, dá pra cobrir bastante coisa. Cada request retorna odds de todas as casas disponíveis pra um esporte específico.

python
import requests
import os
from dotenv import load_dotenv
from datetime import datetime

load_dotenv()

API_KEY = os.getenv("ODDS_API_KEY")
BASE_URL = "https://api.the-odds-api.com/v4/sports"

def get_odds(sport: str = "soccer_brazil_serie_a", regions: str = "us,eu", markets: str = "h2h,spreads,totals") -> list[dict]:
    """Coleta odds de todas as casas disponíveis pra um esporte."""
    url = f"{BASE_URL}/{sport}/odds"
    params = {
        "apiKey": API_KEY,
        "regions": regions,
        "markets": markets,
        "oddsFormat": "decimal",
        "dateFormat": "iso",
    }
    response = requests.get(url, params=params, timeout=30)
    response.raise_for_status()
    
    remaining = response.headers.get("x-requests-remaining", "?")
    print(f"[{datetime.now():%H:%M}] Requests restantes: {remaining}")
    
    return response.json()

def extract_best_odds(events: list[dict]) -> list[dict]:
    """Pra cada evento, encontra a melhor odd em cada outcome."""
    results = []
    for event in events:
        game = {
            "id": event["id"],
            "home": event["home_team"],
            "away": event["away_team"],
            "start": event["commence_time"],
            "best_odds": {},
        }
        for bookmaker in event.get("bookmakers", []):
            for market in bookmaker.get("markets", []):
                for outcome in market.get("outcomes", []):
                    key = f"{market['key']}_{outcome['name']}"
                    current_best = game["best_odds"].get(key, {"price": 0})
                    if outcome["price"] > current_best.get("price", 0):
                        game["best_odds"][key] = {
                            "price": outcome["price"],
                            "bookmaker": bookmaker["title"],
                        }
        results.append(game)
    return results

# Uso
events = get_odds("soccer_brazil_serie_a")
best = extract_best_odds(events)
for game in best:
    print(f"\n{game['home']} vs {game['away']}")
    for market, info in game["best_odds"].items():
        print(f"  {market}: {info['price']:.2f} ({info['bookmaker']})")

Cuidado com os limites

O plano gratuito da The Odds API tem 500 requests/mês. Cada chamada pra um esporte consome 1 request.

Não fique rodando o script em loop sem controle. Use um scheduler (cron, APScheduler) com intervalos de pelo menos 15 minutos.

Salve os resultados no banco de dados pra não precisar consultar a API toda vez que quiser analisar algo.

Alternativa: OddsPortal e outras fontes

A OddsPortal não tem API pública oficial, mas dá pra usar como fonte de dados históricos. Já a BetfairExchange tem uma API bem documentada, só que exige cadastro e aprovação. Pra quem tá começando, The Odds API é disparada a melhor opção.

Método 2: Scraping com Beautiful Soup + Selenium

Quando a API não cobre o que você precisa — tipo odds de mercados mais nichados ou casas regionais — aí o scraping entra em cena. Eu uso Beautiful Soup pra sites estáticos e Selenium quando o conteúdo é carregado via JavaScript.

Vou ser direto: scraping de casas de apostas é uma briga constante. Eles mudam o HTML, adicionam anti-bot, colocam Cloudflare na frente. Não é um script que você faz uma vez e esquece. Precisa de manutenção.

python
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
from bs4 import BeautifulSoup
import time
import json

def create_driver() -> webdriver.Chrome:
    """Cria driver Chrome em modo headless."""
    options = Options()
    options.add_argument("--headless=new")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
    
    service = Service(ChromeDriverManager().install())
    return webdriver.Chrome(service=service, options=options)

def scrape_odds_page(url: str) -> list[dict]:
    """Faz scraping de uma pagina de odds."""
    driver = create_driver()
    results = []
    
    try:
        driver.get(url)
        # Esperar conteúdo dinâmico carregar
        WebDriverWait(driver, 15).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".event-row, .match-row, [data-testid='event']"))
        )
        time.sleep(2)  # buffer pra JS terminar
        
        soup = BeautifulSoup(driver.page_source, "html.parser")
        
        # Seletor genérico - adapte pro site alvo
        events = soup.select(".event-row, .match-row")
        for event in events:
            teams = event.select(".team-name, .participant")
            odds_els = event.select(".odd-value, .price, [data-odd]")
            
            if len(teams) >= 2 and len(odds_els) >= 2:
                results.append({
                    "home": teams[0].get_text(strip=True),
                    "away": teams[1].get_text(strip=True),
                    "odds": [el.get_text(strip=True) for el in odds_els],
                })
    finally:
        driver.quit()
    
    return results

# Exemplo de uso (adapte a URL pro site que voce quer)
odds = scrape_odds_page("https://example-odds-site.com/football/brazil")
for match in odds:
    print(f"{match['home']} vs {match['away']}: {match['odds']}")

API Oficial

Coleta dados via endpoints REST com chave de API

+ Prós

  • • Dados estruturados e confiáveis
  • • Sem risco de bloqueio ou CAPTCHA
  • • Atualizações em tempo real
  • • Legal e dentro dos termos de serviço

− Contras

  • • Limites de requests no plano gratuito
  • • Nem todas as casas estão disponíveis
  • • Custos altos nos planos pagos ($79-199/mês)

Scraping (BS4 + Selenium)

Extrai dados diretamente do HTML das páginas

+ Prós

  • • Acessa qualquer site público
  • • Sem limites de requests
  • • Grátis
  • • Pega mercados que APIs não cobrem

− Contras

  • • Quebra quando o site muda o layout
  • • Anti-bot pode bloquear (Cloudflare, reCAPTCHA)
  • • Pode violar termos de serviço
  • • Precisa de manutenção constante

Armazenando e analisando (SQLite, PostgreSQL + Grafana)

Coletar odds sem guardar em lugar nenhum é jogar trabalho fora. Eu comecei com SQLite porque é zero config — o banco é um arquivo só. Pra maioria dos casos, é mais do que suficiente.

python
from sqlalchemy import create_engine, Column, Integer, Float, String, DateTime
from sqlalchemy.orm import declarative_base, sessionmaker
from datetime import datetime

Base = declarative_base()

class OddsRecord(Base):
    __tablename__ = "odds"
    
    id = Column(Integer, primary_key=True, autoincrement=True)
    event_id = Column(String, index=True)
    sport = Column(String)
    home_team = Column(String)
    away_team = Column(String)
    bookmaker = Column(String)
    market = Column(String)
    outcome = Column(String)
    price = Column(Float)
    collected_at = Column(DateTime, default=datetime.utcnow)
    commence_time = Column(DateTime)

# Setup banco
engine = create_engine("sqlite:///odds_data.db")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)

def save_odds(events: list[dict], sport: str):
    """Salva odds no banco."""
    session = Session()
    count = 0
    for event in events:
        for bookmaker in event.get("bookmakers", []):
            for market in bookmaker.get("markets", []):
                for outcome in market.get("outcomes", []):
                    record = OddsRecord(
                        event_id=event["id"],
                        sport=sport,
                        home_team=event["home_team"],
                        away_team=event["away_team"],
                        bookmaker=bookmaker["title"],
                        market=market["key"],
                        outcome=outcome["name"],
                        price=outcome["price"],
                        commence_time=datetime.fromisoformat(event["commence_time"].replace("Z", "+00:00")),
                    )
                    session.add(record)
                    count += 1
    session.commit()
    session.close()
    print(f"Salvos {count} registros de odds")

# Pipeline completo
events = get_odds("soccer_brazil_serie_a")
save_odds(events, "soccer_brazil_serie_a")

Quando migrar pra PostgreSQL

Se você tá coletando odds de mais de 5 esportes com frequência de 15 em 15 minutos, o SQLite vai começar a ficar lento. Nesse ponto, migra pro PostgreSQL. A mudança é uma linha de código — troca a connection string e pronto.

E se quiser visualizar os dados, o Grafana conecta direto no PostgreSQL. Dá pra montar dashboards com movimentação de odds ao vivo, alertas quando uma odd cai ou sobe demais, e gráficos de comparação entre casas. É bonito demais quando funciona.

Query útil: encontrando as melhores odds

python
import pandas as pd

def find_best_odds_per_event(db_path: str = "sqlite:///odds_data.db") -> pd.DataFrame:
    """Encontra a melhor odd pra cada outcome de cada evento."""
    engine = create_engine(db_path)
    query = """
        SELECT event_id, home_team, away_team, market, outcome,
               MAX(price) as best_price,
               bookmaker
        FROM odds
        WHERE collected_at > datetime('now', '-2 hours')
        GROUP BY event_id, market, outcome
        ORDER BY event_id, market
    """
    df = pd.read_sql(query, engine)
    return df

# Ver resultados
df = find_best_odds_per_event()
print(df.to_string(index=False))

Precauções legais e éticas

Vou ser honesto aqui porque muita gente ignora essa parte. Scraping de sites de apostas pode violar os termos de serviço dessas plataformas. Não é crime no Brasil — scraping de dados públicos é legal — mas a casa pode fechar sua conta se detectar acesso automatizado.

O caminho mais seguro é usar APIs oficiais sempre que possível. The Odds API, por exemplo, já faz o trabalho pesado pra você e distribui os dados de forma legal. Você paga pelo serviço e evita qualquer problema.

Boas práticas de scraping responsável

Respeite o robots.txt do site — se ele proíbe scraping, pense duas vezes

Não sobrecarregue os servidores. Use intervalos de pelo menos 5 segundos entre requests

Nunca faça scraping de dados pessoais de outros usuários

Use os dados pra análise pessoal, não pra revender ou distribuir

Prefira APIs oficiais sempre que disponíveis

Regulamentação de apostas no Brasil

Desde 2024, as apostas esportivas online são regulamentadas no Brasil. As casas que operam legalmente precisam de licença da Secretaria de Prêmios e Apostas. Isso é bom porque dá mais segurança pro apostador, mas também significa que as casas estão mais rígidas com automação.

Na prática: use as ferramentas de coleta de odds pra tomar decisões melhores. Não tente automatizar as apostas em si dentro da plataforma — isso quase certamente viola os termos de uso e pode resultar em bloqueio de conta e perda de saldo.

Montando o pipeline completo

Agora vamos juntar tudo num script que roda automaticamente. A ideia é simples: a cada 30 minutos, coleta odds, salva no banco, e se encontrar uma oportunidade boa, manda um alerta.

python
import schedule
import time
from collectors.odds_api import get_odds, extract_best_odds
from storage.database import save_odds

SPORTS = [
    "soccer_brazil_serie_a",
    "soccer_brazil_serie_b",
    "soccer_epl",
]

def collect_and_store():
    """Pipeline principal: coleta e armazena."""
    for sport in SPORTS:
        try:
            events = get_odds(sport)
            save_odds(events, sport)
            best = extract_best_odds(events)
            
            # Checa oportunidades de arbitragem
            for game in best:
                check_arbitrage(game)
                
            print(f"OK: {sport} - {len(events)} eventos")
        except Exception as e:
            print(f"ERRO: {sport} - {e}")

def check_arbitrage(game: dict):
    """Verifica se existe oportunidade de arbitragem."""
    h2h_odds = {}
    for key, info in game["best_odds"].items():
        if key.startswith("h2h_"):
            h2h_odds[key] = info["price"]
    
    if len(h2h_odds) >= 2:
        inv_sum = sum(1 / odd for odd in h2h_odds.values())
        if inv_sum < 1.0:
            margin = (1 - inv_sum) * 100
            print(f"  ARBITRAGEM! {game['home']} vs {game['away']} - margem: {margin:.2f}%")

# Rodar a cada 30 minutos
schedule.every(30).minutes.do(collect_and_store)

print("Pipeline iniciado. Ctrl+C pra parar.")
collect_and_store()  # roda uma vez imediatamente
while True:
    schedule.run_pending()
    time.sleep(60)

Próximos passos

Com esse pipeline rodando, você já tem a base pra qualquer estratégia de apostas baseada em dados. O próximo passo natural é adicionar análise de value betting — comparar as odds das casas com probabilidades calculadas por modelos estatísticos.

Dá pra evoluir esse projeto de várias formas: adicionar notificações via Telegram quando surgir uma arbitragem, criar um dashboard web com Streamlit, ou integrar com modelos de machine learning pra prever resultados. Mas começa pelo básico. Coleta funcionando, banco de dados rodando, e só depois vai adicionando camadas.

Checklist do projeto

  • Ambiente Python configurado com virtualenv
  • Chave da The Odds API criada e salva no .env
  • Script de coleta via API funcionando
  • Banco de dados SQLite configurado com SQLAlchemy
  • Pipeline automatizado com schedule
  • Detecção de arbitragem implementada
  • Boas práticas de scraping sendo seguidas