Справочник

Токенизатор

Токенизатор в Yargy реализован на регулярных выражениях. Для каждого типа токена есть правило с регуляркой:

In [1]:
from yargy.tokenizer import RULES


RULES
Out[1]:
[TokenRule(
     type='RU',
     pattern='[а-яё]+'
 ),
 TokenRule(
     type='LATIN',
     pattern='[a-z]+'
 ),
 TokenRule(
     type='INT',
     pattern='\\d+'
 ),
 TokenRule(
     type='PUNCT',
     pattern='[-\\\\/!#$%&()\\[\\]\\*\\+,\\.:;<=>[email protected]^_`{|}~№…"\\\'«»„“ʼʻ”]'
 ),
 TokenRule(
     type='EOL',
     pattern='[\\n\\r]+'
 ),
 TokenRule(
     type='OTHER',
     pattern='\\S'
 )]

Токенизатор инициализируется списком правил. По-умолчанию — это RULES:

In [2]:
from yargy.tokenizer import Tokenizer


text = '[email protected]'
tokenizer = Tokenizer()
list(tokenizer(text))
Out[2]:
[Token(
     value='a',
     span=[0, 1),
     type='LATIN'
 ),
 Token(
     value='@',
     span=[1, 2),
     type='PUNCT'
 ),
 Token(
     value='mail',
     span=[2, 6),
     type='LATIN'
 ),
 Token(
     value='.',
     span=[6, 7),
     type='PUNCT'
 ),
 Token(
     value='ru',
     span=[7, 9),
     type='LATIN'
 )]

Пользователь может убрать часть правил из списка или добавить новые. Уберём токены с переводами строк:

In [3]:
tokenizer = Tokenizer()

text = '''
abc
123
'''
list(tokenizer(text))
Out[3]:
[Token(
     value='\n',
     span=[0, 1),
     type='EOL'
 ),
 Token(
     value='abc',
     span=[1, 4),
     type='LATIN'
 ),
 Token(
     value='\n',
     span=[4, 5),
     type='EOL'
 ),
 Token(
     value='123',
     span=[5, 8),
     type='INT'
 ),
 Token(
     value='\n',
     span=[8, 9),
     type='EOL'
 )]

Для этого удалим правило EOL:

In [4]:
tokenizer = Tokenizer().remove_types('EOL')

list(tokenizer(text))
Out[4]:
[Token(
     value='abc',
     span=[1, 4),
     type='LATIN'
 ),
 Token(
     value='123',
     span=[5, 8),
     type='INT'
 )]

В Yargy есть примитивные правила для токенизации емейлов и телефонов. По-умолчанию они отключены:

In [5]:
from yargy.tokenizer import EMAIL_RULE, PHONE_RULE


text = 'email: [email protected] call: 8 915 132 54 76'
tokenizer = Tokenizer().add_rules(EMAIL_RULE, PHONE_RULE)
list(tokenizer(text))
Out[5]:
[Token(
     value='email',
     span=[0, 5),
     type='LATIN'
 ),
 Token(
     value=':',
     span=[5, 6),
     type='PUNCT'
 ),
 Token(
     value='[email protected]',
     span=[7, 17),
     type='EMAIL'
 ),
 Token(
     value='call',
     span=[18, 22),
     type='LATIN'
 ),
 Token(
     value=':',
     span=[22, 23),
     type='PUNCT'
 ),
 Token(
     value=' 8 915 132 54 76',
     span=[23, 39),
     type='PHONE'
 )]

Добавим собственное для извлечения доменов:

In [6]:
from yargy.tokenizer import TokenRule


DOMAIN_RULE = TokenRule('DOMAIN', '[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+')

    
text = 'на сайте www.VKontakte.ru'
tokenizer = Tokenizer().add_rules(DOMAIN_RULE)
list(tokenizer(text))
Out[6]:
[Token(
     value='на',
     span=[0, 2),
     type='RU'
 ),
 Token(
     value='сайте',
     span=[3, 8),
     type='RU'
 ),
 Token(
     value='www.VKontakte.ru',
     span=[9, 25),
     type='DOMAIN'
 )]

По умолчанию, Yargy использует не Tokenizer, а MorphTokenizer. Для каждого токена с типом 'RU' он запускает Pymorphy2, добавляет поле forms с морфологией:

In [7]:
from yargy.tokenizer import MorphTokenizer

