Guide pour l’encodage de caractères

Guide de codage de caractères

1. Vue d'ensemble

Dans ce didacticiel, nous aborderons les bases du codage de caractères et la manière dont nous le gérons en Java.

2. Importance de l'encodage de caractères

Nous devons souvent traiter des textes appartenant à plusieurs langues avec des écritures diverses comme le latin ou l'arabe. Chaque caractère dans chaque langue doit en quelque sorte être associé à un ensemble de uns et de zéros. C'est vraiment étonnant que les ordinateurs puissent traiter correctement toutes nos langues.

Pour faire cela correctement,we need to think about character encoding. Ne pas le faire peut souvent entraîner des pertes de données et même des failles de sécurité.

Pour mieux comprendre cela, définissons une méthode pour décoder un texte en Java:

String decodeText(String input, String encoding) throws IOException {
    return
      new BufferedReader(
        new InputStreamReader(
          new ByteArrayInputStream(input.getBytes()),
          Charset.forName(encoding)))
        .readLine();
}

Notez que le texte d'entrée que nous alimentons ici utilise le codage de plate-forme par défaut.

If we run this method with input as “The façade pattern is a software design pattern.” and encoding as “US-ASCII”, il affichera:

The fa��ade pattern is a software design pattern.

Eh bien, pas exactement ce à quoi nous nous attendions.

Qu'est-ce qui aurait pu mal tourner? Nous essaierons de comprendre et de corriger cela dans la suite de ce didacticiel.

3. Fondamentaux

Avant d'approfondir, examinons rapidement trois termes:encoding,charsets etcode point.

3.1. Codage

Les ordinateurs ne peuvent comprendre que les représentations binaires telles que1 et0. Le traitement de toute autre tâche nécessite une sorte de mappage du texte du monde réel à sa représentation binaire. This mapping is what we know as character encoding or simply just as encoding.

Par exemple, la première lettre de notre message, «T», en US-ASCIIencodes to «01010100».

3.2. Jeux de caractères

Le mappage des caractères sur leurs représentations binaires peut varier considérablement en termes de caractères inclus. Le nombre de caractères inclus dans un mappage peut varier de quelques-uns à tous les caractères utilisés. The set of characters that are included in a mapping definition is formally called a charset.

Point de code3.3.

Un point de code est une abstraction qui sépare un caractère de son codage réel. A code point is an integer reference to a particular character.

Nous pouvons représenter l'entier lui-même dans des bases décimales simples ou alternatives, comme les bases hexadécimales ou octales. Nous utilisons des bases alternatives pour faciliter le référencement de grands nombres.

Par exemple, la première lettre de notre message, T, en Unicode a un point de code «U + 0054» (ou 84 en décimal).

4. Comprendre les schémas d'encodage

Un codage de caractères peut prendre différentes formes en fonction du nombre de caractères qu’il code.

Le nombre de caractères codés est directement lié à la longueur de chaque représentation, qui est généralement mesurée en nombre d'octets. Having more characters to encode essentially means needing lengthier binary representations.

Passons en revue quelques-uns des schémas de codage les plus répandus en pratique aujourd'hui.

4.1. Encodage à un octet

L’un des premiers schémas de codage, appelé ASCII (code américain normalisé pour l’échange d’informations), utilise un schéma de codage à un octet. Cela signifie essentiellement queeach character in ASCII is represented with seven-bit binary numbers. Cela laisse encore un bit libre dans chaque octet!

Le jeu de 128 caractères ASCII couvre les alphabets anglais en minuscules et majuscules, les chiffres et certains caractères spéciaux et de contrôle.

Définissons une méthode simple en Java pour afficher la représentation binaire d’un caractère sous un schéma de codage particulier:

String convertToBinary(String input, String encoding)
      throws UnsupportedEncodingException {
    byte[] encoded_input = Charset.forName(encoding)
      .encode(input)
      .array();
    return IntStream.range(0, encoded_input.length)
        .map(i -> encoded_input[i])
        .mapToObj(e -> Integer.toBinaryString(e ^ 255))
        .map(e -> String.format("%1$" + Byte.SIZE + "s", e).replace(" ", "0"))
        .collect(Collectors.joining(" "));
}

