Historia pewnego małego projektu, który trwał dwanaście lat (o BIRMA.NET po raz pierwszy i szczerze z pierwszej ręki)

Narodziny tego projektu można uznać za mały pomysł, który przyszedł mi do głowy gdzieś pod koniec 2007 roku, który miał znaleźć swój ostateczny kształt dopiero 12 lat później (w tym momencie – oczywiście, choć obecna realizacja, zdaniem dla autora jest bardzo zadowalający).

Wszystko zaczęło się od tego, że podczas wykonywania swoich ówczesnych obowiązków służbowych w bibliotece zwróciłem uwagę na fakt, że proces wprowadzania danych ze zeskanowanych tekstów spisów treści publikacji książkowych (i muzycznych) do istniejącej bazy danych, pozornie można znacznie uprościć i zautomatyzować, wykorzystując właściwość uporządkowania i powtarzalności wszystkich danych wymaganych do wprowadzenia, takich jak imię i nazwisko autora artykułu (jeśli mówimy o zbiorze artykułów), tytuł artykuł (lub podtytuł odzwierciedlony w spisie treści) i numer strony aktualnej pozycji spisu treści. Na początku byłem praktycznie przekonany, że system odpowiedni do realizacji tego zadania można łatwo znaleźć w Internecie. Kiedy pewne zdziwienie wywołał fakt, że nie mogłem znaleźć takiego projektu, postanowiłem spróbować go wdrożyć samodzielnie.

Po dość krótkim czasie zaczął działać pierwszy prototyp, który od razu zacząłem wykorzystywać w swoich codziennych działaniach, jednocześnie debugując go na wszystkich egzemplarzach, które wpadły mi w ręce. Na szczęście w moim zwykłym miejscu pracy, gdzie bynajmniej nie byłem programistą, uchodziły mi wtedy widoczne „przestoje” w pracy, podczas których intensywnie debugowałem swój pomysł – co w obecnych realiach jest wręcz nie do pomyślenia, co implikuje codzienne raporty z pracy wykonanej w ciągu dnia. Proces doszlifowania programu trwał w sumie nie mniej niż około rok, ale nawet po tym czasie trudno uznać wynik za w pełni udany – początkowo sformułowano zbyt wiele różnych koncepcji, które nie były do ​​końca jasne do wdrożenia: elementy opcjonalne, które można zostać pominięty; przeglądanie elementów w przód (w celu podstawienia poprzednich elementów w wynikach wyszukiwania); nawet nasza własna próba zaimplementowania czegoś w rodzaju wyrażeń regularnych (które mają unikalną składnię). Muszę powiedzieć, że wcześniej trochę odpuściłem programowanie (na około 8 lat, jeśli nie dłużej), więc nowa możliwość wykorzystania moich umiejętności do ciekawego i potrzebnego zadania całkowicie przykuła moją uwagę. Nie jest zaskakujące, że powstały kod źródłowy - wobec braku jasnego podejścia do jego projektowania z mojej strony - dość szybko stał się niewyobrażalną mieszaniną odrębnych elementów w języku C z pewnymi elementami C++ i aspektami programowania wizualnego (początkowo zdecydowano się zastosować taki system projektowania jak Borland C++ Builder – „prawie Delphi, ale w C”). Wszystko to jednak ostatecznie zaowocowało automatyzacją codziennych czynności naszej biblioteki.

