Faire sauter les diacritiques et casser les ligatures d’une chaîne de caractères
Sommaire
Introduction
Je veux écrire un filtre qui, dans une chaîne de caractères :
- remplace les lettres pourvues de diacritiques par leur version sans diacritique :
ô
deviento
; - délie les ligatures :
œ
devientoe
.
En résumé, il s’agit de produire en sortie une chaîne dont les lettres appartiennent au répertoire de caractères du codage ASCII.
Les exemples qui suivent ont été testés sur Debian 12 et macOS 14. Le comportement de certaines commandes – tr
et iconv
– varie d’un système à l’autre : je signale les différences que j’ai pu constater et m’efforce de rechercher une solution qui soit portable.
Quels sont les caractères concernés ?
Une circulaire de 2014 émanant du ministère de la Justice et relative à l’état-civil donne une liste de « voyelles et consonnes accompagnées d’un signe diacritique connues de la langue française » ; elle mentionne également les ligatures.
Nous nous en tiendrons aux caractères de cette liste, c’est-à-dire :
àâäéèêëïîôöùûüÿçæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇÆŒ
À partir de la chaîne ci-dessus, le filtre doit produire la sortie suivante :
aaaeeeeiioouuuycaeoeAAAEEEEIIOOUUUYCAEOE
Avec tr : mauvaise idée
On s’attendrait à ce qu’une commande comme tr liste1 liste2
– dans laquelle l’énième caractère de liste 1
est remplacé par l’énième caractère de liste 2
– nous soit utile (voir mes notes sur la commande tr).
Or deux difficultés se présentent :
La première est que tr
remplace un caractère par un seul et unique caractère : on ne peut donc s’en servir pour traiter les ligatures, car il faut pour cela substituer deux caractères (o
et e
par exemple) à un seul (œ
). Si l’on choisi d’utiliser tr
pour les diacritiques, il sera donc nécessaire d’envoyer sa sortie vers un second programme qui finira le travail. Je cherche plus simple.
Seconde difficulté : la version de tr
fournie par les GNU coreutils (qui est celle installée sur les systèmes GNU/Linux) ne gère pas les caractères accentués. Ou plutôt elle ne gère pas les caractères qui sont codés sur plus d’un octet, ce qui est le cas des lettres accentuées (et des ligatures) dans le codage UTF-8.
Sur ce dernier point, je renvoie là encore à mes notes sur tr.
Voici simplement deux exemples de problèmes qu’on peut rencontrer avec l’implémentation GNU de la commande tr
:
$ echo 'été' | tr 'ét' 'ub'
ubbub
On attendrait ubu
, on a ubbub
.
Si on essaye avec tous nos caractères accentués, on génère de la poésie d’avant-garde :
$ echo 'àâäéèêëïîôöùûüÿçÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇ' | tr 'àâäéèêëïîôöùûüÿçÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇ' 'aaaeeeeiioouuuycAAAEEEEIIOOUUUYC'
CaCeCeCiCoCuCuCcCACECECICOCUCUCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
(Essayez de réciter à voix haute la sortie de cette commande, vous épaterez votre auditoire.)
La version de tr
installée par défaut sur macOS, quant à elle, gère bien les caractères accentués :
$ echo 'été' | tr 'ét' 'ub'
ubu
Signalons au passage qu’elle permet d’utiliser les classes d’équivalence de POSIX :
$ echo 'aàâ' | tr '[=a=]' 'a'
aaa
(Avec la version GNU, ça ne marche pas, la même commande renvoie la sortie suivante : aàâ
.)
Avec sed : cela fonctionne sur GNU/Linux et macOS
La commande y
de sed
effectue des translittérations à la manière de tr
. Elle s’utilise sous la forme suivante : y/liste 1/liste 2/
, dans laquelle liste 1
est la liste des caractères à remplacer et liste 2
celle des caractères de remplacement. De même qu’avec tr
, l’énième caractère de liste 1
est remplacé par l’énième caractère de liste 2
.
Bonne nouvelle, les caractères à plusieurs octets sont pris en charge, que l’on travaille sur GNU/Linux ou macOS :
$ sed 'y/àâ/aa/' <<< 'aàâä'
aaaä
La commande y
de sed
possède la même limitation que tr
: elle remplace un caractère par un unique caractère. Pour les ligatures, on peut toutefois faire suivre la commande y
par une série de commandes s
, qui opèreront les substitutions voulues – par exemple s/æ/ae/g
remplacera toutes les occurrences de æ
par ae
(le g
final indiquant de remplacer toutes les occurrences de æ
).
Autre difficulté posée par sed
: pour écrire notre commande, il faut établir la liste de tous les caractères à rechercher, puis celle de tous les caractères de remplacement. Un oubli dans l’une ou l’autre liste, et la sortie sera faussée. Ce n’est donc pas le genre de chose qu’on peut composer à la volée dans son terminal ; il faut en faire un script.
À quoi ressemblerait ce script ? Voici une proposition :
$ cat translit-avec-sed
#!/bin/sh
avec_diacritiques=àâäéèêëïîôöùûüÿçÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇ
sans_diacritiques=aaaeeeeiioouuuycAAAEEEEIIOOUUUYC
sed "y/$avec_diacritiques/$sans_diacritiques/
s/æ/ae/g
s/Æ/AE/g
s/œ/oe/g
s/Œ/OE/g"
J’ai stocké les listes de caractères dans des variables : je peux ainsi m’assure d’un coup d’œil qu’elles font la même longueur, et il est plus facile de les modifier, par exemple pour les adapter à d’autres langues.
Essayons de lancer notre script :
$ resultat_attendu=aaaeeeeiioouuuycaeoeAAAEEEEIIOOUUUYCAEOE
$ resultat_obtenu="$(echo "àâäéèêëïîôöùûüÿçæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇÆŒ" | ./translit-avec-sed)"
$ [[ $resultat_attendu = $resultat_obtenu ]] && echo "OK"
OK
On peut le modifier pour qu’il soit exécuté directement par sed
:
#!/usr/bin/sed -f
y/àâäéèêëïîôöùûüÿçÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇ/aaaeeeeiioouuuycAAAEEEEIIOOUUUYC/
s/æ/ae/g
s/Æ/AE/g
s/œ/oe/g
s/Œ/OE/g
Avec iconv : cela fonctionne sur GNU/Linux mais pas sur macOS
Le programme iconv
convertit du texte d’un codage de caractères à un autre. Par exemple, pour passer en UTF-8 un fichier codé en UTF-16, on lancera la commande suivante, dans laquelle l’option -f
indique le codage d’entrée et -t
celui de sortie :
$ iconv -f UTF-16 -t UTF-8 < fichier-utf-16 > fichier utf-8
$ file fichier-utf-*
fichier-utf-16: Unicode text, UTF-16, little-endian text
fichier-utf-8: Unicode text, UTF-8 text
Comme indiqué dans l’introduction, nous souhaitons que les lettres écrites en sortie de commande appartiennent toutes au répertoire du codage ASCII. Essayons donc d’utiliser iconv
avec les options -f UTF-8
et -t ASCII
:
$ iconv -f UTF-8 -t ASCII <<< 'àâäéèêëïîôöùûüÿçæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇÆŒ'
iconv: séquence d'échappement non permise à la position 0
La commande renvoie une erreur, à laquelle le manuel indique qu’il est possible de remédier :
Lorsque la chaîne
//TRANSLIT
est ajoutée à encodage-cible, la translitération est activée. Cela signifie que lorsqu’un caractère ne peut pas être représenté dans le jeu de caractères cible, il pourra être approximé par un ou plusieurs caractères de forme similaire. Les caractères qui sont en-dehors du jeu de caractères cible et pour lesquels la translitération n’est pas possible sont remplacés par un point d’interrogation (?
) dans l’affichage.
Essayons :
$ iconv -f UTF-8 -t ASCII//TRANSLIT <<< 'àâäéèêëïîôöùûüÿçæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇÆŒ'
aaaeeeeiioouuuycaeoeAAAEEEEIIOOUUUYCAEOE
Cela fonctionne. Nous avons donc une commande plutôt simple à écrire, qui traite d’un coup diacritiques et ligatures, et qui fait le travail pour d’autres caractères que ceux qui figurent dans la circulaire de 2014.
Pour simplifier davantage, on peut en faire un alias :
$ echo "alias ,2ascii='iconv -f UTF-8 -t ASCII//TRANSLIT'" >> ~/.bash_aliases
$ tail -n1 ~/.bash_aliases
alias ,2ascii='iconv -f UTF-8 -t ASCII//TRANSLIT'
$ . ~/.bash_aliases
$ echo 'à' | ,2ascii
a
Malheureusement la version d’iconv
installée sur macOS n’offre pas la possibilité d’ajouter la chaîne //TRANSLIT
à l’encodage-cible.
$ iconv -f UTF-8 -t ASCII <<< 'àâäéèêëïîôöùûüÿçæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇÆŒ'
iconv: iconv(): Illegal byte sequence
$ iconv -f UTF-8 -t ASCII//TRANSLITT <<< 'àâäéèêëïîôöùûüÿçæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÇÆŒ'
???"O`U"UAE`UAE^U"O
iconv: warning: invalid characters: 32
Avec le module unicodedata de Python : cela fonctionne sur GNU/Linux et macOS
Voici enfin un script en Python qui apporte une solution à ce problème, et qui devrait fonctionner sur GNU/Linux et sur macOS :
#! /usr/bin/env python3
import sys
import unicodedata
def remove_diacritics(str):
nfd_str = unicodedata.normalize("NFD", str)
# unicode 'Mn' category = mark,nonspacing
letters_without_diacritics = [
c for c in nfd_str
if unicodedata.category(c) != 'Mn'
]
return unicodedata.normalize(
"NFC","".join(letters_without_diacritics))
def decompose_ligatures(str):
return (str.replace("œ","oe")
.replace("Œ","OE")
.replace("æ","ae")
.replace("Æ","AE")
)
lines = sys.stdin.readlines()
for line in lines:
print(decompose_ligatures(
remove_diacritics(line)), end='')
L’indentation du programme peut paraître étrange, mais j’ai essayé de garder des lignes courtes pour la présentation sur cette page.
Le code repose sur le module unicodedata, bien pratique pour manipuler des caractères Unicode.
Il fait appel à deux fonctions distinctes, l’une pour les diacritiques et l’autre pour les ligatures.
Je n’ai pas trouvé comment scinder les ligatures, sinon par un codage en dur – le programme doit donc être adapté si le texte contient d’autres ligatures que les quatre du français.
En revanche le traitement des diacritiques est propre :
- la fonction
unicodedata.normalize
convertit le texte dans la forme décomposée de l’UTF-8 (NFD
) : c’est-à-dire que la lettreà
, correspondant à un unique caractère Unicode dans sa forme composée (NFC
), est convertie en une forme à deux caractères, lea
d’une part et l’accent grave d’autre part ; - puis on passe en revue les caractères du texte ainsi converti, en supprimant ceux qui appartiennent à la catégorie Unicode
Mn
, initiales de l’anglais mark,nonspacing, littéralement « marque sans chasse » (il s’agit de la traduction donnée par Yannis Haralambous à la page 95 de Fontes et codages [Paris, O’Reilly, 2004]) – c’est-à-dire les accents, cédilles et autres diacritiques. Dans la chaîne décomposée formée dua
d’une part et de son accent d’autre part, on ne garde que lea
.
L’avantage du script en Python sur celui que je proposais avec sed
est qu’il traite a priori tous les diacritiques, tandis que sed
ne traite que les caractères qu’on lui indique. Reste qu’il est plus long à écrire – n’étant pas à l’aise en programmation, j’apprécie le shell pour sa souplesse, et même Python me paraît un peu lourd.
Conclusion
Nous avons éliminé tr
pour cette tâche.
Le filtre écrit avec sed
est portable, mais il faut lui indiquer tous les caractères à remplacer. Un oubli demeure possible.
Les programmes fondés sur iconv
et sur Python sont plus universels :
- la commande faisant appel à
iconv
est simple à écrire. Malheureusement elle ne fonctionne pas sur macOS ; - la commande en Python fonctionne sur les deux plateformes, traite a priori tous les diacritiques, mais réclame tout de même que les ligatures soient indiquées une par une.