POO em Python: extraindo dados do Cartão CNPJ com PyPDF2

Numa entrevista técnica para uma startup de automação para indústrias, o pessoal comentou que usava bastante PyPDF2 para manipular PDF e trabalhava muito com orientação a objetos no dia a dia. Eu conhecia os dois conceitos.
Decidi resolver um problema real que eu já tinha e, de quebra, treinar POO na prática em vez de num exemplo de curso.
O problema
Todo mês eu precisava cadastrar clientes novos no sistema de emissão de notas fiscais. O processo começava sempre do mesmo jeito: abrir o PDF do Cartão CNPJ do cliente e digitar, campo por campo, CNPJ, razão social, CEP, número e complemento do endereço, e-mail e telefone.
Os dados já estavam todos ali, no PDF, só que em texto solto, sem estrutura nenhuma pra extrair programaticamente, e cada campo aparecia num formato diferente: o CNPJ com pontuação (12.345.678/0001-90), o CEP com um ponto no meio (01.310-100), o telefone com DDD entre parênteses.
Copiar e colar campo por campo não é difícil, mas também não era produtivo, e é fácil errar um dígito no CNPJ sem perceber.
Extraindo texto de PDF com PyPDF2
A primeira versão do script (ainda no histórico de commits do repositório no GitHub) era só um punhado de funções soltas, cada uma abrindo o PDF de novo e rodando sua própria regex. Funcionava, mas resolvi refatorar pra uma classe. O objetivo era praticar POO de verdade, não só ler PDF com Python.
import PyPDF2
import re
class ExtratorCartaoCnpj():
def __init__(self, caminho_arquivo):
self.caminho_arquivo = caminho_arquivo
self.texto_pdf = ''
def carregar_texto_pdf(self):
with open(self.caminho_arquivo, "rb") as arquivo:
leitor_pdf = PyPDF2.PdfReader(arquivo)
partes = []
for pagina in leitor_pdf.pages:
partes.append(pagina.extract_text() or "")
self.texto_pdf = "\n".join(partes)
return self.texto_pdf
O __init__ só guarda o caminho do arquivo. O texto do PDF é carregado sob demanda em carregar_texto_pdf(), e fica armazenado em self.texto_pdf pra todos os métodos de extração reaproveitarem sem reabrir o arquivo.
Organizando a extração com POO
Com o texto do PDF em mãos, cada campo (CNPJ, CEP, telefone…) virou um método próprio da classe, usando re.search pra localizar o padrão:
def extrair_cnpj(self):
m = re.search(r"\b\d{2}\.\d{3}\.\d{3}/\d{4}-\d{2}\b", self.texto_pdf)
return m.group(0) if m else None
def extrair_razao_social(self):
m = re.search(r"NOME EMPRESARIAL\s+([A-Z0-9 .!'\-–&]+)", self.texto_pdf)
return m.group(1).strip() if m else None
def extrair_tudo(self):
return {
"cnpj": self.extrair_cnpj(),
"razao_social": self.extrair_razao_social(),
"cep": self.extrair_cep(),
"numero": self.extrair_numero_logradouro(),
"complemento": self.extrair_complemento_logradouro(),
"email": self.extrair_email(),
"telefone": self.extrair_telefone(),
}
Uso básico:
extrator = ExtratorCartaoCnpj("cartao_cnpj.pdf")
extrator.carregar_texto_pdf()
dados = extrator.extrair_tudo()
print(dados["cnpj"], dados["razao_social"])
A parte mais trabalhosa não foi entender self ou __init__. Foi decidir como dividir a extração em métodos. Cada campo ganhou seu próprio método (extrair_cnpj, extrair_cep, extrair_telefone…), todos lendo do mesmo self.texto_pdf, e um método extrair_tudo() que só orquestra e junta o resultado num dicionário.
Isso trouxe um ganho que eu não esperava: quando um regex parava de funcionar num PDF com formatação diferente, eu sabia exatamente qual método (e qual campo) ajustar, sem mexer no resto da classe. Com as funções soltas da primeira versão, esse isolamento não existia.
Um motor, não só uma solução pontual
O cadastro de cliente na emissão de nota fiscal foi só o motivo que me fez começar, mas a classe em si não depende desse contexto: ela só extrai dados do Cartão CNPJ e devolve num dicionário.
Na prática, virou um motor que dá pra plugar em qualquer automação que precise desses dados: validação de cadastro, integração com outro sistema, geração de relatório. O extrair_tudo() é o ponto de entrada único; o que acontece com os dados depois é decisão de quem consome a classe.
Resultado
O cadastro que antes era digitado campo por campo virou copiar o resultado de extrair_tudo(), o que tirou a parte manual e repetitiva do processo e reduziu bastante o risco de erro de digitação num CNPJ ou CEP.
Pontos importantes
Regex depende de padrão consistente: os métodos fazem re.search direto no texto do PDF. Se o Cartão CNPJ vier com uma formatação diferente da esperada (mudança de layout, PDF de fonte diferente), o campo simplesmente retorna None. Vale sempre validar o resultado antes de usar.
POO ajuda mais na organização do que na “mágica”: o ganho real de virar classe não foi performance nem funcionalidade nova. Foi conseguir isolar cada extração num método próprio, com um ponto de entrada único (extrair_tudo) pra quem só quer consumir a classe sem entender os detalhes de cada regex.
Perguntas frequentes
Como extrair texto de um PDF com Python?
A forma mais simples é usar uma biblioteca como o PyPDF2: você abre o arquivo em modo binário, cria um PdfReader e percorre as páginas chamando extract_text() em cada uma, juntando o texto de todas elas numa única string. Pra PDFs mais complexos, com tabelas ou layout mais visual, vale considerar bibliotecas como pdfplumber.
Como validar um CNPJ em Python?
Extrair um CNPJ de um texto (com regex, por exemplo) não é a mesma coisa que validar se ele é um CNPJ real. Validar exige calcular os dois dígitos verificadores com um algoritmo específico (módulo 11) e comparar com os dígitos que vieram no documento, o que dá pra fazer com uma função própria ou com bibliotecas como validate-docbr.
O PyPDF2 funciona com PDF escaneado (imagem)?
Não. O PyPDF2 lê o texto que já existe digitalmente no PDF, ele não faz OCR (reconhecimento óptico de caracteres). Se o documento for uma foto ou scan sem camada de texto, extract_text() retorna vazio ou None pra tudo. Nesse caso, precisaria de uma ferramenta de OCR como o Tesseract antes de aplicar os regex.
Dá pra adaptar uma classe de extração de PDF em Python pra outros documentos, como RG, contrato ou boleto?
Sim, e essa é a vantagem de separar a extração do texto bruto (que não depende do tipo de documento) dos métodos que interpretam esse texto (que são específicos de cada documento). Um método cuida só de abrir o PDF e extrair o texto; métodos separados, um por campo, usam regex pra localizar cada informação dentro desse texto. Pra adaptar pra outro documento, basta trocar os regex desses métodos de interpretação pelo padrão do novo tipo de documento, mantendo a mesma estrutura de classe.
O que fazer se o PyPDF2 não encontrar um campo (retorna None)?
Sempre valide o retorno antes de usar: se o regex não encontrar o padrão esperado, o método retorna None em vez de lançar um erro. Isso costuma acontecer quando o PDF vem com uma formatação diferente da esperada, como outro layout ou fonte diferente. Vale logar quais campos vieram None pra saber quando o regex precisa de ajuste.
Curtiu o conteúdo ou tem um problema parecido pra resolver? Bora trocar uma ideia.
Fale comigo no WhatsApp