Skip to content
SergeyShkPublic

About

Быстрый морфологический анализатор русского языка на Rust с API для Python

Topics

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

ruMorph

ruMorph

Russian Morphology - быстрый морфологический анализатор русского языка на Rust с API для Python

Документация · PyPI · crates.io · English

Версия Поддерживаемые версии Python Крейт Сборка Ruff Лицензия DOI


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

Авторы

Лицензия

Код распространяется под лицензией 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}
}

About

Быстрый морфологический анализатор русского языка на Rust с API для Python

Topics

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages