Atvirkštinė inžinerija: Dabartinės lietuvių kalbos žodynas

Posted on | Tags: reverse engineering

Jau kurį laiką galvojau apie naują taikinį atvirkštinės inžinerijos (“reverse engineering”) projektui. Galų gale 2023 m. birželio mėnesį įsigijau Lietuvių Kalbos Instituto 2006-ais metais išleistą šeštąjį Dabartinės Lietuvių Kalbos Žodyno leidimą. Tai, jog norėjau vėl krapštytis kažkieno programiniame kode, yra tik viena priežastis. Kita - jau kurį laiką galvojau, kaip būtų gerai turėti kompiuteryje ar telefone lengvai pasiekiamą žodyną.

Skaitant knygas ar naujienas dažnai norisi išsiaiškinti kokio nors žodžio reikšmę, tačiau jei namie neturi fizinio žodyno, tenka žodžio ieškoti internete. Esu toje stovykloje žmonių, kurie mėgsta būti programinės įrangos savininkais ir, nusipirkę programą, turėti prieigą prie jos visam laikui. Šis žodynas atitiko visus kriterijus, tad nedvejodamas jį nusipirkau už apie 30 eurų.

Gan neįprasta 2023-iais metais, kai beveik viska programinė įranga egzistuoja skaitmeniniame formate, parsinešti į namus kompaktinį diską. Ir ne bet kokį, o supakuotą dėžėje, kuriame tiek diskas, tiek instrukcinė knygutė. Dėžutė - standartinės knygos dydžio, tad greitai rado vietą knygų spintoje, tarp kitų knygų. Įdėjus diską į diskasukį, pavarčiau knygutę, kurioje - standartinė informacija, įskaitant reikalavimus kompiuteriui, žodyno sandara, kirčiavimo pradžiamokslis, ir daugiau.

Dėžutė su instrukcine knygute.

Dėžutė su instrukcine knygute.

Vos atsivėrus įrašymo programai pastebėjau, kad netinkamai atvaizduojamos lietuviškos raidės. Laimei, Windows 10 tai pataisyti visai nesunku. Tereikia atverti nustatymų langą, “Time & Language” → “Language” → “Administrative language settings” → “Change system locale”, ir “Current system locale” pasirinkti lietuvių kalbą. Atlikus šį pakeitimą ir iš naujo paleidus įrašymo programą, lietuviškų raidžių atvaizdavimas iškart susitvarkė.

Įjungus žodyno programą prieš akis atsiveria langas su žodžių sąrašu kairėje, ir paaiškinimais centre. Dešinėje pusėje - skaitytų žodžių istorija, kurios dydis gali būti koreguojamas keičiant nustatymus. Be žodyno, programoje yra atskiras “Vietovardžių” langas, lietuvių kalbos abėcėlė, ir įvairių žodyne vartojamų sutrumpinimų bei ženklų paaiškinimai. Iki šiol visas mano dėmesys buvo nukreiptas būtent į pirmąjį - “Žodyno” - langą.

Dabartinės lietuvių kalbos žodynas Windows terpėje.

Dabartinės lietuvių kalbos žodynas Windows terpėje.

Uždavinys

Praleidau nemažai laiko tiesiog sukdamas pelės ratuką ir skaitydamas įvairių pasitaikiusių žodžių aprašymus, tačiau gan greit supratau, jog ši programa būtų labai pravarti telefone, ar net kompiuterio terminalo lange. Taigi, pagrindinis išsikeltas uždavinys buvo perprasti programoje naudojamų duomenų struktūrą, ir perkelti ją į kitą erdvę. Pradžiai vietoje grafinės sąsajos, kaip pavyzdžiui JavaFX, tikslu pasirinkau paprasčiausią duomenų atvaizdavimą terminalo lange.

Taip ir prasidėjo keletą vakarų trukęs projektas, kuriame itin gražiai susijungė keli skirtingi programos komponentai, kuriuos visus dabar ir aprašysiu.

