playbook/antigravity-awesome-skills/skills/junta-leiloeiros/scripts/run_all.py

191 lines
6.1 KiB
Python

"""
Orquestrador de scraping — coleta dados de todas as 27 Juntas Comerciais do Brasil.
Uso:
python scripts/run_all.py # todos os estados
python scripts/run_all.py --estado SP RJ MG # estados específicos
python scripts/run_all.py --concurrency 5 # paralelismo (default: 5)
python scripts/run_all.py --dry-run # mostra o que seria coletado
"""
from __future__ import annotations
import argparse
import asyncio
import json
import logging
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Optional
# Ajusta PYTHONPATH para imports relativos funcionarem
sys.path.insert(0, str(Path(__file__).parent))
from scraper.states import SCRAPERS, get_all_scrapers, get_scraper
from db import Database
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger(__name__)
LOG_FILE = Path(__file__).parent.parent / "data" / "scraping_log.json"
async def scrape_state(estado: str, semaphore: asyncio.Semaphore) -> dict:
"""Scrapa um estado e retorna resultado com metadados."""
async with semaphore:
scraper = get_scraper(estado)
if not scraper:
return {
"estado": estado,
"status": "SCRAPER_NAO_ENCONTRADO",
"count": 0,
"records": [],
"error": None,
"scraped_at": datetime.now(timezone.utc).isoformat(),
}
try:
records = await scraper.scrape()
return {
"estado": estado,
"junta": scraper.junta,
"url": scraper.url,
"status": "OK" if records else "VAZIO",
"count": len(records),
"records": [r.to_dict() for r in records],
"error": None,
"scraped_at": datetime.now(timezone.utc).isoformat(),
}
except Exception as exc:
logger.exception("[%s] Erro no scraping: %s", estado, exc)
return {
"estado": estado,
"status": "ERRO",
"count": 0,
"records": [],
"error": str(exc),
"scraped_at": datetime.now(timezone.utc).isoformat(),
}
async def run(estados: Optional[List[str]], concurrency: int, dry_run: bool) -> None:
estados_alvo = [e.upper() for e in estados] if estados else list(SCRAPERS.keys())
if dry_run:
print(f"\n[DRY-RUN] Estados que seriam coletados ({len(estados_alvo)}):")
for uf in estados_alvo:
s = get_scraper(uf)
if s:
print(f" {uf}: {s.junta} -> {s.url}")
else:
print(f" {uf}: scraper nao encontrado")
return
logger.info("Iniciando coleta de %d estados (concurrency=%d)", len(estados_alvo), concurrency)
semaphore = asyncio.Semaphore(concurrency)
tasks = [scrape_state(uf, semaphore) for uf in estados_alvo]
results = await asyncio.gather(*tasks)
# Persistir no banco
db = Database()
db.init()
total_coletados = 0
total_salvos = 0
log_entries = []
for res in results:
estado = res["estado"]
status = res["status"]
count = res["count"]
total_coletados += count
if res["records"]:
saved = db.upsert_many(res["records"])
total_salvos += saved
logger.info("[%s] %d leiloeiros coletados, %d salvos", estado, count, saved)
else:
if status == "OK":
status = "VAZIO"
logger.warning("[%s] STATUS=%s error=%s", estado, status, res.get("error"))
log_entries.append({
"estado": estado,
"junta": res.get("junta", "?"),
"url": res.get("url", "?"),
"status": status,
"count": count,
"error": res.get("error"),
"scraped_at": res["scraped_at"],
})
# Salvar log
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(LOG_FILE, "w", encoding="utf-8") as f:
json.dump(
{
"run_at": datetime.now(timezone.utc).isoformat(),
"total_estados": len(estados_alvo),
"total_coletados": total_coletados,
"total_salvos": total_salvos,
"estados": log_entries,
},
f,
ensure_ascii=False,
indent=2,
)
# Resumo final
print("\n" + "=" * 60)
print(f"RESUMO DA COLETA")
print("=" * 60)
ok = [e for e in log_entries if e["status"] == "OK"]
vazios = [e for e in log_entries if e["status"] == "VAZIO"]
erros = [e for e in log_entries if e["status"] not in ("OK", "VAZIO")]
print(f" OK: {len(ok)} estados | {total_coletados} leiloeiros coletados")
print(f" VAZIO: {len(vazios)} estados | {[e['estado'] for e in vazios]}")
print(f" ERRO: {len(erros)} estados | {[e['estado'] for e in erros]}")
print(f" TOTAL SALVO NO BANCO: {total_salvos}")
print(f" LOG: {LOG_FILE}")
print("=" * 60)
# Estatísticas do banco
stats = db.get_stats()
if stats:
print("\nESTATÍSTICAS POR ESTADO:")
print(f"{'UF':<5} {'Junta':<12} {'Total':>6} {'Ativos':>7}")
print("-" * 35)
for s in stats:
print(f"{s['estado']:<5} {s['junta']:<12} {s['total']:>6} {s['ativos']:>7}")
def main():
parser = argparse.ArgumentParser(
description="Coleta dados de leiloeiros de todas as Juntas Comerciais do Brasil"
)
parser.add_argument(
"--estado", nargs="*", metavar="UF",
help="Estados específicos (ex: SP RJ MG). Padrão: todos os 27."
)
parser.add_argument(
"--concurrency", type=int, default=5,
help="Número de scrapers em paralelo (default: 5)"
)
parser.add_argument(
"--dry-run", action="store_true",
help="Mostra o que seria coletado sem executar"
)
args = parser.parse_args()
asyncio.run(run(args.estado, args.concurrency, args.dry_run))
if __name__ == "__main__":
main()