Pull to refresh

Gorp.NET – новая библиотека для создания реверсивных шаблонов с целью извлечения данных из структурированного текста

Reading time 10 min
Views 3.1K
Gorp.NET – новая библиотека для создания реверсивных шаблонов с целью извлечения данных из структурированного текста, основанная на имеющейся кодовой базе Salesforce Gorp.

В этой публикации я немного расскажу о способах использования библиотеки для разбора структурированного текста под названием Gorp (одного из примеров средств, которые ещё иногда называют системами построения реверсивных шаблонов).
Что представляет собой реверсивный шаблон в общем виде? Предположим, что у нас есть некая система, позволяющая генерировать нужный нам текст на основе определённых нами исходных данных согласно строгим правилам, задаваемым синтаксисом шаблонов. А теперь представим себе противоположную по смыслу задачу – у нас имеется текст, обладающий некоторой структурной целостностью, которая могла быть достигнута посредством использования системы на основе шаблонов из предыдущего примера. Наша цель – извлечь из этого текста исходные данные, на базе которых он был сформирован. Если мы попробуем придумать для решения данной задачи некий обобщённый синтаксис, подаваемый на вход соответствующему парсеру, разбирающему входной текст на отдельные элементы, то это и будет примером синтаксиса для реализации концепции реверсивных шаблонов.