Duomenų analizė

Prieš atvaizduojant programos duomenis kitoje terpėje, turėjau suprasti, kaip tie duomenys išsaugoti failuose. Atvėręs programos katalogą, radau bylų su įvairiomis galūnėmis - “ddc”, fpt", “idx” - kurios man visai nieko nesakė. Paieškos internete galų gale privedė prie “Visual FoxPro” - procedūrinio programavimo kalbos su objektinio programavimo funkcijomis. Ši kalba, anot Vikipedijos, yra glaustai integruota su savo reliacinės duomenų bazės varikliu, kuris leidžia pasiekti duomenis rašant SQL užklausas. Trumpai tariant - tai kodo biblioteka, leidžianti kurti įvairiausias programas.

Verta paminėti, kad paskutinė “Visual FoxPro” versija išėjo 2007-ais metais.

Išbandžiau keletą skirtingų JDBC tvarkyklių, tačiau nė viena tinkamai neveikė su DLKŽ failais. Laimei, internete yra nemažai informacijos apie šių failų struktūrą, tad nutariau tiesiog pats Java kalba parašyti klases jų skaitymui. Nereikėjo liesti daug failų, užteko perskaityti tris:

Failas Tipas Turinys
tab01c.ddc Duomenų bazė (.dbc) Žodžiai
tab04c.ddc Duomenų bazė (.dbc) Žodžių paaiškinimų vieta “memo” faile
tab04c.fpt Memo failas (.fpt) Duomenų blokai, saugantys žodžių paaiškinimus

Atidarius programą, žodžių sąrašas kairėję pusėje užpildomas perskaičius pirmąjį (tab01c.ddc) failą. Paspaudus ant žodžio, programa užklausia duomenų bazės (tab04c.ddc), kurioje vietoje “memo” faile yra to žodžio reikšmė, ir tada nuskaitomas tikslus skaičius baitų iš tab04c.fpt. Ši struktūra reiškia, kad programai nebūtina perskaityti visų žodžių reikšmių ir jų pastoviai laikyti kompiuterio atmintyje. Vietoje to, iki norimos reikšmės galima labai sparčiai nusigauti kaskart paspaudus ant žodžio.

Java klasių struktūra.

Java klasių struktūra.

Diagrama aukščiau parodo (nepilną) Java klasių struktūrą, bei ryšius tarp klasių ir failų, su kuriais jos dirba. Pavyko viską padalinti į atskirus domenus - “Visual Fox Pro” (pažymėtas geltonai) bei paties žodyno (pažymėtas mėlynai). Pervedant failų skaitymą klasėms, kurios žino viską apie “Visual FoxPro” failų struktūrą, bet nežino nieko apie šios specifinės programos (žodyno) domeną, pavyko supaprastinti kodą bei panaudoti tas pačias klases keliems darbams. Pavyzdžiui, klasė TableFileParser naudojama skaitant tiek žodžių sąrašą, tiek žodžių reikšmes.

Žodynas komandinėje eilutėje

Pradžiai nenorėjau kurti programai grafinės terpės, pagalvojau, kad užtektų jai pateikti žodžius komandine eilute. Tad greit aprašiau begalinį ciklą, kuris priima vartotojo įvestą žodį, paklausia žodyno, kokios šio žodžio reikšmės, ir visas jas atspausdina tame pačiame komandinės eilutės lange. Programa veikė, bet greit prisiminiau, jog “Visual FoxPro” “memo” faile duomenys yra saugomi raiškiojo teksto formatu (RTF), tad jų papildomai neapdorojant, programa pateikia štai tokius atsakymus (žemiau programos paklausiau, ką reiškia žodis “duomuo”):

