Pular para o conteúdo
Backend

Web scraping de odds: como coletar dados

Quando a API não existe ou é cara demais, o scraping entra em cena. Dá pra coletar odds de qualquer site de apostas com Python, Selenium e BeautifulSoup

Por que isso é importante

Web scraping de odds: como coletar dados. Quando a API não existe ou é cara demais, o scraping entra em cena. Dá pra coletar odds de qualquer site de apostas com Python, Selenium e BeautifulSoup — e aqui eu mostro como fazer isso direito.

Galera, vou ser direto sobre quando usar scraping e quando não usar. Se existe uma API pra coletar os dados que você precisa, use a API. Scraping é mais frágil, mais trabalhoso de manter e pode violar os termos de uso do site. Mas quando não há API disponível — ou quando a API é cara demais pro seu orçamento — scraping é a alternativa.

Aviso importante: sempre verifique os Termos de Serviço do site antes de fazer scraping. Alguns sites explicitamente proíbem coleta automatizada de dados. Este tutorial é puramente educacional.

Por que scraping de odds — e quando usar API em vez disso

Sites como Oddschecker, OddsPortal e Betfair agregam odds de várias casas numa única página. Scraping dessas fontes dá acesso a um comparativo de odds sem precisar raspar cada casa individualmente. O problema: são sites dinâmicos com JavaScript pesado, o que exige Selenium ou Playwright em vez de um simples requests + BeautifulSoup.

Web Scraping

Coleta direta de dados de sites usando automação de browser.

+ Prós

  • • Sem custo de API — dados públicos dos sites
  • • Acesso a dados que não têm API disponível
  • • Flexível: qualquer site com dados visíveis pode ser raspado

− Contras

  • • Frágil: qualquer mudança no HTML quebra o scraper
  • • Sites de apostas têm anti-bot sofisticado
  • • Pode violar Termos de Serviço
  • • Mais lento que APIs e consome mais recursos

API de Odds

Dados estruturados via endpoint REST com chave de API.

+ Prós

  • • Dados normalizados e estáveis — sem risco de HTML mudando
  • • Rate limiting previsível e documentado
  • • Histórico de odds disponível na maioria das APIs pagas
  • • Muito mais rápido e confiável

− Contras

  • • Custo mensal (US$ 79-599 nas principais)
  • • Dependência de terceiros — se a API cair, você cai
  • • Planos gratuitos têm limite baixo de requests

Setup: Python + Selenium + BeautifulSoup

Para sites com JavaScript, você precisa de um browser headless que renderize o conteúdo antes de extrair o HTML. Selenium com ChromeDriver é a opção mais clássica. Playwright é mais moderno e um pouco mais rápido. Para páginas estáticas, requests + BeautifulSoup ainda é a forma mais simples.

bash
# Instalando as dependências
pip install selenium beautifulsoup4 lxml requests playwright

# Instalando o ChromeDriver via selenium-manager (selenium 4.6+)
# O selenium 4.6+ baixa o driver automaticamente — não precisa instalar manualmente

# Alternativamente, para Playwright:
python -m playwright install chromium
python
# Python 3.11+
# browser.py — configuração do Selenium para scraping de odds

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time


def criar_driver(headless: bool = True) -> webdriver.Chrome:
    """
    Cria driver Chrome com configurações otimizadas para scraping.
    """
    options = Options()
    
    if headless:
        options.add_argument("--headless=new")  # modo headless moderno
    
    # Configurações para evitar detecção
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--disable-blink-features=AutomationControlled")
    options.add_experimental_option("excludeSwitches", ["enable-automation"])
    options.add_experimental_option("useAutomationExtension", False)
    
    # User agent real de browser normal
    options.add_argument(
        "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/122.0.0.0 Safari/537.36"
    )
    
    driver = webdriver.Chrome(options=options)
    
    # Remove propriedade webdriver do navigator
    driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
    
    return driver


def aguardar_elemento(driver: webdriver.Chrome, selector: str, timeout: int = 10):
    """Aguarda elemento aparecer no DOM."""
    return WebDriverWait(driver, timeout).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, selector))
    )

