Морфология
Библиотека pymorphy2
Вы уже знаете стандартные строковые функции и пользовались ими. Мы умеем работать со строками посимвольно и знаем, как представляется текстовая информация. Давайте перейдем на уровень выше. Строка и текст в общем случае состоят не из набора букв, а из слов, и иногда нужно работать именно со словами, а не просто с последовательностями байтов.
Возьмем для примера склонение существительных с числительными. Например, на форуме надо написать, что в теме «21 комментарий», но «24 комментария». То же самое нужно делать и для других слов: например, «новость», «пользователь». Иногда на сайтах обходят эту проблему и вставляют машинное «комментариев: 21».
Довольно много статей на эту тему. Например, эта.
Давайте посмотрим, какие средства работы со словами есть в Python, и познакомимся с библиотекой pymorphy2 (морфология).
Если эта библиотека отсутствует в вашем Python, ее надо установить с помощью утилиты pip.
Словари распространяются отдельными пакетами. Для русского языка используется pymorphy2-dicts-ru.
Они обновляются время от времени, для обновления используйте команду
Морфологический анализ
В pymorphy2 для морфологического анализа слов есть класс MorphAnalyzer.
Морфологический анализ — определение характеристик слова на основе того, как оно пишется. При морфологическом анализе не учитываются соседние слова.
Для любого слова библиотека делает несколько предположений, что оно может означать, и обозначает свою уверенность в этом предположении.
Метод MorphAnalyzer.parse() возвращает один или несколько объектов типа Parse с информацией о том, как слово может быть разобрано.
[
Parse(word='ваня', tag=OpencorporaTag('NOUN,anim,masc,Name sing,
nomn'), normal_form='ваня', score=1.0, methods_stack=((
<DictionaryAnalyzer>, 'ваня', 407, 0),))
]
В данном случае предположение одно с уверенностью score=1.0. Итак, мы имеем дело с существительным NOUN, именем собственным, одушевленным, мужского рода. Конечно, предположений может быть несколько:
4
[Parse(word='пила', tag=OpencorporaTag('NOUN,inan,femn sing,nomn'), normal_form='пила', score=0.428571, methods_stack=((<DictionaryAnalyzer>, 'пила', 55, 0),)),
Parse(word='пила', tag=OpencorporaTag('VERB,impf,tran femn,sing,past, indc'), normal_form='пить', score=0.285714, methods_stack=((<DictionaryAnalyzer>, 'пила', 444, 8),)),
Parse(word='пила', tag=OpencorporaTag('NOUN,anim,masc,Name sing, gent'), normal_form='пил', score=0.142857, methods_stack=((<DictionaryAnalyzer>, 'пила', 1124, 1),)),
Parse(word='пила', tag=OpencorporaTag('NOUN,anim,masc,Name sing, accs'),normal_form='пил', score=0.142857, methods_stack=((<DictionaryAnalyzer>, 'пила', 1124, 3),))]
У каждого разбора есть тег:
Parse(word='пила', tag=OpencorporaTag('VERB,impf,tran femn,sing,past, indc'), normal_form='пить', score=0.285714, methods_stack=((<DictionaryAnalyzer>, 'пила', 444, 8),))
Тег
Тег — набор граммем (грамматических признаков), характеризующих данное слово.
Например, тег 'VERB,impf, tran femn, sing,past,indc' означает, что:
- слово — глагол (VERB)
- несовершенного вида (impf)
- переходный (tran)
- женского рода (femn)
- единственного числа (sing)
- прошедшего времени (past)
- изъявительного наклонения (indc).
Полный список граммем (грамматических единиц) можно посмотреть тут и тут.
Работа с тегами
Чтобы проверить, есть ли в данном теге отдельная граммема (или все граммемы из указанного множества), используйте оператор in:
Пример:
Кроме того, у каждого тега есть атрибуты, через которые можно получить часть речи, число и другие характеристики:
| Параметр | Описание |
|---|---|
| p.tag.POS | часть речи |
| p.tag.animacy | одушевленность |
| p.tag.aspect | вид:совершенный или несовершенный |
| p.tag.gender | род (мужской, женский, средний) |
| p.tag.involvement | включенность говорящего в действие |
| p.tag.mood | наклонение (повелительное, изъявительное) |
| p.tag.number | число (единственное, множественное) |
| p.tag.person | лицо (1, 2, 3) |
| p.tag.tense | время (настоящее, прошедшее, будущее) |
| p.tag.transitivity | переходность (переходный, непереходный) |
| p.tag.voice | залог (действительный, страдательный) |
Если мы попытаемся получить доступ к граммеме, которой нет для слова данной части речи, получим None.
Итак, мы можем разбирать большие тексты на части и узнавать информацию о словах: например, искать глаголы, подсчитывать имена и т. д.
Для изменения слов (к примеру, склонения существительных) можно использовать метод inflect:
Parse(
word='случая', tag=OpencorporaTag('NOUN,inan,masc sing,gent'),
normal_form='случай', score=1.0,
methods_stack=((<DictionaryAnalyzer>, 'случая', 175, 1),)
)
И во множественном числе:
Parse(word='случаев', tag=OpencorporaTag('NOUN,inan,masc plur,gent'), normal_form='случай', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'случаев', 175, 7),))
Метод inflect работает и с другими частями речи — например, глаголами:
Parse(word='программировал', tag=OpencorporaTag('VERB,impf,tran masc, sing,past,indc'),normal_form='программировать', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'программировал', 168, 7),))
Вернемся к параметру score.
[Parse(word='пила', tag=OpencorporaTag('NOUN,inan,femn sing, nomn'), normal_form='пила', score=0.428571, methods_stack=((<DictionaryAnalyzer>,'пила', 55, 0),)),
Parse(word='пила', tag=OpencorporaTag('VERB,impf,tran femn,sing, past,indc'), normal_form='пить', score=0.285714, methods_stack=((<DictionaryAnalyzer>, 'пила', 444, 8),)),
Parse(word='пила', tag=OpencorporaTag('NOUN,anim,masc,Name sing, gent'), normal_form='пил', score=0.142857, methods_stack=((<DictionaryAnalyzer>, 'пила', 1124, 1),)),
Parse(word='пила', tag=OpencorporaTag('NOUN,anim,masc,Name sing, accs'), normal_form='пил', score=0.142857, methods_stack=((<DictionaryAnalyzer>, 'пила', 1124, 3),))]
Мы видим, что предложенные четыре варианта разбора имеют параметр score, который говорит нам о том, какой вариант предпочтительнее. Pymorphy2 использует статистические методы и ориентируется на данные проекта OpenCorpora, чтобы вычислить значение параметра score. Мы не будем останавливаться на этом подробно. Интересующиеся могут прочитать о внутренней кухне на странице документации по Pymorphy2. Скажем только, что эти вычисления не всегда точны.
Разборы сортируются по убыванию score, поэтому почти везде в примерах берется первый вариант разбора из возможных (например, morph.parse('пила')[0]).
Постановка слов в начальную форму
Нормальную (начальную) форму слова можно получить через атрибуты Parse.normal_form и Parse.normalized. Например, для глаголов в нем будет храниться инфинитив. Таким образом, можно привести любую форму глагола к единому виду.
Но что считается за нормальную форму у других слов? Например, возьмем слово «изучающим». Иногда мы захотим нормализовать его в «изучать», иногда — в «изучающий», иногда — в «изучающая».
Посмотрим на примере, что сделает pymorphy2:
Pymorphy2 сейчас использует алгоритм нахождения нормальной формы, который работает наиболее быстро (берется первая форма в лексеме) — именно поэтому, например, все причастия сейчас нормализуются в инфинитивы. Это можно считать деталью реализации.
Если требуется нормализовать слова иначе, можно воспользоваться методом Parse.inflect():
Согласование с числительными
Помимо разбора слов, библиотека может их изменять — например, сопоставлять с числами. Давайте посмотрим, как можно решить задачу с подсчетом и выводом количества комментариев на форуме:
Интересно, что библиотека пытается работать даже со словами, которых не знает, обращаясь к оракулу:
Задачи
Задача 1
Напишите программу, которая считывает слово со стандартного ввода и выводит его часть речи (атрибут POS тега первого разбора).
Тест 1:
Ввод:
Вывод:Тест 2:
Ввод:
Вывод:Задача 2
Напишите программу, которая считывает слово со стандартного ввода и выводит его начальную форму (normal_form первого разбора).
Тест 1:
Ввод:
Вывод:Тест 2:
Ввод:
Вывод:Задача 3
Напишите программу, которая считывает существительное со стандартного ввода и выводит его в родительном падеже множественного числа.
Тест 1:
Ввод:
Вывод:Тест 2:
Ввод:
Вывод:Задача 4
Напишите функцию agree(n, word), которая принимает число и слово и возвращает строку вида "N слово" с правильно согласованной формой слова.
Тест 1:
Ввод:
Вывод:Тест 2:
Ввод:
Вывод:Задача 5
Напишите программу, которая считывает предложение (несколько слов) со стандартного ввода и для каждого слова выводит его часть речи. Каждое слово на новой строке в формате слово: ЧАСТЬ_РЕЧИ.
Тест 1:
Ввод:
Вывод:Тест 2:
Ввод:
Вывод:Задача 6
Напишите функцию is_noun(word), которая возвращает True, если хотя бы один из разборов слова определяет его как существительное (NOUN), и False иначе.
Тест 1:
Ввод:
Вывод:Тест 2:
Ввод:
Вывод:Задача 7
Напишите программу, которая считывает существительное и его число (целое) со стандартного ввода и выводит правильно согласованную фразу.
Тест 1:
Ввод:
Вывод:Тест 2:
Ввод:
Вывод:Задача 8
Напишите функцию get_gender(word), которая принимает существительное и возвращает его род (masc, femn или neut). Используйте первый разбор слова.
Тест 1:
Ввод:
Вывод:Тест 2:
Ввод:
Вывод:Задача 9
Напишите функцию inflect_to_case(word, case), которая принимает слово и название падежа (nomn, gent, datv, accs, ablt, loct) и возвращает слово в этом падеже. Используйте первый разбор.
Тест 1:
Ввод:
Вывод:Тест 2:
Ввод:
Вывод:Задача 10
Напишите программу, которая считывает текст со стандартного ввода (несколько слов через пробел) и выводит только существительные в начальной форме, по одному на строку. Слова, не являющиеся существительными ни в одном из разборов, пропускаются.
Тест 1:
Ввод:
Вывод:Тест 2:
Ввод:
Вывод: