Quelle est la meilleure façon de supprimer les accents dans une chaîne unicode Python?

507

J'ai une chaîne Unicode en Python, et je voudrais supprimer tous les accents (diacritiques).

J'ai trouvé sur le Web une manière élégante de le faire en Java:

  1. convertir la chaîne Unicode dans sa longue forme normalisée (avec un caractère séparé pour les lettres et les signes diacritiques)
  2. supprimez tous les caractères dont le type Unicode est "diacritique".

Dois-je installer une bibliothèque telle que pyICU ou est-ce possible avec seulement la bibliothèque standard python? Et qu'en est-il de python 3?

Remarque importante: je voudrais éviter le code avec un mappage explicite des caractères accentués à leur homologue non accentué.

MiniQuark
la source

Réponses:

448

Unidecode est la bonne réponse à cela. Il transcrit toute chaîne unicode dans la représentation la plus proche possible dans le texte ascii.

Exemple:

accented_string = u'Málaga'
# accented_string is of type 'unicode'
import unidecode
unaccented_string = unidecode.unidecode(accented_string)
# unaccented_string contains 'Malaga'and is of type 'str'
Christian Oudard
la source
67
Semble bien fonctionner avec le chinois, mais la transformation du nom français "François" donne malheureusement "FranASSois", ce qui n'est pas très bon, par rapport au "François" plus naturel.
Eric O Lebigot
10
dépend de ce que vous essayez de réaliser. par exemple je fais une recherche en ce moment, et je ne veux pas translitérer grec / russe / chinois, je veux juste remplacer "± / ę / ś / ć" par "a / e / s / c"
kolinko
58
@EOL unidecode fonctionne très bien pour les chaînes comme "François", si vous lui passez des objets unicode. Il semble que vous ayez essayé avec une chaîne d'octets simples.
Karl Bartel
26
Notez que unidecode> = 0,04.10 (décembre 2012) est GPL. Utilisez des versions antérieures ou consultez github.com/kmike/text-unidecode si vous avez besoin d'une licence plus permissive et pouvez supporter une implémentation légèrement pire.
Mikhail Korobov
10
unidecoderemplace °par deg. Il fait plus que simplement supprimer les accents.
Eric Duminil
274

Que dis-tu de ça:

import unicodedata
def strip_accents(s):
   return ''.join(c for c in unicodedata.normalize('NFD', s)
                  if unicodedata.category(c) != 'Mn')

Cela fonctionne aussi sur les lettres grecques:

>>> strip_accents(u"A \u00c0 \u0394 \u038E")
u'A A \u0394 \u03a5'
>>> 

La catégorie de caractères "Mn" signifieNonspacing_Mark , qui est similaire à unicodedata.combining dans la réponse de MiniQuark (je n'ai pas pensé à unicodedata.combining, mais c'est probablement la meilleure solution, car elle est plus explicite).

Et gardez à l'esprit que ces manipulations peuvent modifier considérablement la signification du texte. Les accents, trémas, etc. ne sont pas de la "décoration".

oefe
la source
6
Ce ne sont pas des caractères composés, malheureusement - même si "ł" est nommé "LETTRE MINUSCULE LATINE L BARRÉ"! Vous devrez soit jouer à des jeux avec analyse syntaxique unicodedata.name, soit décomposer et utiliser une table similaire - dont vous auriez besoin pour les lettres grecques de toute façon (Α est juste "LETTRE MAJUSCULE GRECQUE ALPHA").
alexis
2
@andi, j'ai bien peur de ne pas deviner ce que vous voulez dire. L'échange d'email reflète ce que j'ai écrit ci-dessus: Parce que la lettre "ł" n'est pas une lettre accentuée (et n'est pas traitée comme une dans la norme Unicode), elle n'a pas de décomposition.
alexis
2
@alexis (suivi tardif): Cela fonctionne aussi parfaitement pour le grec - par exemple. "LETTRE MAJUSCULE GRECQUE ALPHA ESPRIT DASIA ET VARIA" est normalisée en "LETTRE MAJUSCULE GRECQUE ALPHA" comme prévu. Sauf si vous faites référence à la translittération (par exemple "α" → "a"), qui n'est pas la même chose que "supprimer les accents" ...
lenz
@lenz, je ne parlais pas de supprimer les accents du grec, mais du "coup" sur l'ell. Comme ce n'est pas un diacritique, le changer en plaine est identique à changer le grec Alpha en A. Si vous ne le voulez pas, ne le faites pas, mais dans les deux cas, vous remplacez un sosie latin (proche).
alexis
Fonctionne surtout bien :) Mais il ne se transforme pas ßen ascii sspar exemple. J'utiliserais toujours unidecodepour éviter les accidents.
Art
147

