Juegos de Caracteres II: Unicode y UTF

Continuamos con nuestro viaje por el maravilloso mundo de las codificaciones de caracteres. Si has llegado aquí de primeras te aconsejo que antes leas la primera parte de este artículo.

Lo habíamos dejamos en un momento bastante caótico de la historia. Como dijimos, tenemos una infinidad de juegos de caracteres incompatibles entre sí, llamados ASCII Extendido, que usan el octavo bit de ASCII para añadir 128 caracteres más.

Así pues era necesaria una forma global de representar todos los posibles caracteres existentes. Y Unicode fue el encargado de acometer dicha tarea.

Unicode

Este estándar fue desarrollado por la UTC (no, no tiene nada que ver con los husos horarios) y tiene como particularidad que no es un juego de caracteres. Como vimos en el primer artículo, la primera tarea para representar los distintos caracteres es asignarles un código numérico. Pues esto es exactamente lo que hace Unicode, y nada más. Básicamente es el estándar encargado de asignarle un código numérico a cada uno de los distintos caracteres representables sobre la faz de la tierra (y a muchas chorradas más).

A la hora de referirnos a un carácter unicode vamos a usar el siguiente formato: U+XXXX, siendo las X el código numérico del respectivo carácter (en base hexadecimal). Por poner un ejemplo, el carácter e se representa mediante la secuencia U+0065. Por poner otro ejemplo, el carácter montón de mierda tiene el código unicode U+1F4A9 (sí, en efecto, hay un carácter que representa un montón de mierda, podéis comprobarlo en el siguiente enlace).

El lector avispado se habrá dado cuenta de que el carácter e usa el mismo código numérico en ASCII y en Unicode. Y es que esta es una de las grandes cualidades de Unicode, los primeros caracteres son los ASCII, por lo que igual que en el caso anterior, tenemos que este set, en sus primeros valores al menos, es compatible con el estándar de facto anterior.

Pero nos faltan muchos actores aún en la película. Como hemos dicho, Unicode no es un juego de caracteres, por lo que en el estándar no hay ninguna referencia a como se han de representar los caracteres de manera binaria (dentro de un ordenador). Y esta tarea no es tan sencilla como hasta ahora, debido a que Unicode contiene muchísimos más caracteres de los que caben en un byte, por lo que nuestra forma de codificarlos tendrá que tener en cuenta también la ordenación de bytes de la arquitectura (little-endian o big-endian). Y es aquí donde aparecen las tres codificaciones más usadas en la actualidad: UTF-8, UTF-16 y UTF-32. Vamos a ver como funcionan en orden inverso, ya que es la forma más simple de ver sus ventajas y desventajas. Y si me lo permitís, vamos a ignorar el tema de la ordenación de bytes hasta el final del artículo.

UTF-32

Esta es la variante más sencilla que hay. Los caracteres se guardan en 32bits (4Bytes), por lo que la correspondencia entre el código numérico Unicode y la representación binaria en esta codificación es directa. Es decir, el carácter Unicode e, que como vimos antes tiene el código U+00000065 (puedo añadir todos los ceros que quiera delante) se representará de la siguiente manera:

e
00000000 00000000 00000000 01100101

Sí, un sólo carácter ocupa toda esa cantidad de espacio. Si os acordáis del primer artículo, de hecho, podréis ver como cada carácter ocupará 4 veces más (1B contra 4B). Al hecho de que es un derroche de espacio sangrante hay que sumarle que en un gran porcentaje de los casos la información que circula es de los primeros 127 caracteres, es decir, de la tabla ASCII inicial, por lo que estaremos mandando bytes de ceros a mansalva, ya que los caracteres ASCII se codificarán siempre con los tres primeros bytes a cero, y el último con el código del carácter en cuestión.

Las mentes pensantes vieron que esto no podía ser (decidle a Google que tiene que gastar 4 veces más ancho de banda en servir sus páginas webs por ejemplo), por lo que idearon otras formas más óptimas de codificar Unicode. Decir que, por esta razón, muy pocas aplicaciones/sistemas usan esta codificación para representar Unicode.

UTF-16

