vendredi 8 mai 2015

Comprendre les problèmes d'encoding en java

Il m'arrive souvent d'intervenir sur les problèmes d'encoding. Le constat que je fais c'est que la plupart du temps les équipes connaissent mal les concepts autour de l'encoding, ce qui rend le debuggage laborieux.

Pourtant ça n'a rien de bien compliqué. Je vais d’abord vous faire un petit rappel des concepts puis ensuite je vous montrerai quelques exemples d'accidents, en général une fois qu'on a compris ce qu'il se passait la résolution n'est plus très loin.

2 concepts majeurs: l'encoding et la table des symbôles

La table des symbôles

La table des symbôles n'est pas un concept informatique à proprement parlé il s'agit d'un tableau ou pour être plus exact d'une liste exhaustive de tous les symbôles qu'une plateforme applicative décide de gérer il n'est pas question ici d'octet de mémoire ou autre mais juste de symbôles.

En java la table des symboles utilisée est l'unicode. Unicode est aujourd'hui un standard de l'industrie.


L'encoding

L'encoding c'est ce qui fait le lien entre le symbôle et l'octet. Par exemple le symbôle "é" e accent aigu à pour valeur  en octet 
  • En ISO-8859-1 :   -23
  • En UTF-8 :            -61 -87
  • En UTF-32 :          0 0 0 -23
Le é en unicode c'est à dire dans la table des symbole a pour valeur U00E9 en java voici le code que j'ai utilisé pour obtenir le résultat précédent : 

 public static void main(String[] args) {
  System.out.println(showBytes("\u00E9","ISO-8859-1"));
  System.out.println(showBytes("\u00E9","UTF-8"));
  System.out.println(showBytes("\u00E9","UTF-32"));
 }
 
 private static String showBytes(String str, String encoding){
  String result = "";
  try {
   for (byte b : str.getBytes(encoding)){
    result += " " + b;
   }
  } catch (UnsupportedEncodingException e) {
   e.printStackTrace();
  }
  return result;
 }

Les accidents

Supposons que j'écrive le "é" dans un fichier en utilisant l'encoding ASCII puis que je charge les byte du fichier dans un string en utiisant le mauvais encoding. Voici un exemple de ce que cela peut donner :



 On peut émuler toutes ces erreurs avec le code suivant
 
byte[] e_accent_aigu_en_ASCII = "\u00E9".getBytes("ASCII");
byte[] e_accent_aigu_en_ISO_8859_1 = "\u00E9".getBytes("ISO-8859-1");
byte[] e_accent_aigu_en_UTF_8 = "\u00E9".getBytes("UTF-8");
byte[] e_accent_aigu_en_UTF_32 = "\u00E9".getBytes("UTF-32");
  
System.out.println("ASCII byte, ASCII encoding = " + new String(e_accent_aigu_en_ASCII,"ASCII"));
System.out.println("ASCII byte, ISO-8859-1 encoding = " + new String(e_accent_aigu_en_ASCII,"ISO-8859-1"));
System.out.println("ASCII byte, UTF-8 encoding = " + new String(e_accent_aigu_en_ASCII,"UTF-8"));
System.out.println("ASCII byte, UTF-32 encoding = " + new String(e_accent_aigu_en_ASCII,"UTF-32"));
System.out.println("ISO-8859-1 byte, ASCII encoding = " + new String(e_accent_aigu_en_ISO_8859_1,"ASCII"));
System.out.println("ISO-8859-1 byte, ISO-8859-1 encoding = " + new String(e_accent_aigu_en_ISO_8859_1,"ISO-8859-1"));
System.out.println("ISO-8859-1 byte, UTF-8 encoding = " + new String(e_accent_aigu_en_ISO_8859_1,"UTF-8"));
System.out.println("ISO-8859-1 byte, UTF-32 encoding = " + new String(e_accent_aigu_en_ISO_8859_1,"UTF-32"));
System.out.println("UTF-8 byte, ASCII encoding = " + new String(e_accent_aigu_en_UTF_8,"ASCII"));
System.out.println("UTF-8 byte, ISO-8859-1 encoding = " + new String(e_accent_aigu_en_UTF_8,"ISO-8859-1"));
System.out.println("UTF-8 byte, UTF-8 encoding = " + new String(e_accent_aigu_en_UTF_8,"UTF-8"));
System.out.println("UTF-8 byte, UTF-32 encoding = " + new String(e_accent_aigu_en_UTF_8,"UTF-32"));
System.out.println("UTF-32 byte, ASCII encoding = " + new String(e_accent_aigu_en_UTF_32,"ASCII"));
System.out.println("UTF-32 byte, ISO-8859-1 encoding = " + new String(e_accent_aigu_en_UTF_32,"ISO-8859-1"));
System.out.println("UTF-32 byte, UTF-8 encoding = " + new String(e_accent_aigu_en_UTF_32,"UTF-8"));
System.out.println("UTF-32 byte, UTF-32 encoding = " + new String(e_accent_aigu_en_UTF_32,"UTF-32"));
  

Quelques points qu'il faut avoir en-tête

On voit que le "é" en ASCII n'existe pas, java l'a remplacé par le le byte du "?" en ASCII qui a pour valeur 63.

Quand java exécute String.equals,  String.contains ou String.match en interne il le fait symbole par symbole et non byte par byte comme peuvent le faire certains langage.

Enfin UTF-8 est un encodage qui peut être sur 1, 2, 3 ou 4 bytes ce qui signifie que les algorithmes qui vont interpréter les fichiers encodés en UTF-8 seront plus complexes à mettre en oeuvre que les algorithmes qui devront interpréter UTF-16 ou UTF-32 qui eux sont soit sur 2 ou 4 bytes pour chaque caractères. UTF-8 va partager le même encodage que ISO-8859-1 pour tous les caractères ascii ce qui permet en français de gérer des fichiers beaucoup plus légers car on ajoute des bytes en plus que pour les caractères non ascii.

Mais pour un texte entièrement en chinois ou en arabe UTF-32 permettra une interprétation plus rapide. Cela peut devenir très significatif pour un livre entier par exemple.