Перейти к содержанию

Морфология

Библиотека pymorphy2

Вы уже знаете стандартные строковые функции и пользовались ими. Мы умеем работать со строками посимвольно и знаем, как представляется текстовая информация. Давайте перейдем на уровень выше. Строка и текст в общем случае состоят не из набора букв, а из слов, и иногда нужно работать именно со словами, а не просто с последовательностями байтов.

Возьмем для примера склонение существительных с числительными. Например, на форуме надо написать, что в теме «21 комментарий», но «24 комментария». То же самое нужно делать и для других слов: например, «новость», «пользователь». Иногда на сайтах обходят эту проблему и вставляют машинное «комментариев: 21».

Довольно много статей на эту тему. Например, эта.

Давайте посмотрим, какие средства работы со словами есть в Python, и познакомимся с библиотекой pymorphy2 (морфология).

Если эта библиотека отсутствует в вашем Python, ее надо установить с помощью утилиты pip.

pip install pymorphy2

Словари распространяются отдельными пакетами. Для русского языка используется pymorphy2-dicts-ru.

Они обновляются время от времени, для обновления используйте команду

pip install -U pymorphy2-dicts-ru

Морфологический анализ

В pymorphy2 для морфологического анализа слов есть класс MorphAnalyzer.

Морфологический анализ — определение характеристик слова на основе того, как оно пишется. При морфологическом анализе не учитываются соседние слова.

Для любого слова библиотека делает несколько предположений, что оно может означать, и обозначает свою уверенность в этом предположении.

Метод MorphAnalyzer.parse() возвращает один или несколько объектов типа Parse с информацией о том, как слово может быть разобрано.

1
2
3
import pymorphy2
morph = pymorphy2.MorphAnalyzer()
morph.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, именем собственным, одушевленным, мужского рода. Конечно, предположений может быть несколько:

1
2
3
4
5
6
7
import pymorphy2
import pprint

morph = pymorphy2.MorphAnalyzer()
res = morph.parse('пила')
print(len(res))
pprint.pprint(res)
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),))]

У каждого разбора есть тег:

p = morph.parse('пила')[1]
print(p)
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:

'NOUN' in p.tag # => False

Пример:

import pymorphy2

morph = pymorphy2.MorphAnalyzer()
res = morph.parse('питона')[1]
# В нулевом предположении будет родительный падеж - он чаще
# встречается проверка на глагол
print("VERB" in res.tag)
# проверка на винительный падеж
print("accs" in res.tag)
# cуществительное в винительном падеже?
print({"NOUN", 'accs'} in res.tag)
False
True
True

Кроме того, у каждого тега есть атрибуты, через которые можно получить часть речи, число и другие характеристики:

Параметр Описание
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.

1
2
3
4
5
6
7
8
import pymorphy2

morph = pymorphy2.MorphAnalyzer()
res = morph.parse('писал')[0]
print(res.tag.mood)
print(res.tag.tense)
# попытка доступа к граммеме, которой нет для этой части речи
print(res.tag.case)
indc
past
None

Итак, мы можем разбирать большие тексты на части и узнавать информацию о словах: например, искать глаголы, подсчитывать имена и т. д.

Для изменения слов (к примеру, склонения существительных) можно использовать метод inflect:

word = morph.parse('случай')[0]
print(word.inflect({'gent'}))
Parse(
    word='случая', tag=OpencorporaTag('NOUN,inan,masc sing,gent'),
    normal_form='случай', score=1.0,
    methods_stack=((<DictionaryAnalyzer>, 'случая', 175, 1),)
)

И во множественном числе:

print(word.inflect({'gent', 'plur'}))
Parse(word='случаев', tag=OpencorporaTag('NOUN,inan,masc plur,gent'), normal_form='случай', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'случаев', 175, 7),))

Метод inflect работает и с другими частями речи — например, глаголами:

word = morph.parse('программировать')[0]
print(word.inflect({'VERB', 'impf', 'masc', 'sing', 'past', 'indc'}))
Parse(word='программировал', tag=OpencorporaTag('VERB,impf,tran masc, sing,past,indc'),normal_form='программировать', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'программировал', 168, 7),))

Вернемся к параметру score.

morph.parse('пила')
[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:

morph.parse('изучающим')[0].normal_form  # => 'изучать'

Pymorphy2 сейчас использует алгоритм нахождения нормальной формы, который работает наиболее быстро (берется первая форма в лексеме) — именно поэтому, например, все причастия сейчас нормализуются в инфинитивы. Это можно считать деталью реализации.

Если требуется нормализовать слова иначе, можно воспользоваться методом Parse.inflect():

morph.parse('изучающим')[0].inflect({'sing', 'nomn'}).word  # => 'изучающий'

Согласование с числительными

Помимо разбора слов, библиотека может их изменять — например, сопоставлять с числами. Давайте посмотрим, как можно решить задачу с подсчетом и выводом количества комментариев на форуме:

1
2
3
4
comment = morph.parse('комментарий')[0]
comment.make_agree_with_number(1).word # => 'комментарий'
comment.make_agree_with_number(2).word # => 'комментария'
comment.make_agree_with_number(7).word # => 'комментариев'

Интересно, что библиотека пытается работать даже со словами, которых не знает, обращаясь к оракулу:

1
2
3
word = morph.parse('Мегатрон')[0]
word.make_agree_with_number(7).word # => 'мегатронов'
word.make_agree_with_number(2).word # => 'мегатрона'

Задачи

Задача 1

Напишите программу, которая считывает слово со стандартного ввода и выводит его часть речи (атрибут POS тега первого разбора).

Тест 1:

Ввод:

кот
Вывод:
NOUN

Тест 2:

Ввод:

бежать
Вывод:
INFN


Задача 2

Напишите программу, которая считывает слово со стандартного ввода и выводит его начальную форму (normal_form первого разбора).

Тест 1:

Ввод:

котов
Вывод:
кот

Тест 2:

Ввод:

бежавшими
Вывод:
бежать


Задача 3

Напишите программу, которая считывает существительное со стандартного ввода и выводит его в родительном падеже множественного числа.

Тест 1:

Ввод:

кот
Вывод:
котов

Тест 2:

Ввод:

стол
Вывод:
столов


Задача 4

Напишите функцию agree(n, word), которая принимает число и слово и возвращает строку вида "N слово" с правильно согласованной формой слова.

Тест 1:

Ввод:

agree(1, 'яблоко')
Вывод:
1 яблоко

Тест 2:

Ввод:

agree(5, 'яблоко')
Вывод:
5 яблок


Задача 5

Напишите программу, которая считывает предложение (несколько слов) со стандартного ввода и для каждого слова выводит его часть речи. Каждое слово на новой строке в формате слово: ЧАСТЬ_РЕЧИ.

Тест 1:

Ввод:

кот сидит на диване
Вывод:
кот: NOUN
сидит: VERB
на: PREP
диване: NOUN

Тест 2:

Ввод:

быстро бежать
Вывод:
быстро: ADVB
бежать: INFN


Задача 6

Напишите функцию is_noun(word), которая возвращает True, если хотя бы один из разборов слова определяет его как существительное (NOUN), и False иначе.

Тест 1:

Ввод:

is_noun('пила')
Вывод:
True

Тест 2:

Ввод:

is_noun('бежать')
Вывод:
False


Задача 7

Напишите программу, которая считывает существительное и его число (целое) со стандартного ввода и выводит правильно согласованную фразу.

Тест 1:

Ввод:

комментарий
21
Вывод:
21 комментарий

Тест 2:

Ввод:

комментарий
11
Вывод:
11 комментариев


Задача 8

Напишите функцию get_gender(word), которая принимает существительное и возвращает его род (masc, femn или neut). Используйте первый разбор слова.

Тест 1:

Ввод:

get_gender('стол')
Вывод:
masc

Тест 2:

Ввод:

get_gender('книга')
Вывод:
femn


Задача 9

Напишите функцию inflect_to_case(word, case), которая принимает слово и название падежа (nomn, gent, datv, accs, ablt, loct) и возвращает слово в этом падеже. Используйте первый разбор.

Тест 1:

Ввод:

inflect_to_case('программа', 'datv')
Вывод:
программе

Тест 2:

Ввод:

inflect_to_case('компьютер', 'ablt')
Вывод:
компьютером


Задача 10

Напишите программу, которая считывает текст со стандартного ввода (несколько слов через пробел) и выводит только существительные в начальной форме, по одному на строку. Слова, не являющиеся существительными ни в одном из разборов, пропускаются.

Тест 1:

Ввод:

быстрый кот бежит через широкое поле
Вывод:
кот
поле

Тест 2:

Ввод:

программирование на питоне это интересно
Вывод:
программирование
питон