duomuo
{\rtf1\ansi\ansicpg1257\deff0\deftab720{\fonttbl{\f0\fnil MS Sans Serif;}{\f1\froman\fcharset2 Symbol;}{\f2\froman\fprq2 akademinisLT2000;}{\f3\fswiss\fprq2 MS Sans Serif;}{\f4\fnil akademinisLT Kirciuotos;}}
{\colortbl\red0\green0\blue0;\red128\green128\blue0;\red128\green0\blue128;}
\deflang1024\pard\plain\f2\fs28\cf0\b duom\plain\f2\fs28\cf0 |\plain\f2\fs28\cf0\b u\'f5,\plain\f2\fs28\cf0  \plain\f2\fs28\cf0\i ~e\'b9s\plain\f2\fs28\cf0  \plain\f2\fs28\i v.\plain\f2\fs28\cf0  \plain\f2\fs28\cf2 (3\plain\f2\fs28\cf2\up8 a\plain\f2\fs28\cf2 )\plain\f2\fs28\cf0
\par kas duota, kuo remiamasi, darant i\'f0vadas: \plain\f2\fs28\cf1\i Mokslo d\'faomenys. ~en\'8b tvarkymas\plain\f2\fs28\cf0
\par \plain\lang1063\f3\fs16
\par }

Kuriant programą Android operacinei sistemai, ar naudojant JavaFX grafinę terpę, galėčiau šiuos duomenis be papildomo darbo ir atvaizduoti, nes tiek Android, tiek JavaFX turi metodus RTF duomenų atvaizdavimui, bet kadangi dirbau su komandine eilute, reikėjo šiuos duomenis pritaikyti būtent jai.

RTF skaitymas

Java turi nemažai bibliotekų darbui su RTF formato failais. Paskaitęs apie kelias jų, nutariau pasinaudoti Jon Iles sukurtu RTF Parser Kit. Kelios eilutės kodo, ir galėjau matyti žodžių reikšmes:

duomuo
duom|uõ, ~e¹s v. (3a)
kas duota, kuo remiamasi, darant išvadas: Mokslo dśomenys. ~en‹ tvarkymas

Nors reikšmių prasmę suprasti daugmaž galima, iškart tapo akivaizdu, kad yra kažkokia problema su teksto koduote. Palyginkite komandinės eilutės rodomą tekstą su matomu žodyno programoje (žemiau). Matosi keletas pagrindinių skirtumų:

Programoje naudojamas šriftas “akademinisLT2000”. Nors RTF duomenų pradžioje matosi, jog naudojama “Windows-1257” koduotė, tekstas neatrodė tiksliai pakeitus šriftą į kitą, taip pat palaikantį šią koduotę, pavyzdžiui “Times New Roman”. Beieškodamas informacijos apie lietuviškas koduotes, aptikau standartą “LST 1590-4:2000 - Informacinės technologijos. Ženklų kodavimas 8 bitais. 4 dalis. Lietuviškų kirčiuotų raidžių ir transkripcijos ženklų rinkinys Windows aplinkai”, priimtą 2000-aisiais metais. Puslapyje lietuvybė.lt galima rasti kiek daugiau informacijos, įskaitant ir kodų lentelę. Nors ši koduotė beveik atitinka “akademinisLT2000”, kai kurie simboliai vistiek yra skirtingose vietose, pavyzdžiui riestinis “ų” pagal LST 1590-4 atitinka “0xFF”, tačiau šrifte “akademinisLT2000” jis yra vietoje “0x2039”.

Nutariau tinkamam teksto konvertavimui paprasčiausiai apsirašyti funkciją, kuri priima vieną simbolį ir grąžina UTF-8 koduotės ekvivalentą. Vienoje klasėje aprašiau “LST 1590-4” pakeitimus, o kitoje - papildomai viską, kuo nuo “LST 1590-4” skiriasi “akademikas2000”. Turint šią klasių struktūrą, užteko “RTF Parser Kit” bibliotekos pagalba ištraukti iš RTF duomenų visą tekstą, ir kiekvieną gautą žodį transformuoti. Pagaliau turėjau tikslius kirčio ženklus komandinėje eilutėje.

