martes, 21 de octubre de 2014

Sobre las reglas de codificación o... ¿de dónde salen esos caracteres "raros"?.

1. Introducción.

En este tutorial vamos a tratar de dar algo de luz a los errores que tenemos habitualmente con la codificación de caracteres en aplicaciones en las que se ven implicados varios sistemas que intercambian o almacenan información.
Vamos a ir de abajo a arriba, haciendo una pequeña introducción a la codificación de caracteres, mostrando cómo provocar esos errores, para terminar explicando cómo solucionarlos.

2. Codificación de caracteres.

La codificación de caracteres es el método que permite convertir un caracter del lenguage natural, el de los humanos, en un símbolo de otro sistema de representación, aplicando una serie de normas o reglas de codificación. El ejemplo más gráfico suele ser el del código morse, cuyas reglas permiten convertir letras y números en señales (rayas y puntos) emitidas de forma intermitente.
En informática, las normas de codificación permiten que dos sistemas intercambien información usando el mismo código numérico para cada caracter. Las normas más conocidas de codificación son las siguientes:
  • ASCII: basado en el alfabeto latino tal como se usa en inglés moderno y en otras lenguas occidentales. Utiliza 7 bits para representar los caracteres, aunque inicialmente empleaba un bit adicional (bit de paridad) que se usaba para detectar errores en la transmisión. Incluye, básicamente, letras mayúsculas y minúsculas del inglés, dígitos, signos de puntuación y caracteres de control, dejando fuera los caracteres específicos de los idiomas distintos del inglés, como por ejemplo, las vocales acentuadas o la letra ñ.
  • ISO-8859-1 (Latin-1): es una extensión del código ascii que utiliza 8 bits para proporcionar caracteres adicionales usados en idiomas distintos al inglés, como el español. Existen 15 variantes y cada una cubre las necesidades de un alfabeto diferente: latino, europa del este, hebreo cirílico,... la norma ISO-8859-15, es el Latin-1, con el caracter del euro.
  • cp1252 (codepage 1252): Windows usa sus propias variantes de los estándares ISO. La cp1252 es compatible con ISO-8859-1, menos en los 32 primeros caracteres de control, que han usado para incluir, por ejemplo, el caracter del euro.
  • UTF-8: es el formato de transformación Unicode, de 8 bits de longitud variable. Unicode es un estándar industrial cuyo objetivo es proporcionar el medio por el cual un texto en cualquier forma e idioma pueda ser codificado para el uso informático. Cubre la mayor parte de las escrituras usadas actualmente.
En la enumeración hemos ido de menos a más, no solo en el tiempo, por el momento de aparición de la norma, sino también por los caracteres que soporta cada una, UTF-8 es la más ambiciosa.
Visto así, la recomendación debería ser el uso de UTF-8 puesto que, escriba en la lengua que escriba, sus caracteres van a ser codificables. Pero, si sólo escribo en castellano, podría limitarme a usar ISO-8859-1, o ISO-8859-15 si necesito el caracter del euro, sin ningún problema.
Más relevante que la norma de codificación que se use para escribir, que no es poco, es ser conscientes que la lectura se debe realizar con la misma norma de codificación con la que se escribió. Y tiene toda la lógica del mundo, puesto que si escribimos un fichero con ISO-8859-1 no debemos esperar que un sistema que lee en UTF-8 lo entienda sin más (aunque realmente entienda gran parte).

3. ¿Caracteres "raros"?.

Los caracteres "raros" aparecen por una conversión incorrecta entre dos codificaciones distintas. Se suelen producir porque se utiliza la codificación por defecto del sistema o programa y esta no coincide con la original o, directamente, por desconocimiento de la norma de codificación de la fuente de lectura.
Así, por ejemplo, podemos encontrarnos con los siguientes caracteres "raros" escribiendo la misma palabra:
  • España → España: si escribimos en UTF-8 y leemos en ISO-8859-1. La letra eñe se codifica en UTF-8 con dos bytes que en ISO-8859-1 representan la A mayúscula con tilde (Ã) y el símbolo más-menos (±).
  • España → Espa�a: si escribimos en ISO-8859-1 y leemos en UTF-8. La codificación de la eñe en ISO-8859-1 es inválida en UTF-8 y se sustituye por un caracter de sustitución, que puede ser una interrogación, un espacio en blanco... depende de la implementación.
Podemos provocar un error fácilmente haciendo uso de un editor que permita modificar el formato de escritura, comopspad, y utilizando un lector que permita modificar el de lectura, como por ejemplo un navegador .
España en ISO-8859-1 leído en UTF-8
Si nos encontramos frente a una aplicación web cliente-servidor tenemos, como mínimo, los siguientes actores implicados: una base de datos con un set de caracteres, una aplicación escrita en un lenguaje que usará su propio encoding para las lecturas y escrituras en esa base de datos y en el sistema de ficheros, un servidor web dinámico o un servidor de aplicaciones que servirá peticiones a un cliente escribiendo en la respuesta con una codificación preestablecida y un cliente que debe leer la respuesta del servidor. Todos esos actores del proceso deben usar la misma norma para leer y escribir, a ser posible estandar, lo deseable: UTF-8.

4. Especificar la codificación de caracteres.

Para evitar problemas con la codificación, siempre debemos indicar explícitamente en nuestros fuentes y sistemas de lectura con qué norma estamos trabajando, con ello le indicaremos al lector la regla de codificación.
En HTML con la siguiente etiqueta en la cabecera del documento
  1. <META httpequiv="ContentType" content="text/html; charset=UTF-8">  
En XML con el valor del atributo encoding (por defecto es UTF-8):
  1. <?xml version="1.0" encoding="ISO-8859-1" ?>  
Con ello reducimos los posibles problemas pero no los evitamos, puesto que no sirve de nada indicar en el fuente de un fichero html que su encoding es utf-8 si, al guardarlo en disco, lo guardo con un encoding distinto o el servidor lo lee con un encoding diferente para servirlo.
Y, ¿que ocurre con aquellos fuentes en los que no se indica el encoding?, como fuentes java o ficheros de propiedades. Los primeros son leídos por el compilador y los segundos por el propio fuente java compilado, en runtime.
La Máquina Virtual Java (JVM) utiliza una codificación por defecto, que suele ser la del sistema operativo donde se está ejecutando, en windows cp1252. La codificación por defecto se puede conocer obteniendo la propiedad del sistema "file.encoding" y se puede modificar programáticamente (asignando valor a dicha propiedad) o en la compilación. Si bien, volvemos a tener el mismo problema, puesto que, de nada sirve que se asigne dicha propiedad si, al escribir el fuente java, estoy guardando con una codificación distinta.
Con lo dicho, la preocupación no es sólo el encoding asignado al documento en su fuente, sino el encoding con el se escribe el mismo.

5. Solución al problema de codificación en lenguajes de marcado.

La solución al problema de codificación en lenguajes de marcado como html o xml pasa por hacer uso de entidades que sustituyen los caracteres no incluidos en la códificación básica (ascii). Dichas entidades son una clave escrita con los propios caracteres de la norma ascii, aunque también podemos hacer uso de la correspondiente clave del caracter en unicode o de su cógido en decimal. Con ello, son interpretados independientemente de la codificación, puesto que no usan ningún caracter fuera del código ascii.
Así, podemos acudir a cualquier tabla de referencia en las que se incluyen los caracteres y su correspondiente entidad, clave en unicode y en decimal. Por incluir algún ejemplo:
CaracterEntidadUnicodeDecimal
"quotU+002234
áaacuteU+00E1225
ñntildeU+00F1241
En cualquier fuente xml o html, independientemente del encoding asignado a la cabecera y del usado para guardar el documento, podemos incluir cualquiera de esas tres claves para sustituir su correpondiente caracter de la siguiente forma:
  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">  
  2. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="es" lang="es">  
  3. <head>  
  4.   <title>Character encoding</title>  
  5.   <META httpequiv="ContentType" content="text/html; charset=UTF-8">  
  6. </head>  
  7. <body>  
  8. <p>Usando el nombre de la entidad: Espa&ntilde;a</p>  
  9. <p>Usando el código unicode: Espa&#x00F1;a</p>  
  10. <p>Usando el código decimal: Espa&#241;a</p>  
  11. </body>  
  12. </html>  