Je viens de trouver cette réponse sur le Web:

import unicodedata

def remove_accents(input_str):
    nfkd_form = unicodedata.normalize('NFKD', input_str)
    only_ascii = nfkd_form.encode('ASCII', 'ignore')
    return only_ascii

Cela fonctionne bien (pour le français, par exemple), mais je pense que la deuxième étape (supprimer les accents) pourrait être mieux gérée que de supprimer les caractères non ASCII, car cela échouera pour certaines langues (grec, par exemple). La meilleure solution serait probablement de supprimer explicitement les caractères unicode qui sont étiquetés comme étant des signes diacritiques.

Edit : cela fait l'affaire:

import unicodedata

def remove_accents(input_str):
    nfkd_form = unicodedata.normalize('NFKD', input_str)
    return u"".join([c for c in nfkd_form if not unicodedata.combining(c)])

unicodedata.combining(c) retournera vrai si le caractère c peut être combiné avec le caractère précédent, c'est principalement s'il s'agit d'un diacritique.

Edit 2 : remove_accentsattend une chaîne unicode , pas une chaîne d'octets. Si vous avez une chaîne d'octets, vous devez la décoder en une chaîne unicode comme celle-ci:

encoding = "utf-8" # or iso-8859-15, or cp1252, or whatever encoding you use
byte_string = b"café"  # or simply "café" before python 3.
unicode_string = byte_string.decode(encoding)
MiniQuark
la source
5
J'ai dû ajouter 'utf8' à unicode:nkfd_form = unicodedata.normalize('NFKD', unicode(input_str, 'utf8'))
Jabba
@Jabba: , 'utf8'est un "filet de sécurité" nécessaire si vous testez l'entrée dans le terminal (qui par défaut n'utilise pas unicode). Mais généralement , vous ne devez l'ajouter, car si vous supprimez des accents alors input_strest très susceptible d'être déjà UTF8. Mais ça ne fait pas de mal d'être en sécurité.
MestreLion
1
@rbp: vous devez passer une chaîne unicode à la remove_accentsplace d'une chaîne régulière (u "é" au lieu de "é"). Vous avez transmis une chaîne régulière à remove_accents, donc lorsque vous essayez de convertir votre chaîne en chaîne unicode, le asciicodage par défaut a été utilisé. Ce codage ne prend en charge aucun octet dont la valeur est> 127. Lorsque vous avez tapé "é" dans votre shell, votre système d'exploitation l'a encodé, probablement avec UTF-8 ou un encodage de page de codes Windows, et cela comprenait des octets> 127. Je vais changer ma fonction afin de supprimer la conversion en unicode: elle bombardera plus clairement si une chaîne non unicode est passée.
MiniQuark
1
@MiniQuark qui fonctionnait parfaitement >>> remove_accents (unicode ('é'))
rbp
1
Cette réponse m'a donné le meilleur résultat sur un grand ensemble de données, la seule exception est "ð" - les unicodedata n'y toucheraient pas!
s29
43

En fait, je travaille sur des projets compatibles python 2.6, 2.7 et 3.4 et je dois créer des identifiants à partir d'entrées utilisateur gratuites.

Grâce à vous, j'ai créé cette fonction qui fait des merveilles.

import re
import unicodedata

