Skip to the content.

Presidio em português: por que o padrão não pega CPF, e como resolver

PT: esta página está em português primeiro, ao contrário do resto da documentação, porque o problema que ela resolve é procurado em português. EN: this page is Portuguese first, unlike the rest of the docs, because the problem it solves is searched for in Portuguese. Short English summary at the end.

O Presidio é a biblioteca aberta da Microsoft para achar e anonimizar dado pessoal em texto. Ele funciona bem em inglês. Em português, a primeira coisa que quase todo mundo tenta é achar um CPF, e não acha nada. Esta página explica por quê, mostra três caminhos e diz quando cada um vale.

O que acontece

from presidio_analyzer import AnalyzerEngine

analyzer = AnalyzerEngine()
texto = "Paciente Maria, CPF 529.982.247-25, cartao SUS 729 1417 7763 1701."
print(analyzer.analyze(text=texto, language="en"))

O CPF não aparece. Dependendo da configuração, ou não sai nada, ou sai um PERSON errado, ou o número é classificado como algo genérico. Não é defeito de instalação.

Por que

Três razões somadas, e todas as três precisam ser entendidas para a solução fazer sentido.

O Presidio não procura “dado pessoal”, ele procura entidades que alguém registrou. A arquitetura é um registro de reconhecedores, um por tipo de entidade. Existem reconhecedores prontos para US_SSN, UK_NHS, IN_AADHAAR e outros. Nenhum para CPF, CNPJ, título de eleitor ou cartão do SUS. Onde não há reconhecedor, não há detecção, e o Presidio não avisa que não sabe procurar.

Reconhecedor é preso a um idioma. Cada reconhecedor declara supported_language. Carregar os reconhecedores padrão com language="pt" traz muito pouco, porque quase todos declaram en. Hoje a chave pt não aparece no default_recognizers.yaml do projeto.

Regex sozinho produz falso positivo em volume. Um CPF é onze dígitos, e onze dígitos em texto administrativo brasileiro também é protocolo, matrícula, número de processo e código interno. Detectar por formato apenas transforma o relatório em ruído. É aqui que entra a parte que mais muda o resultado, adiante.

Caminho 1: escrever o seu reconhecedor

É o caminho que ensina como o Presidio funciona, e que serve para qualquer identificador, inclusive os internos da sua empresa.

Um CPF tem dois dígitos verificadores calculados por módulo 11 sobre os nove primeiros. Isso é o que separa um CPF de onze dígitos quaisquer, e é a diferença entre um relatório útil e uma lista de números.

from presidio_analyzer import Pattern, PatternRecognizer


def cpf_valido(valor: str) -> bool:
    d = [int(c) for c in valor if c.isdigit()]
    if len(d) != 11 or len(set(d)) == 1:
        return False
    for n in (9, 10):
        soma = sum(d[i] * ((n + 1) - i) for i in range(n))
        dv = (soma * 10) % 11
        if (0 if dv == 10 else dv) != d[n]:
            return False
    return True


class CpfRecognizer(PatternRecognizer):
    PATTERNS = [Pattern("cpf", r"\b\d{3}\.?\d{3}\.?\d{3}-?\d{2}\b", 0.3)]
    CONTEXT = ["cpf", "cadastro de pessoa fisica"]

    def __init__(self, supported_language: str = "pt"):
        super().__init__(
            supported_entity="BR_CPF",
            patterns=self.PATTERNS,
            context=self.CONTEXT,
            supported_language=supported_language,
        )

    def validate_result(self, pattern_text: str):
        # True sobe o score p/ 1.0, False descarta o achado, None deixa como está
        return cpf_valido(pattern_text)

E registrar:

from presidio_analyzer import AnalyzerEngine, RecognizerRegistry

registry = RecognizerRegistry(supported_languages=["pt"])
registry.add_recognizer(CpfRecognizer())
analyzer = AnalyzerEngine(registry=registry, supported_languages=["pt"])

for r in analyzer.analyze(text=texto, language="pt"):
    print(r.entity_type, texto[r.start:r.end], round(r.score, 2))

