Jaki jest najlepszy sposób na usunięcie akcentów w łańcuchu Unicode w Pythonie?

504

Mam w Pythonie ciąg znaków Unicode i chciałbym usunąć wszystkie akcenty (znaki diakrytyczne).

Znalazłem w sieci elegancki sposób na zrobienie tego w Javie:

  1. przekonwertować ciąg Unicode na jego długą znormalizowaną formę (z osobnym znakiem dla liter i znaków diakrytycznych)
  2. usuń wszystkie znaki, których typ Unicode jest „diakrytyczny”.

Czy muszę instalować bibliotekę, taką jak pyICU, czy jest to możliwe tylko przy użyciu standardowej biblioteki python? A co z python 3?

Ważna uwaga: chciałbym uniknąć kodu z wyraźnym odwzorowaniem znaków akcentowanych na ich nieakcentowany odpowiednik.

MiniQuark
źródło

Odpowiedzi:

446

Unidecode jest prawidłową odpowiedzią na to pytanie. Transliteruje dowolny ciąg Unicode do najbliższej możliwej reprezentacji w tekście ascii.

Przykład:

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
źródło
67
Wydaje się, że dobrze współpracuje z chińczykami, ale transformacja francuskiej nazwy „François” niestety daje „FranASSois”, co nie jest zbyt dobre w porównaniu z bardziej naturalnymi „Francois”.
Eric O Lebigot,
10
zależy od tego, co próbujesz osiągnąć. na przykład teraz szukam i nie chcę tłumaczyć greckiego / rosyjskiego / chińskiego, chcę tylko zastąpić „ą / ę / ś / ć” słowem „a / e / s / c”
kolinko
58
@EOL unidecode działa świetnie dla ciągów takich jak „François”, jeśli przekażesz do niego obiekty Unicode. Wygląda na to, że próbowałeś zwykłym ciągiem bajtów.
Karl Bartel,
26
Pamiętaj, że unidecode> = 0.04.10 (grudzień 2012) to GPL. Użyj wcześniejszych wersji lub sprawdź github.com/kmike/text-unidecode, jeśli potrzebujesz bardziej liberalnej licencji i możesz wytrzymać nieco gorszą implementację.
Michaił Korobow
10
unidecodezastępuje °się deg. To coś więcej niż tylko usuwanie akcentów.
Eric Duminil
273

Co powiesz na to:

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

Działa to również na litery greckie:

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

Kategoria charakter „Mn” oznacza Nonspacing_Mark, która jest podobna do unicodedata.combining w odpowiedzi MiniQuark za (nie myśleć unicodedata.combining, ale prawdopodobnie jest lepszym rozwiązaniem, ponieważ jest to bardziej wyraźne).

I pamiętaj, że te manipulacje mogą znacznie zmienić znaczenie tekstu. Akcenty, umlauty itp. Nie są „dekoracją”.

oefe
źródło
6
Nie są to niestety skomponowane postacie - mimo że „ł” nosi nazwę „LATIN SMALL LETTER L WITH STROKE”! Musisz albo zagrać w parsowanie unicodedata.name, albo załamać się i użyć podobnego do stołu - który i tak potrzebujesz greckich liter (Α to po prostu „GRECKA LITERA LITEROWA ALFA”).
Alexis
2
@ Andi, obawiam się, że nie mogę zgadnąć, co chcesz zrobić. Wymiana wiadomości e-mail odzwierciedla to, co napisałem powyżej: Ponieważ litera „ł” nie jest literą akcentowaną (i nie jest traktowana jako jedna w standardzie Unicode), nie ma rozkładu.
Alexis
2
@alexis (późne uzupełnienie): Działa to doskonale również w przypadku języka greckiego - np. „GRECKA LISTA KAPITAŁOWA ALFA Z DASIĄ I VARIĄ” jest znormalizowana do „GRECKA LITERA KAPITAŁOWA ALPHA”, zgodnie z oczekiwaniami. Chyba że masz na myśli transliterację (np. „Α” → „a”), która nie jest tym samym, co „usuwanie akcentów” ...
lenz
@lenz, nie mówiłem o usuwaniu akcentów z języka greckiego, ale o „uderzeniu” w ell. Ponieważ nie jest to diakrytyczny, zmiana go na zwykły ell jest tym samym, co zmiana greckiej alfy na A. Jeśli nie chcesz, nie rób tego, ale w obu przypadkach zastępujesz łaciński (prawie) podobny wygląd.
Alexis
Przeważnie działa dobrze :) Ale ßna ssprzykład nie przekształca się w ascii . Nadal używałbym, unidecodeaby uniknąć wypadków.
Art.
145

Właśnie znalazłem tę odpowiedź w Internecie:

