domingo, 27 de marzo de 2011

El Mínimo Absoluto que Todos los Desarrolladores Deberían Conocer Sobre Unicode y Codificaciones.

Este artículo pretende ser una traducción más o menos fiel [1] de el artículo de Joel Spolsky. Me ha gustado tanto que me he decidido a traducirlo para hacerlo accesible a todos aquellos que no dominan el inglés, o que prefieren leer en castellano. Es un magnífico texto que todavía hoy sigue vigente. Te recomiendo la lectura, ya seas programador, administrador de sistemas, o simplemente un curioso de la informática.


----------------------------------------------------------------------------------------------

El Mínimo Absoluto que Todos los Desarrolladores Deberían Conocer Sobre Unicode y Codificaciones.

Miércoles, 08 de Octubre de 2003 by Joel Spolsky

Alguna vez te has preguntado acerca de la etiqueta misteriosa "Content-Type"? Ya sabes, la que se supone que debes poner en el HTML y que nunca has sabido realmente que poner?

Alguna vez has recibido un correo de tus amigos de Bulgaria con un asunto igual a "???? ??? ?? ????? ??" ?

Me he molestado al descubrir que muchos desarrolladores no saben realmente como funciona el misterioso mundo de las codificaciones de caracteres, Unicode, etc... Unos años atrás un beta tester de FogBUGZ se estaba preguntando si podría recibir correos en Japonés. Japonés? Ellos escriben correos en Japonés? No tenía ni idea. Cuando miré mas de cerca el componente comercial que estábamos desarrollando en ActiveX para parsear las cabeceras MIME de los e-mails, nos dimos cuenta de que lo estábamos haciendo mal con las codificaciones de caracteres, por lo que tuvimos que reescribir el código de conversión. Cuando miré el código de otra aplicación comercial, también tenia una mala implementación en la codificación de caracteres. Mandé un par de e-mails al desarrollador del paquete, pero el dijo algo así como: "no puedo hacer nada al respecto".

Cuando descubrí que en el lenguaje de programación PHP no se habían tenido en cuenta las condificaciones y que usaba tan solo 8 bits para la codificación de caracteres, haciendo prácticamente imposible el desarrollo de buenas aplicaciones internacionales, pensé: ya es suficiente.

Tengo un anuncio que hacer: si eres un programador y no tienes unos conocimientos mínimos sobre caracteres, tabas de caracteres, codificaciones y Unicode, te voy a pillar y te castigaré haciéndote pelar cebollas durante 6 meses en un submarino. Te juro que te pillaré.

Y algo más:
** NO ES TAN DIFÍCIL **

En este artículo voy a explicar exactamente lo que todos los programadores deberían saber. Todo esto de que "texto en claro = ASCII = caracteres de 8 bits" no está solamente mal, sino que está fatal, y si sigues programando de este modo, no eres mucho mejor que un doctor que no cree en los gérmenes. POR FAVOR, no escribas otra línea de código antes de haber acabado de leer este artículo.

Antes de que empiece, debería advertirte de que si eres una de estas raras personas que sabe algo sobre la internacionalización, vas a encontrar todo lo de este posto un poco "simplificado". Yo estoy intentando establecer un mínimo, para que todo el mundo pueda entender de qué va el tema, y pueda escribir código que tenga alguna oportunidad de funcionar en textos escritos en cualquier lenguaje ( o en otro subset de inglés que no incluya palabras acentuadas ¬¬ ). Y debo advertirte que el tratamiento de caracteres es solo una pequeña parte de lo que conlleva crear software que funcione intencionalmente. Me temo que yo solo puedo escribir artículos sobre una cosa a la vez, o sea que hoy tocan codificaciones de caracteres.

Des de la perspectiva histórica
========================

La manera mas fácil de entender todo esto es ir viéndolo de manera cronológica.

Probablemente estarás pensando que voy a hablar sobre antiguas tablas de caracteres como EBCDIC. Bueno, no voy a hacerlo. EBCDIC no es relevante para tu vida. No tenemos que ir tan atrás en el tiempo.