tokenizer = MorphTokenizer()
list(tokenizer('X век стал'))
Out[7]:
[Token(
     value='X',
     span=[0, 1),
     type='LATIN'
 ),
 MorphToken(
     value='век',
     span=[2, 5),
     type='RU',
     forms=[Form('век', Grams(ADVB)),
      Form('веко', Grams(NOUN,gent,inan,neut,plur)),
      Form('век', Grams(NOUN,inan,masc,nomn,sing)),
      Form('век', Grams(NOUN,accs,inan,masc,sing))]
 ),
 MorphToken(
     value='стал',
     span=[6, 10),
     type='RU',
     forms=[Form('стать', Grams(VERB,indc,intr,masc,past,perf,sing))]
 )]

Газеттир

Словарь профессий, географических объектов можно записывать стандартные средствами через rule, or_, normalized, caseless:

In [8]:
from yargy import rule, or_
from yargy.predicates import normalized, caseless


POSITION = or_(
    rule(normalized('генеральный'), normalized('директор')),
    rule(normalized('бухгалтер'))
)

GEO = or_(
    rule(normalized('Ростов'), '-', caseless('на'), '-', caseless('Дону')),
    rule(normalized('Москва'))
)

Это неудобно, легко ошибиться. Для составления словарей в Yargy используется pipeline. Реализовано два типа газеттиров: morph_pipeline и caseless_pipeline. morph_pipeline перед работой приводит слова к нормальной форме:

In [9]:
from yargy import Parser
from yargy.pipelines import morph_pipeline


TYPE = morph_pipeline(['электронный дневник'])

parser = Parser(TYPE)
text = 'электронным дневником, электронные дневники, электронное дневнику'
for match in parser.findall(text):
    print([_.value for _ in match.tokens])
['электронным', 'дневником']
['электронные', 'дневники']
['электронное', 'дневнику']

caseless_pipeline ищет слова без нормализации. Например, найдём в тексте арабские имена: "Абд Аль-Азиз Бин Мухаммад", "Абд ар-Рахман Наср ас-Са ди":

In [10]:
from yargy.pipelines import caseless_pipeline


NAME = caseless_pipeline([
    'Абд Аль-Азиз Бин Мухаммад',
    'Абд ар-Рахман Наср ас-Са ди'
])
    
parser = Parser(NAME)
text = 'Абд Аль-Азиз Бин Мухаммад, АБД АР-РАХМАН НАСР АС-СА ДИ'
for match in parser.findall(text):
    print([_.value for _ in match.tokens])
['Абд', 'Аль', '-', 'Азиз', 'Бин', 'Мухаммад']
['АБД', 'АР', '-', 'РАХМАН', 'НАСР', 'АС', '-', 'СА', 'ДИ']

Предикаты

In [11]:
from IPython.display import HTML

from yargy.predicates import bank

def html():
    for name in bank.__all__:
        yield f'<h3>{name} <a name="predicates.{name}"><a href="#predicates.{name}">#</a></h3>'
        doc = getattr(bank, name).__doc__.strip()
        yield f'<br/><pre>    {doc}</pre>'

HTML('\n'.join(html()))
Out[11]:

eq #


    a == b

    >>> predicate = eq('1')
    >>> token, = tokenize('1')
    >>> predicate(token)
    True

caseless #


    a.lower() == b.lower()

    >>> predicate = caseless('Рано')
    >>> token, = tokenize('РАНО')
    >>> predicate(token)
    True

in_ #


    a in b

    >>> predicate = in_({'S', 'M', 'L'})
    >>> a, b = tokenize('S 1')
    >>> predicate(a)
    True
    >>> predicate(b)
    False

in_caseless #


    a.lower() in b

    >>> predicate = in_caseless({'S', 'M', 'L'})
    >>> a, b = tokenize('S m')
    >>> predicate(a)
    True
    >>> predicate(b)
    True

gte #


    a >= b

    >>> predicate = gte(4)
    >>> a, b, c = tokenize('3 5 C')
    >>> predicate(a)
    False
    >>> predicate(b)
    True
    >>> predicate(c)
    False

lte #


    a <= b

    >>> predicate = lte(4)
    >>> a, b, c = tokenize('3 5 C')
    >>> predicate(a)
    True
    >>> predicate(b)
    False
    >>> predicate(c)
    False

