viernes, 30 de enero de 2015

Conversión de Charset/Encoding en Java

Introducción


Un problema bastante común para los programadores, especialmente para los hispanohablantes, es la codificación de caracteres (Character encoding en inglés).

Un juego de caracteres (en inglés Character set o más comúnmente charset) es la definición de un conjunto de caracteres determinado identificándolos con un número.

Un charset puede tener uno o varios encodings. Un encoding determina la representación de cada carácter mediante secuencias de bytes (1 o más bytes). Es decir, el mismo carácter puede tener distintas representaciones en encodings diferentes, o visto desde el otro lado, una misma secuencia de bytes puede representar caracteres diferentes en un encoding u otro.

El manejo de encodings suele ser peor que un dolor de muelas, sobre todo cuando se necesita realizar comparaciones de Strings que provienen de diferentes fuentes: entrada de usuario, base de datos, código fuente, etc. Para obtener resultados correctos, hay que asegurarse de que cada String se construye a partir del encoding adecuado.

Problemas


Los problemas suelen aparecer con caracteres no ASCII como las vocales acentuadas. Por ejemplo, la cadena aáeéiíoóuú se representaría así:


Encoding / Bytes000102030405060708091011121314151617181920212223
ISO-8859-161e165e969ed6ff375fa0a
UTF-861c3a165c3a969c3ad6fc3b375c3ba0a
UTF-16fffe6100e1006500e9006900ed006f00f3007500fa000a00


Se puede observar que una misma secuencia de caracteres da lugar a 3 secuencias de bytes diferentes.

Las secuencias de bytes probablemente no sean compatibles entre sí: es decir, no se puede leer una secuencia codificada en UTF-8 como si lo estuviese ISO-8859-1 o en UTF-16. En el ejemplo, al usar un editor ISO-8859-1 con la secuencia en UTF-8 se vería aáeéiíoóuú y al hacer lo inverso se vería a�e�i�o�u.

Puede ocurrir que secuencias válidas en un encoding sean inválidas en otro. Por ejemplo, la secuencia hexadecimal C1C1 representa la cadena ÁÁ en ISO-8859-1 pero es una secuencia inválida en UTF-8.

También puede pasar que haya caracteres de un charset que no se puedan representar en otro: por ejemplo, el charset ISO-8859-15 tiene el símbolo del Euro €, por lo que no se podrían convertir con exactitud textos con dicho símbolo al charset ISO-8859-1 que no lo tiene.

Strings en Java


En Java, los caracteres y secuencias de caracteres (String, StringBuilder, etc) se representan en una de las versiones del encoding UTF-16 de Unicode. Eso significa que da igual desde que fuente se haya creado el carácter, internamente se almacena en UTF-16.

Los constructores de las clases String, InputStreamReader y OutputStreamWriter entre otras permiten indicar el charset (Charset), de modo que se puede indicar como interpretar las secuencias de bytes para convertirlas en caracteres y viceversa. Por defecto se utiliza Charset.defaultCharset(), un charset obtenido de la configuración del sistema operativo para el usuario (entorno) que arrancó la máquina virtual Java.

En el fondo, para realizar la conversión entre caracteres y secuencias de bytes al leer o escribir, se utilizan las siguientes clases:

  • CharsetEncoder: permite obtener la representación como secuencia de bytes de una secuencia de caracteres en determinado charset.
  • CharsetDecoder: permite interpretar una secuencia de bytes como una secuencia de caracteres en un determinado charset.
Dichas clases proporcionan el mayor control sobre el proceso de conversión, permitiendo especificar por ejemplo el comportamiento ante secuencias de bytes inválidas o caracteres no representables.

Por el contrario, otras alternativas tienen restricciones que se deben tener en cuenta. Por ejemplo, en la documentación oficial de la clase String:

Ejemplos


He realizado una implementación simple del comando iconv. El código se puede ver en el siguiente enlace:

El modo de uso es el siguiente:

$ java -jar Project.jar input.file input-charset ouput-charset

Por ejemplo:

$ java -jar Project.jar utf8.txt utf8 iso-8859-1

Si no se invoca con 3 argumentos, se muestra la lista de charsets disponible y se indica el modo de uso correcto.

El código incluye la función:

public static String doConversion(
    String input,
    String inputCharsetName,
    String outputCharsetName
) throws IOException;

para transformar una String desde un charset a otro. He explicado que en memoria una String siempre está en UTF-16, por lo que la verdadera utilidad de esta función es ser un esqueleto para tratar los posibles problemas de conversión que se pueden presentar al pasar una String desde un charset origen (es decir, se está seguro de que todos los caracteres que la componen son representables en dicho charset) a otro charset destino según se configuren:

Para hacer pruebas, es recomendable utilizar 2 de los charsets cuya presencia debe estar garantizada en cualquier implementación de la plataforma Java (ver java.nio.charset.StandardCharsets): ISO-8859-1 y UTF-8. Y utilizar cadenas de caracteres cuya representación varíe de un charset a otro, como por ejemplo, las vocales acentuadas: áéíóú.

Enlaces


Enlaces de interés relacionados con este artículo:


(Actualizado 31/01/2015)

1 comentario:

  1. Oye que buen trabajo y explicación. Muchas gracias. Le estudiaré más afondo porque en efecto es un tema importantísimo y que no se le da tanta atención como se debería. De nuevo gracias por tu aporte!

    ResponderEliminar