Todo sobre Unicode (en realidad lo mínimo indispensable)
4 Jun 2019 22 minsTraducción del articulo original de Joel Spolsky, co-fundador de Trello y Fog Creek Software y CEO de StackOverflow. Es un texto que ya tiene más de 15 años, pero todavía sigue sorprendiendome la ignoracia que existe en este tema por mucho programadores, incluído yo mismo.
Lo mínimo indispensable que todo desarrollador de software debería, absoluta y positivamente saber sobre Unicode y los conjuntos de caracteres (sin excusas!)
¿Se han preguntado acerca de esa misteriosa etiqueta Content-Type
? ya saben, la que se supone se debe indicar siempre en todo archivo HTML y nunca se sabe bien con que completarla?
¿Alguna vez recibió un correo electrónico de amigos de Bulgaria con el asunto “???? ?????? ??? ????”
?
He estado consternado al descubrir que muchos desarrolladores de software no están completamente al día en el misterioso mundo de los juegos de caracteres, codificaciones, Unicode, y todas esas cosas. Hace un par de años, un “Betatester” para FogBUGZ se preguntaba si podría gestionar el correo electrónico entrante en japonés. ¿Japonés? Tienen correo electrónico en japonés? No tenía ni idea. Cuando miré de cerca el control ActiveX que estábamos usando para analizar los mensajes de correo electrónico MIME, descubrimos que estaba haciendo justamente lo incorrecto con los juegos de caracteres, así que tuvimos que escribir un código “salvador” para deshacer la mala conversión que hacía y rehacerla correctamente. Cuando miré en otra biblioteca comercial, ésta también tenía una implementación de código de caracteres completamente errónea. Me contacté con el desarrollador de ese paquete y él pensaba que “no podían hacer nada al respecto”. Como muchos programadores, sólo deseaba que el problema desapareciera de alguna manera.
Pero no lo hará. Cuando descubrí que el popular lenguaje de desarrollo web PHP tiene casi completa ignorancia de los problemas de codificación de caracteres, usando alegremente 8 bits para manejar los mismos, de modo que es casi imposible desarrollar buenas aplicaciones web internacionales con él, pensé: ya es suficiente.
Así que tengo un anuncio que hacer: si usted es un programador que trabaja en 2003 y que no sabe lo básico de caracteres, juegos de caracteres, codificaciones y Unicode, y te llego a atrapar, como castigo: seis meses de pelar cebollas en un submarino. Juro que lo haré.
Y una cosa más:
NO ES TAN DIFICIL.
En este artículo voy a explicar exactamente lo que cada programador debería saber. Toda esa idea de “texto plano = ASCII = los caracteres son de 8 bits” no sólo es errónea, está irremediablemente mal, y si todavía estás programando de esa manera, no sos mucho mejor que un médico que no cree en gérmenes. Por favor, no escribas otra línea de código hasta que termines de leer este artículo.
Antes de empezar, debo advertirle que si sos es una de esas pocas personas que sabe de internacionalización, va a encontrar toda esta discusión un poco simplificada. Realmente estoy tratando de establecer un piso mínimo para que todos entiendan lo que está pasando y puedan escribir código con la esperanza de poder trabajar con texto en cualquier idioma que no sea solo el subconjunto de Inglés que no incluye palabras con acentos. Y te advierto que el manejo de caracteres es sólo una pequeña porción de lo que se necesita para crear un software que trabaja a nivel internacional, pero sólo puedo escribir sobre una cosa a la vez por lo que hoy es conjuntos de caracteres.
Una perspectiva histórica
La forma más fácil de entender esto es ir en orden cronológico.
Probablemente crees que voy a hablar de juegos de caracteres muy antiguos como EBCDIC. Bueno, no lo haré. EBCDIC no es relevante para tu vida hoy. No tenemos que retroceder tanto en el tiempo.
Allá por los tiempos de la semimodernidad, cuando se inventó Unix y K&R escribían “El lenguaje de programación en C”, todo era muy sencillo. El EBCDIC estaba a punto de salir. Los únicos caracteres que importaban eran las viejas letras inglesas no acentuadas, y teníamos un código para ellas llamado ASCII que era capaz de representar cada carácter usando un número entre 32 y 127. El espacio era 32, la letra “A” era 65, etc. Esto se puede almacenar cómodamente en 7 bits. La mayoría de las computadoras en aquellos días usaban octetos de 8 bits, por lo que no sólo se podía almacenar todos los posibles caracteres ASCII, sino que también se tenía un poco de “espacio” más, lo cual, si eras malvado, podías usar para tus propios propósitos retorcidos: las bombillas tenues de WordStar en realidad encendían el bit alto para indicar la última letra en una palabra, condenando a WordStar al texto en inglés solamente. Los códigos por debajo de 32 se llamaban no imprimibles y se usaban para maldecir. Sólo bromeaba. Se utilizaron para caracteres de control, como el 7 que emitía un pitido en el ordenador y 12 que provocaba que la página de papel actual saliera volando de la impresora y que se introdujera una nueva.
Y todo era perfecto, mientras tu lengua materna fuera el Inglés.
Debido a que los bytes tienen espacio para hasta ocho bits, mucha gente se puso a pensar, “Dios, podemos usar los códigos 128-255 para nuestros propios propósitos”. El problema fue que mucha gente tenía esta idea al mismo tiempo, y ellos tenían sus propias ideas de lo que debería ir, a dónde ir, en el espacio entre los caracteres 128 y 255. La IBM-PC tenía algo que llegó a conocerse como el juego de caracteres OEM que proporcionaba algunos caracteres acentuados para los idiomas europeos y un montón de caracteres de dibujo de línea, barras horizontales, barras verticales, barras horizontales, con pequeños dingle-dangles colgando del lado derecho, etc., y podía utilizar estos caracteres de dibujo de línea para hacer cajas y líneas en la pantalla, que todavía se pueden ver corriendo en el ordenador 8088 en su tintorería. De hecho, tan pronto como la gente comenzó a comprar PCs fuera de América, se inventaron todo tipo de juegos de caracteres OEM diferentes, que utilizaron los 128 caracteres principales para sus propios propósitos. Por ejemplo, en algunos PCs el código de carácter 130 se mostraría como é, pero en los ordenadores vendidos en Israel era la letra hebrea Gimel, por lo que cuando los estadounidenses enviaban su currículum vitae a Israel llegaban como sumas. En muchos casos, como el ruso, había muchas ideas diferentes sobre qué hacer con los 128 caracteres superiores, por lo que ni siquiera se podía intercambiar documentos rusos de forma fiable.
Finalmente, este juego de caracteres OEM “free-for-all” fue codificado en el estándar ANSI. En el estándar ANSI, todos estaban de acuerdo en qué hacer por debajo del 128, lo que era más o menos lo mismo que el ASCII, pero había muchas maneras diferentes de manejar los caracters a partir del 128 en adelante, dependiendo del lugar donde vivías. Estos diferentes sistemas fueron llamados páginas de código (code pages). Así, por ejemplo, en Israel, el DOS utilizó una página de código llamada 862, mientras que los usuarios griegos utilizaron la
- Eran los mismos por debajo del caracter 128 pero diferentes del 128 en adelante, donde residían todas esas graciosas letras. Las versiones nacionales de MS-DOS tenían docenas de estas páginas de código, manejando todo desde inglés hasta islandés e incluso tenían algunas páginas de código “multilingües” que podían hacer esperanto y gallego en la misma computadora! Wow! Pero conseguir, digamos, hebreo y griego en la misma computadora era una imposibilidad completa a menos que escribieras tu propio programa personalizado que mostrara todo usando gráficos con mapas de bits, porque el hebreo y el griego requerían diferentes páginas de códigos con diferentes interpretaciones de los números altos.
Mientras tanto, en Asia, ocurrian cosas más descabelladas, tengamos en cuenta que los alfabetos asiáticos tienen miles de letras, que nunca iban a caber en 8 bits. Esto se resolvió generalmente por el desordenado sistema llamado DBCS, el “juego de caracteres de doble byte” en el que algunas letras se almacenaban en un byte y otras tomaban dos. Era fácil avanzar en una cadena (string), pero casi imposible retroceder. Se animó a los programadores a que no usaran contadores como s++ y s- para moverse hacia atrás y hacia delante, sino para llamar a funciones como AnsiNext y AnsiPrev de Windows, que sabían cómo manejar todo el desorden.
Pero aun así, la mayor parte de la gente fingió que un byte era un carácter y un carácter eran 8 bits, mientras nunca se moviera una cadena de una computadora a otra, o se tuviera que “hablar” más de un idioma, esta idea siempre funcionaría. Pero por supuesto, tan pronto como apareció Internet, se hizo bastante común mover las cadenas (strings) entre computadoras, y todo este desorden se vino abajo . Afortunadamente, para ese momento ya se había inventado Unicode.
Unicode
Unicode fue un esfuerzo valiente para crear un único juego de caracteres que incluyera todos los sistemas de escritura razonables del planeta y algunos de los que usan los que creen en la fantasía como Klingon, también. Algunas personas están bajo la idea equivocada de que Unicode es simplemente un código de 16 bits dónde cada carácter toma 16 bits y por lo tanto hay 65,536 caracteres posibles. Esto no es así. Es el mito más común acerca de Unicode, así que si pensaste eso, no te sientas mal.
De hecho, Unicode tiene una forma diferente de pensar los caracteres, y hay que entender bien esta forma, sino nada tendrá sentido.
Hasta ahora, hemos asumido que una letra se representa con unos bits que puedes almacenar en disco o en memoria:
A -> 0100 0001
En Unicode, una letra se representa con algo llamado “code point”, que todavía es sólo un concepto teórico. Cómo ese “code point” se representa en la memoria o en el disco es una historia más loca todavía.
En Unicode, la letra A es un ideal “platónico”. Está flotando en el cielo:
A
Esta A
“platónica” es diferente a la B
, y diferente a la a
, pero igual que la **A**
y la _A_
y la A
. La idea de que la letra A
en una fuente Times New Roman es el mismo carácter que la letra A
en una fuente Helvética, pero diferente de la letra a
en minúsculas, no parece algo muy controvertido, pero en algunos idiomas el mero hecho de averiguar qué es una letra, puede causar controversia. ¿Es la letra alemana ß
una letra real o sólo una forma elegante de escribir ss? Si la forma de una letra cambia al final de la palabra, ¿es una letra diferente? El hebreo dice que sí, el árabe que no. De todos modos, la gente inteligente del consorcio Unicode lo ha estado descubriendo durante la última década, acompañada por una gran cantidad de debate altamente político, y no tienes que preocuparte por ello. Ya se han dado cuenta de todo.
A cada letra “platónica”, de cada alfabeto, el consorcio Unicode le asigna un número mágico que se escribe: U+0639
. Este número mágico es el “code point”. El U+
significa “Unicode” y los números son hexadecimales. U+0639
es la letra árabe Ain. La letra A en inglés sería U+0041
. Puede encontrarlas todas usando la utilidad charmap
en Windows 2000/XP o visitando el sitio web de Unicode.
No hay límite real en el número de letras que Unicode puede definir y de hecho han superado los 65.536, así que no todas las letras de unicode pueden ser realmente comprimidas en dos bytes, pero eso era un mito de todos modos.
Bien, digamos que tenemos una cadena:
Hello
el cual, en Unicode, se corresponde con esto cinco “code points”:
U+0048 U+0065 U+006C U+006C U+006F
Sólo un montón de “code points”. Números, en verdad. Todavía no hemos dicho nada acerca de cómo guardar este en la memoria o representarlo en un mensaje de correo electrónico.
Codificaciones
Ahí es donde entran las codificaciones.
La idea inicial para la codificación Unicode, y que llevó al mito de los dos bytes, era: bueno, vamos a guardar esos números en dos bytes cada uno. Así convertimos Hello
en:
00 48 00 65 00 6C 00 00 6F 6C
¿Está bien, no? ¡No tan rapido! ¿Acaso no podría ser también?:
48 00 65 00 6C 00 6C 00 00 6F?
Bueno, técnicamente, sí, yo creo que sí se pude, y de hecho, las primeros implementaciones buscaban ser capaces de almacenar sus “code points” en modo “high-endian” o “low-endian”, esto para respetar la arquitectura de cualquier CPU particular. Y apenas arrancamos con esto, ya teníamos dos formas de almacenar Unicode, por los que las personas fueron obligadas a inventar la extraña convención de almacenar un FE FF
al principio de cada cadena Unicode; esto se llamó: marca de orden de bytes y si se tiene los bytes altos y bajos intercambiados, la marca entonces sería FF FE
y entonces la persona que lea esta cadena va a saber que tienen que dar vuelta todos los otros bytes que le lleguen. Ufff. No todas las cadenas Unicode tienen estas marcas de orden de bytes al principio.
Durante un tiempo pareció que eso sería suficiente, pero los programadores, en particular los estadounidenses, se quejaban. “Miren todos esos ceros”, dijeron, y claro, estaban mirando un texto en inglés que raramente usaba “code points” por encima de U+00FF
. También eran hippies liberales en California que querían ahorrarse los bytes. Si fueran tejanos no les habría importado engullir el doble de bytes. Pero esos californianos no podían soportar la idea de duplicar la cantidad de almacenamiento que se necesita para las cadenas, y de todos modos, ya existían todos estos documentos que utilizaban los juegos de caracteres ANSI y DBCS y ¿quién los va a convertir a todos? ¿Moi? Sólo por esta razón, la mayoría de la gente decidió ignorar Unicode durante varios años y mientras tanto las cosas empeoraron.
Así se inventó el brillante concepto de UTF-8. UTF-8 era otro sistema para almacenar los “code points” Unicode, esos mágicos números U+
, en memoria usando bytes de 8 bits. En UTF-8, cada punto de código de 0-127 se almacena en un solo byte. Sólo los puntos de código 128 y superiores se almacenan utilizando 2, 3, de hecho, hasta 6 bytes.
Esto tiene el efecto secundario, de que el texto en inglés se ve exactamente igual en UTF-8 que en ASCII, por lo que los estadounidenses no notaban nada extraño. Sólo el resto del mundo tuvo que pasar por el aro. Específicamente, Hello
, que era U+0048 U+0065 U+006C U+006C U+006C U+006F
, se almacenará como 48 65 6C 6C 6C 6F
, que es lo mismo que se almacenaba en ASCII, y ANSI, y en todos los juegos de caracteres OEM del planeta. Ahora, si eras tan atrevido como para usar letras acentuadas, griegas o klingon, tendrás que usar varios bytes para almacenar un único “code point”, pero los estadounidenses nunca se darán cuenta. (UTF-8 también tiene la agradable propiedad de que el ignorante código de procesamiento de cadenas que quiera usar un solo byte 0 como terminador nulo no truncará las cadenas).
Hasta ahora te comenté tres modos de codificación Unicode. La tienda-que-en-dos bytes tradicionales métodos son llamados UCS-2 (porque tiene dos bytes) o UTF-16 (ya que tiene 16 bits), y todavía tiene que averiguar si se trata de alta endian UCS- 2 o de bajo endian UCS-2. Y ahí está el nuevo y popular UTF-8 estándar que tiene la propiedad agradable de trabajo también respetable, si usted tiene la feliz coincidencia de texto Inglés y programas de descerebrados que son completamente conscientes de que no es que no sea nada ASCII.
Hasta ahora te comenté tres modos de codificar Unicode. Los métodos tradicionales de almacenamiento en dos bytes, se denominan UCS-2 (porque tiene dos bytes) o UTF-16 (porque tiene 16 bits), y todavía tiene que averiguar si se trata de UCS-2 “big endian” o de UCS-2 “low endian”. Y está el nuevo y popular estándar UTF-8, que tiene la agradable propiedad de funcionar también de manera respetable si se tiene la feliz coincidencia de texto en inglés y programas con muerte cerebral que no saben que hay algo más que ASCII.
En realidad, hay un montón de otras formas de codificación Unicode. Hay algo llamado UTF-7, que se parece mucho a UTF-8, pero garantiza que el bit alto siempre sea cero, de modo que si usted tiene que pasar Unicode a través de algún tipo de sistema de correo electrónico de estado policial draconiano que piensa que 7 bits son suficientes, gracias a esto todavía puede pasar sin problemas. Hay UCS-4, que almacena cada “code point” en 4 bytes, que tiene la propiedad agradable que cada “code point” se puede almacenar con el mismo número de bytes, pero, caramba, incluso los tejanos no sería tan osados como para perder tal cantidad de memoria.
Y de hecho, ahora que estás pensando las cosas en términos de letras platónicas que están representadas por “code points” Unicode, esos puntos de código Unicode también pueden ser codificados en cualquier esquema de codificación de la vieja escuela! Por ejemplo, podría codificar la cadena Unicode para Hello
(U+0048 U+0065 U+006C U+006C U+006C U+006F
) en ASCII, o la antigua codificación griega OEM, o la codificación hebrea ANSI, o cualquiera de los cientos de codificaciones que se han inventado hasta ahora, con una sola contra: ¡puede que algunas de las letras no aparezcan! Si no hay equivalente para el punto de código Unicode que estás intentando representar en la codificación en la que estás intentando representarlo, normalmente obtienes un pequeño signo de interrogación: ?
o, si eres realmente bueno, un rombo: �
Hay cientos de codificaciones tradicionales que sólo pueden almacenar algunos “code point” correctos y cambiar todos los demás en signos de interrogación. Algunas codificaciones populares de texto Inglés son Windows-1252 (el estándar de Windows 9x para los idiomas de Europa occidental) y la ISO-8859-1 , también conocida como Latin-1 (también útil para cualquier lengua europea occidental). Pero intenta almacenar letras rusas o hebreo en estas codificaciones y obtienes un montón de signos de interrogación. UTF-7, 8, 16, y 32 tienen todos la buena propiedad de ser capaz de almacenar cualquier punto de código correctamente.
El hecho más importante acerca de las codificaciones
Si olvidas completamente todo lo que acabo de explicar, por favor recuerda un hecho extremadamente importante. No tiene sentido tener una cadena sin saber qué codificación utiliza. Ya no se puede meter la cabeza en la arena y pretender que un texto “simple” es ASCII.
No existe la cosa que llamamos texto plano
Si tienes una cadena, en memoria, en un archivo o en un mensaje de correo electrónico, tienes que saber en qué codificación está o no puedes interpretarla o mostrarla a los usuarios correctamente.
Casi todos los estúpidos problemas de “mi sitio web parece un galimatías” o “no puede leer mis correos electrónicos cuando uso acentos” se reducen a un programador ingenuo que no entiende el simple hecho de que si no me dices si una cadena en particular está codificada usando UTF-8 o ASCII o ISO 8859-1 (Latín 1) o Windows 1252 (Europa Occidental), simplemente no puedes mostrarla correctamente o ni siquiera averiguar dónde termina. Hay más de cien codificaciones y por encima del código 127 del código, todas las apuestas pagan lo mismo.
¿Cómo conservamos esta información sobre el uso de la codificación de una cadena? Bueno, hay formas estándar de hacerlo. Para un mensaje de correo electrónico, se espera que éste tenga una cadena en el encabezado del formulario
Content-Type: text/plain; charset="UTF-8"
En el caso de una página web, la idea original era que el servidor devolviera un encabezado http similar a Content-Type
, junto con la propia página web, no en el HTML, sino como uno de los encabezados de respuesta que se envían antes de la página.
Pero esto causa problemas. Supongamos que se tiene un gran servidor web con muchos sitios y cientos de páginas de mucha gente en muchos idiomas diferentes y todo ello utilizando cualquier codificación que la copia de Microsoft FrontPage de cada uno, considera adecuada. El servidor web en sí mismo no sabría realmente en qué codificación esta escrito cada archivo, por lo que no podtía enviar el encabezado Content-Type
.
Sería conveniente si pudieras poner el Content-Type
del archivo HTML directamente en el propio archivo, usando algún tipo de etiqueta especial. Por supuesto que esto volvió locos a los puristas… ¡¿Cómo puedes leer el archivo HTML hasta que no sepas en qué codificación está?! Afortunadamente, casi todas las codificaciones de comunes hacen lo mismo con caracteres entre el 32 y el 127, por lo que siempre se puede llegar hasta aquí en la página HTML sin tener que empezar a usar letras graciosas:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
Pero esa metaetiqueta tiene que ser la primera cosa en la sección <head>
porque tan pronto como el navegador web la vea, va a dejar de analizar la página y empezará de nuevo a reinterpretar toda la página usando la codificación que se especificó.
¿Qué hacen los navegadores web si no encuentran ningún Content-Type
, ya sea en las cabeceras http o en la metaetiqueta? Internet Explorer hace algo bastante interesante: intenta adivinar qué idioma y codificación se utilizó, basándose en la frecuencia con la que aparecen ciertos bytes en un texto típico en codificaciones típicas de varios idiomas. Debido a que las antiguas páginas de código de 8 bits tendían a poner sus letras de nacionalidad en diferentes rangos entre 128 y 255, y debido a que cada idioma humano tiene un histograma característico diferente de uso de letras, esto realmente tiene una oportunidad de funcionar. Es realmente extraño, pero parece que funciona lo suficientemente bien a menudo, tal que los programadores ingenuos de páginas web que nunca supieron que necesitaban un encabezado de Content-Type
miran su página en un navegador web y parece que está bien, hasta que un día escriben algo que no se ajusta exactamente a la distribución de frecuencia de letras de su idioma nativo, e Internet Explorer decide que es coreano y lo muestra así, demostrando, creo, que el punto de que la Ley de Postel acerca de ser “conservador en lo que emites y liberal en lo que aceptas” francamente no es un buen principio de ingeniería. De todos modos, ¿qué hace el pobre lector de este sitio web, que fue escrito en búlgaro pero que parece ser coreano (y ni siquiera un coreano coherente)? Utiliza el menú “Ver | Codificación” e intenta un montón de codificaciones diferentes (hay al menos una docena para los idiomas de Europa del Este) hasta que la imagen aparece más clara. Claro, si supiera hacer esto, lo que la mayoría de la gente no hace.
Para la última versión de CityDesk, el software de gestión de sitios web publicado por mi empresa, decidimos hacer todo internamente en UCS-2 (dos bytes) Unicode, que es lo que Visual Basic, COM y Windows NT/2000/XP utilizan como su tipo de cadena nativo. En el código C++ sólo declaramos las cadenas como wchar_t
(“wide char”) en lugar de char
y usamos las funciones wcs
en lugar de las funciones str
(por ejemplo wcscat
y wcslen
en lugar de strcat
y strlen
). Para crear una cadena UCS-2 literal en código C sólo tienes que poner una L
delante de ella como tal: L"Hola"
.
Cuando CityDesk publica la página web, la convierte a codificación UTF-8, que ha sido bien soportada por los navegadores web durante muchos años. Esa es la forma en que están codificadas todas las versiones de Joel on Software en 29 idiomas y aún no he oído a ninguna persona que haya tenido problemas para ver estas páginas.
Este artículo se está haciendo bastante largo, y no puedo cubrir todo lo que hay que saber sobre codificaciones de caracteres y Unicode, pero espero que si has leído hasta aquí, sepas lo suficiente como para volver a la programación, usando antibióticos en lugar de sanguijuelas y hechizos, una tarea a la que te dejaré ahora.