Manejando UNICODE y UTF8 desde Java | Viricmind Labs



Manejando UNICODE y UTF8 desde Java




Unicode LogoQuien
lleve un tiempo programando seguramente se habrá encontrado en algún
momento de su carrera profesional algún que otro problema de
codificación de texto.
Éstos son frecuentes cuando nuestras
aplicaciones tienen que interactuar con aplicaciones de tercero, y la
Red es el ambiente idóneo para que aparezcan los errores de
codificación.
La gran variedad de lenguas y alfabetos
que coexisten hoy en día en el planeta Tierra, junto con la falta de
prevención y consenso (siendo justos, en parte por necesidades de
optimización), nos llevaron a una igualmente grande explosión de
sistemas de codificación, la mayoría de ellos muy incompletos.
ASCII y sus “extensiones” ISO-8859-1 (Latin 1) e ISO-8859-15 (Latin 9) son de las más conocidas por estos lares. Con el objetivo de facilitar el trabajo a los desarrolladores, la IETF se puso manos a la obra, y consiguió que empezara a extenderse el uso de UTF-8, un sistema de codificación multibyte de longitud variable que permite representar todos los carácteres Unicode, y compatible con ASCII.
Aunque en teoría Unicode y UTF-8 (así
como sus hermanas UTF-16 y UTF-32) iban a reducir los problemas de
interoperabilidad entre aplicaciones, la complejidad inherente al hecho
de ser multibyte con longitud variable, las malas prácticas de algunos
desarrolladores, y su adopción tardía por parte de otros… han minado el
idílico paisaje que nos prometía este estándar.¹
Hoy os dejo un par o tres de pequeños
consejos que os pueden ayudar a manejar cadenas de texto Unicode
(generalmente UTF-8) desde Java. Espero que os sirvan :) .

Detectar codificación de un flujo de bytes

Aunque no está estrictamente ligado a
Unicode, determinar al codificación de un flujo de bytes es algo muy
útil cuando lidiamos con sistemas de codificación. No hay ningún
mecanismo determinista que permita conocer la codificación de un flujo
de bytes, pero se puede llegar a saber con un nivel suficientemente alto
de certidumbre.
La fundación Mozilla liberó una
biblioteca de código que hace precisamente eso, aunque debido a la
naturaleza estocástica del problema requiere un flujo con un número
relativamente alto de bytes. Podéis encontrarla en los repositorios de
código de Google: http://code.google.com/p/juniversalchardet/ .²

Convertir codificación de texto

Plantearé dos casos, convertir un objeto String a un objeto byte[], y convertir un objeto byte[] a un objeto String. El primer caso es muy sencillo, usaremos el método getBytes de la clase String pasándole el nombre de la codificación como parámetro:
String originalText = "Españistán";
byte[] utf8BytesStream = originalText.getBytes("UTF8");
Suponiendo que tengamos un array de
bytes que representen una cadena de texto y que además conozcamos cual
es su codificación, podemos recuperar el objeto String correspondiente de la siguiente manera:
String copyText = new String(utf8BytesStream, "UTF8");
// copyText.equals(originalText) == true
Fijáos en que donde he usado "UTF8" podría haber usado alguna de las otras codificaciones soportadas por Java, podéis verlas en la documentación de la clase Charset (notad que también podríamos pasar un objeto Charset en vez de la cadena "UTF8").

Normalización de cadenas de texto Unicode

Unicode presenta algunas características
peculiares que, aunque interesantes, nos pueden traer fuertes dolores
de cabeza. Una de ellas es la posibilidad de presentar ciertos
caracteres acentuados o con marcas como, o bien un solo símbolo, o la
combinación de dos caracteres, uno para el símbolo en sí, y otro para su
correspondiente marca.
Por poner un caso, el texto "á" podría estar formado por uno o dos caracteres, aunque nosotros siempre veamos uno solo. Lo mismo sucede con caracteres como 'ñ' o 'ç'.
Los primeros problemas que a uno le
pueden venir a la mente son relativos a búsquedas y a comparaciones, yo
no he encontrado ninguno más, pero estos ya son suficientes. Por suerte,
Java provee la clase Normalizer,
que nos permite solucionar esos inconvenientes, o bien normalizando el
texto a su forma “compacta”, o bien normalizando el texto a su forma
“expandida” (lo que puede resultar útil, como veremos).
Antes de aplicar la normalización es
conveniente comprobar si el texto está ya normalizado o no, para evitar
cálculos inútiles y reducir la probabilidad de destrozar nuestros datos.
He aquí un ejemplo con los dos tipos de
normalización que he comentado (hay dos más, llamadas de
“compatibilidad”, pero por lo general no resultarán útiles):
// Normalización "compacta"
if (!Normalizer.isNormalized(text, Normalizer.Form.NFC)) {
    text = Normalizer.normalize(text, Normalizer.Form.NFC);
}

