Capítulo 4. Texto Unicode frente a Bytes
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Los humanos usamos texto. Los ordenadores hablan bytes.
Esther Nam y Travis Fischer, "Codificación de caracteres y Unicode en Python"1
Python 3 introdujo una clara distinción entre cadenas de texto humano y secuencias de bytes sin procesar. La conversión implícita de secuencias de bytes a texto Unicode es cosa del pasado. Este capítulo trata de las cadenas Unicode, las secuencias binarias y las codificaciones utilizadas para convertir entre ellas.
Dependiendo del tipo de trabajo que hagas con Python, puedes pensar que entender Unicode no es importante. Es poco probable, pero de todos modos no hay forma de escapar a la división entre str
y byte
. Como extra, descubrirás que los tipos de secuencia binaria especializados proporcionan características que el tipo "multiuso" de Python 2 str
no tenía.
En este capítulo, visitaremos los siguientes temas:
-
Caracteres, puntos de código y representaciones de bytes
-
Características únicas de las secuencias binarias:
bytes
,bytearray
, ymemoryview
-
Codificaciones para Unicode completo y conjuntos de caracteres heredados
-
Evitar y tratar los errores de codificación
-
Buenas prácticas en el manejo de archivos de texto
-
La trampa de codificación por defecto y los problemas de E/S estándar
-
Comparaciones de texto Unicode seguras con normalización
-
Funciones útiles para normalizar, plegar mayúsculas y minúsculas y eliminar diacríticos por fuerza bruta
-
Ordenación correcta de texto Unicode con
locale
y la biblioteca pyuca -
Metadatos de caracteres en la base de datos Unicode
-
API de modo dual que gestionan
str
ybytes
Novedades de este capítulo
Soporte para Unicode en Python 3 ha sido completo y estable, por lo que la adición más notable es "Encontrar caracteres por nombre", que describe una utilidad para buscar en la base de datos Unicode, una forma estupenda de encontrar dígitos en círculo y gatos sonrientes desde la línea de comandos.
Un cambio menor que merece la pena mencionar es el soporte de Unicode en Windows, que es mejor y más sencillo desde Python 3.6, como veremos en "Cuidado con la codificación por defecto".
Empecemos con los conceptos no tan nuevos, pero fundamentales, de caracteres, puntos de código y bytes.
Nota
Para la segunda edición, amplié la sección sobre el módulo struct
y la publiqué en línea en"Parsing binary records with struct", en el sitio web complementario fluentpython.com.
En también encontrarás"Creación de emojis de varios caracteres", que describe cómo hacer banderas de países, banderas arco iris, personas con distintos tonos de piel e iconos de familias diversas combinando caracteres Unicode.
Cuestiones de carácter
El concepto de "cadena" es bastante sencillo: una cadena es una secuencia de caracteres. El problema reside en la definición de "carácter".
En 2021, la mejor definición de "carácter" que tenemos es un carácter Unicode. En consecuencia, los elementos que obtenemos de un objeto Python 3 str
son caracteres Unicode, igual que los elementos de un objeto unicode
en Python 2, y no los bytes en bruto que obteníamos de un objeto Python 2 str
.
La norma Unicode separa explícitamente la identidad de los caracteres de las representaciones específicas en bytes:
-
La identidad de un carácter -su punto de código - esun número del 0 al 1.114.111 (base 10), mostrado en el estándar Unicode como 4 a 6 dígitos hexadecimales con un prefijo "U+", de U+0000 a U+10FFFF. Por ejemplo, el punto de código para la letra A es U+0041, el signo Euro es U+20AC, y el símbolo musical clave de Sol está asignado al punto de código U+1D11E. Aproximadamente el 13% de los puntos de código válidos tienen caracteres asignados en Unicode 13.0.0, el estándar utilizado en Python 3.10.0b4.
-
Los bytes reales que representan un carácter dependen de la codificación que se utilice. Una codificación es un algoritmo que convierte puntos de código en secuencias de bytes y viceversa. El punto de código para la letra A (U+0041) se codifica como el byte único
\x41
en la codificación UTF-8, o como los bytes\x41\x00
en la codificación UTF-16LE. Como otro ejemplo, UTF-8 requiere tres bytes -\xe2\x82\xac
- para codificar el signo Euro (U+20AC), pero en UTF-16LE el mismo punto de código se codifica como dos bytes:\xac\x20
.
Convertir de puntos de código a bytes es codificar; convertir de bytes a puntos de código es descodificar. Véase el ejemplo 4-1.
Ejemplo 4-1. Codificación y descodificación
>>
>
s
=
'
café
'
>>
>
len
(
s
)
4
>>
>
b
=
s
.
encode
(
'
utf8
'
)
>>
>
b
b
'
caf
\xc3
\xa9
'
>>
>
len
(
b
)
5
>>
>
b
.
decode
(
'
utf8
'
)
'
café
'
La dirección
str
'café'
tiene cuatro caracteres Unicode.Codifica
str
enbytes
utilizando la codificación UTF-8.bytes
tienen un prefijob
.bytes
b
tiene cinco bytes (el punto de código para "é" se codifica como dos bytes en UTF-8).Descodifica
bytes
astr
utilizando la codificación UTF-8.
Consejo
Si necesitas una ayuda para la memoria que te ayude a distinguir .decode()
de .encode()
, convéncete de que las secuencias de bytes pueden ser volcados crípticos del núcleo de la máquina, mientras que los objetos Unicode str
son texto "humano". Por tanto, tiene sentido que descodifiquemos bytes
a str
para obtener texto legible por humanos, y que codifiquemos str
a bytes
para su almacenamiento o transmisión.
Aunque el tipo str
de Python 3 es más o menos el tipo unicode
de Python 2 con un nuevo nombre, el tipo bytes
de Python 3 no es simplemente el antiguo str
con un nuevo nombre, y también existe el tipo bytearray
, estrechamente relacionado. Así que merece la pena echar un vistazo a los tipos de secuencia binaria antes de avanzar en cuestiones de codificación/decodificación.
Byte Esencial
Los nuevos tipos de secuencias binarias son distintos de los de Python 2 str
en muchos aspectos. Lo primero que debes saber es que hay dos tipos básicos incorporados para las secuencias binarias: el tipo inmutable bytes
, introducido en Python 3, y el mutable bytearray
, añadido ya en Python 2.6.2 La documentación de Python utiliza a veces el término genérico "cadena de bytes" para referirse tanto a bytes
como a bytearray
. Yo evito ese término confuso.
Cada elemento de bytes
o bytearray
es un número entero de 0 a 255, y no una cadena de un carácter como en Python 2 str
. Sin embargo, una rebanada de una secuencia binaria siempre produce una secuencia binaria del mismo tipo, incluidas las rebanadas de longitud 1. Véase el Ejemplo 4-2.
Ejemplo 4-2. Una secuencia de cinco bytes como bytes
y como bytearray
>>>
cafe
=
bytes
(
'
café
'
,
encoding
=
'
utf_8
'
)
>>>
cafe
b'caf\xc3\xa9'
>>>
cafe
[
0
]
99
>>>
cafe
[
:
1
]
b'c'
>>>
cafe_arr
=
bytearray
(
cafe
)
>>>
cafe_arr
bytearray(b'caf\xc3\xa9')
>>>
cafe_arr
[
-
1
:
]
bytearray(b'\xa9')
bytes
puede construirse a partir de unstr
, dada una codificación.Cada elemento es un número entero en
range(256)
.Los trozos de
bytes
también sonbytes
, incluso trozos de un solo byte.No hay sintaxis literal para
bytearray
: se muestran comobytearray()
con un literalbytes
como argumento.Una rodaja de
bytearray
también es unbytearray
.
Advertencia
El hecho de que my_bytes[0]
recupere un int
pero my_bytes[:1]
devuelva una secuencia bytes
delongitud 1 sólo es sorprendente porque estamos acostumbrados al tipo str
de Python, donde s[0] == s[:1]
. Para todos los demás tipos de secuencia de Python, 1 elemento no es lo mismo que una porción delongitud 1.
Aunque las secuencias binarias son en realidad secuencias de números enteros, su notación literal refleja el hecho de que en ellas suele ir incrustado texto ASCII, por lo que se utilizan cuatro visualizaciones diferentes, según el valor de cada byte:
-
Para los bytes con códigos decimales del 32 al 126 -del espacio a
~
(tilde)- se utiliza el propio carácter ASCII. -
Para los bytes correspondientes a tabulador, nueva línea, retorno de carro y
\
, se utilizan las secuencias de escape\t
,\n
,\r
y\\
. -
Si ambos delimitadores de cadena
'
y"
aparecen en la secuencia de bytes, toda la secuencia se delimita con'
, y cualquier'
que haya dentro se escapa como\'
.3 -
Para otros valores de byte, se utiliza una secuencia de escape hexadecimal (por ejemplo,
\x00
es el byte nulo).
Por eso en el Ejemplo 4-2 ves b'caf\xc3\xa9'
: los tres primeros bytes b'caf'
están en el rango ASCII imprimible, los dos últimos no.
Tanto bytes
como bytearray
soportan todos los métodos de str
excepto los que hacen formateo (format
, format_map
) y los que dependen de datos Unicode, incluyendo casefold
, isdecimal
, isidentifier
, isnumeric
, isprintable
, y encode
. Esto significa que puedes utilizar métodos de cadena conocidos como endswith
, replace
, strip
, translate
, upper
, y docenas de otros con secuencias binarias -sólo utilizando bytes
y no str
argumentos.
Además, las funciones de expresiones regulares del módulo re
también funcionan con secuencias binarias, si la expresión regular se compila a partir de una secuencia binaria en lugar de una str
. Desde Python 3.5, el operador %
vuelve a funcionar con secuencias binarias.4
Las secuencias binarias tienen un método de clase que no tiene str
, llamado fromhex
, que construye una secuencia binaria analizando pares de dígitos hexadecimales opcionalmente separados por espacios:
>>>
bytes
.
fromhex
(
'31 4B CE A9'
)
b'1K\xce\xa9'
Las otras formas de construir instancias de bytes
o bytearray
son llamar a sus constructores con:
-
Un argumento
str
y una palabra claveencoding
-
Un iterable que proporciona elementos con valores de 0 a 255
-
Un objeto que implementa el protocolo de búfer (por ejemplo,
bytes
,bytearray
,memoryview
,array.array
) que copia los bytes del objeto fuente a la secuencia binaria recién creada.
Advertencia
Hasta Python 3.5, también era posible llamar a bytes
o bytearray
con un único número entero para crear una secuencia binaria de ese tamaño inicializada con bytes nulos. Esta firma quedó obsoleta en Python 3.5 y se eliminó en Python 3.6. Véase PEP 467-Menores mejoras de la API para secuencias binarias.
Construir una secuencia binaria a partir de un objeto tipo buffer es una operación de bajo nivel que puede implicar una fundición de tipos. Mira una demostración en el Ejemplo 4-3.
Ejemplo 4-3. Inicializar bytes a partir de los datos brutos de una matriz
>>>
import
array
>>>
numbers
=
array
.
array
(
'
h
'
,
[
-
2
,
-
1
,
0
,
1
,
2
]
)
>>>
octets
=
bytes
(
numbers
)
>>>
octets
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'
Typecode
'h'
crea unarray
de enteros cortos (16 bits).octets
contiene una copia de los bytes que componennumbers
.Son los 10 bytes que representan los 5 enteros cortos.
La creación de un objeto bytes
o bytearray
a partir de cualquier fuente tipo buffer siempre copiará los bytes. En cambio, los objetos memoryview
te permiten compartir memoria entre estructuras de datos binarias, como vimos en "Vistas de la memoria".
Tras esta exploración básica de los tipos de secuencias binarias en Python, veamos cómo se convierten en/de cadenas.
Codificadores/Decodificadores básicos
La distribución de Python incluye más de 100 códecs (codificadores/decodificadores) para la conversión de texto a bytes y viceversa. Cada códec tiene un nombre, como 'utf_8'
, y a menudo alias, como 'utf8'
, 'utf-8'
, y 'U8'
, que puedes utilizar como argumento encoding
en funciones comoopen()
, str.encode()
, bytes.decode()
, etc.El ejemplo 4-4 muestra el mismo texto codificado como tres secuencias de bytes diferentes.
Ejemplo 4-4. La cadena "El Niño" codificada con tres códecs que producen secuencias de bytes muy diferentes
>>>
for
codec
in
[
'latin_1'
,
'utf_8'
,
'utf_16'
]:
...
(
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'
La Figura 4-1 muestra diversos códecs que generan bytes a partir de caracteres como la letra "A" hasta el símbolo musical G-clef. Observa que las tres últimas codificaciones son codificaciones multibyte de longitud variable.
Todos esos asteriscos de la Figura 4-1 dejan claro que algunas codificaciones, como ASCII e incluso la multibyte GB2312, no pueden representar todos los caracteres Unicode. Las codificaciones UTF, sin embargo, están diseñadas para manejar todos los puntos de código Unicode.
Las codificaciones que aparecen en la Figura 4-1 se eligieron como muestra representativa:
latin1
aliasiso8859_1
-
Importante porque es la base de otras codificaciones, como
cp1252
y el propio Unicode (observa cómo los valores de los bytes delatin1
aparecen en los bytes decp1252
e incluso en los puntos de código). cp1252
-
Un útil superconjunto de
latin1
creado por Microsoft, que añade símbolos útiles como las comillas rizadas y € (euro); algunas aplicaciones de Windows lo llaman "ANSI", pero nunca fue un verdadero estándar ANSI. cp437
-
El juego de caracteres original del IBM PC, con caracteres de dibujo de recuadros. Incompatible con
latin1
, que apareció más tarde. gb2312
-
Norma heredada para codificar los ideogramas chinos simplificados utilizados en China continental; una de las varias codificaciones multibyte ampliamente implementadas para las lenguas asiáticas.
utf-8
-
La codificación de 8 bits más común en la web, con diferencia, en julio de 2021,"W3Techs: Usage statistics of character encodings for websites"afirma que el 97% de los sitios utilizan UTF-8, frente al 81,4% cuando escribí este párrafo en la primera edición de este libro, en septiembre de 2014.
utf-16le
-
Una forma del esquema de codificación UTF de 16 bits; todas las codificaciones UTF-16 admiten puntos de código más allá de U+FFFF mediante secuencias de escape llamadas "pares sustitutos".
Advertencia
UTF-16 sustituyó a la codificación original Unicode 1.0 de 16 bits -UCS-2- allá por 1996. UCS-2 se sigue utilizando en muchos sistemas a pesar de estar obsoleta desde el siglo pasado porque sólo admite puntos de código hasta U+FFFF. En 2021, más del 57% de los puntos de código asignados están por encima de U+FFFF, incluidos los importantísimos emojis.
Una vez completada esta visión general de las codificaciones más comunes, pasamos a tratar los problemas de las operaciones de codificación y descodificación.
Comprender los problemas de codificación/decodificación
Aunque en hay una excepción genérica UnicodeError
, el error notificado por Python suele ser más específico: o bien un UnicodeEncodeError
(al convertir str
a secuencias binarias) o bien un UnicodeDecodeError
(al leer secuencias binarias en str
). La carga de módulos Python también puede lanzar SyntaxError
cuando la codificación fuente es inesperada. Mostraremos cómo manejar todos estos errores en las siguientes secciones.
Consejo
Lo primero que hay que tener en cuenta cuando se produce un error Unicode es el tipo exacto de la excepción. ¿Es un UnicodeEncodeError
, un UnicodeDecodeError
, o algún otro error (por ejemplo, SyntaxError
) que menciona un problema de codificación? Para resolver el problema, primero tienes que entenderlo.
Hacer frente a UnicodeEncodeError
La mayoría de los códecs no UTF de sólo manejan un pequeño subconjunto de caracteres Unicode. Al convertir texto a bytes, si un carácter no está definido en la codificación de destino, apareceráUnicodeEncodeError
, a menos que se proporcione un tratamiento especial pasando un argumento errors
al método o función de codificación. El comportamiento de los manejadores de errores se muestra en el Ejemplo 4-5.
Ejemplo 4-5. Codificación a bytes: éxito y tratamiento de errores
>>>
city
=
'
São Paulo
'
>>>
city
.
encode
(
'
utf_8
'
)
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
'
)
b'S\xe3o Paulo'
>>>
city
.
encode
(
'
cp437
'
)
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
'
)
b'So Paulo'
>>>
city
.
encode
(
'
cp437
'
,
errors
=
'
replace
'
)
b'S?o Paulo'
>>>
city
.
encode
(
'
cp437
'
,
errors
=
'
xmlcharrefreplace
'
)
b'São Paulo'
Las codificaciones UTF manejan cualquier
str
.iso8859_1
también funciona para la cadena'São Paulo'
.cp437
no puede codificar la'ã'
("a" con tilde). El gestor de errores por defecto -'strict'
- lanzaUnicodeEncodeError
.El gestor
error='ignore'
omite los caracteres que no se pueden codificar; esto suele ser una muy mala idea, ya que provoca la pérdida silenciosa de datos.Al codificar,
error='replace'
sustituye los caracteres no codificables por'?'
; también se pierden datos, pero los usuarios tendrán una pista de que algo va mal.'xmlcharrefreplace'
sustituye los caracteres no codificables por una entidad XML. Si no puedes utilizar UTF, y no puedes permitirte perder datos, ésta es la única opción.
Nota
El tratamiento de errores de codecs
es extensible. Puedes registrar cadenas adicionales para el argumento errors
pasando un nombre y una función de tratamiento de errores a la función codecs.register_error
. Consulta la documentación de codecs.register_error
.
ASCII es un subconjunto común a todas las codificaciones que conozco, por lo que la codificación siempre debería funcionar si el texto está formado exclusivamente por caracteres ASCII. Python 3.7 ha añadido un nuevo método booleano str.isascii()
para comprobar si tu texto Unicode es 100% ASCII puro. Si lo es, deberías poder codificarlo en bytes en cualquier codificación sin que aparezca UnicodeEncodeError
.
Cómo hacer frente a UnicodeDecodeError
No cada byte contiene un carácter ASCII válido, y no todas las secuencias de bytes son UTF-8 o UTF-16 válidas; por tanto, cuando asumas una de estas codificaciones al convertir una secuencia binaria a texto, obtendrás un UnicodeDecodeError
si se encuentran bytes inesperados.
Por otra parte, muchas codificaciones heredadas de 8 bits como 'cp1252'
, 'iso8859_1'
, y 'koi8_r'
son capaces de descodificar cualquier flujo de bytes, incluido el ruido aleatorio, sin informar de errores. Por tanto, si tu programa asume una codificación de 8 bits incorrecta, descodificará silenciosamente basura.
Consejo
Los caracteres desvirtuados se conocen como gremlins o mojibake (文字化け-"texto transformado" en japonés).
El ejemplo 4-6 ilustra cómo el uso de un códec incorrecto puede producir gremlins o unUnicodeDecodeError
.
Ejemplo 4-6. Descodificación de str
a bytes: éxito y tratamiento de errores
>>>
octets
=
b
'
Montr
\xe9
al
'
>>>
octets
.
decode
(
'
cp1252
'
)
'Montréal'
>>>
octets
.
decode
(
'
iso8859_7
'
)
'Montrιal'
>>>
octets
.
decode
(
'
koi8_r
'
)
'MontrИal'
>>>
octets
.
decode
(
'
utf_8
'
)
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
'
)
'Montr�al'
La palabra "Montreal" codificada como
latin1
;'\xe9'
es el byte para "é".La descodificación con Windows 1252 funciona porque es un superconjunto de
latin1
.La norma ISO-8859-7 está pensada para el griego, por lo que el byte
'\xe9'
se malinterpreta y no se emite ningún error.KOI8-R es para el ruso. Ahora
'\xe9'
significa la letra cirílica "И".El códec
'utf_8'
detecta queoctets
no es un UTF-8 válido, y levantaUnicodeDecodeError
.Utilizando el tratamiento de errores
'replace'
, el\xe9
se sustituye por "�" (punto de códigoU+FFFD), elREPLACEMENT CHARACTER
oficial de Unicode destinado a representar caracteres desconocidos.
SyntaxError al cargar módulos con una codificación inesperada
UTF-8 es la codificación fuente por defecto para Python 3, igual que ASCII era la predeterminada para Python 2. Si cargas un módulo .py que contenga datos que no sean UTF-8 y no haya declaración de codificación, recibirás un mensaje como éste:
SyntaxError: Non-UTF-8 code starting with '\xe1' in file ola.py on line 1, but no encoding declared; see https://python.org/dev/peps/pep-0263/ for details
Dado que UTF-8 está ampliamente implementado en los sistemas GNU/Linux y macOS, un escenario probable es abrir un archivo .py creado en Windows con cp1252
. Ten en cuenta que este error se produce incluso en Python para Windows, porque la codificación por defecto para el código fuente de Python 3 es UTF-8 en todas las plataformas.
Para solucionar este problema, añade un comentario mágico coding
al principio del archivo, como se muestra en el Ejemplo 4-7.
Ejemplo 4-7. ola.py: "¡Hola, mundo!" en portugués
# coding: cp1252
(
'Olá, Mundo!'
)
Consejo
Ahora que el código fuente de Python 3 ya no está limitado a ASCII y se utiliza por defecto la excelente codificación UTF-8, el mejor "arreglo" para el código fuente en codificaciones heredadas como 'cp1252'
es convertirlas ya a UTF-8, y no molestarse con los comentarios de coding
. Si tu editor no admite UTF-8, es hora de cambiar.
Supón que tienes un archivo de texto, ya sea código fuente o poesía, pero no conoces su codificación. ¿Cómo detectas la codificación real? Respuestas en la siguiente sección.
Cómo descubrir la codificación de una secuencia de bytes
¿Cómo encontrar la codificación de una secuencia de bytes? Respuesta corta: no puedes. Hay que decírtelo.
Algunos protocolos de comunicación y formatos de archivo, como HTTP y XML, contienen cabeceras que nos indican explícitamente cómo está codificado el contenido. Puedes estar seguro de que algunas secuencias de bytes no son ASCII porque contienen valores de bytes superiores a 127, y la forma en que están construidos UTF-8 y UTF-16 también limita las posibles secuencias de bytes.
Sin embargo, teniendo en cuenta que las lenguas humanas también tienen sus reglas y restricciones, una vez que asumes que una secuencia de bytes es texto plano humano, puede ser posible olfatear su codificación utilizando la heurística y la estadística.
Por ejemplo, si los bytes b'\x00'
son comunes, probablemente se trate de una codificación de 16 o 32 bits, y no de un esquema de 8 bits, porque los caracteres nulos en el texto plano son errores. Cuando la secuencia de bytes b'\x20\x00'
aparece a menudo, es más probable que sea el carácter espacio (U+0020) en una codificación UTF-16LE, en lugar del oscuro carácter U+2000 EN QUAD
-sea lo que sea-.
En ese es como funciona el paquete "Chardet-The Universal Character Encoding Detector"para adivinar una de las más de 30 codificaciones admitidas.Chardet es una biblioteca de Python que puedes utilizar en tus programas, pero también incluye una utilidad de línea de comandos, chardetect
. Esto es lo que informa en el archivo fuente de este capítulo:
$ chardetect04
-text-byte.asciidoc04
-text-byte.asciidoc: utf-8 with confidence0
.99
Aunque las secuencias binarias de texto codificado no suelen llevar indicaciones explícitas de su codificación, los formatos UTF pueden anteponer una marca de orden de bytes al contenido textual. Esto se explica a continuación.
Lista de materiales: Un Gremlin útil
En Ejemplo 4-4, puede que hayas observado un par de bytes de más al principio de una secuencia codificada en UTF-16. Aquí están de nuevo:
>>>
u16
=
'El Niño'
.
encode
(
'utf_16'
)
>>>
u16
b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'
Los bytes son b'\xff\xfe'
. Se trata de una marca de orden de bytes (BOM) que indica el orden de bytes "little-endian" de la CPU Intel en la que se realizó la codificación.
En una máquina little-endian, para cada punto de código el byte menos significativo va primero: la letra 'E'
, punto de código U+0045 (decimal 69), se codifica en los desplazamientos de byte 2 y 3 como 69
y 0
:
>>>
list
(
u16
)
[255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
En una CPU big-endian, la codificación se invertiría; 'E'
se codificaría como 0
y 69
.
Para evitar confusiones, la codificación UTF-16 antepone al texto que se va a codificar el carácter especial invisible ZERO WIDTH NO-BREAK SPACE
(U+FEFF). En un sistema little-endian, se codifica como b'\xff\xfe'
(255, 254 decimales). Dado que, por diseño, no existe el carácter U+FFFE en Unicode, la secuencia de bytes b'\xff\xfe'
debe significar ZERO WIDTH NO-BREAK SPACE
en una codificación little-endian, para que el códec sepa qué orden de bytes debe utilizar.
Existe una variante de UTF-16 -UTF-16LE- que es explícitamente little-endian, y otra explícitamente big-endian, UTF-16BE. Si las utilizas, no segenera una lista de materiales:
>>>
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]
Si está presente, se supone que el códec UTF-16 filtra la lista de materiales para que sólo obtengas el contenido real del texto del archivo sin el encabezamiento ZERO WIDTH NO-BREAK SPACE
. El estándar Unicode dice que si un archivo es UTF-16 y no tiene lista de materiales, debe asumirse que es UTF-16BE (big-endian). Sin embargo, la arquitectura Intel x86 es little-endian, por lo que hay muchos UTF-16 little-endian sin lista de materiales.
Todo este asunto de la endianidad sólo afecta a las codificaciones que utilizan palabras de más de un byte, como UTF-16 y UTF-32. Una gran ventaja de UTF-8 es que produce la misma secuencia de bytes independientemente de la endianidad de la máquina, por lo que no se necesita ninguna lista de materiales.
No obstante, algunas aplicaciones de Windows (sobre todo el Bloc de Notas) añaden la lista de materiales a los archivos UTF-8 de todos modos, y Excel depende de la lista de materiales para detectar un archivo UTF-8, pues de lo contrario asume que el contenido está codificado con una página de códigos de Windows.
Esta codificación UTF-8 con BOM se denomina UTF-8-SIG en el registro de códecs de Python. El carácter U+FEFF codificado en UTF-8-SIG es la secuencia de tres bytes b'\xef\xbb\xbf'
. Por tanto, si un archivo comienza con esos tres bytes, es probable que sea un archivo UTF-8 con BOM.
Consejo de Caleb sobre UTF-8-SIG
Caleb Hattingh -uno de los revisores técnicos- sugiere utilizar siempre el códec UTF-8-SIG al leer archivos UTF-8. Esto es inofensivo porque UTF-8-SIG lee archivos con o sin lista de materiales correctamente y no devuelve la lista de materiales en sí. Esto es inofensivo porque UTF-8-SIG lee correctamente archivos con o sin lista de materiales, y no devuelve la lista de materiales en sí. Al escribir, recomiendo utilizar UTF-8 por razones de interoperabilidad general. Por ejemplo, los scripts de Python pueden hacerse ejecutables en sistemas Unix si empiezan con el comentario: #!/usr/bin/env python3
.
Los dos primeros bytes del archivo deben ser b'#!'
para que eso funcione, pero la lista de materiales rompe esa convención. Si tienes un requisito específico para exportar datos a aplicaciones que necesitan la lista de materiales, utiliza UTF-8-SIG, pero ten en cuenta quela documentación de los códecs de Python dice: "En UTF-8, se desaconseja el uso de la lista de materiales y, en general, debe evitarse".
Manejo de archivos de texto
La mejor práctica para manejar la E/S de texto es el "sándwich Unicode"(Figura 4-2).5
Esto significa que bytes
debe descodificarse a str
lo antes posible en la entrada (por ejemplo, al abrir un archivo para su lectura). El "relleno" del sándwich es la lógica de negocio de tu programa, donde el manejo del texto se realiza exclusivamente en objetos str
. Nunca debes estar codificando o descodificando en medio de otro procesamiento. En la salida, los str
se codifican a bytes
lo más tarde posible. La mayoría de los marcos web funcionan así, y rara vez tocamos bytes
al utilizarlos. En Django, por ejemplo, tus vistas deben dar salida Unicode str
; el propio Django se encarga de codificar la respuesta a bytes
, utilizando UTF-8 por defecto.
Con Python 3 es más fácil seguir los consejos del bocadillo Unicode, porque el built-in open()
realiza la descodificación necesaria al leer y la codificación alescribir archivos en modo texto, de modo que todo lo que obtienes de my_file.read()
y pasas a my_file.write(text)
son objetos str
.
Por lo tanto, utilizar archivos de texto es aparentemente sencillo. Pero si confías en las codificaciones por defecto, acabarás picando.
Considera la sesión de consola del Ejemplo 4-8. ¿Puedes detectar el error?
Ejemplo 4-8. Un problema de codificación de la plataforma (si pruebas esto en tu máquina, puede que veas o no el problema)
>>>
open
(
'cafe.txt'
,
'w'
,
encoding
=
'utf_8'
)
.
write
(
'café'
)
4
>>>
open
(
'cafe.txt'
)
.
read
()
'café'
El fallo: Especifiqué la codificación UTF-8 al escribir el archivo, pero no lo hice al leerlo, por lo que Python asumió la codificación de archivo por defecto de Windows -código de página 1252- y los bytes finales del archivo se descodificaron como caracteres 'é'
en lugar de 'é'
.
He ejecutado el Ejemplo 4-8 en Python 3.8.1, 64 bits, en Windows 10 (build 18363). Las mismas sentencias ejecutadas en GNU/Linux o macOS recientes funcionan perfectamente porque su codificación por defecto es UTF-8, dando la falsa impresión de que todo va bien. Si se omitiera el argumento de codificación al abrir el archivo a escribir, se utilizaría la codificación por defecto de la configuración regional, y leeríamos el archivo correctamente utilizando la misma codificación. Pero entonces este script generaría archivos con contenidos de bytes diferentes según la plataforma o incluso según la configuración regional en la misma plataforma, creando problemas de compatibilidad.
Consejo
El código que tenga que ejecutarse en varias máquinas o en varias ocasiones nunca debe depender de la codificación por defecto. Pasa siempre un argumento encoding=
explícito al abrir archivos de texto, porque el valor por defecto puede cambiar de una máquina a otra, o de un día para otro.
Un detalle curioso del Ejemplo 4-8 es que la función write
de la primera sentencia informa de que se han escrito cuatro caracteres, pero en la línea siguiente se leen cinco.El Ejemplo 4-9 es una versión ampliada del Ejemplo 4-8, que explica ese y otros detalles.
Ejemplo 4-9. Una inspección más detallada del Ejemplo 4-8 ejecutado en Windows revela el error y cómo solucionarlo
>>>
fp
=
open
(
'
cafe.txt
'
,
'
w
'
,
encoding
=
'
utf_8
'
)
>>>
fp
<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
>>>
fp
.
write
(
'
café
'
)
4
>>>
fp
.
close
(
)
>>>
import
os
>>>
os
.
stat
(
'
cafe.txt
'
)
.
st_size
5
>>>
fp2
=
open
(
'
cafe.txt
'
)
>>>
fp2
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp1252'>
>>>
fp2
.
encoding
'cp1252'
>>>
fp2
.
read
(
)
'café'
>>>
fp3
=
open
(
'
cafe.txt
'
,
encoding
=
'
utf_8
'
)
>>>
fp3
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'>
>>>
fp3
.
read
(
)
'café'
>>>
fp4
=
open
(
'
cafe.txt
'
,
'
rb
'
)
>>>
fp4
<_io.BufferedReader name='cafe.txt'>
>>>
fp4
.
read
(
)
b'caf\xc3\xa9'
Por defecto,
open
utiliza el modo texto y devuelve un objetoTextIOWrapper
con una codificación específica.El método
write
deTextIOWrapper
devuelve el número de caracteres Unicode escritos.os.stat
dice que el archivo tiene 5 bytes; UTF-8 codifica'é'
como 2 bytes, 0xc3 y 0xa9.Abrir un archivo de texto sin codificación explícita devuelve un
TextIOWrapper
con la codificación establecida por defecto en la configuración regional.Un objeto
TextIOWrapper
tiene un atributo de codificación que puedes inspeccionar:cp1252
en este caso.En la codificación
cp1252
de Windows, el byte 0xc3 es una "Ã" (A con tilde), y 0xa9 es el signo de copyright.Abrir el mismo archivo con la codificación correcta.
El resultado esperado: los mismos cuatro caracteres Unicode para
'café'
.La bandera
'rb'
abre un archivo para su lectura en modo binario.El objeto devuelto es un
BufferedReader
y no unTextIOWrapper
.La lectura devuelve bytes, como era de esperar.
Consejo
No abras archivos de texto en modo binario a menos que necesites analizar el contenido del archivo para determinar la codificación; incluso en ese caso, deberías utilizar Chardet en lugar de reinventar la rueda (consulta "Cómo descubrir la codificación de una secuencia de bytes"). El código ordinario sólo debería utilizar el modo binario para abrir archivos binarios, como imágenes rasterizadas.
El problema del Ejemplo 4-9 tiene que ver con confiar en una configuración por defecto al abrir un archivo de texto. Hay varias fuentes para tales valores por defecto, como muestra la siguiente sección.
Cuidado con la codificación por defecto
Varios ajustes de afectan a los valores predeterminados de codificación para la E/S en Python. Consulta el script default_encodings.py del Ejemplo 4-10.
Ejemplo 4-10. Explorar la codificación por defecto
import
locale
import
sys
expressions
=
"""
locale.getpreferredencoding()
type(my_file)
my_file.encoding
sys.stdout.isatty()
sys.stdout.encoding
sys.stdin.isatty()
sys.stdin.encoding
sys.stderr.isatty()
sys.stderr.encoding
sys.getdefaultencoding()
sys.getfilesystemencoding()
"""
my_file
=
open
(
'dummy'
,
'w'
)
for
expression
in
expressions
.
split
():
value
=
eval
(
expression
)
(
f
'
{expression:>30}
->
{value!r}
'
)
El resultado del Ejemplo 4-10 en GNU/Linux (Ubuntu 14.04 a 19.10) y macOS (10.9 a 10.14) es idéntico, lo que demuestra que UTF-8
se utiliza en todos estos 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'
En Windows, sin embargo, la salida es Ejemplo 4-11.
Ejemplo 4-11. Codificaciones por defecto en Windows 10 PowerShell (la salida es la misma en cmd.exe)
>
chcp
Active
code
page:
437
>
python
default_encodings.py
locale.getpreferredencoding
(
)
->
'cp1252'
type
(
my_file
)
->
<
class
'_io.TextIOWrapper'
>
my_file.encoding
->
'cp1252'
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'
chcp
muestra la página de código activa para la consola:437
.Ejecutando codificaciones_por_defecto.py con salida a consola.
locale.getpreferredencoding()
es el ajuste más importante.Los archivos de texto utilizan
locale.getpreferredencoding()
por defecto.La salida va a la consola, así que
sys.stdout.isatty()
esTrue
.¡Ahora bien,
sys.stdout.encoding
no es lo mismo que la página de código de la consola de la que informachcp
!
El soporte Unicode en el propio Windows, y en Python para Windows, mejoró desde que escribí la primera edición de este libro.El Ejemplo 4-11 solía informar de cuatro codificaciones diferentes en Python 3.4 en Windows 7.
Las codificaciones para stdout
, stdin
, y stderr
solían ser las mismas que la página de código activa informada por el comando chcp
, pero ahora todas son utf-8
gracias ala PEP 528-Cambiar la codificación de la consola de Windows a UTF-8implementada en Python 3.6, y al soporte Unicode en PowerShell en cmd.exe (desde Windows 1809 a partir de octubre de 2018).6
Es raro que chcp
y sys.stdout.encoding
digan cosas diferentes cuando stdout
está escribiendo en la consola, pero es genial que ahora podamos imprimir cadenas Unicode sin errores de codificación en Windows -a menos que el usuario redirija la salida a un archivo, como veremos pronto-. Eso no significa que todos tus emojis favoritos de vayan a aparecer en la consola: eso también depende de la fuente que esté utilizando la consola.
Otro cambio fue el PEP 529-Cambiar la codificación del sistema de archivos de Windows a UTF-8, también implementado en Python 3.6, que cambió la codificación del sistema de archivos (utilizada para representar nombres de directorios y archivos) de MBCS, propiedad de Microsoft, a UTF-8.
Sin embargo, si la salida del Ejemplo 4-10 se redirige a un archivo, así:
Z:\>
python default_encodings.py > encodings.log
entonces, el valor de sys.stdout.isatty()
se convierte en False
, y sys.stdout.encoding
es fijado por locale.getpreferredencoding()
,'cp1252'
en esa máquina -pero sys.stdin.encoding
y sys.stderr.encoding
siguen siendo utf-8
.
Consejo
En Ejemplo 4-12 utilizo el escape '\N{}'
para literales Unicode, donde escribimos el nombre oficial del carácter dentro de \N{}
. Es bastante verboso, pero explícito y seguro: Python invoca SyntaxError
si el nombre no existe; mucho mejor que escribir un número hexadecimal que podría ser incorrecto, pero que sólo descubrirás mucho más tarde. Probablemente querrías escribir un comentario explicando los códigos de los caracteres de todos modos, así que la verbosidad de \N{}
es fácil de aceptar.
Esto significa que un script como el del Ejemplo 4-12 funciona cuando se imprime en la consola, pero puede romperse cuando la salida se redirige a un archivo.
Ejemplo 4-12. stdout_check.py
import
sys
from
unicodedata
import
name
(
sys
.
version
)
()
(
'sys.stdout.isatty():'
,
sys
.
stdout
.
isatty
())
(
'sys.stdout.encoding:'
,
sys
.
stdout
.
encoding
)
()
test_chars
=
[
'
\N{HORIZONTAL ELLIPSIS}
'
,
# exists in cp1252, not in cp437
'
\N{INFINITY}
'
,
# exists in cp437, not in cp1252
'
\N{CIRCLED NUMBER FORTY TWO}
'
,
# not in cp437 or in cp1252
]
for
char
in
test_chars
:
(
f
'Trying to output {name(char)}:'
)
(
char
)
El ejemplo 4-12 muestra el resultado de sys.stdout.isatty()
, el valor de sys.stdout.encoding
, y estos tres caracteres:
-
'…'
HORIZONTAL ELLIPSIS
-existe en CP 1252 pero no en CP 437. -
'∞'
INFINITY
-existe en CP 437 pero no en CP 1252. -
'㊷'
CIRCLED NUMBER FORTY TWO
-no existe en CP 1252 ni en CP 437.
Cuando ejecuto stdout_check.py en PowerShell o cmd.exe, funciona como se muestra en la Figura 4-3.
A pesar de que chcp
informa del código activo como 437, sys.stdout.encoding
es UTF-8, por lo que HORIZONTAL ELLIPSIS
y INFINITY
salen correctamente. CIRCLED NUMBER FORTY TWO
se sustituye por un rectángulo, pero no se produce ningún error. Presumiblemente se reconoce como un carácter válido, pero la fuente de la consola no tiene el glifo para mostrarlo.
Sin embargo, cuando redirijo la salida de stdout_check.py a un archivo, obtengo la Figura 4-4.
El primer problema que muestra la Figura 4-4 es la mención UnicodeEncodeError
del carácter '\u221e'
, porque sys.stdout.encoding
es 'cp1252'
-una página de códigos que no tiene el carácter INFINITY
.
La lectura de out.txt con el comando type
-o con un editor de Windows como VS Code o Sublime Text- muestra que en lugar de ELIPSIS HORIZONTAL, obtuve 'à'
(LATIN SMALL LETTER A WITH GRAVE
). Resulta que el valor de byte 0x85 en CP 1252 significa '…'
, pero en CP 437 el mismo valor de byte representa 'à'
. Así que parece que la página de código activa sí importa, no de forma sensata o útil, sino como explicación parcial de una mala experiencia Unicode.
Nota
He utilizado un portátil configurado para el mercado estadounidense, con Windows 10 OEM para realizar estos experimentos. Las versiones de Windows localizadas para otros países pueden tener configuraciones de codificación diferentes. Por ejemplo, en Brasil la consola de Windows utiliza por defecto la página de códigos 850, no la 437.
Para terminar con este enloquecedor tema de las codificaciones por defecto, echemos un último vistazo a las distintas codificaciones del Ejemplo 4-11:
-
Si omites el argumento
encoding
al abrir un archivo, el valor por defecto viene dado porlocale.getpreferredencoding()
('cp1252'
en el Ejemplo 4-11). -
La codificación de
sys.stdout|stdin|stderr
solía establecerse mediante la variable de entornoPYTHONIOENCODING
antes de Python 3.6; ahora esa variable se ignora, a menos quePYTHONLEGACYWINDOWSSTDIO
De lo contrario, la codificación para la E/S estándar es UTF-8 para la E/S interactiva, o definida porlocale.getpreferredencoding()
si la salida/entrada se redirige a/desde un archivo. -
sys.getdefaultencoding()
es utilizado internamente por Python en las conversiones implícitas de datos binarios a/desdestr
. No es posible cambiar esta configuración. -
sys.getfilesystemencoding()
se utiliza para codificar/decodificar nombres de archivo (no su contenido). Se utiliza cuandoopen()
recibe un argumentostr
para el nombre de archivo; si el nombre de archivo se da como argumentobytes
, se pasa sin cambios a la API del SO.
Nota
En GNU/Linux y macOS, todas estas codificaciones están configuradas en UTF-8 por defecto, y así ha sido durante varios años, por lo que la E/S maneja todos los caracteres Unicode. En Windows, no sólo se utilizan diferentes codificaciones en el mismo sistema, sino que suelen ser páginas de código como 'cp850'
o 'cp1252'
que sólo admiten ASCII, con 127 caracteres adicionales que no son iguales de una codificación a otra. Por tanto, los usuarios de Windows tienen muchas más probabilidades de enfrentarse a errores de codificación, a menos que sean muy cuidadosos.
En resumen, la configuración de codificación más importante es la devuelta por locale.getpreferredencoding()
: es la predeterminada para abrir archivos de texto y para sys.stdout/stdin/stderr
cuando se redirigen a archivos. Sin embargo, la documentación dice (en parte):
locale.getpreferredencoding(do_setlocale=True)
Devuelve la codificación utilizada para los datos de texto, según las preferencias del usuario. Las preferencias del usuario se expresan de forma diferente en los distintos sistemas, y puede que no estén disponibles mediante programación en algunos sistemas, por lo que esta función sólo devuelve una suposición. [...]
Por tanto, el mejor consejo sobre la codificación por defecto es: no confíes en ella.
Te evitarás muchos dolores si sigues los consejos del sándwich Unicode y eres siempre explícito sobre las codificaciones en tus programas. Desgraciadamente, Unicode es doloroso incluso si consigues convertir correctamente tu bytes
a str
. Las dos secciones siguientes tratan temas que son sencillos en la tierra del ASCII, pero que se vuelven bastante complejos en el planeta Unicode: la normalización del texto (es decir, convertir el texto a una representación uniforme para lascomparaciones) y la ordenación.
Normalizar Unicode para comparaciones fiables
Comparaciones de cadenas se complican por el hecho de que Unicode tiene caracteres de combinación: diacríticos y otras marcas que se unen al carácter precedente, apareciendo como uno solo cuando se imprimen.
Por ejemplo, la palabra "café" puede componerse de dos formas, utilizando cuatro o cinco puntos de código, pero el resultado es exactamente el mismo:
>>>
s1
=
'café'
>>>
s2
=
'cafe
\N{COMBINING ACUTE ACCENT}
'
>>>
s1
,
s2
('café', 'café')
>>>
len
(
s1
),
len
(
s2
)
(4, 5)
>>>
s1
==
s2
False
Si se coloca COMBINING ACUTE ACCENT
(U+0301) después de "e", se obtiene "é". En el estándar Unicode, las secuencias como 'é'
y 'e\u0301'
se denominan "equivalentes canónicos", y se supone que las aplicaciones deben tratarlas como iguales. Pero Python ve dos secuencias diferentes de puntos de código, y las considera no iguales.
La solución es unicodedata.normalize()
. El primer argumento de esa función es una de cuatro cadenas: 'NFC'
, 'NFD'
, 'NFKC'
, y 'NFKD'
. Empecemos por las dos primeras.
Normalización Forma C (NFC) compone los puntos de código para producir la cadena equivalente más corta, mientras que NFD descompone, expandiendo los caracteres compuestos en caracteres base y caracteres de combinación separados. Ambas normalizaciones hacen que las comparaciones funcionen como se espera, como muestra el siguiente ejemplo:
>>>
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
Los controladores de teclado suelen generar caracteres compuestos, por lo que el texto que escriban los usuarios estará en NFC por defecto. Sin embargo, para estar seguros, puede ser bueno normalizar las cadenas con normalize('NFC', user_text)
antes de guardarlas. NFC es también la forma de normalización recomendada por el W3C en"Character Model for the World Wide Web: String Matching and Searching".
Algunos caracteres simples son normalizados por NFC en otro carácter simple. El símbolo del ohmio (Ω), unidad de resistencia eléctrica, se normaliza a la omega mayúscula griega. Son visualmente idénticos, pero se comparan como desiguales, por lo que es esencial normalizar para evitar sorpresas:
>>>
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
Las otras dos formas de normalización son NFKC y NFKD, en las que la letra K significa "compatibilidad", y son formas de normalización más fuertes, que afectan a los llamados "caracteres de compatibilidad" Aunque uno de los objetivos de Unicode es tener un único punto de código "canónico" para cada carácter, algunos caracteres aparecen más de una vez porcompatibilidad con normas preexistentes.
Por ejemplo, el MICRO SIGN
, µ
(U+00B5
), se añadió a Unicode para soportar la conversión de ida y vuelta a latin1
, que lo incluye, aunque el mismo carácter forme parte del alfabeto griego con el punto de código U+03BC
(GREEK SMALL LETTER MU
). Así pues, el micro signo se considera un "carácter de compatibilidad".
En los formularios NFKC y NFKD, cada carácter compatible se sustituye por una "descomposición de compatibilidad" de uno o más caracteres que se consideran una representación "preferida", aunque haya alguna pérdida de formato; lo ideal es que el formato sea responsabilidad del marcado externo, no parte de Unicode. Por ejemplo, la descomposición decompatibilidad de la media fracción '½'
(U+00BD
) es la secuencia de tres caracteres '1/2'
, y la descomposición de compatibilidad del micro signo 'µ'
(U+00B5
) es la mu minúscula 'μ'
(U+03BC
).7
He aquí cómo funciona en la práctica el NFKC:
>>>
from
unicodedata
import
normalize
,
name
>>>
half
=
'
\N{VULGAR FRACTION ONE HALF}
'
>>>
(
half
)
½
>>>
normalize
(
'NFKC'
,
half
)
'1⁄2'
>>>
for
char
in
normalize
(
'NFKC'
,
half
):
...
(
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')
Aunque '1⁄2'
es un sustituto razonable de '½'
, y el signo micro es en realidad una mu griega minúscula, convertir '4²'
en '42'
cambia el significado. Una aplicación podría almacenar '4²'
como '4<sup>2</sup>'
, pero la función normalize
no sabe nada del formato. Por tanto, NFKC o NFKD pueden perder o distorsionar información,pero pueden producir representaciones intermedias convenientes para la búsqueda y laindexación.
Por desgracia, con Unicode todo es siempre más complicado de lo que parece a primera vista. Para el VULGAR FRACTION ONE HALF
, la normalización NFKC produjo 1 y 2 unidos por FRACTION SLASH
, en lugar de SOLIDUS
, también conocido como "barra", el conocido carácter con código ASCII decimal 47. Por lo tanto, al buscar la secuencia ASCII de tres caracteres '1/2'
no se encontraría la secuencia Unicode normalizada.
Advertencia
La normalización NFKC y NFKD causa pérdida de datos y sólo debe aplicarse en casos especiales como la búsqueda y la indexación, y no para el almacenamiento permanente de texto.
Al preparar un texto para su búsqueda o indexación, resulta útil otra operación: el plegado de casos, nuestro próximo tema.
Maletín plegable
Doblar mayúsculas y minúsculas consiste esencialmente en convertir todo el texto a minúsculas, con algunas transformaciones adicionales. Es compatible con el método str.casefold()
.
Para cualquier cadena s
que contenga sólo caracteres latin1
, s.casefold()
produce el mismo resultado que s.lower()
, con sólo dos excepciones: el micro signo 'µ'
se cambia por la mu minúscula griega (que tiene el mismo aspecto en la mayoría de los tipos de letra) y la Eszett o "s aguda" alemana (ß) se convierte en "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')
Hay casi 300 puntos de código para los que str.casefold()
y str.lower()
devuelven resultados diferentes.
Como suele ocurrir con todo lo relacionado con Unicode, el plegado de mayúsculas y minúsculas es un tema difícil con multitud de casos lingüísticos especiales, pero el equipo del núcleo de Python ha hecho un esfuerzo por ofrecer una solución que esperamos que funcione para la mayoría de los usuarios.
En las próximas secciones, pondremos en práctica nuestros conocimientos sobre normalización desarrollando funciones de utilidad.
Funciones de utilidad para la comparación de textos normalizados
Como hemos visto en, NFC y NFD son seguros de usar y permiten comparaciones sensatas entre cadenas Unicode. NFC es la mejor forma normalizada para la mayoría de las aplicaciones. str.casefold()
es el camino a seguir para las comparaciones que no distinguen mayúsculas de minúsculas.
Si trabajas con texto en varios idiomas, un par de funciones como nfc_equal
y fold_equal
del Ejemplo 4-13 son adiciones útiles a tu caja de herramientas.
Ejemplo 4-13. normeq.py: comparación normalizada de cadenas Unicode
"""
Utility functions for normalized Unicode string comparison.
Using Normal Form C, case sensitive:
>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1 == s2
False
>>> nfc_equal(s1, s2)
True
>>> nfc_equal('A', 'a')
False
Using Normal Form C with case folding:
>>> s3 = 'Straße'
>>> s4 = 'strasse'
>>> s3 == s4
False
>>> nfc_equal(s3, s4)
False
>>> fold_equal(s3, s4)
True
>>> fold_equal(s1, s2)
True
>>> fold_equal('A', 'a')
True
"""
from
unicodedata
import
normalize
def
nfc_equal
(
str1
,
str2
):
return
normalize
(
'NFC'
,
str1
)
==
normalize
(
'NFC'
,
str2
)
def
fold_equal
(
str1
,
str2
):
return
(
normalize
(
'NFC'
,
str1
)
.
casefold
()
==
normalize
(
'NFC'
,
str2
)
.
casefold
())
Más allá de la normalización Unicode y el plegado de mayúsculas y minúsculas -que forman parte del estándar Unicode-, a veces tiene sentido aplicar transformaciones más profundas, como cambiar 'café'
por 'cafe'
. Veremos cuándo y cómo en la siguiente sección.
"Normalización" extrema: Eliminar los diacríticos
La salsa secreta de la Búsqueda de Google implica muchos trucos, pero uno de ellos aparentemente es ignorar los diacríticos (por ejemplo, acentos, cedillas, etc.), al menos en algunos contextos. Eliminar los diacríticos no es una forma adecuada de normalización porque a menudo cambia el significado de las palabras y puede producir falsos positivos al buscar. Pero ayuda a hacer frente a algunos hechos de la vida: la gente a veces es perezosa o ignorante sobre el uso correcto de los diacríticos, y las normas ortográficas cambian con el tiempo, lo que significa que los acentos van y vienen en las lenguas vivas.
Aparte de las búsquedas, eliminar los diacríticos también hace que las URL sean más legibles, al menos en las lenguas de base latina. Echa un vistazo a la URL del artículo de Wikipedia sobre la ciudad de São Paulo:
https://en.wikipedia.org/wiki/S%C3%A3o_Paulo
La parte %C3%A3
es la representación UTF-8 de la letra única "ã" ("a" con tilde). Lo siguiente es mucho más fácil de reconocer, aunque no sea lagrafía correcta:
https://en.wikipedia.org/wiki/Sao_Paulo
Para eliminar todos los diacríticos de un str
, puedes utilizar una función como la del Ejemplo 4-14.
Ejemplo 4-14. simplificar.py: función para eliminar todos los signos de combinación
import
unicodedata
import
string
def
shave_marks
(
txt
)
:
"""Remove all diacritic marks"""
norm_txt
=
unicodedata
.
normalize
(
'
NFD
'
,
txt
)
shaved
=
'
'
.
join
(
c
for
c
in
norm_txt
if
not
unicodedata
.
combining
(
c
)
)
return
unicodedata
.
normalize
(
'
NFC
'
,
shaved
)
Descompone todos los caracteres en caracteres base y signos de combinación.
Filtra todas las marcas de combinación.
Recomponer todos los caracteres.
El Ejemplo 4-15 muestra un par de usos de shave_marks
.
Ejemplo 4-15. Dos ejemplos utilizando shave_marks
del Ejemplo 4-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.”'
>>>
Greek
=
'
Ζέφυρος, Zéfiro
'
>>>
shave_marks
(
Greek
)
'Ζεφυρος, Zefiro'
La función shave_marks
del Ejemplo 4-14 funciona bien, pero quizá va demasiado lejos. A menudo, la razón para eliminar los diacríticos es cambiar el texto latino a ASCII puro, pero shave_marks
también cambia los caracteres no latinos -como las letras griegas- que nunca se convertirán en ASCII sólo por perder sus acentos. Así que tiene sentido analizar cada carácter base y eliminar los acentos sólo si el carácter base es una letra del alfabeto latino. Esto es lo que hace el Ejemplo 4-16.
Ejemplo 4-16. Función para eliminar los signos de combinación de los caracteres latinos (se omiten las declaraciones import, ya que forma parte del módulo simplify.py del Ejemplo 4-14)
def
shave_marks_latin
(
txt
)
:
"""Remove all diacritic marks from Latin base characters"""
norm_txt
=
unicodedata
.
normalize
(
'
NFD
'
,
txt
)
latin_base
=
False
preserve
=
[
]
for
c
in
norm_txt
:
if
unicodedata
.
combining
(
c
)
and
latin_base
:
continue
# ignore diacritic on Latin base char
preserve
.
append
(
c
)
# if it isn't a combining char, it's a new base char
if
not
unicodedata
.
combining
(
c
)
:
latin_base
=
c
in
string
.
ascii_letters
shaved
=
'
'
.
join
(
preserve
)
return
unicodedata
.
normalize
(
'
NFC
'
,
shaved
)
Descompone todos los caracteres en caracteres base y signos de combinación.
Omite los signos de combinación cuando el carácter base es latino.
Si no, mantén el carácter actual.
Detecta un nuevo carácter base y determina si es latino.
Recomponer todos los caracteres.
Un paso aún más radical sería sustituir los símbolos habituales en los textos occidentales (por ejemplo, las comillas, los guiones, las viñetas, etc.) por equivalentes en ASCII
. Esto es lo que hace la función asciize
en el Ejemplo 4-17.
Ejemplo 4-17. Transforma algunos símbolos tipográficos occidentales en ASCII (este fragmento también forma parte de simplificar.py del Ejemplo 4-14)
single_map
=
str
.
maketrans
(
"""
‚ƒ„ˆ‹‘’“”•–—˜›
"""
,
"""'f"^<''""---~>"""
)
multi_map
=
str
.
maketrans
(
{
'
€
'
:
'
EUR
'
,
'
…
'
:
'
...
'
,
'
Æ
'
:
'
AE
'
,
'
æ
'
:
'
ae
'
,
'
Œ
'
:
'
OE
'
,
'
œ
'
:
'
oe
'
,
'
™
'
:
'
(TM)
'
,
'
‰
'
:
'
<per mille>
'
,
'
†
'
:
'
**
'
,
'
‡
'
:
'
***
'
,
}
)
multi_map
.
update
(
single_map
)
def
dewinize
(
txt
)
:
"""Replace Win1252 symbols with ASCII chars or sequences"""
return
txt
.
translate
(
multi_map
)
def
asciize
(
txt
)
:
no_marks
=
shave_marks_latin
(
dewinize
(
txt
)
)
no_marks
=
no_marks
.
replace
(
'
ß
'
,
'
ss
'
)
return
unicodedata
.
normalize
(
'
NFKC
'
,
no_marks
)
Construye una tabla de asignación para sustituir los caracteres.
Construye una tabla de asignación para sustituir caracteres por cadenas.
Fusionar tablas de asignación.
dewinize
no afecta al texto deASCII
olatin1
, sólo a los añadidos de Microsoft alatin1
encp1252
.Aplica
dewinize
y elimina los signos diacríticos.Sustituye el Eszett por "ss" (aquí no utilizamos el doblez de mayúsculas porque queremos conservar las mayúsculas).
Aplica la normalización NFKC para componer caracteres con sus puntos de código de compatibilidad.
El Ejemplo 4-18 muestra asciize
en uso.
Ejemplo 4-18. Dos ejemplos utilizando asciize
del Ejemplo 4-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í."'
>>>
asciize
(
order
)
'"Herr Voss: - 1⁄2 cup of OEtker(TM) caffe latte - bowl of acai."'
dewinize
sustituye las comillas rizadas, las viñetas y el ™ (símbolo de marca registrada).asciize
aplicadewinize
, elimina los diacríticos y sustituye el'ß'
.
Advertencia
Las distintas lenguas tienen sus propias reglas para eliminar los diacríticos. Por ejemplo, los alemanes cambian 'ü'
por 'ue'
. Nuestra función asciize
no es tan refinada, por lo que puede o no ser adecuada para tu idioma. Sin embargo, funciona aceptablemente para el portugués.
En resumen, las funciones de simplificar. py van mucho más allá de la normalización estándar y realizan una cirugía profunda en el texto, con muchas posibilidades de cambiar su significado. Sólo tú puedes decidir si ir tan lejos, conociendo la lengua de destino, tus usuarios y cómo se utilizará el texto transformado.
Con esto terminamos nuestra discusión sobre la normalización del texto Unicode.
Ordenar texto Unicode
Python ordena secuencias de cualquier tipo comparando uno a uno los elementos de cada secuencia. Para las cadenas, esto significa comparar los puntos de código. Por desgracia, esto produce resultados inaceptables para cualquiera que utilice caracteres no ASCII.
Considera la posibilidad de ordenar una lista de frutas cultivadas en Brasil:
>>>
fruits
=
[
'caju'
,
'atemoia'
,
'cajá'
,
'açaí'
,
'acerola'
]
>>>
sorted
(
fruits
)
['acerola', 'atemoia', 'açaí', 'caju', 'cajá']
Las reglas de ordenación varían según el país, pero en portugués y en muchas lenguas que utilizan el alfabeto latino, los acentos y las cedillas rara vez marcan la diferencia a la hora de ordenar.8 Así, "cajá" se ordena como "caja", y debe ir antes que "caju".
La lista fruits
ordenada debe ser:
[
'açaí'
,
'acerola'
,
'atemoia'
,
'cajá'
,
'caju'
]
La forma estándar de ordenar texto no ASCII en Python es utilizar la función locale.strxfrm
que, según la documentación del módulolocale
, "transforma una cadena en una que pueda utilizarse en comparaciones con reconocimiento de localización".
Para activar locale.strxfrm
, primero debes establecer una configuración regional adecuada para tu aplicación, y rezar para que el SO la admita. La secuencia de comandos del Ejemplo 4-19 puede funcionarte.
Ejemplo 4-19. locale_sort.py: utilizando la función locale.strxfrm
como clave de ordenación
import
locale
my_locale
=
locale
.
setlocale
(
locale
.
LC_COLLATE
,
'pt_BR.UTF-8'
)
(
my_locale
)
fruits
=
[
'caju'
,
'atemoia'
,
'cajá'
,
'açaí'
,
'acerola'
]
sorted_fruits
=
sorted
(
fruits
,
key
=
locale
.
strxfrm
)
(
sorted_fruits
)
Si ejecuto el Ejemplo 4-19 en GNU/Linux (Ubuntu 19.10) con la configuración regional pt_BR.UTF-8
instalada, obtengo el resultado correcto:
'pt_BR.UTF-8'
[
'açaí'
,
'acerola'
,
'atemoia'
,
'cajá'
,
'caju'
]
Así que tienes que llamar a setlocale(LC_COLLATE, «your_locale»)
antes de utilizar locale.strxfrm
como clave al ordenar.
Sin embargo, hay algunas advertencias:
-
Dado que la configuración regional es global, no se recomienda llamar a
setlocale
en una biblioteca. Tu aplicación o marco de trabajo debe establecer la configuración regional cuando se inicie el proceso, y no debe cambiarla después. -
La configuración regional debe estar instalada en el sistema operativo, de lo contrario
setlocale
lanza una excepciónlocale.Error: unsupported locale setting
. -
Debes saber cómo se escribe el nombre de la configuración regional.
-
La configuración regional debe estar correctamente implementada por los fabricantes del sistema operativo. Tuve éxito en Ubuntu 19.10, pero no en macOS 10.14. En macOS, la llamada
setlocale(LC_COLLATE, 'pt_BR.UTF-8')
devuelve la cadena'pt_BR.UTF-8'
sin ninguna queja. Perosorted(fruits, key=locale.strxfrm)
produjo el mismo resultado incorrecto quesorted(fruits)
. También probé las localizacionesfr_FR
,es_ES
yde_DE
en macOS, perolocale.strxfrm
nunca hizo su trabajo.9
Así que la solución de la biblioteca estándar para la ordenación internacionalizada funciona, pero parece que sólo está bien soportada en GNU/Linux (quizá también en Windows, si eres un experto). Incluso entonces, depende de la configuración regional, lo que crea quebraderos de cabeza en la implementación.
Afortunadamente, existe una solución más sencilla: la biblioteca pyuca, disponible en PyPI.
Ordenar con el Algoritmo de Colación Unicode
James Tauber, prolífico colaborador de Django, debió de sentir el dolor y creó pyuca, una implementación en Python puro del Algoritmo de Cotejo Unicode (UCA). El Ejemplo 4-20 muestra lo fácil que es utilizarlo.
Ejemplo 4-20. Utilizar el método 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']
Esto es sencillo y funciona en GNU/Linux, macOS y Windows, al menos con mi pequeña muestra.
pyuca
no tiene en cuenta la configuración regional. Si necesitas personalizar la ordenación, puedes proporcionar la ruta a una tabla de cotejo personalizada al constructor Collator()
. Por defecto, utiliza allkeys.txt, que se incluye con el proyecto. No es más que una copia de la Tabla de elementos de cotejo por defecto de Unicode.org.
PyICU: Recomendación de Miro para ordenar Unicode
(El crítico técnico Miroslav Šedivý es políglota y experto en Unicode. Esto es lo que escribió sobre pyuca).
pyuca tiene un algoritmo de ordenación que no respeta el orden de ordenación de cada idioma. Por ejemplo, Ä en alemán está entre A y B, mientras que en sueco va después de Z. Echa un vistazo aPyICU, que funciona como locale sin cambiar la configuración regional del proceso. También es necesario si quieres cambiar el caso de iİ/ıI en turco. PyICU incluye una extensión que hay que compilar, por lo que puede ser más difícil de instalar en algunos sistemas que pyuca, que es sólo Python.
Por cierto, esa tabla de cotejo es uno de los muchos archivos de datos que componen la base de datos Unicode, nuestro próximo tema.
La base de datos Unicode
La norma Unicode proporciona una base de datos completa -en forma de varios archivos de texto estructurado- que incluye no sólo la tabla que asigna los puntos de código a los nombres de los caracteres, sino también metadatos sobre los caracteres individuales y cómo están relacionados. Por ejemplo, la base de datos Unicode registra si un carácter es imprimible, es una letra, es un dígito decimal o es algún otro símbolo numérico. Así es como funcionan los métodos str
isalpha
, isprintable
, isdecimal
y isnumeric
.str.casefold
también utiliza información de una tabla Unicode.
Nota
La función unicodedata.category(char)
devuelve la categoría de dos letras de char
de la base de datos Unicode. Los métodos de nivel superior str
son más fáciles de usar. Por ejemplo,label.isalpha()
devuelve True
si cada carácter de label
pertenece a una de estas categorías: Lm
, Lt
, Lu
, Ll
, o Lo
. Para saber qué significan esos códigos, consulta"Categoría general"enel artículo "Propiedades de los caracteres Unicode" de la Wikipedia en inglés.
Encontrar personajes por su nombre
El módulo unicodedata
tiene funciones para recuperar metadatos de caracteres, entre ellas unicodedata.name()
, que devuelve el nombre oficial de un carácter en la norma.La Figura 4-5 muestra esa función.10
Puedes utilizar la función name()
para crear aplicaciones que permitan a los usuarios buscar caracteres por su nombre.La Figura 4-6 muestra el script de línea de comandos cf .py, que toma una o varias palabras como argumentos y enumera los caracteres que tienen esas palabras en sus nombres Unicode oficiales. El código fuente completo de cf.py está en el Ejemplo 4-21.
Advertencia
La compatibilidad con emoji varía mucho según los sistemas operativos y las aplicaciones. En los últimos años, el terminal de macOS ofrece la mejor compatibilidad con emojis, seguido de los terminales gráficos modernos de GNU/Linux. cmd.exe y PowerShell de Windows ahora admiten la salida Unicode, pero mientras escribo esta sección en enero de 2020, siguen sin mostrar emojis, al menos no "fuera de la caja" El crítico técnico Leonardo Rochael me habló de un nuevo Terminal de Windows de código abierto de Microsoft, que podría tener mejor compatibilidad con Unicode que las antiguas consolas de Microsoft. No tuve tiempo de probarlo.
En el Ejemplo 4-21, fíjate en la sentencia if
de la función find
que utiliza el método .issubset()
para comprobar rápidamente si todas las palabras del conjunto query
aparecen en la lista de palabras construida a partir del nombre del personaje. Gracias a la rica API de conjuntos de Python, no necesitamos un bucle for
anidado y otro if
para realizar esta comprobación.
Ejemplo 4-21. cf.py: la utilidad para encontrar caracteres
#!/usr/bin/env python3
import
sys
import
unicodedata
START
,
END
=
ord
(
'
'
)
,
sys
.
maxunicode
+
1
def
find
(
*
query_words
,
start
=
START
,
end
=
END
)
:
query
=
{
w
.
upper
(
)
for
w
in
query_words
}
for
code
in
range
(
start
,
end
)
:
char
=
chr
(
code
)
name
=
unicodedata
.
name
(
char
,
None
)
if
name
and
query
.
issubset
(
name
.
split
(
)
)
:
(
f
'
U+
{code:04X}
\t
{char}
\t
{name}
'
)
def
main
(
words
)
:
if
words
:
find
(
*
words
)
else
:
(
'
Please provide words to find.
'
)
if
__name__
==
'
__main__
'
:
main
(
sys
.
argv
[
1
:
]
)
Establece por defecto el rango de puntos de código a buscar.
find
aceptaquery_words
y argumentos opcionales de sólo palabra clave para limitar el alcance de la búsqueda, con el fin de facilitar las pruebas.Convierte
query_words
en un conjunto de cadenas en mayúsculas.Obtén el carácter Unicode para
code
.Obtiene el nombre del carácter, o
None
si el punto de código no está asignado.Si hay un nombre, divídelo en una lista de palabras y comprueba que el conjunto
query
es un subconjunto de esa lista.Imprime la línea con el punto de código en formato
U+9999
, el carácter y su nombre.
El módulo unicodedata
tiene otras funciones interesantes. A continuación, veremos algunas relacionadas con la obtención de información a partir de caracteres con significado numérico.
Significado numérico de los caracteres
El módulo unicodedata
incluye funciones para comprobar si un carácter Unicode representa un número y, en caso afirmativo, su valor numérico para los humanos, en contraposición a su número de punto de código.El Ejemplo 4-22 muestra el uso de unicodedata.name()
y unicodedata.numeric()
, junto con los métodos .isdecimal()
y .isnumeric()
de str
.
Ejemplo 4-22. Demostración de metadatos de caracteres numéricos de la base de datos Unicode (las llamadas describen cada columna de la salida)
import
unicodedata
import
re
re_digit
=
re
.
compile
(
r
'
\
d
'
)
sample
=
'
1
\xbc
\xb2
\u0969
\u136b
\u216b
\u2466
\u2480
\u3285
'
for
char
in
sample
:
(
f
'
U+{ord(char):04x}
'
,
char
.
center
(
6
)
,
'
re_dig
'
if
re_digit
.
match
(
char
)
else
'
-
'
,
'
isdig
'
if
char
.
isdigit
(
)
else
'
-
'
,
'
isnum
'
if
char
.
isnumeric
(
)
else
'
-
'
,
f
'
{unicodedata.numeric(char):5.2f}
'
,
unicodedata
.
name
(
char
)
,
sep
=
'
\t
'
)
Punto de código en formato
U+0000
.Carácter centralizado en un
str
de longitud 6.Muestra
re_dig
si el carácter coincide con la expresión regularr'\d'
.Muestra
isdig
sichar.isdigit()
esTrue
.Muestra
isnum
sichar.isnumeric()
esTrue
.Valor numérico formateado con anchura 5 y 2 decimales.
Nombre del carácter Unicode.
Si ejecutas el Ejemplo 4-22, obtendrás la Figura 4-7, si la fuente de tu terminal tiene todos esos glifos.
La sexta columna de la Figura 4-7 es el resultado de llamar a unicodedata.numeric(char)
sobre el carácter. Demuestra que Unicode conoce el valor numérico de los símbolos que representan números. Así que si quieres crear una aplicación de hoja de cálculo que admita dígitos tamiles o números romanos, ¡adelante!
La Figura 4-7 muestra que la expresión regular r'\d'
coincide con el dígito "1" y con el dígito devanagari 3, pero no con algunos otros caracteres que la función isdigit
considera dígitos. El módulo re
no es tan experto en Unicode como podría serlo. El nuevo módulo regex
disponible en PyPI se diseñó para sustituir con el tiempo a re
y proporciona un mejor soporte Unicode.11
Volveremos al módulo re
en la próxima sección.
A lo largo de este capítulo hemos utilizado varias funciones de unicodedata
, pero hay muchas más que no hemos cubierto. Consulta la documentación de la biblioteca estándar sobre el módulounicodedata
.
A continuación echaremos un vistazo rápido a las API de modo dual, que ofrecen funciones que aceptan argumentos de str
o bytes
con un tratamiento especial según el tipo.
API de modo dual str y bytes
La biblioteca estándar de Python tiene funciones que aceptan argumentos str
o bytes
y se comportan de forma diferente según el tipo. Puedes encontrar algunos ejemplos en losmódulos re
y os
.
str frente a bytes en expresiones regulares
Si construye una expresión regular con bytes
, patrones como \d
y \w
sólo coinciden con caracteres ASCII; en cambio, si estos patrones se dan como str
, coinciden con dígitos Unicode o letras más allá de ASCII. El Ejemplo 4-23 y la Figura 4-8 comparan cómo los patrones str
y bytes
hacen coincidir letras, dígitos ASCII, superíndices y dígitos Tamil.
Ejemplo 4-23. ramanujan.py: comparar el comportamiento de las expresiones regulares simples str
y bytes
import
re
re_numbers_str
=
re
.
compile
(
r
'
\
d+
'
)
re_words_str
=
re
.
compile
(
r
'
\
w+
'
)
re_numbers_bytes
=
re
.
compile
(
rb
'
\
d+
'
)
re_words_bytes
=
re
.
compile
(
rb
'
\
w+
'
)
text_str
=
(
"
Ramanujan saw
\u0be7
\u0bed
\u0be8
\u0bef
"
"
as 1729 = 1³ + 12³ = 9³ + 10³.
"
)
text_bytes
=
text_str
.
encode
(
'
utf_8
'
)
(
f
'
Text
\n
{text_str!r}
'
)
(
'
Numbers
'
)
(
'
str :
'
,
re_numbers_str
.
findall
(
text_str
)
)
(
'
bytes:
'
,
re_numbers_bytes
.
findall
(
text_bytes
)
)
(
'
Words
'
)
(
'
str :
'
,
re_words_str
.
findall
(
text_str
)
)
(
'
bytes:
'
,
re_words_bytes
.
findall
(
text_bytes
)
)
Las dos primeras expresiones regulares son del tipo
str
.Los dos últimos son del tipo
bytes
.Texto Unicode a buscar, que contiene los dígitos Tamil para
1729
(la línea lógica continúa hasta el token del paréntesis derecho).Esta cadena se une a la anterior en tiempo de compilación (véase "2.4.2. Concatenación de literales de cadena" en La Referencia del Lenguaje Python).
Se necesita una cadena
bytes
para buscar con las expresiones regularesbytes
.El patrón
str
r'\d+'
coincide con los dígitos Tamil y ASCII.El patrón
bytes
rb'\d+'
sólo coincide con los bytes ASCII de los dígitos.El patrón
str
r'\w+'
coincide con las letras, superíndices, Tamil ydígitos ASCII.El patrón
bytes
rb'\w+'
sólo coincide con los bytes ASCII de letras y dígitos.
El ejemplo 4-23 es un ejemplo trivial para dejar claro un punto: puedes utilizar expresiones regulares en str
y bytes
, pero en el segundo caso, los bytes fuera del rango ASCII se tratan como no dígitos y caracteres no-palabra.
Para las expresiones regulares str
, existe una bandera re.ASCII
que hace que \w
, \W
, \b
, \B
, \d
, \D
, \s
, y \S
realicen coincidencias sólo ASCII. Consulta la documentación del módulo re
para más detalles.
Otro módulo importante de modo dual es os
.
str Versus bytes in os Funciones
El núcleo de GNU/Linux no es experto en Unicode, por lo que en el mundo real puedes encontrarte con nombres de archivo formados por secuencias de bytes que no son válidas en ningún esquema de codificación sensato, y no pueden descodificarse a str
. Los servidores de archivos con clientes que utilizan diversos SO son especialmente propensos a este problema.
Para evitar este problema, todas las funciones del módulo os
que aceptan nombres de archivo o rutas de acceso toman argumentos como str
o bytes
. Si se llama a una de estas funciones con un argumento str
, el argumento se convertirá automáticamente utilizando el códec nombrado por sys.getfilesystemencoding()
, y la respuesta del SO se descodificará con el mismo códec. Esto es casi siempre lo que quieres, de acuerdo con las buenas prácticas del sándwich Unicode.
Pero si debes tratar (y quizás arreglar) nombres de archivo que no se pueden manejar de esa manera, puedes pasar argumentos bytes
a las funciones os
para obtener valores de retorno bytes
. Esta función te permite tratar con cualquier archivo o nombre de ruta, por muchos gremlins que encuentres. Mira el Ejemplo 4-24.
Ejemplo 4-24. listdir
con str
y bytes
argumentos y resultados
>>>
os
.
listdir
(
'
.
'
)
['abc.txt', 'digits-of-π.txt']
>>>
os
.
listdir
(
b
'
.
'
)
[b'abc.txt', b'digits-of-\xcf\x80.txt']
El segundo nombre de archivo es "dígitos-de-π.txt" (con la letra griega pi).
Dado un argumento
byte
,listdir
devuelve los nombres de archivo como bytes:b'\xcf\x80'
es la codificación UTF-8 de la letra griega pi.
Para ayudar en el manejo manual de las secuencias str
o bytes
que son nombres de archivo o nombres de ruta, el módulo os
proporciona funciones especiales de codificación y descodificación os.fsencode(name_or_path)
y os.fsdecode(name_or_path)
. Ambas funciones aceptan un argumento de tipo str
, bytes
, o un objeto que implemente la interfaz os.PathLike
desde Python 3.6.
Unicode es una profunda madriguera de conejo. Es hora de terminar nuestra exploración de str
y bytes
.
Resumen del capítulo
En comenzamos el capítulo descartando la idea de que 1 character == 1 byte
. A medida que el mundo adopta Unicode, necesitamos mantener el concepto de cadenas de texto separadas de las secuencias binarias que las representan en los archivos, y Python 3 impone esta separación.
Tras un breve repaso de los tipos de datos de secuencias binarias -bytes
, bytearray
, ymemoryview
-pasamos a la codificación y descodificación, con una muestra de códecs importantes, seguida de enfoques para evitar o tratar los infames UnicodeEncodeError
, UnicodeDecodeError
, y el SyntaxError
causados por una codificación errónea en los archivos fuente de Python.
Después consideramos la teoría y la práctica de la detección de codificaciones en ausencia de metadatos: en teoría, no se puede hacer, pero en la práctica el paquete Chardet lo hace bastante bien para varias codificaciones populares. A continuación, se presentaron las marcas de orden de bytes como la única pista de codificación que suele encontrarse en los archivos UTF-16 y UTF-32 -a veces también en los archivos UTF-8-.
En la siguiente sección, demostramos cómo abrir archivos de texto, una tarea fácil salvo por un escollo: el argumento de la palabra clave encoding=
no es obligatorio cuando abres un archivo de texto, pero debería serlo. Si no especificas la codificación, acabas con un programa que consigue generar "texto plano" incompatible entre plataformas, debido a codificaciones por defecto conflictivas. A continuación, expusimos las distintas configuraciones de codificación que Python utiliza por defecto y cómo detectarlas. Una triste constatación para los usuarios de Windows es que estas configuraciones suelen tener valores distintos dentro de la misma máquina, y los valores son incompatibles entre sí; los usuarios de GNU/Linux y macOS, en cambio, viven en un lugar más feliz donde UTF-8 es el valor por defecto prácticamente en todas partes.
Unicode proporciona múltiples formas de representar algunos caracteres, por lo que la normalización es un requisito previo para la concordancia de texto. Además de explicar la normalización y el plegado de mayúsculas y minúsculas, presentamos algunas funciones de utilidad que puedes adaptar a tus necesidades, incluidas transformaciones drásticas como la eliminación de todos los acentos. A continuación, vimos cómo ordenar correctamente el texto Unicode aprovechando el módulo estándar locale
-con algunas advertencias- y una alternativa que no depende de complicadas configuraciones regionales: el paquete externo pyuca.
Aprovechamos la base de datos Unicode para programar una utilidad de línea de comandos para buscar caracteres por su nombre, en 28 líneas de código, gracias a la potencia de Python. Echamos un vistazo a otros metadatos Unicode y tuvimos una breve visión general de las API de modo dual, en las que algunas funciones se pueden llamar con argumentos str
o bytes
, produciendo resultados diferentes.
Otras lecturas
La charla de Ned Batchelder en la 2012 PyCon US "Pragmatic Unicode, or, How Do I Stop the Pain?" fue excepcional. Ned es tan profesional que proporciona una transcripción completa de la charla junto con las diapositivas y el vídeo.
"Codificación de caracteres y Unicode en Python: Cómo (╯°□°)╯︵┻━┻ con dignidad"(diapositivas, vídeo) fue la excelente charla de la PyCon 2014 de Esther Nam y Travis Fischer, donde encontré el enjundioso epígrafe de este capítulo: "Los humanos usan texto. Los ordenadores hablan bytes".
Lennart Regebro -uno de los revisores técnicos de la primera edición de este libro- comparte su "Modelo Mental Útil de Unicode (UMMU)" en el breve post "Unconfusing Unicode: ¿Qué es Unicode?". Unicode es un estándar complejo, por lo que el UMMU de Lennart es un punto de partida realmente útil.
El "Unicode HOWTO" oficial de la documentación de Python aborda el tema desde varios ángulos diferentes, desde una buena introducción histórica hasta detalles de sintaxis, códecs, expresiones regulares, nombres de archivo y buenas prácticas para la E/S compatible con Unicode (es decir, el sándwich Unicode), con multitud de enlaces de referencia adicionales en cada sección. El capítulo 4, "Strings", del impresionante libro de Mark Pilgrim Dive into Python 3 (Apress) también proporciona una muy buena introducción al soporte Unicode en Python 3. En el mismo libro, el Capítulo 15 describe cómo se portó la biblioteca Chardet de Python 2 a Python 3, un valioso caso de estudio dado que el cambio de la antigua str
a la nueva bytes
es la causa de la mayoría de los dolores de migración, y esa es una preocupación central en una biblioteca diseñada para detectar codificaciones.
Si conoces Python 2 pero eres nuevo en Python 3, "Qué hay de nuevo en Python 3.0" de Guido van Rossum tiene 15 viñetas que resumen lo que ha cambiado, con muchos enlaces. Guido empieza con una afirmación contundente "Todo lo que creíassaber sobre datos binarios y Unicode ha cambiado". La entrada del blog de Armin Ronacher "La guía actualizada de Unicode en Python" es profunda ydestaca algunos de los escollos de Unicode en Python 3 (Armin no es un gran fan dePython 3).
El capítulo 2, "Cadenas y texto", del Python Cookbook, 3ª ed. (O'Reilly), de David Beazley y Brian K. Jones, contiene varias recetas sobre normalización Unicode, limpieza de texto y realización de operaciones orientadas a texto en secuencias de bytes. El capítulo 5 trata de los archivos y la E/S, e incluye la "Receta 5.17. Escribir bytes en un archivo de texto", que muestra que debajo de cualquier archivo de texto siempre hay un flujo binario al que se puede acceder directamente cuando sea necesario. Más adelante en el libro de recetas, el módulo struct
se utiliza en la "Receta 6.11. Lectura y escritura de matrices binarias de estructuras".
El blog "Python Notes" de Nick Coghlan tiene dos entradas muy relevantes para este capítulo: "Python 3 y los protocolos binarios compatibles con ASCII" y "Procesamiento de archivos de texto en Python 3". Muy recomendables.
Puedes consultar una lista de las codificaciones admitidas por Python en "Codificaciones estándar", en la documentación del módulo codecs
. Si necesitas obtener esa lista mediante programación, mira cómo se hace en el script/Tools/unicode/listcodecs.pyque viene con el código fuente de CPython.
Los libros Unicode Explained de Jukka K. Korpela (O'Reilly) y Unicode Demystified de Richard Gillam (Addison-Wesley) no son específicos de Python, pero me resultaron muy útiles mientras estudiaba los conceptos de Unicode. Programming with Unicode de Victor Stinner es un libro gratuito autopublicado (CreativeCommons BY-SA) que cubre Unicode en general, así como herramientas y API en el contexto de los principales sistemas operativos y algunos lenguajes de programación, incluido Python.
Las páginas del W3C "Case Folding: Una introducción" y "Modelo de caracteres para la World Wide Web: String Matching" cubren los conceptos de normalización, siendo la primera una suave introducción y la segunda una nota del grupo de trabajo escrita en lenguaje estándar seco, el mismo tono del "Unicode Standard Annex #15-Unicode Normalization Forms". La sección "Preguntas frecuentes sobre normalización" de Unicode. org es más legible, al igual que las "Preguntas frecuentes sobre NFC" de Mark Davis, autor de varios algoritmos Unicode y presidente del Consorcio Unicode en el momento de escribir estas líneas.
En 2016, el Museo de Arte Moderno (MoMA) de Nueva York incorporó a su colecciónel emoji original, los 176 emojis diseñados por Shigetaka Kurita en 1999 para NTT DOCOMO, la compañía de telefonía móvil japonesa. Yendo más atrás en la historia, la Emojipedia publicó"Corrigiendo el récord del primer conjunto de emoji", atribuyendo a la japonesa SoftBank el primer conjunto de emoji conocido, implementado en teléfonos móviles en 1997.
El conjunto de SoftBank es la fuente de 90 emojis que ahora están en Unicode, incluido U+1F4A9 (PILE OF POO
). emojitracker.comde Matthew Rothenberg es un panel de control en directo que muestra el recuento del uso de emojis en Twitter, actualizado en tiempo real. Mientras escribo esto, FACE WITH TEARS OF JOY
(U+1F602)es el emoji más popular en Twitter, con más de 3.313.667.315apariciones registradas.
1Diapositiva 12 de la charla PyCon 2014 "Codificación de caracteres y Unicode en Python"(diapositivas, vídeo).
2 Python 2.6 y 2.7 también tenían bytes
, pero era sólo un alias del tipo str
.
3 Curiosidad: el carácter ASCII "comilla simple" que Python utiliza por defecto como delimitador de cadenas se llama en realidad APOSTROPHE en el estándar Unicode. Las comillas simples reales son asimétricas: la izquierda es U+2018 y la derecha U+2019.
4 No funcionaba en Python 3.0 a 3.4, causando mucho dolor a los desarrolladores que trataban con datos binarios. La inversión está documentada en PEP 461-Añadir formato % a bytes y bytearray.
5 La primera vez que vi el término "sándwich Unicode" fue en la excelente charla "Unicode Pragmático" de Ned Batchelder en la US PyCon 2012.
6 Fuente: "Línea de comandos de Windows: Búfer de texto de salida Unicode y UTF-8".
7 Curiosamente, el signo micro se considera un "carácter de compatibilidad", pero el símbolo ohm no. El resultado final es que NFC no toca el signo micro pero cambia el símbolo ohm a omega mayúscula, mientras que NFKC y NFKD cambian tanto el ohm como el micro a caracteres griegos.
8 Los diacríticos sólo afectan a la ordenación en el raro caso de que sean la única diferencia entre dos palabras; en ese caso, la palabra con diacrítico se ordena después de la palabra normal.
9 De nuevo, no pude encontrar una solución, pero sí encontré a otras personas que informaban del mismo problema. Alex Martelli, uno de los revisores técnicos, no tuvo ningún problema al utilizar setlocale
y locale.strxfrm
en su Macintosh con macOS 10.9. En resumen: tu experiencia puede variar.
10 Esto es una imagen -no un listado de código- porque los emojis no están bien soportados por la cadena de herramientas de publicación digital de O'Reilly mientras escribo esto.
11 Aunque no fue mejor que re
en la identificación de dígitos en esta muestra concreta.
Get Python fluido, 2ª edición now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.