Jednocześnie zdecydowałem się, na wszelki wypadek, na kursy kształcące profesjonalnych programistów. Nie wiem, czy w ogóle da się tam nauczyć „być programistą” od zera, ale biorąc pod uwagę umiejętności, które wówczas posiadałem, udało mi się w pewnym stopniu opanować technologie, które były wówczas bardziej istotne, jak np. jak C#, Visual Studio do programowania pod .NET, a także niektóre technologie związane z Java, HTML i SQL. Całe szkolenie trwało w sumie dwa lata i było punktem wyjścia do kolejnego mojego projektu, który ostatecznie rozciągnął się na kilka lat - ale to temat na osobną publikację. W tym miejscu wypada jedynie zauważyć, że podjąłem próbę zaadaptowania rozwiązań, które posiadałem już w opisywanym projekcie, aby stworzyć w C# i WinForms pełnoprawną aplikację okienkową, implementującą niezbędną funkcjonalność i wykorzystać ją jako podstawę dla nadchodzący projekt dyplomowy.
Z biegiem czasu pomysł ten zaczął wydawać mi się godny wypowiadania się na corocznych konferencjach z udziałem przedstawicieli różnych bibliotek, takich jak „LIBKOM” i „KRYM”. Pomysł tak, ale nie moja ówczesna realizacja. Wtedy też miałem nadzieję, że ktoś napisze to od nowa, stosując bardziej kompetentne podejście. Tak czy inaczej, do 2013 roku postanowiłem napisać raport ze swoich prac wstępnych i przesłać go do Komitetu Organizacyjnego Konferencji z wnioskiem o grant na udział w konferencji. Ku mojemu pewnemu zaskoczeniu, mój wniosek został zaakceptowany i zacząłem wprowadzać pewne udoskonalenia do projektu, aby przygotować go do prezentacji na konferencji.

Do tego czasu projekt otrzymał już nową nazwę BIRMA, uzyskał różne dodatkowe (nie tyle wdrożone, ale raczej zakładane) możliwości - wszystkie szczegóły można znaleźć w moim raporcie.

Szczerze mówiąc, trudno było nazwać BIRMA 2013 czymś kompletnym; Szczerze mówiąc, było to bardzo hackerskie rzemiosło, wykonane w pośpiechu. Jeśli chodzi o kod, praktycznie nie było żadnych specjalnych innowacji, poza raczej bezradną próbą stworzenia jakiejś ujednoliconej składni dla parsera, z wyglądu przypominającej język formatowania IRBIS 64 (a właściwie także system ISIS - z nawiasami jako strukturami cyklicznymi; dlaczego W tamtym czasie wydawało mi się to całkiem fajne). Parser beznadziejnie natknął się na te kręgi nawiasów odpowiedniego typu (ponieważ nawiasy pełniły także inną rolę, a mianowicie oznaczały podczas analizowania opcjonalne struktury, które można pominąć). Wszystkich chcących zapoznać się z trudną wówczas do wyobrażenia, nieuzasadnioną składnią BIRMY ponownie odsyłam do mojej ówczesnej relacji.

Generalnie poza zmaganiami z własnym parserem nie mam nic więcej do powiedzenia na temat kodu tej wersji - poza odwrotną konwersją istniejących źródeł do C++ z zachowaniem pewnych typowych cech kodu .NET (szczerze mówiąc, jest to trudne do zrozumienia, co dokładnie skłoniło mnie do przesunięcia wszystkiego z powrotem - pewnie jakaś głupia obawa przed utrzymaniem moich kodów źródłowych w tajemnicy, jakby to było coś równoważnego tajnej recepturze Coca-Coli).

Być może w tej głupiej decyzji leżą także przyczyny trudności w sparowaniu powstałej biblioteki DLL z istniejącym interfejsem domowej roboty stacji roboczej do wprowadzania danych do katalogu elektronicznego (tak, nie wspomniałem o innym ważnym fakcie: odtąd wszystko kod „silnika” BIRMA był zgodny z oczekiwaniami, jest oddzielony od części interfejsu i spakowany w odpowiedniej bibliotece DLL). Dlaczego do tych celów trzeba było napisać osobną stację roboczą, która zresztą swoim wyglądem i sposobem interakcji z użytkownikiem bezwstydnie kopiowała tę samą stację roboczą „Catalogizer” systemu IRBIS 64 – to już osobne pytanie. W skrócie: nadało to niezbędną solidność moim ówczesnym opracowaniom na potrzeby mojego projektu dyplomowego (w przeciwnym razie sam niestrawny silnik parsera byłby w jakiś sposób niewystarczający). Ponadto napotkałem wówczas pewne trudności w implementacji interfejsu stacji roboczej Cataloger z własnymi modułami, zaimplementowanymi zarówno w C++, jak i C#, oraz z bezpośrednim dostępem do mojego silnika.