// Normalización "expandida"
if (!Normalizer.isNormalized(text, Normalizer.Form.NFD)) {
    text = Normalizer.normalize(text, Normalizer.Form.NFD);
}

Eliminar acentos y sustituir caracteres especiales

A veces podemos tener la necesidad de
eliminar acentos, diéresis, y otras marcas de las letras de nuestros
textos… o bien cambiar algunos carácteres “especiales”, como la ñ, o la ç, por sus versiones más frecuentes, como la n o la c.
Para conseguirlo haremos uso de la
normalización “expandida” que he explicado en el apartado anterior. En
primer lugar, aplicamos la normalización “expandida”, y después usamos
el método replaceAll de la clase String de la siguiente manera:
text = text.replaceAll("\\p{IsM}", "");
Aunque esta línea puede resultar
atractiva por su sencillez, no se trata del método óptimo. Lo ideal
sería compilar la expresión regular usada como primer parámetro para
reutilizarla cada vez que queramos eliminar acentos. Esto es así porque
el método replaceAll recompila la expresión regular cada vez que es
llamado. Debería quedar algo así:
// Este objeto debería guardarse para su posterior reutilización
Pattern isMPattern = Pattern.compile("\\p{IsM}");

// Esta es la parte que tendremos que hacer siempre
text = isMPattern.matcher(text).replaceAll("");
Fijémonos en la expresión regular usada, \p{IsM} , hará matching con 3 tipos de marcas: non spacing marks, spacing marks y enclosing marks.³
Los hay que usan la expresión regular \p{InCombiningDiacriticalMarks}+ , pero por lo visto no cubre todos los caos posibles.


Escapar caracteres Unicode

A veces, aun con todas las herramientas
expuestas en los apartados anteriores, necesitaremos escapar (y
desescapar) nuestros caracteres Unicode para conseguir compatibilidad
con el sistema de codificación ASCII.
Volvemos a tener suerte, esta vez
gracias a las librerías Apache Commons. No me andaré por las ramas, pues
un snippet de código vale más que mil palabras:
import org.apache.commons.lang3.text.translate.UnicodeEscaper;
import org.apache.commons.lang3.text.translate.UnicodeUnescaper;

void metodo () {
    UnicodeEscaper         escaper     = UnicodeEscaper.above(127);
    UnicodeUnescaper     unescaper     = new UnicodeUnescaper();

    String textoProblematico = "Música";

    String textoEscapado = escaper.translate(textoProblematico);
    // textoEscapado == "M\\u00FAsica", que se imprime como "M\u00FAsica"

    String textoDesescapado = unescaper.translate(textoEscapado);

    // textoDesescapado == textoProblematico
}
Fijaos en que cuando construyo el objeto UnicodeEscaper
lo hago pasando el número 127 al método above de la misma clase. Esto
lo hago para forzar que solo me escape los caracteres con valor numérico
mayor a 127, dejando intactos los caracteres que coinciden con el
antiguo sistema de codificación ASCII.
Si queréis descargar esta librería, podéis hacerlo desde el siguiente enlace : http://commons.apache.org/proper/commons-lang/download_lang.cgi. Os recomiendo usar la última versión, que a día de hoy es la 3.1 .

Conclusiones

Aunque hoy no os he traído nada del otro
mundo, se trata de pequeños detalles que al menos a mi, me costaron
descubrir en su momento… Así que espero estar ayudando a alguien con
este pequeño compendio :) , así como que os haya gustado. Saludos!