Executando verificação de segurança...
11
kht
6 min de leitura ·

Quando um espaço não é um espaço - ou é, mas de outro tipo (ou, Unicode e suas bizarrices 4)

Este post é mais um da série de "bizarrices do Unicode" que estou escrevendo. Os anteriores são:

Era uma vez um programador que precisava fazer um programa bem simples: receber uma string contendo um valor monetário formatado, e extrair a moeda e o valor. Ele tentou o seguinte (todos os exemplos estão em Python):

texto = 'R$ 42,00'
moeda, valor = texto.split(' ')

Parece simples, não? split para separar por espaço, e em seguida pegar os valores. Mas para a surpresa do programador, o código deu erro:

Traceback (most recent call last):
  File "/home/dev_perdido/script.py", line 2, in <module>
    moeda, valor = texto.split(' ')
    ^^^^^^^^^^^^
ValueError: not enough values to unpack (expected 2, got 1)

Atenção: se vc for testar os códigos, sugiro fazer o copy-paste direto dos respectivos trechos, pois se vc usar a tecla de espaço do seu teclado, não ocorrerá o erro. Mais abaixo entenderemos o motivo.

Basicamente, o erro indica que o resultado do split não tem dois valores, por isso ele não conseguiu atribuí-los nas duas variáveis (moeda e valor). Confuso, o programador resolveu verificar o resultado do split, imprimindo o tamanho da lista retornada e o seu conteúdo:

texto = 'R$ 42,00'
v = texto.split(' ')
print(len(v)) # imprime o tamanho da lista
print(v)      # imprime a própria lista

O resultado foi:

1
['R$\xa042,00']

Ou seja, a lista retornada por split contém apenas um elemento (e não dois, como ele esperava). E ao imprimir a própria lista, ele reparou que o único valor era a string 'R$\xa042,00'. Ao ver esse \xa0 em vez de um espaço, ele ficou mais confuso ainda e pediu ajuda a um programador mais experiente, que respondeu:

– Ah, é um NO-BREAK SPACE.
– Que @#@$% é essa? - perguntou o jovem programador


Um espaço que não é um espaço (ou é outro "tipo" de espaço)

O NO-BREAK SPACE (que também é chamado de non-breaking space) é um caractere diferente do SPACE – o "espaço tradicional" que é produzido quando usamos a tecla de espaço do teclado (por isso que o erro acima só ocorre se vc copiar e colar o código, pois se usar o teclado, não será produzido o NO-BREAK SPACE). Apesar de ser renderizado da mesma forma (ou seja, "a olho nu" não dá pra ver a diferença), é outro caractere.

Para ver a diferença, podemos fazer um programa que verifica cada caractere da string:

from unicodedata import name

texto = 'R$ 42,00'
for c in texto:
    print(f'{c} - {ord(c):04X} - {name(c)}')

Novamente, se for testar este código, faça copy-paste do trecho acima. Se vc usar a tecla de espaço do teclado, ele não vai produzir o NO-BREAK SPACE.

O resultado é:

R - 0052 - LATIN CAPITAL LETTER R
$ - 0024 - DOLLAR SIGN
  - 00A0 - NO-BREAK SPACE
4 - 0034 - DIGIT FOUR
2 - 0032 - DIGIT TWO
, - 002C - COMMA
0 - 0030 - DIGIT ZERO
0 - 0030 - DIGIT ZERO

E como podem ver, o terceiro caractere é um NO-BREAK SPACE, cujo code point1 é U+00A0. Por isso que ele foi mostrado como \xa0: o \xhh é uma sequência de escape na qual hh são os dígitos hexadecimais do caractere em questão (no caso, a0). Curiosamente, se imprimirmos diretamente print(texto), ele é mostrado como um espaço, em vez do código hexadecimal (devido a detalhes internos da linguagem quanto à forma na qual listas e strings são impressas).

