Сложно представить задачу более востребованную и частотную, чем задачу текстового поиска. Упростить ее помогают совершенно разные инструменты и методы, однако универсального решения нет. Как один из оптимальных вариантов в статье представлен парсер библиотеки 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, но для массового поиска фактов/цепочек в огромных данных парсер – отличный помощник, который обеспечит сравнимо высокую точность.

Комментарии (4)


  1. michael108
    20.11.2021 12:32

    У меня жена — библиограф, работает в библиотеке. Периодически их отдел составляет именные указатели. Задача — по списку фамилий с инициалами найти все вхождения этих фамилий в текст, составленный из описаний библиографических ссылок на статьи, материалы конференций, отзывы в разных источниках и т.д. Посмотрев, как жена днями и неделями мучается с этой работой, я написал программу для автоматизации этого процесса. Без конечной проверки человеком обойтись не удалось, но весь процесс теперь занимал несколько часов и сводился в основном к проверке, не пропущена ли какая-то ссылка или фамилия.

    Изначально программа была написана на BlitzBasic, с которым я тогда активно работал (дело было ну очень давно), поэтому недавно я захотел переписать ее на C#. А заодно увеличить уровень автоматизации за счет автоматического составления исходного списка фамилий. Это классический поиск именованных сущностей. Для этого решил попробовать Natasha, которую так хвалят. И сильно разочаровался. Половину фамилий она вообще не находила, а среди того, что находила, было довольно много мусора. Пришлось оставить «человеческий» поиск фамилий в тексте.

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


    1. NewTechAudit Автор
      22.11.2021 12:55

      @michael108 точно не берусь утверждать, что библиотека идеальна, она успешно охватывает определенный круг задач, как и любой другой модуль. Мы рассматриваем настраиваемый поиск с помощью парсера, где можно указать собственные правила, опираясь на закономерности указания имен.

      «Мусор» встречается из-за обобщенных правил, которые указывают, что в тексте может встретиться, например, только фамилия. Для ее поиска в коде «NameExtractor» указывается правило – «слово с заглавной буквы». И парсер найдет всё, что попадает под это указание. Можно ограничить поиск еще одним правилом вроде «оканчивается на –ов/-ова/-ко». Это – пользовательская настройка, которую нужно писать самостоятельно.

      Интерес разработчиков был скорее оставить ложноположительные вхождения, нежели отсечь ложноотрицательные вместе с истинно отрицательными. Сущности вроде имен и фамилий в принципе сложно найти механически, если у них нет статичного окружения. В таких случаях мы обращаемся к ML.

      Не берусь делать однозначные выводы относительно качества работы их модели для NER на разных объемах и структуре текста. Она обучена на новостях и художественной литературе, что охватывает далеко не весь спектр жанров письменной речи.


  1. iforvard
    20.11.2021 23:14

    А если и форм больше (уфк, ано, мп, итд)и записаны они не только сокращённо?


    1. NewTechAudit Автор
      22.11.2021 12:56

      Парсер – механический поиск. Нужно указывать все формы, если они нужны Вам в результате поиска. В том числе, развернутые аббревиатуры.