Maintenant, le caractère "T" a un code de 84 en US-ASCII (ASCII est appelé US-ASCII en Java).

Et si nous utilisons notre méthode d’utilité, nous pouvons voir sa représentation binaire:

assertEquals(convertToBinary("T", "US-ASCII"), "01010100");

Comme nous nous y attendions, il s’agit d’une représentation binaire de sept bits pour le caractère «T».

The original ASCII left the most significant bit of every byte unused. Dans le même temps, l'ASCII avait laissé pas mal de caractères non représentés, en particulier pour les langues autres que l'anglais.

Cela a conduit à un effort pour utiliser ce bit inutilisé et inclure 128 caractères supplémentaires.

There were several variations of the ASCII encoding scheme proposed and adopted over the time. Celles-ci sont devenues vaguement appelées «extensions ASCII».

Beaucoup d'extensions ASCII ont eu différents niveaux de succès, mais visiblement, cela n'était pas suffisant pour une adoption plus large, car de nombreux caractères n'étaient toujours pas représentés.

One of the more popular ASCII extensions was ISO-8859-1, également appelé «ISO Latin 1».

4.2. Encodage multi-octets

Alors que la nécessité de prendre en charge de plus en plus de caractères augmentait, les systèmes de codage à un octet tels que ASCII n'étaient pas viables.

Cela a donné lieu à des schémas de codage multi-octets qui ont une capacité bien meilleure, mais au prix d'une augmentation de l'espace requis.

BIG5 et SHIFT-JIS sont des exemples demulti-byte character encoding schemes which started to use one as well as two bytes to represent wider charsets. La plupart d'entre eux ont été créés pour répondre à la nécessité de représenter des scripts chinois et des scripts similaires comportant un nombre de caractères nettement supérieur.

Appelons maintenant la méthodeconvertToBinary avecinput comme «語», un caractère chinois etencoding comme «Big5»:

assertEquals(convertToBinary("語", "Big5"), "10111011 01111001");

La sortie ci-dessus montre que l’encodage Big5 utilise deux octets pour représenter le caractère «».

Uncomprehensive list d'encodages de caractères, ainsi que leurs alias, sont gérés par l'International Number Authority.

5. Unicode

Il n’est pas difficile de comprendre que si le codage est important, le décodage est également essentiel pour donner un sens aux représentations. This is only possible in practice if a consistent or compatible encoding scheme is used widely.

Différents schémas de codage développés isolément et mis en pratique dans des zones géographiques locales ont commencé à devenir difficiles.

Ce défi a donné lieu àa singular encoding standard called Unicode which has the capacity for every possible character in the world. Cela inclut les personnages en cours d’utilisation et même ceux qui sont disparus!

Eh bien, cela nécessite plusieurs octets pour stocker chaque caractère? Honnêtement oui, mais Unicode a une solution ingénieuse.

Unicode as a standard defines code points for every possible character in the world. Le point de code du caractère «T» en Unicode est 84 en décimal. Nous appelons généralement cela «U + 0054» en Unicode qui n'est rien d'autre que U + suivi du nombre hexadécimal.

Nous utilisons hexadécimal comme base pour les points de code dans Unicode car il y a 1 114 112 points, ce qui est un nombre assez important pour communiquer facilement en décimal!

How these code points are encoded into bits is left to specific encoding schemes within Unicode. Nous couvrirons certains de ces schémas d'encodage dans les sous-sections ci-dessous.

5.1. UTF-32

UTF-32 estan encoding scheme for Unicode that employs four bytes to represent every code point défini par Unicode. Évidemment, utiliser quatre octets pour chaque caractère est inefficace.

Voyons comment un simple caractère comme «T» est représenté en UTF-32. Nous utiliserons la méthodeconvertToBinary introduite précédemment:

assertEquals(convertToBinary("T", "UTF-32"), "00000000 00000000 00000000 01010100");

La sortie ci-dessus montre l’utilisation de quatre octets pour représenter le caractère «T», où les trois premiers octets sont simplement de l’espace perdu.

5.2. UTF-8

UTF-8 estanother encoding scheme for Unicode which employs a variable length of bytes to encode. Bien qu'il utilise généralement un seul octet pour coder les caractères, il peut utiliser un nombre d'octets plus élevé si nécessaire, économisant ainsi de l'espace.

Appelons à nouveau la méthodeconvertToBinary avec l'entrée "T" et le codage comme "UTF-8":

assertEquals(convertToBinary("T", "UTF-8"), "01010100");

La sortie est exactement similaire à ASCII en utilisant un seul octet. En fait, UTF-8 est complètement compatible avec ASCII.

Appelons à nouveau la méthodeconvertToBinary avec l'entrée «語» et le codage comme «UTF-8»:

assertEquals(convertToBinary("語", "UTF-8"), "11101000 10101010 10011110");

Comme nous pouvons le voir ici, UTF-8 utilise trois octets pour représenter le caractère «». This is known as variable-width encoding.

UTF-8, en raison de son efficacité spatiale, est l'encodage le plus couramment utilisé sur le Web.

6. Prise en charge du codage en Java

Java prend en charge un large éventail de codages et leurs conversions les uns aux autres. La classeCharset définit unset of standard encodings que chaque implémentation de la plate-forme Java doit prendre en charge.

Cela inclut US-ASCII, ISO-8859-1, UTF-8 et UTF-16, pour n'en nommer que quelques-uns. A particular implementation of Java may optionally support additional encodings.

Il existe certaines subtilités dans la manière dont Java récupère un jeu de caractères avec lequel travailler. Passons en revue plus en détail.

6.1. Jeu de caractères par défaut

La plate-forme Java dépend fortement d'une propriété appeléethe default charset. The Java Virtual Machine (JVM) determines the default charset during start-up.

Cela dépend des paramètres régionaux et du jeu de caractères du système d'exploitation sous-jacent sur lequel JVM est exécuté. Par exemple sur MacOS, le jeu de caractères par défaut est UTF-8.

Voyons comment nous pouvons déterminer le jeu de caractères par défaut:

Charset.defaultCharset().displayName();

Si nous exécutons cet extrait de code sur une machine Windows, le résultat obtenu est le suivant:

windows-1252

Désormais, «windows-1252» est le jeu de caractères par défaut de la plate-forme Windows en anglais. Dans ce cas, il a déterminé le jeu de caractères par défaut de la machine virtuelle Java s'exécutant sous Windows.

6.2. Qui utilise le jeu de caractères par défaut?

La plupart des API Java utilisent le jeu de caractères par défaut défini par la machine virtuelle Java. Pour en nommer quelques uns:

Donc, cela signifie que si nous exécutons notre exemple sans spécifier le jeu de caractères:

new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input.getBytes()))).readLine();

alors il utiliserait le jeu de caractères par défaut pour le décoder.

Et plusieurs API font ce même choix par défaut.

Le jeu de caractères par défaut assume donc une importance que nous ne pouvons ignorer en toute sécurité.

6.3. Problèmes avec le jeu de caractères par défaut

Comme nous l'avons vu, le jeu de caractères par défaut en Java est déterminé dynamiquement au démarrage de la machine virtuelle Java. Cela rend la plate-forme moins fiable ou sujette aux erreurs lorsqu'elle est utilisée sur différents systèmes d'exploitation.

Par exemple, si nous courons

new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input.getBytes()))).readLine();

sur macOS, il utilisera UTF-8.

Si nous essayons le même extrait de code sous Windows, il utilisera Windows-1252 pour décoder le même texte.

Ou imaginez écrire un fichier sur un macOS, puis lire ce même fichier sous Windows.

Il n’est pas difficile de comprendre qu’en raison des différents schémas de codage, cela peut entraîner une perte ou une corruption de données.

6.4. Pouvons-nous remplacer le jeu de caractères par défaut?

