Colete preços de mais de um país e você encontrará o mesmo número escrito de uma dúzia de formas diferentes. Um site alemão mostra 1.234,56 €. Um site francês mostra 1 234,56 €, com um espaço que você talvez nem consiga ver. Um site suíço mostra CHF 1’234.50. Um site indiano agrupa os dígitos como 12,34,567. E uma data como 03/04/2026 significa 4 de março nos Estados Unidos e 3 de abril em quase todo o resto do mundo.
Errar qualquer um desses casos raramente é óbvio: um preço errado por um fator de mil, uma data um mês fora do lugar, uma moeda confundida com outra que compartilha o mesmo símbolo. Este guia cobre os formatos que você vai encontrar, as regras que os definem e um código de parsing que se recusa a adivinhar quando um valor não se encaixa.
Principais conclusões
- Faça o parsing de cada valor com o locale da página de onde ele veio. Os mesmos caracteres significam números diferentes em locales diferentes.
- Não remova separadores torcendo para dar certo. Decida qual símbolo é o separador decimal a partir do locale, e rejeite valores que não se encaixam.
- Trate símbolos de moeda como ambíguos. “$10.00” é dólar canadense em uma página canadense, peso em uma página mexicana e dólar australiano em uma página australiana. Armazene os códigos ISO 4217.
- Armazene valores monetários em unidades menores com a precisão correta. O iene não tem casas decimais; os dinares do Bahrein e do Kuwait têm três.
- Datas numéricas precisam de um locale, ou melhor ainda, de uma fonte legível por máquina. Prefira o ISO 8601 vindo de dados estruturados quando a página o fornecer.
De onde vêm as regras
As convenções de locale não são folclore. Elas são publicadas no Unicode Common Locale Data Repository, CLDR, que sistemas operacionais, navegadores e a maioria das bibliotecas de internacionalização usam. Os exemplos abaixo vêm diretamente dos dados do CLDR, via a biblioteca Python Babel, formatando o número 1234567.891 e o valor de 1.234,50 euros:
| Locale | Decimal | Agrupamento | 1234567.891 | €1,234.50 |
|---|---|---|---|---|
| Inglês, Estados Unidos | . | , | 1,234,567.891 | €1,234.50 |
| Alemão, Alemanha | , | . | 1.234.567,891 | 1.234,50 € |
| Francês, França | , | espaço estreito sem quebra | 1 234 567,891 | 1 234,50 € |
| Alemão, Suíça | . | ’ | 1’234’567.891 | EUR 1’234.50 |
| Inglês, Índia | . | , | 12,34,567.891 | €1,234.50 |
| Português, Brasil | , | . | 1.234.567,891 | € 1.234,50 |
Três detalhes costumam pegar as pessoas de surpresa. O francês agrupa os milhares com um espaço estreito sem quebra, um caractere que parece um espaço e não é. O alemão suíço usa um caractere parecido com um apóstrofo. E o inglês indiano agrupa os três primeiros dígitos e depois pares, então 1.234.567 é escrito como 12,34,567.
Faça o parsing com o locale, e rejeite o que não se encaixa
A abordagem tentadora é remover tudo, exceto os dígitos e um separador, e depois tentar adivinhar qual separador é o ponto decimal. Isso funciona até encontrar “1.234”, que é mil duzentos e trinta e quatro na Alemanha e um vírgula dois três quatro nos Estados Unidos. Nenhuma quantidade de esperteza recupera a resposta certa a partir da string sozinha. O locale sim.
import re
from babel.dates import parse_date
from babel.numbers import NumberFormatError, get_currency_precision, get_group_symbol, parse_decimal
# Mantém dígitos, separadores e as variantes de espaço que os sites usam entre milhares.
NUMBER_CHARS = re.compile("[^0-9.,'\u2019 \u00a0\u2009\u202f-]")
SPACES = re.compile("[ \u00a0\u2009\u202f]")
def parse_amount(text, locale):
"""Faz o parsing de um valor exibido usando o locale da página. Rejeita entradas que não se encaixam nele."""
number = NUMBER_CHARS.sub("", text).strip()
group = get_group_symbol(locale)
if SPACES.fullmatch(group):
# Os sites misturam espaços comuns, sem quebra e estreitos sem quebra; usa o próprio do locale.
number = SPACES.sub(group, number)
try:
return parse_decimal(number, locale=locale, strict=True)
except NumberFormatError as e:
raise ValueError(f"{text!r} does not parse as a {locale} number: {e}") from None
def to_minor_units(amount, currency):
"""Armazena o valor monetário como um inteiro na menor unidade da moeda (centavos, iene, fils)."""
return int((amount * 10 ** get_currency_precision(currency)).to_integral_value())
def parse_display_date(text, locale):
"""Datas numéricas são ambíguas sem um locale: 03/04/2026 é março ou abril."""
return parse_date(text.strip(), locale=locale)
Testado em formatos do mundo real:
| Exibido | Locale | Resultado |
|---|---|---|
| $1,234.56 | Inglês, Estados Unidos | 1234.56 |
| 1.234,56 € | Alemão, Alemanha | 1234.56 |
| 1 234,56 € (qualquer um dos três caracteres de espaço) | Francês, França | 1234.56 |
| CHF 1’234.50 | Alemão, Suíça | 1234.50 |
| ₹12,34,567.00 | Inglês, Índia | 1234567.00 |
| R$ 1.234,56 | Português, Brasil | 1234.56 |
| 1.234,56 € | Inglês, Estados Unidos | Rejeitado |
| 9,99 | Inglês, Estados Unidos | Rejeitado |
As duas últimas linhas são o ponto central. Um preço formatado no padrão alemão em uma página que você acreditava ser americana é um sinal de que algo está errado: a variante errada de locale foi servida, a página mudou, ou sua configuração está incorreta. Um erro que você consegue ver é melhor do que um preço silenciosamente errado por um fator de mil.
O tratamento de espaços conquistou seu lugar durante os testes. Nossa primeira versão rejeitava um preço em francês escrito com um espaço comum, porque o separador francês do CLDR é o espaço estreito sem quebra. Sites reais misturam os três tipos de caractere de espaço, então o código agora mapeia qualquer um deles para o do próprio locale antes de fazer o parsing.
Moeda: nunca confie apenas no símbolo
Muitas moedas compartilham um símbolo. O CLDR formata dez unidades da moeda local como “$10.00” no Canadá, no México e na Austrália igualmente, e mostra dólares americanos como “US$10.00” em uma página canadense. Um scraper que mapeia ”$” para USD estará errado em uma grande parte dos sites do mundo.
Extraia a moeda de algo inequívoco: um código ISO 4217 nos dados estruturados da página, o valor de um seletor de moeda, ou a moeda padrão conhecida do site e do mercado de onde você coletou. Armazene o código de três letras junto com cada preço, e trate um preço que tenha apenas o símbolo, sem outra evidência, como incompleto em vez de adivinhar.
Unidades menores e precisão
Depois de feito o parsing, armazene valores monetários como inteiros na menor unidade da moeda, porque a aritmética de ponto flutuante em dinheiro acumula erros de arredondamento. O número de casas decimais nem sempre é dois:
| Moeda | Casas decimais | 1.234 exibido se torna |
|---|---|---|
| Euro, dólar americano | 2 | 123400 |
| Iene japonês, won sul-coreano, peso chileno | 0 | 1234 |
| Dinar do Bahrein, dinar do Kuwait | 3 | 1234000 |
Registre a unidade junto com o valor. Um pipeline que mistura unidades maiores e menores, como no preço que chegou como 10000 para um produto de $100 descrito em desvio de esquema em dados coletados por scraping, produz erros de exatamente um fator de cem que parecem inteiramente plausíveis.
Datas: prefira fontes legíveis por máquina
A mesma data numérica é interpretada de forma diferente conforme o locale:
| Exibido | Locale | Resultado |
|---|---|---|
| 03/04/2026 | Inglês, Estados Unidos | 4 de março de 2026 |
| 03/04/2026 | Inglês, Reino Unido | 3 de abril de 2026 |
| 03/04/2026 | Alemão, Alemanha | 3 de abril de 2026 |
Quando possível, evite fazer o parsing de datas exibidas. Muitas páginas publicam datas legíveis por máquina em dados estruturados ou no atributo datetime de um elemento <time>, geralmente em ISO 8601, que é inequívoco; extraí-las é abordado em pare de fazer parsing de HTML. Quando for necessário fazer o parsing de uma data exibida, use o locale da página explicitamente, armazene o resultado em ISO 8601 com fuso horário, e sinalize valores em que tanto o dia quanto o mês sejam doze ou menos, para sites cujo locale você não confirmou.
Acerte o locale desde o início
Cada regra acima depende de saber qual locale uma página usou, e isso é em parte uma decisão de coleta. A mesma página de produto pode ser renderizada em um idioma, moeda e formato numérico diferentes dependendo do país do visitante, e é por isso que um preço coletado por meio de uma saída na Alemanha e um coletado por meio de uma saída nos Estados Unidos não são diretamente comparáveis até que ambos sejam normalizados. Registre o mercado de onde cada valor foi coletado, e mantenha as sessões consistentes em idioma e região, conforme abordado em combinando geolocalização, fuso horário e locale de proxy residencial.
Conclusão
Números, preços e datas parecem universais e não são. Os mesmos caracteres significam valores diferentes em locales diferentes, símbolos correspondem a várias moedas, a precisão varia conforme a moeda, e datas numéricas são ambíguas por design.
Faça o parsing de cada valor com o locale em que foi escrito, rejeite o que não se encaixa em vez de adivinhar, armazene a moeda como um código ISO e o valor monetário em unidades menores com a precisão correta, e prefira datas legíveis por máquina sempre que a página as fornecer. Cada uma dessas regras transforma um erro silencioso e plausível em um erro visível.
Fontes e referências
- Unicode Consortium, Common Locale Data Repository (CLDR).
- Babel, biblioteca Python de internacionalização, versão 2.18.0, usada nos exemplos acima.
- Códigos de moeda ISO 4217 e formato de data e hora ISO 8601.