Las cabezas pensantes de las que hablábamos antes se dieron cuenta de que la inmensa mayoría de la humanidad usa un subconjunto de lo que ellos codificaron en Unicode, y que este subconjunto estaba situado en la primera mitad de la tabla. A este subconjunto lo llamaron plano básico multilingüe (o BMP de sus siglas en inglés). El BMP puede representarse mediante 2Bytes, por lo que esta codificación usa de forma fija 2Bytes para poder representar todos los caracteres de este subconjunto (2Bytes = 16bits, ¿empezáis a sospechar que significa el número que acompaña a UTF? ;) ).

Es decir, si cogéis un carácter que esté desde el U+0000 al U+FFFF su representación binaria será exacta (tal como lo era en UTF-32 pero con 2Bytes de ceros menos, ya que ahora los caracteres ocupan la mitad). Por poner el mismo ejemplo de antes, ahora el carácter e se representaría con la siguiente secuencia:

e
00000000 01100101

Como podemos ver, es una mejora enorme con respecto a UTF-32. Pero, ¿y qué pasa con los caracteres más allá de U+FFFF? Pues que también se pueden codificar, pero no de forma directa. En este caso UTF-16 empleará 2 pares de 2Bytes (es decir, que ocupará exactamente lo mismo que en UTF-32). Mediante estos dos pares y una serie de transformaciones se podrán codificar estos bytes. La explicación de como lo hace queda fuera de este artículo, ya que es algo intrincada. Esta codificación si que es más usada en la actualidad. Por poner un ejemplo, Qt, el framework de programación en C++ usa UTF-16 en su clase QString.

Pero aun así este sistema sigue teniendo varios problemas. El primero, que seguimos mandando muchos ceros debido a que toda la tabla ASCII tiene el primer byte a cero siempre. El segundo, que el sistema para codificar bytes más allá del U+FFFF es bastante complicado. Tercero, que todos los documentos escritos hasta la fecha se dejarían de ver bien!! UTF-16 (y 32) no son compatibles con ASCII debido a que los caracteres ocupan un número distintos de bytes, a pesar de que el código numérico sea equivalente. Para solucionar estos problemas, tenemos nuestra última codificación.

UTF-8

Este sistema de codificación está realmente bien pensado. Es un sistema dinámico, en cuanto a que a diferencia de los dos anteriores, no ocupa una cantidad de bytes fijo. En su lugar, usa un formato predefinido a la hora de codificar los caracteres, y en base a este formato ese carácter ocupará más o menos, siendo el rango entre 1Byte y 6Bytes (¡sí! puede llegar a ocupar más que UTF-32 para los caracteres más lejanos de la tabla Unicode.)

|filename|images/0011-juegos-caracteres-ii/utf8.png

Este sistema funciona de manera muy simple. En base al código numérico unicode, el carácter ocupará uno o más bytes. Para indicar esto, el formato de los bytes será representativo de cuantos bytes ocupa. Esto que parece muy complicado es muy sencillo de ver con la tabla anterior . La primera columna indica el último código numérico unicode que entra en ese grupo, y las siguientes columnas indican como se codifica cada uno de los bytes UTF-8. Los valores numéricos (en verde) son fijos, y gracias a que lo son, dependiendo de como empiece el primer byte se puede saber cuantos bytes ocupa el carácter. Por otra parte, los valores que tienen una X (en rojo) se rellenan con el código numérico Unicode del carácter que queramos codificar.

Por si aún no ha quedado claro, veamos un ejemplo completo. Intentemos codificar el carácter € (euro) en UTF-8:

|filename|images/0011-juegos-caracteres-ii/euro.png

Para codificar el carácter, primero vemos el código numérico unicode. El carácter euro tiene el código unicode U+20AC, que está en el tercer grupo, por lo que ocupará un total de 3Bytes. Así que una vez colocado el formato del tercer grupo (3 octetos, el primero empieza por 1110, y los otros dos empiezan por 10) solo se deberá sustituir las X por el valor binario del código 20AC.

