Трагедия гуманитария в трех актах.
Важный дисклеймер: все, написанное в статье ниже - это исключительно нубский взгляд на решение проблемы. Но конечным результатом я вполне довольна, поэтому и выношу его на публичный суд.
Так сложились обстоятельства, что передо мной встала задача создать базу данных словаря-тезауруса для дальнейшего использования в лингвистических исследованиях. Собственно, лингвистика - это и есть моя специальность, к IT я имею весьма опосредованное отношение в виде одного (незаконченного) онлайн-курса по основам Python и одного семестра программирования в универе (на который я почти не ходила).
На руках у меня была электронная версия сверстанного для печати словаря в формате .pdf, текст которого после извлечения выглядел примерно вот так:
Небольшой фрагмент для сравнения
Ажиота́ж, перен.
Ажиота́жный
Ажита́ция, устар.
Аза́рт
Аффе́кт
Безу́мство, перен.
Беспа́мятство
Беспоко́йно
Беспоко́йный
Беспоко́йство
Беспоко́иться
Беспоря́дки
Вообще, сам текст в целом получился очень "грязным", приведенный пример далеко не отражает всех тех проблем, с которыми я столкнулась. Но то, как я его чистила - это совсем другая история.
Пока что сфокусируемся на одной проблеме - ударениях. Ударения в словах полезны для человека, который к словарю подходит с позиции пользователя. Но исследователю они только усложняют жизнь, потому что делают практически невозможной задачу автоматического поиска по словам. Поэтому я приняла волевое решение сразу от них избавиться.
Итак, какие способы я опробовала:
Замена гласных букв с диакритикой на обычные при помощи метода replace()
То же самое, но с использованием метода translate()
Удаление диакритики с использованием модуля unicodedata
Метод replace()
Это, наверное, один из первых (если не самый первый) метод, который изучают при знакомстве со строками в Python. Так что само собой разумеется, что первым делом я подумала про него.
Опробуем его на каком-нибудь одном слове:
s = "Беспа́мятство"
new = s.replace('а́', 'а')
print(new)
В результате получаем:
Беспамятство
Работает! Вот только появляется одна очевидная проблема. Гласных букв в русском алфавите всего 10: а, я, у, ю, о, е, ё, э, и, ы. Букву ё можно пока выкинуть, потому что над ней ударение не ставится, и тогда остается 9.
Но знак ударения у нас может быть и над первой буквой в слове, так что к строчным вариантам букв добавляются прописные - теперь их всего 17 (букву ы можно не учитывать, потому что в этом словаре нет слов, которые бы с нее начинались). Итого - 17 вариантов замен и 34 буквы в целом (набор с диакритикой и без). Конечно, можно было бы весь текст привести к строчному написанию, но я этого делать пока не хочу в силу других причин.
В общем, решение какое-то громоздкое получается. И на практике я от него сразу отказалась. Но вот сейчас решила развлечения ради посмотреть, как это будет выглядеть:
#Сначала заведем списки букв с ударением и без
no_accent = ['а', 'я', 'у', 'ю', 'о', 'е', 'э', 'и', 'ы', 'А', 'Я', 'У', 'Ю', 'О', 'Е', 'Э', 'И']
accented = ['а́', 'я́', 'у́', 'ю́', 'о́', 'е́', 'э́', 'и́', 'ы́', 'А́', 'Я́', 'У́', 'Ю́', 'О́', 'Е́', 'Э́', 'И́']
#Вручную прописывать каждую замену - долго и муторно
#Поэтому определим новую функцию
def replace_characters(original, new, str):
for i in range(len(original) - 1):
str = str.replace(original[i], new[i])
return str
#Возьмем в качестве примера строку подлиннее
str = "Беспа́мятство\nБеспоко́йно\nБеспоко́йный\nБеспоко́йство\nБеспоко́иться\nБеспоря́дки\nА́ховый"
res = replace_characters(accented, no_accent, str)
print(res)
Результат:
Беспамятство
Беспокойно
Беспокойный
Беспокойство
Беспокоиться
Беспорядки
Аховый
В принципе, все работает, на этом можно было бы остановиться. Но как-то не понравилась мне идея вручную задавать списки вариантов для замены.
Метод translate()
На самом деле в эту сторону я во время решения задачи даже не смотрела по той же самой причине, по которой я не стала в итоге использовать метод replace(). Но давайте проверим, как оно будет работать. В конце концов, список букв я уже задала выше, так что код напишется очень быстро.
no_accent = ['а', 'я', 'у', 'ю', 'о', 'е', 'э', 'и', 'ы', 'А', 'Я', 'У', 'Ю', 'О', 'Е', 'Э', 'И']
accented = ['а́', 'я́', 'у́', 'ю́', 'о́', 'е́', 'э́', 'и́', 'ы́', 'А́', 'Я́', 'У́', 'Ю́', 'О́', 'Е́', 'Э́', 'И́']
#Зададим таблицу перевода при помощи функции zip()
trans_table = dict(zip(accented, no_accent))
str = "Беспа́мятство\nБеспоко́йно\nБеспоко́йный\nБеспоко́йство\nБеспоко́иться\nБеспоря́дки\nА́ховый"
res = str.translate(trans_table)
print(res)
Выглядит лаконичнее, но результат...
Беспа́мятство
Беспоко́йно
Беспоко́йный
Беспоко́йство
Беспоко́иться
Беспоря́дки
А́ховый
... нулевой. Почему оно не сработало - я на правах нуба не имею ни малейшего понятия. Возможно, кто-нибудь мне объяснит.
Модуль unicodedata
А вот это решение я в итоге и использовала в своем финальном варианте кода. Разочаровавшись в методе replace(), я пошла бороздить просторы интернета в поисках более изящного кода и наткнулась на подходящее обсуждение на Stack Overflow.
Правда, речь там шла про удаление диакритики из текста на английском языке, что, как оказалось, не на 100% работает с русским. Но об этом позже.
В общем, взгляд мой зацепил вот этот кусочек кода, предложенный пользователем MiniQuark:
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)])
И я решила опробовать его на своем материале:
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)])
str = "Беспа́мятство\nБеспоко́йно\nБеспоко́йный\nБеспоко́йство\nБеспоко́иться\nБеспоря́дки\nА́ховый"
res = remove_accents(str)
print(res)
Результат:
Беспамятство
Беспокоино
Беспокоиныи
Беспокоиство
Беспокоиться
Беспорядки
Аховыи
Кажется, что-то пошло немного не так...
Проблема в том, что эта функция полностью убирает диакритические знаки. А в русском языке есть две буквы с диакритикой - это многострадальная Ё и Й. И если буквой Ё еще можно пожертвовать (хотя конкретно здесь - нельзя, потому что в словаре она используется для различения произносительных вариантов слов), то букву Й терять никак нельзя.
Кто-нибудь другой, наверное, закопался бы глубже и подкорректировал функцию таким образом, чтобы она убирала только знак ударения, а другую диакритику не трогала. Но я все-таки нубка, и решения у меня нубские.
Поэтому я задала функцию для временной замены нужных мне букв специальными знаками, которые больше нигде не используются в тексте и не создадут путаницы (да, это та же функция, которую я использовала выше).
def replace_characters(original, new, str):
for i in range(len(original) - 1):
str = str.replace(original[i], new[i])
return str
#буквы, которые нужно сохранить
char_preserve = ["й", "ё", "Ё"]
#знаки, на которые мы будем их менять
placeholders = ["@", "#", "%"]
str = "Беспа́мятство\nБеспоко́йно\nБеспоко́йный\nБеспоко́йство\nБеспоко́иться\nБеспоря́дки\nА́ховый"
temp = replace_characters(char_preserve, placeholders, str)
print(temp)
В результате получаем такую красоту:
Беспа́мятство
Беспоко́@но
Беспоко́@ны@
Беспоко́@ство
Беспоко́иться
Беспоря́дки
А́ховы@
Теперь осталось только убрать диакритику при помощи уже заданной функции remove_accents и произвести обратную замену символов на буквы:
temp = remove_accents(temp)
res = replace_characters(placeholders, char_preserve, temp)
print(res)
Конечный результат:
Беспамятство
Беспокойно
Беспокойный
Беспокойство
Беспокоиться
Беспорядки
Аховый
В общем, как уже говорилось выше, в конечном итоге я воспользовалась последним способом. Все-таки здесь мне пришлось вручную прописывать 3 замены, что в разы меньше, чем 17 в случае с методом replace(). Ну а метод translate(), по всей видимости, останется примером того, как делать не надо.