def strip_accents(text):
    """
    Strip accents from input String.

    :param text: The input string.
    :type text: String.

    :returns: The processed String.
    :rtype: String.
    """
    try:
        text = unicode(text, 'utf-8')
    except (TypeError, NameError): # unicode is a default on python 3 
        pass
    text = unicodedata.normalize('NFD', text)
    text = text.encode('ascii', 'ignore')
    text = text.decode("utf-8")
    return str(text)

def text_to_id(text):
    """
    Convert input text to id.

    :param text: The input string.
    :type text: String.

    :returns: The processed String.
    :rtype: String.
    """
    text = strip_accents(text.lower())
    text = re.sub('[ ]+', '_', text)
    text = re.sub('[^0-9a-zA-Z_-]', '', text)
    return text

résultat:

text_to_id("Montréal, über, 12.89, Mère, Françoise, noël, 889")
>>> 'montreal_uber_1289_mere_francoise_noel_889'
hexaJer
la source
2
Avec Py2.7, passer une erreur de chaîne déjà unicode à text = unicode(text, 'utf-8'). Une solution de contournement pour cela était d'ajouterexcept TypeError: pass
Daniel Reis
Très noice! A travaillé dans mon cas. Uma seleção de poesia brasileira para desenvolver a capacidade de escuta dos alunos idioma Português.
Aaron
23

Cela gère non seulement les accents, mais aussi les "traits" (comme en ø etc.):

import unicodedata as ud

def rmdiacritics(char):
    '''
    Return the base character of char, by "removing" any
    diacritics like accents or curls and strokes and the like.
    '''
    desc = ud.name(char)
    cutoff = desc.find(' WITH ')
    if cutoff != -1:
        desc = desc[:cutoff]
        try:
            char = ud.lookup(desc)
        except KeyError:
            pass  # removing "WITH ..." produced an invalid name
    return char

C'est la façon la plus élégante à laquelle je peux penser (et cela a été mentionné par alexis dans un commentaire sur cette page), bien que je ne pense pas que ce soit vraiment très élégant. En fait, c'est plus un hack, comme souligné dans les commentaires, car les noms Unicode sont - vraiment des noms, ils ne garantissent pas la cohérence ou quoi que ce soit.

Il y a encore des lettres spéciales qui ne sont pas gérées par cela, comme les lettres tournées et inversées, car leur nom unicode ne contient pas 'AVEC'. Cela dépend de ce que vous voulez faire de toute façon. J'avais parfois besoin d'un dépouillement d'accent pour obtenir l'ordre de tri du dictionnaire.

NOTE ÉDITÉE:

Intégration des suggestions des commentaires (gestion des erreurs de recherche, code Python-3).

lenz
la source
8
Vous devez intercepter l'exception si le nouveau symbole n'existe pas. Par exemple, il y a CARRÉ AVEC REMPLISSAGE VERTICAL ▥, mais il n'y a pas de CARRÉ. (sans oublier que ce code transforme le PARAPLUIE AVEC RAIN DROPS ☔ en PARAPLUIE ☂).
janek37
Cela semble élégant en exploitant les descriptions sémantiques des personnages disponibles. Avons-nous vraiment besoin de l' unicodeappel de fonction avec python 3? Je pense qu'un regex plus serré à la place du findéviterait tous les problèmes mentionnés dans le commentaire ci-dessus, et aussi, la mémorisation aiderait les performances quand c'est un chemin de code critique.
matanster
1
@matanster non, c'est une vieille réponse de l'ère Python-2; le unicodetranstypage n'est plus approprié dans Python 3. Dans tous les cas, d'après mon expérience, il n'y a pas de solution universelle et élégante à ce problème. Selon l'application, toute approche a ses avantages et ses inconvénients. Des outils de qualité comme unidecodesont basés sur des tables fabriquées à la main. Certaines ressources (tables, algorithmes) sont fournies par Unicode, par exemple. pour le classement.
lenz
1
Je répète juste ce qui est au dessus (py3): 1) unicode (char) -> char 2) try: return ud.lookup (desc) sauf KeyError: return char
mirek
@mirek vous avez raison: puisque ce fil est si populaire, cette réponse mérite une mise à jour / amélioration. Je l'ai édité.
lenz
15

