Important

в примерах используется синтаксис Python 3.

Руководство пользователя

Установка

Для установки воспользуйтесь pip:

pip install pymorphy2

Если вы используете CPython (не PyPy), и в системе есть компилятор и т.д., то можно установить pymorphy2 с дополнительными зависимостями (библиотекой DAWG вместо DAWG-Python), что позволит pymorphy2 работать быстрее:

pip install pymorphy2[fast]

Словари обновляются время от времени; чтоб обновить словари, используйте

pip install -U pymorphy2-dicts

Для установки требуются более-менее современные версии pip и setuptools.

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

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

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

>>> import pymorphy2
>>> morph = pymorphy2.MorphAnalyzer()

Экземпляры класса MorphAnalyzer обычно занимают порядка 10-15Мб оперативной памяти (т.к. загружают в память словари, данные для предсказателя и т.д.); старайтесь организовать свой код так, чтобы создавать экземпляр MorphAnalyzer заранее и работать с этим единственным экземпляром в дальнейшем.

С помощью метода MorphAnalyzer.parse() можно разобрать слово:

>>> morph.parse('стали')
[Parse(word='стали', tag=OpencorporaTag('VERB,perf,intr plur,past,indc'), normal_form='стать', score=0.983766, methods_stack=((<DictionaryAnalyzer>, 'стали', 884, 4),)),
 Parse(word='стали', tag=OpencorporaTag('NOUN,inan,femn sing,gent'), normal_form='сталь', score=0.003246, methods_stack=((<DictionaryAnalyzer>, 'стали', 12, 1),)),
 Parse(word='стали', tag=OpencorporaTag('NOUN,inan,femn sing,datv'), normal_form='сталь', score=0.003246, methods_stack=((<DictionaryAnalyzer>, 'стали', 12, 2),)),
 Parse(word='стали', tag=OpencorporaTag('NOUN,inan,femn sing,loct'), normal_form='сталь', score=0.003246, methods_stack=((<DictionaryAnalyzer>, 'стали', 12, 5),)),
 Parse(word='стали', tag=OpencorporaTag('NOUN,inan,femn plur,nomn'), normal_form='сталь', score=0.003246, methods_stack=((<DictionaryAnalyzer>, 'стали', 12, 6),)),
 Parse(word='стали', tag=OpencorporaTag('NOUN,inan,femn plur,accs'), normal_form='сталь', score=0.003246, methods_stack=((<DictionaryAnalyzer>, 'стали', 12, 9),))]

Note

если используете Python 2.x, то будьте внимательны - юникодные строки пишутся как u'стали'.

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

В приведенном примере слово “стали” может быть разобрано и как глагол (“они стали лучше справляться”), и как существительное (“кислородно-конверторный способ получения стали”). На основе одной лишь информации о том, как слово пишется, понять, какой разбор правильный, нельзя, поэтому анализатор может возвращать несколько вариантов разбора.

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

>>> p = morph.parse('стали')[0]
>>> p.tag
OpencorporaTag('VERB,perf,intr plur,past,indc')

Тег - это набор граммем, характеризующих данное слово. Например, тег 'VERB,perf,intr plur,past,indc' означает, что слово - глагол (VERB) совершенного вида (perf), непереходный (intr), множественного числа (plur), прошедшего времени (past), изъявительного наклонения (indc).

Доступные граммемы описаны тут: Обозначения для граммем.

Кроме того, у каждого разбора есть нормальная форма, которую можно получить, обратившись к атрибутам normal_form или normalized:

>>> p.normal_form
'стать'
>>> p.normalized
Parse(word='стать', tag=OpencorporaTag('INFN,perf,intr'), normal_form='стать', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'стать', 884, 0),))

pymorphy2 умеет разбирать не только словарные слова; для несловарных слов автоматически задействуется предсказатель. Например, попробуем разобрать слово “бутявковедами” - pymorphy2 поймет, что это форма творительного падежа множественного числа существительного “бутявковед”, и что “бутявковед” - одушевленный и мужского рода:

>>> morph.parse('бутявковедами')
[Parse(word='бутявковедами', tag=OpencorporaTag('NOUN,anim,masc plur,ablt'), normal_form='бутявковед', score=1.0, methods_stack=((<FakeDictionary>, 'бутявковедами', 51, 10), (<KnownSuffixAnalyzer>, 'едами')))]

Работа с тегами

Для того, чтоб проверить, есть ли в данном теге отдельная граммема (или все граммемы из указанного множества), используйте оператор in:

>>> p.tag
OpencorporaTag('VERB,perf,intr plur,past,indc')
>>> 'NOUN' in p.tag  # то же самое, что и {'NOUN'} in p.tag
False
>>> 'VERB' in p.tag
True
>>> {'VERB'} in p.tag
True
>>> {'plur', 'past'} in p.tag
True
>>> {'NOUN', 'plur'} in p.tag
False

Note

В Python 2.6 не поддерживается {'NOUN', 'plur'} синтаксис для задания множеств. Если у вас Python 2.6, то тут и дальше в примерах используйте форму записи set(['NOUN', 'plur']).

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

>>> p.tag
OpencorporaTag('VERB,perf,intr plur,past,indc')
>>> p.tag.POS           # Part of Speech, часть речи
'VERB'
>>> p.tag.animacy       # одушевленность
None
>>> p.tag.aspect        # вид: совершенный или несовершенный
'perf'
>>> p.tag.case          # падеж
None
>>> p.tag.gender        # род (мужской, женский, средний)
None
>>> p.tag.involvement   # включенность говорящего в действие
None
>>> p.tag.mood          # наклонение (повелительное, изъявительное)
'indc'
>>> p.tag.number        # число (единственное, множественное)
'plur'
>>> p.tag.person        # лицо (1, 2, 3)
None
>>> p.tag.tense         # время (настоящее, прошедшее, будущее)
'past'
>>> p.tag.transitivity  # переходность (переходный, непереходный)
'intr'
>>> p.tag.voice         # залог (действительный, страдательный)
None

Если запрашиваемая характеристика для данного тега не определена, то возвращается None.

В написании граммем достаточно просто ошибиться; для борьбы с ошибками pymorphy2 выкидывает исключение, если встречает недопустимую граммему:

>>> 'foobar' in p.tag
Traceback (most recent call last):
...
ValueError: Grammeme is unknown: foobar
>>> {'NOUN', 'foo', 'bar'} in p.tag
Traceback (most recent call last):
...
ValueError: Grammemes are unknown: {'bar', 'foo'}

Это работает и для атрибутов:

>>> p.tag.POS == 'plur'
Traceback (most recent call last):
...
ValueError: 'plur' is not a valid grammeme for this attribute.

Русские названия тегов и граммем

Теги и граммемы в pymorphy2 записываются латиницей (например, NOUN). Но часто удобнее использовать кириллические названия граммем (например, СУЩ вместо NOUN). Чтобы получить тег в виде строки, записанной кириллицей, используйте свойство OpencorporaTag.cyr_repr:

>>> p.tag
OpencorporaTag('VERB,perf,intr plur,past,indc')
>>> p.tag.cyr_repr
'ГЛ,сов,неперех мн,прош,изъяв'

Для преобразования произвольных строк с тегами/граммемами между кириллицей и латиницей используйте методы MorphAnalyzer.cyr2lat() и MorphAnalyzer.lat2cyr():

>>> morph.lat2cyr('NOUN,anim,masc plur,ablt')
'СУЩ,од,мр мн,тв'
>>> morph.cyr2lat('СУЩ,од,мр мн,тв')
'NOUN,anim,masc plur,ablt'

Склонение слов

pymorphy2 умеет склонять (ставить в какую-то другую форму) слова. Чтобы просклонять слово, его нужно сначала разобрать - понять, в какой форме оно стоит в настоящий момент и какая у него лексема:

>>> butyavka = morph.parse('бутявка')[0]
>>> butyavka
Parse(word='бутявка', tag=OpencorporaTag('NOUN,inan,femn sing,nomn'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явка', 8, 0), (<UnknownPrefixAnalyzer>, 'бут')))

Для склонения используйте метод Parse.inflect():

>>> butyavka.inflect({'gent'})  # нет кого? (родительный падеж)
Out[13]:
Parse(word='бутявки', tag=OpencorporaTag('NOUN,inan,femn sing,gent'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явки', 8, 1), (<UnknownPrefixAnalyzer>, 'бут')))
>>> butyavka.inflect({'plur', 'gent'})  # кого много?
Parse(word='бутявок', tag=OpencorporaTag('NOUN,inan,femn plur,gent'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явок', 8, 8), (<UnknownPrefixAnalyzer>, 'бут')))

С помощью атрибута Parse.lexeme можно получить лексему слова:

>>> butyavka.lexeme
[Parse(word='бутявка', tag=OpencorporaTag('NOUN,inan,femn sing,nomn'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явка', 8, 0), (<UnknownPrefixAnalyzer>, 'бут'))),
 Parse(word='бутявки', tag=OpencorporaTag('NOUN,inan,femn sing,gent'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явки', 8, 1), (<UnknownPrefixAnalyzer>, 'бут'))),
 Parse(word='бутявке', tag=OpencorporaTag('NOUN,inan,femn sing,datv'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явке', 8, 2), (<UnknownPrefixAnalyzer>, 'бут'))),
 Parse(word='бутявку', tag=OpencorporaTag('NOUN,inan,femn sing,accs'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явку', 8, 3), (<UnknownPrefixAnalyzer>, 'бут'))),
 Parse(word='бутявкой', tag=OpencorporaTag('NOUN,inan,femn sing,ablt'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явкой', 8, 4), (<UnknownPrefixAnalyzer>, 'бут'))),
 Parse(word='бутявкою', tag=OpencorporaTag('NOUN,inan,femn sing,ablt,V-oy'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явкою', 8, 5), (<UnknownPrefixAnalyzer>, 'бут'))),
 Parse(word='бутявке', tag=OpencorporaTag('NOUN,inan,femn sing,loct'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явке', 8, 6), (<UnknownPrefixAnalyzer>, 'бут'))),
 Parse(word='бутявки', tag=OpencorporaTag('NOUN,inan,femn plur,nomn'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явки', 8, 7), (<UnknownPrefixAnalyzer>, 'бут'))),
 Parse(word='бутявок', tag=OpencorporaTag('NOUN,inan,femn plur,gent'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явок', 8, 8), (<UnknownPrefixAnalyzer>, 'бут'))),
 Parse(word='бутявкам', tag=OpencorporaTag('NOUN,inan,femn plur,datv'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явкам', 8, 9), (<UnknownPrefixAnalyzer>, 'бут'))),
 Parse(word='бутявки', tag=OpencorporaTag('NOUN,inan,femn plur,accs'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явки', 8, 10), (<UnknownPrefixAnalyzer>, 'бут'))),
 Parse(word='бутявками', tag=OpencorporaTag('NOUN,inan,femn plur,ablt'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явками', 8, 11), (<UnknownPrefixAnalyzer>, 'бут'))),
 Parse(word='бутявках', tag=OpencorporaTag('NOUN,inan,femn plur,loct'), normal_form='бутявка', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'явках', 8, 12), (<UnknownPrefixAnalyzer>, 'бут')))]

Постановка слов в начальную форму

Как уже было написано, нормальную (начальную) форму слова можно получить через атрибуты Parse.normal_form и Parse.normalized.

Но что считается за нормальную форму? Например, возьмем слово “думающим”. Иногда мы захотим нормализовать его в “думать”, иногда - в “думающий”, иногда - в “думающая”.

Посмотрим, что сделает pymorphy2 в этом примере:

>>> morph.parse('думающему')[0].normal_form
'думать'

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

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

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

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

Слово нужно ставить в разные формы в зависимости от числительного, к которому оно относится. Например: “1 бутявка”, “2 бутявки”, “5 бутявок”

Для этих целей используйте метод Parse.make_agree_with_number():

>>> butyavka = morph.parse('бутявка')[0]
>>> butyavka.make_agree_with_number(1).word
'бутявка'
>>> butyavka.make_agree_with_number(2).word
'бутявки'
>>> butyavka.make_agree_with_number(5).word
'бутявок'

Выбор правильного разбора

pymorphy2 возвращает все допустимые варианты разбора, но на практике обычно нужен только один вариант, правильный.

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

>>> morph.parse('на')
[Parse(word='на', tag=OpencorporaTag('PREP'), normal_form='на', score=0.999628, methods_stack=((<DictionaryAnalyzer>, 'на', 23, 0),)),
 Parse(word='на', tag=OpencorporaTag('INTJ'), normal_form='на', score=0.000318, methods_stack=((<DictionaryAnalyzer>, 'на', 20, 0),)),
 Parse(word='на', tag=OpencorporaTag('PRCL'), normal_form='на', score=5.3e-05, methods_stack=((<DictionaryAnalyzer>, 'на', 21, 0),))]

score - это оценка P(tag|word), оценка вероятности того, что данный разбор правильный.

Условная вероятность P(tag|word) оценивается на основе корпуса OpenCorpora: ищутся все неоднозначные слова со снятой неоднозначностью, для каждого слова считается, сколько раз ему был сопоставлен данный тег, и на основе этих частот вычисляется условная вероятность тега (с исползованием сглаживания Лапласа).

На данный момент оценки P(tag|word) на основе OpenCorpora есть примерно для 15 тыс. слов (исходя из примерно 110тыс. наблюдений). Для тех слов, для которых такой оценки нет, вероятность P(tag|word) либо считается равномерной (для словарных слов), либо оценивается на основе эмпирических правил (для несловарных слов).

На практике это означает, что первый разбор из тех, что возвращают методы MorphAnalyzer.parse() и MorphAnalyzer.tag(), более вероятен, чем остальные. Для слов (без учета пунктуации и т.д.) цифры такие:

  • случайно выбранный разбор (из допустимых) верен примерно в 66% случаев;
  • первый по словарю разбор (pymorphy2 < 0.4) верен примерно в 72% случаев;
  • разбор, который выдает pymorphy2 == 0.4, выбранный на основе оценки P(tag|word), верен примерно в 79% случаев.

Разборы сортируются по убыванию score, поэтому везде в примерах берется первый вариант разбора из возможных (например, morph.parse('бутявка')[0]).

Оценки P(tag|word) помогают улучшить разбор, но их недостаточно для надежного снятия неоднозначности, как минимум по следующим причинам:

  • то, как нужно разбирать слово, зависит от соседних слов; pymorphy2 работает только на уровне отдельных слов;
  • условная вероятность P(tag|word) оценена на основе сбалансированного набора текстов; в специализированных текстах вероятности могут быть другими - например, возможно, что в металлургических текстах P(NOUN|стали) > P(VERB|стали);
  • в OpenCorpora у большинства слов неоднозначность пока не снята; выполняя задания на сайте OpenCorpora, можно непосредственно помочь улучшить оценку P(tag|word) и, следовательно, качество работы pymorphy2.

Если вы берете первый разбор из возможных (как в примерах), то стоит учитывать эту проблему.

Иногда могут помочь какие-то особенности задачи. Например, если нужно просклонять слово, и известно, что на входе ожидается слово в именительном падеже, то лучше брать вариант разбора в именительном падеже, а не первый. В общем же случае для выбора точного разбора необходимо каким-то образом учитывать не только само слово, но и другие слова в предложении.