Nivel 1: bits y bytes
El ordenador solo tiene 0 y 1. Un bit es un interruptor; 8 bits forman un byte y con ellos se representan 28=256 valores distintos. Sumar los pesos de cada posición convierte el binario en decimal: 01000001 = 64 + 1 = 65. Todo texto es, al final, una cadena de bytes; una «codificación de caracteres» es una tabla que dice qué número representa a qué carácter, y cada tabla distinta es una codificación distinta.
Niveles 2–3: ASCII
El ASCII de 1963 definió 128 caracteres con 7 bits: de 0 a 31 son caracteres de control como el salto de línea, el tabulador o el timbre; de 48 a 57 están las cifras 0–9; de 65 a 90, las mayúsculas; y de 97 a 122, las minúsculas. La tabla no está puesta al azar: mayúsculas y minúsculas solo se diferencian en 32 (el sexto bit), así que c | 0x20 convierte una mayúscula en minúscula, y a un carácter de cifra le basta restarle 48 para obtener el valor. El hexadecimal es la forma habitual de leer bytes, dos dígitos por byte: 0x48 0x69 = Hi. ASCII solo usa 7 bits y deja el octavo libre; cada país lo fue rellenando a su manera (Latin-1, GBK, Shift_JIS…), de forma incompatible entre sí: ahí está la raíz histórica del mojibake.
Nivel 4: puntos de código Unicode
Unicode asigna un número único a los caracteres de todo el mundo, el punto de código, escrito como U+ más hexadecimal: A es U+0041, 中 es U+4E2D y 😀 es U+1F600. El espacio va de U+0000 a U+10FFFF y se divide en 17 «planos»; los caracteres habituales están casi todos en el plano básico (U+0000–U+FFFF), y los emoji y los ideogramas poco frecuentes, en los planos suplementarios. La clave: el punto de código es solo un número; en qué bytes se convierte lo deciden las «formas de codificación» UTF-8, UTF-16 o UTF-32. Unicode no es una codificación: utf-8 sí.
Niveles 5–8: las plantillas de longitud variable de UTF-8
UTF-8 (RFC 3629) representa un punto de código con 1 a 4 bytes y todas las reglas van escritas en el prefijo del primer byte:
| Rango de puntos de código |
Bytes |
Plantilla |
Bits de carga |
| U+0000–U+007F |
1 |
0xxxxxxx |
7 |
| U+0080–U+07FF |
2 |
110xxxxx 10xxxxxx |
11 |
| U+0800–U+FFFF |
3 |
1110xxxx 10xxxxxx 10xxxxxx |
16 |
| U+10000–U+10FFFF |
4 |
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
21 |
Se rellena con ceros a la izquierda el binario del punto de código hasta el número de bits de carga y se van rellenando las x en orden: eso da los bytes. é = U+00E9 = 11101001; hasta 11 bits queda 00011 101001, y al meterlo en la plantilla de dos bytes sale 11000011 10101001 = C3 A9.
Este diseño tiene tres ventajas: el texto ASCII no cambia ni un byte al codificarse en UTF-8 (compatibilidad total hacia atrás); cualquier byte siguiente empieza por 10, así que desde cualquier punto del flujo se puede encontrar el inicio del carácter siguiente (autosincronía: perder un byte no descoloca todo lo demás); y las codificaciones de distinta longitud no se solapan, de modo que un byte cero como 00 siempre representa U+0000 (seguridad para las cadenas de C). El precio es que cada carácter chino ocupa 3 bytes, un 50 % más que los 2 de GBK; pero el inglés, los signos de puntuación y el código no se ven afectados, y por eso UTF-8 es hoy la codificación de más del 98 % de las páginas web.
Los niveles 7 y 8 piden introducir a mano un carácter de 3 bytes y otro de 4. Este último revela una trampa que sigue muy viva: el conjunto de caracteres utf8 de MySQL solo llega a 3 bytes, así que guardar un emoji da error o se trunca; hay que usar utf8mb4.
Nivel 9: UTF-16 y .length
Las cadenas de JavaScript, Java, C# y la API de Windows usan UTF-16 por dentro: cada unidad de código son 16 bits. Un carácter del plano básico ocupa una unidad; un punto de código del plano suplementario se parte en dos unidades, lo que se llama par subrogado, en los rangos D800–DBFF y DC00–DFFF. Por eso "😀".length === 2, "😀".split("") lo corta en dos mitades sin sentido y str[0] devuelve el subrogado alto. Para contar caracteres reales, usa [...str].length (itera por puntos de código); para contar «una letra» tal como la percibe la persona (emoji compuestos, signos diacríticos), usa Intl.Segmenter.
Niveles 10–11: Base64
Base64 (RFC 4648) no es cifrado, sino una forma de transportar cualquier byte convertido en 64 caracteres ASCII imprimibles (A–Z a–z 0–9 + /) para meter binario donde solo se acepta texto: archivos adjuntos de correo, campos JSON, URL data:, JWT. Se toman 3 bytes = 24 bits, se cortan en 4 trozos de 6 bits (26=64) y cada trozo se busca en la tabla: Man = 4D 61 6E = 01001101 01100001 01101110 → 010011 010110 000101 101110 → T W F u. El tamaño se hincha, por tanto, a 4/3 del original.
Cuando el número de bytes no es múltiplo de 3, el último grupo queda incompleto: si falta 1 byte se añade un =, y si faltan 2, dos =, para que la salida mida siempre un múltiplo de 4. A es un byte de 8 bits que se completa con ceros hasta 12: 010000 01(0000) → QQ, más ==. La variante URL-safe cambia + / por - _ (para no tener que escaparlos en una URL) y suele omitir el =; al decodificar se rellenan según la longitud.
Nivel 12: codificación por ciento de las URL
En una URL solo puede aparecer tal cual una pequeña parte de los caracteres ASCII (RFC 3986 §2). Los caracteres reservados con significado sintáctico —espacio, &, =, ?, /— y todos los que no son ASCII deben escribirse como % + el hexadecimal del byte. Por eso la codificación URL es primero UTF-8 y después un % por byte: 中 → E4 B8 AD → %E4%B8%AD, y un solo carácter chino se convierte en 9 caracteres. En JavaScript, encodeURIComponent es la que más codifica (incluso / ? & =), ideal para meter valores en un parámetro, mientras que encodeURI conserva los caracteres estructurales y sirve para una dirección completa. El application/x-www-form-urlencoded de los formularios escribe además el espacio como +: es una convención histórica de los formularios HTML, no forma parte de RFC 3986.
Niveles 13–14: de dónde sale el mojibake y cómo rescatarlo
El mojibake no es corrupción de datos, sino escribir con la codificación A y leer con la codificación B. El 中 de UTF-8 es E4 B8 AD:
- Leído como Windows-1252 / Latin-1 (un byte, un carácter), los tres bytes se convierten en
ä¸, tres letras de Europa occidental: una ristra de letras con diacríticos es la señal de «UTF-8 leído como codificación occidental».
- Leído como GBK (dos bytes, un carácter chino),
E4 B8 es el carácter chino 涓 y sobra un AD suelto: «caracteres chinos raros + medio carácter» es la señal de «UTF-8 leído como GBK», y tanto 浣犲ソ como 涓枃 están en ese grupo.
- Al contrario, cuando un texto GBK se lee como UTF-8, las secuencias de bytes inválidas producen un montón de caracteres de reemplazo
� (U+FFFD).
Mientras no se hayan perdido los bytes originales, basta con reinterpretar con la codificación correcta y el texto queda intacto. Por eso la primera regla para arreglar mojibake es conservar el archivo original y no convertir una y otra vez: cada conversión puede sustituir bytes inválidos por ? o �, y eso sí es pérdida real de información. El nivel 14 propone hacer el camino inverso: coger el texto ilegible 浣犲ソ, devolverlo a los bytes E4 BD A0 E5 A5 BD según GBK y leerlo como UTF-8 para obtener 你好. La forma moderna de evitarlo es declarar UTF-8 en todas partes: cabeceras HTTP, <meta charset> del HTML, conexión de la base de datos y diálogo de guardado del editor, para que el desajuste no tenga ocasión de ocurrir.
BOM y orden de bytes
UTF-16 tiene orden de bytes big-endian y little-endian, y al principio del archivo suele llevar una marca de orden de bytes (BOM, FE FF o FF FE). UTF-8 no tiene ese problema, pero el Bloc de notas de Windows escribía por defecto el BOM UTF-8 EF BB BF, y eso hacía que fallara la primera línea de un script de shell o que delante del primer nombre de columna de un CSV apareciera un carácter invisible. Si en la vista de desglose UTF-8 del curso ves esos tres bytes al principio, ya sabes de dónde salen.