Enfim, o problema é esse. Ao fazer split(' '), estou considerando apenas o espaço "normal" (ou "espaço ASCII", por assim dizer), cujo code point é U+0020. Por isso a quebra não é feita e no final a lista só tem um elemento.

E não pense que é um caso raro. Para formatar valores monetários, muitas API's usam o NO-BREAK SPACE, e em muitas páginas HTML ele também é usado. A diferença dele para o espaço "normal" é que o NO-BREAK SPACE impede a quebra de linha caso não haja espaço horizontal.

Por exemplo, dado um HTML com duas div's, uma com o espaço normal e outra com o NO-BREAK SPACE:

<div>
O valor é R$ 42,00
</div>
<br>
<div>
O valor é R$&nbsp;42,00
</div>

&nbsp; é um "atalho" (também chamado de "HTML Entity" ou "Character Reference") para o NO-BREAK SPACE. A lista completa pode ser encontrada aqui.

E este CSS:

div {
  font: 15px "Arial";
  border: 1px solid red;
  width: 90px;
}

O resultado será:

Divs com e sem NO-BREAK SPACE

Repare que com espaço "normal", a quebra de linha é feita logo depois do R$. Mas com NO-BREAK SPACE, o R$ não é separado do 42,00, então a quebra de linha é feita antes. Esta é uma forma de garantir que o valor não ficará separado do símbolo da moeda. Ou seja, a escolha pelo tipo de espaço ali não foi aleatório, e sim algo bem pensado com um propósito específico.


E a solução?

O programador ficou tão encantado com a explicação que quase esqueceu de perguntar como resolver no código. "Ah, é", respondeu o programador experiente.

Uma alternativa é fazer o split com o NO-BREAK SPACE em vez do espaço "normal". Sabendo que o code point dele é U+00A0, basta usar esse valor:

texto = 'R$ 42,00'
# O prefixo 0x é para que "a0" seja interpretado como hexadecimal,
# e chr retorna o caractere que corresponde a esse valor
moeda, valor = texto.split(chr(0xa0))
print(f'Moeda={moeda}, Valor={valor}') # Moeda=R$, Valor=42,00

"Espera", lembrou o programador, "tem vezes que pode vir com espaço normal". Sem problemas, nesse caso podemos usar uma regex:

import re

spaces = re.compile(r'\s')

# com NO-BREAK SPACE
texto = 'R$ 42,00'
moeda, valor = spaces.split(texto)
print(f'Moeda={moeda}, Valor={valor}') # Moeda=R$, Valor=42,00

# com espaço "normal"
texto = 'R$ 42,00'
moeda, valor = spaces.split(texto)
print(f'Moeda={moeda}, Valor={valor}') # Moeda=R$, Valor=42,00

Obs: lembrando novamente, o NO-BREAK SPACE é renderizado como um espaço "normal", então só olhando o código acima não dá pra ver a diferença. Se quiser verificar, sugiro copiar as duas strings acima e usar o código anterior que verifica cada um dos caracteres (o que usa o módulo unicodedata).

No caso, o atalho \s pega qualquer espaço, tanto o "normal" quanto o NO-BREAK SPACE. Mas vale lembrar que ele também pega quebras de linha, TAB e muitos outros caracteres (escrevi uma explicação bem detalhada aqui, e vale notar que a lista completa pode variar conforme a linguagem ou a engine/lib de regex sendo usada). Então se quiser apenas os dois tipos de espaço em questão, e nenhum outro caractere, pode mudar para re.compile(f' |{chr(0xa0)}') por exemplo.


Enfim, o Unicode tem várias dessas pegadinhas/esquisitices (ou, pra usar um nome bonito, "idiossincrasias"). Existe uma categoria específica chamada "Separator, Space" com diferentes caracteres de espaço. Explicar um a um já foge do escopo deste texto, mas é importante pelo menos saber que eles existem, pra não ser pego de surpresa quando surgir um na sua frente. Alguns parecem ter um uso mais restrito (muitos ali eu nunca me deparei, por exemplo), mas no caso do NO-BREAK SPACE, como já dito, não é tão raro assim.