Volviendo atrás en los tiempos semi-antiguos, cuando Unix fue inventado y K&R estaba escribiendo The C Programming Language, todo parecía muy simple. EBCDIC estaba ya de salida. Los únicos caracteres que importaban eran los caracteres del inglés, con letras no acentuadas. Así se creó el ASCII que eran unas tablas capaces de representar todos los caracteres usando números del 32 al 127. El carácter que se correspondía con el espacio era el 32, la letra "A" se correspondía con el 65, etc. Todo esto se podía guardar perfectamente en 7 bits. La mayoría de ordenadores en estos días usan bytes de 8 bits, entonces no solo podrías almacenar todos los caracteres ASCII, sino que tenías un bit adicional que "sobraba" ( no se usaba ) y que, si querías podías utilizar para tus propios ( y maléficos ) fines. Este bit adicional se utilizó en WordStar para indicar la última letra de una palabra, condenando, así, WordStart a que sólo funcionara en inglés. Los códigos por debajo de 32 se llamaban "unprintables" ( que no se pueden escribir ) y se utilizaban para insultar. Estaba bromeando. Realmente se usaban como caracteres de control, como el 7, que hacía que ordenador emitiese un "beep", y el 12, que se interpretaba por las impresoras como un salto de página, por lo que dejaba de imprimir en la hoja actual y cargaba otra hoja.

Todo esto estaba muy bien. Suponiendo, claro, que fueses de habla inglesa.

Como el código ASCII solo ocupaba 7 bits, mucha gente pensó "caramba, puedo utilizar los código 128 hasta el 255 para almacenar mis cosas". El problema fue que MUCHA gente pensó esto en el mismo momento, pero cada uno tuvo su propia idea de qué debería ir en el espacio de 128 a 255. El IBM-PC creó algo que se llegó a conocer como mapa de caracteres OEM, que contenía caracteres acentuados para los lenguajes Europeos, así como muchos caracteres para dibujar líneas.... barras horizontales, barras verticales, etc... así que tu podías usar estas líneas para dibujar caracteres en la pantalla y hacer cuadros y líneas a tu gusto ( era un recurso para embellecer las aplicaciones de línea de comandos ). Asimismo, cuando la gente empezó a comprar ordenadores fuera de EEUU, se diseñaron muchos tipos diferentes de tablás de caracteres OEM. Cada una de ellas usaba los últimos 128 caracteres para sus propios propósitos. Por ejemplo, en algunos ordenadores el código 130 se mostraba como "é", pero en los ordenadores vendidos en Isael se veía la letra Hebrea Gimel (ג), así que si des de Estados Unidos enviaban un documento con la palabra "résumés" los israelitas las recibían como "rגsumגs". En muchos casos, como en Rusia, hubieron muchas ideas sobre qué hacer con los últimos 128 caracteres, por lo que intercambiar documentos en la misma Rusia se convertía en un problema.

Eventualmente, este "libre albedrío" de qué hacer en los últimos 128 caracteres se acabó con el estándar ANSI. En este nuevo estándar, todo el mundo accedió en "qué hacer con estos 128 caracteres". Pero este nuevo método establecía muchos maneras de tratar los últimos 128 caracteres, dependiendo del sitio donde vivieras. Estos sistemas diferentes se llamaron *code pages* (páginas de código). En Israel, por ejemplo, el sistema operativo DOS usaba la página con código 862, mientras que en Grecia usaban la página de códigos 737. O sea, sus tablas de códigos eran igual por debajo de los 128 ( ASCII ), pero eran diferentes por encima de los 128 ( donde se ponían todos aquellos "caracteres divertidos" de cada país o región ). Las versiones de MS-DOS tenían docenas de páginas de códigos, que permitían hacer que un mismo ordenador fuera "multilenguaje", de modo que podían abrir documentos escritos en inglés, islandés o esperanto (usando una tabla de códigos que suportara los tres idiomas al mismo tiempo). Pero qué pasaba si querías usar el hebreo y el griego en el mismo ordenador? Pues que no podías ya que cada uno tenía su tabla de códigos específica, y no había ninguna tabla de códigos "compartida" entre estos dos idiomas.

Mientras tanto en Asia, estaban pasando cosas bastante mas complicadas por el hecho de que los alfabetos asiáticos tienen cientos de carácteres. Ellos lo solucionaron con un sistema muy lioso llamado DBCS, "double byte charse set" en el que _algunas_ letras se guardaban en un byte, otras letras se guardaban en dos bytes. Este sistema te permitía moverte sin problemas hacia "adelante" en un string, pero era realmente complicado moverte "hacia atrás". Por esto los programadores dejaron de usar s++ o s-- y empezaron a usar las funciones de Windows AnsiNext y AnsiPrev para no tener que pelearse con este complicado sistema.

Pero aún así la gente seguía programando de modo que un carácter fuese un byte, y que un carácter tenía 8 bits, lo que significava que "nunca muevas un texto de un ordenador a otro". Pero claro, con la aparición de Internet hubo un sitio muy propicio para intercambiar textos de un ordenador a otro, y todo el tema de las codificaciones se vino abajo. Por suerte se inventó Unicode.


Unicode
=======
Unicode fue el resultado de un gran esfuerzo por crear un solo set de caracteres que incluía cualquier sistema de escritura razonable en el planeta, y algún otro como el Klingon, también. Algunas personas, malentendiendo lo que Unicode es, piensan realmente que es un sistema de 16 bits, donde cada carácter ocupa 16 bits. Esto significaría que con este sistema se podrían escribir hasta 65.536 caracteres. Pero esto no es realmente cierto. Es uno de los mitos mas tontos sobre Unicode, por lo que si creías que que era así, no te sientas mal.

De hecho, Unicode tiene un modo diferente de pensar sobre los caracteres, y tu tienes que entender este modo de pensar, o nada va a tener ningún sentido para ti.

Hasta ahora, hemos asumido que un carácter se guarda en un número determinado de bytes como por ejemplo:

A = 0100 0001

En Unicode, los caracteres se llaman "code point", que son solo un concepto teórico. El cómo un "code point" se representa en memoria o en disco, es otra historia.

En Unicode, la letra A es una idea platónica. Solo está flotando en el aire:
A
Esta platónica A es diferente de B, y diferente de a, pero igual a A, y A, y A. La es idea que A en el tipo de letra Times Roman es lo mismo que A en el tipo de fuente Helvetica, pero diferente de "a" en minúscula. Esto no parece que tenga que causar mucha controversia, pero en algunos lenguajes, determinar lo que realmente una letra es puede ser complicado. Por ejemplo en Alemán la letra ß es una letra real o es otro modo de escribir ss? Si una letra cambia, por el hecho de estar escrita al final de una palabra, es la misma letra? Los hebreos dicen que si. Los arábigos dicen que no. De todos modos, la gente inteligente de el consorcio de Unicode, se han estado preguntando todo esto durante la última década, acompañado por un gran debate político. Por esto tu no debes preocuparte por todo esto. Ellos ya lo han calculado todo ya.

Cualquier letra platónica ( concepto de letra ) en todos los alfabetos, es un número mágico en el consorcio Unicode, que se escribe tal que así: U+0639. Este número mágico es llamado "code point". El U+ significa Unicode y los números son hexadecimal.

No hay un límite real en el número de letras que Unicode puede definir, aún a pesar de que parezca de que solo se utilicen 2 bytes, y por tanto, solo se pueden definir 65535 letras. Esto es otro mito sobre Unicode.

Bien, aquí tenemos una cadena de texto:
Hello

que, en Unicode, corresponde a estos cinco puntos de código:

U+0048 U+0065 U+006C U+006C U+006F

