История одного небольшого проекта длиною в двенадцать лет (о BIRMA.NET впервые и начистоту из первых рук)

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

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

Спустя достаточно непродолжительное время заработал первый прототип, который я сразу же начал использовал в своей повседневной деятельности, попутно отлаживая его на всех попадающихся мне под руку примерах. К счастью, на моём привычном рабочем месте, где я числился отнюдь не программистом, мне тогда ещё сходили с рук видимые «простои» в работе, на протяжении которых я усиленно занимался отладкой своего детища – практически немыслимое дело в нынешних реалиях, подразумевающих каждодневные отчёты о проделанной за день работе. Процесс шлифовки программы занял в общей сложности ни много ни мало около года, но даже и после этого результат сложно было назвать полностью успешным – слишком уж много там изначально было заложено разных не вполне вразумительных для реализации концепций: необязательные элементы, которые можно пропускать; опережающий просмотр элементов (для целей подстановки к результатам поиска предшествующих элементов); даже собственная попытка реализации чего-то вроде регулярных выражений (имеющего самобытный синтаксис). Надо сказать, что до этого я успел несколько подзабросить программирование (лет так на 8, если не больше), так что новая возможность приложить свои навыки к интересной и нужной задаче всецело овладела моим вниманием. Неудивительно, что полученный в результате исходный код – в отсутствие с моей стороны каких-либо внятных подходов к его проектированию – довольно быстро стал представлять собой невообразимую мешанину разрозненных кусков на языке Си с некоторыми элементами Си++ и аспектами визуального программирования (изначально было решено использовать такую систему проектирования, как Borland C++ Builder – “почти Delphi, но на Си”). Однако всё это в конце концов принесло свои плоды в деле автоматизации повседневной деятельности нашей библиотеки.

Одновременно с этим я решил на всякий случай пройти курсы по подготовке профессиональных разработчиков ПО. Не знаю, можно ли там на самом деле с нуля выучиться «на программиста», но с учётом уже имевшихся у меня на то время навыков мне удалось немного освоить такие уже более актуальные к тому моменту технологии, как C#, Visual Studio для разработки под .NET, а также немного из технологий, относящихся к Java, HTML и SQL. Всё обучение заняло в общей сложности два года, и послужило отправной точкой для ещё одного моего проекта, в конце концов растянувшегося на несколько лет – но это уже тема для отдельной публикации. Здесь же будет уместно отметить лишь то, что я сделал попытку адаптировать уже имевшиеся у меня наработки по описываемому проекту для создания полноценного оконного приложения на C# и WinForms, реализующего нужный функционал, и положить его в основу предстоящего дипломного проекта.
Со временем данная идея начала казаться мне достойной быть озвученной и на таких ежегодных конференциях с участием представителей различных библиотек, как «ЛИБКОМ» и «КРЫМ». Идея – да, но отнюдь не моя тогдашняя её реализация. Тогда ещё я надеялся в том числе и на то, что кто-нибудь перепишет её с применением более грамотных подходов. Так или иначе, к 2013 году я решился составить доклад о своей предварительной работе и прислать его в Оргкомитет конференции с заявкой на получение гранта для участия в конференции. К моему некоторому удивлению, моя заявка была удовлетворена, и я начал вносить некоторые доработки в проект для его подготовки к представлению на конференции.

К тому времени проект уже получил новое имя BIRMA, обрёл разные дополнительные (не столько полноценно реализованные, сколько предполагаемые) возможности – все подробности можно найти в моём докладе.

Признаться честно, BIRMA образца 2013 года сложно было назвать чем-то законченным; откровенно говоря, она представляла собой выполненную на скорую руку весьма халтурную поделку. По кодовой части вообще практически не было никаких особых нововведений, не считая довольно беспомощную попытку создания некого унифицированного синтаксиса для парсера, по своему внешнему виду напоминающему язык форматирования ИРБИС 64 (а по сути, ещё системы ISIS – с круглыми скобочками в роли циклических структур; почему-то тогда мне казалось, что это выглядит весьма круто). Синтаксический анализатор безнадёжно спотыкался на этих круговертях из скобок соответствующего вида (поскольку круглые скобки выполняли там же и ещё одну роль, а именно – ими отмечались необязательные структуры в ходе разбора, которые можно пропустить). Всех желающих ознакомиться поподробнее с тогдашним трудно представимым, ничем не оправданным синтаксисом BIRMA, я опять же отсылаю к своему докладу того времени.

В общем, если не считать борьбы с собственным синтаксическим анализатором, то по части кода этой версии мне больше сказать и нечего – если не считать обратной конвертации имевшихся исходников на Си++ с сохранением некоторых типичных черт .NET-кода (честно говоря, трудно понять, что именно побудило меня перенести всё обратно – наверное, какое-то дурацкое опасение за сохранность в тайне своих исходных кодов, словно это было нечто равноценное секретному рецепту Coca-Cola).

