Nivel de dificultad:3 sobre 5
¿Sabías que la gente de Bougainville tiene el alfabeto más pequeño del mundo? El alfabeto Rotokas está compuesto únicamente por 12 letras: A, E, G, I, K, O, P, R, S, T, U y V. En el otro lado del espectro los lenguajes como el Chino, Japonés y Koreano tienen miles de caracteres. El inglés, desde luego, tiene 26 letras --52 si cuentas las mayúsculas y minúsculas de forma separada-- más un puñado de símbolos de puntuación !@#$%&?.
Cuando las personas hablan sobre ``texto'' piensan en ``caracteres y símbolos en la pantalla del ordenador''. Pero los ordenadores no conocen ni símbolos ni caracteres, conocen bits y bytes. Cada elemento textual que ves en la pantalla está almacenado con una codificación de caracteres particular. Explicándolo de manera informal, la codificación de caracteres proporciona una conversión entre lo que ves en la pantalla y lo que el ordenador realmente almacena en memoria o en disco. Existen muchas codificaciones de caracteres diferentes, algunas optimizadas para determinados lenguajes como el ruso, el chino o el inglés, y otras que se pueden utilizar para diferentes lenguajes.
En realidad es más complicado. Muchos caracteres son comunes a diferentes codificaciones, pero cada codificación puede utilizar una secuencia de bytes diferente para almacenar esos caracteres en memoria o disco. Puedes imaginarte que una codificación de caracteres es como una especia de clave de desencriptado. Cuando tengas una secuencia de bytes --un fichero, una página web o cualquier otra cosa-- y se considere que esos bytes representan ``texto'', necesitas conocer en qué codificación de caracteres se encuentra para poder decodificar los bytes y conocer a qué caracteres representan. Si tienes una clave de decodificación equivocada o no dispones de ninguna, la decodificación no será posible o será errónea (si se usa una decodificación equivocada), y el resultado será un texto sin sentido.
Todo lo que que pensabas que sabías sobre las cadenas de texto es erróneo.
Seguramente habrás visto a veces paginas web con extraños caracteres de interrogación o similar, en donde esperabas algún carácter como el apóstrofo o vocales acentuadas. Esto suele indicar que el autor de la página no declaró correctamente la codificación de caracteres que utilizó por lo que tu navegador la tiene que adivinar y el resultado es una mezcla de caracteres esperados e inesperados. En inglés esto es simplemente desconcertante, pero en otros lenguajes el resultado puede ser completamente ilegible.
Existen tablas de codificación de caracteres para cada uno de los lenguajes importantes del mundo. Puesto que cada lenguaje es diferente, y la memoria y el espacio en disco ha sido caro históricamente, cada tabla de codificación de caracteres está optimizada para un lenguaje en particular. Lo que quiero decir con esto es que cada una de las codificaciones usa los mismos números (0 - 255) para representar los caracteres de un lenguaje determinado. Por ejemplo, posiblemente estés familiarizado con la codificación ASCII, que almacena los caracteres del inglés como números que van del 0 al 127 (65 es la ``A'', 97 es la ``a'', etc). El inglés es un alfabeto muy simple, por lo que puede expresarse con menos de 128 números. Para aquellos que sepan contar en base 2, eso significa 7 bits de los 8 de un byte.
Algunos lenguajes de Europa como el francés, español y alemán necesitan más letras que el inglés. O, para ser más precisos, tienen letras que se combinan con diversas marcas diacríticas, como el carácter ñ del español. La tabla de codificación de caracteres más común para estos lenguajes es CP-1252, que también es conocida como windows-1252 porque se utiliza ampliamente en el sistema operativo Microsoft Windows. La codificación CP-1252 comparte con ASCII los primeros 128 caracteres (0-127), pero luego se extiende en el rango de 128 a 255 para los caracteres restantes (241 es la ``ñ'', 252 es la ``ü'', etc). Continúa siendo una tabla de codificación de un único byte. El valor mayor, 255, aún cabe en un byte.
Además existen lenguajes como el chino, japonés y coreano, que tienen tantos caracteres que requieren tablas de codificación de caracteres multibyte. Esto significa que cada ``carácter'' se representa como un número de dos bytes lo que abarca del 0 al 65535. Pero las codificaciones multibyte también tienen el mismo problema que las diferentes codificaciones de un único byte: cada una de ellas puede utilizar el mismo número para expresar un carácter diferente. La única diferencia entre ellas es que el rango de caracteres disponible es mayor en las codificaciones multibyte.
Esto no suponía demasiado problema en un mundo desconectado, en donde ``texto'' era algo que tecleabas para ti y ocasionalmente imprimías. No existía mucho ``texto plano''. El código fuente era ASCII y todo el mundo usaba procesadores de textos que definían su propio formato que tenían en cuenta la información de codificación de caracteres junto con la información de estilo, etc. La gente leía estos documentos con el mismo programa procesador de texto que el autor original, por lo que todo funcionaba, más o menos.
Ahora piensa en la aparición de las redes globales y en el correo y la web. Mucho ``texto plano'' anda suelto por el mundo, se crea en un ordenador, se transmite a un segundo y se muestra en un tercero. Los ordenadores únicamente distinguen números, pero los números pueden significar cosas diferentes. ¡Oh no! ¿Qué hacer? Bien, los sistemas tuvieron que diseñarse para transportar la información de codificación junto con el ``texto plano''. Recuerda, se trata de las claves de descodificación que mapean los números entendidos por el ordenador a caracteres legibles por personas. Una clave de descodificación perdida da lugar a texto ilegible.
Ahora piensa en intentar almacenar diversos documentos de texto en el mismo lugar, como en una tabla de una misma base de datos que almacena todo el correo electrónico que hayas recibido. Aún necesitas almacenar la codificación de caracteres junto con cada correo electrónico para que se pueda leer apropiadamente. ¿Parece difícil? Prueba a buscar en tu base de datos de correos, eso significa convertir entre múltiples tablas de codificación de caracteres sobre la marcha. ¿No suena divertido?.
Piensa ahora en la posibilidad de documentos multilíngües, en donde aparecen caracteres en diferentes lenguajes4.1. Y, por supuesto, querrás buscar el contenido de esos documentos.
Ahora llora un rato, porque todo lo que creías conocer sobre las cadenas de texto es erróneo, y no existe algo así como el ``texto plano''.
Entra en Unicode.
Unicode es un sistema diseñado para representar todos los caracteres de todos los lenguajes. Representa cada letra, carácter o ideograma como un número de cuatro bytes. Cada número representa a un único carácter que se use en al menos uno de los lenguajes del mundo (No se usan todos los números, pero se usan más de 65535 por lo que no es suficiente utilizar dos bytes). Los caracteres que se utilizan en diferentes lenguajes tienen el mismo número generalmente, a menos que exista una buena razón etimológica para que no sea así. De todos modos hay exactamente un número por cada carácter y un carácter por número. De esta forma, cada número siempre significa una única cosa. No existen ``modos'' que rastrear, U+0041 siempre corresponde a ``A'', incluso si tu lenguaje no usa tal símbolo.
A primera vista parece una gran idea, una tabla de codificación de caracteres para gobernarlos a todos. Múltiples lenguajes por documento, no más ``cambios de modo'' para conmutar entre tablas de codificación en medio de un documento. Pero existe una pregunta obvia. ¿Cuatro bytes? ¿Para cada carácter? Parece un gasto inútil la mayor parte de las ocasiones, especialmente para idiomas como el inglés o el español, que necesitan menos de un byte (256 números) para expresar cada uno de los caracteres posibles. De hecho, es un desperdicio de espacio incluso para los lenguajes basados en ideogramas como el chino, que nunca necesitan más de dos caracteres para cada carácter.
Existe una tabla de codificación Unicode que utiliza cuatro bytes por cada carácter. Se denomina UTF-32 porque 32 bits es igual a 4 bytes. UTF-32 es una codificación directa; toma cada carácter Unicode (un número de 4 bytes) y representa al carácter con ese mismo número. Esto tiene varias ventajas siendo la más importante que puedes encontrar el ``enésimo'' carácter de una cadena en un tiempo constante ya que se encuentra en a partir del byte 4*n. También tiene varios inconvenientes, siendo el más obvio que necesita cuatro bytes para almacenar cada carácter.
Incluso aunque existen muchos caracteres Unicode, resulta que la mayoría de la gente nunca usará nada más allá de los primeros 65535. Por eso existe otra codificación Unicode denominada UTF-16 (16 bits son 2 bytes) que codifica cada uno de los caracteres de 0 a 65535 como dos bytes. Además utiliza algunos ``trucos sucios'' por si necesitas representar aquellos caracteres que se usan raramente y que están más allá del 65535. La ventaja más obvia es que esta tabla de codificación de caracteres es el doble de eficiente que la de UTF-32 puesto que cada carácter requiere únicamente dos bytes para almacenarse, en lugar de cuatro bytes. Aún se puede encontrar fácilmente el enésimo carácter de una cadena en un tiempo constante, siempre que se asuma que no existen caracteres especiales de los que están por encima de 65535. Lo que suele ser una buena asunción... ¡hasta el momento en que no lo es!
También existen algunos inconvenientes no tan obvios tanto en UTF-32 y UTF-8. Los ordenadores de sistemas diferentes suelen almacenar los bytes de diferentes formas. Esto significa que el carácter U+4E2D podría almacenarse en UTF-16 bien como 4E 2D o como 2D 4E, dependiendo de que el sistema sea ``big-endian'' o ``little-endian''4.2 (para UTF-32 existen más posibilidades de ordenación de los bytes). Mientras tus documentos no dejen tu ordenador estás seguro --las diferentes aplicaciones del mismo ordenador utilizarán la misma ordenación de bytes. Pero en el momento que transfieras los documentos entre sistemas, tal vez a través de Internet, vas a necesitar una forma de indicar el orden en el que se han almacenado los bytes. De otra forma el sistema que recibe los datos no tiene forma de saber si la secuencia de dos bytes 4E 2D significa U+4E2D o U+2D4E.
Para resolver este problema, las codificaciones multibyte de Unicode definen el ``Byte Orden Mark'' (BOM)4.3, que es un carácter especial no imprimible que se puede incluir al comienzo de tu documento para indica qué ordenación de bytes tiene el documento. Para UTF-16 la marca de ordenación de bytes es U+FEFF, por lo que si recibes un documento UTF-16 que comienza con los bytes FF FE, ya sabes en qué forma vienen ordenados los bytes; si comienza con FE FF sabes que el orden es el contrario.
Aún así, UTF-16 no es exactamente el ideal, especialmente si la mayor parte de los caracteres que utilizas son ASCII. Si lo piensas, incluso una página web china contendrá muchos caracteres ASCII --todos los elementos y atributos que rodean a los caracteres imprimibles chinos. Poder encontrar el ``enésimo'' carácter está bien, pero existe el problema de los caracteres que requieren más de dos bytes, lo que significa que no puedes garantizar que todos los caracteres ocupan exactamente dos bytes, por lo que en la práctica no puedes encontrar el carácter de la posición ``enésima'' en un tiempo constante a no ser que mantengas un índice separado. Y muchacho, te aseguro que hay mucho texto ASCII por el mundo...
Otras personas valoraron estas preguntas y plantearon una solución:
UTF-8
UTF-8 es un sistema de codificación de longitud variable para Unicode. Esto significa que los caracteres pueden utilizar diferente número de bytes. Para los caracteres ASCII utiliza un único byte por carácter. De hecho, utiliza exactamente los mismos bytes que ASCII por lo que los 128 primeros caracteres son indistinguibles. Los caracteres ``latinos extendidos'' como la ñ o la ö utilizan dos bytes4.4. Los caracteres chinos utilizan tres bytes, y los caracteres más ``raros'' utilizan cuatro.
Desventajas: debido a que los caracteres pueden ocupar un número diferente de bytes, encontrar el carácter de la posición ``enésima'' es una operación de orden de complejidad O(n) --lo que significa que cuanto más larga sea la cadena, más tiempo lleva encontrar un carácter específico. Asimismo, hay que andar codificando y decodificando entre bytes y caracteres.
Ventajas: se trata de una codificación supereficiente de los caracteres ASCII. No es peor que UTF-16 para los caracteres latinos extendidos, y es mejor que UTF-32 para los caracteres chinos. También (aquí tendrás que confiar en mi, porque no te voy a mostrar la matemática involucrada), debido a la naturaleza exacta de la manipulación de los bits, no existen problemas de ordenación de bits. Un documento codificado en UTF-8 siempre contiene la misma cadena de bytes sea cual sea el ordenador y sistema operativo.
En Python 3 todas las cadenas de texto son secuencias de caracteres Unicode. En Python 3 no existen cadenas codificadas en UTF-8 o en CP-1252. No es correcto decir ¿Es esta cadena una cadena codificada en UTF-8?. UTF-8 es una forma de codificar caracteres en una secuencia de bytes. Si quieres convertir una cadena de caracteres en una secuencia de bytes en una codificación de caracteres particular, Python 3 puede ayudarte con ello. Si quieres tomar una secuencia de bytes y convertirla en una cadena de texto, también te puede ayudar Python 3. Los bytes no son caracteres, los bytes son bytes. Los caracteres son una abstracción. Una cadena es una secuencia de esas abstracciones.
»> s = '快乐 Python' »> len(s) 9 »> s[0] '快' »> s + ' 3' '快乐 Python 3' |
Las cadenas de texto se pueden definir utilizando comillas simples o dobles.
Vamos a echarle otro vistazo a parahumanos.py:
# parahumanos.py |
Python 3 permite formatear valores en cadenas de texto. Aunque este sistema permite expresiones muy complejas, su uso más básico consiste en insertar un valor en una cadena de texto en el lugar definido por un ``marcador''.
»> usuario = 'mark' »> clave = 'PapayaWhip' »> "La clave de 0 es 1".format(usuario, clave) "La clave de mark es PapayaWhip" |
En el ejemplo anterior se muestra el ejemplo más simple, aquél en el que los campos de reemplazo son números enteros. Los campos de reemplazo enteros se tratan como índices posicionales en la lista de parámetros del método format(). Eso significa que el {0} se reemplaza por el primer parámetro (usuario en este caso), {1} se reemplaza por el segundo (clave en este caso), etc. Puedes utilizar tantos campos de reemplazo posicional como parámetros se pasen en el método format(). Pero los campos de reemplazo permiten mucha más funcionalidad.
»> import parahumanos »> mis_sufijos = parahumanos.SUFIJOS[1000] »> mis_sufijos ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] »> '10000[0] = 10[1]'.format(mis_sufijos) '1000KB = 1MB' |
El {0} se reemplaza por el primer parámetro, {1} se reemplaza por el segundo.
Lo que este ejemplo te enseña es que los especificadores de formato pueden utilizarse para acceder a los elementos y propiedades de las estructuras de datos utilizando (casi) sintaxis de Python. Esto se denomina nombres de campos compuestos. Estos son los nombres de campos que funcionan:
Solamente para despejar tu cerebro te muestro aquí un ejemplo que combina todo lo anterior.
»> import parahumanos »> import sys »> '1MB = 10000.modules[parahumanos].SUFIJOS[1000][0]'.format(sys) '1MB = 1000KB' |
Así es como funciona:
¡Pero espera! ¡Hay mas! Vamos a echar otro vistazo a la línea de código más extraña de parahumanos.py:
if tamanyo < multiplo: return '0:.1f 1'.format(tamanyo, sufijo) |
Como ya sabemos, {1} se sustituye por el segundo parámetro sufijo del método format(). Pero ¿qué es {0:.1f}? Tiene dos partes, {0} que ya conoces, y :.f que no conoces. La segunda parte (desde los dos puntos hasta la letra f) define el especificador de forma que afina cómo debe sustituirse la variable al formatearla.
Los especificadores de formato te permiten indicar cómo se debe efectuar la sustitución del texto, como sucede con la función printf() en el lenguaje C. Puedes añadir ceros o espacios de relleno delante del número, alinear cadenas, controlar la precisión de decimales o convertir el número a hexadecimal.
Dentro del campo de sustitución se utiliza el símbolo de dos puntos (:) para marcar el comienzo del especificador de formato. El especificador de formato .1 significa que se ``redondee a la décima más próxima'' (que se muestre únicamente un dígito después del punto decimal). El especificador ``f'' indica que el número debe mostrarse en formato punto fijo (por oposición a la notación exponencial u otra representación de un número). Si la variable tamanyo vale 698.24 y la variable sufijo vale ``GB'' la cadena formateada resultante es ``698.2 GB'', porque 698.24 se redondea con un solo dígito después del punto decimal.
»> '0:.1f 1'.format(698.24, 'GB') '698.2 GB' |
Para conocer los detalles exactos de los especificadores de formato puedes consultar el apartado Mini-lenguaje de especificación de formato4.5 de la documentación oficial de Python 3.
Además de formatearlas, es posible hacer muchas otras cosas de utilidad con las cadenas de texto.
»> s = '''Los archivos terminados son el re- ... sultado de años de estudio cientí- ... fico combinados con la ... experiencia de años.''' »> s.splitlines() ['Los archivos terminados son el re-', 'sultado de años de estudio cientí-', 'fico combinados con la ', 'experiencia de años.'] »> print(s.lower()) los archivos terminados son el re- sultado de años de estudio cientí- fico combinados con la experiencia de años. »> s.lower().count('l') 4 |
Pongamos un caso muy común. Supón que tienes una cadena de texto en forma de parejas clave-valor, clave1=valor1&clave2=valor2, y quieres dividirla y crear un diccionario de la forma {clave1: valor1, clave2: valor2}.
»> consulta = 'usuario=pilgrim&basededatos=master&clave=PapayaWhip' »> una_lista = consulta.split('&') »> una_lista ['usuario=pilgrim', 'basededatos=master', 'clave=PapayaWhip'] »> una_lista_de_listas = [v.split('=', 1) for v in una_lista] »> una_lista_de_listas [['usuario', 'pilgrim'], ['basededatos', 'master'], ['clave', 'PapayaWhip']] »> a_dict = dict(una_lista_de_listas) »> a_dict 'clave': 'PapayaWhip', 'usuario': 'pilgrim', 'basededatos': 'master' |
El ejemplo anterior, explica un caso que se parece a lo que habría que hacer para reconocer los parámetros de una URL. Pero en la vida real, el reconocimiento de los parámetros de una URL es más complejo. Si vas a tener que reconocer los parámetros que recibes mediante una URL utiliza la función de la librería urlib.parse denominada parse_qs()4.6, que reconoce los casos más complejos.
Cuando ya has definido una cadena puedes recuperar cualquier parte de ella creando una nueva cadena de texto. A esto se denomina troceado/particionado de cadenas4.7. Esto funciona de forma idéntica a como funciona para las listas, lo que tiene sentido, porque las cadenas de texto no son más que cadenas de caracteres.
»> una_cadena = 'Mi vocabulario comienza donde el tuyo termina' »> una_cadena[3:14] 'vocabulario' »> una_cadena[3:-3] 'vocabulario comienza donde el tuyo term' »> una_cadena[0:2] 'Mi' »> una_cadena[:23] 'Mi vocabulario comienza' »> una_cadena[23:] ' donde el tuyo termina' |
Los bytes son bytes; los caracteres son una abstracción. A una secuencia inmutable de caracteres Unicode se le llama cadena de texto. Una secuencia inmutable de números entre el 0 y el 255 es un objeto que se denomina bytes.
»> by = b'abcd65' »> by b'abcde' »> type(by) <class 'bytes'> »> len(by) 5 »> by += b'' »> by b'abcde' »> len(by) 6 »> by[0] 97 »> by[0] = 102 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'bytes' object does not support item assignment |
»> by = b'abcd65' »> barr = bytearray(by) »> barr bytearray(b'abcde') »> len(barr) 5 »> barr[0] = 102 »> barr bytearray(b'fbcde') |
Algo que no se puede hacer es mezclar bytes y cadenas.
»> by = b'd' »> s = 'abcde' »> by + s Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can't concat bytes to str »> s.count(by) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't convert 'bytes' object to str implicitly »> s.count(by.decode('ascii')) 1 |
Y de este modo has llegado a la relación que existe entre las cadenas de texto y los bytes: los objetos bytes tienen un método decode() que toma como parámetro una tabla de codificación de caracteres y retorna una cadena. Y las cadenas de texto tienen un método denominado encode() que toma una tabla de codificación de caracteres y retorna un objeto bytes. En el ejemplo anterior, la decodificación fue relativamente directa --convertir una secuencia de bytes que estaba en la codificación de caracteres ASCII en una cadena de texto. Pero el mismo proceso funciona con cualquier tabla de codificación de caracteres siempre que dicha tabla soporte los caracteres existentes en la cadena --incluso con codificaciones heredadas (previas a Unicode).
»> s = '快乐 Python' »> len(s) 9 »> by = s.encode('utf-8') »> by b'54990 Python' »> len(by) 13 »> by = s.encode('gb18030') »> by b'06 Python' »> len(by) 11 »> by = s.encode('utf-16') »> by b'_PN 00P00y00t00h00o00n00' »> len(by) 20 »> vuelta = by.decode('utf-16') '快乐 Python' »> vuelta == s True |
Python 3 asume que el código fuente --cada fichero .py-- está codificado en UTF-8.
En Python 2, la codificación de caracteres por defecto de los ficheros .py era ASCII. En Python 3, la codificación por defecto de los ficheros es UTF-8
Si quisieras utilizar una codificación de caracteres diferente en el fichero con el código fuente, puedes incluir una declaración de codificación de caracteres en la primera línea cada fichero. La siguiente declaración define que el fichero se encuentra en una codificación windows-1252
# -*- coding: windows-1252 -*- |
Técnicamente la indicación de la codificación de caracteres puede estar en la segunda línea si la primera línea es una declaración de lanzador de ejecución del estilo de UNIX.
#!/usr/bin/python3 # -*- coding: windows-1252 -*- |
Para disponer de más información consulta la propuesta de mejora de Python PEP 2634.8.
Sobre Unicode en Python:
Sobre Unicode en general:
Sobre codificación de caracteres en otros formatos:
Sobre cadenas y formateo de cadenas:
José Miguel González Aguilera 2016-08-18