Détection de l’encodage des caractères avec NUniversalCharDet

Le Framework .Net permet de lire facilement et rapidement un fichier texte grâce à la fonction ReadAllLines().

Facilement, oui, mais il peut y avoir de mauvaises surprises au niveau du codage des caractères (j’utiliserai le mot « encodage » dans la suite de cet article). En effet, les caractères de la phrase « Le garçon ne pouvait être là cet été » peuvent vite devenir « Du Le garçon ne pouvait être là cet été » si le codage n’a pas été bien précisé à la lecture.

La fonction ReadAllLines() propose en effet deux prototypes, dont l’un demande en argument l’encodage du contenu du fichier (argument de la classe System.Text.Encoding). Et ce fameux encodage n’est pas simple à déterminer.

Nous allons donc voir comment détecter, ou plutôt « déduire » l’encodage des caractères d’un fichier texte, et ce grâce à la bibliothèque « nuniversalchardet », issue de la bibliothèque « universalchardet » de Mozilla.

Quelques définitions

Voici quelques précisions techniques, sans toutefois entrer dans les détails (taille des caractères à l’échelle des bits, définitions des normes, etc.).

Par défaut, sous Windows et en français, les fichiers textes sont encodés en ANSI, alias Windows-1252, alias CP1252. Ce jeu de caractère, dédié aux langues d’Europe de l’Ouest, est une extension de la norme ISO/CEI 8859-1, elle-même également connue sous le nom de « Latin-1 » ou « Européen occidental ».

Bref, ce jeu de caractère est donc limité à certains caractères, certes rarement utilisés en français, mais indispensables pour des applications à vocation internationales par exemple.

De ce fait, avec l’encodage ANSI, impossible d’écrire en cyrillique, hébreu, arabe, chinois, etc.

Exemple plus marquant : le nom « Ibrahimović » ne pourrait pas être orthographié correctement en ANSI puisque le caractère « ć » (C accent aigu) n’est pas reconnu par ANSI. Preuve qu’il est indispensable d’utiliser l’UTF-8 !

Vous pouvez faire le test avec NotePad++ :

  • Créez un nouveau fichier ;
  • Allez dans « Encodage », « Encoder en ANSI » ;
  • Copiez-collez le terme « Ibrahimović » dans NotePad++ : le « c » perd son accent.
  • Renouvelez l’opération en choisissant l’option « Encoder en UTF-8 (sans BOM) » : le « ć » garde son accent. On notera qu’en bas à droite, NotePad++ indique « ANSI as UTF-8 ».

Quelle différence entre la détection et la « déduction » ?

Il y a deux façons de connaître l’encodage des caractères d’un fichier :

C’est pour cette dernière que la bibliothèque de Mozilla intervient (bien qu’elle remplisse son rôle pour les deux cas).

En effet, si la lecture du BOM est simple, la déduction nécessite quant à elle de comparer tous les octets du fichier texte avec des caractères de références (tous les caractères des langages du monde). La création des caractères de référence demande donc un travail titanesque sur un domaine qui n’est pas directement complètement à l’informatique, mais plutôt à la linguistique. Et Mozilla a bien dû concocter un algorithme de comparaison optimisé. Les performances sont en effets tout à fait acceptables !

Autant ne pas réinventer la roue, et utiliser cette merveilleuse bibliothèque : http://code.google.com/p/nuniversalchardet/downloads/detail?name=UniversalCharDet.rar&can=2&q=

Nous allons donc voir comment l'utiliser en C# avec le Framework. Net.

Exemple de code

Au plus simple, la fonction ReadAllLines() est utilisée ainsi :

var content = File.ReadAllLines(filePath); // Encodage du système par défaut

Si l’on connaît l’encodage des caractères du ficher, on l’utilise comme ceci :

var content = File.ReadAllLines(filePath, Encoding.UTF8);

Et donc, je vous propose un exemple d’utilisation comme suit :

var content = File.ReadAllLines(filePath, GetEncodingWithNUniversalCharDet(filePath));

La fonction GetEncodingWithNUniversalCharDet() prend en paramètre le chemin du fichier, et retourne une énumération de la classe System.Text.Encoding.