En réponse à la réponse de @ MiniQuark:

J'essayais de lire dans un fichier csv qui était à moitié français (contenant des accents) et aussi quelques chaînes qui finiraient par devenir des entiers et des flottants. À titre de test, j'ai créé un test.txtfichier qui ressemblait à ceci:

Montréal, über, 12.89, Mère, Françoise, noël, 889

J'ai dû inclure des lignes 2et 3le faire fonctionner (que j'ai trouvé dans un ticket python), ainsi qu'incorporer le commentaire de @ Jabba:

import sys 
reload(sys) 
sys.setdefaultencoding("utf-8")
import csv
import unicodedata

def remove_accents(input_str):
    nkfd_form = unicodedata.normalize('NFKD', unicode(input_str))
    return u"".join([c for c in nkfd_form if not unicodedata.combining(c)])

with open('test.txt') as f:
    read = csv.reader(f)
    for row in read:
        for element in row:
            print remove_accents(element)

Le résultat:

Montreal
uber
12.89
Mere
Francoise
noel
889

(Remarque: je suis sous Mac OS X 10.8.4 et j'utilise Python 2.7.3)

aseagram
la source
1
remove_accentsétait destiné à supprimer les accents d'une chaîne unicode. Si une chaîne d'octets lui est transmise, il essaie de la convertir en une chaîne unicode avec unicode(input_str). Cela utilise l'encodage par défaut de python, qui est "ascii". Étant donné que votre fichier est codé en UTF-8, cela échouerait. Les lignes 2 et 3 changent l'encodage par défaut de python en UTF-8, alors cela fonctionne, comme vous l'avez découvert. Une autre option est de passer remove_accentsune chaîne unicode: supprimez les lignes 2 et 3, et sur la dernière ligne remplacez elementpar element.decode("utf-8"). J'ai testé: ça marche. Je mettrai à jour ma réponse pour clarifier les choses.
MiniQuark
Joli montage, bon point. (Sur une autre note: le vrai problème que j'ai réalisé est que mon fichier de données est apparemment encodé iso-8859-1, ce que je ne peux malheureusement pas utiliser avec cette fonction!)
aseagram
aseagram: remplacez simplement "utf-8" par "iso-8859-1", et cela devrait fonctionner. Si vous êtes sous Windows, vous devriez probablement utiliser "cp1252" à la place.
MiniQuark
BTW, reload(sys); sys.setdefaultencoding("utf-8")est un hack douteux parfois recommandé pour les systèmes Windows; voir stackoverflow.com/questions/28657010/… pour plus de détails.
PM 2Ring
14

gensim.utils.deaccent (texte) de Gensim - modélisation de sujets pour les humains :

'Sef chomutovskych komunistu dostal postou bily prasek'

Une autre solution est unidecode .

Notez que la solution suggérée avec unicodedata ne supprime généralement les accents que dans certains caractères (par exemple, elle se transforme 'ł'en '', plutôt qu'en 'l').

Piotr Migdal
la source
1
deaccentdonne toujours łau lieu de l.
lcieslak
Vous n'avez pas besoin d'installer NumPyet SciPyde supprimer les accents.
Nuno André
merci pour la référence gensim! comment se compare-t-il à unidecode (en termes de vitesse ou de précision)?
Etienne Kintzler
3

Certaines langues combinent les signes diacritiques comme lettres de langue et les signes diacritiques accentués pour spécifier l'accent.

Je pense qu'il est plus sûr de spécifier explicitement les diactriques que vous souhaitez supprimer:

def strip_accents(string, accents=('COMBINING ACUTE ACCENT', 'COMBINING GRAVE ACCENT', 'COMBINING TILDE')):
    accents = set(map(unicodedata.lookup, accents))
    chars = [c for c in unicodedata.normalize('NFD', string) if c not in accents]
    return unicodedata.normalize('NFC', ''.join(chars))
sirex
la source