import unicodedata

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

Działa to dobrze (na przykład w języku francuskim), ale myślę, że drugim krokiem (usunięcie akcentów) można by się lepiej zająć niż upuszczenie znaków spoza ASCII, ponieważ nie powiedzie się to w niektórych językach (na przykład po grecku). Najlepszym rozwiązaniem byłoby prawdopodobnie jawne usunięcie znaków Unicode oznaczonych jako znaki diakrytyczne.

Edycja : to załatwia sprawę:

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)zwróci true, jeśli znak cmożna połączyć z poprzednim, to znaczy, jeśli jest to znak diakrytyczny.

Edycja 2 : remove_accentsoczekuje ciągu znaków Unicode , a nie ciągu bajtów. Jeśli masz ciąg bajtów, musisz go zdekodować na ciąg znaków Unicode:

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
źródło
5
Musiałem dodać „utf8” do Unicode:nkfd_form = unicodedata.normalize('NFKD', unicode(input_str, 'utf8'))
Jabba
@Jabba: , 'utf8'jest „siatką bezpieczeństwa” potrzebną, jeśli testujesz dane wejściowe w terminalu (który domyślnie nie używa Unicode). Ale zwykle nie musisz go dodawać, ponieważ jeśli usuwasz akcenty, input_strnajprawdopodobniej jest to już utf8. Jednak nie zaszkodzi być bezpiecznym.
MestreLion,
1
@rbp: należy przekazać ciąg znaków Unicode remove_accentszamiast zwykłego ciągu (u „é” zamiast „é”). Przekazałeś zwykły ciąg znaków remove_accents, więc przy próbie konwersji łańcucha na ciąg znaków Unicode asciiużyto domyślnego kodowania. To kodowanie nie obsługuje żadnego bajtu, którego wartość wynosi> 127. Kiedy wpiszesz „é” w swojej powłoce, twój system operacyjny zakodował to, prawdopodobnie za pomocą UTF-8 lub jakiegoś kodowania strony kodowej Windows, i które zawierało bajty> 127. Zmienię swoją funkcję, aby usunąć konwersję na Unicode: będzie bombardować wyraźniej, jeśli zostanie przekazany ciąg inny niż Unicode.
MiniQuark,
1
@MinQuark, który działał idealnie >>> remove_accents (unicode ('é'))
rbp
1
Ta odpowiedź dała mi najlepszy wynik na dużym zestawie danych, jedynym wyjątkiem jest „ð” - unicodedata by tego nie dotknął!
s29
43

Właściwie pracuję nad zgodnym z projektem Pythonem 2.6, 2.7 i 3.4 i muszę tworzyć identyfikatory z wolnych wpisów użytkowników.

Dzięki tobie stworzyłem tę funkcję, która działa cuda.

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

wynik:

text_to_id("Montréal, über, 12.89, Mère, Françoise, noël, 889")
>>> 'montreal_uber_1289_mere_francoise_noel_889'
hexaJer
źródło
2
Z Py2.7, przekazywanie błędów już ciągów znaków Unicode na text = unicode(text, 'utf-8'). Obejściem tego było dodanieexcept TypeError: pass
Daniel Reis
Bardzo głośno! W moim przypadku zadziałało. Uma seleção de poesia brasileira para desenvolver a capacidade de escuta dos alunos idioma Português.
Aaron,
23

Dotyczy to nie tylko akcentów, ale także „pociągnięć” (jak w ø itp.):

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

Jest to najbardziej elegancki sposób, jaki mogę wymyślić (o czym wspomniała Alexis w komentarzu na tej stronie), chociaż nie sądzę, aby był on naprawdę elegancki. W rzeczywistości jest to raczej hack, jak wskazano w komentarzach, ponieważ nazwy Unicode są - tak naprawdę tylko nazwami, nie dają żadnej gwarancji, że będą spójne lub coś w tym rodzaju.

Nadal istnieją specjalne litery, które nie są przez to obsługiwane, takie jak litery odwrócone i odwrócone, ponieważ ich nazwa Unicode nie zawiera „Z”. To i tak zależy od tego, co chcesz zrobić. Czasami potrzebowałem usuwania akcentów, aby uzyskać porządek sortowania w słowniku.

EDYTUJ NOTATKĘ:

Uwzględniono sugestie z komentarzy (obsługa błędów wyszukiwania, kod Python-3).