Возможно, в этом дурацком решении кроется также и причина сложностей в сопряжении получившейся DLL-библиотеки с имевшимся интерфейсом самопального АРМа для ввода данных в электронный каталог (да, я же не упомянул ещё об одном немаловажном факте: отныне весь код «движка» BIRMA был, как и положено, отделён от интерфейсной части и упакован в соответствующую DLL). Зачем понадобилось писать ещё и отдельный АРМ для этих целей, который всё равно по своему внешнему виду и способу взаимодействия с пользователем беспардонно копировал тот же АРМ «Каталогизатор» системы ИРБИС 64 – это отдельный вопрос. Если вкратце: он придавал должную солидность моим тогдашним наработкам для дипломного проекта (а то одного лишь неудобоваримого движка парсера было как-то маловато). Кроме того, я тогда столкнулся с некоторыми трудностями при реализации сопряжения АРМ «Каталогизатор» с собственными модулями, реализованными что на C++, что на C#, и обращающимися уже непосредственно к моему движку.

В общем, как это ни странно, но именно этому довольно неуклюжему прототипу будущей BIRMA.NET и суждено было стать моей «рабочей лошадкой» ещё на последующие четыре года. Нельзя сказать, что в течение этого времени я не пытался хотя бы нащупать пути для новой, уже более полноценной реализации давнишней задумки. Там должны были уже присутствовать в числе прочих нововведений и вложенные циклические последовательности, которые могли бы включать в себя в том числе и необязательные элементы – именно так я собирался воплотить в жизнь идею универсальных шаблонов для библиографического описания изданий и разных других интересных вещей. Однако в моей тогдашней практической деятельности всё это было слабо востребовано, а для ввода оглавлений вполне хватало и имеющейся у меня на тот момент реализации. Кроме того, вектор направления развития нашей библиотеки начал всё больше уклоняться в сторону оцифровки музейных архивов, формирования отчётности и прочих малоинтересных для меня занятий, что в конце концов и заставило меня окончательно её покинуть, уступив своё место тем, кому всё это было бы более приятно.

Как ни парадоксально, но именно после этих драматических событий проект BIRMA, на тот момент уже обладавший всеми характерными чертами типичного долгостроя, казалось, начал обретать свою долгожданную новую жизнь! У меня появилось больше свободного времени на досужие размышления, я снова принялся прочёсывать Всемирную сеть в поисках чего-то похожего (благо, теперь я уже мог догадаться искать всё это не где попало, а именно на GitHub‘е), и где-то в начале нынешнего года я наконец натолкнулся на соответствующую поделку небезызвестной конторы Salesforce под малозначащим названием Gorp. Она уже сама по себе могла делать практически всё, что мне было нужно от такого парсерного движка – а именно, по-умному вычленять отдельные фрагменты из произвольного, но обладающего чёткой структурой текста, при этом обладая довольно удобоваримым интерфейсом для конечного пользователя, включающим такие понятные сущности, как паттерн, шаблон и вхождение, и задействующим в то же время привычный синтаксис регулярных выражений, становящийся несравненно более читабельным в силу разбиения на означенные смысловые группы для разбора.

В общем, я решил, что этот самый Gorp (интересно, что сиё название означает? Быть может, какой-нибудь «general oriented regular parser»?) – именно то, что я давно и искал. Правда, его немедленное внедрение для моих собственных нужд имело такую проблему, что данный движок требовал слишком неукоснительно точного соблюдения структурной последовательности исходного текста. Для каких-нибудь отчётов типа log-файлов (а именно они и были помещены разработчиками в качестве наглядных примеров использования проекта) это вполне сгодится, а вот для тех же текстов отсканированных оглавлений – уже вряд ли. Ведь та же самая страничка с оглавлением может начинаться со слова «Оглавление», «Содержание» и ещё каких-либо предваряющих описаний, которые нам совсем не нужно помещать в результаты предполагаемого разбора (а отсекать их каждый раз вручную тоже неудобно). Кроме того, между отдельными повторяющимися элементами, такими как имя автора, заглавие и номер страницы, на странице может содержаться некоторое количество мусора (например, рисунки, да и просто случайные знаки), которое тоже неплохо бы уметь отсекать. Впрочем, последний аспект был ещё не так значителен, а вот в силу первого имеющаяся реализация не могла начать искать нужные структуры в тексте с какого-то определённого места, а вместо этого просто обрабатывала его с самого начала, не находила там указанных шаблонов и… оканчивала свою работу. Очевидно, что требовалась соответствующая доработка, позволившая бы по крайней мере оставлять некоторые промежутки между повторяющимися структурами, и это заставило меня вновь засесть за работу.

Другая проблема заключалась в том, что сам проект был реализован на Java, а я, если и планировал в дальнейшем реализовывать некоторые средства сопряжения данной технологии с привычными приложениями для ввода данных в имеющиеся базы (такими как ирбисовский «Каталогизатор»), то уж по крайней мере делать это на C# и .NET. Не то, чтобы Java сам по себе был плохой язык – когда-то я даже именно на нём реализовал небезынтересное оконное приложение, реализующее функционал отечественного программируемого калькулятора (в рамках курсового проекта). Да и по синтаксису он весьма схож с тем же Си-шарпом. Что ж, это только плюс: тем легче мне будет доработать уже имеющийся проект. Однако мне не хотелось вновь погружаться в этот довольно непривычный мир оконных (вернее, десктопных) Java-технологий – в конце концов, сам язык не был «заточен» на такое использование, а я вовсе не жаждал повторения прежнего опыта. Возможно, именно из-за того, что C# в связке с WinForms намного ближе к Delphi, с которого многие из нас когда-то начинали. К счастью, нужное решение нашлось довольно быстро – в лице проекта IKVM.NET, позволяющего легко перетранслировать имеющиеся программы на Java в управляемый код .NET. Правда, сам проект к тому моменту уже был заброшен авторами, однако его последняя реализация позволила мне вполне успешно проделать нужные действия для исходных текстов Gorp.

Так что я внёс все необходимые правки и скомпоновал это всё в DLL соответствующего вида, которую уже легко могли «подхватить» любые проекты для .NET Framework, создаваемые в Visual Studio. Между делом я создал ещё одну прослойку для удобного представления результатов, возвращаемых Gorp, в виде соответствующих структур данных, которые было бы удобно обрабатывать в табличном представлении (причём беря за основу как строки, так и столбцы; как словарные ключи, так и численные индексы). Ну, а сами нужные утилиты для обработки и отображения полученных результатов написались уже довольно быстро.

Так же особых осложнений не вызвал и процесс адаптации шаблонов для нового движка с целью научить его разбирать имеющиеся образцы отсканированных текстов оглавлений. По сути, мне даже вообще не пришлось обращаться к своим прежним заготовкам: я просто создал все необходимые шаблоны с нуля. Более того, если шаблоны, предназначенные для работы с прежней версией системы, задавали достаточно узкие рамки для текстов, поддающихся правильному разбору с их помощью, то новый движок уже позволял разрабатывать достаточно универсальные шаблоны, подходящие для нескольких типов разметки сразу. Я даже попытался написать некий всеобъемлющий шаблон для любого произвольного текста оглавления, хотя, конечно, даже при всех открывающихся для меня новых возможностях, включающих, в частности, ограниченную возможность реализации всё тех же вложенных повторяющихся последовательностей (таких, как, например, фамилии и инициалы нескольких авторов, идущие подряд), это оказалось утопией.

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

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

Немного подумав, я решил ввести пару служебных паттернов (%all_before) и (%all_after), служащих очевидной цели обеспечения пропусков всего того, что может содержаться в исходном тексте до любого следующего за ними паттерна (маски). При этом если (%all_before) просто игнорировал все эти произвольные включения, то (%all_after), напротив, позволял добавить их к искомому фрагменту после перехода от предыдущего фрагмента. Звучит довольно просто, но вот для реализации данной концепции мне пришлось снова «прочесать» gorp‘овские исходники для внесения нужных модификаций с тем, чтобы при этом не поломать уже реализованную логику. В конце концов это удалось сделать (хотя даже самая-самая первая, пусть и очень глючная реализация моего парсера была написана и то быстрее – за пару недель). Отныне система приняла по-настоящему универсальный вид – спустя ни много ни мало 12 лет после первых попыток заставить её функционировать.

Разумеется, это ещё не предел мечтаний. Можно ещё полностью переписать синтаксический анализатор gorp‘овских шаблонов на C#, используя какую-нибудь из имеющихся библиотек для реализации свободной грамматики. Я думаю, код при этом должен значительно упроститься, и это позволит избавиться от наследия в виде имеющихся исходников на Java. Но и с имеющимся видом движка тоже уже вполне можно делать разные интересные вещи, включая попытку реализации уже упомянутых мной меташаблонов, не говоря о парсинге различных данных со всевозможных веб-сайтов (впрочем, не исключаю, что уже имеющиеся специализированные программные средства подходят для этого больше – просто у меня ещё не было соответствующего опыта их использования).

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

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

Источник: habr.com