length_eq #


    len(a) == b

    >>> predicate = length_eq(3)
    >>> a, b = tokenize('XXX 123')
    >>> predicate(a)
    True
    >>> predicate(b)
    True

normalized #


    Нормальная форма слова == value

    >>> a = activate(normalized('сталь'))
    >>> b = activate(normalized('стать'))
    >>> token, = tokenize('стали')
    >>> a(token)
    True
    >>> b(token)
    True

dictionary #


    Нормальная форма слова in value

    >>> predicate = activate(dictionary({'учитель', 'врач'}))
    >>> a, b = tokenize('учителя врачи')
    >>> predicate(a)
    True
    >>> predicate(b)
    True

gram #


    value есть среди граммем слова

    >>> a = activate(gram('NOUN'))
    >>> b = activate(gram('VERB'))
    >>> token, = tokenize('стали')
    >>> a(token)
    True
    >>> b(token)
    True

type #


    Тип токена равен value

    >>> predicate = activate(type('INT'))
    >>> a, b = tokenize('3 раза')
    >>> predicate(a)
    True
    >>> predicate(b)
    False

tag #


    Тег токена равен value

custom #


    function в качестве предиката

    >>> from math import log
    >>> f = lambda x: int(log(int(x), 10)) == 2
    >>> predicate = activate(custom(f, types=INT))
    >>> a, b = tokenize('12 123')
    >>> predicate(a)
    False
    >>> predicate(b)
    True

true #


    Всегда возвращает True

    >>> predicate = true()
    >>> predicate(False)
    True

is_lower #


    str.islower

    >>> predicate = is_lower()
    >>> a, b = tokenize('xxx Xxx')
    >>> predicate(a)
    True
    >>> predicate(b)
    False

is_upper #


    str.isupper

    >>> predicate = is_upper()
    >>> a, b = tokenize('XXX xxx')
    >>> predicate(a)
    True
    >>> predicate(b)
    False

is_title #


    str.istitle

    >>> predicate = is_title()
    >>> a, b = tokenize('XXX Xxx')
    >>> predicate(a)
    False
    >>> predicate(b)
    True

is_capitalized #


    Слово написано с большой буквы

    >>> predicate = is_capitalized()
    >>> a, b, c = tokenize('Xxx XXX xxX')
    >>> predicate(a)
    True
    >>> predicate(b)
    True
    >>> predicate(c)
    False

is_single #


    Слово в единственном числе

    >>> predicate = is_single()
    >>> token, = tokenize('слово')
    >>> predicate(token)
    True

Интерпретация

Объект-результат интерпретации описывает конструктор fact. attribute задаёт значение поля по-умолчанию. Например, в Date по-умолчанию год будет равен 2017:

In [12]:
from IPython.display import display

from yargy import Parser, rule, and_, or_
from yargy.interpretation import fact, attribute
from yargy.predicates import dictionary, gte, lte


Date = fact(
    'Date',
    [attribute('year', 2017), 'month', 'day']
)


MONTHS = {
    'январь',
    'февраль',
    'март',
    'апрель',
    'мая',
    'июнь',
    'июль',
    'август',
    'сентябрь',
    'октябрь',
    'ноябрь',
    'декабрь'
}


MONTH_NAME = dictionary(MONTHS)
DAY = and_(
    gte(1),
    lte(31)
)
YEAR = and_(
    gte(1900),
    lte(2100)
)
DATE = rule(
    DAY.interpretation(
        Date.day
    ),
    MONTH_NAME.interpretation(
        Date.month
    ),
    YEAR.interpretation(
        Date.year
    ).optional()
).interpretation(
    Date
)


text = '''18 июля 2016
15 марта
'''
parser = Parser(DATE)
for line in text.splitlines():
    match = parser.match(line)
    display(match.fact)
Date(
    year='2016',
    month='июля',
    day='18'
)
Date(
    year=2017,
    month='марта',
    day='15'
)

Для дат деревья разбора выглядят просто: вершина-конструктор и несколько детей-атрибутов:

In [13]:
parser = Parser(DATE)
for line in text.splitlines():
    match = parser.match(line)
    display(match.tree.as_dot)
G 0 Date 1 Date.day 0->1 3 Date.month 0->3 5 R3 0->5 2 18 1->2 4 июля 3->4 6 Date.year 5->6 7 2016 6->7
G 0 Date 1 Date.day 0->1 3 Date.month 0->3 2 15 1->2 4 марта 3->4

