vendredi 19 mars 2010

Une tour de Babel moderne: les "charsets" en PHP

Vaste sujet, qui mérite les nombreuses pages qui lui sont consacré: la gestion des caractères étendues en PHP. Le texte ci-dessous est une rapide explication d'une méthode permettant d'éviter les problèmes d'affichage liés à ces caractères.

De quel problème s'agit-il?
Quand je parle d'un caractère étendue, je parle naturellement des caractères spéciaux, ceux traditionnellement codés avec une code ASCII supérieur à 127. Ces caractères étendues sont normalisés à travers des centaines de "charset" différents et spécifiques à chaque pays. Je désigne par "Charset", une table de correspondance de 256 caractères, sachant que les 127 premiers sont normalisés.
Voici pour l'exemple une série de caractères posant problème:
é É è È ê Ê ë î ï ç Ç ç Ç æ œ ä Ä ö €
Cette liste est un florilège des caractères pouvant s'afficher bizarrement. Si vous pouvez lire ces caractères spéciaux sans signe cabalistique, cela signifie que blogger.com gère très bien cette douloureuse problématique (ce dont je ne doutais pas). Premier soucis, les "charsets" les plus populaires ne possède pas en même temps tous ces caractères. Il manque souvent le signe monétaire €, ou le œ("e dans l'o"). Ces spécificités françaises rend donc délicat l'usage de ces "charsets".

Si vous sauvegardez cette liste de caractères sous "Notepad" (de Windows), vous avez plusieurs solutions de format de caractère à votre disposition: ANSI, unicode, UTF-8. Le premier est un charset propriétaire, popularisé par Windows, dérivant de l'ISO-8859-1 par quelques caractères. C'est un standard de fait comme on voit si souvent. Problème, il n'est pas apprécié par PHP. En revanche l'ISO-8859-1 (appelé aussi par son surnom "Latin 1"), est adoré par PHP, mais certains caractères manquent à l'appel. Il faut souvent utiliser les petits frères "Latin 2" (apprécié par certain éditeur tel mon PSPad) ou encore "Latin 9".

Pourquoi se soucier de tout cela? Pour au moins deux raisons: les fichiers php de votre site, et les fichiers textes coté serveur, chacun contenant potentiellement des chaines de caractères à afficher. Le problème est le même avec mysSQL par exemple. Si ces textes contiennent des caractères étendus, il faut pouvoir les afficher correctement coté navigateur internet. C'est pour cela que l'on a besoin de connaitre un minimum de chose sur les "charsets", sinon il y a un risque de voir des caractères sibyllins sur son navigateur à la place des caractères étendus attendus ;).

Un peu de pratique
Pour illustrer le problème, voici un exemple d'une ligne PHP, présente dans un fichier texte sauvegardé au format UTF-8:
echo ('é É è È ê Ê ë î ï ç Ç ç Ç æ œ ä Ä ö €');
Pour que cette ligne s'affiche correctement, il faut prévenir le navigateur du "charset" employé. Il existe pour cela deux moyens (là où un aurait suffit):
une directive HTML de type META:
<meta equiv="content-type" content="text/html; charset=utf-8">
Ou alors une ligne dans le header HTTP que l'on spécifie en PHP par une ligne:
header('Content-Type: text/html; charset=utf-8');

La deuxième méthode prédomine sur la première, ce qui oblige le développeur PHP de faire les deux pour être certain d'être bien compris.
Voici quelques exemples d'affichage exotique pouvant s'afficher:
é É è È ê Ê ë î ï ç Ç ç Ç æ œ ä Ä ö €
ou bien:
é � è � ê � ë î ï ç � ç � æ � ä � ö �

La problématique est la même pour un fichier dont on doit afficher une partie de son contenu. Il faut d'une part lire correctement le fichier en PHP et ensuite l'afficher correctement (toujours en PHP). Pour cela il faut prévenir PHP de la nature du fichier, et ensuite prévenir le navigateur.

Pour finir, il ne faut pas oublier la problématique des formulaires HTML et de leurs champs de saisie. Là encore, il faudra prendre en considération le format qui sera utilisé pour récupérer et traiter correctement les informations correspondantes à travers les variables $_POST. Il faut également afficher correctement les caractères étendues dans le formulaire, mais dans ce cas il n'y a pas de choix, car seul les entités HTML le permettent. Je ne rentre pas dans les détails car seul le principe général est intéressant pour l'instant.

Dernier point critique à signaler: la plupart des fonctions de gestion de caractères en PHP sont incompatible avec les "unicode" ou autre "UTF8", car PHP nativement manipule uniquement les caractères codé sur un octet, et gère donc très mal les caractères multi-octets. L'UTF-8, l'unicode sont donc à déconseiller dès lors que l'on a besoin d'utiliser les nombreuses fonctions PHP de gestion de caractères.