lenz
źródło
8
Powinieneś złapać wyjątek, jeśli nowy symbol nie istnieje. Na przykład jest KWADRAT Z WYPEŁNIENIEM PIONOWYM ▥, ale nie ma KWADRATU. (nie wspominając, że ten kod przekształca UMBRELLA WITH RAIN DROPS ☔ w UMBRELLA ☂).
janek37
Wygląda elegancko dzięki wykorzystaniu semantycznych opisów dostępnych znaków. Czy naprawdę potrzebujemy tam unicodewywołania funkcji z Pythonem 3? Myślę, że ściślejsze wyrażenie regularne zamiast tego finduniknęłoby wszystkich problemów wymienionych w powyższym komentarzu, a także zapamiętywanie pomogłoby w wydajności, gdy jest to krytyczna ścieżka kodu.
Matanster
1
@ matanster nie, to stara odpowiedź z ery Python-2; unicodetypecast nie jest już właściwe w Pythonie 3. W każdym przypadku, w moim doświadczeniu nie ma uniwersalnego, eleganckie rozwiązanie tego problemu. W zależności od aplikacji każde podejście ma swoje zalety i wady. Dobrze prosperujące narzędzia, takie jak unidecodeoparte na ręcznie wykonanych stołach. Niektóre zasoby (tabele, algorytmy) są dostarczane przez Unicode, np. do zestawienia.
lenz
1
Po prostu powtarzam, co jest powyżej (py3): 1) unicode (char) -> char 2) try: return ud.lookup (desc) oprócz KeyError: return char
mirek
@mirek masz rację: ponieważ ten wątek jest tak popularny, ta odpowiedź zasługuje na aktualizację / poprawę. Zredagowałem to.
lenz
15

W odpowiedzi na odpowiedź @ MiniQuark:

Próbowałem odczytać plik csv, który był w połowie francuski (zawierający akcenty), a także niektóre ciągi, które ostatecznie stałyby się liczbami całkowitymi i liczbami zmiennoprzecinkowymi. W ramach testu utworzyłem test.txtplik, który wyglądał tak:

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

Musiałem dołączyć wiersze 2i 3sprawić, by zadziałało (co znalazłem w bilecie Pythona), a także dołączyć komentarz @ 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)

Wynik:

Montreal
uber
12.89
Mere
Francoise
noel
889

(Uwaga: korzystam z Mac OS X 10.8.4 i używam Python 2.7.3)

aseagram
źródło
1
remove_accentsmiał usunąć akcenty z ciągu znaków Unicode. W przypadku przekazania ciągu bajtów, próbuje przekonwertować go na ciąg znaków Unicode za pomocą unicode(input_str). Używa domyślnego kodowania Pythona, którym jest „ascii”. Ponieważ plik jest zakodowany za pomocą UTF-8, to się nie powiedzie. Linie 2 i 3 zmieniają domyślne kodowanie Pythona na UTF-8, więc działa, jak się dowiedziałeś. Inną opcją jest przekazanie remove_accentsciągu znaków Unicode: usuń linie 2 i 3, aw ostatnim wierszu zastąp elementje element.decode("utf-8"). Testowałem: działa. Zaktualizuję moją odpowiedź, aby była jaśniejsza.
MiniQuark,
Niezła edycja, dobra uwaga. (Inna uwaga: prawdziwym problemem, który sobie uświadomiłem, jest to, że mój plik danych jest najwyraźniej zakodowany, w iso-8859-1którym niestety nie mogę pracować z tą funkcją!)
aseagram
aseagram: po prostu zamień „utf-8” na „iso-8859-1” i powinno działać. Jeśli korzystasz z systemu Windows, prawdopodobnie powinieneś zamiast tego użyć „cp1252”.
MiniQuark,
BTW, reload(sys); sys.setdefaultencoding("utf-8")to wątpliwy hack czasami zalecany dla systemów Windows; szczegółowe informacje można znaleźć na stronie stackoverflow.com/questions/28657010/ ...
PM 2,
14

gensim.utils.deaccent (tekst) z Gensim - modelowanie tematów dla ludzi :

'Sef chomutovskych komunistu dostal postou bily prasek'

Innym rozwiązaniem jest kod unidecode .

Zauważ, że sugerowane rozwiązanie z unicodedata zazwyczaj usuwa akcenty tylko w niektórych postaciach (np. Zmienia się 'ł'w '', a nie w 'l').

Piotr Migdal
źródło
1
deaccentnadal daje łzamiast l.
lcieslak
Nie musisz instalować NumPyi SciPyusuwać akcentów.
Nuno André,
dzięki za referencje gensim! jak wypada w porównaniu do kodu unidecode (pod względem szybkości lub dokładności)?
Etienne Kintzler,
3

Niektóre języki łączą znaki diakrytyczne jako litery języka i znaki diakrytyczne z akcentem, aby określić akcent.

Myślę, że bezpieczniej jest wyraźnie określić, jakie diactrics chcesz usunąć:

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
źródło