Charsets y Encodings
Juegos de caracteres y codificaciones.
Es un tema que puede ser confuso y lioso. Este es mi intento de aclarar términos y explicarlo de extremo a extremo.
Introducción y conceptos
El RFC2278 define Coded Character Set (CCS) como un mapeo entre un conjunto de caracteres abstractos y un conjunto de enteros. Es decir un CCS no es mas que una tabla que asocia glifos a números.
Ese mismo RFC define Character Encoding Scheme (CES) como una manera de transformar los enteros de un CCS en una serie de octetos.
Y en realidad con estos dos conceptos tenemos resueltos los dos problemas de representar cualquier caracter:
- una tabla que relaciona caracteres con números
- una manera de que si esos números son mayores que un octeto (255) los transformemos en octetos, y de octetos al número
Ejemplos de CCS: ASCII, Unicode, ISO-10646, ISO-8859, etc.
Ejemplos de CES: UTF-8, UCS-2, UCS-4, etc.
CCSs: ISO-10646 y Unicode
El ISO-10646 definió el Universal Character Set (UCS). Es un CCS que mapea glifos a enteros de 32 bits (4 bytes).
Unicode fue un esfuerzo paralelo. A partir de Unicode 2.0 se unifican. A día de hoy son 100% compatibles.
Lo que pretenden ambos era unificar y sustituir a los viejos e insuficientes CCSs como ASCII (7 bits) y sus extensiones locales de 8 bits, los ISO-8859-x.
Estas extensiones locales, como ISO-8859-1 (también llamado Latin1), definen los caracteres del 129 al 255. Este [enlace explica estupendamente estos ISO.
En resumen, es sorprendente saber cuantos años hemos ido tirando con 127 caracteres (ASCII), y luego con el empujon de 127 más (varios ISO-8859-x), hasta que por fin tenemos un estandar que cubre la representación de cualquier glifo posible (Unicode o ISO-10646).
Encodings: UTF-8
Pero aquí surge el problema, los ordenadores y las redes, por su arquitectura, están acostumbrados a manejar octetos, las representaciones de información de más de 8 bits les dan problemas así que necesitan métodos fiables de transformar enteros mayores de 255 a octetos.
Eso es lo que hace UTF-8 (UCS Transformation Format), que es un CES que transforma los enteros del Unicode (un CCS) en secuencias de bytes.
Ojo, aqui suele venir la confusión, a veces se entiende UTF-8 como una tabla de caracteres (CCS), y no lo es, es un Encoding (CES). El error es comprensible porque el UTF-8 implica usar Unicode, que si es una tabla de caracteres.
UTF-8 no es el único CES, hay otros como UTF-12 o UCS-2.
Ejemplos
Estos 12 glifos:
ÁÉÍÓÚÑáéíóúñ
En el CCS ISO-8859-1 serían:
00000000 c1 c9 cd d3 da d1 e1 e9 ed f3 fa f1 |............|
Nótese como son todos mayores de 127 (caen fuera de ASCII), pero menores que 255. Como se trata de un CCS de un solo byte no necesita encoding, se almacena tal cual, ocupan 12 bytes.
Algo similar obtenemos con:
$ echo -n Ññ | iconv -t ISO-8859-1 -f UTF-8 -t ISO-8859-1 | od --endian=big -x 0000000 d1f1
Explicación: imprimimos (sin salto de carro) los caracteres Ñ y ñ. Como estamos en un Linux moderno (Debian 8) el terminal ya trabaja en UTF-8, así que convertimos a un juego de caracteres de 8 bits y luego representamos el resultado en hexadecimal. Resultado, los índices de la ñ y la Ñ en la tabla de caracteres ISO-8859-1.
Si vieramos un fichero escrito en UTF-8 con esos mismos 12 glifos, veríamos esto:
00000000 c3 81 c3 89 c3 8d c3 93 c3 9a c3 91 c3 a1 c3 a9 |................| 00000010 c3 ad c3 b3 c3 ba c3 b1 |........| 00000018
Aquí vemos con un solo glifo (Á) da lugar a más de 1 byte (C3 81). Por tanto la representación de estos 12 glifos en UTF-8 ocupa 24 bytes.
Es lo mismo que obtenemos si hacemos:
$ echo -n Ññ | od --endian=big -x 0000000 c391 c3b1
Por último como ejemplo de otras codificaciones veamos lo que ocurre si usamos UCS-2 y UCS-4:
$ echo -n Ññ | iconv -f UTF-8 -t UCS-2 | od --endian=big -x 0000000 d100 f100
$ echo -n Ññ | iconv -f UTF-8 -t UCS-4 | od --endian=big -x 0000000 0000 00d1 0000 00f1
Estas son codificaciones muy triviales que para los números bajos se limitan a añadir ceros hasta completar los 2 o 4 bytes.
Lenguajes de programación
Desde el punto de vista de los lenguajes y como representan internamente Unicode hay dos aproximaciones. Una es usar cadenas de enteros de 4 bytes (wchar[]) de ese modo se almacena un caracter Unicode por cada wchar sin ninguna transformación. Otra es usar cadenas de bytes (char[]) y guardar la transformación UTF-8 de los caracteres Unicode.