Pular para o conteúdo
Backend

Web Scraping de Odds com Python: APIs em Tempo

Como fazer scraping de odds em tempo real de casas de apostas usando Python. APIs públicas e boas práticas.

Por que isso é importante

Web Scraping de Odds com Python: APIs em Tempo. Como fazer scraping de odds em tempo real de casas de apostas usando Python. APIs públicas e boas práticas.

Já passei uns dias quebrando a cabeça tentando fazer scraping de casas de apostas antes de descobrir que a maioria tem APIs semi-públicas muito mais fáceis de usar. Vou te poupar esse tempo. A ordem de prioridade é sempre: API oficial primeiro, dados no JSON da página segundo, scraping de HTML como último recurso.

É legal fazer scraping de casas de apostas?

A resposta curta: depende. A coleta de dados públicos disponíveis a qualquer usuário geralmente é legal do ponto de vista do código. O que as casas proíbem nos termos de serviço é o uso desses dados pra fins comerciais ou criar serviços derivados sem autorização. Scraping pra uso pessoal e análise própria fica numa zona cinza — tecnicamente pode violar os termos, raramente viola a lei.

O risco prático não é legal — é técnico. Casas de apostas são muito boas em detectar e bloquear bots. Se você usar o IP da sua casa, sua conta pessoal pode ser suspensa. A abordagem mais segura: use APIs públicas quando disponíveis e nunca scrapeie em conta que você usa pra apostar de verdade.

Aviso legal e sobre apostas

Este tutorial tem propósito educacional sobre web scraping e Python. Antes de coletar dados de qualquer site, leia os Termos de Serviço do site em questão. Apostas esportivas envolvem risco financeiro real. Nunca aposte mais do que pode perder. Em alguns países, apostas online têm restrições legais — verifique a legislação do seu país.

APIs oficiais vs scraping (prós e contras)

APIs Oficiais

Use a API quando o site oferece acesso estruturado a dados

+ Prós

  • • Dados estruturados e confiáveis
  • • Suporte e documentação disponíveis
  • • Não viola termos de serviço
  • • Rate limits claros e previsíveis

− Contras

  • • Nem sempre gratuito
  • • Pode não ter todos os mercados
  • • Dependência de terceiro

Web Scraping

Extrai dados diretamente do HTML ou requisições de rede

+ Prós

  • • Acesso a dados sem API disponível
  • • Geralmente gratuito
  • • Mais flexível

− Contras

  • • Quebra quando o site muda layout
  • • Pode violar termos de serviço
  • • Mais complexo (anti-bot, JavaScript)
  • • Manutenção constante necessária

Tutorial com Python: coletando odds com requests

Antes do scraping de HTML, sempre inspeciona as requisições de rede do site (F12 → Network no Chrome). A maioria das casas modernas faz requisições Ajax pra uma API interna que retorna JSON. Capturar essa URL é muito mais fácil do que parsear HTML e muito mais estável.

python
import requests
import time
import random
from typing import Optional