Podéis probar a guardar el documento en UTF-8 y visualizarlo desde un navegador en ISO-8859-1, los únicos caracteres que no se mostrarán correctamente son las oes acentuadas, puesto que no están escapadas.
Algo tedioso si hubiese que hacerlo manualmente... para ello las herramientas de edición de páginas web realizan esa conversión a entidades automáticamente.
E insisto, esta conversión es del todo innecesaria si especificamos el encoding UTF-8 en la cabecera de nuestros fuentes, guardamos el fichero con ese mismo encoding, y todos los actores implicados en el proceso de lectura usan esa misma norma de codificación.

6. Solución al problema de codificación en java.

Volvemos a los fuentes java y ficheros de propiedades que nos permiten internacionalizar nuestra aplicación; ahora se almacenan en un repositorio de código tipo subversion y son compartidos por un equipo de desarrollo más o menos dimensionado. ¿En qué codificación se almacenarán esos ficheros en el repositorio de código?, trataremos que sea en UTF-8, pero más importante será que todo el equipo de desarrollo tenga asignado esa misma codificación en sus entornos de desarrollo o IDEs.
Un problema común se da trabajando con Eclipse en una plataforma windows, puesto que viene configurado por defecto con el encoding cp-1252, porque es el de por defecto de la JVM en ese SO. Si los fuentes están en el repositorio en UTF-8 y me los descargo sin preasignar dicha codificación, he perdido todas las tildes de los comentarios del fuente java (y quizás el valor de alguna constante de tipo cadena) y, peor aún, de los literales internacionalizados de los ficheros de propiedades. No es mayor problema, si me doy cuenta y modifico el encoding a UTF-8, pero si no caigo en ello y hago commit sin modificarlo, he cambiado la codificación del fuente en el repositorio de código y lo he subido con errores de codificación, con caracteres "raros".
Para solucionar este problema podemos hacer uso de las claves unicode en el fuente de nuestros .java o .properties. Así, por ejemplo:
  1. path = path.replace('\u00E1','a');  
donde 00E1 se correponde con la a acentuada, y el código reemplazaría de la cadena las aes acentuadas por aes sin acento.
Al igual que el punto anterior, algo tedioso para hacerlo manualmente... para ello la propia JDK distribuye una utilidad llamada native2ascii que covierte un fichero de una codificación nativa (no Unicode o no Latin-1) a otro en ascii con caracteres codificados en unicode.
Previene el problema de la codificación de caracteres convirtiendo el contenido de los ficheros en un formato que puede ser almacenado como ascii. Y se suele usar para la internacionalización de los ficheros de mensajes (ficheros de propiedades) de manera que puedan ser leidos por las clases Properties y ResourceBundle, sin necesidad de una configuración específica del entorno.
Es una utilidad por línea de comandos que puedes encontrar en el mismo directorio que el compilador, y que permite:
  • introducir caracteres para que los convierta y les de salida por consola
  • introducir un path a fichero y que lo convierta dándole salida por consola o a fichero.
Ejemplo de uso de native2ascii
Volvemos a recordar la importancia de que exista una sincronía entre la codificación de lectura y escritura, puesto que native2ascii es una utilidad java que usará para leer el encoding por defecto de la JVM, si no lo modificamos, será el del SO. Como consecuencia, si nuestro fichero de propiedades está escrito en UTF-8, y no le indicamos lo contrario, la utilidad le leerá como cp1252 en windows, y sustituirá todos los caracteres que no estén incluidos en dicha codificación como \ufffd. De ahí la necesidad de indicar el parámetro -encoding.

7. Solución al problema de codificación en el entorno de un proyecto gestionado por maven.

Maven gestiona todo el ciclo de vida de nuestro proyecto: compilación, tests, empaquetamiento, versionado,... en base a plugins que configuramos en nuestros pom.xml. Esos plugins estan escritos en java y leen y escriben nuestros ficheros, para lo cuál necesitan la especificación de un encoding. Por defecto, usan el de la JVM, esto es, el del SO, con lo que si estamos en windows será cp-1252. Todos esos plugins aceptan la configuración del encoding de lectura y escritura, y es necesario configurarlo.
  1.   <build>  
  2.     <plugins>  
  3.       <plugin>  
  4.         <artifactId>maven-compiler-plugin</artifactId>  
  5.         <configuration>  
  6.           <source>1.5</source>  
  7.           <target>1.5</target>  
  8.         <encoding>UTF-8</encoding>  
  9.         </configuration>  
  10.       </plugin>  
  11.       <plugin>  
  12.         <groupId>org.apache.maven.plugins</groupId>  
  13.         <artifactId>maven-resources-plugin</artifactId>  
  14.         <configuration>  
  15.           <encoding>UTF-8</encoding>  
  16.         </configuration>  
  17.       </plugin>  
  18.       <plugin>  
  19.         <groupId>org.apache.maven.plugins</groupId>  
  20.         <artifactId>maven-site-plugin</artifactId>  
  21.         <configuration>  
  22.           <inputEncoding>UTF-8</inputEncoding>  
  23.           <outputEncoding>UTF-8</outputEncoding>  
  24.         </configuration>  
  25.       </plugin>  
  26.   </plugins>  
  27. </build>              
