UTF-8 es un diseño brillante
(iamvishnu.com)- UTF-8 es un método de codificación de longitud variable que representa millones de caracteres mientras mantiene compatibilidad retroactiva con ASCII
- La misma región de 7 bits que ASCII (
U+0000~U+007F) usa 1 byte sin cambios, por lo que un archivo ASCII es también un archivo UTF-8 válido - Los demás caracteres se representan con secuencias de 2 a 4 bytes; el patrón de bits del byte inicial define la longitud y los bytes siguientes comienzan con
10para indicar que son bytes de continuación - Gracias a este diseño, UTF-8 puede manejar un conjunto universal de caracteres y a la vez ser perfectamente compatible con los sistemas ASCII existentes, por eso se convirtió en la codificación de caracteres más usada
- Otras codificaciones Unicode como UTF-16 y UTF-32 no ofrecen esta compatibilidad con ASCII
La excelencia del diseño de UTF-8
- Cuando uno conoce UTF-8 por primera vez, impresiona mucho su estructura compatible con ASCII existente mientras integra en un solo sistema millones de caracteres de distintos idiomas y escrituras
- En esencia, UTF-8 aprovecha hasta 32 bits, pero ASCII solo usa 7 bits
- Los principios de diseño de UTF-8 son los siguientes
- Todo archivo codificado en ASCII es un archivo UTF-8 válido
- Todo archivo UTF-8 que solo contenga caracteres ASCII es un archivo ASCII válido
- La idea de fusionar un sistema antiguo limitado a apenas 128 caracteres con otro que abarca millones de caracteres fue realmente innovadora
Concepto básico de UTF-8
- UTF-8 es una codificación de caracteres de longitud variable (variable-width encoding) diseñada para representar todos los caracteres del conjunto Unicode
- Codifica cada carácter con 1 a 4 bytes
- Los primeros 128 caracteres (
U+0000~U+007F) se almacenan en un solo byte, lo que asegura compatibilidad retroactiva con ASCII - Los demás caracteres se codifican en dos, tres o cuatro bytes
- Los bits iniciales del primer byte determinan el número total de bytes necesarios para la codificación
| Patrón de 1 byte | Cantidad de bytes | Patrón de la secuencia completa de bytes |
|---|---|---|
| 0xxxxxxx | 1 | 0xxxxxxx (ASCII común) |
| 110xxxxx | 2 | 110xxxxx 10xxxxxx |
| 1110xxxx | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
| 11110xxx | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
- El 2.º, 3.º y 4.º byte de una secuencia multibyte siempre comienzan con
10, lo que marca claramente que son bytes de continuación - Al combinar los bits restantes del byte principal y de los bytes de continuación, se forma un code point
- Un code point es un identificador único de un carácter Unicode, representado con el prefijo "U+" y en hexadecimal
- Ejemplo: el code point de "A" es
U+0041
- El flujo para interpretar un carácter a partir de bytes codificados en UTF-8 es el siguiente
- 1. Se lee un byte y, si empieza con 0, se considera un carácter de un solo byte (ASCII); se usa el resto de los 7 bits para representar el carácter y se pasa al siguiente byte
- 2. Si no empieza con 0, entonces
- si empieza con 110, es un carácter de 2 bytes y se lee 1 byte adicional
- si empieza con 1110, es un carácter de 3 bytes y se leen los siguientes 2 bytes
- si empieza con 11110, es un carácter de 4 bytes y se leen 3 bytes adicionales
- 3. De los bytes determinados, se combinan los bits restantes excluyendo los bits iniciales y se usan como valor binario del code point
- 4. Se busca el code point en el conjunto Unicode y se muestra en pantalla
- 5. Se repite con el siguiente byte
Ejemplo: el carácter hindi "अ"
- Representación en UTF-8:
11100000 10100100 10000101(3 bytes) - El primer byte (
11100000) → indica que es un carácter de 3 bytes - Combinación de los bits válidos de los tres bytes →
00001001 00000101= hexadecimal0x0905 - El code point
U+0905corresponde al carácter devanagari "अ"
Ejemplos de archivos
-
1.
Hey👋 Buddy- Consta de 13 bytes en total
- Caracteres ASCII (H, e, y, B, u, d, d, y, espacio) → 1 byte cada uno
- 👋 (U+1F44B) → 4 bytes
11110000 10011111 10010001 10001011
- Este archivo es un archivo UTF-8 válido, pero como incluye un carácter no ASCII (emoji), no es compatible retroactivamente con ASCII
- Consta de 13 bytes en total
-
2.
Hey Buddy- Tiene 9 bytes en total, todos dentro del rango ASCII
- Por lo tanto, este archivo es a la vez un archivo ASCII válido y un archivo UTF-8 válido
Comparación con otras codificaciones
- Existen algunas codificaciones que ofrecen compatibilidad con ASCII, pero ninguna se usa tanto como UTF-8
- GB18030 (estándar chino), por ejemplo, también ofrece compatibilidad con ASCII, pero no tiene un uso tan extendido
- La familia ISO/IEC 8859 es una extensión de un solo byte (máximo 256 caracteres), así que tiene limitaciones
- UTF-16/UTF-32 no tienen compatibilidad con ASCII
- 'A' (U+0041): en UTF-16 es
00 41, en UTF-32 es00 00 00 41
- 'A' (U+0041): en UTF-16 es
Bonus: UTF-8 Playground
- Herramienta interactiva para explorar visualmente el proceso de codificación en UTF-8
- https://utf8-playground.netlify.app/
1 comentarios
Opiniones de Hacker News
En UTF-8, como los bytes de continuación siempre empiezan con
10, incluso si saltas a un byte arbitrario puedes verificar de inmediato si esa posición es el inicio de un carácter o un byte de continuación, así que es fácil encontrar el inicio del siguiente o del anterior carácter. Si se codificara como los enteros de longitud variable de EBML (con inversión de1/0para mantener compatibilidad ASCII de un solo byte), sería difícil saber enseguida desde una posición arbitraria dónde empieza un carácter. Para más detalles, ver RFC8794 section 4.4Sí, esa es una gran ventaja de UTF-8. Puedes moverte libremente hacia adelante y hacia atrás en una cadena UTF-8 sin tener que leerla desde el principio. En Python, para permitir indexar cadenas por carácter, CPython usa wide characters. En algún momento se podía elegir entre caracteres de 2 o 4 bytes, y después se hacía el cambio automáticamente en tiempo de ejecución. Pero sigue siendo wide character, no UTF-8. Por ejemplo, con un solo emoji el tamaño de la cadena puede cuadruplicarse. Yo más bien había pensado en usar UTF-8 internamente y hacer que el tipo de índice fuera un objeto opaco, implementando suma o resta de pequeños enteros para moverse hacia adelante o atrás dentro de la cadena. Solo al convertirlo a entero o usar subscripting directo se calcularía el índice de la cadena. Con ese enfoque, cosas como expresiones regulares también podrían aprovechar objetos de índice opacos y funcionar bien sobre la representación UTF-8
Creo que LEB128/VLQ es mejor que el esquema de enteros variables de EBML. Se distingue con el MSB dentro del byte:
0significa fin de secuencia y el siguiente byte inicia una nueva;1significa retroceder hasta encontrar un MSB0. Incluso hay implementaciones eficientes optimizadas con SIMD. La diferencia entre LEB128 y VLQ es solo el endianness. ASCII sería0xxxxxxx, los caracteres extendidos1xxxxxxx 0xxxxxxx,1xxxxxxx 1xxxxxxx 0xxxxxxx, etc.; en 3 bytes se puede codificar hasta0x1FFFFF, más de lo que necesita Unicode. No es self-synchronizing, pero es más compacto. ASCII seguiría siendo de 1 byte, y code points por debajo de U+3FFF, como símbolos matemáticos o japonés, podrían representarse en 2 bytes, lo que ayuda a reducir el tamaño del códigoYo diría que eso solo es posible bajo la suposición de que el texto no esté corrupto ni haya sido manipulado maliciosamente. Han aparecido muchísimas vulnerabilidades de seguridad al parsear o escapar secuencias UTF-8 inválidas. Algunos ejemplos: el problema CVE-2025-1094 de PostgreSQL, y también la lista de CVE relacionados con UTF-8
No es estrictamente cierto. Con UTF-8 inválido, un carácter puede cambiarse por un byte de continuación. Por ejemplo, si entra
0b01100001 0b10000000 0b01100001, salen tres caracteres:a�a. Para saber si un carácter mostrado empieza en cierto punto, hay que mirar los 1 a 3 bytes anterioresSi el tamaño máximo multibyte es de 4 bytes, basta con revisar hasta 3 bytes hacia atrás para saber si la posición actual es un byte de continuación. Si no aparece un byte inicial, sabes que es un carácter de un solo byte. Sospecho que esto se diseñó así con fines de recuperación: aunque una librería no reconozca correctamente UTF-8, puede ignorar los bytes inválidos al inicio y final de un slice recortado y aun así extraer una cadena razonable
Me parece que UTF-8 es realmente excelente. La clave está en la decisión de que ASCII usara solo 7 bits. Incluso en 1963 eso era una elección algo curiosa. Me pregunto si fue puro accidente histórico, o si quienes diseñaron ASCII consideraron usar un bit extra para añadir más símbolos, o si ya estaban pensando en code pages o extensibilidad
No sé la razón exacta, pero antes no siempre se disponía de 8 bits completos. Era común usar 7 bits + 1 bit de paridad o de bandera (por eso el e-mail todavía codifica 8 bits usando solo 7 con quoted-printable). A la capacidad de transmitir los 8 bits tal cual se le llama 8-bit clean. En ese contexto, UTF-8 termina siendo también un buen ejemplo de cómo aprovechar ese octavo bit sobrante de ASCII. Como referencia, también está esta explicación de 8-bit clean
No soy experto, pero hace tiempo leí sobre la historia de ASCII. ASCII viene del código de teletipo, que a su vez evolucionó desde códigos telegráficos. El código Morse tenía longitud variable, así que era incómodo de implementar mecánicamente. Por eso apareció el código Baudot de 5 bits. Era un código de longitud fija para simplificar las máquinas y también para reducir el cansancio del operador. Por el código Baudot todavía hoy se usa “baud” como símbolo de tasa. Luego, con la entrada por cinta perforada usando máquinas de escribir, se ganó más flexibilidad y se añadieron símbolos especiales como Carriage Return y Line Feed. La industria temprana de las computadoras adoptó tarjetas perforadas como entrada, e IBM desarrolló un nuevo esquema de 8 bits para procesarlas más rápido, basado en ASCII. Al final, el código binario se fue expandiendo conforme avanzó la tecnología. ASCII también fue un producto transicional, anterior a la costumbre del byte de 8 bits
En realidad, el bit sobrante se reutilizaba para paridad
Las extensiones de 8 bits de ASCII (tipo ISO 8859-x) se usaron muchísimo durante décadas y todavía siguen presentes en code pages estándar de Windows. Incluso si ASCII hubiera sido de 8 bits desde el principio, los caracteres clave igual habrían quedado concentrados en los primeros 128, así que creo que UTF-8 también habría encajado bien. Si hubo un accidente histórico, no fue que ASCII fuera de 7 bits, sino que el desarrollo informático de la época ocurrió principalmente en el mundo angloparlante, donde el inglés podía representarse cómodamente con 7 bits
El hecho de que fuera de 7 bits no era tan raro. Baudot era de 5 bits, luego aparecieron códigos de 6 bits porque eso ya no bastaba, y después llegó ASCII de 7 bits. IBM estandarizó el byte de 8 bits con System/360 (código EBCDIC), pero otros fabricantes de computadoras no tenían un tamaño fijo de byte. Aunque 7 bits parezca extraño, en ese momento no era necesario que los caracteres y las palabras del sistema encajaran limpiamente entre sí
Coincido en que UTF-8 tiene un diseño mejor de lo esperado. Pero Unicode tiene el problema de que su alcance se ha vuelto demasiado amplio. Termina apareciendo la pregunta de qué debería estar incluido en Unicode. Intuitivamente uno pensaría en “todos los caracteres impresos distinguibles que usa la humanidad para comunicarse”, pero en la práctica no es así.
La distinción no es clara. Hay code points que existen para combinación
No es concreto. Un mismo carácter puede escribirse de varias maneras. Incluso letras que se ven iguales tienen code points y significados distintos
No todo es imprimible. Existen caracteres de control. Se incluyeron por compatibilidad ASCII, pero también siguen apareciendo caracteres de control propios Parece que todavía no existen puntos Unicode animados. Al menos lo imprimible puede plasmarse en papel. Pero no sé si esa invariancia vaya a mantenerse en el futuro. Por cierto, entre las codificaciones UTF que el autor no mencionó también está UTF-7. Es parecida a UTF-8, pero se creó bajo la suposición de que en los entornos de red de los 80 no era seguro usar el último bit. Una vez recibí por accidente un correo codificado en UTF-7. Todavía no sé cómo lo enviaron
UTF-7 se creó principalmente para entornos de transmisión no 8-bit clean, como el correo electrónico. Hoy está obsoleta y tampoco puede codificar planos suplementarios salvo mediante pares sustitutos de UTF-16. También existe UTF-9, pero es una parodia presentada en un RFC del Día de los Inocentes (para entornos de 36 bits como PDP-10)
Siempre me he preguntado algo: que un code point Unicode pueda codificarse también como una secuencia de bytes innecesariamente larga. UTF-8 lo prohíbe y solo permite la secuencia más corta. Por ejemplo,
00000001y11000000 10000001representarían lo mismo. Entonces, ¿no habría sido posible diseñarlo de otra manera para que ni siquiera existieran codificaciones inválidas? Por ejemplo, haciendo que el inicio de una secuencia de 2 bytes representara el último valor válido, de modo que11000000 10000001fuera128+1, y0-127se tratara como 1 byte. Así no habría códigos inválidos y, en los casos límite, las cadenas serían un poco más cortas. Me pregunto si simplemente no se consideró por el costo del hardware de la época. (Actualización: la secuencia de bits correcta debería ser10000001, ya quedó corregido)c2 80y noc0 80(el primero después de7f). Creo que la razón es la siguiente a) Si se permitieran overlong encodings, eso abriría huecos de seguridad cuando alguna parte solo verificara secuencias cortas b) La codificación/decodificación UTF-8 estándar puede hacerse solo con masking (bitmask) y shifting (bitshift). El esquema propuesto requeriría además una resta En una discusión por e-mail de 1992 se habló de esto, y FSS-UTF sí incluye additive constants (ver abajo)La clave es mantener la self-synchronicity de los patrones de bytes. Si no se conservaran bytes de continuación como en
11000000 10000001, se perdería la propiedad de poder encontrar siempre los límites de code points en un stream UTF-8 truncado. Si además añades operaciones de suma/resta, el rendimiento del decoder cae. Hoy todo puede procesarse solo con operaciones de bitsComo comentó quectophoton, los bytes de continuación deben empezar siempre con
10para que el parser pueda encontrar los límites de code points desde cualquier punto. De hecho, eso se tuvo muy en cuenta en el diseño de UTF-8 a inicios de los 90, cuando todavía había muchos entornos de transmisión poco confiablesCon el enfoque propuesto, el cálculo de codificación/decodificación se vuelve más complejo y lento. Hoy bastan unos cuantos bit shifts, pero en esa época (los 90), con computadoras lentas, eso importaba mucho
Si quieres leer más sobre el diseño de UTF-8, vale la pena revisar el one-pager de Russ Cox y el resumen histórico de Rob Pike
UTF-8 es excelente y ojalá se usara en todos lados (te estoy viendo, JavaScript). Pero su único defecto es que el estándar no define con claridad cómo interpretar secuencias de bytes inválidas. Creo que habría sido aún más perfecto si el diseño “especificara obligatoriamente una forma de interpretar toda secuencia de bytes”. Pienso que algo como la especificación de HTML5 podría funcionar exitosamente
Tengo una relación de amor y odio con la compatibilidad hacia atrás. No me gusta lo confuso, pero sí me cae bien la idea de avanzar aunque implique romper algo. Al mismo tiempo, me resultan muy agradables casos como UTF-8 o EAN, donde se mantuvo compatibilidad y aun así el diseño fue inteligente. Sinceramente, UTF-8 da la impresión de no haber sacrificado casi nada por la compatibilidad
Si hubiera que cambiar algo, probablemente habría reemplazado algunos caracteres de control por caracteres más comunes para ahorrar aunque fuera un poco de espacio (asumiendo incluso que también se rompiera compatibilidad con Unicode). Como formato de codificación multibyte, incluso visto de forma independiente, me parece casi óptimo
Me gustó mucho el enlace al playground de UTF-8 (utf8-playground.netlify.app). Estaría bien que la UI permitiera introducir code points directamente (por ahora parecía posible solo por URL). (Actualización: ya se puede, porque el PR ya fue mergeado)
Si quieres profundizar en este tema y te gusta el estilo Advent of Code, en i18n-puzzles hay varios acertijos sobre codificación de texto. Ayudan a internalizar completamente cómo funcionan UTF-8 y UTF-16, entre otros
Gracias, fue un buen artículo. Yo también recomiendo UTF-8, pero creo que solo es realmente bueno si se usa siempre junto con BOM. De lo contrario, la aplicación no puede saber que es UTF-8 y se le puede pasar por alto que también debe guardarse como UTF-8. Por ejemplo, si en Windows creas un nuevo documento de texto y, cuando el archivo está vacío, solo tiene BOM, cualquier app podrá reconocer automáticamente después, al editarlo o guardarlo, que debe guardarse en UTF-8. Sin BOM, aunque la app intente detectar la codificación automáticamente, no puede hacerlo con total fiabilidad, y la confusión crece en cuanto agregas caracteres especiales como acentos (el editor puede adivinar mal el idioma, o Notepad puede cambiar la codificación predeterminada tras una actualización). Así que sí, estoy de acuerdo con usar UTF-8, pero BOM debería ser obligatoriamente la configuración predeterminada del sistema operativo y las apps