Russian Morphology - быстрый морфологический анализатор русского языка на Rust с API для Python
Документация · PyPI · crates.io · English
ruMorph - морфологический анализатор русского языка: ядро на Rust и API для Python. Для словоформы анализатор находит по словарю OpenCorpora и словам Викисловаря варианты разбора - начальную форму (лемму), часть речи и признаки Universal Dependencies - упорядочивает их по вероятности и строит формы слова.
Разбор и лемматизация в 6-8 раз быстрее, чем в pymorphy3 с ускорением pymorphy3[fast], форма по признакам и согласование с количеством - в 16-19 раз; списки слов разбираются без GIL. Части речи, признаки и леммы следуют руководству UD, а где оно молчит - соглашениям трибанка SynTagRus, поэтому результат можно сравнивать с разметкой spaCy, Stanza и других инструментов на UD.
- Анализатор - разборы словоформы по убыванию вероятности, разбор списков слов и лемматизация без GIL, проверка словоформы по словарю
- Разбор предложения - выбор разбора по соседним словам контекстной моделью: часть речи, лемма и признаки верны у 82-94 % слов против 71-82 % без контекста; документ сегментатора anySeg размечает
annotate - Части речи и признаки UD - части речи, признаки и леммы по руководству UD и соглашениям SynTagRus; пометы OpenCorpora, у которых нет признака UD
- Токены вне словаря - числа, знаки, латиница, инициалы и сокращения по правилам
- Незнакомые слова - разборы по окончанию, слова через дефис и с приставками, формы угаданных слов
- Словоизменение - формы лексемы, форма по признакам UD, согласование с количеством
- Команда rumorph - разбор файлов и потоков: леммы, CoNLL-U, TSV, JSON Lines и текст с восстановленной «ё»
- spaCy - компонент конвейера spaCy вместо теггера, морфологизатора и лемматизатора
- Переход с pymorphy - соответствие вызовов pymorphy2 и pymorphy3, частей речи и граммем
- Сравнение - точность и скорость в сравнении с pymorphy3, mystem, spaCy, Stanza и Slovnet, загрузка словаря и память
Note
Ранняя версия: до 1.0 API может меняться между версиями 0.x, изменения описываются в заметках к выпускам.
Требуется Python 3.11 или новее.
pip install rumorphИли с помощью uv:
uv add rumorphКолеса собираются для Linux (в том числе Alpine), macOS и Windows, включая Python 3.14t без GIL; Rust для установки не нужен, словарь входит в пакет. На других платформах pip соберет пакет из исходников - для этого нужен Rust 1.88 или новее. Подробнее - на странице Установка.
Для Rust - крейт rumorph со словарем (cargo add rumorph, документация - на docs.rs); команда rumorph без Python ставится через cargo install rumorph --features cli.
>>> from rumorph import Analyzer
>>> morph = Analyzer()
>>> morph.lemmatize("Мы стали друзьями в ковиде".split())
['мы', 'стать', 'друг', 'в', 'ковид']
>>> [(a.lemma, a.pos, a.feats.get("Case"), round(a.prob, 3)) for a in morph.analyze("стали")][:2]
[('стать', 'VERB', None, 0.988), ('сталь', 'NOUN', 'Gen', 0.005)]
>>> morph.analyze("кошка")[0].inflect(Case="Gen", Number="Plur")
'кошек'
>>> morph.analyze("рубль")[0].agree(5)
'рублей'Анализатор достаточно создать один раз: при создании он загружает словарь, дальше им можно пользоваться во всей программе.
Метод analyze возвращает разборы словоформы по убыванию вероятности - объекты Analysis с леммой, частью речи, признаками UD, пометами словаря, вероятностью и источником разбора. Регистр для поиска не важен, но меняет доли имен собственных («шаров» - «шар», «Шаров» - фамилия), буква «е» может обозначать «ё». Разборы неизменяемы: их можно сравнивать, класть в множества и сохранять через pickle.
>>> from rumorph import Analyzer
>>> morph = Analyzer()
>>> morph.analyze("стали")[0]
Analysis(word='стали', lemma='стать', pos='VERB', feats='Aspect=Perf|Mood=Ind|Number=Plur|Tense=Past|VerbForm=Fin|Voice=Act', labels='intr', prob=0.9877829877829878, source='dictionary')
>>> [len(analyses) for analyses in morph.analyze_many(["мама", "мыла", "раму"])]
[1, 4, 2]
>>> morph.is_known("Ежи"), morph.is_known("вайбкодер")
(True, False)Списки слов методы analyze_many и lemmatize разбирают без GIL, поэтому несколько потоков с одним анализатором работают параллельно; в Python 3.14t без GIL параллельно работают и analyze, inflect и agree. Анализатор и разборы сохраняются через pickle и работают с multiprocessing и joblib.
Подробнее - в документации: анализатор, разбор словоформы.
Метод analyze_sentence упорядочивает разборы слов предложения с учетом соседних слов: первым идет разбор, который выбрала контекстная модель, а вероятность - ее оценка в этом предложении. Модель (BiLSTM, 0,9 МБ) обучена на трибанках UD_Russian-Taiga, UD_Russian-GSD и корпусе OpenCorpora и входит в словарь пакета; слова - токены одного предложения.
>>> words = ["Я", "читаю", "книги"]
>>> [analyses[0].feats.get("Case") for analyses in morph.analyze_many(words)]
['Nom', None, 'Gen']
>>> [analyses[0].feats.get("Case") for analyses in morph.analyze_sentence(words)]
['Nom', None, 'Acc']
>>> morph.analyze_sentence(["Мост", "сделан", "из", "стали"])[3][0].lemma
'сталь'Части речи и признаки Universal Dependencies
Части речи и признаки следуют руководству Universal Dependencies, а где оно молчит - соглашениям крупнейшего русского трибанка UD_Russian-SynTagRus. Отличия от частей речи OpenCorpora:
- «который», «этот», «свой», «весь», «такой» и другие местоименные прилагательные -
DET - «быть» и «бы» -
AUX - вводные слова «однако», «например», «конечно» -
ADV - «можно», «надо», «нельзя» -
VERBбез признаков - предлоги -
ADP, сочинительные союзы -CCONJ, подчинительные -SCONJ, частицы -PART
Признаки: падеж, число, род, одушевленность, лицо, время, вид, наклонение, залог, форма глагола, степень, краткая форма, тип имени собственного, сокращение, притяжательность, тип числительного и его запись, тип местоимения, возвратность, несклоняемость, отрицание. Леммы тоже следуют SynTagRus: «Ивановой» - «иванова», «важнейшей» - «важнейший», «денег» - «деньги». Граммемы OpenCorpora, для которых в UD нет признака, - стиль, переходность, второй предложный падеж и другие - лежат в пометах labels.
>>> from rumorph import Analyzer
>>> morph = Analyzer()
>>> words = ["который", "был", "конечно", "можно", "Ивановой", "денег"]
>>> [(analyses[0].lemma, analyses[0].pos) for analyses in morph.analyze_many(words)]
[('который', 'DET'), ('быть', 'AUX'), ('конечно', 'ADV'), ('можно', 'VERB'), ('иванова', 'PROPN'), ('деньги', 'NOUN')]
>>> lesu = morph.analyze("лесу")[0]
>>> lesu.feats["Case"], lesu.labels
('Loc', frozenset({'loc2'}))Подробнее - в документации; соответствие граммем OpenCorpora признакам UD - на странице Переход с pymorphy.
Токены вне словаря
Числа, знаки, латиницу и сокращения анализатор разбирает по правилам, source у таких разборов - "rule":
- знаки препинания -
PUNCT, символы%,$,№и эмодзи -SYM - числа цифрами -
NUM; целое число может быть и порядковымADJ, у годов это первый разбор - числа с окончанием «2005-го», «5-ти» - порядковые
ADJс падежом или количественныеNUM, римские числа - порядковыеADJ - слова латиницей -
XсForeign=Yes - инициалы «А.» - имя, отчество или фамилия с
Abbr=Yes - сокращения с точкой «т.», «им.» - разборы словарного сокращения, аббревиатуры капсом «НКО» -
PROPNилиNOUNсAbbr=Yes - наречия «по-бурятски», «по-молодому» -
ADV - прочие токены «км/ч», «100%-ный» -
Xбез признаков, лемма - токен в нижнем регистре
Доли неоднозначных разборов взяты из частот в трибанках UD Taiga и GSD, доли имен собственных - с учетом регистра: «Земли» - прежде всего планета (PROPN), «земли» - NOUN.
>>> from rumorph import Analyzer
>>> morph = Analyzer()
>>> [(a.lemma, a.pos, a.feats["NumType"], round(a.prob, 2)) for a in morph.analyze("2026")]
[('2026', 'ADJ', 'Ord', 0.98), ('2026', 'NUM', 'Card', 0.02)]
>>> [(a.lemma, a.pos, a.feats["Case"]) for a in morph.analyze("2005-го")][:1]
[('2005-й', 'ADJ', 'Gen')]
>>> [(a.pos, a.feats, a.source) for a in morph.analyze("iPhone")]
[('X', {'Foreign': 'Yes'}, 'rule')]Подробнее - в документации.
Незнакомые слова
Разборы слова кириллицей, которого нет в словаре, анализатор угадывает по окончанию: берет формы словарных слов с тем же окончанием - их часть речи, признаки и словоизменение. source у таких разборов - "guess".
- Слово в нижнем регистре угадывается как нарицательное, слово с заглавной буквы - в основном как имя собственное: фамилия, имя, топоним
- Слово через дефис разбирается по последней части: «крэш-тестов» - «крэш-тест», «Мак-Киннона» - «мак-киннон», а слово с протяжкой или заиканием - по словарному слову без них: «не-е-ет» - «нет», «с-спасибо» - «спасибо»
- Слово с известной приставкой - «эко», «анти», «супер», «микро» и другими - разбирается по словарному слову без нее: «экодомов» - «экодом»
- У угаданного разбора есть формы, и они сохраняют заглавные буквы: «Мак-Киннона» в дательном падеже - «Мак-Киннону»
- Слово, которое нужно разбирать точно, можно добавить по образцу - словарному слову, которое изменяется так же:
Analyzer(words={"вайбкодер": "геймер"})
>>> from rumorph import Analyzer
>>> morph = Analyzer()
>>> [(a.lemma, a.pos, a.feats["Case"], a.source) for a in morph.analyze("дипфейкеру")][:1]
[('дипфейкер', 'NOUN', 'Dat', 'guess')]
>>> guess = morph.analyze("Мартова")[0]
>>> guess.lemma, guess.pos, guess.feats["NameType"], guess.inflect(Case="Dat")
('мартов', 'PROPN', 'Sur', 'Мартову')
>>> morph.analyze("крэш-тестов")[0].lemma, morph.analyze("экодомов")[0].lemma
('крэш-тест', 'экодом')
>>> coders = Analyzer(words={"вайбкодер": "геймер"})
>>> coders.analyze("вайбкодерами")[0].lemma, coders.is_known("вайбкодеры")
('вайбкодер', True)Подробнее - в документации.
Словоизменение
Каждый разбор знает свою лексему - все формы слова с той же леммой:
forms- формы лексемы в порядке словаряinflect- форма с заданными признаками UD; признаки, которые не названы, по возможности сохраняютсяagree- форма существительного, прилагательного или причастия, согласованная с количеством: «1 рубль», «2 рубля», «5 рублей», «2 суток»
Если у лексемы нет нужной формы, inflect и agree бросают FormNotFoundError, а на опечатку в признаке inflect бросает ValueError со списком допустимых значений.
>>> from rumorph import Analyzer
>>> morph = Analyzer()
>>> cat = morph.analyze("кошка")[0]
>>> [form.word for form in cat.forms()][:5]
['кошка', 'кошки', 'кошке', 'кошку', 'кошкой']
>>> cat.inflect(Case="Gen"), cat.inflect(Case="Gen", Number="Plur")
('кошки', 'кошек')
>>> verb = morph.analyze("читал")[0]
>>> verb.inflect(Gender="Fem"), verb.inflect(Tense="Pres", Person="3", Number="Plur")
('читала', 'читают')
>>> ruble = morph.analyze("рубль")[0]
>>> [f"{n} {ruble.agree(n)}" for n in (1, 2, 5, 21)]
['1 рубль', '2 рубля', '5 рублей', '21 рубль']Подробнее - в документации.
Команда rumorph
Команда ставится вместе с пакетом и разбирает текст из файлов или стандартного ввода: леммы, CoNLL-U, TSV, JSON Lines со всеми разборами или исходный текст с «ё», восстановленной по выбранным разборам. На входе - текст (абзацы до пустой строки, токены и предложения находит сегментатор anySeg), токены по одному в строке или CoNLL-U, в котором заполняются LEMMA, UPOS и FEATS.
$ echo "Мы стали друзьями в ковиде." | rumorph
мы стать друг в ковид .
$ rumorph --format conllu text.txt > text.conlluПодробнее - в документации: параметры, форматы и коды выхода.
spaCy
Компонент rumorph (pip install "rumorph[spacy]") размечает токены конвейера spaCy частью речи, признаками UD и леммой по разбору с контекстом. В моделях ru_core_news_* он заменяет морфологизатор и лемматизатор: на SynTagRus test часть речи, лемма и признаки вместе верны у 93,7 % слов против 82,0 % у ru_core_news_sm, а конвейер работает быстрее.
import spacy
nlp = spacy.load("ru_core_news_sm", exclude=["morphologizer", "lemmatizer"])
nlp.add_pipe("rumorph", after="parser")
doc = nlp("Мост сделан из стали.")Подробнее - в документации: предложения, параметры и разбор ruMorph токена.
Переход с pymorphy
ruMorph работает с тем же словарем OpenCorpora, что pymorphy2 и pymorphy3, дополненным словами Викисловаря, но API у него свое: части речи и признаки UD вместо граммем OpenCorpora, вероятность вместо score, словоформы строкой, исключения вместо None. Здесь morph - анализатор, p - разбор pymorphy, a - разбор ruMorph:
| pymorphy2, pymorphy3 | ruMorph |
|---|---|
MorphAnalyzer() |
Analyzer() |
morph.parse(word) |
morph.analyze(word) |
[morph.parse(w)[0].normal_form for w in words] |
morph.lemmatize(words) |
morph.word_is_known(word) |
morph.is_known(word) |
p.normal_form, p.tag.POS, p.score |
a.lemma, a.pos, a.prob |
p.tag.case, p.tag.number |
a.feats.get("Case"), a.feats.get("Number") |
p.lexeme |
a.forms() |
p.inflect({"gent", "plur"}).word |
a.inflect(Case="Gen", Number="Plur") |
p.make_agree_with_number(n).word |
a.agree(n) |
Подробнее - в документации: все вызовы, части речи и граммемы.
Сравнение
Доля слов, у которых верны часть речи и лемма первого разбора, % (тестовые части трибанков UD 2.18):
| Система | SynTagRus | Taiga | GSD | Токенов в секунду |
|---|---|---|---|---|
| ruMorph | 94,1 | 90,8 | 92,1 | 650 493 |
| pymorphy3 | 84,8 | 82,9 | 84,3 | 22 815 |
| mystem | 81,9 | 79,4 | 79,9 | 79 730 |
С контекстом (analyze_sentence) ruMorph по части речи, лемме и признакам вместе точнее всех теггеров на Taiga и GSD (90,4 и 81,7 % слов против 85,6 и 80,3 % у Stanza), а на SynTagRus идет вровень со Stanza (93,7 и 93,5 %), хотя Stanza обучена на SynTagRus. При этом он разбирает 107 тыс. токенов в секунду против 2 921 у Stanza. На короткий текст вместе с загрузкой ruMorph отвечает за 6 мс против 0,2-2 с у теггеров, процессы делят словарь, и каждый следующий добавляет 1,5 МБ. Замер - Apple M3 Pro, Python 3.11.
Подробнее - в документации: теггеры с контекстом, график скорости от объема текста, операции в сравнении с pymorphy3, память и как это измерено.
Проект использует uv для управления зависимостями, maturin для сборки модуля на Rust и ruff для линтинга и форматирования. Нужен Rust 1.88 или новее.
git clone https://github.com/SergeyShk/ruMorph.git
cd ruMorph
make deps # создать окружение, установить зависимости и собрать модуль на Rust
make dictionary # собрать словарь из OpenCorpora, Викисловаря и трибанков UD
make test # запустить тесты Python, примеры из докстрингов (doctest) и тесты Rust
make lint # ruff, mypy, rustfmt и clippyПолный список команд - make help. Документация двуязычная: русские страницы docs/*.md, английские - docs/*.en.md рядом с ними; при правке страницы обновляйте обе версии.
Перед отправкой изменений стоит установить хуки, которые прогонят линтеры на коммите и тесты на пуше:
uv run pre-commit installБаг-репорты, идеи и пул-реквесты приветствуются - issues открыты. Как устроен процесс и что проверить перед отправкой пул-реквеста - в CONTRIBUTING.md; правила общения - в кодексе поведения.
Структура проекта
- crates/rumorph - ядро анализатора на Rust: формат словаря, сборка, разбор, контекстная модель и словоизменение
- crates/rumorph-build - сборка словаря из выгрузки OpenCorpora и Викисловаря, перевод тегов OpenCorpora в UD и частоты разборов по трибанкам UD
- crates/rumorph-dict-ru - крейт со словарем для
rumorphна crates.io - crates/rumorph-py - модуль Python
rumorph._rumorphна PyO3 - python/rumorph - пакет Python
- scripts - выгрузка словаря и корпуса OpenCorpora, перевод слов Викисловаря, выгрузка счетчиков трибанков UD, обучение контекстной модели, проверка установленного пакета перед публикацией и сравнение с другими анализаторами
- data - веса контекстной модели, которые сборщик вшивает в словарь
- docs - документация MkDocs на русском и английском
- tests - тесты пакета Python
- Шкарин Сергей (kouki.sergey@gmail.com)
Код распространяется под лицензией MIT. Словарь в пакете собран из словаря OpenCorpora с частотами и контекстной моделью по трибанкам UD_Russian-Taiga и UD_Russian-GSD и корпусу OpenCorpora и распространяется по лицензии CC BY-SA 4.0 (LICENSE-DICTIONARY.txt).
Если вы используете ruMorph в исследованиях или программах, пожалуйста, цитируйте библиотеку по следующей записи BibTeX - это помогает развитию и поддержке проекта. Те же метаданные лежат в CITATION.cff - GitHub показывает их по кнопке «Cite this repository». Concept DOI 10.5281/zenodo.23520738 на Zenodo указывает на все версии библиотеки; DOI конкретной версии - на странице релиза.
@software{ruMorph,
author = {Sergey Shkarin},
title = {{ruMorph, a fast morphological analyzer for Russian}},
year = 2026,
doi = {10.5281/zenodo.23520738},
url = {https://github.com/SergeyShk/ruMorph}
}