Scraping de odds: exemplo prático

O exemplo abaixo mostra a estrutura de um scraper genérico para páginas de odds. Adaptar para um site específico requer inspecionar o HTML com DevTools e ajustar os seletores CSS — mas a lógica é sempre a mesma.

python
# Python 3.11+
# odds_scraper.py — scraper genérico com Selenium + BeautifulSoup

from selenium import webdriver
from bs4 import BeautifulSoup
import time
import random


def scrape_pagina_odds(url: str, driver: webdriver.Chrome) -> list[dict]:
    """
    Raspa odds de uma página de comparação.
    Adapte os seletores para o site específico.
    
    Returns:
        Lista de dicts com dados dos eventos e odds
    """
    driver.get(url)
    
    # Aguarda carregamento do JavaScript
    time.sleep(random.uniform(2, 4))  # delay humano
    
    # Scroll para carregar conteúdo lazy-loaded
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight / 2)")
    time.sleep(1)
    
    # Extrai HTML renderizado pelo browser
    html = driver.page_source
    soup = BeautifulSoup(html, "lxml")
    
    resultados = []
    
    # Adapte esses seletores para o site alvo
    # Inspecione o HTML com F12 no browser
    eventos = soup.select(".evento-container")  # ajustar
    
    for evento in eventos:
        try:
            times = evento.select(".nome-time")  # ajustar
            odds_elements = evento.select(".odd-valor")  # ajustar
            
            if len(times) >= 2 and len(odds_elements) >= 2:
                resultados.append({
                    "home": times[0].text.strip(),
                    "away": times[1].text.strip(),
                    "odd_home": float(odds_elements[0].text.strip().replace(",", ".")),
                    "odd_away": float(odds_elements[1].text.strip().replace(",", ".")),
                })
        except (ValueError, IndexError):
            continue
    
    return resultados


def scrape_com_requests_simples(url: str) -> list[dict]:
    """
    Para sites com HTML estático (sem JavaScript).
    Muito mais rápido que Selenium.
    """
    import requests
    from bs4 import BeautifulSoup
    
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
    }
    resp = requests.get(url, headers=headers, timeout=15)
    resp.raise_for_status()
    
    soup = BeautifulSoup(resp.text, "lxml")
    # ... parsing igual ao acima
    return []

Lidando com anti-scraping

Sites de apostas têm proteção anti-bot séria. Cloudflare, DataDome, PerimeterX — são camadas de defesa que detectam comportamento automatizado. As técnicas básicas pra contornar: delays aleatórios entre requests, rotação de user agents, mover o mouse programaticamente, e usar proxies residenciais.

A abordagem mais eficaz e legítima: interceptar as requisições de API que o site já faz internamente. Quase todo site de apostas consome uma API JSON própria — você abre o DevTools, vai na aba Network, filtra por XHR/Fetch e vê as chamadas. Muitas vezes dá pra chamar essa API diretamente sem precisar renderizar o browser.

python
# Python 3.11+
# api_interceptor.py — interceptando a API interna do site
# Esta técnica funciona quando o site usa sua própria API JSON internamente

import requests
import json


def descobrir_api_interna(
    url_site: str,
    filtro_endpoint: str = "/api/"
) -> None:
    """
    Use o DevTools do browser para isso:
    1. Abra o site no Chrome
    2. F12 -> Network -> XHR/Fetch
    3. Recarregue a página
    4. Olhe os requests — procure JSON com dados de odds
    5. Copie o endpoint e os headers necessários
    """
    print("""
    Como interceptar APIs internas de sites de apostas:
    
    1. Abra o site no Chrome (não headless)
    2. F12 -> aba Network
    3. Filtre por XHR ou Fetch
    4. Navegue no site normalmente
    5. Observe os requests na lista
    6. Procure endpoints que retornem JSON com odds
    7. Clique no request, veja Headers e Response
    8. Copie a URL, os headers (incluindo cookies) e replique com requests
    """)


