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.
# 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 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 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 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 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 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()