Cet article décrit
ce qu'est un encodage, quel usage en est fait en XML, puis comment Perl les traite, avant de finir
par quelques exemples pratiques d'utilisation de XML::Parser avec différents documents.
Michel Rodriguez
Somerville,
dimanche 7 mai 2000
Cet article a été publié sur XML.com sous le titre "Character Encodings in XML and Perl".
Introduction
Le sujet du jour est: encodages et Perl en général et XML::Parser en particulier. Cet article décrit
ce qu'est un encodage, quel usage en est fait en XML, puis comment Perl les traite, avant de finir
par quelques exemples pratiques d'utilisation de XML::Parser avec différents documents.
Un mot d'avertissement préliminaire: ceci est un sujet brûlant, qui génère moult débats
enflammés sur les mailing listes XML, généralement entre les apôtres du "tout-Unicode" et les
tenants du " je bosse moi, M'sieur!".
Encodages
Ah les encodages! La face cachée d'XML. Pour beaucoup, surtout aux US, XML est un format
qui décrit des éléments, des attributs, et la manière de les ordonner proprement dans une jolie
structure arborescente.
Mais en vérité je vous le dis, pour pouvoir stocker un texte, la première chose à faire c'est de
définir son encodage. Le plus connu des encodages, du moins en occident est bien sûr ASCII,
EBCDIC rappellera à certains d'entre vous le bon vieux temps où IBM et ordinateur étaient
synonymes, tandis que Shift-JIS est l'un des encodages japonais et Big5 un des chinois.
Tous ces encodages ont en commun d'être largement... incompatibles. Ceci pour diverses
raisons, la première étant que les langues occidentales peuvent se contenter de 256 caractères
codés sur 8 bits, alors que bon nombre de langues asiatiques nécessitent bien plus, et donc
requièrent un encodage sur plusieurs octets. Un standard récent tente d'unifier et de remplacer
ces divers encodages: Unicode, alias ISO-10646 (en fait Unicode est le produit du consortium
Unicode et ISO-10646 est publie par l'ISO, mais ces 2 standards sont largement compatibles et
dans cet article nous les considérerons équivalents
Unicode et UTF-8
Unicode est un standard dont le but est de remplacer tous les autres standards par un encodage
unique, extensible de surcroît, Unicode couvre tous les langages modernes, occidentaux comme
asiatiques, ainsi qu'une large gamme de symboles mathématiques et techniques (plus besoin de
GIFs pour les lettres grecques dans les standards IEEE!). Le mécanisme d'extension permet
l'addition de nouveaux caractères (comme les caractères crées en chinois chaque année) ou
nouveaux langages.
Sous Unix, Unicode est habituellement... encodé en UTF-8, une couche supplémentaire qui
permet aux systèmes implémentant POSIX de le traiter sans (trop de) problèmes. UTF-8 est
également rendu correctement par les browser web récents, à l'exception notable d'Opera. Pour
afficher des caractères non-ASCII il faut cependant que les fontes Unicode installées incluent ces
caractères. UTF-8 Sampler est une page contenant des textes dans différentes langues qui vous
permettra de tester vos fontes.
La plupart des systèmes d'exploitation incluent des fontes Unicode, plus ou moins complêtes:
par exemple, les fontes fournies par Windows 98 incluent exactement 1 caractère asiatique (Yen),
mais affichent (de gauche a droite au lieu du sens inverse qui serait correct) les caractères
hébreux , contrairement à ma station Solaris.
Au passage, pour le plus grand bonheur des anglais et américains, les caractères de base ASCII,
ceux encodés de 1 à 127, ont le même code en UTF-8, Donc un texte ASCII anglais est aussi un
texte UTF-8, sans conversion, ni traitement spécial... rien, ça marche!
Mon but dans cet article n'est pas de décrire Unicode en détail, n'étant pas un expert en la
matière. Pour plus d'informations (en anglais), voir Unicode Home Page ou UTF-8 and Unicode
FAQ for Unix/Linux.
Pour simplifier les choses sachez aussi qu'Unicode soulève certaines réticences en Asie: la fin de
l'article Brief History of Character Codes offre une opinion intéressante. La plupart des outils
traitant UTF-8 sont également limités à des caractères encodés sur au plus 2 octets alors que
certaines langues en demandent plus.
Encodages et XML
La spécification XML, disponible sur le site du W3C ou, accompagnée des commentaires de
Tim Bray sur XML.com ne prescrit aucun encodage particulier. Un document peut inclure
n'importe quel encodage de la liste définie par l' IANA. (the Internet Assigned Numbers
Authority).
Le seul problème est qu'un parser XML n'est obligé de reconnaître que 2 encodages: UTF-8 et
UTF-16 (une autre méthode pour encoder les caractères Unicode sur 2 octets, surtout employée
en Java).
Par exemple expat, le parser sur lequel XML::Parser est basé, reconnaît UTF-8, UTF-16 et
ISO-8859-1 alias latin 1 qui couvre la plupart des langues ouest-européennes et africaines (à
l'exception notable de l'arabe). Nous verrons comment l'étendre à d'autres encodages un peu
plus tard.
La plus importante caractéristique d'expat est qu'il convertit tous les caractères en UTF-8 quel
que soit l'encodage de départ du document, à condition qu'il soit reconnu. Une application basée
sur expat recevra tous les textes en UTF-8.
Perl et Unicode
A partir de la version 5.6 Perl supporte UTF-8. Ce qui veut dire notamment qu'il devient
possible d'utiliser les expressions régulières sans crainte que des caractères encodés sur plusieurs
octets ne posent problème.
En fait toutes les fonctions qui traitent des caractères ont été mises à jour pour se comporter
correctement avec des caractères multi-octets. Plus quelques extra, après tout Perl reste Perl.
Pour plus de détails sur les nouvelles fonctionnalités de Perl (et leurs limitations) voir l'article de
Simon Cozens: What's New in Perl 5.6.0?
Donc tout va bien ! Nos documents encodés en UTF-8 sont lus par expat, puis traités par
correctement par Perl... facile non ? Pas tout à fait...
Le problème est que la plupart des applications XML sont interfaces avec d'autres logiciels, bases
de données, traitement de texte... qui ne supportent pas Unicode et UTF-8. Ce qui force les
applications XML à non seulement accepter des entrées en divers encodages mais aussi à
produire des sorties de même, pour qu'elles soient utilisables par le reste de l'environnement.
C'est pourquoi nous allons maintenant détailler les outils Perl pouvant être utilisés pour
transcoder des documents XML.
XML::Parser et Unicode
Le modèle de traitement imposé par XML::Parser est le même que celui d'expat: quel que soit
l'encodage de départ les données sont transmises à l'application en UTF-8.
XML::Parser accepte nativement des documents en UTF-8, UTF-16 et ISO-8859-1. Pour
pouvoir traiter des documents encodés différemment le module XML::Encoding lui ajoute
d'autres tables d'encodages.
Pour pouvoir produire d'autres encodages en sortie il faut utiliser le module XML::UM, le
module Unicode::String ou une substitution (tr///). Une autre option, risquée mais plus rapide,
est d'utiliser la méthode d'XML::Parser qui fournit la chaîne que le parser a reconnue avant son
transcodage en UTF-8.
Ajouter des encodages grâce a XML::Encoding
Le module XML::Encoding peut être utilisé pour ajouter de nouveaux encodages à
XML::Parser.
XML::Encoding fournit des tables d'encodages utilisées par XML::Parser pour pouvoir parser des
documents dans d'autres encodages. Le simple ajout de l'instruction "use XML::Encoding"
donne accès a toutes les tables définies dans le répertoire source d'XML::Encoding.
La liste des encodages disponibles inclut notamment big5 (caractères chinois classiques),
iso-8859-2 à 9 (qui couvrent toutes les langues européennes, le cyrillique, l'arabe et l'hébreu,
quoique iso-8859-6, le jeu de caractères arabes semble poser problème), diverses variantes de
x-euc-jp et s-sjis (2 encodages japonais, le fichier Japanese Encodings.msg donne plus de détails sur
les variantes et la manière de les utiliser) et windows-1250. Le manque le plus crucial semble
celui de GB, l'encodage des caractères chinois simplifiés utilisés en Chine continentale.
La méthode permettant de définir d'autres encodages est donnée dans la documentation
d'XML::Encoding.
Le module Unicode::String
Le module Unicode::String permet la conversion entre caractères Unicode et latin 1:
$u= Unicode::String::utf8; # $utf8 est une chaîne UTF-8
$latin1= $u->latin1;
Unicode::String n'est plus nécessaire en Perl 5.6, l'opérateur tr suffit:
$string=~ tr/\0-\x{ff}//UC;
Le module XML::UM
Le module XML::UM utilise les tables d'encodage d'XML::Encoding pour effectuer l'opération
inverse. Il crée des fonctions de transcodage d'UTF-8 vers l'encodage voulu.
Ce module est encore alpha mais il vaut la peine d'être essayé. Il est assez lent, et si un volontaire
se propose pour le re-écrire en C il sera le bienvenu.
Note: la version d' XML::UM incluse dans libxml-enno-1.02 a un problème d' installation. Avant
de faire perl Makefile.PL, éditer le fichier XML::UM.pm dans le répertoire lib/XML et
remplacer la valeur de $ENCDIR par le répertoire contenant les tables d'encodage d'
XML::Encoding (quelque chose comme /usr/local/src/XML-Encoding-1.01/maps ou
/opt/src/XML-Encoding-1.01/maps/).
Un scénario typique se présente comme suit:
# créer la fonction de transcodage (une seule fois)
$encode= XML::UM::get_encode( Encoding => 'big5');
# convertir la chaîne $utf8_en big5
$encoded_string= $encode( $utf8_string);
Le module XML::Code
Pour encoder des documents dans des langues européennes autres que l'anglais tout en s'assurant
qu'ils pourront être au moins stockés proprement par des systèmes ne traitant que de l'ASCII-US
(les caractères de 1 à 127), une méthode simple consiste à encoder tous les autres caractères dans
des entités caractères XML (une entité dont le nom est le code Unicode du caractère, par
exemple é devient é). Cette technique a une limite: les entités caractères sont interdites
dans les noms d'éléments et d'attributs, qui devront donc utiliser les caractères ASCII de base. A
ce détail prés cela vous permet de traiter les documents en UTF-8 sous Perl et de les stocker ou
traiter également avec des logiciels non-Unicode.
Je ne connais pas de module CPAN offrant cette fonctionnalité, mais elle est proposée par
XML::DOM, développé par Enno Derksen. J'ai donc extrait le code de DOM.pm et en ai fait un
module rudimentaire, XML::Code. XML::Code est très simple, il encode des chaînes CDATA,
PCDATA ou des balises. Il devrait en fait faire partie d'XML::UM, et y sera probablement
intégré dans l'avenir.
La méthode original_string
Si rien d'autre ne marche, et si vous n'avez pas besoin d'utiliser des expressions régulières, il vous
est possible d'utiliser la méthode original_stringd'XML::Parser.
Cette méthode, appelée depuis un handler, renvoie la chaîne de caractères reconnue par le
parser, avant que les entités n'y soient résolues et qu'elle ne soit convertie en UTF-8. Au passage
notons que la méthode recognized_string renvoie la chaîne de caractères reconnue après ces deux
opérations.
Quelques détails ennuyeux:
-
expat doit pouvoir parser le document, ce qui implique la présence d'une table pour son
encodage,
-
Si certains caractères sont encodés sur plusieurs octets le moteur d'expressions régulières
de Perl risque d'avoir des problèmes (La seule exception à cette règle étant Perl 5.6 et Unicode),
-
Si vous utilisez des noms d'éléments, d'attributs ou même des valeurs d'attributs
contenant des caractères non UTF-8 vous ne pouvez plus compter sur XML::Parser pour parser
les balises et vous devrez écrire le bout de code qui extrait les différentes parties de la balise, avec
le risque que le moteur d'expressions régulières ne soit perturbé par des caractères multi-octets.
Ces détails ne sont pas forcément dramatiques: beaucoup d'applications XML n'utilisent pas
d'expressions régulières, après tout un document XML est déjà pré-découpé en éléments et les
traitements qui lui sont appliqués consistent souvent à ré-organiser ces éléments et à modifier
leurs propriétés, il est donc possible que vous puissiez vous accommoder de ces restrictions. A
vos risques et périls cependant !
Exemples
Bon, il est temps de passer aux exemples pratiques. Ils ont tous le même but: prendre un
document en entrée et le reproduire en sortie. Les deux documents sont identiques, à l'encodage
près. Tous les scripts utilisent XML::Parser, sans style.
Ces exemples ne sont pas d'une grande utilité en eux-mêmes mais il vous donneront un point de
départ pour traiter les problèmes d'encodage avec XML::Parser.
Note: cette section demande une connaissance minimum d'XML::Parser. Pour une introduction à
ce module voir l'article de Clark Cooper: Using The Perl XML::Parser Module.
Les documents
Voici les 3 exemples que j'utilise, un en français, un en japonais et un en chinois., sous forme de
graphiques afin de simplifier leur affichage :
Français:
 Chinois:
 Japonais:
 Ces exemples sont déclinés en plusieurs versions, utilisant différents encodages.
Premier exemple (entrée et sortie en UTF-8)
Ce script (enc_ex1.pl) peut être appliqué aux documents suivants: doc_fr_utf.xml, doc_ch_utf.xml
ou doc_jp_utf.xml.
Note: pour une raison indépendante de ma volonté le handler Default n'est pas activé pour les
balises de début et de fin d'élément, les handlers Start et End doivent donc être définis.
#!/bin/perl -w
use strict;
use XML::Parser;
my $p= new XML::Parser( Handlers =>
{ Start => \&default,
End => \&default,
Default => \&default
},
);
$p->parsefile($ARGV[0]);
exit;
# par défaut imprimer la chaîne de caractères convertie en UTF-8
{ my $p= shift;
my $string= $p->recognized_string();
print $string;
}
Ce premier exemple ne fait pas grand chose, il lit le document en entrée et l'imprime en sortie,
mais au moins il vérifie qu'il parse, qu'il est bien en UTF-8 et il résout les appels d'entités. Le but
principal de ce script est de donner le cadre dans lequel nous allons intégrer les transcodages
présentés dans les exemple suivants.
Le document en entrée est dans un encodage différent
Cet exemple est similaire au précédent, au détail prés qu'ici le script doit enlever la déclaration
d'encodage de la déclaration XML de départ.
Vous pouvez utiliser ce script (enc_ex2.pl) avec les documents: doc_fr_latin1.xml (pas besoin
d'utiliser XML::Encodings pour celui-là), doc_ch_big5.xml, doc_jp_sjis.xml ou doc_jp_euc.xml
#!/bin/perl -w
use strict;
use XML::Parser;
use XML::Encoding;
my $p= new XML::Parser( Handlers =>
{ Start => \&default,
End => \&default,
Default => \&default,
XMLDecl => sub { decl( "UTF-8", @_); }
},
);
$p->parsefile($ARGV[0]);
exit;
# mettre à jour l'encodage
sub decl
{ my( $new_encoding, $p, $version, $encoding, $standalone)=@_;
print "<?xml version=\"$version\" encoding=\"$new_encoding\"";
print "standalone=\"$standalone\"" if( $standalone);
print "?>\n";
}
# par défaut imprimer la chaîne de caractères convertie en UTF-8
sub default
{ my $p= shift;
my $string= $p->recognized_string();
print $string;
}
Traiter des documents HTML
Cet exemple consiste a parser un document ASCII (donc également UTF-8) comportant des
entités HTML (par exemple é). Ce genre de problème se pose fréquemment pour des
documents en XHTML, ou en "HTML-plus".
Les entités HTML ne sont pas pré-définies en XML, il faut donc les déclarer. Pour cela j'utilise
un fichier, html2utf8.ent, constitué à partir des différentes déclarations associées à la DTD
HTML du W3C.
Voici le script (enc_ex3.pl), toujours très simple:
#!/bin/perl -w
use strict;
use XML::Parser;
my $p= new XML::Parser( Handlers =>
{ Start => \&default,
End => \&default,
Char => \&default,
},
ParseParamEnt => 1
);
$p->parsefile($ARGV[0]);
exit;
# par défaut imprimer la chaîne de caractères convertie en UTF-8
sub default
{ my $p= shift;
my $string= $p->recognized_string();
print $string;
}
Document d'entrée Unicode, sortie en latin 1
Le script (enc_ex4.pl) peut être écrit de 2 manières différentes suivant que vous utilisiez Perl 5.6
ou une version plus ancienne. Vous pouvez l'utiliser sur le document doc_fr_latin1.xml.
Avant Perl 5.6 il fallait utiliser le module Unicode::String:
#!/bin/perl -w
use strict;
use XML::Parser;
use Unicode::String;
my $p= new XML::Parser( Handlers =>
{ Start => \&default_latin1,
End => \&default_latin1,
XMLDecl => sub { decl( "ISO-8859-1",@_); },
Default => \&default_latin1
},
);
$p->parsefile($ARGV[0]);
exit;
sub decl
{ my( $new_encoding, $p, $version, $encoding, $standalone)=@_;
print "<?xml version=\"$version\" encoding=\"$new_encoding\"";
print "standalone=\"$standalone\"" if( $standalone);
print "?>\n";
}
sub default_latin1
{ my $p= shift;
my $string= $p->recognized_string(); # récupérer l'Unicode
my $u= Unicode::String::utf8( $string);# créer l'objet String
print $u->latin1; # le convertir en latin1
}
Avec Perl 5.6 il n'est plus nécessaire d'utiliser Unicode::String, un simple tr/// fera l'affaire, la
fonction default_latin1 devient:
sub default_latin1
{ my $p= shift;
my $string= $p->recognized_string(); # récupérer l'UTF-8
my $string=~ tr/\0-\x{ff}//UC; # le convertir en latin 1
print $string;
}
Document en entrée non-unicode et sortie dans le même encodage
Si le document en entrée est en latin 1 vous pouvez utiliser le script précédent sinon vous devez
utiliser XML::UM, voici un exemple (enc_ex5.pl):
#!/bin/perl -w
use strict;
use XML::Parser;
use XML::Encoding;
use XML::UM;
# indiquer à XML::UM où sont les tables d'encodage
$XML::UM::ENCDIR="/usr/local/src/XML-Encoding-1.01/maps/";
my $encode; # la fonction de transcodage, créée dans le
# handler de déclaration XML (qui donne l'encodage)
my $p= new XML::Parser( Handlers =>
{ Start => \&default,
End => \&default,
XMLDecl => \&decl,
Default => \&default,
},
);
$p->parsefile($ARGV[0]);
exit;
sub default
{ my $p= shift;
print $encode->($p->recognized_string());
}
# récupérer l'encodage et créer la fonction encode
sub decl
{ my($p, $version, $encoding, $standalone)=@_;
print "<?xml version=\"$version\" encoding=\"$encoding\"";
print "standalone=\"$standalone\"" if( $standalone);
print "?>\n";
$encode= XML::UM::get_encode( Encoding => $encoding);
}
Conclusion
Unicode c'est génial. Vraiment ! Je suis sûr que vous avez tous eu les pires difficultés à
transmettre des textes français, comportant des caractères accentués, entre différents systèmes.
Et utiliser des GIFs pour afficher des lettres grecques au milieu d'un texte n'est pas vraiment
une solution satisfaisante ! Unicode résout ces problèmes. Donc je vous encourage à utiliser
autant que possible des logiciels qui le supportent et à faire pression sur les vendeurs pour qu'ils
fassent d'Unicode une priorité dans leurs évolutions. A long terme cela vous épargnera bien des
maux de têtes.
En attendant, si votre environnement vous impose d'autres encodages mais que vous ne voulez
pas manquer la vague XML voici vos options:
-
vos documents XML sont tous en anglais. Pas de problème, XML::Parser fonctionne
parfaitement, jusqu'à ce que vous tombiez sur un fragment comportant des caractères accentués,
auquel cas une solution similaire à celle proposée par XML::Code fera l'affaire.
-
plus probablement, votre environnement utilise Latin 1, auquel cas vos documents
peuvent utiliser cet encodage (n'oubliez pas de le déclarer en ajoutant encoding="ISO-5589-1"
dans la déclaration XML), utilisez la sortie d'XML::Parser en UTF-8 pour les traitements et
convertissez les sorties en utilisant Unicode::String ou tr///UC sous Perl 5.6,
-
votre environnement vous impose un autre encodage supporté par XML::Parser (grâce à
XML::Encoding), et XML::UM vous permettra de produire des sorties dans cet encodage.
-
l'encodage que vous utilisez n'est pas reconnu par XML::Parser, auquel cas la meilleure
solution est probablement de créer la table d'encodage correspondante et de l'inclure dans
XML::Encoding ( et si en plus vous publiez cette table le problème disparaît pour tout le
monde).
Voila ! J'espère que cet article vous aura aidé à comprendre les problèmes d'encodage, et vous
aura donné des éléments pour résoudre vos problèmes dans le domaine. Bonne chance !
Ressources
La liste des modules référencés dans cet article:
-
XML::Parser: le parser XML Perl
-
XML::Encoding: permet a XML::Parser de lire des encodages additionnels
-
Unicode::String: convertit UTF-8 en ISO-8859-1 (latin 1)
-
XML::UM (inclus dans libxml-enno): convertit UTF-8 vers un jeu de caractère définit
par XML::Encoding
-
XML::Code: conversion UTF-8 vers ASCII + Entités caractères XML
|