Ogólnie rzecz biorąc, co dziwne, to właśnie ten dość niezgrabny prototyp przyszłej BIRMA.NET miał stać się moim „koniem pociągowym” na następne cztery lata. Nie można powiedzieć, że w tym czasie przynajmniej nie próbowałem znaleźć sposobów na nową, pełniejszą realizację wieloletniego pomysłu. Wśród innych nowości powinny pojawić się już zagnieżdżone sekwencje cykliczne, które mogłyby zawierać elementy opcjonalne - w ten sposób miałem zamiar wprowadzić w życie pomysł uniwersalnych szablonów opisów bibliograficznych publikacji i różnych innych ciekawostek. Jednak w mojej ówczesnej działalności praktycznej wszystko to było niewielkie, a implementacja, którą miałem w tamtym czasie, była wystarczająca do wprowadzania spisów treści. Poza tym wektor rozwoju naszej biblioteki zaczął coraz bardziej odchylać się w kierunku digitalizacji archiwów muzealnych, sprawozdawczości i innych mało interesujących mnie działań, co ostatecznie zmusiło mnie do ostatecznego jej opuszczenia, ustępując miejsca tym, którzy chcieli być bardziej zadowolony z tego wszystkiego.

Paradoksalnie, to właśnie po tych dramatycznych wydarzeniach wydawało się, że projekt BIRMA, który w tamtym czasie posiadał już wszystkie cechy typowego długoterminowego projektu budowlanego, zaczął nabierać długo oczekiwanego nowego życia! Miałem więcej wolnego czasu na bezczynne przemyślenia, ponownie zacząłem przeczesywać sieć WWW w poszukiwaniu czegoś podobnego (na szczęście teraz mogłem już domyślać się, że tego wszystkiego szukać nie byle gdzie, ale na GitHubie) i gdzieś w na początku tego roku w końcu trafiłem na odpowiedni produkt znanej firmy Salesforce pod niepozorną nazwą gruba. Sam mógłby zrobić prawie wszystko, czego potrzebowałem od takiego parsera - mianowicie inteligentnie izolować poszczególne fragmenty z dowolnego, ale wyraźnie ustrukturyzowanego tekstu, mając jednocześnie w miarę przyjazny dla użytkownika końcowego interfejs, zawierający takie zrozumiałe esencje, jak wzorca, szablonu i wystąpienia, a jednocześnie wykorzystując znaną składnię wyrażeń regularnych, która staje się nieporównywalnie bardziej czytelna dzięki podziałowi na wyznaczone do analizy grupy semantyczne.

Generalnie stwierdziłem, że to jest to gruba (Ciekawe, co oznacza ta nazwa? Może jakiś „zwykły parser zorientowany ogólnie”?) – dokładnie to, czego szukałem od dawna. Co prawda jego natychmiastowa implementacja na własne potrzeby miała taki problem, że silnik ten wymagał zbyt ścisłego trzymania się sekwencji strukturalnej tekstu źródłowego. W przypadku niektórych raportów, takich jak pliki dziennika (mianowicie zostały one umieszczone przez programistów jako wyraźne przykłady wykorzystania projektu) jest to całkiem odpowiednie, ale w przypadku tych samych tekstów zeskanowanych spisów treści jest to mało prawdopodobne. Przecież tę samą stronę ze spisem treści można rozpocząć od słów „Spis treści”, „Spis treści” i wszelkich innych wstępnych opisów, których nie musimy umieszczać w wynikach zamierzonej analizy (i wycinać je ręcznie za każdym razem jest również niewygodne). Poza tym pomiędzy poszczególnymi powtarzającymi się elementami, takimi jak imię i nazwisko autora, tytuł i numer strony, na stronie może znajdować się pewna ilość śmieci (np. rysunków i po prostu przypadkowych znaków), które również miło byłoby móc umieścić odciąć. Ten ostatni aspekt nie był jednak jeszcze tak znaczący, ale z powodu pierwszego istniejąca implementacja nie mogła zacząć szukać potrzebnych struktur w tekście od określonego miejsca, a zamiast tego po prostu przetworzyć go od początku, nie znalazła określiłem tam wzory i... zakończyłem pracę. Oczywiście potrzebne były pewne poprawki, aby przynajmniej zapewnić trochę miejsca pomiędzy powtarzającymi się strukturami, i to skłoniło mnie do powrotu do pracy.

Dodatkowym problemem było to, że sam projekt był realizowany w Javie i jeśli planowałem w przyszłości wdrożyć jakiś sposób łączenia tej technologii ze znanymi aplikacjami do wprowadzania danych do istniejących baz danych (jak np. „Cataloguer” Irbisa, to przynajmniej Przynajmniej zrób to w C# i .NET. Nie chodzi o to, że Java sama w sobie jest złym językiem – kiedyś nawet użyłem jej do zaimplementowania ciekawej aplikacji okienkowej, która implementowała funkcjonalność domowego programowalnego kalkulatora (w ramach projektu kursowego). Pod względem składni jest bardzo podobny do tego samego C-sharp. Cóż, to tylko plus: tym łatwiej będzie mi sfinalizować istniejący projekt. Nie chciałem jednak ponownie zanurzać się w ten dość nietypowy świat okiennych (a raczej desktopowych) technologii Java - wszak sam język nie był „szyty” na takie zastosowania i wcale nie miałem ochoty na powtarzanie poprzednie doświadczenie. Być może dzieje się tak właśnie dlatego, że C# w połączeniu z WinForms jest znacznie bliższy Delphi, od którego wielu z nas kiedyś zaczynało. Na szczęście dość szybko znaleziono potrzebne rozwiązanie – w postaci projektu IKVM.NET, co ułatwia tłumaczenie istniejących programów Java na zarządzany kod .NET. Co prawda sam projekt został już wtedy porzucony przez autorów, ale jego najnowsza realizacja pozwoliła mi z powodzeniem przeprowadzić niezbędne działania dla tekstów źródłowych gruba.

Dokonałem więc wszystkich niezbędnych zmian i zmontowałem to wszystko w DLL odpowiedniego typu, który z łatwością mógłby zostać „wybrany” przez dowolne projekty dla .NET Framework utworzone w Visual Studio. W międzyczasie stworzyłem kolejną warstwę dla wygodnej prezentacji zwracanych wyników gruba, w postaci odpowiednich struktur danych, które można wygodnie przetwarzać w widoku tabelarycznym (biorąc za podstawę zarówno wiersze, jak i kolumny, zarówno klucze słownikowe, jak i indeksy numeryczne). Cóż, same niezbędne narzędzia do przetwarzania i wyświetlania wyników zostały napisane dość szybko.

Również proces dostosowywania szablonów do nowego silnika w celu nauczenia go analizowania istniejących próbek zeskanowanych tekstów spisów treści nie spowodował szczególnych komplikacji. Tak naprawdę nie musiałem nawet odwoływać się do moich poprzednich szablonów: po prostu stworzyłem od zera wszystkie niezbędne szablony. Co więcej, o ile szablony zaprojektowane do współpracy z poprzednią wersją systemu wyznaczały dość wąskie ramy dla tekstów, które można było za ich pomocą poprawnie przeanalizować, to nowy silnik umożliwił już opracowanie dość uniwersalnych szablonów, odpowiednich dla kilku rodzajów znaczników na raz. Próbowałem nawet napisać jakiś kompleksowy szablon dla dowolnego tekstu spisu treści, choć oczywiście nawet przy wszystkich otwierających się przede mną nowych możliwościach, w tym w szczególności ograniczonej możliwości implementacji tych samych zagnieżdżonych sekwencji powtarzających się ( jak na przykład nazwiska i inicjały kilku autorów z rzędu), okazało się to utopią.

Być może w przyszłości uda się wdrożyć pewną koncepcję metaszablonów, która będzie w stanie sprawdzić tekst źródłowy pod kątem zgodności z kilkoma dostępnymi szablonami na raz, a następnie, zgodnie z uzyskanymi wynikami, wybrać odpowiedni najbardziej odpowiedni, wykorzystując jakiś inteligentny algorytm. Ale teraz bardziej martwiło mnie inne pytanie. Parser podobny gruba, pomimo całej swojej wszechstronności i modyfikacji, które wprowadziłem, nadal z natury nie był w stanie wykonać jednej pozornie prostej rzeczy, którą mój samodzielnie napisany parser był w stanie wykonać od pierwszej wersji. Mianowicie: potrafił znaleźć i wydobyć z tekstu źródłowego wszystkie fragmenty pasujące do maski określonej w zastosowanym szablonie w odpowiednim miejscu, zupełnie nie będąc zainteresowanym tym, co dany tekst zawiera w przestrzeniach pomiędzy tymi fragmentami. Jak na razie tylko nieznacznie ulepszyłem nowy silnik, pozwalając mu wyszukiwać wszystkie możliwe nowe powtórzenia danej sekwencji takich masek z aktualnej pozycji, pozostawiając możliwość obecności w tekście zestawów dowolnych znaków, które zostały całkowicie nieuwzględnione w analizie, zawarte pomiędzy wykrytymi powtarzającymi się strukturami. Nie pozwalało to jednak na ustawienie kolejnej maski niezależnie od wyników wyszukiwania poprzedniego fragmentu za pomocą odpowiedniej maski: ścisłość opisywanej struktury tekstu w dalszym ciągu nie pozostawiała miejsca na dowolne wtrącenia nieregularnych znaków.

I jeśli dla przykładów spisów treści, z którymi się spotkałem, problem ten nie wydawał się jeszcze aż tak poważny, to przy próbie zastosowania nowego mechanizmu analizującego do podobnego zadania analizowania zawartości strony internetowej (tj. ograniczenia są tutaj, pojawiły się z całą swoją oczywistością. Przecież dość łatwo jest ustawić niezbędne maski dla fragmentów znaczników sieciowych, pomiędzy którymi powinny znajdować się dane, których szukamy (które należy wydobyć), ale jak zmusić parser do natychmiastowego przejścia do następnego podobny fragment, pomimo wszystkich możliwych znaczników i atrybutów HTML, które można umieścić w odstępach między nimi?

Po chwili namysłu zdecydowałem się wprowadzić kilka wzorców usług (%all_przed) и (%all_after), służąc oczywistemu celowi, jakim jest zapewnienie, że wszystko, co może być zawarte w tekście źródłowym, zostanie pominięte przed jakimkolwiek wzorcem (maską), który po nich następuje. Co więcej, jeśli (%all_przed) w takim razie po prostu zignorowałem wszystkie te arbitralne włączenia (%all_after)wręcz przeciwnie, pozwoliło na dodanie ich do żądanego fragmentu po przejściu z poprzedniego fragmentu. Brzmi to dość prosto, jednak aby wdrożyć tę koncepcję, musiałem ponownie przeszukać źródła gorp, aby wprowadzić niezbędne modyfikacje, aby nie złamać już zaimplementowanej logiki. W końcu udało się to zrobić (choć napisano nawet pierwszą, choć bardzo błędną, implementację mojego parsera, a nawet szybciej - w ciągu kilku tygodni). Odtąd system nabrał prawdziwie uniwersalnej formy – nie mniej niż 12 lat od pierwszych prób jego funkcjonowania.

To oczywiście nie koniec naszych marzeń. Możesz także całkowicie przepisać parser szablonów gorp w C#, używając dowolnej z dostępnych bibliotek do implementacji darmowej gramatyki. Myślę, że kod powinien zostać znacznie uproszczony, a to pozwoli nam pozbyć się spuścizny w postaci istniejących źródeł Java. Ale przy istniejącym typie silnika całkiem możliwe jest też zrobienie różnych ciekawych rzeczy, łącznie z próbą zaimplementowania metaszablonów, o których już wspomniałem, nie mówiąc już o analizowaniu różnych danych z różnych stron internetowych (choć nie wykluczam że istniejące specjalistyczne narzędzia programowe są do tego bardziej odpowiednie – po prostu nie mam jeszcze odpowiedniego doświadczenia w ich używaniu).

Nawiasem mówiąc, tego lata otrzymałem już zaproszenie e-mailem od firmy korzystającej z technologii Salesforce (twórcy oryginału gruba), przejść rozmowę kwalifikacyjną do dalszej pracy w Rydze. Niestety w tej chwili nie jestem gotowy na takie przesunięcia.

Jeśli ten materiał wzbudzi jakieś zainteresowanie, to w drugiej części postaram się bardziej szczegółowo opisać technologię kompilacji, a następnie parsowania szablonów na przykładzie implementacji zastosowanej w Salesforce gruba (moje własne dodatki, z wyjątkiem kilku już opisanych słów funkcyjnych, nie wprowadzają praktycznie żadnych zmian w samej składni szablonu, więc prawie cała dokumentacja oryginalnego systemu gruba Pasuje również do mojej wersji).

Źródło: www.habr.com

Dodaj komentarz