9.4. Unicode

Unicode es un sistema para representar caracteres de todos los diferentes idiomas en el mundo. Cuando Python analiza un documento XML, todos los datos se almacenan en memoria como unicode.

Llegaremos a eso en un momento, pero antes, unos antecedentes.

Nota histórica. Antes de unicode, había diferentes sistemas de codificación de caracteres para cada idioma, cada uno usando los mismos números (0-255) para representar los caracteres de ese lenguaje. Algunos (como el ruso) tienen varios estándares incompatibles que dicen cómo representar los mismos caracteres; otros idiomas (como el japonés) tienen tantos caracteres que precisan más de un byte. Intercambiar documentos entre estos sistemas era difícil porque no había manera de que un computador supiera con certeza qué esquema de codificación de caracteres había usado el autor del documento; el computador sólo veía números, y los números pueden significar muchas cosas. Piense entonces en almacenar estos documentos en el mismo sitio (como en una tabla de una base de datos); necesitaría almacenar el tipo de codificación junto con cada texto, y asegurarse de adjuntarlo con el texto cada vez que accediese a él. Ahora imagine documentos multilingües, con caracteres de varios idiomas en el mismo document. (Habitualmente utilizaban códigos de escape para cambiar de modos; ¡puf!, está en modo ruso koi8-r así que el carácter 241 significa esto; ¡puf!, ahora está en modo Mac Greek, así que el carácter 241 significa otra cosa. Y así con todo). Para resolver estos problemas se diseñó unicode.

Para resolver estos problemas, unicode representa cada carácter como un número de 2 bytes, de 0 a 65535.[10] Cada número de 2 bytes representa un único carácter utilizado en al menos un idioma del mundo (los caracteres que se usan en más de un idioma tienen el mismo código numérico). Hay exactamente 1 número por carácter, y exactamente 1 carácter por número. Los datos de unicode nunca son ambiguos.

Por supuesto, sigue estando el problema de todos esos sistemas de codificación anticuados. Por ejemplo, el ASCII de 7 bits que almacena los caracteres ingleses como números del 0 al 127 (65 es la “A”, mayúscula, 97 es la “a” minúscula, etc.). El inglés tiene un alfabeto sencillo, así que se puede expresar en ASCII de 7 bits. Los idiomas europeos occidentales como el francés, español y alemán usan todos un sistema llamado ISO-8859-1 (también conocido como “latin-1”), que usa los caracteres del ASCII de 7 bits del 0 al 127, pero lo extiende en el rango 128-255 para tener caracteres como n-con-una-tilde-sobre-ella (241) y u-con-dos-puntitos-sobre-ella (252). Y unicode usa los mismos caracteres que el ASCII de 7 bits para los números del 0 al 127, y los mismos caracteres que ISO-8859-1 del 128 al 255, y de ahí en adelante se extiende para otros lenguajes que usan el resto de los números, del 256 al 65535.

Puede que en algún momento al tratar con datos unicode tengamos la necesidad de convertirlos en alguno de estos otros sistemas anticuados. Por ejemplo, por necesidad de integración con algún sistema computador que espera que sus datos estén en un esquema específico de 1 byte, o para imprimirlo en alguna terminal o impresora que desconozca unicode. O para almacenarlo en un documento XML que especifique explícitamente la codificación de los caracteres.

Y dicho esto, volvamos a Python.

Python trabaja con unicode desde la versión 2.0 del lenguaje. El paquete XML utiliza unicode para almacenar todos los datos XML, pero puede usar unicode en cualquier parte.

Ejemplo 9.13. Presentación de unicode

>>> s = u'Dive in'            1
>>> s
u'Dive in'
>>> print s                   2
Dive in
1 Para crear una cadena unicode en lugar de una ASCII normal, añada la letra “u” antes de la cadena. Observe que esta cadena en particular no tiene ningún carácter que no sea ASCII. Esto no es problema; unicode es un superconjunto de ASCII (un superconjunto muy grande, por cierto), así que también se puede almacenar una cadena ASCII normal como unicode.
2 Cuando Python imprime una cadena intentará convertirla a la codificación por omisión, que suele ser ASCII (más sobre esto en un momento). Como la cadena unicode está hecha de caracteres que a la vez son ASCII, imprimirlos tiene el mismo resultado que imprimir una cadena ASCII normal; la conversión es consistente, y si no supiera que s era una cadena unicode nunca llegaría a notar la diferencia.