# Simula um browser real — sem isso a maioria dos sites bloqueia imediatamente
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': 'application/json, text/plain, */*',
    'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
    'Accept-Encoding': 'gzip, deflate, br',
    'Connection': 'keep-alive',
    'Referer': 'https://www.example-sportsbook.com/',
    'Sec-Fetch-Dest': 'empty',
    'Sec-Fetch-Mode': 'cors',
    'Sec-Fetch-Site': 'same-origin'
}

class OddsCollector:
    """
    Coletor genérico de odds via HTTP.
    Adapte a URL e o parsing pra cada casa de apostas.
    """
    
    def __init__(self, base_url: str, delay_min: float = 2.0,
                 delay_max: float = 5.0):
        self.base_url = base_url
        self.delay_min = delay_min
        self.delay_max = delay_max
        self.session = requests.Session()
        self.session.headers.update(HEADERS)
    
    def _delay_aleatorio(self):
        """Aguarda tempo aleatório entre requisições (comportamento humano)."""
        delay = random.uniform(self.delay_min, self.delay_max)
        time.sleep(delay)
    
    def get_json(self, endpoint: str,
                 params: Optional[dict] = None) -> Optional[dict]:
        """Faz requisição GET e retorna JSON."""
        url = f'{self.base_url}{endpoint}'
        try:
            self._delay_aleatorio()
            response = self.session.get(url, params=params, timeout=15)
            response.raise_for_status()
            return response.json()
        except requests.HTTPError as e:
            if e.response.status_code == 403:
                print('Bloqueado! Tente rotacionar User-Agent ou usar proxy.')
            elif e.response.status_code == 429:
                print('Rate limit atingido. Aguarde antes de continuar.')
                time.sleep(60)  # Espera 1 minuto
            return None
        except Exception as e:
            print(f'Erro: {e}')
            return None

# Exemplo usando The Odds API (API oficial, sem scraping)
class TheOddsAPICollector:
    """
    Wrapper pra The Odds API — use isso em vez de scraping direto.
    API gratuita com 500 requests/mês no plano free.
    """
    
    BASE_URL = 'https://api.the-odds-api.com/v4'
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.session = requests.Session()
        self.requests_remaining = None
    
    def listar_esportes(self) -> list:
        """Lista esportes disponíveis na API."""
        url = f'{self.BASE_URL}/sports'
        r = self.session.get(url, params={'apiKey': self.api_key})
        self.requests_remaining = r.headers.get('x-requests-remaining')
        return r.json()
    
    def buscar_odds(self, esporte: str, mercados: str = 'h2h,totals') -> list:
        """Busca odds de um esporte específico."""
        url = f'{self.BASE_URL}/sports/{esporte}/odds'
        params = {
            'apiKey': self.api_key,
            'regions': 'eu,uk',
            'markets': mercados,
            'oddsFormat': 'decimal'
        }
        r = self.session.get(url, params=params)
        self.requests_remaining = r.headers.get('x-requests-remaining')
        print(f'Requests restantes: {self.requests_remaining}')
        return r.json()

# Uso
api = TheOddsAPICollector(api_key='sua_chave')
esportes = api.listar_esportes()
esportes_ativos = [e['key'] for e in esportes if e['active']]
print('Esportes disponíveis:', esportes_ativos[:10])

Armazenando dados em SQLite para análise

Coletar dados sem armazenar bem é tempo perdido. SQLite é perfeito pra começar — zero configuração, arquivo único, suporta consultas SQL completas. Quando a base crescer além de uns 50GB ou precisar de acesso concorrente, migra pra PostgreSQL com SQLAlchemy.

python
import sqlite3
import json
from datetime import datetime
from contextlib import contextmanager

@contextmanager
def get_db(path: str = 'odds.db'):
    """Context manager pra conexão com SQLite."""
    conn = sqlite3.connect(path)
    conn.row_factory = sqlite3.Row  # Retorna dict-like rows
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()

def setup_schema():
    """Cria o schema do banco."""
    with get_db() as conn:
        conn.executescript('''
            CREATE TABLE IF NOT EXISTS snapshots_odds (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                jogo_id TEXT NOT NULL,
                esporte TEXT NOT NULL,
                time_casa TEXT NOT NULL,
                time_fora TEXT NOT NULL,
                data_jogo TEXT NOT NULL,
                bookmaker TEXT NOT NULL,
                mercado TEXT NOT NULL,
                selecao TEXT NOT NULL,
                odd REAL NOT NULL,
                capturado_em TEXT NOT NULL DEFAULT (datetime('now'))
            );
            
            CREATE INDEX IF NOT EXISTS idx_jogo_data 
                ON snapshots_odds (jogo_id, capturado_em);
            
            CREATE INDEX IF NOT EXISTS idx_bookmaker 
                ON snapshots_odds (bookmaker, capturado_em);
            
            CREATE VIEW IF NOT EXISTS odds_mais_recentes AS
            SELECT s.*
            FROM snapshots_odds s
            INNER JOIN (
                SELECT jogo_id, bookmaker, mercado, selecao,
                       MAX(capturado_em) as ultima_captura
                FROM snapshots_odds
                GROUP BY jogo_id, bookmaker, mercado, selecao
            ) m ON s.jogo_id = m.jogo_id
                AND s.bookmaker = m.bookmaker
                AND s.mercado = m.mercado
                AND s.selecao = m.selecao
                AND s.capturado_em = m.ultima_captura;
        ''')
    print('Schema criado com sucesso')

def salvar_snapshot_odds(dados_jogos: list):
    """Salva snapshot de odds coletadas."""
    registros = []
    agora = datetime.utcnow().isoformat()
    
    for jogo in dados_jogos:
        for bm in jogo.get('bookmakers', []):
            for market in bm.get('markets', []):
                for outcome in market.get('outcomes', []):
                    registros.append((
                        jogo['id'],
                        jogo.get('sport_key', ''),
                        jogo['home_team'],
                        jogo['away_team'],
                        jogo['commence_time'],
                        bm['key'],
                        market['key'],
                        outcome['name'],
                        outcome['price'],
                        agora
                    ))
    
    with get_db() as conn:
        conn.executemany('''
            INSERT INTO snapshots_odds 
            (jogo_id, esporte, time_casa, time_fora, data_jogo,
             bookmaker, mercado, selecao, odd, capturado_em)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        ''', registros)
    
    print(f'{len(registros)} registros salvos em {agora}')

# Consulta útil: ver evolução de odd ao longo do tempo
def historico_odd(jogo_id: str, bookmaker: str, selecao: str) -> list:
    """Mostra como a odd mudou ao longo do tempo."""
    with get_db() as conn:
        rows = conn.execute('''
            SELECT odd, capturado_em
            FROM snapshots_odds
            WHERE jogo_id = ? AND bookmaker = ? AND selecao = ?
            ORDER BY capturado_em ASC
        ''', (jogo_id, bookmaker, selecao)).fetchall()
    return [dict(r) for r in rows]

Automatizando com cron jobs

Com os módulos de coleta e armazenamento prontos, precisa de automação pra rodar de hora em hora sem você precisar ficar na frente do computador. Duas opções principais: cron do sistema operacional ou APScheduler dentro do Python.

python
# scheduler.py — Usando APScheduler pra automatizar coleta
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
from loguru import logger
import sys

# Configure loguru pra arquivo + console
logger.remove()
logger.add(sys.stdout, level='INFO')
logger.add('logs/coleta_{time}.log', rotation='1 day',
           retention='7 days', level='DEBUG')

def tarefa_coleta_horaria():
    """Roda a cada hora pra capturar snapshot de odds."""
    logger.info('Iniciando coleta horária')
    try:
        from main_coleta import coletar_todos_esportes
        coletar_todos_esportes()
        logger.info('Coleta horária concluída')
    except Exception as e:
        logger.error(f'Erro na coleta: {e}')

def tarefa_coleta_pre_jogo():
    """Coleta mais frequente (15min) pra jogos nas próximas 2h."""
    logger.info('Coleta pré-jogo (15min)')
    try:
        from main_coleta import coletar_jogos_iminentes
        coletar_jogos_iminentes(horas=2)
    except Exception as e:
        logger.error(f'Erro na coleta pré-jogo: {e}')

if __name__ == '__main__':
    scheduler = BlockingScheduler(timezone='America/Sao_Paulo')
    
    # Coleta a cada hora
    scheduler.add_job(
        tarefa_coleta_horaria,
        CronTrigger(minute=0),  # No início de cada hora
        id='coleta_horaria',
        name='Coleta horária de odds'
    )
    
    # Coleta a cada 15 minutos (pra jogos próximos)
    scheduler.add_job(
        tarefa_coleta_pre_jogo,
        CronTrigger(minute='*/15'),
        id='coleta_pre_jogo',
        name='Coleta pré-jogo 15min'
    )
    
    logger.info('Scheduler iniciado. Ctrl+C para parar.')
    
    try:
        scheduler.start()
    except KeyboardInterrupt:
        logger.info('Scheduler encerrado pelo usuário')