Solo sólo un montón de puntos de código (codepoints). Números, en realidad. Todavía no hemos hablado de cómo guardar esto en memoria o en disco, o cómo representarlo en un mensaje de correo electrónico.


Econdings
=========
Aquí es donde entran los "encodings" (codificaciones).

La primera idea de las codificaciones en Unicode, que nos deja el mito sobre los dos bytes, fué: "ei! vamos a guardar estos números en dos bytes cada uno". Así que "Hello" se convierte en:
00 48 00 65 00 6C 00 6C 00 6F

Bien? No, demasiado rápido! No podría escribirse como?
48 00 65 00 6C 00 6C 00 6F 00 ?

Bueno, técnicamente, si. Creo que se podría, y de hecho, los primeros programadores querían ser capaces de guardar los "code points" de Unicode en "high-endian" o "low-endian" en función de si su procesador iba mas rápido en un sistema que en el otro. Entonces la gente estuvo forzada a adoptar la convención bizarra de guardar FE FF al principio de cada texto Unicode; esto se llama Unicode Byte Order Mark ( Marca de orden de bytes Unicode ), y, si tu estabas girando el orden tus bytes de mayor y menor peso, esta marca sería FF FE, así la persona que leyera tu texto sabría si tiene que girar el orden de los bytes o no. Ufff. No todos los textos Unicode tendría la marca de orden al principio, o sea que ya os podéis imaginar los problemas que conllevaría esto.

Durante un tiempo, esto parecía que debería suficiente, pero los programadores se estaban quejando y diciendo: "Ei! Mira todos estos ceros!", ya que ellos eran americanos y estaban mirando texto en inglés. El texto en inglés cumplía la propiedad de que rara vez habían "code points"por encima de U+00FF. También estaban los hippies liberales de California que querían conservar (grrr). Si fueran tejanos, no les habría importado el consumo del doble de memoria para guardar un texto en inglés ( chiste americano ). Pero los cobardes de California no podían soportar la idea de duplicar la cantidad de espacio almacenado para guardar textos. De todos modos habían muchos documentos antiguos por ahí almacenados en ANSI y DBCS. Y que iban a hacer, convertirlos todos? Ellos? Solo por esta razón hubo mucha gente que decidió hacer caso omiso a Unicode durante varios años, y por tanto la cosa empeoró.

Así, se inventó el concepto brillante de UTF-8. UTF-8 fue otro sistema para guardar "code points" en Unicode, pero solo usando 8 bits de memoria. En UTF-8, cada "code point" de 0 a 127 solo se guarda en un solo byte". Solo los "code points" de 128 para arriba se guardan en 2, 3, 4, 5 o 6 bytes.

Esto también tuvo un "efecto secundario" muy bueno en los textos escritos inglés. El texto era exactamente igual en UTF-8, que en ASCII, así que los americanos no verían nada extraño. Solo el resto del mundo debía pasar por el aro. Específicamente "Hello", que es: U+0048 U+0065 U+006C U+006C U+006F, se guardaría como: 48 65 6C 6C 6F!! Exactamente igual como se guardaría en ASCII y ANSI, y en todos los grupos de caracteres OEM del planeta! Ahora bien, si eres tan valiente como para utilizar letras acentuadas, o letras griegas o letras Klingon, tendrás que utilizar varios bytes para almacenar un "code point" único, pero los estadounidenses nunca se darán cuenta de eso.

Hasta ahora te he dicho tres formas de codificación Unicode. Los métodos tradicionales guardalo-en-dos-bytes son llamados UCS-2 ( por que tiene 2 bytes ) o UTF-16 ( porque tiene 16 bytes ), y uno todavía tiene que averiguar si se trata de UCS-2 big-endian o UCS-2 low-endian. Y también tenemos el nuevo y popular estándar UTF-8 que tiene la agradable propiedad de funcionar perfectamente bien con el texto en ingles ( tanto ASCII como ANSI ) y con programas que no tienen ni idea de que existan otras cosas que no sean ASCII.