Ejemplo 9.14. Almacenamiento de caracteres no ASCII

>>> s = u'La Pe\xf1a'         1
>>> print s                   2
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
UnicodeError: ASCII encoding error: ordinal not in range(128)
>>> print s.encode('latin-1') 3
La Peña
1 La verdadera ventaja de unicode, por supuesto, es su capacidad de almacenar caracteres que no son ASCII, como la “ñ” española. El carácter unicode para la ñ es 0xf1 en hexadecimal (241 en decimal), que se puede escribir así: \xf1[11]
2 ¿Recuerda que dije que la función print intenta convertir una cadena unicode en ASCII para poder imprimirla? Bien, eso no funcionará aquí, porque la cadena unicode contiene caracteres que no son de ASCII, así que Python produce un error UnicodeError.
3 Aquí es donde entra la conversión-de-unicode-a-otros-esquemas-de-codificación. s es una cadena unicode, pero print sólo puede imprimir cadenas normales. Para resolver este problema, llamamos al método encode, disponible en cada cadena unicode, para convertir la cadena unicode en una cadena normal en el esquema dado, que le pasamos como parámetro. En este caso usamos latin-1 (también conocido como iso-8859-1), que incluye la ñ (mientras que el código ASCII no, ya que sólo incluye caracteres numerados del 0 al 127).

¿Recuerda cuando le dije que Python normalmente convierte el unicode a ASCII cuando necesita hacer una cadena normal partiendo de una unicode? Bien, este esquema por omisión es una opción que puede modificar.

Ejemplo 9.15. sitecustomize.py

# sitecustomize.py                   1
# this file can be anywhere in your Python path,
# but it usually goes in ${pythondir}/lib/site-packages/
import sys
sys.setdefaultencoding('iso-8859-1') 2
1 sitecustomize.py es un script especial; Python lo intentará importar cada vez que arranca, así que se ejecutará automáticamente cualquier código que incluya. Como menciona el comentario, puede estar en cualquier parte (siempre que import pueda encontrarlo), pero normalmente se incluye en el directorio site-packages dentro del directorio lib de Python.
2 La función setdefaultencoding establece, pues eso, la codificación por omisión. Éste es el esquema de codificación que Python intentará usar cada vez que necesite convertir automáticamente una cadena unicode a una normal.

Ejemplo 9.16. Efectos de cambiar la codificación por omisión

>>> import sys
>>> sys.getdefaultencoding() 1
'iso-8859-1'
>>> s = u'La Pe\xf1a'
>>> print s                  2
La Peña
1 Este ejemplo asume que ha hecho en el fichero sitecustomize.py los cambios mencionados en el ejemplo anterior, y reiniciado Python. Si la codificación por omisión sigue siendo 'ascii', significa que no configuró adecuadamente el sitecustomize.py, o que no ha reiniciado Python. La codificación por omisión sólo se puede cambiar durante el inicio de Python; no puede hacerlo más adelante. (Debido a ciertos trucos de programación en los que no voy a entrar ahora, ni siquiera puede invocar a sys.setdefaultencoding tras que Python haya arrancado. Busque “setdefaultencoding” en el site.py para averiguar la razón).
2 Ahora que el esquema de codificación por omisión incluye todos los caracteres que usa en la cadena, Python no tiene problemas en autoconvertir la cadena e imprimirla.

Ejemplo 9.17. Especificación de la codificación en ficheros .py

Si va a almacenar caracteres que no sean ASCII dentro de código de Python, necesitará especificar la codificación en cada fichero .py poniendo una declaración de codificación al inicio de cada uno. Esta declaración indica que el fichero .py contiene UTF-8:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

Ahora, ¿qué pasa con XML? Bueno, cada documento XML está en una codificación específica. ISO-8859-1 es una codificación popular para los datos en idiomas europeos occidentales. KOI8-R es habitual en textos rusos. Si se especifica, la codificación estará en la cabecera del documento XML.

