Le blog de la CT2C

Les encodages Ruby

Par Kulgar , publié le 21 Novembre 2012

Aaah ! Voilà un sujet souvent méconnu et délaissé, l'encodage des caractères !

Pour savoir ce que sont les encodages, rendez-vous sur ce magnifique article sur Wikipédia. Le sujet de cet article n'est pas d'expliquer ce que sont les encodages, mais plutôt de vous expliquer comment faire pour que vos encodages dans votre application Rails soient tous les mêmes, pour quelle raison c'est si important et comment être sûr d'enregistrer des données externes dans le bon encodage.
Quel est l'intérêt d'avoir le même encodage partout ?
Pour une raison très simple : si deux caractères encodés de façon différentes se rencontrent, ils ne se comprennent pas. Exactement comme deux personnes ne parlant pas et ne connaissant pas les mêmes langues.
Cela vous évitera également l'apparition de caractères bizarres dans votre site comme : Ãܧ/ٟ�...

Pour vous assurer que je ne raconte pas n'importe quoi, nous allons voir un petit exemple classique. Créez un fichier .rb (peu importe où), puis mettez à l'intérieur le code suivant :

#encoding: utf-8
puts("noël".force_encoding("ISO-8859-1").encode("UTF-8"))

Exécutez ensuite ce fichier à l'aide de la commande ruby dans un terminal encodé en UTF-8. Vous devriez récupérer : noël

Alors que se passe-t-il concrètement ? Grâce au petit commentaire encoding: utf-8, nous signalons à Ruby que tout ce qui est dans ce fichier source doit être encodé en utf-8. Donc "noël" est d'abord encodé en UTF-8. La méthode force_encoding ne modifie en rien l'encodage des caractères, elle ne fait que signaler à Ruby d'interpréter cette chaîne de caractère comme si elle était encodée en ISO, ce qui n'est pas le cas.
Ruby va donc essayer d'interpréter le caractère spécial "ë" comme s'il était encodé en ISO mais comme ce n'est pas le cas, son interprétation sera donc forcément erronée. Enfin, la dernière méthode encode, contrairement à force_encoding, permet d'encoder les caractères dans un autre encodage, ici UTF-8. Finalement, nous obtenons des caractères étranges à la place du ë.

Et c'est exactement ce qui se passerait si vous dites que vos pages Web sont encodées en UTF-8 mais que vous récupérez de l'ISO depuis votre base de données.

Ruby (dans ses versions récentes) possède pas mal d'outils et de tests pré-faits sur les encodages, et Ruby On Rails récupère également très bien les erreurs générées par les bases de données.
Par exemple, si votre application attend de l'UTF-8 et que vous lui donnez à manger de l'ISO 8859-1 (Europe du nord, souvent utilisé dans les bases de données MySQL), il y a de forte chance que vous receviez une erreur du type "invalid byte sequence in UTF-8".

Les développeurs Ruby ont également pris la (sage) décision de renvoyer une erreur lorsque vous essayez de concaténer (insérer une chaîne dans une autre) deux chaînes de caractères encodées de façon différentes : # incompatible character encodings: ISO-8859-1 and UTF-8 (Encoding::CompatibilityError)

Pourquoi ? Tout simplement parce qu'il est impossible d'effectuer une traduction d'un encodage vers un autre en étant sûr qu'il n'y aura aucune "perte" d'information. Donc, plutôt que de tenter l'impossible, Ruby préfère vous signaler que vous avez un souci au niveau de vos encodages. Et c'est mieux, car croyez-moi, il vaut mieux pour vous que tout soit encodé de la même façon plutôt que d'essayer de traduire vos informations dans les différents encodages. La première solution sera bien moins douloureuse : encoder tout pareil dès le départ = gain de temps et aucune prise de tête.

Alors, pour éviter tous ces désagréments sur les encodages, cet article va effectuer un petit tour d'horizon pour voir où définir les encodages, pourquoi et comment. (Si j'oublie certains points, n'hésitez pas à commenter ;) ).

Encodons tout en UTF-8