duomuo
duom‖uõ, ~eñs v. (3a)
kas duota, kuo remiamasi, darant išvadas: Mokslo dúomenys. ~enų̃ tvarkymas

Spalva

Tinkamai atvaizduotas tekstas galėjo būti šio projekto pabaiga, tačiau norėjau nesustoti čia, ir taip pat terminale atkartoti programoje matomas spalvas. Žodyne naudojamos trys pagrindinės spalvos: juoda spalva vaizduojamas žodžio aprašymas, violetine - žodžio kirčiuotė, o geltonai-žaliai - vartosenos pavyzdžiai. Pats paprasčiausias būdas komandinėje eilutėje atvaizduoti spalvas - vadinamieji “ANSI escape codes”, arba specialios simbolių sekos, kurias supranta komandinė eilutė. Atspausdinus šiuos simbolius, jie nematomi akimi, tačiau visas po jų einantis tekstas atvaizduojamas tam tikra spalva.

Kadangi programa visus žodžių paaiškinimus saugo raiškiojo teksto formatu (RTF), turėjau perprasti, kaip RTF saugoma spalvų informacija. Anksčiau pateiktame RTF pavyzdyje matome šį tekstą:

{\colortbl\red0\green0\blue0;\red128\green128\blue0;\red128\green0\blue128;}

colortbl kontrolinis žodis nurodo, kad prasideda vadinamosios “spalvų lentelės” (angl. “color table”) aprašymas, po kurio seka vienas ar daugiau spalvų aprašymų, nurodant jų raudonos, geltonos ir mėlynos spalvų komponentus (RGB formatas aprašo spalvą trimis aštuonių bitų reikšmėmis, nuo 0 iki 255). Aukščiau esančioje spalvų lentelėje nurodytos trys reikšmės:

Naudodamasis “RTF Parser Kit” biblioteka, apsirašiau minimalų baigtinį automatą.

Turėdamas tokį būsenų sąrašą, su kiekvienai būsenai tiksliai aprašytais atributais (priimama RTF komanda, sekanti būsena, ir taip toliau), žinodamas jog esu spalvų lentelės viduje RTF faile, galėjau tiesiog priimti signalus iš RTF bibliotekos, ir keliauti per savo aprašyto automato būsenas. Mano programa kiekvienai spalvai kaupia raudonos, žalios ir mėlynos komponentų reikšmes, ir surinkusi visas tris, konvertuodavo jas į vieną galutinę spalvą, kartodama šį procesą kiekvienai spalvų lentelėje aprašytai spalvai.

Perėjus per visą spalvų lentelę, kai transformuojami žodžių paaiškinimai, užtenka sekti \cf simbolius, ir iš jau išsaugotų spalvų masyvo pasirinkti tinkamą. Pavyzdžiui, jei mūsų spalvų lentelėje - trys spalvos, aptikę žodžių paaiškinime \cf0 panaudosime pirmąją išsaugotą spalvą, \cf1 - antrąją, o \cf2 - trečiąją.

Visgi, norėjosi komandinėje eilutėje ne tiesiogiai naudoti DLKŽ pasirinktas spalvas, o atlikti dar vieną konvertavimą į šviesesnes:

Aptikta spalva Konvertuojama į ANSI kodas
rgb( 0 , 0 , 0 ) Spalvos atstatymą \033[0m
rgb( 0 , 255, 0 ) rgb( 0 , 255, 0 ) \033[0;92m
rgb(128, 128, 0 ) rgb(255, 255, 0 ) \033[0;33m
rgb(128, 0 , 128) rgb(255, 0 , 255) \033[0;35m

Grįžtant prie prieš anksčiau aprašyto RTF duomenų pavyzdžio:

\deflang1024\pard\plain\f2\fs28\cf0\b duom\plain\f2\fs28\cf0 |\plain\f2\fs28\cf0\b u\'f5,\plain\f2\fs28\cf0  \plain\f2\fs28\cf0\i ~e\'b9s\plain\f2\fs28\cf0  \plain\f2\fs28\i v.\plain\f2\fs28\cf0  \plain\f2\fs28\cf2 (3\plain\f2\fs28\cf2\up8 a\plain\f2\fs28\cf2 )\plain\f2\fs28\cf0
\par kas duota, kuo remiamasi, darant i\'f0vadas: \plain\f2\fs28\cf1\i Mokslo d\'faomenys. ~en\'8b tvarkymas\plain\f2\fs28\cf0

Pakeitus cf0, cf1 ir cf2 į spalvas, gauname tokį tekstą:

Spalvos komandinėje eilutėje.

Spalvos komandinėje eilutėje.

Komandinės eilutės struktūra

Visa auksčiau aprašyta logika aprašyta Java programa, kuria galima naudotis komandinėje eilutėje. Apsirašiau keletą komandų:

Pirmoji komanda taip vadinasi neatsitiktinai - daugiau informacijos apie REPL tipo programas galima rasti Vikipedijoje. REPL aplinkoje galima greitai tikrinti įvairių žodžių reikšmes, nes jai tereikia vienąkart užkrauti žodyno žodžius į atmintį.

Tuo tarpu define komandą galima naudoti neinteraktyviose aplinkose, pavyzdžiui, jeigu įjungus kompiuterį norite parodyti vieno atsitikrinio žodžio reikšmę, ir iškart leisti vartotojui naudotis komandine eilute kitiems tikslams.

Žodynas telefone

Norėjosi žodyną pasiekti ir telefone, kad ir neturint interneto ryšio, tad sukūriau dar vieną žodyno versiją Android operacinei sistemai. Čia žodžiai saugomi SQLite duomenų bazėje, o programoje - viso labo du pagrindiniai langai. Pirmasis, matomas tik įjungus programą - žodžių sąrašas, o į antrąjį patenkama pasirinkus žodį. Laimei, Android supranta anksčiau aprašytą RTF formatą, ir jam galima duoti neapdorotus RTF duomenis tiesiai iš originaliosios duomenų bazės.

Žodžių sąrašas Žodžio reikšmė

Abejuose languose - specialus lengvai pasiekiamas mygtukas, nukeliantis į antrąjį langą ir iškart rodantis atsitikrinio žodžio reikšmę. Tai - pats paprasčiausias būdas greitai susipažinti su nauju žodžiu, neinant per juos iš eilės nuo “a” iki “z”.

Taip pat pirmoje ekrano nuotraukoje apačioje matosi paieškos teksto laukas. Norėdamas atkartoti “Dabartinės lietuvių kalbos žodyno” paieškos logiką, čia papildomai įdarbinau du specialius simbolius - klaustukas reiškia “bet koks vienas simbolis”, o žvaigždutė reiškia “bet kokie nulis ar daugiau simbolių”. Fone kuriant duombazės užklausas, ? konvertuojamas į SQLite suprantamą _, o * - į %, tad paieškos vykdomos itin greitai.

Kas toliau?

Yra daug būdų pagerinti šias programas:

Nors būtų įdomu automatiškai kasdien ryte telefonu gauti pranešimą apie atsitiktinai parinktą žodį, šiai dienai bene viską, ką norėjau šiuo projektu įgyvendinti, jaučiuosi įgyvendinęs. Tiesa, leisti kelias dešimtis eurų žodynui - nebūtina. Visi jie (atnaujinti!) nemokamai pasiekiami Lietuvių kalbos instituto prižiūrimoje svetainėje E. kalba. Kodėl pats leidau pinigus? Tiesiog nenorėjau, kad LKI per daug pyktų, jog krapštausi jų programinėje įrangoje neapsižiūrėjau.