Сложно представить задачу более востребованную и частотную, чем задачу текстового поиска. Упростить ее помогают совершенно разные инструменты и методы, однако универсального решения нет. Как один из оптимальных вариантов в статье представлен парсер библиотеки Natasha для поиска почти любой структурированной информации в тексте.

«Контекстно-свободные грамматики» – громоздкое название. Хотя это безобидная и очень полезная штука. Феномен пришел в IT из лингвистики и натворил море чудес – с их помощью можно и��кать в тексте почти все нужные конструкции: адрес, денежную сумму, словосочетания с глаголом «раскидывать», кличку животного – что угодно. Чтобы раскрыть всю мощь КС-грамматик на примере, предлагаю представить, что перед нами поставили задачу извлечь названия организаций из текста.
texts = [ 'Вознаграждение по Договору "00" июля 2019г. ООО ЛампыДок ИНН 0000011111. НДС…', 'Оплата задолженности ООО Чистки-СВ за содержание зданий за период с 09.03.2010г…', 'Оплата ИП Левин К.Д.: предоплата за июнь по сч. №0010011111 от 02.06.2017…', 'Оплата ИП Моргушина Линда Петровна: обслуживание б/к по сч. №00101111 от 01.11…' ]
Вариантов, как выполнить задачу, несколько:
а) Разбить на токены (в данном случае слова и предложные конструкции), вытянуть цепочку по ключевым словам (ООО, ИП и др.);
б) Использовать NER-модули Python-библиотек. В статье рассматривается Natasha;
в) Использовать парсер для поиска заданных цепочек с помощью контекстно-свободных грамматик;
Сравним варианты б и в.
Парсеры на основе КС-грамматик производят поиск фактов по предложениям, используя правила, по которым строятся цепочки.
Цепочка – это последовательность, любая языковая конструкция. Например, «г. Москва, ул. Рязанская, д. 21», «Евгения Лисовская работает переводчиком». В примерах подчеркнуты компоненты формулы (дескрипторы), которые можно задать правилом и производить поиск подобных сочетаний в любом тексте: Name + Surn + «работает» + Noun.
Факт – это структурированная информация – результат анализа цепочек, – обычно представленная статьей:
RFact(text=’Евгения Лисовская работает переводчиком ’ , span={name: ‘Евгения’, surname: ‘Лисовская’, profession: ‘переводчик}’
Рассмотрим процесс извлечения фактов из текста на примере библиотеки Natasha.
Необходимые импорты:
from yargy import Parser, rule, or_, and_ from yargy.interpretation import fact from yargy.predicates import gram, eq, is_capitalized, length_eq from yargy.pipelines import morph_pipeline from slovnet import NER from navec import Navec navec = Navec.load('C:/…/ Ntsh/navec_news_v1_1B_250K_300d_100q.tar') ner = NER.load('C:/… /Ntsh/slovnet_ner_news_v1.tar') ner.navec(navec)
«navec» – эмбеддинги, их можно загрузить здесь.
«slovnet» – модели, скачивать на этой странице.
Первым делом определимся, из каких полей будет состоять факт. Для рассматриваемых текстов достаточно двух: форма организации и ее название. На Pythonфакт реализуется следующим образом:
Orgname = fact( 'Org', ['orgform', 'name'])
Обозначим правило поиска организационных форм для взятых примеров. Так как их ограниченное количество, задаем набор в явном виде:
ORGFORM = morph_pipeline([ 'ООО ', 'ИП '])
morph_pipeline – это газеттир (Слово «газеттир» придумали в «Яндексе», это обозначение формата словарей - .gzt, с которыми работает Томита-парсер) – т.н. корневой словарь, в котором собирается информация обо всех словарях, грамматиках и прочих элементах.
Вторая часть искомой сущности – название. Оно формируется обобщенными правилами:
С предикатом or_ все просто. Он сообщает парсеру: «Далее перечисляются правила, связанные отношением “или”». Наименование компании может быть выражено фамилией, именем или существительным. Так и запишем. У Natasha есть целый набор предикатов, они лежат в справочнике.
Все части наименования собраны. Если оставить так, как есть, парсер просто соберет цепочки, соответствующие правилам. Чтобы на выходе получить факты, нужно включить в код интерпретацию:
ORGANIZATION = rule( ORGFORM.interpretation(Orgname.orgform), NAME.interpretation(Orgname.name).interpretation(Orgname)) #Передаем парсеру объект «организация» и запускаем поиск: orgparser = Parser(ORGANIZATION) def orgs_extract(text, parser): for match in parser.findall(text): found_values = match.tokens return found_values orgs = [orgs_extract(txt, orgparser) for txt in texts]
Вывод для первого текста:


Дополнительная ценность – координаты сущности в тексте, которые могут пригодиться для расширенного анализа документа.
Если нужны только цепочки, изменим цикл в предыдущей функции:
Вывод:

Мы разобрали основы того, как можно извлекать и структурировать любые факты цепочки из текста с помощью библиотеки Natasha. Но если говорить об организациях – это довольно распространенная сущность, запрос на поиск которой неисчерпаем. Поэтому разработчики библиотеки включили их поиск в NER-модуль. И задача с поиском названия сводится к одной функции:
def organizations_mrkup(text): organizations = [] markup = ner(text) spans = markup.spans for i in range(len(spans)): if spans[i].type == 'ORG': org_spans = spans[i] start, stop = org_spans.start, org_spans.stop organizations.append([text[start:stop], (start, stop)]) return organizations
Вывод:

Из достоинств - название компании из второго текста вытянуто точно и полностью (в отличие от ручной настройки). Организации с упоминаниями форм «ООО», «ОАО» модуль видит лучше всего.
Сравним точность подходов по расстояниею Левенштейна:
NER | Parser | |
'ООО ЛампыДок' | 15 | 0 |
'ООО Чистки-СВ' | 0 | 3 |
‘ИП Левин К.Д.’ | 13 | 4 |
‘ИП Моргушина Линда Петровна’’ | 24 | 16 |
Итого | 52 | 23 |
Сравнение проходит на крайне небольшой выборке, при увеличении количества текстов показатели выровняются.
Поиск названий для ИП (с включением полных имен или инициалов) можно настроить в правилах для парсера.
Пример полной настройки:
PT = eq('.') Name = fact( 'Name', ['surname', 'name', 'lastname', 'orgnm', ‘abbr’]) ORGFORM = morph_pipeline([ 'ООО', 'ИП']) SURN = gram('Surn').interpretation( Name.surname) NAME = gram('Name').interpretation( Name.name) ABBR = (is_capitalized()).interpretation( Name.abbr) PATR = (is_capitalized()).interpretation(Name.lastname) INIT = and_(length_eq(1), is_capitalized()) FRST_INIT = INIT.interpretation(Name.name) LST_INIT = INIT.interpretation(Name.lastname) ORGNAME = and_(gram('NOUN'), is_capitalized()).interpretation( Name.orgnm) )
ORGANIZATION = or_( rule(ORGFORM, SURN, FRST_INIT, PT, LST_INIT, PT), rule(ORGFORM, FRST_INIT, PT, LST_INIT, PT, SURN), rule(ORGFORM, ORGNAME, TR, ABBR) rule(ORGFORM, SURN, FRST_INIT, PT), rule(ORGFORM, FRST_INIT, PT, SURN), rule(ORGFORM, NAME, SURN, PATR), rule(ORGFORM, SURN, NAME, PATR), rule(ORGFORM, SURN, FRST_INIT), rule(ORGFORM, FRST_INIT, SURN), rule(ORGFORM, NAME, SURN), rule(ORGFORM, SURN, NAME), rule(ORGFORM, ORGNAME), ).interpretation(Name) ORG = Parser(ORGANIZATION)
Вывод:

Использование контекстно-свободных грамматик в NLP – огромное поле возможностей для работы с текстом. Для некоторых задач подобный подход – overkill, но для массового поиска фактов/цепочек в огромных данных парсер – отличный помощник, который обеспечит сравнимо высокую точность.