Ejemplo 9.18. russiansample.xml


<?xml version="1.0" encoding="koi8-r"?>       1
<preface>
<title>Предисловие</title>                    2
</preface>
1 Este ejemplo está extraído de un documento XML ruso real; es parte de una traducción al ruso de este mismo libro. Observe la codificación especificada en la cabecera, koi8-r.
2 Éstos son caracteres cirílicos que, hasta donde sé, constituyen la palabra rusa para “Prefacio” . Si abre este fichero en un editor de textos normal los caracteres probablemente parezcan basura, puesto que están codificados usando el esquema koi8-r, pero se mostrarán en iso-8859-1.

Ejemplo 9.19. Análisis de russiansample.xml

>>> from xml.dom import minidom
>>> xmldoc = minidom.parse('russiansample.xml') 1
>>> title = xmldoc.getElementsByTagName('title')[0].firstChild.data
>>> title                                       2
u'\u041f\u0440\u0435\u0434\u0438\u0441\u043b\u043e\u0432\u0438\u0435'
>>> print title                                 3
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
UnicodeError: ASCII encoding error: ordinal not in range(128)
>>> convertedtitle = title.encode('koi8-r')     4
>>> convertedtitle
'\xf0\xd2\xc5\xc4\xc9\xd3\xcc\xcf\xd7\xc9\xc5'
>>> print convertedtitle                        5
Предисловие
1 Estoy asumiendo que ha guardado el ejemplo anterior en el directorio actual con el nombre russiansample.xml. También estoy asumiendo que ha cambiado la codificación por omisión de vuelta a 'ascii' eliminando el fichero sitecustomize.py, o al menos que ha comentado la línea con setdefaultencoding.
2 Observe que el texto de la etiqueta title (ahora en la variable title, gracias a esa larga concatenación de funciones de Python que me he saltado por las prisas y, para su disgusto, no explicaré hasta la siguiente sección), el texto, decía, del elemento title del documento XML está almacenado en unicode.
3 No es posible imprimir el título porque esta cadena unicode contiene caracteres extraños a ASCII, de manera que Python no la convertirá a ASCII ya que eso no tendría sentido.
4 Sin embargo, usted puede convertirlo de forma explícita a koi8-r, en cuyo caso obtendrá una cadena (normal, no unicode) de caracteres de un solo byte (f0, d2, c5, etc.) que son la versión codificada en koi8-r de los caracteres de la cadena unicode original.
5 Imprimir la cadena codificada koi8-r probablemente muestre basura en su pantalla, porque el IDE de Python los interpretará como caracteres iso-8859-1, no koi8-r. Pero al menos los imprime. (Y si lo mira atentamente, verá que es la misma basura que vio cuando abrió el documento original en XML en un editor de texto que no sepa unicode. Python lo ha convertido de koi8-r a unicode cuando analizó el documento XML, y usted lo ha convertido de vuelta).

Para resumir, unicode en sí es un poco intimidante si nunca lo ha visto antes, pero los datos en unicode son muy fáciles de manejar con Python. Si sus documentos XML están todos en ASCII de 7 bits (como los ejemplos de este capítulo), nunca tendrá que pensar sobre unicode, literalmente. Python convertirá los datos ASCII de los documentos XML en unicode mientras lo analiza, y los autoconvertirá a ASCII cuando sea necesario, y ni siquiera lo notará. Pero si necesita tratar con otros idiomas, Python está preparado.

Lecturas complementarias

Footnotes

[10] Tristemente, esto no es más que una simplificación extrema. Unicode ha sido extendido para poder tratar textos clásicos chinos, coreanos y japoneses, que tienen tantos caracteres diferentes que el sistema unicode de 2 bytes no podía representarlos todos. Pero Python no soporta eso de serie, y no sé si hay algún proyecto en marcha para añadirlo. Hemos llegado a los límites de mi conocimiento, lo siento.

[11] N. del T.: Dado que está leyendo este texto en español, es bastante probable que también cuente con una ñ en el teclado y pueda escribirla sin recurrir al hexadecimal, pero aún así he decidido mantener el ejemplo tal cual está en el original, ya que ilustra el concepto.