def chamar_api_interna(endpoint: str, cookies: dict, headers: dict) -> dict:
    """
    Chama a API interna do site após descoberta manual.
    Mais estável que scraping de HTML.
    """
    resp = requests.get(
        endpoint,
        headers={
            "Accept": "application/json",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            **headers
        },
        cookies=cookies,
        timeout=15
    )
    resp.raise_for_status()
    return resp.json()

Armazenando e normalizando dados

Dados de diferentes fontes precisam ser normalizados antes de usar. Nomes de times são escritos de formas diferentes entre sites — "Flamengo", "C.R. Flamengo", "Flamengo RJ". Odds podem estar em formato decimal ou americano. Uma camada de normalização no pipeline evita dor de cabeça depois.

python
# Python 3.11+
# normalizer.py — normalizando dados de odds de diferentes fontes

import re
from difflib import get_close_matches

# Mapeamento de nomes de times por variações conhecidas
TEAM_ALIASES: dict[str, list[str]] = {
    "flamengo": ["c.r. flamengo", "flamengo rj", "crf", "flamengo rio"],
    "palmeiras": ["se palmeiras", "sep", "palmeiras sp"],
    "corinthians": ["sc corinthians", "sccp", "timão"],
    # ... adicione mais conforme necessário
}

# Inverte para busca rápida
ALIAS_MAP: dict[str, str] = {}
for nome_padrao, aliases in TEAM_ALIASES.items():
    for alias in aliases:
        ALIAS_MAP[alias.lower()] = nome_padrao
    ALIAS_MAP[nome_padrao] = nome_padrao


def normalizar_nome_time(nome: str) -> str:
    """Normaliza nome do time para forma padronizada."""
    normalizado = nome.lower().strip()
    normalizado = re.sub(r"[^a-záéíóúàâêîôûãõç\s]", "", normalizado)
    normalizado = re.sub(r"\s+", " ", normalizado).strip()
    
    if normalizado in ALIAS_MAP:
        return ALIAS_MAP[normalizado]
    
    # Busca fuzzy para nomes próximos
    matches = get_close_matches(normalizado, ALIAS_MAP.keys(), n=1, cutoff=0.85)
    if matches:
        return ALIAS_MAP[matches[0]]
    
    return nome.strip()  # retorna original se não encontrar


def normalizar_odd(odd_raw: str | float) -> float:
    """Converte odd de qualquer formato para decimal."""
    if isinstance(odd_raw, float):
        return odd_raw
    
    odd_str = str(odd_raw).strip().replace(",", ".")
    
    # Odd americana (ex: +150, -120)
    if odd_str.startswith("+") or (odd_str.startswith("-") and len(odd_str) > 2):
        americana = float(odd_str)
        if americana > 0:
            return americana / 100 + 1
        else:
            return 100 / abs(americana) + 1
    
    return float(odd_str)

Automatizando com cron jobs

O scraper precisa rodar periodicamente pra manter as odds atualizadas. No Linux/Mac, cron é o jeito mais simples. Em produção, prefira o APScheduler dentro do seu processo Python — dá mais controle sobre erros e logs.

python
# Python 3.11+
# scheduler.py — agendamento com APScheduler

from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.interval import IntervalTrigger
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

scheduler = BlockingScheduler()


@scheduler.scheduled_job(
    IntervalTrigger(minutes=5),
    id="coletar_odds",
    max_instances=1,  # evita sobreposição de execuções
    coalesce=True     # pula execuções perdidas em vez de empilhar
)
def coletar_odds_job() -> None:
    """Coleta odds e grava no banco a cada 5 minutos."""
    logger.info("Iniciando coleta de odds...")
    try:
        # driver = criar_driver(headless=True)
        # odds = scrape_pagina_odds(URL_ALVO, driver)
        # salvar_no_banco(odds)
        logger.info(f"Coleta concluída")
    except Exception as e:
        logger.error(f"Erro na coleta: {e}")
    finally:
        pass  # driver.quit()


if __name__ == "__main__":
    logger.info("Scheduler iniciado. Coleta a cada 5 minutos.")
    scheduler.start()