Парсинг Сервис

Извлечение фактов с томитапарсер.

Извлечение фактов из текста - типичная задача при работе с естественным языком. Ее постоянно решает Яндекс, например, когда выделяет время и место из полученного письма и предлагает внести событие в календарь. Подобные интересные задачи возникают сплошь и рядом в системах, которые напрямую связаны с естественным языком. Поэтому Яндекс разработали свой морфологический парсер и назвали его Томита. Он позволяет выделять факты в тексте, благодаря написанию своих грамматик и словарей. В этом посте мы разберемся как это работает на примере задачи выделения результатов матчей из футбольных новостей.

Задача

Для знакомства с Томита-парсером я собрал несколько десятков футбольных новостей из яндексовского RSS. Среди них есть как информация об околофутбольных событиях (например, трансферы или интервью со спортсменами), так и новости о результатах матчей. Вот небольшая вырезка из RSS:

Бельгия и Мексика сыграли вничью, Лукаку и Лосано сделали по дублю
Польша и Уругвай сыграли вничью в товарищеском матче - 0:0. В еще одном поединке Бельгия и Мексика также не смогли выявить победителя - 3:3.
11 Nov 2017 00:43:00 +0300
Сборная Сенегала вышла в финальную стадию ЧМ-2018
Сборная Сенегала досрочно обеспечила себе путевку в финальную стадию чемпионата мира по футболу, который пройдет в России летом 2018 года.
10 Nov 2017 22:29:10 +0300

Простым xslt я выделил все описания новостей (поле Description) в отдельный текстовый файл. Наша цель: выделить из всего множества статей только факты, содержащие названия двух команд и счет.

Томита-Парсер

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

В парсере имеется три основных понятия:

Газеттир - это словарь. В нем можно определить набор слов, подходящих под какое-то описание, и использовать их в грамматике.
Грамматика - контекстно-свободная грамматика, в которой определяются правила для сопоставления цепочек с входным текстом.
Факты - то, что мы хотим извлечь. Определяются в грамматике и в случае удачного сопоставления с текстом выводятся как результат.

Эти сущности описываются в разных файлах и мы постепенно будем их создавать. Начнем со словаря.

Газеттир

Для описания словаря нужно создать файл с расширением gzt. Этот файл является обязательным и без него парсер работать не будет (другим таким файлом является конфигурационный, но о нем несколько позже). Во-первых, все файлы, где используется русский текст, необходимо начинать с явного определения кодировки utf8. Далее необходимо импортировать служебные файлы. После того как мы создадим файлы со своими типами и фактами, мы импортируем и их.

encoding ""utf8"";
import ""base.proto"";
import ""articles_base.proto"";

Теперь мы создадим несколько так называемых статей, в которых определим наборы глаголов, описывающих результаты матчей. Для этого введем новый тип статей - result_verb. Поле key определяет какие глаголы входят в статью. Поле lemma позволяет заменить найденную цепочку, на ту, которая указана в этом поле.

result_verb ""победа""
{
key = ""победить"" | ""выиграть"" | ""разгромить"" | ""одолеть"";
lemma = ""победа""
}
result_verb ""поражение""
{
key = ""проиграть"" | ""уступить"";
lemma = ""поражение""
}
result_verb ""ничья""
{
key = ""сыграли вничью"" | ""разошлись миром"" | ""не смогли выявить победителя"";
lemma = ""ничья""
}

Для определения новых типов статей нужно создать новый файл. В нем после служебных импортов с помощью ключевого слова message указываются новые типы. При этом они должны наследоваться от базового типа TAuxDicArticle. Код файла kwtypes_football.proto:

import ""base.proto"";
import ""articles_base.proto"";
message result_verb : TAuxDicArticle {}

После создания файла его нужно импортировать в наш словарь:

import ""kwtypes_my.proto"";

Теперь перейдем к созданию грамматики.

Грамматика
Правила

Создадим еще один файл - football_result.cxx. В нем мы опишем набор правил, используемых для распознавания цепочек. Правила состоят из левой и правой частей, разделенных символом ->. В левой части всегда стоит один нетерминал. В правой - последовательность терминалов и нетерминалов. В роли терминалов выступают либо конкретные леммы, заключенные в кавычки, либо предопределенные парсером ключевые слова. Например: Noun (существительное), Word (любое слово), Punct (точка) и др.

Пометы

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

gnc-arg/gc-arg/c-arg - различные типы согласования между парами не(терминалов). Среди перечисленных: по роду, числу и падежу; по числу и падежу; по падежу, соответственно
wff - использование регулярных выражения в символе
h-reg1 - первая буква слова в верхнем регистре
quoted - символ записан в кавычках (l-quoted - кавычки только в начале, r-quoted - только в конце)
gram - граммемы. Грамматические характеристики, позволяющие проверять слова на соответствие конкретным падежам, лицам, формам, и много чему еще. С полным списком можно ознакомиться здесь.
Операторы

Еще одна ключевая возможность при написании правил заключается в применении операторов к не(терминалам). Оператор | позволяет указывать множественный выбор в правой части правил, * означает, что символ встречается ноль или более раз, а оператор + означает появление символа один или более раз. Теперь нам хватит теории, чтобы описать свою грамматику для распознавания цепочек с результатами матчей.

Реализация

Первоначально создадим правила для распознавания названий команд:

National -> Noun | Noun Noun;
Club -> Noun;
Club -> Word Word* Word;
Team -> Club | National;

