Unicode y UTF-8: por qué «España» acaba como «España» y cómo se arregla

De los 128 códigos de ASCII y las codificaciones incompatibles a un único espacio Unicode; por qué un diseño trazado en un mantel de papel en 1992 se convirtió en la codificación de la web, y los errores de normalización y longitud que siguen apareciendo.

everytooleverywhereallatonce · Publicado

Quien haya recibido un correo con «España», «información» o «â€œcomillas※, o haya abierto un CSV en el que todos los apellidos con tilde se convirtieron en pares de símbolos raros, ya conoce el problema central del texto en los ordenadores. Un ordenador no guarda letras: guarda números, y es el programa que los lee quien decide qué letra corresponde a cada número. Si quien escribe y quien lee usan tablas distintas, el texto se rompe. Unicode y UTF-8 son el largo esfuerzo, bastante exitoso, por que todo el mundo use la misma tabla.

ASCII y sus 128 casillas

ASCII, normalizado en Estados Unidos en 1963, usaba siete bits: 128 códigos para letras inglesas, dígitos, puntuación y caracteres de control como el salto de línea. La A es 65 (41 en hexadecimal) y el salto de línea, 10 (0A). No había ñ, ni tildes, ni ¿ ni ¡. Hasta hoy, las primeras 128 posiciones de todas las codificaciones importantes coinciden con ASCII.

El lío llegó con el octavo bit. Un byte admite 256 valores y cada país o fabricante llenó las 128 posiciones superiores a su manera. Europa occidental adoptó ISO 8859-1 (Latin-1), con á, é, ñ, ü, ¿ y ¡. Microsoft distribuyó Windows-1252, casi igual pero con comillas tipográficas y, más tarde, el símbolo del euro en una zona que Latin-1 reserva para controles. Las antiguas páginas de códigos de MS-DOS (437, 850) colocaban la ñ en otra posición distinta, así que un archivo de texto de DOS abierto en Windows ya mostraba caracteres cambiados. Rusia, Grecia o Israel tenían sus propias tablas: el byte 0xF1 era ñ en Madrid y una letra cirílica en Moscú.

De dónde sale «España»

En UTF-8 la ñ ocupa dos bytes, C3 B1. Un programa que supone Windows-1252 lee C3 como à y B1 como ±. Por eso «España» se convierte en «España» y cada vocal acentuada se duplica en basura. Las comillas tipográficas “ ” ocupan tres bytes en UTF-8 (E2 80 9C y E2 80 9D), y leídas como Windows-1252 salen como «â€œ» y «â€». La buena noticia es que los bytes siguen intactos: si se vuelve a leer el archivo como UTF-8, el texto se recupera.

La mala noticia llega cuando un decodificador UTF-8 encuentra bytes que no puede interpretar —por ejemplo, un archivo Latin-1 leído como UTF-8— y los sustituye por U+FFFD, el carácter de reemplazo (�). En ese caso la información original ya se ha perdido y no hay conversión que la devuelva.

Los japoneses sufrieron tanto este fenómeno que su palabra para él, mojibake (文字化け, «transformación de caracteres»), pasó al inglés técnico.

Asia oriental: dos bytes y aun así no bastaba

El chino, el japonés y el coreano necesitaban miles de caracteres, así que sus codificaciones usaban dos bytes por carácter. Shift_JIS en Japón, EUC-KR en Corea y GB2312 en China eran incompatibles entre sí. En Shift_JIS, además, el segundo byte de un carácter podía ser 0x5C, la barra invertida de ASCII, y los programas en C confundían ese byte con un carácter de escape. Todavía hoy, en fuentes japonesas, la ruta C:\Users se ve como C:¥Users porque la norma japonesa colocó el yen en ese mismo byte.

Unicode: un número por carácter

A finales de los años ochenta, Joe Becker en Xerox y Lee Collins y Mark Davis en Apple empezaron a trabajar en un juego de caracteres universal. El Consorcio Unicode se constituyó en California el 3 de enero de 1991 y el primer volumen de The Unicode Standard 1.0 apareció en octubre de ese año.