E como curiosidade, isso não ocorre somente com espaços. Existem vários hífens diferentes (inclusive um non-breaking hyphen, bem similar ao NO-BREAK SPACE), vários tipos de aspas, e por aí vai. E eles podem causar problemas similares, de vc achar que é um caractere mas na verdade é outro parecido. E que pra piorar, pode ser renderizado da mesma forma e vc só vai perceber a diferença se "escovar os bits" e verificar cada caractere.

Footnotes

  1. Code point é um valor numérico que o Unicode definiu para cada caractere existente. Sempre é escrito com o prefixo U+ e o valor em hexadecimal. Aqui tem uma explicação mais completa.

Carregando publicação patrocinada...
2

É um dos meus itens em: https://pt.quora.com/Voc%C3%AA-tem-alguma-opini%C3%A3o-impopular-sobre-o-desenvolvimento-de-software/answer/Antonio-Maniero. É impopular e provavelmente sempre será, ele se tornou popular antes de sofrer críticas suficientes e dar a chance de outras coisas prosperarem. Um dia farei um artigo/vídeo/(talvez)implementação de alternativas melhores com uma desvantagem em relação ao UTF-8 mas quem "ninguém" usa, ou não deveria.

S2


Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente (não vendo nada, é retribuição na minha aposentadoria) (links aqui no perfil também).

0

E eu tenho um programa o aurora, windows optimizer que simplismente otimiza o seu windows e imprime os comandos de terminal na tela... só que tem um problema...
Ele vem com formatação ANCI e mesmo passando isso paraUTF-8/Unicode legível ele continua interpretando os acentos como caracteres não legíveis, infelizmente não sei como resolver! já tentei de várias formas com python e nada! Se você for usar o terminal windows, claramente que ele vai apresentar em uma linguágem fácil e legível de entender, mas com linguagens ao imprimir uma saída do terminal em python em uma interface gráfica por exemplo, (WX). ele continua com essa formatação bizarra!
Resultado do comando em uma output.

def show_output_dialog(self, output):
    try:
        output_dialog = OutputDialog(self, -1, "Command Result", output)
        output_dialog.ShowModal()
    except Exception as e:
        logging.error("Error showing output dialog: %s", e)

A class output

class OutputDialog(wx.Dialog):
def init(self, parent, id, title, output):
super(OutputDialog, self).init(parent, id, title, size=(400, 300))

    panel = wx.Panel(self)

    if output:
        output_text = wx.TextCtrl(panel, -1, value=output.strip(), style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL | wx.VSCROLL)
    else:
        output_text = wx.TextCtrl(panel, -1, value='The command was executed successfully!', style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL | wx.VSCROLL)

    close_button = wx.Button(panel, label="Close")
    close_button.Bind(wx.EVT_BUTTON, self.on_close)

    sizer = wx.BoxSizer(wx.VERTICAL)
    sizer.Add(output_text, 1, wx.EXPAND | wx.ALL, 10)
    sizer.Add(close_button, 0, wx.CENTER | wx.ALL, 10)

    panel.SetSizer(sizer)

def on_close(self, event):
    self.EndModal(wx.ID_OK)
2

Ótimo post! 🚀 Ele ilustra perfeitamente como pequenas nuances do Unicode podem causar grandes dores de cabeça no dia a dia de um programador. O exemplo do NO-BREAK SPACE é um clássico que já pegou muita gente de surpresa – especialmente quando lidamos com dados vindos de fontes externas, como APIs e páginas HTML.

A explicação é didática, e os exemplos práticos ajudam muito a visualizar o problema e a solução. Além disso, o toque de humor na interação entre os programadores deixou a leitura ainda mais envolvente. Excelente trabalho desmistificando mais uma das "bizarrices" do Unicode! 🔥💻 kht, depois dá uma olhada nas minhas publicações no Tabnews:🚀