Humanos usam texto. Computadores falam em bytes.[1]
O Python 3 introduziu uma forte distinção entre strings de texto humano e sequências de bytes puros. A conversão automática de sequências de bytes para texto Unicode ficou para trás no Python 2. Este capítulo trata de strings Unicode, sequências de bytes, e das codificações usadas para converter umas nas outras.
Dependendo do que você faz com o Python, pode achar que entender o Unicode não é importante.
Isso é improvável, mas mesmo que seja o caso, não há como escapar da separação entre str
e bytes
,
que agora exige conversões explícitas.
Como um bônus, você descobrirá que os tipos especializados de sequências binárias bytes
e bytearray
oferecem recursos que a classe str
"pau para toda obra" do Python 2 não oferecia.
Nesse capítulo, veremos os seguintes tópicos:
-
Caracteres, pontos de código e representações binárias
-
Recursos exclusivos das sequências binárias:
bytes
,bytearray
, ememoryview
-
Codificando para o Unicode completo e para conjuntos de caracteres legados
-
Evitando e tratando erros de codificação
-
Melhores práticas para lidar com arquivos de texto
-
A armadilha da codificação default e questões de E/S padrão
-
Comparações seguras de texto Unicode com normalização
-
Funções utilitárias para normalização, case folding (equiparação maiúsculas/minúsculas) e remoção de sinais diacríticos por força bruta
-
Ordenação correta de texto Unicode com
locale
e a biblioteca pyuca -
Metadados de caracteres do banco de dados Unicode
-
APIs duais, que processam
str
ebytes
O suporte ao Unicode no Python 3 sempre foi muito completo e estável, então o acréscimo mais notável é a Encontrando caracteres por nome, descrevendo um utilitário de linha de comando para busca no banco de dados Unicode—uma forma de encontrar gatinhos sorridentes ou hieróglifos do Egito antigo.
Vale a pena mencionar que o suporte a Unicode no Windows ficou melhor e mais simples desde o Python 3.6, como veremos na Cuidado com os defaults de codificação.
Vamos começar então com os conceitos não-tão-novos mas fundamentais de caracteres, pontos de código e bytes.
Note
|
Para essa segunda edição, expandi a seção sobre o módulo Lá você também vai encontrar o "Building Multi-character Emojis" (Criando emojis multi-caractere) (EN), descrevendo como combinar caracteres Unicode para criar bandeiras de países, bandeiras de arco-íris, pessoas com tonalidades de pele diferentes e ícones de diferentes tipos de famílias. |
O conceito de "string" é bem simples: uma string é uma sequência de caracteres. O problema está na definição de "caractere".
Em 2023, a melhor definição de "caractere" que temos é um caractere Unicode. Consequentemente, os itens que compõe um str
do Python 3 são caracteres Unicode, como os itens de um objeto unicode
no Python 2. Em contraste, os itens de uma str
no Python 2 são bytes, assim como os itens num objeto bytes
do Python 3.
O padrão Unicode separa explicitamente a identidade dos caracteres de representações binárias específicas:
-
A identidade de um caractere é chamada de ponto de código (code point). É um número de 0 a 1.114.111 (na base 10), representado no padrão Unicode na forma de 4 a 6 dígitos hexadecimais precedidos pelo prefixo "U+", de U+0000 a U+10FFFF. Por exemplo, o ponto de código da letra A é U+0041, o símbolo do Euro é U+20AC, e o símbolo musical da clave de sol corresponde ao ponto de código U+1D11E. Cerca de 13% dos pontos de código válidos tem caracteres atribuídos a si no Unicode 13.0.0, a versão do padrão usada no Python 3.10.
-
Os bytes específicos que representam um caractere dependem da codificação (encoding) usada. Uma codificação, nesse contexto, é um algoritmo que converte pontos de código para sequências de bytes, e vice-versa. O ponto de código para a letra A (U+0041) é codificado como um único byte,
\x41
, na codificação UTF-8, ou como os bytes\x41\x00
na codificação UTF-16LE. Em um outro exemplo, o UTF-8 exige três bytes para codificar o símbolo do Euro (U+20AC):\xe2\x82\xac
. Mas no UTF-16LE o mesmo ponto de código é U+20AC representado com dois bytes:\xac\x20
.
Converter pontos de código para bytes é codificar; converter bytes para pontos de código é decodificar. Veja o Exemplo 1.
>>> s = 'café'
>>> len(s) # (1)
4
>>> b = s.encode('utf8') # (2)
>>> b
b'caf\xc3\xa9' # (3)
>>> len(b) # (4)
5
>>> b.decode('utf8') # (5)
'café'
-
A
str
'café'
tem quatro caracteres Unicode. -
Codifica
str
parabytes
usando a codificação UTF-8. -
bytes
literais são prefixados com umb
. -
bytes
b
tem cinco bytes (o ponto de código para "é" é codificado com dois bytes em UTF-8). -
Decodifica
bytes
parastr
usando a codificação UTF-8.
Tip
|
Um jeito fácil de sempre lembrar a distinção entre |
Apesar do str
do Python 3 ser quase o tipo unicode
do Python 2 com um novo nome,
o bytes
do Python 3 não é meramente o velho str
renomeado,
e há também o tipo estreitamente relacionado bytearray
.
Então vale a pena examinar os tipos de sequências binárias antes de avançar para questões de codificação/decodificação.
Os novos tipos de sequências binárias são diferentes do str
do Python 2 em vários aspectos.
A primeira coisa importante é que existem dois tipos embutidos básicos de sequências binárias:
o tipo imutável bytes
, introduzido no Python 3, e o tipo mutável bytearray
,
introduzido há tempos, no Python 2.6[2]. A documentação do Python algumas vezes usa o termo genérico "byte string" (string de bytes, na documentação em português) para se referir a bytes
e bytearray
.
Cada item em bytes
ou bytearray
é um inteiro entre 0 e 255,
e não uma string de um caractere, como no str
do Python 2.
Entretanto, uma fatia de uma sequência binária sempre produz uma sequência binária do mesmo tipo—incluindo fatias de tamanho 1. Veja o Exemplo 2.
bytes e como `bytearray
>>> cafe = bytes('café', encoding='utf_8') (1)
>>> cafe
b'caf\xc3\xa9'
>>> cafe[0] (2)
99
>>> cafe[:1] (3)
b'c'
>>> cafe_arr = bytearray(cafe)
>>> cafe_arr (4)
bytearray(b'caf\xc3\xa9')
>>> cafe_arr[-1:] (5)
bytearray(b'\xa9')
-
bytes
pode ser criado a partir de umastr
, dada uma codificação. -
Cada item é um inteiro em
range(256)
. -
Fatias de
bytes
também sãobytes
—mesmo fatias de um único byte. -
Não há uma sintaxe literal para
bytearray
: elas aparecem comobytearray()
com um literalbytes
como argumento. -
Uma fatia de
bytearray
também é umabytearray
.
Warning
|
O fato de |
Apesar de sequências binárias serem na verdade sequências de inteiros, sua notação literal reflete o fato delas frequentemente embutirem texto ASCII. Assim, quatro formas diferentes de apresentação são utilizadas, dependendo do valor de cada byte:
-
Para bytes com código decimais de 32 a 126—do espaço ao
~
(til)—é usado o próprio caractere ASCII. -
Para os bytes correspondendo ao tab, à quebra de linha, ao carriage return (CR) e à
\
, são usadas as sequências de escape\t
,\n
,\r
, e\\
. -
Se os dois delimitadores de string,
'
e"
, aparecem na sequência de bytes, a sequência inteira é delimitada com'
, e qualquer'
dentro da sequência é precedida do caractere de escape, assim\'
.[3] -
Para qualquer outro valor do byte, é usada uma sequência de escape hexadecimal (por exemplo,
\x00
é o byte nulo).
É por isso que no Exemplo 2 vemos b’caf\xc3\xa9'
:
os primeiros três bytes, b’caf'
, estão na faixa de impressão do ASCII, ao contrário dos dois últimos.
Tanto bytes
quanto bytearray
suportam todos os métodos de str
, exceto aqueles relacionados a formatação (format
, format_map
)
e aqueles que dependem de dados Unicode, incluindo casefold
, isdecimal
, isidentifier
, isnumeric
, isprintable
, e encode
.
Isso significa que você pode usar os métodos conhecidos de string, como endswith
, replace
, strip
, translate
, upper
e dezenas de outros, com sequências binárias—mas com argumentos bytes
em vez de str
.
Além disso, as funções de expressões regulares no módulo re
também funcionam com sequências binárias, se a regex for compilada a partir de uma sequência binária ao invés de uma str
.
Desde o Python 3.5, o operador %
voltou a funcionar com sequências binárias.[4]
As sequências binárias tem um método de classe que str
não possui, chamado fromhex
, que cria uma sequência binária a partir da análise de pares de dígitos hexadecimais, separados opcionalmente por espaços:
>>> bytes.fromhex('31 4B CE A9')
b'1K\xce\xa9'
As outras formas de criar instâncias de bytes
ou bytearray
são chamadas a seus construtores com:
-
Uma
str
e um argumento nomeadoencoding
-
Um iterável que forneça itens com valores entre 0 e 255
-
Um objeto que implemente o protocolo de buffer (por exemplo
bytes
,bytearray
,memoryview
,array.array
), que copia os bytes do objeto fonte para a recém-criada sequência binária
Warning
|
Até o Python 3.5, era possível chamar |
Criar uma sequência binária a partir de um objeto tipo buffer é uma operação de baixo nível que pode envolver conversão de tipos. Veja uma demonstração no Exemplo 3.
>>> import array
>>> numbers = array.array('h', [-2, -1, 0, 1, 2]) (1)
>>> octets = bytes(numbers) (2)
>>> octets
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00' (3)
-
O typecode
'h'
cria umarray
de short integers (inteiros de 16 bits). -
octets
mantém uma cópia dos bytes que compõemnumbers
. -
Esses são os 10 bytes que representam os 5 inteiros pequenos.
Criar um objeto bytes
ou bytearray
a partir de qualquer fonte tipo buffer vai sempre copiar os bytes. Já objetos memoryview
permitem compartilhar memória entre estruturas de dados binários, como vimos na [memoryview_sec].
Após essa exploração básica dos tipos de sequências de bytes do Python, vamos ver como eles são convertidos de e para strings.
A distribuição do Python inclui mais de 100 codecs (encoders/decoders, _codificadores/decodificadores) para conversão de texto para bytes e vice-versa.
Cada codec tem um nome, como 'utf_8'
, e muitas vezes apelidos, tais como 'utf8'
, 'utf-8'
, e 'U8'
,
que você pode usar como o argumento de codificação em funções como
open()
, str.encode()
, bytes.decode()
, e assim por diante.
O Exemplo 4 mostra o mesmo texto codificado como três sequências de bytes diferentes.
>>> for codec in ['latin_1', 'utf_8', 'utf_16']:
... print(codec, 'El Niño'.encode(codec), sep='\t')
...
latin_1 b'El Ni\xf1o'
utf_8 b'El Ni\xc3\xb1o'
utf_16 b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'
A Figura 1 mostra um conjunto de codecs gerando bytes a partir de caracteres como a letra "A" e o símbolo musical da clave de sol. Observe que as últimas três codificações tem bytes múltiplos e tamanho variável.
Aqueles asteriscos todos na Figura 1 deixam claro que algumas codificações, como o ASCII e mesmo o multi-byte GB2312, não conseguem representar todos os caracteres Unicode. As codificações UTF, por outro lado, foram projetadas para lidar com todos os pontos de código do Unicode.
As codificações apresentadas na Figura 1 foram escolhidas para montar uma amostra representativa:
latin1
a.k.a.iso8859_1
-
Importante por ser a base de outras codificações,tal como a
cp1252
e o próprio Unicode (observe que os valores binários dolatin1
aparecem nos bytes docp1252
e até nos pontos de código). cp1252
-
Um superconjunto útil de
latin1
, criado pela Microsoft, acrescentando símbolos convenientes como as aspas curvas e o € (euro); alguns aplicativos de Windows chamam essa codificação de "ANSI", mas ela nunca foi um padrão ANSI real. cp437
-
O conjunto de caracteres original do IBM PC, com caracteres de desenho de caixas. Incompatível com o
latin1
, que surgiu depois. gb2312
-
Padrão antigo para codificar ideogramas chineses simplificados usados na República da China; uma das várias codificações muito populares para línguas asiáticas.
utf-8
-
De longe a codificação de 8 bits mais comum na web. Em julho de 2021, o "W3Techs: Usage statistics of character encodings for websites" afirma que 97% dos sites usam UTF-8, um grande avanço sobre os 81,4% de setembro de 2014, quando escrevi este capítulo na primeira edição.
utf-16le
-
Uma forma do esquema de codificação UTF de 16 bits; todas as codificações UTF-16 suportam pontos de código acima de U+FFFF, através de sequências de escape chamadas "pares substitutos".
Warning
|
A UTF-16 sucedeu a codificação de 16 bits original do Unicode 1.0—a UCS-2—há muito tempo, em 1996. A UCS-2 ainda é usada em muitos sistemas, apesar de ter sido descontinuada ainda no século passado, por suportar apenas ponto de código até U+FFFF. Em 2021, mas de 57% dos pontos de código alocados estava acima de U+FFFF, incluindo os importantíssimos emojis. |
Após completar essa revisão das codificações mais comuns, vamos agora tratar das questões relativas a operações de codificação e decodificação.
Apesar de existir uma exceção genérica, UnicodeError
, o erro relatado pelo Python em geral é mais específico:
ou é um UnicodeEncodeError
(ao converter uma str
para sequências binárias) ou é um UnicodeDecodeError
(ao ler uma sequência binária para uma str
).
Carregar módulos do Python também pode geram um SyntaxError
, quando a codificação da fonte for inesperada.
Vamos ver como tratar todos esses erros nas próximas seções.
Tip
|
A primeira coisa a observar quando aparece um erro de Unicode é o tipo exato da exceção.
É um |
A maioria dos codecs não-UTF entendem apenas um pequeno subconjunto dos caracteres Unicode.
Ao converter texto para bytes, um UnicodeEncodeError
será gerado se um caractere não estiver definido na codificação alvo, a menos que seja fornecido um tratamento especial, passando um argumento errors
para o método ou função de codificação.
O comportamento para tratamento de erro é apresentado no Exemplo 5.
>>> city = 'São Paulo'
>>> city.encode('utf_8') (1)
b'S\xc3\xa3o Paulo'
>>> city.encode('utf_16')
b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
>>> city.encode('iso8859_1') (2)
b'S\xe3o Paulo'
>>> city.encode('cp437') (3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/.../lib/python3.4/encodings/cp437.py", line 12, in encode
return codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in
position 1: character maps to <undefined>
>>> city.encode('cp437', errors='ignore') (4)
b'So Paulo'
>>> city.encode('cp437', errors='replace') (5)
b'S?o Paulo'
>>> city.encode('cp437', errors='xmlcharrefreplace') (6)
b'São Paulo'
-
As codificações UTF lidam com qualquer
str
-
iso8859_1
também funciona com a string'São Paulo'
. -
cp437
não consegue codificar o'ã'
("a" com til). O método default de tratamento de erro, —'strict'
—gera umUnicodeEncodeError
. -
O método de tratamento
errors='ignore'
pula os caracteres que não podem ser codificados; isso normalmente é uma péssima ideia, levando a perda silenciosa de informação. -
Ao codificar,
errors='replace'
substitui os caracteres não-codificáveis por um'?'
; aqui também há perda de informação, mas os usuários recebem um alerta de que algo está faltando. -
'xmlcharrefreplace'
substitui os caracteres não-codificáveis por uma entidade XML. Se você não pode usar UTF e não pode perder informação, essa é a única opção.
Note
|
O tratamento de erros de |
O ASCII é um subconjunto comum a todas as codificações que conheço, então a codificação deveria sempre funcionar se o texto for composto exclusivamente por caracteres ASCII.
O Python 3.7 trouxe um novo método booleano, str.isascii()
, para verificar se seu texto Unicode é 100% ASCII.
Se for, você deve ser capaz de codificá-lo para bytes em qualquer codificação sem gerar um UnicodeEncodeError
.
Nem todo byte contém um caractere ASCII válido, e nem toda sequência de bytes é um texto codificado em UTF-8 ou UTF-16 válidos; assim, se você presumir uma dessas codificações ao converter um sequência binária para texto, pode receber um UnicodeDecodeError
, se bytes inesperados forem encontrados.
Por outro lado, várias codificações de 8 bits antigas, como a 'cp1252'
, a 'iso8859_1'
e a 'koi8_r'
são capazes de decodificar qualquer série de bytes, incluindo ruído aleatório, sem reportar qualquer erro. Portanto, se seu programa presumir a codificação de 8 bits errada, ele vai decodificar lixo silenciosamente.
Tip
|
Caracteres truncados ou distorcidos são conhecidos como "gremlins" ou "mojibake" (文字化け—"texto modificado" em japonês). |
O Exemplo 6 ilustra a forma como o uso do codec errado pode produzir gremlins ou um UnicodeDecodeError
.
str
para bytes: sucesso e tratamento de erro>>> octets = b'Montr\xe9al' (1)
>>> octets.decode('cp1252') (2)
'Montréal'
>>> octets.decode('iso8859_7') (3)
'Montrιal'
>>> octets.decode('koi8_r') (4)
'MontrИal'
>>> octets.decode('utf_8') (5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5:
invalid continuation byte
>>> octets.decode('utf_8', errors='replace') (6)
'Montr�al'
-
A palavra "Montréal" codificada em
latin1
;'\xe9'
é o byte para "é". -
Decodificar com Windows 1252 funciona, pois esse codec é um superconjunto de
latin1
. -
ISO-8859-7 foi projetado para a língua grega, então o byte
'\xe9'
é interpretado de forma incorreta, e nenhum erro é gerado. -
KOI8-R é foi projetado para o russo. Agora
'\xe9'
significa a letra "И" do alfabeto cirílico. -
O codec
'utf_8'
detecta queoctets
não é UTF-8 válido, e gera umUnicodeDecodeError
. -
Usando
'replace'
para tratamento de erro, o\xe9
é substituído por "�" (ponto de código #U+FFFD), o caractere oficial do Unicode chamadoREPLACEMENT CHARACTER
, criado exatamente para representar caracteres desconhecidos.
UTF-8 é a codificação default para fontes no Python 3, da mesma forma que ASCII era o default no Python 2. Se você carregar um módulo .py contendo dados que não estejam em UTF-8, sem declaração codificação, receberá uma mensagem como essa:
link:code/04-text-byte/syntax-msg.txt[role=include]
Como o UTF-8 está amplamente instalado em sistemas GNU/Linux e macOS,
um cenário onde isso tem mais chance de ocorrer é na abertura de um arquivo .py criado no Windows, com cp1252
.
Observe que esse erro ocorre mesmo no Python para Windows, pois a codificação default para fontes de Python 3 é UTF-8 em todas as plataformas.
Para resolver esse problema, acrescente o comentário mágico coding
no início do arquivo, como no Exemplo 7.
# coding: cp1252
print('Olá, Mundo!')
Tip
|
Agora que o código fonte do Python 3 não está mais limitado ao ASCII, e por default usa a excelente codificação UTF-8, a melhor "solução" para código fonte em codificações antigas como |
Suponha que você tem um arquivo de texto, seja ele código-fonte ou poesia, mas não sabe qual codificação foi usada. Como detectar a codificação correta? Respostas na próxima seção.
Como descobrir a codificação de uma sequência de bytes? Resposta curta: não é possível. Você precisa ser informado.
Alguns protocolos de comunicação e formatos de arquivo, como o HTTP e o XML, contêm cabeçalhos que nos dizem explicitamente como o conteúdo está codificado. Você pode ter certeza que algumas sequências de bytes não estão em ASCII, pois elas contêm bytes com valores acima de 127, e o modo como o UTF-8 e o UTF-16 são construídos também limita as sequências de bytes possíveis.
(Os próximos parágrafos vieram de uma nota escrita pelo revisor técnico Leonardo Rochael no rascunho desse livro.)
Pela forma como o UTF-8 foi projetado, é quase impossível que uma sequência aleatória de bytes, ou mesmo uma sequência não-aleatória de bytes de uma codificação diferente do UTF-8, seja acidentalmente decodificada como lixo no UTF-8, ao invés de gerar um UnicodeDecodeError
.
As razões para isso são que as sequências de escape do UTF-8 nunca usam caracteres ASCII, e tais sequências de escape tem padrões de bits que tornam muito difícil que dados aleatórioas sejam UTF-8 válido por acidente.
Portanto, se você consegue decodificar alguns bytes contendo códigos > 127 como UTF-8, a maior probabilidade é de sequência estar em UTF-8.
Trabalhando com os serviços online brasileiros, alguns dos quais alicerçados em back-ends antigos, ocasionalmente precisei implementar uma estratégia de decodificação que tentava decodificar via UTF-8, e tratava um UnicodeDecodeError
decodificando via cp1252
.
Uma estratégia feia, mas efetiva.
Entretanto, considerando que as linguagens humanas também tem suas regras e restrições, uma vez que você supõe que uma série de bytes é um texto humano simples,
pode ser possível intuir sua codificação usando heurística e estatística.
Por exemplo, se bytes com valor b'\x00'
bytes forem comuns, é provável que seja uma codificação de 16 ou 32 bits, e não um esquema de 8 bits, pois caracteres nulos em texto simples são erros.
Quando a sequência de bytes `b'\x20\x00'` aparece com frequência, é mais provável que esse seja o caractere de espaço (U+0020) na codificação UTF-16LE, e não o obscuro caractere U+2000 (EN QUAD
)—seja lá o que for isso.
É assim que o pacote "Chardet—The Universal Character Encoding Detector (Chardet—O Detector Universal de Codificações de Caracteres)" trabalha para descobrir cada uma das mais de 30 codificações suportadas.
Chardet é uma biblioteca Python que pode ser usada em seus programas, mas que também inclui um utilitário de comando de linha, chardetect
.
Aqui está a forma como ele analisa o código fonte desse capítulo:
$ chardetect 04-text-byte.asciidoc
04-text-byte.asciidoc: utf-8 with confidence 0.99
Apesar de sequências binárias de texto codificado normalmente não trazerem dicas sobre sua codificação, os formatos UTF podem preceder o conteúdo textual por um marcador de ordem dos bytes. Isso é explicado a seguir.
No Exemplo 4, você pode ter notado um par de bytes extra no início de uma sequência codificada em UTF-16. Aqui estão eles novamente:
>>> u16 = 'El Niño'.encode('utf_16')
>>> u16
b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'
Os bytes são b'\xff\xfe'
. Isso é um BOM—sigla para byte-order mark (marcador de ordem de bytes)—indicando a ordenação de bytes "little-endian" da CPU Intel onde a codificação foi realizada.
Em uma máquina little-endian, para cada ponto de código, o byte menos significativo aparece primeiro:
a letra 'E'
, ponto de código U+0045 (decimal 69), é codificado nas posições 2 e 3 dos bytes como 69
e 0
:
>>> list(u16)
[255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
Em uma CPU big-endian, a codificação seria invertida; 'E'
seria codificado como 0
e 69
.
Para evitar confusão, a codificação UTF-16 precede o texto a ser codificado com o caractere especial invisível ZERO WIDTH NO-BREAK SPACE
(U+FEFF).
Em um sistema little-endian, isso é codificado como b'\xff\xfe'
(decimais 255, 254).
Como, por design, não existe um caractere U+FFFE em Unicode, a sequência de bytes b'\xff\xfe'
tem que ser o ZERO WIDTH NO-BREAK SPACE
em uma codificação little-endian,
e então o codec sabe qual ordenação de bytes usar.
Há uma variante do UTF-16—o UTF-16LE—que é explicitamente little-endian, e outra que é explicitamente big-endian, o UTF-16BE. Se você usá-los, um BOM não será gerado:
>>> u16le = 'El Niño'.encode('utf_16le')
>>> list(u16le)
[69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
>>> u16be = 'El Niño'.encode('utf_16be')
>>> list(u16be)
[0, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111]
Se o BOM estiver presente, supõe-se que ele será filtrado pelo codec UTF-16,
então recebemos apenas o conteúdo textual efetivo do arquivo, sem o ZERO WIDTH NO-BREAK SPACE
inicial.
O padrão Unicode diz que se um arquivo é UTF-16 e não tem um BOM, deve-se presumir que ele é UTF-16BE (big-endian). Entretanto, a arquitetura x86 da Intel é little-endian, daí que há uma grande quantidade de UTF-16 little-endian e sem BOM no mundo.
Toda essa questão de ordenação dos bytes (endianness) só afeta codificações que usam palavras com mais de um byte, como UTF-16 e UTF-32.
Uma grande vantagem do UTF-8 é produzir a mesma sequência independente da ordenação dos bytes, então um BOM não é necessário.
No entanto, algumas aplicações Windows (em especial o Notepad) mesmo assim acrescentam o BOM a arquivos UTF-8—e o Excel depende do BOM para detectar um arquivo UTF-8, caso contrário ele presume que o conteúdo está codificado com uma página de código do Windows.
Essa codificação UTF-8 com BOM é chamada UTF-8-SIG no registro de codecs do Python.
O caractere U+FEFF codificado em UTF-8-SIG é a sequência de três bytes b'\xef\xbb\xbf'
.
Então, se um arquivo começa com aqueles três bytes, é provavelmente um arquivo UTF-8 com um BOM.
Tip
|
A dica de Caleb sobre o UTF-8-SIG
Caleb Hattingh—um dos revisores técnicos—sugere sempre usar o codec UTF-8-SIG para ler arquivos UTF-8. Isso é inofensivo, pois o UTF-8-SIG lê corretamente arquivos com ou sem um BOM, e não devolve o BOM propriamente dito.
Para escrever arquivos, recomendo usar UTF-8, para interoperabilidade integral.
Por exemplo, scripts Python podem ser tornados executáveis em sistemas Unix, se começarem com o comentário: |
Vamos agora ver como tratar arquivos de texto no Python 3.
A melhor prática para lidar com E/S de texto é o "Sanduíche de Unicode" (Unicode sandwich)
(Figura 2).[5]
Isso significa que os bytes
devem ser decodificados para str
o mais cedo possível na entrada
(por exemplo, ao abrir um arquivo para leitura).
O "recheio" do sanduíche é a lógica do negócio de seu programa,
onde o tratamento do texto é realizado exclusivamente sobre objetos str
.
Você nunca deveria codificar ou decodificar no meio de outro processamento.
Na saída, as str
são codificadas para bytes
o mais tarde possível.
A maioria dos frameworks web funciona assim, e raramente tocamos em bytes
ao usá-los.
No Django, por exemplo, suas views devem produzir str
em Unicode; o próprio Django se encarrega de codificar a resposta para bytes
, usando UTF-8 como default.
O Python 3 torna mais fácil seguir o conselho do sanduíche de Unicode, pois o embutido open()
executa a decodificação necessária na leitura e a codificação ao escrever arquivos em modo texto. Dessa forma, tudo que você recebe de my_file.read()
e passa para my_file.write(text)
são objetos str
.
Assim, usar arquivos de texto é aparentemente simples. Mas se você confiar nas codificações default, pode acabar levando uma mordida.
Observe a sessão de console no Exemplo 8. Você consegue ver o erro?
>>> open('cafe.txt', 'w', encoding='utf_8').write('café')
4
>>> open('cafe.txt').read()
'café'
O erro: especifiquei a codificação UTF-8 ao escrever o arquivo, mas não fiz isso na leitura, então o Python assumiu a codificação de arquivo default do Windows—página de código 1252—e os bytes finais foram decodificados como os caracteres 'é'
ao invés de 'é'
.
Executei o Exemplo 8 no Python 3.8.1, 64 bits, no Windows 10 (build 18363). Os mesmos comandos rodando em um GNU/Linux ou um macOS recentes funcionam perfeitamente, pois a codificação default desses sistemas é UTF-8, dando a falsa impressão que tudo está bem. Se o argumento de codificação fosse omitido ao abrir o arquivo para escrita, a codificação default do locale seria usada, e poderíamos ler o arquivo corretamente usando a mesma codificação. Mas aí o script geraria arquivos com conteúdo binário diferente dependendo da plataforma, ou mesmo das configurações do locale na mesma plataforma, criando problemas de compatibilidade.
Tip
|
Código que precisa rodar em múltiplas máquinas ou múltiplas ocasiões não deveria jamais depender de defaults de codificação.
Sempre passe um argumento |
Um detalhe curioso no Exemplo 8 é que a função write
na primeira instrução informa que foram escritos quatro caracteres, mas na linha seguinte são lidos cinco caracteres.
O Exemplo 9 é uma versão estendida do Exemplo 8, e explica esse e outros detalhes.
>>> fp = open('cafe.txt', 'w', encoding='utf_8')
>>> fp # (1)
<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
>>> fp.write('café') # (2)
4
>>> fp.close()
>>> import os
>>> os.stat('cafe.txt').st_size # (3)
5
>>> fp2 = open('cafe.txt')
>>> fp2 # (4)
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp1252'>
>>> fp2.encoding # (5)
'cp1252'
>>> fp2.read() # (6)
'café'
>>> fp3 = open('cafe.txt', encoding='utf_8') # (7)
>>> fp3
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'>
>>> fp3.read() # (8)
'café'
>>> fp4 = open('cafe.txt', 'rb') # (9)
>>> fp4 # (10)
<_io.BufferedReader name='cafe.txt'>
>>> fp4.read() # (11)
b'caf\xc3\xa9'
-
Por default,
open
usa o modo texto e devolve um objetoTextIOWrapper
com uma codificação específica. -
O método
write
de umTextIOWrapper
devolve o número de caracteres Unicode escritos. -
os.stat
diz que o arquivo tem 5 bytes; o UTF-8 codifica'é'
com 2 bytes, 0xc3 e 0xa9. -
Abrir um arquivo de texto sem uma codificação explícita devolve um
TextIOWrapper
com a codificação configurada para um default do locale. -
Um objeto
TextIOWrapper
tem um atributo de codificação que pode ser inspecionado: neste caso,cp1252
. -
Na codificação
cp1252
do Windows, o byte 0xc3 é um "Ã" (A maiúsculo com til), e 0xa9 é o símbolo de copyright. -
Abrindo o mesmo arquivo com a codificação correta.
-
O resultado esperado: os mesmo quatro caracteres Unicode para
'café'
. -
A flag
'rb'
abre um arquivo para leitura em modo binário. -
O objeto devolvido é um
BufferedReader
, e não umTextIOWrapper
. -
Ler do arquivo obtém bytes, como esperado.
Tip
|
Não abra arquivos de texto no modo binário, a menos que seja necessário analisar o conteúdo do arquivo para determinar sua codificação—e mesmo assim, você deveria estar usando o Chardet em vez de reinventar a roda (veja a Como descobrir a codificação de uma sequência de bytes). Programas comuns só deveriam usar o modo binário para abrir arquivos binários, como arquivos de imagens raster ou bitmaps. |
O problema no Exemplo 9 vem de se confiar numa configuração default ao se abrir um arquivo de texto. Há várias fontes de tais defaults, como mostra a próxima seção.
Várias configurações afetam os defaults de codificação para E/S no Python. Veja o script default_encodings.py script no Exemplo 10.
link:code/04-text-byte/default_encodings.py[role=include]
A saída do Exemplo 10 no GNU/Linux (Ubuntu 14.04 a 19.10) e no macOS (10.9 a 10.14) é idêntica, mostrando que UTF-8
é usado em toda parte nesses sistemas:
$ python3 default_encodings.py
locale.getpreferredencoding() -> 'UTF-8'
type(my_file) -> <class '_io.TextIOWrapper'>
my_file.encoding -> 'UTF-8'
sys.stdout.isatty() -> True
sys.stdout.encoding -> 'utf-8'
sys.stdin.isatty() -> True
sys.stdin.encoding -> 'utf-8'
sys.stderr.isatty() -> True
sys.stderr.encoding -> 'utf-8'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'
No Windows, porém, a saída é o Exemplo 11.
> chcp (1)
Active code page: 437
> python default_encodings.py (2)
locale.getpreferredencoding() -> 'cp1252' (3)
type(my_file) -> <class '_io.TextIOWrapper'>
my_file.encoding -> 'cp1252' (4)
sys.stdout.isatty() -> True (5)
sys.stdout.encoding -> 'utf-8' (6)
sys.stdin.isatty() -> True
sys.stdin.encoding -> 'utf-8'
sys.stderr.isatty() -> True
sys.stderr.encoding -> 'utf-8'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'
-
chcp
mostra a página de código ativa para o console:437
. -
Executando default_encodings.py, com a saída direcionada para o console.
-
locale.getpreferredencoding()
é a configuração mais importante. -
Arquivos de texto usam`locale.getpreferredencoding()` por default.
-
A saída está direcionada para o console, então
sys.stdout.isatty()
éTrue
. -
Agora,
sys.stdout.encoding
não é a mesma que a página de código informada porchcp
!
O suporte a Unicode no próprio Windows e no Python para Windows melhorou desde que escrevi a primeira edição deste livro.
O Exemplo 11 costumava informar quatro codificações diferentes no Python 3.4 rodando no Windows 7.
As codificações para stdout
, stdin
, e stderr
costumavam ser iguais à da página de código ativa informada pelo comando chcp
, mas agora são todas utf-8
, graças à PEP 528—Change Windows console encoding to UTF-8 (Mudar a codificação do console no Windows para UTF-8) (EN), implementada no Python 3.6, e ao suporte a Unicode no PowerShell do cmd.exe (desde o Windows 1809, de outubro de 2018).[6]
É esquisito que o chcp
e o sys.stdout.encoding
reportem coisas diferentes quando o stdout
está escrevendo no console, mas é ótimo podermos agora escrever strings Unicode sem erros de codificação no Windows—a menos que o usuário redirecione a saída para um arquivo, como veremos adiante.
Isso não significa que todos os seus emojis favoritos vão aparecer: isso também depende da fonte usada pelo console.
Outra mudança foi a PEP 529—Change Windows filesystem encoding to UTF-8 (Mudar a codificação do sistema de arquivos do Windows para UTF-8), também implementada no Python 3.6, que modificou a codificação do sistema de arquivos (usada para representar nomes de diretórios e de arquivos), da codificação proprietária MBCS da Microsoft para UTF-8.
Entretanto, se a saída do Exemplo 10 for redirecionada para um arquivo, assim…
Z:\>python default_encodings.py > encodings.log
…aí o valor de sys.stdout.isatty()
se torna False
, e sys.stdout.encoding
é determinado por locale.getpreferredencoding()
, 'cp1252'
naquela máquina—mas sys.stdin.encoding
e sys.stderr.encoding
seguem como utf-8
.
Tip
|
No Exemplo 12, usei a expressão de escape |
Isso significa que um script como o Exemplo 12 funciona quando está escrevendo no console, mas pode falhar quando a saída é redirecionada para um arquivo.
link:code/04-text-byte/stdout_check.py[role=include]
O Exemplo 12 mostra o resultado de uma chamada a sys.stdout.isatty()
, o valor de sys.stdout.encoding
, e esses três caracteres:
-
'…'
HORIZONTAL ELLIPSIS
—existe no CP 1252 mas não no CP 437. -
'∞'
INFINITY
—existe no CP 437 mas não no CP 1252. -
'㊷'
CIRCLED NUMBER FORTY TWO
—não existe nem no CP 1252 nem no CP 437.
Quando executo o stdout_check.py no PowerShell ou no cmd.exe, funciona como visto na Figura 3.
Apesar de chcp
informar o código ativo como 437, sys.stdout.encoding
é UTF-8, então tanto HORIZONTAL ELLIPSIS
quanto INFINITY
são escritos corretamente.
O CIRCLED NUMBER FORTY TWO
é substituído por um retângulo, mas nenhum erro é gerado.
Presume-se que ele seja reconhecido como um caractere válido, mas a fonte do console não tem o glifo para mostrá-lo.
Entretanto, quando redireciono a saída de stdout_check.py para um arquivo, o resultado é o da Figura 4.
O primeiro problema demonstrado pela Figura 4 é o UnicodeEncodeError
mencionando o caractere '\u221e'
,
porque sys.stdout.encoding
é 'cp1252'
—uma página de código que não tem o caractere INFINITY
.
Lendo out.txt com o comando type
—ou um editor de Windows como o VS Code ou o Sublime Text—mostra que, ao invés do HORIZONTAL ELLIPSIS, consegui um 'à'
(LATIN SMALL LETTER A WITH GRAVE
).
Acontece que o valor binário 0x85 no CP 1252 significa '…'
, mas no CP 437 o mesmo valor binário representa o 'à'
.
Então, pelo visto, a página de código ativa tem alguma importância, não de uma forma razoável ou útil, mas como uma explicação parcial para uma experiência ruim com o Unicode.
Note
|
Para realizar esses experimentos, usei um laptop configurado para o mercado norte-americano, rodando Windows 10 OEM. Versões de Windows localizadas para outros países podem ter configurações de codificação diferentes. No Brasil, por exemplo, o console do Windows usa a página de código 850 por default—e não a 437. |
Para encerrar esse enlouquecedor tópico de codificações default, vamos dar uma última olhada nas diferentes codificações no Exemplo 11:
-
Se você omitir o argumento
encoding
ao abrir um arquivo, o default é dado porlocale.getpreferredencoding()
('cp1252'
no Exemplo 11). -
Antes do Python 3.6, a codificação de
sys.stdout|stdin|stderr
costumava ser determinada pela variável do ambientePYTHONIOENCODING
—agora essa variável é ignorada, a menos quePYTHONLEGACYWINDOWSSTDIO
seja definida como uma string não-vazia. Caso contrário, a codificação da E/S padrão será UTF-8 para E/S interativa, ou definida porlocale.getpreferredencoding()
, se a entrada e a saída forem redirecionadas para ou de um arquivo. -
sys.getdefaultencoding()
é usado internamente pelo Python em conversões implícitas de dados binários de ou parastr
. Não há suporte para mudar essa configuração. -
sys.getfilesystemencoding()
é usado para codificar/decodificar nomes de arquivo (mas não o conteúdo dos arquivos). Ele é usado quandoopen()
recebe um argumentostr
para um nome de arquivo; se o nome do arquivo é passado como um argumentobytes
, ele é entregue sem modificação para a API do sistema operacional.
Note
|
Já faz muito anos que, no GNU/Linux e no macOS, todas essas codificações são definidas como UTF-8 por default,
então a E/S entende e exibe todos os caracteres Unicode.
No Windows, não apenas codificações diferentes são usadas no mesmo sistema,
elas também são, normalmente, páginas de código como |
Resumindo, a configuração de codificação mais importante devolvida por locale.getpreferredencoding()
é a default para abrir arquivos de texto e para sys.stdout/stdin/stderr
, quando eles são redirecionados para arquivos.
Entretanto, a documentação diz (em parte):
locale.getpreferredencoding(do_setlocale=True)
Retorna a codificação da localidade usada para dados de texto, de acordo com as preferências do usuário. As preferências do usuário são expressas de maneira diferente em sistemas diferentes e podem não estar disponíveis programaticamente em alguns sistemas, portanto, essa função retorna apenas uma estimativa. […]
Assim, o melhor conselho sobre defaults de codificação é: não confie neles.
Você evitará muitas dores de cabeça se seguir o conselho do sanduíche de Unicode, e sempre tratar codificações de forma explícita em seus programas.
Infelizmente, o Unicode é trabalhoso mesmo se você converter seus bytes
para str
corretamente.
As duas próximas seções tratam de assuntos que são simples no reino do ASCII,
mas ficam muito complexos no planeta Unicode: normalização de texto (isto é, transformar o texto em uma representação uniforme para comparações) e ordenação.
Comparações de strings são dificultadas pelo fato do Unicode ter combinações de caracteres: sinais diacríticos e outras marcações que são anexadas aos caractere anterior, ambos aparecendo juntos como um só caractere quando impressos.
Por exemplo, a palavra "café" pode ser composta de duas formas, usando quatro ou cinco pontos de código, mas o resultado parece exatamente o mesmo:
>>> s1 = 'café'
>>> s2 = 'cafe\N{COMBINING ACUTE ACCENT}'
>>> s1, s2
('café', 'café')
>>> len(s1), len(s2)
(4, 5)
>>> s1 == s2
False
Colocar COMBINING ACUTE ACCENT
(U+0301) após o "e" resulta em "é".
No padrão Unicode, sequências como 'é'
e 'e\u0301'
são chamadas de "equivalentes canônicas",
e se espera que as aplicações as tratem como iguais. Mas o Python vê duas sequências de pontos de código diferentes, e não as considera iguais.
A solução é a unicodedata.normalize()
.
O primeiro argumento para essa função é uma dessas quatro strings: 'NFC'
, 'NFD'
, 'NFKC'
, e 'NFKD'
.
Vamos começar pelas duas primeiras.
A Forma Normal C (NFC) combina os ponto de código para produzir a string equivalente mais curta, enquanto a NFD decompõe, expandindo os caracteres compostos em caracteres base e separando caracteres combinados. Ambas as normalizações fazem as comparações funcionarem da forma esperada, como mostra o próximo exemplo:
>>> from unicodedata import normalize
>>> s1 = 'café'
>>> s2 = 'cafe\N{COMBINING ACUTE ACCENT}'
>>> len(s1), len(s2)
(4, 5)
>>> len(normalize('NFC', s1)), len(normalize('NFC', s2))
(4, 4)
>>> len(normalize('NFD', s1)), len(normalize('NFD', s2))
(5, 5)
>>> normalize('NFC', s1) == normalize('NFC', s2)
True
>>> normalize('NFD', s1) == normalize('NFD', s2)
True
Drivers de teclado normalmente geram caracteres compostos, então o texto digitado pelos usuários estará na NFC por default. Entretanto, por segurança, pode ser melhor normalizar as strings com normalize('NFC', user_text)
antes de salvá-las.
A NFC também é a forma de normalização recomendada pelo W3C em
"Character Model for the World Wide Web: String Matching and Searching" (Um Modelo de Caracteres para a World Wide Web: Correspondência de Strings e Busca) (EN).
Alguns caracteres singulares são normalizados pela NFC em um outro caractere singular. O símbolo para o ohm (Ω), a unidade de medida de resistência elétrica, é normalizado para a letra grega ômega maiúscula. Eles são visualmente idênticos, mas diferentes quando comparados, então a normalizaçào é essencial para evitar surpresas:
>>> from unicodedata import normalize, name
>>> ohm = '\u2126'
>>> name(ohm)
'OHM SIGN'
>>> ohm_c = normalize('NFC', ohm)
>>> name(ohm_c)
'GREEK CAPITAL LETTER OMEGA'
>>> ohm == ohm_c
False
>>> normalize('NFC', ohm) == normalize('NFC', ohm_c)
True
As outras duas formas de normalização são a NFKC e a NFKD, a letra K significando "compatibilidade".
Essas são formas mais fortes de normalizaçào, afetando os assim chamados "caracteres de compatibilidade".
Apesar de um dos objetivos do Unicode ser a existência de um único ponto de código "canônico" para cada caractere, alguns caracteres aparecem mais de uma vez, para manter compatibilidade com padrões pré-existentes.
Por exemplo, o MICRO SIGN
, µ
(U+00B5
), foi adicionado para permitir a conversão bi-direcional com o latin1
, que o inclui, apesar do mesmo caractere ser parte do alfabeto grego com o ponto de código U+03BC
(GREEK SMALL LETTER MU
).
Assim, o símbolo de micro é considerado um "caractere de compatibilidade".
Nas formas NFKC e NFKD, cada caractere de compatibilidade é substituído por uma "decomposição de compatibilidade" de um ou mais caracteres, que é considerada a representação "preferencial", mesmo se ocorrer alguma perda de formatação—idealmente, a formatação deveria ser responsabilidade de alguma marcação externa, não parte do Unicode. Para exemplificar, a decomposição de compatibilidade da fração um meio, '½'
(U+00BD
), é a sequência de três caracteres '1/2'
, e a decomposição de compatibilidade do símbolo de micro, 'µ'
(U+00B5
), é o mu minúsculo, 'μ'
(U+03BC
).[7]
É assim que a NFKC funciona na prática:
>>> from unicodedata import normalize, name
>>> half = '\N{VULGAR FRACTION ONE HALF}'
>>> print(half)
½
>>> normalize('NFKC', half)
'1⁄2'
>>> for char in normalize('NFKC', half):
... print(char, name(char), sep='\t')
...
1 DIGIT ONE
⁄ FRACTION SLASH
2 DIGIT TWO
>>> four_squared = '4²'
>>> normalize('NFKC', four_squared)
'42'
>>> micro = 'µ'
>>> micro_kc = normalize('NFKC', micro)
>>> micro, micro_kc
('µ', 'μ')
>>> ord(micro), ord(micro_kc)
(181, 956)
>>> name(micro), name(micro_kc)
('MICRO SIGN', 'GREEK SMALL LETTER MU')
Ainda que '1⁄2'
seja um substituto razoável para '½'
,
e o símbolo de micro ser realmente a letra grega mu minúscula, converter '4²'
para '42'
muda o sentido.
Uma aplicação poderia armazenar '4²'
como '4<sup>2</sup>'
,
mas a função normalize
não sabe nada sobre formatação.
Assim, NFKC ou NFKD podem perder ou distorcer informações,
mas podem produzir representações intermediárias convenientes para buscas ou indexação.
Infelizmente, com o Unicode tudo é sempre mais complicado do que parece à primeira vista.
Para o VULGAR FRACTION ONE HALF
, a normalização NFKC produz 1 e 2 unidos pelo FRACTION SLASH
,
em vez do SOLIDUS
, também conhecido como "barra" ("slash" em inglês)—o familiar caractere com código decimal 47 em ASCII.
Portanto, buscar pela sequência ASCII de três caracteres '1/2'
não encontraria a sequência Unicode normalizada.
Warning
|
As normalizações NFKC e NFKD causam perda de dados e devem ser aplicadas apenas em casos especiais, como busca e indexação, e não para armazenamento permanente do texto. |
Ao preparar texto para busca ou indexação, há outra operação útil: case folding [8], nosso próximo assunto.
Case folding é essencialmente a conversão de todo o texto para minúsculas, com algumas transformações adicionais. A operação é suportada pelo método str.casefold()
.
Para qualquer string s
contendo apenas caracteres latin1
, s.casefold()
produz o mesmo resultado de s.lower()
, com apenas duas exceções—o símbolo de micro, 'µ'
, é trocado pela letra grega mu minúscula (que é exatamente igual na maioria das fontes) e a letra alemã Eszett (ß), também chamada "s agudo" (scharfes S) se torna "ss":
>>> micro = 'µ'
>>> name(micro)
'MICRO SIGN'
>>> micro_cf = micro.casefold()
>>> name(micro_cf)
'GREEK SMALL LETTER MU'
>>> micro, micro_cf
('µ', 'μ')
>>> eszett = 'ß'
>>> name(eszett)
'LATIN SMALL LETTER SHARP S'
>>> eszett_cf = eszett.casefold()
>>> eszett, eszett_cf
('ß', 'ss')
Há quase 300 pontos de código para os quais str.casefold()
e str.lower()
devolvem resultados diferentes.
Como acontece com qualquer coisa relacionada ao Unicode, case folding é um tópico complexo, com muitos casos linguísticos especiais, mas o grupo central de desenvolvedores do Python fez um grande esforço para apresentar uma solução que, espera-se, funcione para a maioria dos usuários.
Nas próximas seções vamos colocar nosso conhecimento sobre normalização para trabalhar, desenvolvendo algumas funções utilitárias.
Como vimos, é seguro usar a NFC e a NFD, e ambas permitem comparações razoáveis entre strings Unicode. A NFC é a melhor forma normalizada para a maioria das aplicações, e str.casefold()
é a opção certa para comparações indiferentes a maiúsculas/minúsculas.
Se você precisa lidar com texto em muitas línguas diferentes, seria muito útil acrescentar às suas ferramentas de trabalho um par de funções como nfc_equal
e fold_equal
, do Exemplo 13.
link:code/04-text-byte/normeq.py[role=include]
Além da normalização e do case folding do Unicode—ambos partes desse padrão—algumas vezes faz sentido aplicar transformações mais profundas, como por exemplo mudar 'café'
para 'cafe'
. Vamos ver quando e como na próxima seção.
O tempero secreto da busca do Google inclui muitos truques, mas um deles aparentemente é ignorar sinais diacríticos (acentos e cedilhas, por exemplo), pelo menos em alguns contextos. Remover sinais diacríticos não é uma forma regular de normalização, pois muitas vezes muda o sentido das palavras e pode produzir falsos positivos em uma busca. Mas ajuda a lidar com alguns fatos da vida: as pessoas às vezes são preguiçosas ou desconhecem o uso correto dos sinais diacríticos, e regras de ortografia mudam com o tempo, levando acentos a desaparecerem e reaparecerem nas línguas vivas.
Além do caso da busca, eliminar os acentos torna as URLs mais legíveis, pelo menos nas línguas latinas. Veja a URL do artigo da Wikipedia sobre a cidade de São Paulo:
https://en.wikipedia.org/wiki/S%C3%A3o_Paulo
O trecho %C3%A3
é a renderização em UTF-8 de uma única letra, o "ã" ("a" com til). A forma a seguir é muito mais fácil de reconhecer, mesmo com a ortografia incorreta:
https://en.wikipedia.org/wiki/Sao_Paulo
Para remover todos os sinais diacríticos de uma str
, você pode usar uma função como a do Exemplo 14.
link:code/04-text-byte/simplify.py[role=include]
-
Decompõe todos os caracteres em caracteres base e marcações combinadas.
-
Filtra e retira todas as marcações combinadas.
-
Recompõe todos os caracteres.
Exemplo 15 mostra alguns usos para shave_marks
.
shave_marks
do Exemplo 14>>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”'
>>> shave_marks(order)
'“Herr Voß: • ½ cup of Œtker™ caffe latte • bowl of acai.”' (1)
>>> Greek = 'Ζέφυρος, Zéfiro'
>>> shave_marks(Greek)
'Ζεφυρος, Zefiro' (2)
-
Apenas as letras "è", "ç", e "í" foram substituídas.
-
Tanto "έ" quando "é" foram substituídas.
A função shave_marks
do Exemplo 14 funciona bem, mas talvez vá longe demais. Frequentemente, a razão para remover os sinais diacríticos é transformar texto de uma língua latina para ASCII puro, mas shave_marks
também troca caracteres não-latinos—como letras gregas—que nunca se tornarão ASCII apenas pela remoção de seus acentos. Então faz sentido analisar cada caractere base e remover as marcações anexas apenas se o caractere base for uma letra do alfabeto latino. É isso que o Exemplo 16 faz.
link:code/04-text-byte/simplify.py[role=include]
-
Decompõe todos os caracteres em caracteres base e marcações combinadas.
-
Pula as marcações combinadas quando o caractere base é latino.
-
Caso contrário, mantém o caractere original.
-
Detecta um novo caractere base e determina se ele é latino.
-
Recompõe todos os caracteres.
Um passo ainda mais radical substituiria os símbolos comuns em textos de línguas ocidentais (por exemplo, aspas curvas, travessões, os círculos de bullet points, etc) em seus equivalentes ASCII
. É isso que a função asciize
faz no Exemplo 17.
link:code/04-text-byte/simplify.py[role=include]
-
Cria uma tabela de mapeamento para substituição de caractere para caractere.
-
Cria uma tabela de mapeamento para substituição de string para caractere.
-
Funde as tabelas de mapeamento.
-
dewinize
não afeta texto emASCII
oulatin1
, apenas os acréscimos da Microsoft aolatin1
nocp1252
. -
Aplica
dewinize
e remove as marcações de sinais diacríticos. -
Substitui o Eszett por "ss" (não estamos usando case folding aqui, pois queremos preservar maiúsculas e minúsculas).
-
Aplica a normalização NFKC para compor os caracteres com seus pontos de código de compatibilidade.
O Exemplo 18 mostra a asciize
em ação.
asciize
, do Exemplo 17>>> order = '“Herr Voß: • ½ cup of Œtker™ caffè latte • bowl of açaí.”'
>>> dewinize(order)
'"Herr Voß: - ½ cup of OEtker(TM) caffè latte - bowl of açaí."' (1)
>>> asciize(order)
'"Herr Voss: - 1⁄2 cup of OEtker(TM) caffe latte - bowl of acai."' (2)
-
dewinize
substitui as aspas curvas, os bullets, e o ™ (símbolo de marca registrada). -
asciize
aplicadewinize
, remove os sinais diacríticos e substitui o'ß'
.
Warning
|
Cada língua tem suas próprias regras para remoção de sinais diacríticos. Por exemplo, os alemães trocam o |
Resumindo, as funções em simplify.py vão bem além da normalização padrão, e realizam uma cirurgia profunda no texto, com boas chances de mudar seu sentido. Só você pode decidir se deve ir tão longe, conhecendo a língua alvo, os seus usuários e a forma como o texto transformado será utilizado.
Isso conclui nossa discussão sobre normalização de texto Unicode.
Vamos agora ordenar nossos pensamentos sobre ordenação no Unicode.
O Python ordena sequências de qualquer tipo comparando um por um os itens em cada sequência. Para strings, isso significa comparar pontos de código. Infelizmente, isso produz resultados inaceitáveis para qualquer um que use caracteres não-ASCII.
Considere ordenar uma lista de frutas cultivadas no Brazil:
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
>>> sorted(fruits)
['acerola', 'atemoia', 'açaí', 'caju', 'cajá']
As regras de ordenação variam entre diferentes locales, mas em português e em muitas línguas que usam o alfabeto latino, acentos e cedilhas raramente fazem diferença na ordenação.[9] Então "cajá" é lido como "caja," e deve vir antes de "caju."
A lista fruits
ordenada deveria ser:
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
O modo padrão de ordenar texto não-ASCII em Python é usar a função locale.strxfrm
que, de acordo com a
documentação do módulo locale
, "Transforma uma string em uma que pode ser usada em comparações com reconhecimento de localidade."
Para poder usar locale.strxfrm
, você deve primeiro definir um locale adequado para sua aplicação, e rezar para que o SO o suporte. A sequência de comando no Exemplo 19 pode funcionar para você.
locale.strxfrm
como chave de ornenamentolink:code/04-text-byte/locale_sort.py[role=include]
Executando o Exemplo 19 no GNU/Linux (Ubuntu 19.10) com o locale pt_BR.UTF-8
instalado, consigo o resultado correto:
'pt_BR.UTF-8'
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
Portanto, você precisa chamar setlocale(LC_COLLATE, «your_locale»)
antes de usar locale.strxfrm
como a chave de ordenação.
Porém, aqui vão algumas ressalvas:
-
Como as configurações de locale são globais, não é recomendado chamar
setlocale
em uma biblioteca. Sua aplicação ou framework deveria definir o locale no início do processo, e não mudá-lo mais depois disso. -
O locale desejado deve estar instalado no SO, caso contrário
setlocale
gera uma exceção delocale.Error: unsupported locale setting
. -
Você tem que saber como escrever corretamente o nome do locale.
-
O locale precisa ser corretamente implementado pelos desenvolvedores do SO. Tive sucesso com o Ubuntu 19.10, mas não no macOS 10.14. No macOS, a chamada
setlocale(LC_COLLATE, 'pt_BR.UTF-8')
devolve a string'pt_BR.UTF-8'
sem qualquer reclamação. Massorted(fruits, key=locale.strxfrm)
produz o mesmo resultado incorreto desorted(fruits)
. Também tentei os localesfr_FR
,es_ES
, ede_DE
no macOS, maslocale.strxfrm
nunca fez seu trabalho direito.[10]
Portanto, a solução da biblioteca padrão para ordenação internacionalizada funciona, mas parece ter suporte adequado apenas no GNU/Linux (talvez também no Windows, se você for um especialista). Mesmo assim, ela depende das configurações do locale, criando dores de cabeça na implantação.
Felizmente, há uma solução mais simples: a biblioteca pyuca, disponível no PyPI.
James Tauber, contribuidor muito ativo do Django, deve ter sentido essa nossa mesma dor, e criou a pyuca, uma implementação integralmente em Python do Algoritmo de Ordenação do Unicode (UCA, sigla em inglês para Unicode Collation Algorithm). O Exemplo 20 mostra como ela é fácil de usar.
pyuca.Collator.sort_key
>>> import pyuca
>>> coll = pyuca.Collator()
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
>>> sorted_fruits = sorted(fruits, key=coll.sort_key)
>>> sorted_fruits
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
Isso é simples e funciona no GNU/Linux, no macOS, e no Windows, pelo menos com a minha pequena amostra.
A pyuca
não leva o locale em consideração. Se você precisar personalizar a ordenação, pode fornecer um caminho para uma tabela própria de ordenação para o construtor Collator()
. Sem qualquer configuração adicional, a biblioteca usa o allkeys.txt, incluído no projeto. Esse arquivo é apenas uma cópia da Default Unicode Collation Element Table (Tabela Default de Ordenação de Elementos Unicode) do Unicode.org .
Tip
|
PyICU: A recomendação do Miro para ordenação com Unicode
(O revisor técnico Miroslav Šedivý é um poliglota e um especialista em Unicode. Eis o que ele escreveu sobre a pyuca.) A pyuca tem um algoritmo de ordenação que não respeita o padrão de ordenação de linguagens individuais. Por exemplo, [a letra] Ä em alemão fica entre o A e o B, enquanto em sueco ela vem depois do Z. Dê uma olhada na PyICU, que funciona como locale sem modificar o locale do processo. Ela também é necessária se você quiser mudar a capitalização de iİ/ıI em turco. A PyICU inclui uma extensão que precisa ser compilada, então pode ser mais difícil de instalar em alguns sistemas que a pyuca, que é toda feita em Python. |
E por sinal, aquela tabela de ordenação é um dos muitos arquivos de dados que formam o banco de dados do Unicode, nosso próximo assunto.
O padrão Unicode fornece todo um banco de dados—na forma de vários arquivos de texto estruturados—que inclui não apenas a tabela mapeando pontos de código para nomes de caracteres, mas também metadados sobre os caracteres individuais e como eles se relacionam. Por exemplo, o banco de dados do Unicode registra se um caractere pode ser impresso, se é uma letra, um dígito decimal ou algum outro símbolo numérico.
É assim que os métodos de str
isalpha
, isprintable
, isdecimal
e isnumeric
funcionam.
str.casefold
também usa informação de uma tabela do Unicode.
Note
|
A função |
O módulo unicodedata
tem funções para obter os metadados de caracteres, incluindo
unicodedata.name()
, que devolve o nome oficial do caractere no padrão.
A Figura 5 demonstra essa função.[11]
Você pode usar a função name()
para criar aplicações que permitem aos usuários buscarem caracteres por nome.
A Figura 6 demonstra o script de comando de linha cf.py, que recebe como argumentos uma ou mais palavras, e lista os caracteres que tem aquelas palavras em seus nomes Unicode oficiais.
O código fonte completo de cf.py aparece no Exemplo 21.
Warning
|
O suporte a emojis varia muito entre sistemas operacionais e aplicativos. Nos últimos anos, o terminal do macOS tem oferecido o melhor suporte para emojis, seguido por terminais gráficos GNU/Linux modernos. O cmd.exe e o PowerShell do Windows agora suportam saída Unicode, mas enquanto escrevo essa seção, em janeiro de 2020, eles ainda não mostram emojis—pelo menos não sem configurações adicionais. O revisor técnico Leonardo Rochael me falou sobre um novo terminal para Windows da Microsoft, de código aberto, que pode ter um suporte melhor a Unicode que os consoles antigos da Microsoft. Não tive tempo de testar. |
No Exemplo 21, observe que o comando if
, na função find
, usa o método .issubset()
para testar rapidamente se todas as palavras no conjunto query
aparecem na lista de palavras criada a partir do nome do caractere.
Graças à rica API de conjuntos do Python, não precisamos de um loop for
aninhado e de outro if
para implementar essa verificação
link:code/04-text-byte/charfinder/cf.py[role=include]
-
Configura os defaults para a faixa de pontos de código da busca.
-
find
aceitaquery_words
e somente argumentos nomeados (opcionais) para limitar a faixa da busca, facilitando os testes. -
Converte
query_words
em um conjunto de strings capitalizadas. -
Obtém o caractere Unicode para
code
. -
Obtém o nome do caractere, ou
None
se o ponto de código não estiver atribuído a um caractere. -
Se há um nome, separa esse nome em uma lista de palavras, então verifica se o conjunto
query
é um subconjunto daquela lista. -
Mostra uma linha com o ponto de código no formato
U+9999
, o caractere e seu nome.
O módulo unicodedata
tem outras funções interessantes. A seguir veremos algumas delas, relacionadas a obter informação de caracteres com sentido numérico.
O módulo unicodedata
inclui funções para determinar se um caractere Unicode representa um número e, se for esse o caso, seu valor numérico em termos humanos—em contraste com o número de seu ponto de código.
O Exemplo 22 demonstra o uso de unicodedata.name()
e unicodedata.numeric()
,
junto com os métodos .isdecimal()
e .isnumeric()
de str
.
link:code/04-text-byte/numerics_demo.py[role=include]
-
Ponto de código no formato
U+0000
. -
O caractere, centralizado em uma
str
de tamanho 6. -
Mostra
re_dig
se o caractere casa com a regexr'\d'
. -
Mostra
isdig
sechar.isdigit()
éTrue
. -
Mostra
isnum
sechar.isnumeric()
éTrue
. -
Valor numérico formatado com tamanho 5 e duas casa decimais.
-
O nome Unicode do caractere.
Executar o Exemplo 22 gera a Figura 7, se a fonte do seu terminal incluir todos aqueles símbolos.
re_dig
significa que o caractere casa com a expressão regular r'\d'
.A sexta coluna da Figura 7 é o resultado da chamada a unicodedata.numeric(char)
com o caractere. Ela mostra que o Unicode sabe o valor numérico de símbolos que representam números. Assim, se você quiser criar uma aplicação de planilha que suporta dígitos tamil ou numerais romanos, vá fundo!
A Figura 7 mostra que a expressão regular r'\d'
casa com o dígito "1" e com o dígito devanágari 3, mas não com alguns outros caracteres considerados dígitos pela função isdigit
.
O módulo re
não é tão conhecedor de Unicode quanto deveria ser.
O novo módulo regex
, disponível no PyPI, foi projetado para um dia substituir o re
, e fornece um suporte melhor ao Unicode.[12]
Voltaremos ao módulo re
na próxima seção.
Ao longo desse capítulo, usamos várias funções de unicodedata
, mas há muitas outras que não mencionamos. Veja a documentação da biblioteca padrão para o módulo unicodedata
.
A seguir vamos dar uma rápida passada pelas APIs de modo dual, com funções que aceitam argumentos str
ou bytes
e dão a eles tratamento especial dependendo do tipo.
A biblioteca padrão do Python tem funções que aceitam argumentos str
ou bytes
e se comportam de forma diferente dependendo do tipo recebido. Alguns exemplos podem ser encontrados nos módulos re
e os
.
Se você criar uma expressão regular com bytes
, padrões tal como \d
e \w
vão casar apenas com caracteres ASCII; por outro lado, se esses padrões forem passados como str
, eles vão casar com dígitos Unicode ou letras além do ASCII. O Exemplo 23 e a Figura 8 comparam como letras, dígitos ASCII, superescritos e dígitos tamil casam em padrões str
e bytes
.
str
e como bytes
link:code/04-text-byte/ramanujan.py[role=include]
-
As duas primeiras expressões regulares são do tipo
str
. -
As duas últimas são do tipo
bytes
. -
Texto Unicode para ser usado na busca, contendo os dígitos tamil para
1729
(a linha lógica continua até o símbolo de fechamento de parênteses). -
Essa string é unida à anterior no momento da compilação (veja "2.4.2. String literal concatenation" (Concatenação de strings literais) em A Referência da Linguagem Python).
-
Uma string
bytes
é necessária para a busca com as expressões regularesbytes
. -
O padrão
str
r'\d+'
casa com os dígitos ASCII e tamil. -
O padrão
bytes
rb'\d+'
casa apenas com os bytes ASCII para dígitos. -
O padrão
str
r'\w+'
casa com letras, superescritos e dígitos tamil e ASCII. -
O padrão
bytes
rb'\w+'
casa apenas com bytes ASCII para letras e dígitos.
O Exemplo 23 é um exemplo trivial para destacar um ponto: você pode usar expressões regulares com str
ou bytes
, mas nesse último caso os bytes fora da faixa do ASCII são tratados como caracteres que não representam dígitos nem palavras.
Para expressões regulares str
, há uma marcação re.ASCII
, que faz \w
, \W
, \b
, \B
, \d
, \D
, \s
, e \S
executarem um casamento apenas com ASCII. Veja a documentaçào do módulo re
para maiores detalhes.
Outro módulo importante é o os
.
O kernel do GNU/Linux não conhece Unicode então, no mundo real, você pode encontrar nomes de arquivo compostos de sequências de bytes que não são válidas em nenhum esquema razoável de codificação, e não podem ser decodificados para str
. Servidores de arquivo com clientes usando uma variedade de diferentes SOs são particularmente inclinados a apresentar esse cenário.
Para mitigar esse problema, todas as funções do módulo os
que aceitam nomes de arquivo ou caminhos podem receber seus argumentos como str
ou bytes
. Se uma dessas funções é chamada com um argumento str
, o argumento será automaticamente convertido usando o codec informado por sys.getfilesystemencoding()
, e a resposta do SO será decodificada com o mesmo codec. Isso é quase sempre o que se deseja, mantendo a melhor prática do sanduíche de Unicode.
Mas se você precisa lidar com (e provavelmente corrigir) nomes de arquivo que não podem ser processados daquela forma, você pode passar argumentos bytes
para as funções de os
, e receber bytes
de volta. Esse recurso permite que você processe qualquer nome de arquivo ou caminho, independende de quantos gremlins encontrar. Veja o Exemplo 24.
listdir
com argumentos str
e bytes
, e os resultados>>> os.listdir('.') # (1)
['abc.txt', 'digits-of-π.txt']
>>> os.listdir(b'.') # (2)
[b'abc.txt', b'digits-of-\xcf\x80.txt']
-
O segundo nome de arquivo é "digits-of-π.txt" (com a letra grega pi).
-
Dado um argumento
byte
,listdir
devolve nomes de arquivos como bytes:b'\xcf\x80'
é a codificação UTF-8 para a letra grega pi.
Para ajudar no processamento manual de sequências str
ou bytes
que são nomes de arquivos ou caminhos,
o módulo os
fornece funções especiais de codificação e decodificação, os.fsencode(name_or_path)
e os.fsdecode(name_or_path)
. Ambas as funções aceitam argumentos dos tipos str
, bytes
ou, desde o Python 3.6, um objeto que implemente a interface os.PathLike
.
O Unicode é um buraco de coelho bem fundo. É hora de encerrar nossa exploração de str
e bytes
.
Começamos o capítulo descartando a noção de que 1 caractere == 1 byte
. A medida que o mundo adota o Unicode, precisamos manter o conceito de strings de texto separado das sequências binárias que as representam em arquivos, e o Python 3 aplica essa separação.
Após uma breve passada pelos tipos de dados sequências binárias—bytes
, bytearray
, e memoryview
—, mergulhamos na codificação e na decodificação, com uma amostragem dos codecs importantes, seguida por abordagens para prevenir ou lidar com os abomináveis UnicodeEncodeError
, UnicodeDecodeError
e os SyntaxError
causados pela codificação errada em arquivos de código-fonte do Python.
A seguir consideramos a teoria e a prática de detecção de codificação na ausência de metadados: em teoria, não pode ser feita, mas na prática o pacote Chardet consegue realizar esse feito para uma grande quantidade de codificações populares. Marcadores de ordem de bytes foram apresentados como a única dica de codificação encontrada em arquivos UTF-16 e UTF-32—algumas vezes também em arquivos UTF-8.
Na seção seguinte, demonstramos como abrir arquivos de texto, uma tarefa fácil exceto por uma armadilha: o argumento nomeado encoding=
não é obrigatório quando se abre um arquivo de texto, mas deveria ser. Se você não especificar a codificação, terminará com um programa que consegue produzir "texto puro" que é incompatível entre diferentes plataformas, devido a codificações default conflitantes. Expusemos então as diferentes configurações de codificação usadas pelo Python, e como detectá-las.
Uma triste realidade para usuários de Windows é o fato dessas configurações muitas vezes terem valores diferentes dentro da mesma máquina, e desses valores serem mutuamente incompatíveis; usuários do GNU/Linux e do macOS, por outro lado, vivem em um lugar mais feliz, onde o UTF-8 é o default por (quase) toda parte.
O Unicode fornece múltiplas formas de representar alguns caracteres, então a normalização é um pré-requisito para a comparação de textos. Além de explicar a normalização e o case folding, apresentamos algumas funções úteis que podem ser adaptadas para as suas necessidades, incluindo transformações drásticas como a remoção de todos os acentos. Vimos como ordenar corretamente texto Unicode, usando o módulo padrão locale
—com algumas restrições—e uma alternativa que não depende de complexas configurações de locale: a biblioteca externa pyuca.
Usamos o banco de dados do Unicode para programar um utilitário de comando de linha que busca caracteres por nome—em 28 linhas de código, graças ao poder do Python.
Demos uma olhada em outros metadados do Unicode, e vimos rapidamente as APIs de modo dual, onde algumas funções podem ser chamadas com argumentos str
ou bytes
, produzindo resultados diferentes.
A palestra de Ned Batchelder na PyCon US 2012, "Pragmatic Unicode, or, How Do I Stop the Pain?" (Unicode Pragmático, ou, Como Eu Fiz a Dor Sumir?) (EN), foi marcante. Ned é tão profissional que forneceu uma transcrição completa da palestra, além dos slides e do vídeo.
"Character encoding and Unicode in Python: How to (╯°□°)╯︵ ┻━┻ with dignity" (Codificação de caracteres e o Unicode no Python: como (╯°□°)╯︵ ┻━┻ com dignidade) (slides, vídeo) (EN) foi uma excelente palestra de Esther Nam e Travis Fischer na PyCon 2014, e foi onde encontrei a concisa epígrafe desse capítulo: "Humanos usam texto. Computadores falam em bytes."
Lennart Regebro—um dos revisores técnicos da primeira edição desse livro—compartilha seu "Useful Mental Model of Unicode (UMMU)" (Modelo Mental Útil do Unicode) em um post curto, "Unconfusing Unicode: What Is Unicode?" (Desconfundindo o Unicode: O Que É O Unicode?) (EN). O Unicode é um padrão complexo, então o UMMU de Lennart é realmente um ponto de partida útil.
O "Unicode HOWTO" oficial na documentação do Python aborda o assunto por vários ângulos diferentes, de uma boa introdução histórica a detalhes de sintaxe, codecs, expressões regulares, nomes de arquivo, e boas práticas para E/S sensível ao Unicode (isto é, o sanduíche de Unicode), com vários links adicionais de referências em cada seção.
O Chapter 4, "Strings" (Capítulo 4, "Strings"), do maravilhosos livro Dive into Python 3 (EN), de Mark Pilgrim (Apress), também fornece uma ótima introdução ao suporte a Unicode no Python 3. No mesmo livro, o Capítulo 15 descreve como a biblioteca Chardet foi portada do Python 2 para o Python 3, um valioso estudo de caso, dado que a mudança do antigo tipo str
para o novo bytes
é a causa da maioria das dores da migração, e esta é uma preocupação central em uma biblioteca projetada para detectar codificações.
Se você conhece Python 2 mas é novo no Python 3, o artigo "What’s New in Python 3.0" (O quê há de novo no Python 3.0) (EN), de Guido van Rossum, tem 15 pontos resumindo as mudanças, com vários links. Guido inicia com uma afirmação brutal: "Tudo o que você achava que sabia sobre dados binários e Unicode mudou". O post de Armin Ronacher em seu blog, "The Updated Guide to Unicode on Python" O Guia Atualizado do Unicode no Python, é bastante profundo e realça algumas das armadilhas do Unicode no Python (Armin não é um grande fã do Python 3).
O capítulo 2 ("Strings and Text" Strings e Texto) do Python Cookbook, 3rd ed. (EN) (O’Reilly), de David Beazley e Brian K. Jones, tem várias receitas tratando de normalização de Unicode, sanitização de texto, e execução de operações orientadas para texto em sequências de bytes. O capítulo 5 trata de arquivos e E/S, e inclui a "Recipe 5.17. Writing Bytes to a Text File" (Receita 5.17. Escrevendo Bytes em um Arquivo de Texto), mostrando que sob qualquer arquivo de texto há sempre uma sequência binária que pode ser acessada diretamente quando necessário. Mais tarde no mesmo livro, o módulo struct
é usado em "Recipe 6.11. Reading and Writing Binary Arrays of Structures" (Receita 6.11. Lendo e Escrevendo Arrays Binárias de Estruturas).
O blog "Python Notes" de Nick Coghlan tem dois posts muito relevantes para esse capítulo: "Python 3 and ASCII Compatible Binary Protocols" (Python 3 e os Protocolos Binários Compatíveis com ASCII) (EN) e "Processing Text Files in Python 3" (Processando Arquivos de Texto em Python 3) (EN). Fortemente recomendado.
Uma lista de codificações suportadas pelo Python está disponível em "Standard Encodings" (EN), na documentação do módulo codecs
. Se você precisar obter aquela lista de dentro de um programa, pode ver como isso é feito no script /Tools/unicode/listcodecs.py, que acompanha o código-fonte do CPython.
Os livros Unicode Explained (Unicode Explicado) (EN), de Jukka K. Korpela (O’Reilly) e Unicode Demystified (Unicode Desmistificado), de Richard Gillam (Addison-Wesley) não são específicos sobre o Python, nas foram muito úteis para meu estudo dos conceitos do Unicode. Programming with Unicode (Programando com Unicode), de Victor Stinner, é um livro gratuito e publicado pelo próprio autor (Creative Commons BY-SA) tratando de Unicode em geral, bem como de ferramentas e APIs no contexto dos principais sistemas operacionais e algumas linguagens de programação, incluindo Python.
As páginas do W3C "Case Folding: An Introduction" (Case Folding: Uma Introdução) (EN) e "Character Model for the World Wide Web: String Matching" (O Modelo de Caracteres para a World Wide Web: Correspondência de Strings) (EN) tratam de conceitos de normalização, a primeira uma suave introdução e a segunda uma nota de um grupo de trabalho escrita no seco jargão dos padrões—o mesmo tom do "Unicode Standard Annex #15—Unicode Normalization Forms" (Anexo 15 do Padrão Unicode—Formas de Normalização do Unicode) (EN). A seção "Frequently Asked Questions, Normalization" (Perguntas Frequentes, Normalização) (EN) do Unicode.org é mais fácil de ler, bem como o "NFC FAQ" (EN) de Mark Davis—autor de vários algoritmos do Unicode e presidente do Unicode Consortium quando essa seção foi escrita.
Em 2016, o Museu de Arte Moderna (MoMA) de New York adicionou à sua coleção
o emoji original (EN),
os 176 emojis desenhados por Shigetaka Kurita em 1999 para a NTT DOCOMO—a provedora de telefonia móvel japonesa.
Indo mais longe no passado, a Emojipedia (EN) publicou o artigo "Correcting the Record on the First Emoji Set" (Corrigindo o Registro [Histórico] sobre o Primeiro Conjunto de Emojis) (EN), atribuindo ao SoftBank do Japão o mais antigo conjunto conhecido de emojis, implantado em telefones celulares em 1997.
O conjunto do SoftBank é a fonte de 90 emojis que hoje fazem parte do Unicode, incluindo o U+1F4A9 (PILE OF POO
).
O emojitracker.com, de Matthew Rothenberg, é um painel ativo mostrando a contagem do uso de emojis no Twitter, atualizado em tempo real.
Quando escrevo isso, FACE WITH TEARS OF JOY
(U+1F602) é o emoji mais popular no Twitter, com mais de
3.313.667.315 ocorrências registradas.
Nomes não-ASCII no código-fonte: você deveria usá-los?
O Python 3 permite identificadores não-ASCII no código-fonte:
>>> ação = 'PBR' # ação = stock
>>> ε = 10**-6 # ε = epsilon
Algumas pessoas não gostam dessa ideia. O argumento mais comum é que se limitar aos caracteres ASCII torna a leitura e a edição so código mais fácil para todo mundo. Esse argumento erra o alvo: você quer que seu código-fonte seja legível e editável pela audiência pretendida, e isso pode não ser "todo mundo". Se o código pertence a uma corporação multinacional, ou se é um código aberto e você deseja contribuidores de todo o mundo, os identificadores devem ser em inglês, e então tudo o que você precisa é do ASCII.
Mas se você é uma professora no Brasil, seus alunos vão achar mais fácil ler código com variáveis e nomes de função em português, e escritos corretamente. E eles não terão nenhuma dificuldade para digitar as cedilhas e as vogais acentuadas em seus teclados localizados.
Agora que o Python pode interpretar nomes em Unicode, e que o UTF-8 é a codificação padrão para código-fonte, não vejo motivo para codificar identificadores em português sem acentos, como fazíamos no Python 2, por necessidade—a menos que seu código tenha que rodar também no Python 2. Se os nomes estão em português, excluir os acentos não vai tornar o código mais legível para ninguém.
Esse é meu ponto de vista como um brasileiro falante de português, mas acredito que se aplica além de fronteiras e a outras culturas: escolha a linguagem humana que torna o código mais legível para sua equipe, e então use todos os caracteres necessários para a ortografia correta.
O que é "texto puro"?
Para qualquer um que lide diariamente com texto em línguas diferentes do inglês, "texto puro" não significa "ASCII". O Glossário do Unicode (EN) define texto puro dessa forma:
Texto codificado por computador que consiste apenas de uma sequência de pontos de código de um dado padrão, sem qualquer outra informação estrutural ou de formatação.
Essa definição começa muito bem, mas não concordo com a parte após a vírgula. HTML é um ótimo exemplo de um formato de texto puro que inclui informação estrutural e de formatação. Mas ele ainda é texto puro, porque cada byte em um arquivo desse tipo está lá para representar um caractere de texto, em geral usando UTF-8. Não há bytes com significado não-textual, como você encontra em documentos .png ou .xls, onde a maioria dos bytes representa valores binários empacotados, como valores RGB ou números de ponto flutuante. No texto puro, números são representados como sequências de caracteres de dígitos.
Estou escrevendo esse livro em um formato de texto puro chamado—ironicamente— AsciiDoc, que é parte do conjunto de ferramentas do excelente Atlas book publishing platform (plataforma de publicação de livros Atlas) da O’Reilly. Os arquivos fonte de AsciiDoc são texto puro, mas são UTF-8, e não ASCII. Se fosse o contrário, escrever esse capítulo teria sido realmente doloroso. Apesar do nome, o AsciiDoc é muito bom.
O mundo do Unicode está em constante expansão e, nas margens, as ferramentas de apoio nem sempre existem. Nem todos os caracteres que eu queria exibir estavam disponíveis nas fontes usadas para renderizar o livro. Por isso tive que usar imagens em vez de listagens em vários exemplos desse capítulo. Por outro lado, os terminais do Ubuntu e do macOS exibem a maioria do texto Unicode muito bem—incluindo os caracteres japoneses para a palavra "mojibake": 文字化け.
Como os ponto de código numa str são representados na RAM?
A documentação oficial do Python evita falar sobre como os pontos de código de uma str
são armazenados na memória.
Realmente, é um detalhe de implementação.
Em teoria, não importa: qualquer que seja a representação interna, toda str
precisa ser codificada para bytes
na saída.
Na memória, o Python 3 armazena cada str
como uma sequência de pontos de código, usando um número fixo de bytes por ponto de código, para permitir um acesso direto eficiente a qualquer caractere ou fatia.
Desde o Python 3.3, ao criar um novo objeto str
o interpretador verifica os caracteres no objeto, e escolhe o layout de memória mais econômico que seja adequado para aquela str
em particular:
se existirem apenas caracteres na faixa latin1
, aquela str
vai usar apenas um byte por ponto de código.
Caso contrário, podem ser usados dois ou quatro bytes por ponto de código, dependendo da str
.
Isso é uma simplificação; para saber todos os detalhes, dê uma olhada an PEP 393—Flexible String Representation (Representação Flexível de Strings) (EN).
A representação flexível de strings é similar à forma como o tipo int
funciona no Python 3:
se um inteiro cabe em uma palavra da máquina, ele será armazenado em uma palavra da máquina.
Caso contrário, o interpretador muda para uma representação de tamanho variável, como aquela do tipo long
do Python 2.
É bom ver as boas ideias se espalhando.
Entretanto, sempre podemos contar com Armin Ronacher para encontrar problemas no Python 3.
Ele me explicou porque, na prática, essa não é uma ideia tão boa assim:
basta um único RAT
(U+1F400) para inflar um texto, que de outra forma seria inteiramente ASCII, e transformá-lo em um array sugadora de memória, usando quatro bytes por caractere, quando um byte seria o suficiente para todos os caracteres exceto o RAT. Além disso, por causa de todas as formas como os caracteres Unicode se combinam, a capacidade de buscar um caractere arbitrário pela posição é superestimada—e extrair fatias arbitrárias de texto Unicode é no mínimo ingênuo, e muitas vezes errado, produzindo mojibake.
Com os emojis se tornando mais populares, esses problemas vão só piorar.
setlocale
e locale.strxfrm
em seu Macintosh com o macOS 10.9. Em resumo: cada caso é um caso.
re
para identificar dígitos nessa amostra em particular.