Как будет себя вести алгоритм интерпретации, когда ребёнок конструктора не атрибут, а другой конструктор? Или когда ребёнок атрибута другой атрибут? Или когда под конструктором или атрибутом не одна, а несколько вершин с токенами? Пойдём от простого к сложному. Когда под вершиной-атрибутом несколько токенов, они объединяются:

In [14]:
from yargy.predicates import eq, type, dictionary


Money = fact(
    'Money',
    ['value', 'currency']
)
MONEY = rule(
    rule(
        type('INT'),
        dictionary({
            'тысяча',
            'миллион'
        })
    ).interpretation(
        Money.value
    ),
    eq('$').interpretation(
        Money.currency
    )
).interpretation(
    Money
)

parser = Parser(MONEY)
match = parser.match('5 тысяч$')
match.tree.as_dot
Out[14]:
G 0 Money 1 Money.value 0->1 4 Money.currency 0->4 2 5 1->2 3 тысяч 1->3 5 $ 4->5

В Money.value два слова:

In [15]:
match.fact
Out[15]:
Money(
    value='5 тысяч',
    currency='$'
)

Когда под вершиной-атрибутом смесь из токенов и вершин-конструктов, интерпретация кидает TypeError:

In [16]:
from yargy.predicates import true


A = fact(
    'A',
    ['x']
)
B = fact(
    'B',
    ['y']
)
RULE = rule(
    true(),
    true().interpretation(
        B.y
    ).interpretation(
        B
    )
).interpretation(
    A.x
).interpretation(
    A
)

parser = Parser(RULE)
match = parser.match('X Y')
match.tree.as_dot
Out[16]:
G 0 A 1 A.x 0->1 2 X 1->2 3 B 1->3 4 B.y 3->4 5 Y 4->5
In [17]:
# match.fact Будет TypeError

Если под вершиной-атрибутом другая вершина-атрибут, нижняя просто исчезает:

In [18]:
from yargy.predicates import true


A = fact(
    'A',
    ['x', 'y']
)
RULE = true().interpretation(
    A.x
).interpretation(
    A.y
).interpretation(A)

parser = Parser(RULE)
match = parser.match('X')
match.tree.as_dot
Out[18]:
G 0 A 1 A.y 0->1 2 A.x 1->2 3 X 2->3

"X" попадёт в A.y, не в A.x:

In [19]:
match.fact
Out[19]:
A(
    x=None,
    y='X'
)

Что если под вершиной-конструктом несколько одинаковых вершин-атрибутов? Самый правый атрибут перезаписывает все остальные:

In [20]:
A = fact(
    'A',
    ['x']
)
RULE = true().interpretation(
    A.x
).repeatable().interpretation(
    A
)

parser = Parser(RULE)
match = parser.match('1 2 3')
match.tree.normalized.as_dot
Out[20]:
G 0 A 1 R1 0->1 2 A.x 1->2 4 R1 1->4 3 1 2->3 5 A.x 4->5 7 R1 4->7 6 2 5->6 8 A.x 7->8 9 3 8->9

В A.x попадёт "3":

In [21]:
match.fact
Out[21]:
A(
    x='3'
)

Но бывает нужно сохранить содержание всех повторяющихся вершин-атрибутов, не только самой правой. Помечаем поле как repeatable:

In [22]:
from yargy import not_


Item = fact(
    'Item',
    [attribute('titles').repeatable()]
)

TITLE = rule(
    '«',
    not_(eq('»')).repeatable(),
    '»'
)
ITEM = rule(
    TITLE.interpretation(
        Item.titles
    ),
    eq(',').optional()
).repeatable().interpretation(
    Item
)

parser = Parser(ITEM)
text = '«Каштанка», «Дядя Ваня»'
match = parser.match(text)
match.tree.as_dot
Out[22]:
G 0 Item 1 R1 0->1 2 R2 1->2 10 R1 1->10 3 Item.titles 2->3 8 R4 2->8 4 « 3->4 5 R5 3->5 7 » 3->7 6 Каштанка 5->6 9 , 8->9 11 Item.titles 10->11 12 « 11->12 13 R5 11->13 17 » 11->17 14 Дядя 13->14 15 R5 13->15 16 Ваня 15->16

«Дядя Ваня» не перезапишет «Каштанка», они оба окажутся в Item.titles:

