Série 7 – Identification d'encodage
Introduction
Comme nous l'avons vu au cours, il existe un grand nombre d'encodages de caractères différents, p.ex. ASCII, ISO 8859-1 ou encore différents encodages d'Unicode comme UTF-8 et UTF-16. Lorsqu'on désire lire un fichier textuel, il est nécessaire de connaître l'encodage utilisé pour les caractères, faute de quoi il n'est pas possible d'interpréter correctement son contenu. Malheureusement, cette information n'est pas attachée aux fichiers, et il faut donc la connaître d'une manière ou d'une autre.
Lorsqu'on ne connaît pas du tout l'encodage d'un fichier, il est possible d'utiliser différentes heuristiques pour essayer de le deviner. L'une de ces heuristiques consiste à utiliser des automates finis déterministes pour les différents encodages, ce qui est le but de cette série.
Automates d'encodages
Tout encodage de caractères peut être représenté par un automate fini déterministe qui accepte une séquence d'octets si et seulement si elle est valide selon cet encodage.
Par exemple, dans l'encodage ASCII, tout caractère est encodé par un entier de 7 bits, dont la valeur est donc comprise entre 0 et 7F16. L'automate de la figure ci-dessous accepte une séquence d'octets si et seulement si elle est valide selon cet encodage. Sur cette figure, l'état OK est en gras car il s'agit d'un état final, et la flèche lui arrivant dessus de nulle part le désigne de plus comme l'état initial.
Figure 1 : Automate ASCII
Ainsi, la séquence d'octets 3A16 2D16 2916 est acceptée par cet automate, car après trois transitions il se trouve dans l'état OK, qui est final. Par contre, la séquence d'octets 0A16 8116 n'est pas acceptée par cet automate, car aucune transition n'est possible pour le second octet (8116).
Squelette
Pour faciliter votre travail, nous vous fournissons un squelette de projet à importer dans Eclipse, sous la forme d'une archive Zip contenant :
- la classe
DFA
qui modélise un automate fini déterministe, ainsi que son bâtisseurDFA.Builder
, - la classe
Pair
qui modélise une paire générique et qui est uniquement utilisée par la classeDFA
, - un squelette de la classe
CharsetIdentifier
, non instanciable et qui fournit une unique méthode publique (à compléter) permettant de déterminer l'ensemble des encodages valides pour un fichier donné, - six fichiers textuels compressés avec gzip et nommés
f1.txt.gz
àf6.txt.gz
, dont le contenu est à déterminer dans le cadre des exercices ci-dessous.
Avant de vous lancer dans le premier exercice, nous vous conseillons de bien lire les classes DFA
et DFA.Builder
afin de comprendre leur fonctionnement, et également d'examiner la construction de l'automate nommé ASCII
dans la classe CharsetIdentifier
, qui représente l'automate de la figure 1 plus haut.
Notez que la définition de l'automate ASCII utilise la classe EnumSet
que nous n'avons pas vue au cours mais qui représente simplement de manière efficace un ensemble de valeurs faisant partie d'une énumération. Cette classe est donc similaire aux classes HashSet
et TreeSet
, mais moins générale qu'elles car utilisable uniquement avec des énumérations, et plus efficace en contrepartie.
Exercice 1
Complétez la méthode validCharsets
de la classe CharsetIdentifier
, qui retourne l'ensemble des encodages pour lesquels les octets du fichier dont le nom est passé en argument sont valides, après décompression. Etant donné que le seul encodage pour lequel un automate est défini à ce stade est ASCII, cette méthode doit pour l'instant toujours retourner soit un ensemble vide — si le fichier n'est pas un fichier ASCII valide — soit un ensemble contenant uniquement l'encodage ASCII.
Il est conseillé de stocker l'état courant de l'automate dans une variable de type Optional<ASCIIState>
, car la méthode next
de la classe DFA
retourne une valeur de ce type. Un état valide est représenté par une valeur optionnelle non vide contenant cet état, tandis que l'état invalide est représenté par la valeur optionnelle vide.
Attention : le fichier dont le nom est passé à cette méthode est supposé compressé avec gzip, il vous faut donc impérativement utiliser un flot filtrant de type GZIPInputStream
pour décompresser les octets du fichier avant de les fournir à l'automate.
Notez que les encodages retournés par cette méthode sont représentés par des instances de la classe Charset
de la bibliothèque Java. Comme nous l'avons vu au cours, les valeurs correspondant aux principaux encodages se trouvent dans la classe StandardCharsets
, sous la forme d'attributs statiques. Par exemple, l'attribut US_ASCII
de cette classe représente l'encodage ASCII.
Une fois la méthode validCharsets
terminée, écrivez un programme principal dans une classe nommée Main
qui itère (dans l'ordre) sur les six fichiers textuels fournis et détermine pour chacun d'eux si son contenu est interprétable selon l'encodage ASCII. Lorsque c'est le cas, le contenu de ce fichier doit être lu et affiché à l'écran. Pour mémoire, le plus simple pour lire un fichier en utilisant un encodage donné est d'utiliser la classe InputStreamReader
qui, étant donné un encodage et un flot d'entrée d'octets, produit le lecteur (donc un flot de caractères) correspondant.
Une fois votre programme terminé, vous devriez voir une unique ligne s'afficher à l'écran.
Exercice 2
Ajoutez à la classe CharsetIdentifier
la possibilité de reconnaître les fichiers encodés en ISO 8859-1. L'automate correspondant à cet encodage est donné dans la figure ci-dessous et la valeur de type Charset
correspondant à cet encodage est disponible dans l'attribut ISO_8859_1
de la classe StandardCharsets
.
Figure 2 : Automate ISO 8859-1
Notez que cet automate n'est pas strictement correct, car en réalité la plage 8016 à 9F16 contient des caractères de contrôle valides. Comme ceux-ci ne se rencontrent que rarement en pratique, nous avons décidé de les exclure, faute de quoi n'importe quelle séquence d'octets serait reconnue comme une séquence ISO 8859-1 valide. Ce problème illustre les limites de l'approche utilisée ici pour tenter d'identifier les encodages.
Une fois cette possibilité ajoutée, modifiez votre programme principal pour qu'il reconnaisse et affiche également les fichiers encodés en ISO 8859-1. Cela devrait vous permettre de voir deux nouvelles lignes s'afficher à l'écran.
Exercice 3
Ajoutez à la classe CharsetIdentifier
la possibilité de reconnaître les fichiers encodés en UTF-8. L'automate correspondant à cet encodage est donné par la figure ci-dessous. Sur cette figure, les étiquettes H1 à H4 et C attachées aux transitions représentent des octets dont certains bits ont une valeur fixée, selon les règles suivantes, dans lesquelles 0
ou 1
représente un bit dont la valeur est fixée et x
représente un bit dont elle est quelconque :
- H1 représente
0xxxxxxx
, - H2 représente
110xxxxx
, - H3 représente
1110xxxx
, - H4 représente
11110xxx
, - C représente
10xxxxxx
.
Figure 3 : Automate UTF-8
La valeur de type Charset
correspondant à cet encodage est disponible dans l'attribut UTF_8
de la classe StandardCharsets
.
Modifiez votre programme principal pour qu'il reconnaisse et affiche également les fichiers encodés en UTF-8. Une fois encore, cela devrait vous permettre de voir deux nouvelles lignes s'afficher à l'écran.
Exercice 4
Ajoutez à la classe CharsetIdentifier
la possibilité de reconnaître les fichiers encodés en UTF-16BE1. L'automate correspondant à cet encodage est donné dans la figure ci-dessous. La valeur de type Charset
correspondant à cet encodage est disponible dans l'attribut UTF_16BE
de la classe StandardCharsets
.
Figure 4 : Automate UTF-16BE
Une fois cet encodage ajouté, votre programme principal devrait être capable de décoder correctement quatre des six fichiers fournis, les deux autres étant invalides. Leurs textes respectifs mis bout à bout, dans l'ordre, devraient former un poème d'un auteur français connu. Notez que le dernier de ces fichiers contient une émoticône, qui ne sera pas forcément affichée correctement sur votre système. Ne vous en faites donc pas si les caractères suivant le nom de l'auteur sont incompréhensibles.
Notes de bas de page
UTF-16BE est l'une des deux variantes de UTF-16, l'autre étant UTF-16LE. La différence entre ces variantes est que la première place l'octet de poids fort des mots de 16 bits en premier dans le flot, la seconde place celui de poids faible. Les suffixes BE et LE signifient respectivement big endian et little endian, termes généralement utilisés pour désigner les deux manières d'ordonner les octets d'un entier en contenant plusieurs.