Трагедия гуманитария в трех актах.
Важный дисклеймер: все, написанное в статье ниже - это исключительно нубский взгляд на решение проблемы. Но конечным результатом я вполне довольна, поэтому и выношу его на публичный суд.
Так сложились обстоятельства, что передо мной встала задача создать базу данных словаря-тезауруса для дальнейшего использования в лингвистических исследованиях. Собственно, лингвистика - это и есть моя специальность, к 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(), по всей видимости, останется примером того, как делать не надо.
Комментарии (17)
user18383
00.00.0000 00:00+1Метод translate не работает потому что нужно перед этим использовать maketrans
Markscheider
00.00.0000 00:00А это разовая задача или вам часто надо будет словари таким образом чистить?
huder
00.00.0000 00:00+4Честно говоря, первый способ выглядит лучше – да там список замен, но он читается однозначно и не сломает ничего, если по какой-то причине в словах будут
@#%
или просто другие диакритические буквы. А особенно если задача была сделать один раз - то на этом можно было и останавливаться
VWVWV
00.00.0000 00:00Я просто вставил Ваш текст в блокнот windows и сохранил в кодировке по умолчанию, вот что получилось: Беспоко?йство Беспоко?иться Беспоря?дки.
Вопросительные знаки легко удалить можно заменой в том же блокноте.
root4joy
00.00.0000 00:00был у меня файл на мегабайт в юникоде где всего в одном месте было ударение но оно меняло смысл мегабайта - не смог заменить апострофом или заглавной буквой чтобы уменьшить фaйл в два раза ... программно убедился что только в одном месте файла ударение - стóит
Irval
В чем, собственно, проблема просто удалить все вхождения "символа ударения"?
UPD: Мы же не в Индии живем, где за количество строк платят :)
DistortNeo
Юникод — сложная штука. Буква "á" может быть представлена как одним кодпоинтом (225), так и двумя (97, 769) или (1072, 769).
В общем случае сказать, как именно эта буква представлена в произвольном тексте, нельзя.
Rsa97
225 — это латинская a с ударением. Для кириллической а отдельного кодпоинта с ударением нет.
Если же предполагать, что в тексте смешана кириллица с латиницей, то чистить надо гораздо серьёзнее, заменяя символы одинакового начертания a-а, e-е, o-о, p-р и так далее.
aborouhin
По уму, конечно, всё так. А по факту приходится приспосабливаться к тому печальному факту, что куча софта с надстрочными знаками юникода нормально работать до сих пор не умеет. Вот даже в Win11 наисвежайшая версия "блокнота" с модными вкладками и тёмной темой периодически отображает их с глюками (ударение съезжает вправо), а при перемещении курсора / выделении текста стрелками считает за 2 символа.
Поэтому, чтобы минимизировать проблемы, сам тоже ударные á, ó, ý и é пишу а латинских версиях, и только и́, ы́, э́, ю́ и я́, для которых латинских аналогов нет, - в комбинированном виде (у меня на них все хоткеи настроены, т.к. имею дурную привычку во всех случаях, когда из-за разного ударения может меняться смысл слова, его явно проставлять).
arokettu
Я бы тогда предложил заюзать смесь способа @Irval c юникодом, что-то вроде
Сначала нормализуем в конкретное представление, потом удаляем конкретный символ
DistortNeo
Тогда мы ещё и букву "й" покорёжим.
arokettu
й и ё распадутся, как и у автора, на букву и модификатор, можно их собрать обратно нормализовав еще раз в NFKC
DistortNeo
Ок, принимается
CrazyElf
Да, похоже в данном случае это вообще единственный нормальный вариант )
MountainGoat
В форточку влетает зануда
Зануда: Кадратные с-з-з-з-кобки не нуж-ж-ж-ны, не нуж-ж-ж-ны! Лишний масс-с-ифф! Лиш-ш-ш-шний!
Зануда делает круг и вылетает обратно
andreymal
Без кадратных с-з-з-з-кобок на 15% медленнее