Este ejemplo era del tercer grupo, que ocupan 3Bytes, pero como podéis ver en la primera tabla, los caracteres que están entre U+0000 y U+007F se codifican con un solo byte, en el que el primer bit está puesto a 0. Pero si os fijáis bien, ¿qué caracteres son esos? ¡¡Efectivamente!! Son los de la tabla ASCII. Es decir, hemos conseguido que los caracteres de la tabla ASCII y los codificados en UTF-8 se vean exactamente igual (sus códigos numéricos son los mismos, y su representación binaria también). O hablando en plata: Un documento que solo contenga caracteres de la tabla ASCII es indistinguible si está codificado en ASCII o si lo está en UTF-8: son exactamente iguales a nivel binario.

A partir de aquí la historia no es tan bonita. Por ejemplo, nuestra querida ñ tiene el código numérico U+00F1. Como veis, este código está en el segundo conjunto de los mostrados anteriormente, por lo que ocupará 2B. Y ya deja de ser compatible con ISO-8859-1 (extensión de ASCII para Europa occidental, recordad), por lo que si abrimos con UTF-8 un documento en Español que fue codificado con ISO-8859-1, se nos mostrarán los tan temidos caracteres extraños.

Y ya si nos vamos a caracteres más altos en la tabla Unicode podemos ver como empiezan a ocupar bastante más, hasta poder llegar a la friolera de 6Bytes. Pero aun así, esta codificación ha demostrado ser la más óptima a la hora de ahorrar espacio. La mayor parte de la información que se codifica actualmente está contenida en la tabla ASCII o en las siguientes posiciones, por lo que en la mayor parte de los casos esta codificación siempre ocupará lo mismo, o menos que sus hermanas mayores.

Y sí, por las razones vistas anteriormente, esta es la codificación Unicode más usada en la actualidad.

¿Y la ordenación de bytes?

En toda esta explicación hemos ignorado totalmente la ordenación de bytes de la arquitectura del computador que codifica el texto. Y eso es porque la solución a este problema se resuelve de manera similar en todos los casos.

Es un sistema muy de andar por casa, al que han llamado BOM (nosotros y nuestras dichosas siglas), o dicho de otra manera: Byte Order Mark. Este consiste, simplemente, en añadir una firma al principio del documento que le indica al lector (sea el Word, vim o nuestro navegador web preferido) que está leyendo un texto codificado en UTF, y con que ordenamiento de bytes se lo va a encontrar.

Tan simple como eso. En el caso de UTF-16 y UTF-32 el BOM está compuesto por los bytes U+FEFF, por lo que si el lector se encuentra esos bytes al principio del documento, sabrá que se encuentra ante un texto escrito en esa codificación. Que esté en big-endian o little-endian lo podrá saber mirando el orden en el que está escrito el BOM ya que si está en big-endian leerá U+FEFF; mientras que si está en little-endian leerá U+FFFE.

En el caso de UTF-8 se desaconseja totalmente el uso de BOM por varias razones. Para empezar, porque al ser una codificación de formato fijo (aunque tenga ancho variable) no es necesario. Por último, porque como vimos anteriormente, el hecho de que un documento en ASCII y uno en UTF-8 puedan ser totalmente indistingibles (siempre que use solo los primeros 127 caracteres de la tabla ASCII) es una ventaja enorme en cuanto a retrocompatibilidad, ya que todos esos documentos se podrán seguir leyendo sin ningún tipo de cambio en el software. Si metiéramos un BOM al principio del documento, dejaría de ser binariamente igual a su vertiente en ASCII.

Conclusión

Y hasta aquí llega esta serie de artículos. Espero que os haya quedado bastante más claro las distintas formas de codificar documentos que existen, y los puntos fuertes y débiles de cada sistema.

El futuro sin lugar a dudas es Unicode, y UTF-8 se está llevando por méritos propios la copa a la codificación más usada. Es cierto que por ejemplo en nuestro idioma los textos ocupan más (a la hora de usar acentos o la ñ por ejemplo), pero el precio a pagar es muy pequeño (sobretodo comparado con UTF-16 y UTF-32) a cambio de las ventajas de poder usar en todo el mundo el mismo sistema de codificación de caracteres.

Comentarios