La détermination du jeu de caractères par défaut en Java conduit à deux propriétés système:

  • file.encoding: la valeur de cette propriété système est le nom du jeu de caractères par défaut

  • sun.jnu.encoding: la valeur de cette propriété système est le nom du jeu de caractères utilisé lors du codage / décodage des chemins de fichiers

Désormais, il est intuitif de remplacer ces propriétés système via des arguments de ligne de commande:

-Dfile.encoding="UTF-8"
-Dsun.jnu.encoding="UTF-8"

Cependant, il est important de noter que ces propriétés sont en lecture seule en Java. Their usage as above is not present in the documentation. Le remplacement de ces propriétés système peut ne pas avoir un comportement souhaité ou prévisible.

Par conséquent,we should avoid overriding the default charset in Java.

6.5. Pourquoi Java ne résout-il pas cela?

Il y a unJava Enhancement Proposal (JEP) which prescribes using “UTF-8” as the default charset in Java au lieu de le baser sur les paramètres régionaux et le jeu de caractères du système d'exploitation.

Ce PEC est actuellement à l'état de projet et sa résolution (espérons-le!) Résoudra la plupart des problèmes évoqués précédemment.

Notez que les API les plus récentes comme celles dejava.nio.file.Files n'utilisent pas le jeu de caractères par défaut. Les méthodes de ces API lisent ou écrivent des flux de caractères avec le jeu de caractères UTF-8 plutôt que le jeu de caractères par défaut.

6.6. Résoudre ce problème dans nos programmes

Nous devrions normalementchoose to specify a charset when dealing with text instead of relying on the default settings. Nous pouvons déclarer explicitement le codage que nous voulons utiliser dans les classes traitant des conversions de caractère à octet.

Heureusement, notre exemple spécifie déjà le charset. We just need to select the right one and let Java do the rest.

Nous devons maintenant nous rendre compte que les caractères accentués tels que "ç" ne sont pas présents dans le schéma de codage ASCII et nous avons donc besoin d’un codage qui les inclut. Peut-être, UTF-8?

Essayons cela, nous allons maintenant exécuter la méthodedecodeText avec la même entrée mais avec un encodage en "UTF-8":

The façade pattern is a software-design pattern.

Bingo! Nous pouvons voir la sortie que nous espérions voir maintenant.

Ici, nous avons défini le codage que nous pensons le mieux adapté à nos besoins dans le constructeur deInputStreamReader. C'est généralement la méthode la plus sûre pour traiter les caractères et les conversions d'octets en Java.

De même,OutputStreamWriter et de nombreuses autres API prennent en charge la définition d'un schéma de codage via leur constructeur.

6.7. MalformedInputException

Lorsque nous décodons une séquence d'octets, il existe des cas dans lesquels elle n'est pas légale pour lesCharset donnés, ou bien ce n'est pas un Unicode de seize bits légal. En d'autres termes, la séquence d'octets donnée n'a pas de mappage dans lesCharset spécifiés.

Il existe trois stratégies prédéfinies (ouCodingErrorAction) lorsque la séquence d'entrée a une entrée malformée:

  • IGNORE ignorera les caractères mal formés et reprendra l'opération de codage

  • REPLACE remplacera les caractères mal formés dans le tampon de sortie et reprendra l'opération de codage

  • REPORT lancera unMalformedInputException

LemalformedInputAction par défaut pour lesCharsetDecoder is REPORT, et lemalformedInputAction par défaut du décodeur par défaut enInputStreamReader estREPLACE.

Définissons une fonction de décodage qui reçoit unCharset spécifié, un typeCodingErrorAction et une chaîne à décoder:

String decodeText(String input, Charset charset,
  CodingErrorAction codingErrorAction) throws IOException {
    CharsetDecoder charsetDecoder = charset.newDecoder();
    charsetDecoder.onMalformedInput(codingErrorAction);
    return new BufferedReader(
      new InputStreamReader(
        new ByteArrayInputStream(input.getBytes()), charsetDecoder)).readLine();
}

Donc, si nous décodons "Le modèle de façade est un modèle de conception logicielle." avecUS_ASCII, la sortie de chaque stratégie serait différente. Tout d'abord, nous utilisonsCodingErrorAction.IGNORE qui ignore les caractères illégaux:

Assertions.assertEquals(
  "The faade pattern is a software design pattern.",
  CharacterEncodingExamples.decodeText(
    "The façade pattern is a software design pattern.",
    StandardCharsets.US_ASCII,
    CodingErrorAction.IGNORE));

Pour le deuxième test, nous utilisonsCodingErrorAction.REPLACE qui met � au lieu des caractères illégaux:

Assertions.assertEquals(
  "The fa��ade pattern is a software design pattern.",
  CharacterEncodingExamples.decodeText(
    "The façade pattern is a software design pattern.",
    StandardCharsets.US_ASCII,
    CodingErrorAction.REPLACE));

Pour le troisième test, on utiliseCodingErrorAction.REPORT qui conduit à lancerMalformedInputException:

Assertions.assertThrows(
  MalformedInputException.class,
    () -> CharacterEncodingExamples.decodeText(
      "The façade pattern is a software design pattern.",
      StandardCharsets.US_ASCII,
      CodingErrorAction.REPORT));

7. Autres endroits où le codage est important

Nous n'avons pas seulement besoin de prendre en compte le codage des caractères lors de la programmation. Les textes peuvent aller mal en fin de processus à de nombreux autres endroits.

Lesmost common cause of problems in these cases is the conversion of text from one encoding scheme to another, introduisant ainsi éventuellement une perte de données

Passons rapidement en revue quelques endroits où nous pouvons rencontrer des problèmes lors de l'encodage ou du décodage de texte.

7.1. Éditeurs de texte

Dans la plupart des cas, un éditeur de texte est l’origine des textes. Il existe de nombreux éditeurs de texte dans le choix courant, notamment vi, Notepad et MS Word. La plupart de ces éditeurs de texte nous permettent de sélectionner le schéma de codage. Par conséquent, nous devrions toujours nous assurer qu'ils sont adaptés au texte que nous traitons.

7.2. Système de fichiers

Après avoir créé des textes dans un éditeur, nous devons les stocker dans un système de fichiers. Le système de fichiers dépend du système d'exploitation sur lequel il s'exécute. La plupart des systèmes d'exploitation prennent en charge plusieurs schémas de codage. Cependant, il peut toujours y avoir des cas où une conversion de codage entraîne une perte de données.

7.3. Réseau

Les textes transférés sur un réseau utilisant un protocole tel que File Transfer Protocol (FTP) impliquent également une conversion entre des codages de caractères. Pour tout ce qui est encodé en Unicode, il est plus sûr de transférer en tant que binaire pour minimiser le risque de perte lors de la conversion. Cependant, le transfert de texte sur un réseau est l’une des causes les moins fréquentes de corruption des données.

7.4. Bases de données

La plupart des bases de données populaires telles qu'Oracle et MySQL prennent en charge le choix du schéma de codage de caractères lors de l'installation ou de la création de bases de données. Nous devons choisir cela en fonction des textes que nous nous attendons à stocker dans la base de données. C’est l’un des endroits les plus fréquents où la corruption des données de texte se produit en raison de conversions de codage.

7.5. Navigateurs

Enfin, dans la plupart des applications Web, nous créons des textes et les passons à travers différentes couches dans l’intention de les afficher dans une interface utilisateur, comme un navigateur. Ici aussi, il est impératif pour nous de choisir le bon encodage de caractères qui puisse afficher correctement les caractères. La plupart des navigateurs populaires tels que Chrome et Edge permettent de choisir le codage des caractères en fonction de leurs paramètres.

8. Conclusion

Dans cet article, nous avons expliqué comment le codage peut être un problème lors de la programmation.

Nous avons également discuté des principes fondamentaux, y compris l'encodage et les jeux de caractères. De plus, nous avons étudié différents schémas de codage et leurs utilisations.

Nous avons également pris un exemple d'utilisation incorrecte de l'encodage de caractères en Java et avons vu comment l'obtenir. Enfin, nous avons discuté d'autres scénarios d'erreur courants liés au codage de caractères.

Comme toujours, le code des exemples est disponibleover on GitHub.