Под деакцентированием я имею в виду убирание закорючек, черточек, крышечек и прочих умляутов с букв европейских (и не только) алфавитов. Задачка такая то и дело возникает при поиске строк.
Насколько я успел заметить, принятый способ решения — просто составление таблиц вида "á" → "a", "è" → "e" и т.д. Способ хороший, но трудоемкий. И насколько я, опять же, успел заметить, какой-то одной универсальной таблицы нет, и все составляют свои (или копируют первую понравившуюся). Мне захотелось поделиться своим решением, которое использует интересные свойства Юникода.
Кстати, слово "интересно" — это известный программистский эвфемизм для понятия "работает, но непонятно, зачем так было извращаться".
Интересное свойство Юникода заключается в том, что для букв с закорючками у него предусмотрены разные формы представления: в одной буква с закорючкой представляется как единый символ, а в другой, как два — буква отдельно, закорючка отдельно.
Форма | NFC | NFD |
---|---|---|
Символы | é | e + ́ |
Номера | U+00E9 | U+0065 + U+0301 |
NFC и NFD — это как раз названия этих форм нормализации.
Идея избавления от закорючек, соответственно, очень простая: представить символы в разобранном (NFD) виде и повыкидывать те, которые закорючки. В Питоне для таких операций над Юникодом есть специальный модуль — unicodedata. В нем, в частности, есть две функции:
- normalize, которая переводит юникодную строку в одну из форм нормализации
- combining, которая возвращает, выражаясь топорно, "класс закорючки" — некий номер, означающий к какой группе закорючек она относится или нуль, если это не закорючка
Соотвественно весь процесс выглядит очень просто:
def deaccent(value):
from unicodedata import normalize, combining
value = normalize('NFD', value)
value = u''.join(c for c in value if not combining(c))
return value
Это работает, причем, не только для европейских алфавитов, но и например для русских "ё" и "й".
Но есть отдельный вид закорючек, для которых этот способ не работает. Это так называемые "черточки" ("dash"), которые не отделяются от букв в NFD-форме. Я поленился выяснить, почему именно они не отделяются, но у меня есть догадка, что это просто от того, что эти самые черточки очень разные, и их не получается нормально классифицировать: "ø", "Ł", "Đ".
И вот специально для них я составил таки табличку. Получилось так:
def deaccent(value):
STROKES = {
u'Ø': u'O', u'ø': u'o',
u'Đ': u'D', u'đ': u'd',
u'Ħ': u'H', u'ħ': u'h',
u'Ł': u'L', u'ł': u'l',
u'Ŧ': u'T', u'ŧ': u't',
}
from unicodedata import normalize, combining
value = normalize('NFD', value)
value = u''.join(c for c in value if not combining(c))
value = u''.join(STROKES.get(c, c) for c in value)
return value
Вот в таком, приблизительно, виде оно у меня работает в музыкальном сервисе, где каких только написаний исполнителей и альбомов ни встречается.
Для тех, кто еще не спит. Впрочем, с перфекционистской точки зрения, этого для нормализации все равно может не хватать. Помимо букв с акцентами существуют еще и лигатуры — соединения двух рядом стоящих букв в одну по чисто эстетическим соображениям (например "œ" и "ij"; попробуйте выделить — это целые "буквы"). NFD-форма с ними ничего сделать не может (и не должна), но есть еще NFKD-форма (хе-хе :-) ), которая, по идее, как раз придумана, чтобы заменять всякие "научные излишества" на совместимые с ними распространенные буквы. Но и она работает странно: "ij" разделяет на "i" и "j", а "œ" не разделяет. Я в своих исследованиях на этой стадии застрял и постановил, что раз лигатура — это только вариант начертания, а не самостоятельный символ, то можно просто от юзеров требовать, чтобы лигатуры не использовали. Нефиг...
Комментарии: 16
Интересно, а ß в NFKD не разделяется? Надо будет сейчас попробовать. ;)
Спасибо. интересно и полезно.
Alexander Solovyov, с точки зрения дойча должна разделятся на "ss" =)
Должна-то должна, но вот разделяется ли... ;) Блин, почему у меня везде кодировкой koi8-r стоит? Придётся еще эту штуку искать...
Ну, всё-таки делать из «й» букву «и» — это как-то не очень…
Alexander Solovyov,
воспользовался charmap - код у нее U+00DF
Да, не разделяет. :( А жаль! Для поиска по музыке это действительно незаменимая штука...
Иван, а зачем это вообще понадобилось? Или у тебя сами группы (альбомы, композиции) хранятся без "закорючек" и ты также делаешь поиск?
Просто, как уже заметили, если буква ё еще может быть заменена на е, то замена й на и - явно не то..
Заменять немецкое "ö" на "o" тоже не совсем то, это скорее "oe" :) Но мало ли как напишут. Я ведь так понимаю, что эти замены при поиске проделываются, а не при сохранении.
Да уж...
Действительно интиресно и познавательно.
Надо самому поглубже покопатся в unicode.
Денис Лозко
http://ru.wikipedia.org/wiki/Unicode - советую почитать это.
А почему бы разделение тоже не добавить вручную? К тому же там будет всего четыре записи: œ → oe, ß → ss, æ → ae, и dz → dz
Да нет... Я вот прямо сейчас из головы могу вспомнить лигатуры ff и fi... Их там много, на самом деле. А ß вообще к ним не относится, кстати.
В таблице не хватает ещё пары ð → d.
В шрифте комментов этой буквы тольком не видно, это ð, U+00F0, Alt+0240 на клавиатуре в windows.
Здесь хитрость в том что ты пытаешься заменить транслитерацию декомпозицией; к сожалению это работает не всегда - это раз, а два - юникодная транслитерация всегда зависима от локали (поскольку та же диакритика в немецком, шведском, датском и голландском AFAIK транслитерируется в ASCII совершенно по-разному).
Мой небогатый, но существующий тем не менее опыт показывает что использование юникодных текстов в адресной строке браузера работает теперь практическеи везде (правда если пользователю надо набрать эти буквы он должен найти способ это сделать), поэтому декомпозиция-транслитерация для slugs это оверкилл. также по опыту могу сказать что если уж таки дошло до истинной юникодной транслитерации единственным пристойным инструментом является ICU.
Системы допускающие только латиницу в качестве текстовых идентификаторов в ситуации с Юникодом потребуют требуют как ICU, так и выяснения языка, которым помеченны конвертируемые строки (а это дополнительный UI, дополнительные проверки...) - то есть при встрече с такой системой надо не мудрствуя лукаво просто делать идентификаторы числовыми.
Что же касается поиска строк - достаточно знать что хранимые данные и поступивший запрос имеют одну композиционную форму (для поиска это канонически NFKC). Дабы это гарантировать для веб-системы достаточно просто фильтровать весь POST/GET и конвертировать его в нужную композиционную форму (для этого можно сделать Django middleware или использовать рельсовый плагин для нормализации параметров).
Да, в твоем случае с музыкальным сервисом верное решение - это просто уверенно применять одну форму композиции как для поисковых запросов, так и для индексов.