Pourquoi l'UTF-8 ? Hé bien moi, je dis : pourquoi pas ? C'est un encodage universel compris par tous les navigateurs et dans lequel les caractères mondiaux (excepté les plus exotiques) sont tous présents, et utilisé par la majorité des développeurs web. Donc, comment faire pour que toute notre application soit en UTF-8 ? Par défaut, en Ruby, tout est encodé en "ASCII-8BIT" - plus connu sous le nom de codage "binaire". Chaque caractère est encodé dans les suites de "01" d'origine (ie. en anglais - il en découle qu'aucun caractère français ne sera compris dans cet encodage). Tous les fichiers sources en Ruby sont donc encodés, par défaut, en ASCII sur 8 Bits (j'imagine que cela pourrait changer dans les années à venir, vu que tout le monde - y compris les anglophones - aimeraient que Ruby utilise UTF-8 par défaut).

Le site Rails


Commençons par le plus simple. Dans votre projet Rails, ouvrez le fichier "config/application.rb". Ce fichier contient les configurations par défaut de votre serveur que vous pouvez modifier à loisir. Depuis Ruby 1.9, vous pouvez y définir l'encodage :
module MonApplication
class Application < Rails::Application
# ...
# Configure the default encoding used in templates for Ruby 1.9.
config.encoding = "utf-8"
# ...
end
end

Ne vous méprenez pas, cette petite ligne ne fait pas tout. (Si c'était le cas, ça serait super !) Elle indique simplement l'encodage par défaut utilisé lors de la génération des pages Web par votre serveur Rails. D'ailleurs, c'est déjà la configuration par défaut de toute application Rails, mais bon, ça ne coûte rien de s'assurer que Rails utilisera bien UTF-8.

Évidemment, vous devez ensuite signaler aux navigateurs que les pages de votre site sont encodées en UTF-8. Pour cela, il existe une balise meta à mettre entre les balises head de votre site:
	
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
Avec ça, nous partons déjà sur une bonne base.

Les fichiers .rb


Continuons dans le "pas compliqué". Si vous avez lu le cadre d'info ci-dessus, vous savez que les fichiers ".rb" sont encodés par Ruby en ASCII-8BIT. Problème ! Que se passe-t-il lorsque, par exemple, vous récupérez une chaîne de caractères depuis un tel fichier dans une page Web .html.erb ? Un cas typique est celui du controller, dans lequel vous avez :
def create
@product = Product.new(params[:product])

respond_to do |format|
if @product.save
format.html { redirect_to @product, notice: <b>'Produit créé avec succès.'</b> }
format.json { render json: @product, status: :created, location: @product }
else
format.html { render action: "new" }

format.json { render json: @product.errors, status: :unprocessable_entity }
end
end
end

Ce controller, très classique, est généré par un scaffold. J'y ai simplement remplacé la phrase de notification lors de la création d'un produit. Si je l'avais laissée en anglais, il n'y aurait eu aucun souci, car tous les caractères anglais peuvent être encodés en ASCII.

Malheureusement pour nous, nous sommes francophones, et en tant que tels, nous nous ne pouvons pas nous permettre d'omettre les accents. C'est à ce moment là, lorsque Ruby visite ce fichier, qu'apparaîtra l'erreur qu'on a tous rencontrée un jour où l'autre: invalid multibyte char (US-ASCII), parce que les accents ne peuvent pas être encodés en ASCII. Comment faire alors ? Heureusement, les développeurs Ruby ont tout prévu, il suffit d'ajouter un petit commentaire au tout début de votre fichier .rb (et quand je dis au tout début, ce doit être vraiment la première ligne de votre fichier). Ce commentaire est tout simple:
# encoding: utf-8

Je le rappelle, ce commentaire permet de signaler à Ruby qu'il doit utiliser l'encodage UTF-8 pour lire ce fichier. Grâce à ce commentaire, la lecture passe comme un charme, et la notification de création d'un produit en Rails sera bien comme nous l'avons écrite - avec des accents.

Cela signifie qu'il faut mettre ce commentaire dans chaque fichier .rb ? Eh bien oui, du moins, dans chaque fichier .rb contenant des caractères non-ASCII. Mais heureusement, la communauté Ruby/Rails a pensé a tout, et une Gem existe pour nous simplifier la vie (comme toutes les Gems). Magic Encoding permet d'ajouter cette petite ligne de commentaire à tous les fichiers .rb en exécutant simplement la commande : magic_encoding. Et par défaut, ça vous colle de l'UTF-8 partout ! Fantastique, non ?

La base de données


Reste la base de données. En fait, ce n'est pas si compliqué si vous avez une base de données vierge. Et si elle n'est pas vierge, - ce n'est pas vraiment le sujet de l'article -, il vous faudra convertir votre base de données en UTF-8 manuellement, mais heureusement, de nombreux articles ont déjà été rédigés sur le sujet. Je vous laisse faire les recherches Google. Juste une chose : avant toute conversion, faites toujours un backup de votre base de données. :)

Pour ce qui est de Ruby On Rails, comme je le disais, ce n'est pas compliqué. Tout (ou presque) se passe dans le fichier "config/database.yml". C'est effectivement là où vous spécifiez la configuration pour votre base de données. Dans ces configurations, vous pouvez préciser l'encodage comme ceci :

development:
adapter: mysql2
database: votre_base_de_donnees
encoding: utf8
collation: utf8_bin
pool: 5
username: username
password: password
host: localhost
Il suffit donc d'ajouter l'option "encoding:" selon la syntaxe de l'YAML. En petit bonus, j'ai rajouté l'option collation. En quelques mots, "collation" représente (du moins en Mysql) une série de règles utilisées pour comparer les différentes données entre elles. Personnellement, j'utilise utf8_bin, cela peut être très utile et important.

En effet, en Rails, lorsque vous générez une colonne en ajoutant l'option ":unique => true", vous dites à la base de données que chaque donnée de cette colonne doit être unique. Mysql (ou tout autre SGBD) va donc comparer chaque nouvelle donnée avec celles déjà enregistrées et vérifier son unicité. Il se trouve que, par défaut, même si votre encodage est utf8, les méthodes de comparaison de la collation "utf8-general-ci" ne sont pas forcément "conformes" à votre langue - notamment, en français où les mots : "crée" et "créé" seront signalés comme identiques par utf8-general-ci, mais pas lorsque vous utilisez la collation "utf8_bin". Par contre en utf8_bin les règles de tris sont parfois très bizarres (je me souviens avoir eu des résultats inattendus lors d'un tri alphabétique de données). Donc selon si vous souhaitez différencier les mots "crée" et "créé" ou si vous souhaitez faire des tris alphabétiques, prenez l'une ou l'autre collation. A l'heure actuelle il n'en existe aucune qui soit réellement compatible avec le français (ça c'est nul).

Bien ! Revenons-en à Rails. Une fois la configuration de votre base de données faite, utilisez la commande
RAILS_ENV=development rake db:create
Cette tâche rake va créer votre base de données avec les configurations spécifiées pour l'environnement défini.

Encodons les entrées/sorties en UTF-8

Reste encore quelques petits soucis. Que se passe-t-il si, par exemple, vous récupérez des données depuis un autre site non encodé en UTF-8, depuis un fichier CSV, ou une base de données tierces encodées autrement... Bref, comment encoder les entrées/sorties de votre application Ruby On Rails ?

En fait, Ruby 1.9 possède toute une classe pour l'encodage : Encoding. Et dans cette classe, deux variables sont particulièrement utiles : default_external et default_internal.

Pour les définir, rendez-vous dans votre fichier "config/environment.rb" et juste avant l'initialisation de votre application, ajoutez-les :
# Load the rails application
require File.expand_path('../application', __FILE__)

Encoding.default_external = Encoding::UTF_8
Encoding.default_internal = Encoding::UTF_8

# Initialize the rails application
MonApplication::Application.initialize!

Pourquoi les définir ici et pas dans application.rb (par exemple) ? Parce que, comme le signale la doc, ces variables doivent être définies avant que toutes chaînes de caractères n'aient déjà été créées au sein de l'application. Tout ce qui est exécuté avant environment.rb est utile pour la configuration de Rails et de Rack, mais pas pour l'application en elle-même. C'est "environment.rb" qui est le tout premier fichier exécuté pour la configuration de l'application proprement dite. En tant que tel, il faut définir ces variables ici et pas dans un autre fichier et avant l'initialisation de Application. Si vous voulez plus de détail sur l'initialisation d'une appli Rails, rendez-vous sur ce guide.

default_internal est la plus intéressante, elle permet d'encoder toute chaîne de caractères en l'encodage spécifié avant qu'elle ne soit exploitée dans votre application. Donc si vous récupérez une donnée, Ruby va d'abord la convertir en UTF-8, avant toute exploitation. En guise de petit bonus, cela indique également l'encodage par défaut devant être utilisé par la méthode "encode". Si vous l'utilisez, vous n'aurez donc pas à préciser "utf-8".

default_external, c'est pour tout ce qui est externe à Ruby On Rails. Vous n'êtes pas sans savoir que Ruby On Rails peut générer des fichiers PDF/CSV/JSON/XML, etc. notamment grâce à certaines Gems. default_external spécifie l'encodage utilisé par défaut lors de la génération ou de l'écriture dans de tels fichiers. Ceci inclut également les bases de données. Afin de vous assurer que vous allez bien fournir des chaînes de caractères encodées en UTF-8, veillez toujours à vérifier leur encodage à l'aide de la méthode valid_encoding? avant de les insérer dans les fichiers/ressources externes.

Forcer l'encodage


Alors, que nous pourrions penser en avoir fini... il s'avère que j'ai récemment eu un souci très particulier sur les encodages, indirectement lié à un oubli sur la collation. En fait, comme les mots "crée" et "créé" étant identiques pour la collation utf8-general-ci par défaut, MySQL retournait une erreur : Mysql2::Error: Duplicate entry 'créé' qui passait donc par Ruby On Rails pour s'afficher dans le terminal, mais à la place de cette erreur, j'avais : invalid byte sequence in UTF-8.

Pour vous mettre en situation, voici en bref ce que je faisais : j'avais une liste de mots dans un fichier.txt avec un mot par ligne, dans une tâche Rake, je prenais chaque mot pour les insérer dans ma base de données, et chaque mot devait être unique. Le fichier.txt était encodé en UTF-8, j'avais déjà fait toutes les configurations pour l'encodage en UTF-8 excepté la collation. Et malgré tout j'avais l'erreur "invalid byte sequence in utf-8".

J'ai d'abord cru qu'il y avait une erreur d'encodage au niveau du message d'erreur renvoyé par MySQL. J'ai donc d'abord forcé l'encodage de ce message d'erreur en UTF-8, je ne vous dis pas comment car c'était du débuggage et j'ai donc fait ça d'une façon pas très propre. Cela n'avait malheureusement que partiellement résolu l'erreur invalid byte sequence in UTF-8. Partiellement car au lieu d'avoir : Mysql2::Error: Duplicate entry 'créé' j'avais Mysql2::Error: Duplicate entry 'cr��' preuve qu'il y avait toujours une erreur d'encodage. Donc l'erreur d'encodage devait venir d'ailleurs.

La véritable source du problème ne pouvait venir que de l'encodage des mots récupérés. Apparemment, malgré toutes mes configurations, l'encodage de ces mots n'était pas des encodages UTF-8 valides. Donc, avant d'enregistrer ces mots dans la base de données, il me fallait les nettoyer de tout encodage non valide. Pour ce faire, il existe une petite astuce :
str.force_encoding("binary").encode("UTF-8", :invalid => :replace, :undef => :replace)
On demande à Ruby d'interpréter d'abord la chaîne de caractères comme de l'ASCII-8BIT (binary), ceci afin que la méthode d'encodage en UTF-8 puisse nettoyer tous les encodages de caractères non valides. Si nous avions simplement appelé la méthode "encode" (sans "force_encoding"), la méthode aurait tout simplement été sautée car la chaîne de caractères est déjà encodée en UTF-8, c'est juste qu'elle contient des encodages erronés venus d'on ne sait où en fait.

Et ceci termine l'article sur les encodages. J'espère qu'après ça, vous n'aurez jamais plus à être confrontés à du "invalid byte sequence in utf-8" sans savoir comment résoudre l'erreur. Ou tout du moins, j'espère vous avoir donné des pistes pour résoudre ces erreurs. ;)