Hoy en día hay un montón de otras formas de codificar Unicode. Hay algo llamado UTF-7, que se parece mucho a UTF-8 pero que garantiza que el mayor bit va a ser siempre cero. Si tienes, por ejemplo, algún tipo de servidor de correo nazi, que cree que 7 bits son "suficientes, gracias", todavía puedes salir ileso y enviar tus e-mails codificándolos con UTF-7. También hay UCS-4, que guarda cada "code point" en 4 bytes, y tiene la bonita propiedad de que cada "code point" puede ser guardado en el mismo número de bytes, pero, caramba! Incluso la gente de Texas no sería tan osada como para gastar tanta memoria.

De hecho, ahora que estás pensando en la idea platónica que representan los "code points" en Unicode, te puedes dar cuenta que estos "code points" se pueden encodear en cualquier encoding de la "vieja escuela". Por ejemplo, podrías encodear "Hello" ( U+0048 U+0065 U+006C U+006C U+006F ) en ASCII, o el OEM de Grecia, o en la codificación ASCII del Hebreo, o con cualquier codificación anteriormente inventada. Solo con una pega: dejarás de ver algunas letras! Si no existe ningún equivalente para el "code point" de Unicode en el encoding en el que lo estas intentando transformar, normalmente vas a ver el símbolo de pregunta, "?", o quizás puedes llegar a ver el símbolo �. Te suena?

Hay cientos de encodings tradicionales, que solo pueden almacenar ALGUNOS de los "code points" de Unicode. Los "code points" que no tienen una representación en el encoding viejo, se cambian por símbolos de interrogación. Algunos encodings populares en inglés son: Windows-1252 ( el estándar Europeo para Windows 9x ), y el ISO 8859-1 ( también llamado Latin-1 ), también usado por los idiomas del oeste Europeo. Pero intenta almacenar Ruso, o Hebreo en estos encodings, y solo vas a ver un montón de símbolos de interrogación. UTF 7, 8, 16 y 32 tienen la propiedad de poder almacenar cualquier "code point" de manera correcta.


La cosa mas importante sobre los encodings
==========================================
Si has olvidado todo lo que te acabo de explicar, por favor aprende una cosa muy importante. No tiene ningún sentido tener una cadena de texto sin saber que encoding usa. Ya no puedes seguir pensando que "texto claro" es ASCII.

El texto claro (plain text) no existe.

Si tu tienes un texto, en memoria, o en un fichero, o en un e-mail DEBES saber en que encoding ha sido guardado, o no serás capaz de mostrar los caracteres correctamente.

Casi todos los problemas estúpidos "mi web parece un galimatías" o "mi amiga no puede leer mi correo electrónico cuando lleva acentos", se reducen a que un programador ingenuo que no comprendía el simple hecho de que si no me dices como has codificado el texto, ya sea en UTF-8, o ASCII, o ISO-8859-1 o Windows 1252 yo SIMPLEMENTE no puedo mostrar correctamente el texto ( ni saber donde el texto termina )!

¿Dónde podemos preservar la información sobre qué codificación usa el texto que mandamos? Bueno, hay maneras estándar para hacer esto. Por ejemplo, para un correo electrónico, uno espera encontrar en la cabecera un string que ponga:

Content-Type: text/plain; charset="UTF-8"

Para una página web, la idea original era que el servidor web devolviese una cabecera (HTTP) con el Content-Type, ( y no en el HTML ). De manera que uno ya sepa la codificación de la página antes de recibir el HTML.

Esto da problemas. Imagina que tienes un gran servidor web con muchos sitios, y con cientos de páginas con contribuciones de gente de todo el mundo, en diferentes idiomas. Y que todas utilizan la codificación que quiera que use el Microsfot FrontPage. El servidor web no nunca sabrá realmente qué encoding tiene cada archivo, por lo que no podrá mandar una cabecera con el Content-Type.