O validate_result é a peça central e é frequentemente ignorada. Devolver False faz o Presidio descartar o achado, não apenas rebaixá-lo. É isso que tira do relatório o número de protocolo que por acaso tinha onze dígitos.

Repetir esse trabalho para CNPJ, CNS, CNJ, título de eleitor, NIS, RENAVAM, CNH, CEP, placa e telefone dá bastante trabalho, e cada um tem uma regra diferente. O CNS tem rotina oficial do Ministério da Saúde. O processo judicial usa ISO 7064 módulo 97-10. O CNPJ passou a aceitar letras. É por isso que existe o caminho 2.

Caminho 2: usar o tarja pelo Presidio

O tarja implementa as 17 entidades brasileiras com a validação de cada uma, e o tarja-presidio registra todas elas no Presidio de uma vez.

pip install tarja-presidio
python -m spacy download pt_core_news_md

O tarja vem junto, é dependência do plugin.

from presidio_analyzer import AnalyzerEngine, RecognizerRegistry
from presidio_analyzer.nlp_engine import NlpEngineProvider
import tarja_presidio

nlp_engine = NlpEngineProvider(nlp_configuration={
    "nlp_engine_name": "spacy",
    "models": [{"lang_code": "pt", "model_name": "pt_core_news_md"}],
}).create_engine()

registry = RecognizerRegistry(supported_languages=["pt"])
registry.load_predefined_recognizers(languages=["pt"], nlp_engine=nlp_engine)
tarja_presidio.register(registry)

analyzer = AnalyzerEngine(registry=registry, nlp_engine=nlp_engine, supported_languages=["pt"])
for r in analyzer.analyze(text=texto, language="pt", score_threshold=0.4):
    print(r.entity_type, texto[r.start:r.end], round(r.score, 2))

O motor do spaCy continua valendo a pena mesmo com os identificadores resolvidos, porque é ele que traz PERSON e as entidades que dependem de linguagem, que os identificadores estruturados não cobrem.

Caminho 3: usar o tarja direto, sem Presidio

Se o que você precisa é só achar e mascarar identificador brasileiro, o Presidio é uma camada a mais para instalar, configurar e manter.

pip install tarja
import tarja

tarja.mask(texto)   # "Paciente Maria, CPF <BR_CPF>, cartao SUS <BR_CNS>."

O tarja não tem dependência em tempo de execução e o find() é mais rígido que a versão pelo Presidio: uma entidade que exige contexto nunca aparece sem contexto.

Qual escolher

Situação Caminho
Já usa Presidio, quer nome e endereço junto 2
Já usa Presidio e tem identificador interno próprio 1, e 2 para os brasileiros
Só precisa de identificador brasileiro, em lote 3
Quer entender como o Presidio funciona por dentro 1
Texto em inglês, sem identificador brasileiro Presidio padrão, sem nada disto

A última linha é séria. Se o seu texto é em inglês e você não precisa de identificador brasileiro, o Presidio padrão resolve e nada nesta página se aplica.

Estado do upstream

Há uma proposta aberta no repositório do Presidio adicionando reconhecedores de CPF e CNPJ ao projeto, com uma questão em aberto sobre declarar o idioma como pt ou en, já que a chave pt ainda não existe na configuração padrão deles. Se for aceita, os dois passam a vir nativos, e o tarja-presidio continua útil para as outras quinze entidades.

Limites

Nada aqui detecta nome, endereço ou dado clínico em prosa: isso depende de reconhecimento de entidade nomeada e é o que o motor do spaCy faz, com a qualidade que ele tem em português. Ausência de achado não é prova de ausência de dado pessoal. E dígito verificador válido não significa que o número pertence a alguém real, apenas que ele é bem formado.


EN, short version. Presidio ships no recognizer for Brazilian identifiers, and its recognizers are per-language while almost all defaults declare en, so CPF is simply not looked for. Format-only regex is not enough either, because eleven digits is also a protocol or case number in Brazilian administrative text. Three ways out: write a PatternRecognizer whose validate_result checks the mod-11 digits (the code above is complete), register all seventeen Brazilian entities through tarja-presidio, which is powered by tarja, or skip Presidio and use tarja directly when identifiers are all you need.