# Alternativa: cron do sistema (mais simples)
# Adicione ao crontab com: crontab -e
# 0 * * * * /usr/bin/python3 /home/user/bot/coleta.py >> /home/user/bot/logs/coleta.log 2>&1
# */15 * * * * /usr/bin/python3 /home/user/bot/coleta_pre_jogo.py

Boas práticas (rate limiting, user agents, ética)

Dá pra ser técnico e ético ao mesmo tempo. Scraping agressivo derruba servidores e prejudica outros usuários. Rate limiting adequado, delays aleatórios e respeito a robots.txt são práticas básicas que todo scraper decente implementa.

Boas práticas de scraping que você deve seguir

Sempre use delays aleatórios entre requisições (2-5 segundos mínimo)

Respeite o arquivo robots.txt do site — se proibir bots, não scrapeie

Use APIs oficiais quando disponíveis — é mais confiável e mais ético

Não faça mais de uma requisição por segundo em média

Identifique seu bot no User-Agent com seu email se possível

Armazene dados localmente — não re-scrapeie o que já tem

Nunca use proxies pra burlar bloqueios em sites que proibem bots explicitamente

Checklist antes de publicar seu scraper

  • Leu o robots.txt e os Termos de Serviço do site
  • Implementou delays aleatórios entre requisições
  • Tem tratamento de erro pra status 429 (rate limit) e 403 (bloqueado)
  • Loga todas as requisições com timestamp pra debugging
  • Tem mecanismo de retry com backoff exponencial
  • Armazena dados localmente pra não repetir coletas desnecessárias
  • Tem alerta quando a estrutura do site muda e quebra o parser

Com os dados coletados e armazenados, o próximo passo é análise. O artigo sobre como usar Python pra analisar odds mostra como processar esses dados com pandas pra encontrar discrepâncias entre casas. E se quiser o pipeline completo de análise a apostas, o tutorial do bot de apostas com IA integra tudo isso num sistema automatizado.