Под деакцентированием я имею в виду убирание закорючек, черточек, крышечек и прочих умляутов с букв европейских (и не только) алфавитов. Задачка такая то и дело возникает при поиске строк.

Насколько я успел заметить, принятый способ решения — просто составление таблиц вида "á" → "a", "è" → "e" и т.д. Способ хороший, но трудоемкий. И насколько я, опять же, успел заметить, какой-то одной универсальной таблицы нет, и все составляют свои (или копируют первую понравившуюся). Мне захотелось поделиться своим решением, которое использует интересные свойства Юникода.

Кстати, слово "интересно" — это известный программистский эвфемизм для понятия "работает, но непонятно, зачем так было извращаться".

Интересное свойство Юникода заключается в том, что для букв с закорючками у него предусмотрены разные формы представления: в одной буква с закорючкой представляется как единый символ, а в другой, как два — буква отдельно, закорючка отдельно.

Буква "é"
Форма NFC NFD
Символы é e +  ́
Номера U+00E9 U+0065 + U+0301

NFC и NFD — это как раз названия этих форм нормализации.

Идея избавления от закорючек, соответственно, очень простая: представить символы в разобранном (NFD) виде и повыкидывать те, которые закорючки. В Питоне для таких операций над Юникодом есть специальный модуль — unicodedata. В нем, в частности, есть две функции:

Соотвественно весь процесс выглядит очень просто:

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

  1. Alexander Solovyov

    Интересно, а ß в NFKD не разделяется? Надо будет сейчас попробовать. ;)

  2. PartyZan

    Спасибо. интересно и полезно.

  3. Ivan A-R

    Alexander Solovyov, с точки зрения дойча должна разделятся на "ss" =)

  4. Alexander Solovyov

    Должна-то должна, но вот разделяется ли... ;) Блин, почему у меня везде кодировкой koi8-r стоит? Придётся еще эту штуку искать...

  5. Lynn

    Ну, всё-таки делать из «й» букву «и» — это как-то не очень…

  6. buriy

    Alexander Solovyov,
    воспользовался charmap - код у нее U+00DF

    >>> from unicodedata import normalize, combining
    >>> value = u'\u00df'
    >>> value
    u'\xdf'
    >>> value2 = normalize('NFD', value)
    >>> value2
    u'\xdf'
    >>> value2 = normalize('NFKD', value)
    >>> value2
    u'\xdf'
    
  7. Alexander Solovyov

    Да, не разделяет. :( А жаль! Для поиска по музыке это действительно незаменимая штука...

  8. FX Poster

    Иван, а зачем это вообще понадобилось? Или у тебя сами группы (альбомы, композиции) хранятся без "закорючек" и ты также делаешь поиск?
    Просто, как уже заметили, если буква ё еще может быть заменена на е, то замена й на и - явно не то..

  9. Murkt

    Просто, как уже заметили, если буква ё еще может быть заменена на е, то замена й на и - явно не то..

    Заменять немецкое "ö" на "o" тоже не совсем то, это скорее "oe" :) Но мало ли как напишут. Я ведь так понимаю, что эти замены при поиске проделываются, а не при сохранении.

  10. Денис Лозко

    Да уж...

    Действительно интиресно и познавательно.

    Надо самому поглубже покопатся в unicode.

  11. FX Poster

    Денис Лозко
    http://ru.wikipedia.org/wiki/Unicode - советую почитать это.

  12. Адищев Евгений

    А почему бы разделение тоже не добавить вручную? К тому же там будет всего четыре записи: œ → oe, ß → ss, æ → ae, и dz → dz

  13. Иван Сагалаев

    Да нет... Я вот прямо сейчас из головы могу вспомнить лигатуры ff и fi... Их там много, на самом деле. А ß вообще к ним не относится, кстати.

  14. bison

    В таблице не хватает ещё пары ð → d.

    В шрифте комментов этой буквы тольком не видно, это ð, U+00F0, Alt+0240 на клавиатуре в windows.

  15. Julik

    Здесь хитрость в том что ты пытаешься заменить транслитерацию декомпозицией; к сожалению это работает не всегда - это раз, а два - юникодная транслитерация всегда зависима от локали (поскольку та же диакритика в немецком, шведском, датском и голландском AFAIK транслитерируется в ASCII совершенно по-разному).

    Мой небогатый, но существующий тем не менее опыт показывает что использование юникодных текстов в адресной строке браузера работает теперь практическеи везде (правда если пользователю надо набрать эти буквы он должен найти способ это сделать), поэтому декомпозиция-транслитерация для slugs это оверкилл. также по опыту могу сказать что если уж таки дошло до истинной юникодной транслитерации единственным пристойным инструментом является ICU.

    Системы допускающие только латиницу в качестве текстовых идентификаторов в ситуации с Юникодом потребуют требуют как ICU, так и выяснения языка, которым помеченны конвертируемые строки (а это дополнительный UI, дополнительные проверки...) - то есть при встрече с такой системой надо не мудрствуя лукаво просто делать идентификаторы числовыми.

    Что же касается поиска строк - достаточно знать что хранимые данные и поступивший запрос имеют одну композиционную форму (для поиска это канонически NFKC). Дабы это гарантировать для веб-системы достаточно просто фильтровать весь POST/GET и конвертировать его в нужную композиционную форму (для этого можно сделать Django middleware или использовать рельсовый плагин для нормализации параметров).

  16. Julik

    Да, в твоем случае с музыкальным сервисом верное решение - это просто уверенно применять одну форму композиции как для поисковых запросов, так и для индексов.

Добавить комментарий