191 lines
6.1 KiB
Python
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()
|