Voici le code de cette fonction :

       public static Encoding GetEncodingWithNUniversalCharDet(string filePath)
       {
           Encoding detectedEncoding = Encoding.Default;
           FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
           MemoryStream msTemp = new MemoryStream();
           int len = 0;
           byte buff = new byte512;
           while ( ( len = fileStream.Read(buff, 0, 512 ) ) > 0)
           {
               msTemp.Write(buff, 0, len);
           }
           if (msTemp.Length > 0)
           {
               msTemp.Seek(0, SeekOrigin.Begin);
               byte PageBytes = new bytemsTemp.Length;
               msTemp.Read(PageBytes, 0, PageBytes.Length);
               msTemp.Seek(0, SeekOrigin.Begin);
               int DetLen = 0;
               byte DetectBuff = new byte4096;
               UniversalDetector universalDetector = new UniversalDetector(null);
               while ( ( DetLen = msTemp.Read(DetectBuff, 0, DetectBuff.Length)) > 0 && !universalDetector.IsDone() )
               {
                   universalDetector.HandleData(DetectBuff, 0, DetectBuff.Length);
               }
               universalDetector.DataEnd();
               var detectedCharset = universalDetector.GetDetectedCharset();
               if (detectedCharset != null)
               {
                   if (detectedCharset == Constants.CHARSET_UTF_8)
                   {
                       detectedEncoding = Encoding.UTF8;
                   }
                   else if (detectedCharset == Constants.CHARSET_UTF_16BE)
                   {
                       detectedEncoding = Encoding.BigEndianUnicode;
                   }
                   // TODO : compléter avec les autres types d'encodage si besoin
               }
               universalDetector.Reset();
           }
           return detectedEncoding;
       }        public static Encoding GetEncodingWithNUniversalCharDet(string filePath)
       {
           Encoding detectedEncoding = Encoding.Default;
           FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
           MemoryStream msTemp = new MemoryStream();
           int len = 0;
           byte buff = new byte512;
           while ( ( len = fileStream.Read(buff, 0, 512 ) ) > 0)
           {
               msTemp.Write(buff, 0, len);
           }
           if (msTemp.Length > 0)
           {
               msTemp.Seek(0, SeekOrigin.Begin);
               byte PageBytes = new bytemsTemp.Length;
               msTemp.Read(PageBytes, 0, PageBytes.Length);
               msTemp.Seek(0, SeekOrigin.Begin);
               int DetLen = 0;
               byte DetectBuff = new byte4096;
               UniversalDetector universalDetector = new UniversalDetector(null);
               while ( ( DetLen = msTemp.Read(DetectBuff, 0, DetectBuff.Length)) > 0 && !universalDetector.IsDone() )
               {
                   universalDetector.HandleData(DetectBuff, 0, DetectBuff.Length);
               }
               universalDetector.DataEnd();
               var detectedCharset = universalDetector.GetDetectedCharset();
               if (detectedCharset != null)
               {
                   if (detectedCharset == Constants.CHARSET_UTF_8)
                   {
                       detectedEncoding = Encoding.UTF8;
                   }
                   else if (detectedCharset == Constants.CHARSET_UTF_16BE)
                   {
                       detectedEncoding = Encoding.BigEndianUnicode;
                   }
                   // TODO : compléter avec les autres types d'encodage si besoin
               }
               universalDetector.Reset();
           }
           return detectedEncoding;
       }

Ce code provient du projet de démo de la bibliothèque NUniversalCharDet. N’ayant pas trouvé de documentation, j’ai repris leur exemple et l’ai adapté pour retourner un élément Encoding.

Cette adaptation est située au niveau de la comparaison entre la variable « detectedCharset » et les constantes proposées par la bibliothèque (seulement deux cas dans cet exemple).

Vous remarquerez le « TODO ». Il y a en effet ici seulement les cas qui étaient à première vue compatibles avec les types d'encodage du Framework .Net (et surtout, seulement les cas qui me semblaient utiles).

Conclusion

Le but de cet article, au-delà de l'exemple de code, était donc de voir que le sujet de l'encodage des caractères est assez problématique.

Il existe un moyen sûr de connaître l'encodage des caractères d'un fichier grâce au BOM, mais celui-ci n'a pas eu l'essor escompté, et peut même poser problème pour un logiciel qui détecte alors les caractères du BOM comme des caractères illisibles.

Et paradoxalement, sans BOM, l'encodage des caractères reste flou puisqu'il est « deviné »... C'est en tout cas, et a priori, comme cela que fonctionnent les logiciels que nous utilisons tous les jours (en fonction des affinités), tels que NotePad++ et la commande « file » sous Linux.

Quid de l’UTF-16 et de l’UTF-32 ?

C'est une bonne question, mais elle ne sera pas traitée ici. Il faut juste garder à l’esprit que ces encodages existent également et proposent beaucoup plus de caractère que l’UTF-8.

Quelques liens

nuniversalchardet (C# Port of UniversalCharDet) :
http://code.google.com/p/nuniversalchardet/

Le code de démo de nuniversalchardet :
http://code.google.com/p/nuniversalchardet/downloads/detail?name=NUniversalCharDet.rar&can=2&q=

L'original :
http://code.google.com/p/uchardet/

Documentation MSDN :
http://msdn.microsoft.com/fr-fr/library/system.io.file.readalllines(v=vs.110).aspx

Quelques liens wikipédia :
http://fr.wikipedia.org/wiki/Windows-1252
http://fr.wikipedia.org/wiki/ISO/CEI_8859-1
http://en.wikipedia.org/wiki/ANSI_%28disambiguation%29

Un article en français :
http://www.paperblog.fr/1880790/l-encodage-utf-8-avec-bom-fait-apparaitre-un-i-en-haut-de-page/

Quelques articles en anglais :
http://blogs.msdn.com/b/oldnewthing/archive/2007/04/17/2158334.aspx
http://www.joelonsoftware.com/printerFriendly/articles/Unicode.html

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.