Como lidar com texto que contenham números escritos de diferentes formas
Isso é mais chato do que parece no papel. Você pega um documento, um e-mail, uma planilha — o que for — e precisa extrair, normalizar ou comparar valores numéricos que estão espalhados ali de maneiras bem diferentes. Um aparece como 1.500, outro como mil e quinhentos, mais adiante tem um XV, e ainda tem um 15% escrito por extenso. Parece bobo até você abrir o Excel ou dar um replace na mão. Vou explicar como eu resolvo isso no dia a dia, porque a primeira coisa que todo mundo tenta — regex simples — não funciona na prática.
Texto que contenham números escritos de diferentes formas: o que você realmente precisa normalizar
O problema não é só reconhecer números. É reconhecer as formas como eles podem aparecer. Aqui está a lista real que eu encuentro na prática: Algarismos arábicos: 42, 3,14, 1.000.000, -7,5, 0,003. Isso parece óbvio, mas as variações de separador decimal e milhar já estragam tudo se você usar split padrão.
Números por extenso: quarenta e dois, trezentos e cinquenta, negativos como "menos cinco". Em português isso varia bastante entre Brasil e Portugal. "Bilhão" em PT-BR é 10^9, em PT-PT pode ser 10^12. Números romanos: I, V, X, L, C, D, M. Ainda aparecem em atas, certidões, cronogramas, títulos de capítulos. Um romanizador ingênuo transforma "MCMXCIV" corretamente, mas falha feio com variantes medievais como "IIII" no lugar de "IV".
Porcentagens: 25%, vinte e cinco por cento, 0,25, meio, um quarto. Cada um desses representa o mesmo valor, mas para o código são três estruturas completamente diferentes. Frações: 1/2, um meio, meio, 50%. Frações impróprias como 3/2 também são comuns em manuais técnicos.
Ciências e notação técnica: 1,5e3, 1500m, 2GHz, 3 kg. Unidades coladas ou separadas por espaço. Eu trabalhei num projeto de extração de dados de processos judiciais que trazia valores entre aspas latinas como "R$ 1.250,00 (mil e duzentos e cinquenta reais)". A dupla forma era comum nesse tipo de documento. Tentei usar match de string simples e perdi metade dos registros. A solução foi criar um pipeline de duas etapas: primeiro normalizar os algarismos, depois validar contra o texto por extenso.
Método prático de normalização
Eu não faço mágica com AI generativa. O que funciona é um pipeline encadeado. Segue a ordem que eu uso, baseada em testes reais: Passo 1 — Limpeza básica do texto. Remova espaços múltiplos, normalize hífens, padronize pontuação. O regex /[\s]+/g já resolve 80% dos problemas de parsing ruim. Se o texto vier de PDF scanned, trate a quebra de linhas como espaços, não como quebras de palavras.
Passo 2 — Detecte e converta algarismos arábicos para número puro. Use bibliotecas de parsing localizadas. No Python, a função babel.numbers.parse_decimal() com a locale correta resolve separadores. No JavaScript, Intl.NumberFormat junto com toLocaleString inverso funciona, mas exige cuidado com a regex que separa inteiros de decimais. Não use split por vírgula ou ponto sem validar a locale antes — um número como 1.000,50 vira 100050 se você errar. Passo 3 — Reconheça porcentagens. Procure padrões como \d+(\.\d+)?\s*% ou palavras-chave isoladas. Substitua por seu equivalente decimal (25% vira 0,25). Atenção aqui: "50%" não é o mesmo que "metade" semanticamente, mas numericamente são. Decida se quer tratar equivalência ou manter distinção contextual.
Passo 4 — Converta números romanos. Um algoritmo clássico de soma-subtração funciona para a maioria dos casos padrão. A regra é simples: se o numeral seguinte é menor, subtraia; se for maior ou igual, Some. Coloque uma lista de exceções para casos documentais como "IVX" em vez de "IV". Eu encontrei "MMMM" escrito como representação de 4000 em alguns processos antigos. O algoritmo padrão quebra nisso. Solução: adicione um tratamento pós-conversão para valores acima de 3999 com parênteses ou barras, dependendo da convenção. Passo 5 — Números por extenso. Esta é a parte mais complicada. Você precisa de um dicionário/morpher de português. Eu uso a biblioteca num2words no Python, que suporta pt-BR e pt-PT com diferenças de vocabulário. O fluxo é: identificar a frase por extenso (geralmente delimitada por parênteses, travessão ou início/fim da linha), converter para algarismos, cruzar com o valor numérico já encontrado no texto para validação.
Passo 6 — Cruzamento e validação. Quando o mesmo valor aparece em duas formas — por exemplo, "1.500 (mil e quinhentos)" — você valida se a conversão bate. Se não bater, gera um flag de erro. Eu deixei esse flag ativo em todos os arquivos processados porque já vi casos em que o valor por extenso estava errado propositalmente em documentos fraudados.
Problemas que você vai enfrentar (e soluções reais)
Separadores decimais invertidos. No Brasil usamos vírgula para decimal e ponto para milhar. Na Europa ocorre o oposto. Se seu sistema recebe dados internacionais, identifique a locale antes de qualquer parsing. Eu errei isso uma vez e processei 12.000 registros com vírgula como milhar, transformando 1.200,50 em 120050. A correção foi rodar uma análise estatística rápida: se mais de 60% dos números tinham mais de três dígitos antes do separador, inverter a interpretação. Ajuste fino depois com amostragem manual. Palavras numéricas em contexto não numérico. "Segunda-feira" contém "segunda" que pode confundir parsers. "Cem reais" versus "cem amigos". A diferença é contexto. Eu resolvi adicionando uma camada de POS tagging (etiqueta gramatical) antes da conversão. Ferramentas como spaCy com modelo pt_core_news_sm fazem isso rápido. Se o token "cem" estiver seguido de substantivo contável, trata-se como numeral. Se estiver antes de unidade monetária, converte. Sem essa etapa, você converge palavras que não devem ser tocadas.
Frações com unidades mistas. "1/2 kg" e "meio quilo" são equivalentes, mas parsers ingênuos pegam só a fração. A solução é expandir frações antes de converter: substitua "meio" por "1/2", "um quarto" por "1/4", e então processe a fração normalmente. Números grandes e ambiguidade linguística. "Milhão" no Brasil é 10^6. Em francês é o mesmo, mas em alguns contextos jurídicos portugueses eu vi "bilião" usado como 10^9. Se seu corpus é multinacional, verifique a convenção antes de mapear. Eu criei um mapeamento regional simples: se o documento vier de fonte brasileira, uso short scale; se vier de fonte europeia, uso long scale. Isso evita o erro clássico de confundir 1 trillion (short) com 1 trillion (long).
Código mínimo que funciona
Para quem quer começar, aqui está um esqueleto em Python que cobre os casos principais. Ele não é perfeito, mas processa 95% dos textos que chegam na minha mesa: import re, babel.numbers, num2words
from locale import setlocale, LC_ALL setlocale(LC_ALL, 'pt_BR.UTF-8')
def normalizar_numero(texto):
Algarismos arábicos com separadores BR pat_arda = r'[\d\.,]+'
nums = re.findall(pat_arda, texto) resultado = []
for n in nums: limpo = n.replace('.', '').replace(',', '.')
try: resultado.append(float(limpo))
👉 Clique no botão abaixo para saber mais sobre o assunto!
except ValueError: pass
Porcentagens
pat_pct = r'(\d+(?:\.\d+)?)\s*%' for match in re.finditer(pat_pct, texto):
valor = float(match.group(1)) / 100 resultado.append(valor)
Romanos (simplificado)
def roman_to_int(s): vals = {'I':1,'V':5,'X':10,'L':50,'C':100,'D':500,'M':1000}
total = 0 for i,ch in enumerate(s.upper()):
if i+1 len(s) and vals[ch] < vals[s[i+1]]: total -= vals[ch]
else: total += vals[ch]
return total
pat_roma = r'\b[MVDCLXI]+\b' for match in re.finditer(pat_roma, texto):
resultado.append(roman_to_int(match.group()))
return resultado Para números por extenso, adicione uma chamada a num2words.convert() com a string extraída por regex de padrões conhecidos (dezenas, centenas, milhares). O tempo de processamento para um documento de 50 páginas com esse script simples fica em torno de 2 a 3 segundos. Se você precisar de precisão acima de 99%, precise de um modelo NLP supervisionado treinado com corpus jurídico ou contábil, o que aumenta o tempo para cerca de 30 segundos por página em CPU dedicada.
Quando isso não funciona e o que fazer no lugar
A normalização baseada em regras falha quando o texto é manual, contém erros de digitação, ou usa abreviações informais como "1k", "500m", "2,5Mi". Esses padrões exigem matching fuzzy. Eu uso rapidfuzz nesse caso: compara a string digitada com variantes conhecidas e aplica um threshold de similaridade de 85%. Acima disso, converte; abaixo disso, marca para revisão humana. Outro cenário onde regras puras falham é texto multilíngue misturado. Se você tem um documento com "1.000" (formato BR) e "1,000" (formato EN) na mesma linha, o parser precisa detectar a língua de cada trecho. O spaCy com modelos multilíngues resolve isso, mas a precisão cai para cerca de 92% em textos curtos. Com trechos maiores que 200 caracteres, sobe para 97%.
Se o volume for muito grande — milhões de linhas — a abordagem de regra é lenta. Aí eu migro para Spark com UDFs de normalização paralelizada. O throughput sobe para cerca de 50 mil linhas por segundo em cluster de 4 nós, contra 200 linhas por segundo na versão single-thread.
Lista de check para validação final
Antes de considerar o texto como normalizado com sucesso, passe por este checklist rápido: — Todos os algarismos arábicos foram convertidos e os separadores estão consistentes com a locale definida?
— Porcentagens foram transformadas em decimal ou mantidas como strings? Decida um padrão e aplique uniformemente. — Números romanos foram cruzados com datas ou contagens no mesmo documento? Confundi uma vez "X" (10) com o numeral romano de uma data e errei o campo todo.
— Frações e termos como "metade", "quarto" foram expandidos antes do cruzamento? — Valores por extenso e em algarismos batem quando aparecem juntos?
— Houve algum flag de ambiguidade ou invalidação que ficou pendente? Marque para revisão. Documentos com texto que contenham números escritos de diferentes formas exigem paciência. A normalização nunca é 100% automática na primeira passada. O pipeline que eu descrevi aqui resolve a maior parte dos casos comuns, mas sempre sobra alguma exceção — geralmente um erro de digitação intencional ou uma convenção regional rara — que precisa de olhar humano. Gastar uns minutos revisando os flags economiza horas de retrabalho depois.