Une solution théorique serait d'utiliser à la place de tous ces formats dignes de la tour de Babel, un langage universel qui existe dans le monde HTML: les entités HTML. Cet ensemble de méta-caractères HTML permet d'afficher par exemple l'accent "é" en utilisant une série de caractère commençant par & et finissant par un point virgule: "&eacute;". Ces méta-caractères parfaitement compatibles avec la table ASCII originelle, sont un moyen élégant de contourner les problèmes de format de caractères. Malheureusement cette solution est incompatible avec la plupart des éditeurs de texte, même les plus performant comme PSPad. Il est donc déconseillé d'écrire soi-même les entités HTML, surtout si l'on veut par la suite éditer facilement ces caractères étendus. En revanche, ces entités sont excellentes en tant que format d'affichage, puisque tous les navigateurs sont compatibles avec ces entités et ne poseront jamais de problème. Les entités HTML sont également indispensables comme format d'affichage dans un formulaire. Elles sont une solution pratique dans de nombreux cas, surtout que PHP gère nativement le codage et le décodage des ces entités HTML.

Quand faut-il s'en préoccuper
:
En résumé, ce problème de caractères étendues doit vous préoccuper dans au moins 3 cas:
  1. dans un fichier php, quand vous devez afficher des chaines de caractères
  2. dans un fichier (ou une base), quand vous y stocker des chaines de caractères à afficher
  3. dans un formulaire, au moment de récupérer les champs de saisie.
Personnellement, après avoir commencé à utiliser le format de fichier UTF8 pour son universalité et sa compatibilité avec l'ASCII, j'ai commencé à faire marche arrière pour plusieurs raison: les navigateurs pouvait parfois être pris de cours, en voulant tout le temps afficher de l'UTF8. De plus certains fichiers étant destinés à être modifiés directement par notepad ou par un autre éditeur, je voulais un format universel pour Windows. Le format "ANSI" (autrement dit "Windows-1252") m'a semblé plus pratique. Je n'ai pas choisi l'ISO-8859-1 (autrement dit "Latin-1") malgré sa popularité, car certains caractères peuvent poser problème, un peu plus que le "windows-1252". De plus le "latin-1" n'est pas toujours pris en compte dans les éditeurs. PSPad en particulier, ne gère que le "latin-2" dans cette famille, alors que tout logiciel sous Windows n'a aucun problème pour gérer l'"ANSI". Voici donc trois exemples, en sachant que tous mes fichiers coté serveur son sauvegardé en ANSI.

1- Pour afficher des caractères dans PHP
, il suffit normalement de faire:
echo ('é É è È ê Ê ë î ï ç Ç ç Ç æ œ ä Ä ö €');
Pour afficher correctement, je modifie légèrement le source:
echo (htmlentities('é É è È ê Ê ë î ï ç Ç ç Ç æ œ ä Ä ö €')ENT_NOQUOTES,'cp1252');

2- Pour afficher des caractères d'un fichier ANSI, il suffit normalement de faire:
$f=fopen(,"r");
$ligne= fgets($f, 4096);
echo ($ligne);
Pour afficher correctement, je modifie légèrement le source de la même manière:
$f=fopen($txt,"r");
$ligne= fgets($f, 4096);
echo (htmlentities($ligne,ENT_NOQUOTES,'cp1252'));

3- Pour récupérer correctement le champ de saisies:
Rien de plus simple, car il suffit seulement de dire au navigateur quel est le "charset" que l'on veut utiliser dans la page. C'est le navigateur qui s'occupe ainsi d'envoyer le texte avec le format spécifié dans page hébergeant le formulaire. Donc si le même "charset" est utilisé dans le formulaire, et dans la page qui affiche le résultat, il n'y aura pas de problème.
Ne pas oublier de traduire en entité HTML le texte par défaut d'un champ de saisie. Ces entités seront traduites automatiquement en caractères UTF-8 si par exemple c'est l'UTF-8 qui est spécifié dans la page du formulaire. C'est assez déroutant, mais finalement fort pratique.
En résumé il suffit pour être tranquille de mettre dans toutes vos pages une ligne de ce type, en y spécifiant votre "charset" préféré:
<meta equiv="content-type" content="text/html; charset=utf-8">

Et que fait donc blogger.com?
Le gestionnaire de blog de google a choisie la facilité en utilisant dans ses pages le charset UTF-8, et en laissant le navigateur coté client gérer toutes les problématiques de caractères exotiques. C'est évidement la solution la plus sage, car blogger a besoin d'être véritablement universel. Devoir gérer un transcodeur UTF-8=> meta-caractères HTML aurait alourdi le gestionnaire lui-même, ainsi que les pages HTML de ses blogs. En revanche, pour un site purement personnel et pour un usage limité à une seule langue, on peut très bien choisir un charset "ANSI". Le transcodage en entités HTML est ensuite un choix plus philosophique que technique, que j'adopte plus pour me faire plaisir que pour l'efficacité. Toutes les méthodes sont bonnes, à la condition de bien en maitriser les tenants et les aboutissants. C'est d'ailleurs le but de ce documents: expliquer le principe et les limites d'une méthode parmi d'autres.