Первое правило относится к национальным сборным. В новостях их, как правило, записывают в одном из двух форматов, на примере нашей Родины: либо Россия, либо сборная России. Соответственно, мы ищем слова, являющиеся географическими объектами и пары, которые согласуются по падежу. Второе и третье правило относятся к клубам. Названия клубов пишут в кавычках и с большой буквы, при этом они могут состоять из нескольких слов. Нетерминал Team будем использовать для распознавания всех типов команд. Теперь добавим правило для глагола, из нашего словаря:

Result -> Verb;

Кроме того, нам нужен нетерминал для распознавания счета. Для этого будем использовать регулярное выражение:

Score -> AnyWord;

Нам осталось определить корневой нетерминал и правила, которые соберут вместе те нетерминалы, что мы уже создали. Корневой нетерминал назовем S и выпишем несколько возможных вариантов построения предложения:

S -> Team AnyWord* Result AnyWord* Team AnyWord* Score;
S -> Team AnyWord* Team AnyWord* Result AnyWord* Score;
S -> Team AnyWord* Team AnyWord* Score;

После создания грамматики нужно сослаться на нее в словаре:

TAuxDicArticle ""Результат""
{
key = { ""tomita:football_result.cxx"" type=CUSTOM }
}
Факты

Теперь мы можем находить цепочки слов в тексте, но мы еще не научились выделять из них важную для нас информацию. Для этого нужно создать еще один файл - facttypes.proto. В нем мы определим новый тип фактов ResultFact и унаследуем его от NFactType.TFact. Среди атрибутов факта будут использоваться названия команд, счет и слово, описывающее результат (для демонстрации работы нормализации обнаруженных лемм). Атрибуты могут быть обязательными и опциональными:

import ""base.proto"";
import ""facttypes_base.proto"";
message ResultFact: NFactType.TFact
{
required string FirstTeam = 1;
optional string Result = 2;
required string SecondTeam = 3;
optional string Score = 4;
}

Теперь немного изменим нашу грамматику, добавив в нее интерпретацию фактов. Для этого используется ключевое слово interp. К выводу фактов можно применять граммемы. Воспользуемся этим для вывода результата в именительном падеже, единственном числе:

Result -> Verb interp(ResultFact.Result::norm=""nom,sg"");
Score -> AnyWord interp(ResultFact.Score);
S -> Team interp(ResultFact.FirstTeam) AnyWord* Result AnyWord* Team interp(ResultFact.SecondTeam) AnyWord* Score;
S -> Team interp(ResultFact.FirstTeam) AnyWord* Team interp(ResultFact.SecondTeam) AnyWord* Result AnyWord* Score;
S -> Team interp(ResultFact.FirstTeam) AnyWord* Team interp(ResultFact.SecondTeam) AnyWord* Score;
Файл конфигураций

Без этого файла работа парсера невозможна. Он содержит информацию обо всех необходимых сущностях и передается программе как единственный аргумент. В полях конфига указываются словарь, грамматика и факты. Также можно определить отличные от stdin и stdout вход и выход. С помощью PrettyOutput генерируется html-файл с удобным для чтения форматом вывода совпавших цепочек и обнаруженных фактов.

encoding ""utf8"";
TTextMinerConfig {
Dictionary = ""dic.gzt"";
PrettyOutput = ""results.html"";
Input = {
File = ""articles.txt"";
}
Articles = [
{ Name = ""Результат"" }
]
Facts = [
{ Name = ""ResultFact"" }
]
Output = {
Format = text;
}
}
Результаты

Теперь все готово для запуска парсера. Вот список строк в файле articles.txt, содержащих какую-либо информацию о результатах матчей:

Сборная Бразилии в товарищеском матче победила Японию – 3:1. По голу у южноамериканцев забили Неймар, Марселу и Габриэл Жезус.
Хозяева одержали победу со счётом 2:0. «Трёхцветные» открыли счёт на 18-й минуте, когда точным ударом отметился Антуан Гризманн.
Польша и Уругвай сыграли вничью в товарищеском матче - 0:0. В еще одном поединке Бельгия и Мексика также не смогли выявить победителя - 3:3.
Напомним, 2 ноября «Марсель» на выезде играл против португальской «Виктории Гимарайнш» (0:1). Во время разминки перед матчем в адрес Эвра звучали оскорбления от болельщиков «Марселя».
Сборная России по футболу проиграла команде Аргентины в товарищеском матче (0:1). Единственный гол в матче забил форвард «Манчестер Сити» Серхио Агуэро.
Сегодня футболисты саратовского «Сокола» уступили в Красногорске (Московская область) «Зоркому» 1:5.
Юношеская сборная России (U19) проиграла сверстникам из Румынии со счетом 1:2 в матче 1-го квалификационного раунда Евро-2018.
Глушаков дебютировал в сборной России 29 марта 2011 года в товарищеском матче с Катаром (1:1).

А вот такие факты были обнаружены и отображены в файле results.html:

























































ResultFact
FirstTeamResultSecondTeamScore
сборная БразилиипобедаЯпония3:1
польшаничьяУругвай0:0
Бельгия Мексика3:3
Марсель Виктория0:1
сборная РоссиипоражениеАргентина0:1
СоколпоражениеКрасногорск1:5
сборная РоссиипоражениеРумыния1:2
сборная России Катар1:1

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

Заключение

Извлечение фактов из естественного языка является совсем нетривиальной задачей в мире IT. Томита-парсер предоставляет возможности для элегантного решения этой непростой проблемы. Тем не менее, все равно крайне сложно написать универсальную грамматику, которая разберется во всевозможных вариантах входных последовательностей. Поэтому работа с естественными языками по-прежнему остается достаточно сложной.

Written on
November
11th,
2017
by
Alexey Kalina