Sería conveniente que pudieses poner un Content-Type en el HTML donde va el fichero mismo, utilizando algún tipo de tag especial. Por supuesto esto llevó a la locura a los puristas... "¿Cómo se puede leer un fichero HTML hasta que no sepas que codificación lleva?!" Por suerte, casi cada encoding común usa los mismos caracteres entre 32 y 127, entonces siempre podemos empezar a leer HTML sin ver caracteres estaños:


<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">


Por lo que el meta tag tiene que estar estar justo después de declarar la sección head ya que, tan pronto como el navegador web vea este tag, va a parar de interpretar la página y volverá a empezar reinterpretando toda la página con el encoding que has especificado.

¿Que hacen los navegadores si no encuentran ningún Content-Type y ningún tag en el header HTML? Internet Exploiter hace algo bastante interesante: intenta adivinar, basado en la frecuencia en la que aparecen algunos bytes en los típicos encodings en diferentes idiomas. Ya que varios encodings de 8 bits intentaban poner sus letras de su idioma nativo entre el rango entre 128 y 255, y ya que todos los idiomas tienen diferentes características de frecuencia en el uso de determinadas letras en texto escrito, se podía adivinar mas o menos el encoding en el que ha sido escrito el texto. Es bastante raro, pero parece funcionar bastante bien para las páginas programadas por ingenuos programadores que necesitarían echarle un vistazo a lo que es el Content-Type de su página web. ¿ Que pasa? Que si no se ajusta bien el encoding ( la detección falla ), y si Internet Exploiter decide que el idioma es coreano, pues el usuario que ha cargado la página no puede entender nada. Esto prueba, creo, la Ley de Postel sobre: "conservador en lo que dices y liberal en lo que aceptas" no es un principio bueno para un trabajo de ingeniería. De todos modos, ¿Qué hace el pobre lector que entra en una página escrita en búlgaro y la ve en coreano? Va al menú View>Encoding y prueba diferentes codificaciones (hay varias docenas de codificaciones en el Este de Europa ), hasta que ve bien el texto. Esto si sabe que puede hacerlo, claro, y mucha gente ni lo sabe.

Para la última versión de CityDesk un sitio de administración web publicado por mi compañía, decidimos internamente utilizar Unicode UCS-2 (dos bytes), que es el tipo nativo de Visual Basic, COM y Windows NT/2000/XP para los tipos de datos string. En C++, declarábamos los strings como wchar_t, en vez de char, y usábamos la función wcs en vez de str. Para crear un literal UCS-2 en C, simplemente poníamos un L antes, así quedaba algo como: L"Hello".

Cuando CityDesk publicó su página, se convirtió todo en UTF-8, que siempre ha sido una codificación muy bien soportada por todos los navegadores. Aquí tenemos el muro de las 29 versiones en las que está ecodeada la página de "Joel on Software", y yo nunca he escuchado a ninguna persona que haya tenido algún problema al visitar mi página.

Este artículo se está haciendo demasiado largo, y me es imposible explicar todo lo relacionado con la codificación de caracteres en Unicode, pero creo que si has leído hasta aquí, has aprendido suficiente como para volver a programar, y usar antibióticos, en vez de sanguijuelas y hechizos.



--------------------------------------------------------------------------------------------------------



[1]: Teniendo en cuenta que mi nivel de inglés es bastante.... humilde. Aún así he hecho un gran esfuerzo para traducir del mejor modo que he podido el artículo. Se aceptan correcciones.

Nota: Se ha usado “cadena de texto” como sinónimo de “string”.
Nota2: Se ha cambiado intencionadamente “Internet Explorer(tm)(r)” por “Internet Exploiter”. Seguramente el autor no estaría de acuerdo con esto. Es un chiste fácil.
Nota 3: Si te ha gustado el artículo y eres un blogger, agradecería que creases un post mencionando el al artículo original ( o hacia esta traducción, o ambos ). Un mundo con mejor comprensión sobre el Unicode, es un mundo mejor. Gracias. ;)