In [23]:
match.fact
Out[23]:
Item(
    titles=['«Каштанка»',
     '«Дядя Ваня»']
)

Остался последний неочевидный случай, когда ребёнок вершины-конструктора, другая вершина-конструктор. Такая ситуация возникает при использовании рекурсивных грамматик. В примере ребёнок вершины Item другая вершина Item:

In [24]:
from yargy import forward, or_

Item = fact(
    'Item',
    ['title', 'date']
)

ITEM = forward().interpretation(
    Item
)
ITEM.define(or_(
    TITLE.interpretation(
        Item.title
    ),
    rule(ITEM, TITLE),
    rule(
        ITEM,
        DATE.interpretation(
            Item.date
        )
    )
))

parser = Parser(ITEM)
text = '«Каштанка» 18 июня'
match = parser.match(text)
match.tree.as_dot
Out[24]:
G 0 Item 1 R1 0->1 2 Item 1->2 10 Item.date 1->10 3 R1 2->3 4 Item.title 3->4 5 R3 4->5 6 « 5->6 7 R5 5->7 9 » 5->9 8 Каштанка 7->8 11 Date 10->11 12 Date.day 11->12 14 Date.month 11->14 13 18 12->13 15 июня 14->15

В ходе интерпретации появится два объекта: Item(title='«Каштанка»', date=None) и Item(title=None, date=Date('18', 'июня')). В конце произойдёт слияние:

In [25]:
match.fact
Out[25]:
Item(
    title='«Каштанка»',
    date=Date(
        year=2017,
        month='июня',
        day='18'
    )
)

Нормализация

В Yargy реализованы четыре основных метода для нормализации: normalized, inflected, custom и const. normalized возвращает нормальную форму слова, соответствует normal_form в Pymorphy2:

In [26]:
DATE = rule(
    DAY.interpretation(
        Date.day
    ),
    MONTH_NAME.interpretation(
        Date.month
    ),
    YEAR.interpretation(
        Date.year
    )
).interpretation(
    Date
)

parser = Parser(DATE)
match = parser.match('8 июня 2015')
match.fact
Out[26]:
Date(
    year='2015',
    month='июня',
    day='8'
)

С normalized слово "июня" меняется на "июнь":

In [27]:
DATE = rule(
    DAY.interpretation(
        Date.day
    ),
    MONTH_NAME.interpretation(
        Date.month.normalized()
    ),
    YEAR.interpretation(
        Date.year
    )
).interpretation(
    Date
)

parser = Parser(DATE)
match = parser.match('8 июня 2015')
match.fact
Out[27]:
Date(
    year='2015',
    month='июнь',
    day='8'
)

Если в normalized попадает несколько токенов, каждый приводится к нормальной форме без согласования:

In [28]:
from yargy.interpretation import fact
from yargy.predicates import normalized
from IPython.display import display


Geo = fact(
    'Geo',
    ['name']
)

RULE = rule(
    normalized('Красная'),
    normalized('площадь')
).interpretation(
    Geo.name.normalized()
).interpretation(
    Geo
)

parser = Parser(RULE)
for match in parser.findall('на Красной площади'):
    display(match.fact)
Geo(
    name='красный площадь'
)

Особым образом ведёт себя normalized, когда идёт после газеттира. Результат нормализации — ключ газеттира:

In [29]:
from yargy.pipelines import morph_pipeline

RULE = morph_pipeline([
    'красная площадь',
    'первомайская улица'
]).interpretation(
    Geo.name.normalized()
).interpretation(
    Geo
)

parser = Parser(RULE)
for match in parser.findall('c Красной площади на Первомайскую улицу'):
    display(match.fact)
Geo(
    name='красная площадь'
)
Geo(
    name='первомайская улица'
)

inflected склоняет слово, соответствует методу inflect в Pymorphy2:

In [30]:
from yargy.interpretation import fact
from yargy.predicates import gram

Name = fact(
    'Name',
    ['first']
)

NAME = gram('Name').interpretation(
    Name.first.inflected()
).interpretation(
    Name
)

parser = Parser(NAME)
for match in parser.findall('Саше, Маше, Вадиму'):
    display(match.fact)
Name(
    first='саша'
)
Name(
    first='маша'
)
Name(
    first='вадим'
)

inflected принимает набор граммем:

In [31]:
NAME = gram('Name').interpretation(
    Name.first.inflected({'accs', 'plur'})  # винительный падеж, множественное число
).interpretation(
    Name
)