La premisa fundacional era que 16 bits —65 536 puntos de código— bastarían para todas las escrituras en uso. Windows NT y Java se diseñaron sobre esa idea y guardan el texto en unidades de 16 bits. No fue suficiente: solo los ideogramas chinos históricos y poco frecuentes exigían decenas de miles de posiciones más. Unicode 2.0 (1996) amplió el espacio hasta U+10FFFF, algo más de 1,1 millones de puntos de código, e introdujo los pares sustitutos para que los sistemas de 16 bits sobrevivieran: el bloque reservado U+D800–DFFF se usa por parejas para representar cualquier carácter por encima de U+FFFF. Por eso “😀”.length vale 2 en JavaScript y en Java: cuentan unidades de 16 bits, no caracteres.

Unicode define solo la correspondencia entre carácter y número. Cómo se escribe ese número en bytes lo decide una forma de codificación: UTF-8, UTF-16 o UTF-32.

UTF-8: diseñado sobre un mantel

En septiembre de 1992, a Ken Thompson y Rob Pike, de Bell Labs, les pidieron opinión sobre FSS-UTF, una codificación que X/Open iba a votar. Pike objetó que le faltaba una propiedad que él consideraba imprescindible: poder empezar a leer un flujo de bytes por la mitad y reencontrar el límite del siguiente carácter perdiendo menos de un carácter. Esa noche, cenando en un diner de Nueva Jersey, Thompson ideó una mejor disposición de los bits sobre el mantel de papel. Según el relato de Pike, Thompson escribió el codificador y el decodificador esa misma noche, la propuesta se envió a X/Open el 8 de septiembre y al final de esa semana su sistema operativo, Plan 9, funcionaba ya por completo con lo que acabaría llamándose UTF-8.

El esquema cabe en una tabla:

Rango de puntos de código Patrón de bytes Ejemplo
U+0000–007F 0xxxxxxx A → 41
U+0080–07FF 110xxxxx 10xxxxxx ñ → C3 B1
U+0800–FFFF 1110xxxx 10xxxxxx 10xxxxxx € → E2 82 AC
U+10000–10FFFF 11110xxx y tres 10xxxxxx 😀 → F0 9F 98 80

El diseño original admitía hasta seis bytes para cubrir 31 bits. El RFC 3629 (2003) lo redujo a cuatro para ajustarse al techo de Unicode, U+10FFFF, y prohibió codificar en UTF-8 los puntos de código sustitutos. En el visor de bytes UTF-8 se puede ver el patrón aplicado carácter a carácter.

Por qué ganó UTF-8

UTF-16 llegó antes a los sistemas operativos y a los lenguajes, pero UTF-8 se quedó con los archivos, los protocolos y la web. Las razones están en el propio diseño:

  • Compatible con ASCII. Cualquier archivo ASCII ya es UTF-8 válido, byte a byte. No hubo que convertir nada.
  • Los bytes menores que 0x80 son siempre ASCII. Todos los bytes de un carácter multibyte valen 0x80 o más, de modo que las herramientas que buscan /, saltos de línea o NUL nunca parten un carácter. El problema de la barra invertida de Shift_JIS no puede ocurrir.
  • Autosincronizable. Los bytes iniciales empiezan por 11 y los de continuación por 10. Si se empieza a leer en cualquier punto, o se corrompe un byte, en el siguiente carácter todo vuelve a cuadrar.
  • Sin orden de bytes. UTF-16 debe elegir entre big-endian y little-endian y a menudo necesita una marca de orden de bytes (BOM). UTF-8 es una secuencia de bytes; no hay nada que invertir.
  • Ordenar bytes equivale a ordenar puntos de código.

Para el español el coste es mínimo: las letras sin tilde siguen ocupando un byte y las acentuadas pasan de uno (en Latin-1) a dos. Para el chino, el japonés o el coreano, los caracteres que ocupaban dos bytes pasan a tres. Aun así, la compatibilidad se impuso. El Encoding Standard de WHATWG, que siguen los navegadores, establece hoy que los protocolos y formatos nuevos «deben usar exclusivamente la codificación UTF-8».

Mismo texto, bytes distintos: la normalización

Unicode permite a menudo escribir el mismo texto visible de más de una manera. La ñ puede ser el punto de código U+00F1 o una n (U+006E) seguida de TILDE COMBINANTE (U+0303); la á puede ser U+00E1 o a + U+0301.

El Anexo 15 del estándar Unicode define cuatro formas de normalización para conciliarlas:

  • NFC compone siempre que puede. Es lo que producen los teclados y casi todo el contenido web.
  • NFD lo descompone todo.
  • NFKC y NFKD además pliegan los caracteres de «compatibilidad»: ① pasa a 1, fi a fi, la A de ancho completo a A. Los usan buscadores y comprobaciones de nombres de usuario.

Donde más se nota es en los nombres de archivo. El antiguo sistema de archivos HFS+ de Apple guardaba los nombres descompuestos, y los archivos creados en un Mac siguen llegando a menudo con nombres descompuestos dentro de ZIP o en subidas web. Buscar «Peñalver» escrito con el teclado (NFC) no encontrará «Peñalver.pdf» con el nombre en NFD, aunque se vean idénticos, a menos que alguien normalice antes: en JavaScript, str.normalize(“NFC”).

No existe una única «longitud»

Cuántos caracteres tiene una cadena depende de qué se cuente. Tomemos el emoji de familia 👨‍👩‍👧:

Unidad Cantidad Dónde aparece
Bytes UTF-8 18 Tamaño de archivo, límites en bytes de bases de datos, red
Unidades UTF-16 8 .length de JavaScript y Java
Puntos de código 5 len() de Python, Array.from
Grafemas 1 Lo que ve una persona; movimiento del cursor

El emoji es hombre + UNIÓN DE ANCHO CERO (ZWJ, U+200D) + mujer + ZWJ + niña; de ahí los cinco puntos de código. Lo que un lector percibe como un carácter lo define el Anexo 29 de Unicode como «clúster de grafemas extendido», y los navegadores lo exponen mediante Intl.Segmenter. El contador de palabras lo usa para contar caracteres como lo haría una persona. Cuando un formulario, un SMS y una red social no se ponen de acuerdo sobre la longitud de un texto, casi siempre cuentan filas distintas de esta tabla.

Errores que siguen llegando a producción

  • Emojis rechazados por MySQL. El antiguo juego utf8 (utf8mb3) almacena como máximo tres bytes por carácter, así que los emojis de cuatro bytes fallan o se truncan. Use utf8mb4.
  • Truncar por bytes. Cortar una cadena UTF-8 en un byte arbitrario puede separar la C3 de la B1 de una ñ; el byte suelto se convierte en �.
  • Medio par sustituto. substring() en JavaScript puede partir un emoji por la mitad y dejar \uD83D solo. El conversor de escapes Unicode hace visible el huérfano.
  • Excel y el CSV en UTF-8. Al abrir con doble clic un CSV en UTF-8, Excel a menudo supone otra codificación y aparecen las «Ã±». Guardarlo con marca de orden de bytes (EF BB BF) al principio suele resolverlo. Esa misma BOM al principio de un JSON o de un script de shell provoca errores de análisis.
  • HTML escapado dos veces. Ver & en una página significa que el texto se escapó al guardarlo y otra vez al mostrarlo; el codificador de entidades HTML explica qué caracteres hay que escapar y dónde.
  • Caracteres que se parecen. El guion (U+002D), la raya (U+2014) y el signo menos (U+2212), o el indicador ordinal º y el signo de grado °, son distintos para cualquier programa. El selector de símbolos muestra el punto de código de lo que elige.

Cómo diagnosticar texto corrupto

  1. Fíjese en el patrón. Secuencias como ñ, é o “ indican bytes UTF-8 leídos como Latin-1 o Windows-1252. Los bytes siguen ahí: vuelva a abrir el archivo como UTF-8.
  2. Si ve �, un decodificador ya reemplazó los bytes. El texto original se perdió y hay que recuperarlo de la fuente.
  3. Si el texto se ve bien pero no coincide, sospeche de la normalización (NFC frente a NFD), de un carácter parecido o de un espacio invisible como U+00A0 o U+200B. Pegue ambas cadenas en un visor de bytes y compare las filas.

Fuentes