"""
Scraper JUCEMG — Junta Comercial do Estado de Minas Gerais
URLs descobertas em 2026-02-25:
- /pagina/139 = menu principal (links para sub-paginas)
- /pagina/140 = lista alfabetica com contatos completos (USAR ESTA)
- /pagina/141 = lista por antiguidade com tabela (apenas nome + matricula)
- /pagina/142 = matriculas canceladas
Metodo: httpx + BeautifulSoup
Pagina /pagina/140 contem paragrafos com nome + matricula + endereco + telefone + email
Total: 218 leiloeiros ativos (alguns com status inline: Suspenso, Licenciado)
"""
from __future__ import annotations
import logging
import re
from typing import List
from .base_scraper import AbstractJuntaScraper, Leiloeiro
logger = logging.getLogger(__name__)
RE_MATRICULA_MG = re.compile(r"[Mm]atr[íi]cula:?\s*(\d+)\s+de\s+(\d{2}/\d{2}/\d{4})|[Mm]atr[íi]cula:?\s*n[º°]?\s*(\d+)", re.IGNORECASE)
RE_PREPOSTO = re.compile(r"[Pp]reposto:?\s*(.+)")
RE_TELEFONE = re.compile(r"[Tt]elefones?:?\s*(.+)")
RE_EMAIL = re.compile(r"(?:e-mail|email):?\s*(.+)", re.IGNORECASE)
RE_SITE = re.compile(r"(?:site|www\.)(.+)", re.IGNORECASE)
RE_STATUS_INLINE = re.compile(r"\((Suspen|Licencia|Cancel|Irregular)[^)]*\)", re.IGNORECASE)
class JucemgScraper(AbstractJuntaScraper):
estado = "MG"
junta = "JUCEMG"
url = "https://jucemg.mg.gov.br/pagina/139/leiloeiros-oficiais"
# URL da lista alfabetica com contatos completos
_URL_ALFA = "https://jucemg.mg.gov.br/pagina/140/leiloeiros-ordem-alfabetica"
# URL da lista por antiguidade com tabela (nome + matricula)
_URL_ANT = "https://jucemg.mg.gov.br/pagina/141/leiloeiros-antiguidade"
def _parse_alfabetica(self, soup) -> List[dict]:
"""
Parseia a pagina /pagina/140 (ordem alfabetica).
Cada leiloeiro e um bloco
:
NOME COMPLETO
Matricula: N de DD/MM/AAAA
Preposto: ...
Endereco, Bairro, Cidade - MG, CEP
Telefones: ...
email
site
"""
records = []
content = soup.select_one(
".conteudo-pagina, .page-content, .conteudo, article .content, main .content, "
".entry-content, #conteudo, .corpo-pagina"
)
if not content:
content = soup.body or soup
for p in content.find_all("p"):
strong = p.find("strong")
if not strong:
continue
nome_raw = self.clean(strong.get_text())
if not nome_raw or len(nome_raw) < 3:
continue
# Verificar status inline no nome (ex: "NOME (Suspenso)")
status_match = RE_STATUS_INLINE.search(nome_raw)
situacao = None
if status_match:
situacao = status_match.group(0).strip("()")
nome_raw = RE_STATUS_INLINE.sub("", nome_raw).strip()
# Coletar linhas do paragrafo (apos o )
lines = []
for el in p.children:
if el == strong:
continue
if hasattr(el, "get_text"):
line = self.clean(el.get_text())
elif isinstance(el, str):
line = self.clean(str(el))
else:
continue
if line and line != nome_raw:
lines.append(line)
record = {
"nome": nome_raw,
"municipio": "Belo Horizonte",
"situacao": situacao,
}
for line in lines:
m = RE_MATRICULA_MG.search(line)
if m:
record["matricula"] = m.group(1) or m.group(3)
if m.group(2):
record["data_registro"] = m.group(2)
continue
m = RE_TELEFONE.search(line)
if m:
record["telefone"] = self.clean(m.group(1))
continue
m = RE_EMAIL.search(line)
if m:
record["email"] = self.clean(m.group(1))
continue
# Linha de endereco: contem cidade/MG ou CEP
if (re.search(r"/\s*MG\b|\bMG\s*,?\s*CEP|CEP\s*\d", line) or
(len(line) > 10 and not RE_PREPOSTO.match(line) and
not RE_SITE.match(line) and
not record.get("endereco"))):
m_cidade = re.search(r"([A-ZÁÉÍÓÚÀÃÕÇ][A-Za-záéíóúàãõç\s]+)\s*-?\s*MG", line)
if m_cidade:
record["municipio"] = m_cidade.group(1).strip()
if not record.get("endereco"):
record["endereco"] = line
records.append(record)
return records
def _parse_antiguidade(self, soup) -> List[dict]:
"""
Parseia tabela /pagina/141 (antiguidade).
Tabela com 2 colunas: "Ordem de antiguidade e nome" + "No de matricula"
Alguns nomes tem notas de status inline.
"""
records = []
for table in soup.find_all("table"):
rows = table.find_all("tr")
if len(rows) < 2:
continue
for row in rows[1:]:
cells = row.find_all(["td", "th"])
if len(cells) < 2:
continue
nome_raw = self.clean(cells[0].get_text())
matricula = self.clean(cells[1].get_text()) if len(cells) > 1 else None
if not nome_raw or len(nome_raw) < 3:
continue
# Extrair status inline
status_match = RE_STATUS_INLINE.search(nome_raw)
situacao = None
if status_match:
situacao = status_match.group(0).strip("()")
nome_raw = RE_STATUS_INLINE.sub("", nome_raw).strip()
records.append({
"nome": nome_raw,
"matricula": matricula,
"situacao": situacao,
"municipio": "Belo Horizonte",
})
if records:
break
return records
async def parse_leiloeiros(self) -> List[Leiloeiro]:
# Estrategia 1: Pagina alfabetica (tem contatos completos)
soup = await self.fetch_page(url=self._URL_ALFA)
if soup:
records = self._parse_alfabetica(soup)
if records:
logger.info("[MG] Pagina alfabetica: %d registros", len(records))
return [self.make_leiloeiro(**r) for r in records]
# Estrategia 2: Tabela de antiguidade (pelo menos nome + matricula)
soup = await self.fetch_page(url=self._URL_ANT)
if soup:
records = self._parse_antiguidade(soup)
if records:
logger.info("[MG] Tabela antiguidade: %d registros", len(records))
return [self.make_leiloeiro(**r) for r in records]
# Estrategia 3: Pagina principal de leiloeiros
soup = await self.fetch_page(url=self.url)
if not soup:
soup = await self.fetch_page_js(url=self.url, wait_ms=3000)
if not soup:
return []
results: List[Leiloeiro] = []
# Tenta tabela
for table in soup.find_all("table"):
rows = table.find_all("tr")
if len(rows) < 2:
continue
headers = [self.clean(th.get_text()) for th in rows[0].find_all(["th", "td"])]
col = {(h or "").lower(): i for i, h in enumerate(headers)}
def gcol(cells, frags):
for k, i in col.items():
if any(f in k for f in frags) and i < len(cells):
return self.clean(cells[i].get_text())
return None
for row in rows[1:]:
cells = row.find_all(["td", "th"])
if not cells:
continue
nome = gcol(cells, ["nome"]) or self.clean(cells[0].get_text())
if not nome or len(nome) < 3:
continue
results.append(self.make_leiloeiro(
nome=nome,
matricula=gcol(cells, ["matr", "registro"]),
situacao=gcol(cells, ["situ", "status"]),
municipio="Belo Horizonte",
telefone=gcol(cells, ["tel", "fone"]),
email=gcol(cells, ["email"]),
))
logger.info("[MG] Total: %d registros", len(results))
return results