(Comme d'habitude, si j'ai oublié un point important sur les encodages, n'hésitez pas à me le signaler en commentaire. ;) )


Index -- --

  • 2 Commentaire


  • Bonjour et tout d'abord merci ;-)

    Concernant la Gem "Prawn", de ce que j'ai pu lire, vous n'êtes pas le seul à avoir eu ce soucis récemment : https://groups.google.com/forum/?fromgroups=#!msg/prawn-ruby/GAgYhiBpEjY/7u8j5q_2kKsJ

    Il existait une Gem "Prawn arabic" capable, apparemment, de résoudre ce problème, malheureusement elle ne semble plus compatible avec Ruby 1.9+. Vous pouvez essayer de vous en inspirer et la rendre compatible pour Ruby 1.9 : https://rubygems.org/gems/Arabic-Prawn

    Apparemment le code nécessaire tient dans quelques petits fichiers. Si vous y parvenez, n'hésitez pas (si vous avez le temps) à publier votre version de la Gem sur Github. ;-)

    J'espère que cela vous aidera un peu,
    Cordialement,
    Kulgar.


    Par Kulgar, le 16 Janvier 2013

  • Bonjour,
    Des tuto très utiles pour les développeurs RoR. Bravo!
    J'ai développer une application avec une partie du contenu en arabe. J'ai utilisé le gem ''prawn'' pour générer des états en pdf. Cependant, les caractères harabes ne s'affichent pas correctement.
    Merci pour toute aide.
    Bardach


    Par bardach@bardach.info, le 16 Janvier 2013
Insérez votre commentaire
  1. Min: 50 caractères, Max: 800. Actuellement: 0 caractères

  2. ne pas remplir