8 de Junio de 2010

Todo programador que se enfrente habitualmente a cadenas de texto (es decir, todos excepto quizás los que se dedican al cálculo numérico) deberían conocer al menos los conceptos generales relativos a la codificación de caracteres. Desafortunadamente, esto no siempre es así, y en los cursos de formación, incluso avanzados, estos temas se ignoran sistemáticamente. En este artículo me propongo desentrañar los conceptos básicos. En otro posterior me centraré con más detalle en los problemas prácticos que estos plantean.

Introducción

Para empezar, para evitar confusiones, ahí van unas cuantas definiciones:

  • Carácter (character) (el plural es caracteres, no carácteres, que yo sepa uno de los dos únicos plurales irregulares en español): Es la unidad mínima de un alfabeto. Qué es exactamente un carácter es un tema abierto al debate: con alfabetos sencillos como el nuestro, es algo relativamente obvio, pero hay otros mucho más complejos por el mundo adelante. Aun con el alfabeto latino caben dudas: ¿á es un carácter o dos? ¿Y Æ?
  • Byte: Es la unidad mínima de memoria de un ordenador con dirección propia. Un bit es más pequeño, claro, pero no tiene dirección. En todos los ordenadores modernos un byte equivale a 8 bits, por lo que su contenido va de 0 a 255 (o de -128 a 127 si se usa el signo).
  • Tabla o conjunto de caracteres (character set): Una tabla en la que se asignan valores numéricos a un conjunto de caracteres.
  • Página de códigos (codepage): Sinónimo de tabla de caracteres utilizado sobre todo por IBM y Microsoft.
  • Code point: (punto de código) cada una de las casillas de una tabla de caracteres. Un code point es un número y un carácter asociado.
  • Codificación (encoding): El algoritmo con el que se almacenan en la memoria del ordenador los valores numéricos de los code points.
  • Glifo (glyph): Gráfico con el que se representa un determinado carácter en pantalla o impresora. Un mismo carácter puede tener varios glifos asociados según la fuente que se utilice.

NOTA #1: Históricamente se ha abusado mucho de estos términos y se han utilizado de muchas forms distintas. Por esos estas definiciones pueden ser discutibles en muchos casos. He escogido estas porque me parece la forma más útil de separar los conceptos implicados.

NOTA #2: ¿Qué hace la definición de byte en todo eso? La he añadido para evitar la confusión del tipo de C: en contra de la creencia popular, el tipo char no representa un carácter sino un byte. El hecho de que su nombre sugiera character es un accidente histórico desafortunado que provoca incontables confusiones.

El pasado

En los orígenes de la informática (1940-1960), cuando un ordenador necesitaba manejar texto, simplemente se le asignaba un valor numérico a cada uno de los caracteres requeridos y luego se guardaban estos en memoria directamente, normalmente un byte por carácter. En el manual del sistema se incluía un dibujo de la tabla de caracteres. Al principio, que cada fabricante, o incluso cada máquina, tuviese una tabla de caracteres diferente no tenía demasiada importancia, pues los ordenadores se inventaron para manejar números, no texto, pero con el tiempo llegó a ser bastante engorroso. Así que se creó una tabla estándar: la tabla ASCII. Lentamente, todos los fabricantes abandonaron sus códigos privados y se convirtieron al ASCII (bueno, excepto IBM que tenía su propia tabla llamada EBCDIC y se resistió al cambio).

La tabla ASCII contiene 128 code points numerados del 0 al 127 (0x00 a 0x7F en hexadecimal). Es por lo tanto una tabla de 7 bits. Contiene el alfabeto latino, sín acentos ni marcas diacríticas: 26 letras mayúsculas y 26 minúsculas; más los 10 dígitos numéricos, 33 códigos de control, 32 signos de puntuación y el espacio: 26 + 26 + 10 + 33 + 32 + 1 = 128.

La tabla ASCII es la siguiente:

