Percorrer a rede societária a partir de uma empresa ou pessoa — empresa → sócios → empresas dos sócios — controlando profundidade e amplitude
A rede societária brasileira é um grafo: empresas se conectam a pessoas (e a outras empresas) pelo quadro de sócios. A partir de um único CNPJ ou CPF você consegue mapear toda a vizinhança — sócios diretos, as outras empresas desses sócios, os sócios dessas empresas, e assim por diante.Cada salto nesse caminho é um grau de conexão.
A travessia usa apenas dois endpoints, que são o inverso um do outro:
Empresa → sócios
GET /empresas/cnpj/{cnpj} retorna a empresa já com o array socios. Cada sócio traz documento (CPF/CNPJ) e tipo.
Pessoa → empresas
GET /empresas/cpf/{cpf} faz a busca reversa: todas as empresas onde o CPF é sócio — cada uma também já com seu socios.
Como o /empresas/cpfjá devolve os sócios de cada empresa, ao expandir a rede a partir de uma pessoa você ganha o próximo grau na mesma resposta — sem precisar voltar no /empresas/cnpj para cada empresa descoberta. Menos chamadas, menos latência.
A travessia é uma busca em largura (BFS): você processa um grau inteiro antes de descer para o próximo. Três controles são essenciais:
1
Profundidade máxima (max_depth)
Quantos graus descer. O número de nós cresce exponencialmente — raramente faz sentido passar de 2 ou 3.
2
Amplitude por nó (fan-out)
Quantos sócios/empresas expandir por nó. Sócios de uma holding ou empresas de um sócio profissional podem chegar a centenas — limite para não explodir.
3
Deduplicação (visited)
A rede tem ciclos (A é sócio de B, B é sócio de A). Sem um conjunto de “já visitados”, o percurso entra em loop.
import requestsBASE = "https://221b-api.sherlocker.com.br/api/v1"TOKEN = "SEU_TOKEN"def get(endpoint, **params): params["token"] = TOKEN return requests.get(f"{BASE}{endpoint}", params=params).json()# Grau 0 — a empresa e seu quadro societárioempresa = get("/empresas/cnpj/33000167000101")# Grau 1 — loop pelos sóciosfor socio in empresa.get("socios", []): print(f"{empresa['razao_social']} —> {socio['nome']} ({socio['qualificacao']})") # Grau 2 — outras empresas de cada sócio pessoa física doc = socio.get("documento", "") if socio.get("tipo") == "Pessoa Física" and doc: cpf = "".join(c for c in doc if c.isdigit()) for emp in get(f"/empresas/cpf/{cpf}").get("empresas", []): print(f" ↳ também sócio de: {emp['razao_social']}")
Quando você expande uma pessoa, o /empresas/cpf já devolve os sócios de cada empresa. Dá para descer um grau extra sem nenhuma chamada nova:
def expandir_pessoa(cpf, fan_out=10): """Um único GET entrega grau 1 (empresas) e grau 2 (sócios dessas empresas).""" resp = get(f"/empresas/cpf/{so_digitos(cpf)}", limit=fan_out) grau1, grau2 = [], [] for emp in resp.get("empresas", []): grau1.append(emp["documento"]) # empresa do sócio (grau 1) for s in emp.get("socios", [])[:fan_out]: grau2.append((emp["documento"], s["documento"])) # co-sócio (grau 2) — de graça return grau1, grau2
O número de nós cresce de forma exponencial com a profundidade. Se cada nó tem em média f conexões (fan-out) e você desce d graus:
nós no grau k = f^ktotal de nós = 1 + f + f² + ... + f^d = (f^(d+1) − 1) / (f − 1)
E como cada nó expandido custa aproximadamente uma chamada, o número de chamadas à API segue a mesma ordem de grandeza (antes da deduplicação, que reduz o real).
def estimar_rede(fan_out, max_depth): if fan_out == 1: return max_depth + 1 total = (fan_out ** (max_depth + 1) - 1) // (fan_out - 1) por_grau = [fan_out ** k for k in range(max_depth + 1)] return total, por_grautotal, por_grau = estimar_rede(fan_out=10, max_depth=3)print(por_grau) # [1, 10, 100, 1000]print(total) # 1111 nós no pior caso
fan-out
grau 1
grau 2
grau 3
total (pior caso)
5
5
25
125
156
10
10
100
1.000
1.111
20
20
400
8.000
8.421
A tabela é o teto teórico. Na prática, a deduplicação (visited) corta muito, porque redes reais são densamente interligadas — os mesmos sócios e empresas reaparecem. Ainda assim, trate fan_out e max_depth como orçamento de chamadas: 2 graus com fan-out 10 já é o ponto de equilíbrio para a maioria das investigações.
MAX_CHAMADAS = 200def graus_com_orcamento(cnpj_raiz, max_depth=3, fan_out=10, max_chamadas=MAX_CHAMADAS): chamadas = 0 # ... mesma BFS, mas antes de cada get(): # if chamadas >= max_chamadas: break # chamadas += 1 # Garante custo previsível independente do tamanho real da rede.