Podéis comprobar que lo que hemos hecho es escribir UTF-8 en la configuración de todos los plugins que aceptan la asignación de encoding. Si bien, para no repetir la configuración podríamos haber declarado una propiedad y usarla como una variable o, mejor aún, usar directamente una propiedad preestablecida que asigna un encoding por defecto para todos los plugins de nuestros pom.xml.
  1. <properties>  
  2.   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>  
  3.   <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>  
  4. </properties>  
Con ello ahorramos configuración, puesto que ya no es necesaria la configuración individual de cada uno de ellos.
Y para nuestros ficheros de propiedades existe un plugin que permite invocar de manera automática, en la fase de empaquetación, a la utilidad native2ascii, permitiendo realizar la conversión de caracteres de ciertos ficheros dentro del propio ciclo de vida del proyecto.
  1. <plugin>  
  2.       <groupId>org.codehaus.mojo</groupId>  
  3.       <artifactId>native2ascii-maven-plugin</artifactId>  
  4.       <version>1.0-alpha-1</version>  
  5.       <configuration>  
  6.            <src>src/main/resources</src>  
  7.            <dest>target/classes</dest>  
  8.       </configuration>  
  9.       <executions>  
  10.            <execution>  
  11.                  <goals>  
  12.                        <goal>native2ascii</goal>  
  13.                  </goals>  
  14.                  <configuration>  
  15.                        <encoding>UTF8</encoding>  
  16.                        <includes>messages*.properties</includes>   
  17.                  </configuration>  
  18.            </execution>  
  19.       </executions>  
  20. </plugin>  
Se incluye un directorio de origen y destino de los ficheros, el encoding de los fuentes y un patrón para detectar qué ficheros deben ser convertidos, en el ejemplo, todos los ficheros de propiedades que comiencen por "messages".
Aún todo lo anterior, los problemas no estarán del todo solucionados porque siempre existe la posibilidad de que alguien suba al repositorio de código un fichero de propiedades en un formato distinto a UTF-8, con lo que el encoding de escitura difiere del de lectura y la conversión de native2ascii será errónea. En tal caso solo queda identificar al culpable y proceder su escarnio público, con la posibilidad de establecer una sanción económica por haber "roto" el proyecto.

8. Un problema de dificil detección.

Sigo en el mismo proyecto, gestionado por Maven, y tengo que hacer una modificación en un fuente. Se me ocurre que por ser lunes y las 11:15 no voy a hacerlo desde el IDE, desde Eclipse, voy a acceder mediante el explorador de windows para hacerlo desde el block de notas. El fuente estaba en UTF-8 y el notepad al guardar en ese encoding introduce una marca BOM (byte order mark) en el comienzo del fichero. Esos caracteres pueden provocar que el compilador java falle y, si el fuente es un xml, como puede ser un pom.xml de maven, el proyecto dejará de compilar puesto que se trata de un xml mal formado.
  1. <?xml version="1.0" encoding="UTF-8"?>  
La solución pasa por encontrar un editor que permita primero leer esos caracteres y eliminarlos del fuente después. Para ello, jedit tiene un plugin para editar en hexadecimal.

9. Conclusiones.

La codificación es uno de esos temas de los que sí debemos ser conscientes y espero que este tutorial haya ayudado a ello o, al menos, a prevenir ciertos errores comunes.
Un saludo.
Jose

Fuente: http://www.adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=characterencoding-native2ascii

Jesús Moreno - Ingeniero Ténico Informático - consultor Informático

Hola, soy Jesús Moreno Ingeniero Técnico Informático en sistemas por la US y propietario de éste blog. Mi trabajo en los ultimos años se ...