HEX0-1-2-3-4-5-6-7-
-0NULDLE 0@P`p
-1SOHDC1!1AQaq
-2STXDC2"2BRbr
-3ETXDC3#3CScs
-4EOTDC4$4DTdt
-5ENQNAK%5EUeu
-6ACKSYN&6FVfv
-7BELETB'7GWgw
-8BSCAN(8HXhx
-9HTEM)9IYiy
-ALFSUB*:JZjz
-BVTESC+;K[k{
-CFFFS,<L\l|
-DCRGS-=M]m}
-ESORS.>N^n~
-FSIUS/?Oo_DEL

Los code points del 0 al 31 y el 127 son los códigos de control. La mayoría no se utilizan, y sus nombres son de interés puramente arqueológico. Las excepciones son:

  • LF (Line Feed): Avance de línea. Se usa para separar líneas en los ficheros de texto. Se corresponde con la tecla INTRO y con el carácter '\n' de C.
  • CR (Carriage Return): Retorno de carro. El el carácter '\r' en C. Se usa en algunos sistemas operativos junto a LF en los ficheros de texto.
  • NUL (Null): Nulo. Es el carácter nulo. En C es el que se utiliza para marcar el final de una cadena de texto y puede escribirse '\0'.
  • BS (Backspace): Retroceso. Es la tecla de borrar. En C es '\b'.
  • HT (Horizontal Tab): Tabulador horizontal. En C es '\t'.
  • VT (Vertical Tab): Tabulador vertical. En C es '\v'.
  • BEL (Bell): Campana. Produce un pitido, o no, según dónde se escriba. En C es '\a' (de alert, seguramente).
  • FF (Form Feed): Avance de página.
  • DEL (Delete): Suprimir.
  • EOT (End Of Transmission): Fin de transmisión.
  • ESC (Escape): Este código se usa para muchas cosas, pero el uso original era para iniciar una secuencia de control multibyte.

La mayoría de los teclados tienen una o dos teclas etiquetadas Ctrl. Estas se utilizan tradicionalmente para introducir manualmente estos caracteres de control. La idea es que se pulsa Ctrl más la tecla 4 columnas a la derecha según la tabla de arriba. En pantalla normalmente se representa el Ctrl con un acento circunflejo ^. Así por ejemplo LF (avance de línea) equivale a Ctrl+J (^J); CR (retorno de carro) a Ctrl+M (^M), ESC es Ctrl+[ (^[), BS (Backspace) es Ctrl+H (^H), etc. ¿Te suenan? Pruébalas, muchas de estas combinaciones aún funcionan en sistemas modernos de ventanas.

NOTA: El truco de Ctrl más 4 columnas a la derecha de la tabla es estupendo con el teclado americano. Pero con el español hay serias dificultades con 4 o 5 códigos.

Como la tabla ASCII es de solo 128 code points, pero los ordenadores utilizan bytes de 8 bits cada uno, es decir 256 posibilidades, si codificamos un carácter por byte nos quedan 128 posibles caracteres no definidos. Los fabricantes de ordenadores aprovecharon esto para proporcionar más caracteres extra y solventar (en parte) la carencia del ASCII de caracteres internacionales. Nuevamente, a falta de un estándar en este sentido, cada fabricante creó sus propias tablas de lo que algunos llamaban ASCII extendido. En realidad no existe tal cosa, pues cada tabla extendida tiene su propio nombre. Sí son tablas compatibles con ASCII, pues son un superconjunto de aquél. Son notables entre las tablas compatibles con ASCII las creadas por IBM, entre otras cosas, porque son las que utilizaba el IBM-PC original, y por lo tanto MS-DOS y después (aún hoy en día) las ventanas de comandos de MS-Windows. IBM las llamaba code pages y las identificaba por las letras CP seguidas de un número de 3 dígitos. Así la codepage por defecto del IBM-PC original era la CP437, aunque el MS-DOS en español solía cambiarla a CP850. En otros idiomas se usaban otras codepages, hasta unas 20 diferentes. Si tienes interés puedes consultar estas tablas en muchos sitios de Internet, pero mi favorito es, como no, la Wikipedia. Fíjate que en la mayoría de las codepages solo se enumeran los caracteres del 128 al 255, pues los otros son idénticos a los ASCII.

El presente

Si esto se quedara aquí sería demasiado sencillo, así que llegó la ISO y creó un estándar con, no una, sino 15 tablas de caracteres diferentes compatibles con ASCII. Este es el famoso estándar ISO-8859, y las tablas aquí codificadas se llaman ISO-8859-1 a ISO-8859-16 (menos la ISO-8859-12, que no existe, ¿no es estupendo el diseño por comité?). Para simplificar, las tablas de la ISO tienen nombres alternativos fáciles de recordar. Por ejemplo ISO-8859-1 es latin-1, ISO-8859-5 es latin/cyrillic, etc. En inglés se usa habitualmente el ISO-8859-1 y en español se usan solamente ISO-8859-1 (latin-1) y ISO-8859-15 (latin-10), que es una versión actualizada del anterior con el signo de Euro (€) y otros pequeños cambios.

Si esto fuera todo sería solo ligeramente complicado, pero luego llegó Microsoft con su sistema MS-Windows. En un entorno gráfico no se necesitan muchos de los caracteres de las codepages de IBM (caracteres semigráficos, para dibujar tablas y cuadritos), por lo que se pueden aprovechar estos code points para representar más caracteres útiles. Pero en lugar de decidirse por utilizar el estándar ISO-8859, Microsoft decidió crear sus propias tablas de caracteres, algunas muy parecidas a las de la ISO, pero no iguales. Para identificarlas se utiliza la antigua notación CP pero con un número de 4 dígitos. Así, en los MS-Windows de Europa Occidental y América (español, inglés, portugués, francés, alemán, etc.) se utiliza la codepage CP1252, también llamada Windows-1252, que se parece mucho, mucho a la ISO-8859-1, pero no del todo. Igualmente, en Europa del Este se usa la CP1250, que se parece mucho a la ISO-8859-2.

Además de eso, algunos alfabetos no latinos necesitan más de 128 caracteres, así que no pueden utilizar una tabla compatible ASCII. Estos utilizan una variedad de codificaciones, más o menos complejas, en las que no quiero profundizar. Baste decir que muchas de ellas utilizan dos bytes por carácter (DBCS: Double Byte Character Set), o un número variable de bytes (MBCS: Multiple Byte Character Set), o codificaciones con estado interno, etc.

El futuro

He llamado a este apartado futuro para añadirle un poco de dramatismo, porque lo cierto es que el futuro ya está aquí: se llama Unicode. Hartos de tantos estándares y tantas tablas diferentes, los expertos del mundo se reunieron y decidieron construir la tabla de códigos definitiva (vale, no fue exactamente así, pero los detalles históricos son lo de menos). Pero claro, si vamos a crear una única tabla de caracteres universal no podemos limitarnos a 256 code points, hay que pensar a lo grande. Así que crearon una tabla de 16 bits, lo que da hasta 65536 posibles caracteres. ¿Quién va a necesitar nunca más que eso? La respuesta llegó pronto: China, Japón y Corea necesitan ellos solos más de ¡70000 caracteres! Así que Unicode se extendió a 21 bits, con previsión para extenderlo más, hasta 32 bits si fuese necesario. Actualmente están asignados 107361 caracteres. Vale la pena señalar que los primeros 256 code points de Unicode son idénticos a los de ISO-8859-1 (así que los primeros 128 son los ASCII).

Un codepoint Unicode suele representarse en hexadecimal, con al menos 4 dígitos, escribiendo delante U+. Así, el signo de Euro (€), por ejemplo, es U+20AC.

Con una tabla de caracteres tan grande se debe tener en cuenta un concepto que he pasado por alto hasta ahora: la codificación. Recordemos que la codificación es la forma en la que un code point se representa en la memoria del ordenador (o en disco, o en la red). Con una tabla de 8 bits (o 7), la codificación es obvia y trivial: un code point es un byte. Pero con una tabla de caracteres de 21 bits, hay que pensárselo un poquito más. El estándar Unicode define varias codificaciones...

UTF-32

La forma más obvia es utilizar un número entero de 32 bits (podrían usarse 24, pero sería una pesadilla de alineaciones y redondeos). Esta codificación se llama UTF-32 (Unicode Transformation Format, 32 bits) y sin duda funciona, pero se despedicia mucho espacio, sobre todo si tu alfabeto es pequeño, como el nuestro (¡4 bytes por carácter!). Además, si se usa para grabar un fichero o enviar datos por la red se debe tener en cuenta el orden de los bytes dentro del carácter (endianness), si los textos se quedan en la memoria local este detalle no tiene importancia. Así que tenemos en realidad dos variantes: UTF-32LE (little endian) y UTF-32BE (big endian).

UTF-16

Si la mayoría de los caracteres que vamos a codificar están en los primeros 65536 caracteres, entonces pueden representarse solamente con 16 bits. Estos caracteres forman el llamado BMP (Basic Multilingual Plane). Para codificar los codepoints de U+10000 en adelante se usan dos números de 16 bits, llamados surrogate pair (par sustituto). Estos dos sustitutos utilizan dos rangos del BMP reservados para la ocasión: el primero de U+D800 a U+DBFF y el segundo de U+DC00 a U+DFFF. 10 bits por cada codepoint sustituto suman 20, justo los necesarios para el resto de la tabla Unicode. Al igual que con UTF-32 el orden de bytes puede ser significativo, por lo que se indican UTF-16LE y UTF-16BE.

UTF-8

Esta es una codificación de 8 bits. Naturalmente, con 256 posibles valores no tenemos ni para empezar, así que se crea un ingenioso esquema de codificación multibyte, por el que un codepoint puede utilizar desde 1 hasta 4 bytes. Escribí antes que es una codificación de 8 bits porque, aunque un codepoint puede ocupar varios bytes, son secuencias de bytes, no words, por lo que el endianness es irrelevante. Los detalles de la codificación UTF-8 están diseñados con cuidado para que se cumplan las siguientes propiedades:

  • Compatibilidad ASCII: Los codepoints en el rango U+0000 a U+007F se representan con un único byte, el mismo que en ASCII. Además estos bytes no aparecen nunca como parte de un carácter multibyte. Esto implica que un texto que solo contenga caracteres ASCII es automáticamente un texto UTF-8 válido.
  • Auto-sincronización: El primer byte de un carácter multibyte debe estar en el rango 0xC0-0xFF, y los siguientes deben estar en el rango 0x80-0xBF. Por lo tanto si se recibe un fragmento de texto, cortado de cualquier manera, se puede encontrar el comienzo del siguiente carácter multibyte de forma inmediata.
  • Verificable: La codificación UTF-8 tiene una forma estricta, de manera que es muy improbable que una secuencia de bytes aleatorios (o provinientes de otra codificación) sean confundidos con un texto válido. Es decir, si un texto razonablemente largo pasa los filtros de UTF-8 es casi seguro que utiliza esta codificación. El filtro se basa nuevamente en rangos: un carácter unibyte está en el rango 0x00-0x7F, mientras que en un carácter multibyte el primer byte:

    • 0xC2-0xDF: Es un carácter de 2 bytes, de U+0080 a U+07FF.
    • 0xE0-0xEF: Es un carácter de 3 bytes, de U+0800 a U+FFFF.
    • 0xF0-0xF4: Es un carácter de 4 bytes, de U+10000 a U+10FFFF.
    • 0x80-0xC1 o 0xF5-0xFF: Error.

Y después los bytes siguientes deben estar en el rango 0x80-0xBF.

De estas propiedades, la más importante es la primera, pues permite seguir utilizando las mismas APIs y herramientas que existían cuando las codificaciones clásicas de 8 bits con pocos cambios (o incluso ninguno).

Otras codificaciones Unicode

Otras codificaciones menos populares o menos conocidas de Unicode son:

  • UCS-4: Es la misma que UTF-32. Este nombre (Universal Character Set) existe por razones históricas.
  • UCS-2: Es parecida a UTF-16, pero sin pares sustitutos. Por lo tanto solo puede codificar de U+0000 a U+FFFF (el BMP).
  • UTF-7: Una codificación obsoleta y no muy conocida, que solo utiliza los bytes del 0x00 al 0x7F.
  • CESU-8 (Compatibility Encoding Scheme for UTF-16 in 8 bits, o algo así): Esta codificación es un apaño, una variante de UTF-8 por la que los codepoints de U+10000 en adelante se codifican primero en UTF-16, como dos pares sustitutos de 16 bits, y luego, estos dos codepoints se codifican en UTF-8, como 3 bytes cada uno. La codificación UTF-8 directa sería de 4 bytes, y no 6, pero la conversión entre UTF-16 y CESU-8 es bastante más sencilla que entre UTF-16 y UTF-8. De hecho el algoritmo es idéntico a de conversión entre UCS-2 y UTF-8, lo cual me hace pensar que esta codificación se inventó a posteriori, después de observar que varios programas populares se comportaban así.

Conclusión

Hasta aquí he descrito la teoría de las codificaciones de caracteres, creo que es importante conocer todo esto. Pero, en la vida real ¿cómo nos afecta todo esto? Eso, me parece, es un tema para otro artículo...

2 comments to Páginas de código y codificaciones: ASCII, Unicode y otros

  1. Gracias, muy interesante icon_smile.
    (Yo ahora ya no tengo mucho tiempo para publicar icon_razz)
    Saludos.

  2. Ya he visto, ya, que no actualizas mucho...
    Yo intento escribir al menos uno a la semana, para no perder el hábito.

Help
:-(icon_sad :-)icon_smile :roll:icon_rolleyes
:-Dicon_biggrin :-Picon_razz :oops:icon_redface
:-xicon_mad :-|icon_neutral :arrow:icon_arrow
8-)icon_cool 8-Oicon_eek :mrgreen:icon_mrgreen
:!:icon_exclaim :?:icon_question :twisted:icon_twisted
;-)icon_wink :evil:icon_evil :idea:icon_idea
:-oicon_surprised :cry:icon_cry
:-?icon_confused :lol:icon_lol

Leave a comment