Почему я решил написать именно о Gorp? Дело в том, что именно эту систему я решил взять за основу для доработки собственного проекта – об истории самого проекта, включая и некоторые детали всех внесённых мной изменений в оригинальный проект Gorp, можно прочитать в предыдущей статье. Здесь же мы сосредоточимся именно на технической части, в том числе и в отношении использования модифицированной версии движка. Для удобства я буду в дальнейшем называть его Gorp.NET, хотя на самом деле это и никакая не портированная на .NET версия Gorp, а лишь немного отшлифованный и доработанный его вариант всё на той же самой Java. Другое дело, что уже надстройка над самой библиотекой Gorp (в моём варианте) в виде управляемой DLL-библиотеки под названием BIRMA.NET задействует свою специальную сборку – ту самую Gorp.NET, которую вы и сами легко сможете получить, если прогоните исходный текст (адрес его репозитория – https://github.com/S-presso/gorp/) через утилиту IKVM.NET.

Сейчас же замечу, что для всевозможных задач по извлечению данных из сколь либо структурированного текста вам вполне достаточно будет уже самих средств Gorp.NET – по крайней мере, если вы немного владеете Java или хотя бы умеете вызывать методы из внешних джавовских модулей в своих проектах на .NET Framework, а также включать туда различные типы из стандартных библиотек JVM (я добивался этого посредством всё того же IKVM.NET, который, правда, сейчас уже имеет статус неподдерживаемого проекта). Ну, а уж что вы будете дальше делать с извлекаемыми данными – это, как говорится, ваше личное дело. Gorp и Gorp.NET сами по себе предоставляют только голый каркас. Некоторый задел для дальнейшей обработки всех подобных данных содержит вышеупомянутая BIRMA.NET. Но само по себе описание функционала BIRMA.NET – это уже тема для отдельной публикации (хотя кое-что я уже успел упомянуть и в своём предыдущем, сравнительно-историческом обзоре технологий BIRMA). Здесь же, забегая вперёд, позволю себе несколько смелое утверждение о том, что используемая в Gorp.NET (и, соответственно, в BIRMA.NET) технология по описанию реверсивных шаблонов является в некотором роде уникальной в числе прочих поделок подобного рода (я говорю «поделок», поскольку крупные компании как-то не были до сих пор мною замечены в продвижении собственных фреймворков для этих целей – ну, разве что сама Salesforce со своей изначальной реализацией Gorp).

Для наиболее полного раскрытия концепции и технических аспектов, лежащих в основе системы описания шаблонов, задействованной в Gorp, я просто оставлю здесь ссылку на оригинальную документацию на английском. Всё, что в ней изложено, вы смело можете применять и по отношению к Gorp.NET. А сейчас немного перескажу самую суть.

Итак, описание шаблона представляет собой некий текстовый документ (возможно, даже представленный в виде одной большой строки, которую можно передать на обработку соответствующему методу). Он состоит из трёх частей, содержащих последовательные описания трёх важнейших сущностей: паттернов, шаблонов и выборок (экстрактов).

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

Насколько я понимаю, изначальной целью, которую ставили перед собой создатели Gorp, заключалась в разборе последовательностей данных, содержащихся в файлах отчётов (или log-файлах). Рассмотрим же простой пример конкретного применения системы.

Допустим, мы имеем отчёт, содержащий следующую строку:

<86>2015-05-12T20:57:53.302858+00:00 10.1.11.141 RealSource: «10.10.5.3»


Составим примерный шаблон для её разбора средствами Gorp:

pattern %phrase \\S+
pattern %num \\d+\n
pattern %ts %phrase
pattern %ip %phrase

extract interm {
template <%num>$eventTimeStamp(%ts) $logAgent(%ip) RealSource: "$logSrcIp(%ip)"
}


Отметим, что здесь даже опущен блок задания шаблонов, поскольку все нужные шаблоны уже включены в итоговую выборку. Все использованные здесь шаблоны – именованные, их содержимое указывается в круглых скобках вслед за их именем. В итоге будет создан набор текстовых данных с именами eventTimeStamp, logAgent и logSrcIp.

Напишем теперь простую программку для извлечения нужных данных. Предположим, что созданный нами шаблон уже содержится в файле с именем extractions.xtr.
import com.salesforce.gorp.DefinitionReader;
import com.salesforce.gorp.ExtractionResult;
import com.salesforce.gorp.Gorp;

// ...

DefinitionReader r = DefinitionReader.reader(new File("extractions.xtr"));
Gorp gorp = r.read();
final String TEST_INPUT = "<86>2015-05-12T20:57:53.302858+00:00 10.1.11.141 RealSource: \"10.10.5.3\"";
ExtractionResult result = gorp.extract(TEST_INPUT);
if (result == null) { // no match, handle
   throw new IllegalArgumentException("no match!");
}
Map<String,Object> properties = asMap();
// and then use extracted property values



Ещё один пример простого шаблона для разбора:

# Patterns
pattern %num \d+
pattern %hostname [a-zA-Z0-9_\-\.]+
pattern %status \w+

# Templates
@endpoint $srcHost(%hostname): $srcPort(%num)

# Extraction
extract HostDefinition {
template @endpoint $status(%status)
}


Что ж, думаю, суть ясна. Также будет не лишним упомянуть о том, что для метода extract существует и определение с двумя входными параметрами, второй из которых имеет логический тип. Если установить его в true, то при своём выполнении метод будет перебирать все потенциальные наборы данных – до тех пор, пока не встретит подходящий (можно также заменить вызов метода на extractSafe – уже без второго параметра). По умолчанию – false, и метод может «ругаться» на несоответствие входных данных используемому шаблону.
Отмечу заодно, что Gorp.NET привнёс в том числе и новую расширенную реализацию метода extract: теперь имеется версия и с двумя последующими параметрами логического типа. Используя сокращённый вызов вида extractAllFound, мы по умолчанию устанавливаем их оба в true. Положительное значение третьего параметра даёт нам ещё бо́льший простор для вариаций: отныне мы можем анализировать и текст с любыми включениями произвольных символов в промежутках между искомыми, уже структурированными выборками (содержащими наборы извлекаемых данных).

Итак, настало время для ответа на вопрос: а что же, собственно, ещё такого уникального может быть заключено в этой модификации базовой версии Gorp, помимо самого расширения метода извлечения данных (extract)?
Дело в том, что, когда я несколько лет назад уже создавал некое подобие собственного инструмента для извлечения требуемых данных из текста (который тоже был основан на обработке неких шаблонов, имеющих свой специфический синтаксис), он работал на несколько иных принципах. Главным их отличием от подхода, реализованного в Gorp и всех производных фреймворках, является то, что каждый подлежащий извлечению текстовый элемент задавался попросту перечислением своих левой и правой границ (каждая из которых в свою очередь могла быть либо частью самого элемента, либо просто отделять его от всего последующего или предыдущего текста). При этом, собственно, в общем случае не проводился разбор структуры самого исходного текста, как это имеет место в Gorp, а всего лишь вычленялись нужные нам кусочки. Что же до того содержимого текста, что заключено между ними, то оно могло вообще не поддаваться какому-либо структурному анализу (это вполне могли быть бессвязные наборы символов).

Можно ли подобного эффекта добиться в Gorp? В исходном его варианте – пожалуй, что нет (поправьте меня, если я заблуждаюсь на сей счёт). Если мы просто напишем выражение вроде (.*), за которым последует уже непосредственно маска для задания левой границы следующего искомого элемента, то в силу применения квантификатора «жадности» будет попросту захвачен весь последующий текст. А регулярки с «нежадным» синтаксисом мы в существующих реализациях Gorp использовать не можем.
Gorp.NET позволяет плавно обойти данную проблему посредством введения двух специального вида паттернов – (%all_before) и (%all_after). Первый из них, собственно, и является альтернативой «нежадной» версии (.*), годной к употреблению при составлении собственных шаблонов. Что же касается (%all_after), то он так же просматривает исходный текст до первого вхождения следующей за ним части описываемого паттерна – но уже опираясь на результат поиска предыдущего паттерна. Всё, что заключено между ними, тоже попадёт в извлекаемую подстроку текущего элемента. В некотором смысле (%all_after) «оглядывается» назад, а (%all_before), напротив, «смотрит» вперёд. Замечу, что своеобразным аналогом для (%all_before) в первой версии BIRMA служила пропущенная левая граница при описании элемента, а аналогом (%all_after) – соответственно, пустота взамен правой границы. Если же при описании очередного элемента не задать обе границы, то парсер очевидным образом захватывал весь последующий текст! Впрочем, вся эта тогдашняя реализация BIRMA сейчас уже имеет чисто историческое значение (немного подробнее о ней вы можете почитать в моём тогдашнем докладе).
Скрытый текст
Исходники нигде и никогда не выкладывались по причине их крайне низкого качества – поистине они могли бы служить памятником плохому проектированию программных систем.


Давайте же рассмотрим особенности применения служебных паттернов (%all_before) и (%all_after) на примере задачи извлечения конкретных пользовательских данных с определённого веб-сайта. Парсить мы будем сайт Amazon, а конкретно – вот эту страничку: https://www.amazon.com/B06-Plus-Bluetooth-Receiver-Streaming/product-reviews/B078J3GTRK/).
Скрытый текст
Пример взят из тестового задания на вакансию разработчика со специализацией в сфере парсинга данных, присланного мною фирмой, которая, к сожалению, так и не откликнулась на предложенное мной решение задачи. Правда, они просили от меня лишь описать общий процесс решения – без приведения конкретного алгоритма, а я в ответ уже тогда попробовал сослаться на Gorp‘овские шаблоны, в то время как мои собственные расширения на тот момент существовали лишь, что называется, «на бумаге».
Ради любопытства позволю себе привести один фрагмент из своего ответного письма, который, по-видимому, является первым упоминанием Gorp.NET, хоть и частного характера.
“Чтобы приведённый список используемых мной для решения данной задачи регулярных выражений выглядел более наглядным, я составил на его основе готовый шаблон (приложил его к письму), который может быть использован для извлечения всех нужных данных посредством применения моей собственной разработки более универсального характера, как раз призванной решать подобный тип задач. Её код основан на проекте github.com/salesforce/gorp, и на той же странице есть общее описание правил составления таких шаблонов. Вообще говоря, такое описание уже само по себе подразумевает задание как конкретных регулярных выражений, так и логики их обработки. Самый сложный момент здесь заключается в том, что для каждой выборки данных мы должны полностью описывать посредством регулярок всю структуру содержащего их текста, а не только сами отдельные элементы (как можно было бы делать при написании собственной программы, осуществляющей поиск последовательно в цикле, как я это ранее описывал).”

Исходная задача заключалась в том, чтобы собрать с вышеприведённой страницы следующие данные:

  • Имя пользователя
  • Оценку
  • Заголовок отзыва
  • Дату
  • Текст


Ну, а теперь просто приведу составленный мной шаблон, который позволяет достаточно быстро и эффективно выполнить такое задание. Думаю, общий смысл должен быть довольно очевиден – возможно, вы сами сможете предложить и более лаконичное решение.
Скрытый текст
Именно на данном примере я, в общем-то, и отлаживал функционал собственных расширений для Gorp (уже без какого-либо прицела на трудоустройство, а скорее уж исходя из идеологии «Proof of Concept»).


pattern %optspace ( *)
pattern %space ( +)

pattern %cap_letter [A-Z]
pattern %small_letter [a-z]
pattern %letter (%cap_letter|%small_letter)
pattern %endofsentence (\.|\?|\!)+
pattern %delim (\.|\?|\!\,|\:|\;)
pattern %delim2 (\(|\)|\'|\")

pattern %word (%letter|\d)+
pattern %ext_word (%delim2)*%word(%delim)*(%delim2)*

pattern %text_phrase %optspace%ext_word(%space%ext_word)+
pattern %skipped_tags <([^>]+)>

pattern %sentence (%text_phrase|%skipped_tags)+(%endofsentence)?

pattern %start <div class=\"a-fixed-right-grid view-point\">

pattern %username_start <div class=\"a-profile-content\"><span class=\"a-profile-name\">
pattern %username [^\s]+
pattern %username_end </span>

pattern %user_mark_start <i data-hook=\"review-star-rating\"([^>]+)><span class=\"a-icon-alt\">
pattern %user_mark [^\s]+
pattern %user_mark_end ([^<]+)</span>

pattern %title_start data-hook=\"review-title\"([^>]+)>(%skipped_tags)*
pattern %title [^<]+
pattern %title_end </span>

pattern %span class <span class=\"[^\"]*\">

pattern %date_start <span data-hook="review-date"([^>]+)>
pattern %date ([^<]+)
pattern %date_end </span>

pattern %content_start <span data-hook=\"review-body\"([^>]+)>(%skipped_tags)*
pattern %content0 (%sentence)+
pattern %content (%all_after)
pattern %content_end </span>

template @extractUsernameStart (%all_before)%username_start
template @extractUsername $username(%username)%username_end
template @extractUserMarkStart (%all_before)%user_mark_start
template @extractUserMark $user_mark(%user_mark)%user_mark_end
template @extractTitleStart (%all_before)%title_start
template @extractTitle $title(%title)%title_end
template @extractDateStart (%all_before)%date_start
template @extractDate $date(%date)%date_end
template @extractContentStart (%all_before)%content_start
template @extractContent $content(%content)%content_end

extract ToCEntry {
template @extractUsernameStart@extractUsername@extractUserMarkStart@extractUserMark@extractTitleStart@extractTitle@extractDateStart@extractDate@extractContentStart@extractContent
}



На сегодня это, пожалуй, всё. О реализованных мною сторонних средствах, в которых уже был полноценно задействован данный фреймворк, я, возможно, расскажу в другой раз.
Tags:
Hubs:
+7
Comments 8
Comments Comments 8

Articles