parser = Parser(NAME)
for match in parser.findall('Саша, Маша, Вадим'):
    display(match.fact)
Name(
    first='саш'
)
Name(
    first='маш'
)
Name(
    first='вадимов'
)

custom применяет к слову произвольную функцию:

In [32]:
from yargy.interpretation import fact
from yargy.predicates import type

Float = fact(
    'Float',
    ['value']
)


INT = type('INT')
FLOAT = rule(
    INT,
    '.',
    INT
).interpretation(
    Float.value.custom(float)
).interpretation(
    Float
)

parser = Parser(FLOAT)
match = parser.match('3.1415')
match.fact
Out[32]:
Float(
    value=3.1415
)

custom может применяться вместе с normalized. Тогда слово начала ставится в нормальную форму, потом к нему применяется функция:

In [33]:
MONTHS = {
    'январь': 1,
    'февраль': 2,
    'март': 3,
    'апрель': 4,
    'мая': 5,
    'июнь': 6,
    'июль': 7,
    'август': 8,
    'сентябрь': 9,
    'октябрь': 10,
    'ноябрь': 11,
    'декабрь': 12
}

DATE = rule(
    DAY.interpretation(
        Date.day.custom(int)
    ),
    MONTH_NAME.interpretation(
        Date.month.normalized().custom(MONTHS.__getitem__)
    ),
    YEAR.interpretation(
        Date.year.custom(int)
    )
).interpretation(
    Date
)

parser = Parser(DATE)
match = parser.match('8 июня 2015')
match.fact
Out[33]:
Date(
    year=2015,
    month=6,
    day=8
)

const просто заменяет слово или словосочетания фиксированным значением:

In [34]:
Era = fact(
    'Era',
    ['value']
)

BC = morph_pipeline([
    'до нашей эры',
    'до н.э.'
]).interpretation(
    Era.value.const('BC')
)
AD = morph_pipeline([
    'наша эра',
    'н.э.'
]).interpretation(
    Era.value.const('AD')
)
ERA = or_(
    BC,
    AD
).interpretation(
    Era
)

parser = Parser(ERA)
for match in parser.findall('наша эра, до н.э.'):
    display(match.fact)
Era(
    value='AD'
)
Era(
    value='BC'
)

Согласование

В Yargy реализовано четыре типа согласований: gender_relation — согласование по роду, number_relation — по числу, case_relation — по падежу, gnc_relation — по роду, числу и падежу. Метод match указывает согласование:

In [35]:
from yargy.relations import gnc_relation

Name = fact(
    'Name',
    ['first', 'last']
)

gnc = gnc_relation()

NAME = rule(
    gram('Name').interpretation(
        Name.first.inflected()
    ).match(gnc),
    gram('Surn').interpretation(
        Name.last.inflected()
    ).match(gnc)
).interpretation(
    Name
)

parser = Parser(NAME)
match = parser.match('Сашу Иванову')
display(match.fact)
display(match.tree.as_dot)
Name(
    first='саша',
    last='иванова'
)
G 0 Name 1 Name.first.inflected(sing, nomn)^gnc 0->1 3 Name.last.inflected(sing, nomn)^gnc 0->3 2 Сашу 1->2 1->3 gnc 4 Иванову 3->4

main указывает на главное слово во фразе. По-умолчанию главное слово — самое левое:

In [36]:
from yargy.relations import main

POSITION = rule(
    normalized('главный'),
    main(normalized('бухгалтер'))
)

POSITION.as_dot
Out[36]:
G 0 Rule 1 Production 0->1 2 normalized(value='главный') 1->2 3 normalized(value='бухгалтер') 1->3
In [37]:
from yargy.relations import case_relation

case = case_relation()

PERSON = rule(
    POSITION.match(case),
    NAME.match(case)
)


parser = Parser(PERSON)
assert not parser.match('главного бухгалтер марину игореву')

match = parser.match('главного бухгалтера марину игореву')
match.tree.as_dot
Out[37]:
G 0 R0 1 R1^case 0->1 4 Name^case 0->4 2 главного 1->2 3 бухгалтера 1->3 1->4 case 5 Name.first.inflected(sing, nomn)^gnc 4->5 7 Name.last.inflected(sing, nomn)^gnc 4->7 6 марину 5->6 5->7 gnc 8 игореву 7->8
In [ ]: