/python-glr-parser

Попытка сделать свой GLR-парсер для русского языка на Python

Primary LanguagePython

GLRParser

Описание в блоге: http://vas3k.ru/blog/358/

Инструмент извлечения структурированных данных (фактов) из текста на естественном (русском) языке на Python 2.x. Построен с использованием любимого всеми морфологического анализатора pymorphy2 и никому неизвестного GLR-парсера jupyLR (был взят как самый простой для переписывания). Чем-то похож на Томита-парсер Яндекса (http://api.yandex.ru/tomita/), хотя чего греха таить, общем-то проект и был начат из-за сложной применимости Томита-парсера простыми смертными в силу особеностей его распространения и небогатой документации. По которой, в прочем, вполне очевидно, что сам парсер был открыт Яндексом лишь частично, а цель помочь простым смертным при этом была далеко не на первом месте (иначе бы Яндекс распространял его не скудным бинарником, а оформил как остальные свои проекты в библиотеку или демон).

Проект практически повторяет открытую функциональность Томита-парсера, однако написан на чистом python и открыт, что позволяет использовать его в реальных проектах и модифицировать. А еще не болен протобафом головного мозга, а использует понятные и простые питонячие структуры данных. Возможно когда-нибудь Яндекс таки дооткроет Томита-парсер и настанут светлые времена, а пока таким вот отщепенцам как я приходится выживать как могут.

Подробнее про работу GLR-парсеров и алгоритма Томиты можно почитать на википедии или нагуглить. http://ru.wikipedia.org/wiki/GLR-%D0%BF%D0%B0%D1%80%D1%81%D0%B5%D1%80

Для примера можно смотреть example.py.

from glr import GLRParser

dictionaries = {
    u"CLOTHES": [u"куртка", u"пальто", u"шубы"]
}

grammar = u"""
    S = adj<agr-gnc=1> CLOTHES
"""

glr = GLRParser(grammar, dictionaries=dictionaries)

text = u"на вешалке висят пять красивых курток и вонючая шуба"
for parsed in glr.parse(text):
    print "FOUND:", parsed

# FOUND: красивых курток
# FOUND: вонючая шуба

Примерное описание работы

  1. Входной текст разбивается на предложения по символам [!?;.]+, а так же очищается от лишних символов. Если такое поведение не устраивает, модифицируйте splitter.py.
  2. Каждое предложение разбивается на токены согласно регулярным выражениям в glr.py (константа DEFAULT_PARSER). Модифицируя этот список, можно вводить свои терминальные символы в грамматику (токены новых типов, например, можно ввести отдельный тип для чисел).
  3. Каждый токен нормализуется и к нему добавляются его морфологические характеристики. Морфологическая неоднозначность снимается... никак. Берется просто первая форма, которую вернул pymorphy2. Если такое поведение не устраивает - милости прошу в normalizer.py и в пулл реквесты.
  4. Дальше стандартный GLR-парсинг входной строки. Если строка не разбирается, то отбрасывается первый токен и все повторяется заново. Все совпадения откладываются и в конце возвращается список.

Синтаксис грамматик

Начальный символ — S (можно менять параметром root в конструкторе GLRParser);

Нетерминал - зарезервированное слово в нижнем регистре (adj, verb, noun, см. начало GLRParser чтобы научиться вводить свои);

Терминал — слово с большой буквы (зарезервированное Word или любое введенное прям в грамматике AsYouWish);

Словари - слово капсом, означающее название словаря (например CITIES), сами словари задаются как { "CITIES": ["val1", "val2", ...]} и передаются в конструктор GLRParser;

Одно слово - слово в 'одинарных' 'кавычках', будет искаться во всех своих формах (например 'шуба') (hint: в грамматике слово нужно указывать в начальной форме, а то не найдется);

Лейблы - список через запятую в <угловых скобочках> после терминала/нетерминала.

S = SomeClothes
SomeClothes = adj 'шуба'
SomeClothes = adj 'валенок'

Словари

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

dictionaries = {
    u"НАЗВАНИЕ_СЛОВАРЯ": [u"значение1", u"значение2", ...]
}

Пример использования есть выше.

Список зарезервированных терминалов и нетерминалов

Символ Значение
Word Любое слово
noun Существительное
adj Прилагательное
verb Глагол
pr Причастие
dpr Деепричастие
num Числительное (или число)
adv Наречие
pnoun Местоимение
prep Предлог
conj Союз
prcl Частица
lat Слово на латинице

Список лейблов

Лейблами можно задавать ограничения или сочетаемость слов. Заметьте, что лейблы могут сильно повлиять на производительность, если применять их к слишком широким по значению (не)терминалам. Не увлекайтесь. Лейблы всегда проверяются только на этапе свертки, если вы понимаете о чем я.

Символ Значение
gram Слово имеет какое-то грамматическое свойство, например определенный падеж. Пример: adj<gram=gent> — прилагательное в родительном падеже.
reg-l-all Все буквы в слове нижнего регистра. Пример: noun<reg-l-all> - существительное, все буквы маленькие.
reg-h-first Первая буква - заглавная. Пример: noun<reg-h-first> - существительное с заглавной буквы.
reg-h-all Все буквы заглавные (капс). Пример аналогичен предыдущим.
agr-gnc Согласование по роду, числу, падежу (gender, number, case). Пример: adj<arg-gnc=1> noun - прилагательное согласуется со следующим за ним существительным по роду, числу, падежу. Обратите внимание, цифра указывает с каким по счету словом от этого сравнивать (возможно и отрицательное значение).
agr-nc По числу и падежу (number, case). Пример аналогичен.
agr-c По падежу (case).
agr-gn По роду и числу (gender, number).
agr-gc По роду и падежу (gender, case).
regex Слово удовлетворяет регулярному выражению. Пример: word<regex=[0-9]+>.

Можно указывать сразу по нескольку лейблов через запятую. Например noun<reg-l-all, gram=nomn> — существительное в именительном падеже, все буквы которого в нижнем регистре.

Если нужно указать несколько значений одного лейбла, это можно делать так: <gram=nomn, gram=masc> (слово мужского рода в именительном падеже).

Список грамматических категорий см. в доке pymorphy2: https://pymorphy2.readthedocs.org/en/latest/user/grammemes.html