Quer parar de abrir manualmente o site de cada concorrente toda semana para ver se o preço mudou? Um painel de monitoramento automatizado resolve isso: ele coleta preços, catálogo e recursos, passa pelo CAPTCHA que aparece no meio do caminho e guarda um histórico consultável para comparar a evolução ao longo do tempo. Neste tutorial você monta esse pipeline em Python — do modelo de dados no SQLite até o relatório final — usando a CaptchaAI para resolver o desafio automaticamente quando a página de preços exige verificação.
Antes de sair coletando: monitore apenas dados publicamente disponíveis e leia os termos de uso do site-alvo. Se o que for coletado envolver dados pessoais (nomes, e-mails em depoimentos, por exemplo), considere as obrigações da LGPD ao armazenar e processar esse histórico.
Como funciona o pipeline de monitoramento
A arquitetura é simples de propósito: um scraper visita cada site de concorrente, um solucionador de CAPTCHA resolve o desafio quando ele aparece, os dados extraídos vão para um banco SQLite e um gerador de relatórios lê esse histórico para montar a comparação.
Competitor Sites ──> CAPTCHA Solver ──> Data Extractors
│
SQLite Store
│
Dashboard Report
Esse fluxo roda bem como job agendado — um cron diário ou um workflow do GitHub Actions já são suficientes, sem precisar de servidor dedicado.
Modelagem dos dados: histórico e comparação entre concorrentes
Cada métrica coletada (um preço, um recurso, o tamanho de um catálogo) vira uma linha na tabela metrics, com o nome do concorrente, o valor bruto (texto) e, quando possível, um valor numérico para ordenar e comparar. Guardar o texto original junto do número evita perder contexto — "US$ 29,90/mês" continua legível no relatório, mas 29.9 é o que entra no cálculo.
# models.py
import sqlite3
from datetime import datetime
from dataclasses import dataclass
from typing import Optional
@dataclass
class CompetitorData:
competitor: str
metric: str
value: str
numeric_value: Optional[float] = None
url: str = ""
scraped_at: str = ""
def __post_init__(self):
if not self.scraped_at:
self.scraped_at = datetime.now().isoformat()
class CompetitorDB:
def __init__(self, path="competitor_data.db"):
self.conn = sqlite3.connect(path)
self._init()
def _init(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
competitor TEXT,
metric TEXT,
value TEXT,
numeric_value REAL,
url TEXT,
scraped_at TEXT
)
""")
self.conn.commit()
def save(self, data: CompetitorData):
self.conn.execute(
"""INSERT INTO metrics
(competitor, metric, value, numeric_value, url, scraped_at)
VALUES (?, ?, ?, ?, ?, ?)""",
(data.competitor, data.metric, data.value,
data.numeric_value, data.url, data.scraped_at),
)
self.conn.commit()
def get_history(self, competitor, metric, limit=30):
cursor = self.conn.execute(
"""SELECT value, numeric_value, scraped_at
FROM metrics
WHERE competitor = ? AND metric = ?
ORDER BY scraped_at DESC LIMIT ?""",
(competitor, metric, limit),
)
return cursor.fetchall()
def latest_comparison(self, metric):
cursor = self.conn.execute(
"""SELECT competitor, value, numeric_value, MAX(scraped_at) as latest
FROM metrics WHERE metric = ?
GROUP BY competitor ORDER BY numeric_value""",
(metric,),
)
return cursor.fetchall()
get_history alimenta o gráfico de tendência de um único concorrente; latest_comparison monta o retrato mais recente lado a lado entre todos eles.
Resolvendo o CAPTCHA na página do concorrente
Muitas páginas de preços colocam um reCAPTCHA v2 na frente para dificultar coleta automatizada. A classe abaixo detecta a data-sitekey no HTML, envia a tarefa para a API da CaptchaAI (in.php), faz o polling em res.php até receber o token e reenvia a requisição original com g-recaptcha-response preenchido. O reCAPTCHA v2 costuma resolver em menos de 60 segundos, com alta taxa de sucesso nos tipos suportados — para esse volume (um handful de concorrentes, uma vez por dia), o plano BASIC (US$ 15/mês, 5 threads) já cobre o consumo.
# solver.py
import requests
import time
import re
import os
class CaptchaSolver:
def __init__(self):
self.api_key = os.environ["CAPTCHAAI_API_KEY"]
def solve_if_needed(self, session, url, html):
if "data-sitekey" not in html:
return html
match = re.search(r'data-sitekey="([^"]+)"', html)
if not match:
return html
sitekey = match.group(1)
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": url,
"json": 1,
}, timeout=30)
task_id = resp.json()["request"]
time.sleep(15)
for _ in range(24):
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get",
"id": task_id, "json": 1,
}, timeout=15)
data = resp.json()
if data.get("status") == 1:
post_resp = session.post(url, data={
"g-recaptcha-response": data["request"],
}, timeout=30)
return post_resp.text
if data["request"] != "CAPCHA_NOT_READY":
raise RuntimeError(data["request"])
time.sleep(5)
raise TimeoutError("CAPTCHA solve timeout")
Se a página não tiver data-sitekey, o método simplesmente devolve o HTML original — nenhum custo extra é gerado quando o CAPTCHA não aparece.
Coletando preços, recursos e catálogo automaticamente
Com o CAPTCHA resolvido quando necessário, o scraper usa BeautifulSoup para extrair três tipos de métrica: preço por plano (com seletor de card e de preço configuráveis por concorrente), lista de recursos e contagem de produtos no catálogo. Cada valor numérico é limpo com uma regex antes de virar float ou int, para que o relatório e as consultas de tendência funcionem sobre números de verdade, não sobre texto.
# scraper.py
import requests
import re
from bs4 import BeautifulSoup
from solver import CaptchaSolver
from models import CompetitorData
class CompetitorScraper:
def __init__(self):
self.solver = CaptchaSolver()
self.session = requests.Session()
self.session.headers["User-Agent"] = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/125.0.0.0 Safari/537.36"
)
def scrape_pricing(self, competitor_name, url, plan_selector, price_selector):
html = self._fetch(url)
soup = BeautifulSoup(html, "html.parser")
plans = soup.select(plan_selector)
data = []
for plan in plans:
name_el = plan.select_one("h3, h2, .plan-name")
price_el = plan.select_one(price_selector)
if not name_el or not price_el:
continue
price_text = price_el.get_text(strip=True)
match = re.search(r'[\d,.]+', price_text)
numeric = float(match.group().replace(",", "")) if match else None
data.append(CompetitorData(
competitor=competitor_name,
metric=f"price_{name_el.get_text(strip=True).lower().replace(' ', '_')}",
value=price_text,
numeric_value=numeric,
url=url,
))
return data
def scrape_features(self, competitor_name, url, feature_list_selector):
html = self._fetch(url)
soup = BeautifulSoup(html, "html.parser")
features = soup.select(f"{feature_list_selector} li")
return [
CompetitorData(
competitor=competitor_name,
metric="feature",
value=f.get_text(strip=True),
url=url,
)
for f in features if f.get_text(strip=True)
]
def scrape_product_count(self, competitor_name, url, count_selector):
html = self._fetch(url)
soup = BeautifulSoup(html, "html.parser")
el = soup.select_one(count_selector)
if el:
text = el.get_text(strip=True)
match = re.search(r'[\d,]+', text)
if match:
count = int(match.group().replace(",", ""))
return CompetitorData(
competitor=competitor_name,
metric="product_count",
value=text,
numeric_value=count,
url=url,
)
return None
def _fetch(self, url):
resp = self.session.get(url, timeout=20)
return self.solver.solve_if_needed(self.session, url, resp.text)
Repare que _fetch é o único ponto que passa pelo solver — todos os métodos de coleta reaproveitam essa mesma requisição, então o CAPTCHA só é resolvido uma vez por página visitada.
Gerando o relatório comparativo
O gerador de relatório lê o snapshot mais recente de cada métrica com latest_comparison e monta um texto simples, pronto para enviar por e-mail ou colar num canal do Slack. generate_trend faz o mesmo trabalho para um único concorrente e uma única métrica, útil quando você quer acompanhar a curva de preço de um plano específico.
# report.py
from models import CompetitorDB
def generate_report(db: CompetitorDB, metrics):
lines = ["=" * 60, "Competitor Analysis Report", "=" * 60, ""]
for metric in metrics:
results = db.latest_comparison(metric)
if not results:
continue
lines.append(f"--- {metric.replace('_', ' ').title()} ---")
for comp, value, numeric, ts in results:
marker = ""
if numeric is not None:
marker = f" (${numeric:,.2f})" if "price" in metric else f" ({numeric:,.0f})"
lines.append(f" {comp}: {value}{marker}")
lines.append("")
return "\n".join(lines)
def generate_trend(db: CompetitorDB, competitor, metric, periods=10):
history = db.get_history(competitor, metric, limit=periods)
if not history:
return f"No data for {competitor} — {metric}"
lines = [f"Trend: {competitor} — {metric}", "-" * 40]
for value, numeric, ts in reversed(history):
date = ts[:10]
lines.append(f" {date}: {value}")
return "\n".join(lines)
Executando o pipeline de ponta a ponta
main.py amarra tudo: percorre a lista de concorrentes configurados, coleta os preços de cada um, salva no banco e, ao final, gera e imprime o relatório consolidado — além de gravar uma cópia em competitor_report.txt. O time.sleep(5) entre concorrentes evita rajadas de requisições contra o mesmo IP em sequência.
Uma equipe de produto de uma loja virtual, por exemplo, poderia agendar esse script para rodar todo dia às 7h no horário de Brasília (um worker na região sa-east-1 da AWS mantém a latência baixa até os sites-alvo), e revisar o relatório no início do expediente.
# main.py
import time
from models import CompetitorDB
from scraper import CompetitorScraper
from report import generate_report
COMPETITORS = [
{
"name": "Competitor A",
"pricing_url": "https://competitor-a.example.com/pricing",
"plan_selector": ".pricing-plan",
"price_selector": ".price",
},
{
"name": "Competitor B",
"pricing_url": "https://competitor-b.example.com/pricing",
"plan_selector": ".plan-card",
"price_selector": ".plan-price",
},
]
def main():
db = CompetitorDB()
scraper = CompetitorScraper()
for comp in COMPETITORS:
print(f"Scraping {comp['name']}...")
try:
pricing = scraper.scrape_pricing(
comp["name"], comp["pricing_url"],
comp["plan_selector"], comp["price_selector"],
)
for p in pricing:
db.save(p)
print(f" {p.metric}: {p.value}")
except Exception as e:
print(f" Error: {e}")
time.sleep(5)
# Generate report
metrics = ["price_basic", "price_pro", "price_enterprise", "product_count"]
report = generate_report(db, metrics)
print(report)
with open("competitor_report.txt", "w") as f:
f.write(report)
if __name__ == "__main__":
main()
Troque a lista COMPETITORS pelos seus alvos reais e ajuste os seletores CSS de cada um — cada site organiza o HTML de preços de um jeito diferente, então não existe um seletor universal.
Solução de problemas comuns
A maioria dos problemas nesse pipeline cai em uma destas quatro categorias:
| Problema | Causa | Correção |
|---|---|---|
| Preços não extraídos | Incompatibilidade de seletor | Inspecione o HTML da página e atualize os seletores por concorrente |
| Faltam dados históricos | Primeira execução | Os dados se acumulam; rode diariamente para ter visibilidade de tendência |
| CAPTCHA aparece toda vez na página de preços | Detecção de bot | Adicione intervalos entre requisições e reaproveite cookies de sessão |
| Relatório mostra dados desatualizados | Mesma entrada reinserida | Use latest_comparison, que agrupa pela data mais recente (MAX) |
Perguntas frequentes
É permitido monitorar o site de um concorrente assim?
Monitore apenas dados publicamente disponíveis e verifique os termos de uso do site antes de automatizar a coleta. Se o que for armazenado envolver dados pessoais, considere as obrigações da LGPD ao guardar e processar esse histórico.
Qual plano da CaptchaAI atende ao volume desse pipeline?
Para monitorar poucos concorrentes uma vez por dia, o plano BASIC (US$ 15/mês, 5 threads) costuma bastar, já que o script roda sequencialmente. Se você aumentar a frequência ou paralelizar a coleta entre vários concorrentes ao mesmo tempo, o plano STANDARD (US$ 30/mês, 15 threads) dá mais margem.
Como visualizo a evolução dos preços ao longo do tempo?
Exporte os dados do SQLite e plote com matplotlib, ou envie a saída em CSV para o Planilhas Google/Excel e monte o gráfico ali mesmo.
Consigo monitorar recursos e tamanho de catálogo, não só preço?
Sim. Use scrape_features para listas de recursos e scrape_product_count para o tamanho do catálogo. O mesmo padrão serve para qualquer métrica nova: basta escrever um scraper e salvar com CompetitorData.
Como evito ser bloqueado durante a coleta diária?
Mantenha intervalos entre requisições (o time.sleep(5) do exemplo já ajuda), reaproveite a sessão com cookies e evite rodar o pipeline inteiro em rajada. Se o CAPTCHA passar a aparecer com mais frequência, é sinal de que o intervalo está curto demais para aquele site.
Guias relacionados
Para ir além do painel de concorrência, veja também:
Monitore a concorrência em escala — comece com a CaptchaAI.