Conclusion:
L'UTF-8 est un bon compromis, mais relativement dangereux en PHP de par sa mauvaise gestion des chaines de caractères multi-octets. En final, comme PHP ne gère pas beaucoup de charset, le choix est fort limité (voir la liste complète ci-dessous). Pour ma part, j'utilise l'ANSI de Windows (alias "cp1252" qui est en fait son nom officiel) car mes éditeurs de texte sont sous Windows. Ce "charset" n'est pas le meilleur, mais très pratique sous Windows. L'unicode de notepad est une catastrophe à cause de sa non-compatibilité avec les autres formats (il ajoute un zéro binaire à la fin de chaque caractère). En final, un des meilleurs moyens de ne pas se tromper est d'utiliser dès que cela est possible les entités HTML qui possède le double avantage d'être véritablement universelles, et compatibles avec tous les "charsets" et les navigateurs. Pour cela, il faut connaitre les deux fonctions PHP prévues à cet usage. Évitez d'écrire vous-même les entités HTML, car les sources d'erreur sont alors nombreuses et le modifications peu pratiques:
htmlentities() => traduit tout caractère étendue d'une chaine à partir d'un "charset". Il ne faut pas oublier de spécifier l'argument "charset", sinon le résultat peut-être aléatoire.
htmlspecialchars() => permet principalement d'afficher un code HTML, en traduisant en entités HTML les caractères <>' et ". Les caractères étendues ne sont pas traduits avec cette fonction. Attention donc à ne pas la confondre avec la précédente. Utile également dans le cadre d'un formulaire au moment de spécifier le texte par défaut d'un champ de saisie, qui lui doit s'inscrire à l'intérieur de tags HTML.
Je résume ma méthode en une phrase:
Format "ANSI" pour tous les fichiers coté serveur (php ou autre), et substitution de tous les caractères étendues par des entités HML pour toutes les pages HTML coté client.
Ce principe n'est pas le plus efficace, mais c'est celui que je maitrise le mieux et qui me permet d'éviter tous ces caractères bizarres en lieu et place de nos chers caractères nationaux.

Bon courage à vous et pour vos caractères étendues, en espérant qu'ils ne vous trahiront plus à travers un navigateur internet.



Liens:
A l'origine de ce joyeux bordel: la table ASCII
Le charset "Windows-1252", abusivement appelé "ANSI" (utilisé par défaut dans mon Notepad)
Excellente synthèse des formats les plus important
La liste complète des charsets et de leur alias (pour les plus courageux)
Liste des entités HTML (pour l'oublier au plus vite mis à part deux ou trois).


Liste des charsets gérés sous PHP:
  • ISO-8859-1(Latin-1)
  • ISO-8859-15(Latin-9, signe Euro, et quelques caractères français manquant au Latin-1)
  • UTF-8 (Unicode 8 bits multioctets, compatible avec l'ASCII)
  • cp866 (ibm866 Jeu de caractères Cyrillic spécifique à DOS, depuis PHP 4.3.2)
  • cp1251 (Windows-1251 depuis PHP 4.3.2)
  • cp1252 (Windows-1252,spécifique Windows pour l'Europe occidentale=> ANSI)
  • KOI8-R (Russe depuis PHP 4.3.2)
  • BIG5 (Chinois traditionnel, utilisé à Taïwan)
  • GB2312 (936 Chinois simplifié, officiel)
  • BIG5-HKSCS (extensions de Hong Kong, chinois traditionnel)
  • Shift_JIS ( 932 Japonais)
  • EUC-JP (Japonais)

PS:
Pour écrire ce document il m'a valu par deux fois utiliser des entités HTML sans quoi blogger m'interdisait la sauvegarde ou bien un affichage correcte:
1-au moment de citer le tag META, il a valu remplacer les signes "<" et ">" (plus petit et plus grand que) par leur équivalent sous forme d'entité HTML.
2- au moment d'afficher un exemple d'entité, utilisation de l'entité HTML "et commercial".
Comme quoi, il est toujours utile de connaitre par cœur quelques entités HTML.

1 commentaire:

Philippe Lalanne a dit…

Bonjour,
je n'ai qu'une seule chose à dire !!! MERCIIIIIIIIIIII

je connaissais bien le probleme avec utf8_encode et decode et ajax et je l'vvais bien compris mais par contre la lecture d'un fichier ansi par fopen !! ca faisiant plusieurs jours que j'etais dessus !!! Encore merci !!!