PDF Library for Delphi gali surasti tekstą pagal kanoninį lygiavertiškumą, o ne pagal kodo vienetą, todėl užklausa, įvesta kaip iš anksto sudarytas simbolis, suranda turinį, saugomą kaip bazinę raidę plius jungiamąjį ženklą, ir atvirkščiai. Tai valdo dvi paieškos parinktys: soCanonicalEquivalent įjungia Unicode normalizavimą atitikimo metu, o soGraphemeClusters apriboja kiekvieną rezultatą ir kiekvieną pakaitos simbolio žingsnį iki pilnų grafemos klasterių
Klaida, kurią tai ištaiso, yra viena dažniausiai pranešamų ir menkiausiai suprastų dokumentų paieškoje. Vartotojas ieško vardo, nemato rezultatų, nukopijuoja vardą iš dokumento, įklijuoja į paieškos langelį ir jį randa. Niekas akivaizdžiai nesugedę: abi eilutės atrodo identiškos, atspausdinamos identiškai, bet palyginus jos nelygios, nes viena yra U+00E9, o kita – U+0065, po kurio seka U+0301
Kodėl tas pats žodis palyginamas kaip nelygus?
Unicode leidžia kelis koduotus to paties abstraktaus simbolio užrašymus. Lotyniškos raidės su diakritiniais ženklais egzistuoja ir kaip iš anksto sudaryti kodo taškai, ir kaip bazinės raidės plius jungiamojo ženklo sekos. Korėjiečių skiemenys egzistuoja ir kaip iš anksto sudaryti skiemenys, ir kaip išskaidyti jamo. Kurį iš jų turi konkretus PDF failas, priklauso nuo kūrėjo, platformos ir kartais nuo šrifto, ir nė vienas iš šių dalykų nematomas žmogui, kuris atlieka paiešką
Priežastis, kodėl paprastas registro sulyginimas to neišsprendžia, yra struktūrinė, o ne atsitiktinė. Registro ir akcentų sulyginimas yra vienas su vienu kodo vieneto lygmenyje: suvienodinta eilutė turi tą patį ilgį kaip originali, todėl atitikimo pozicija suvienodintame tekste yra atitikimo pozicija ir originale. Normalizavimas nėra vienas su vienu. Vienas iš anksto sudarytas simbolis tampa dviem ar trimis kodo vienetais, išskaidyta seka susijungia atgal į vieną, ir po šios transformacijos pozicijos nebeatitinka teksto, kurį ištraukėte
Rezultatų koordinačių išlaikymas, nukreiptų į originalų tekstą
Būtent ši dalis nulemia, ar normalizuota paieška yra naudinga, o ne tik teisinga. Kiekvienas normalizavimo sukurtas kodo vienetas įrašo originalaus UTF-16 teksto, kuris jį sukūrė, pradžios ir pabaigos poziciją. Rekursyvūs išskaidymai paveldi savo tėvinio elemento šaltinio diapazoną, sudėtiniai elementai sujungia savo įvesčių diapazonus, o radus atitikimą biblioteka nuskaito susiejimo intervalą, ieškodama mažiausios pradžios ir didžiausios pabaigos
Rezultatas tas, kad MatchStart, MatchLength, konteksto eilutės ir abu pakeitimo įėjimo taškai visi ir toliau nurodo į originalų ištrauktą tekstą, o ne į normalizuotą tarpinį variantą. Be šio susiejimo, normalizuota paieška galėtų pasakyti, kad rezultatas egzistuoja, bet nepatikimai – kur jis buvo, o tai sugadintų paryškinimą ir padarytų redagavimą (redaction) pavojingą
Pats normalizatorius yra savarankiškas: kompaktiškos lentelės kanoniniam išskaidymui, sudėčiai ir kanoninei jungimo klasei iš Unicode 15.1, o korėjiečių kalbos atveju tvarkoma algoritminėmis taisyklėmis, o ne lentelės įrašais. Niekas neįkeliama iš išorinio duomenų failo ir nekviečiamas joks platformos normalizavimo API, todėl Windows tarnyba, Linux tarnybinė programa ir FPC statinys visi duoda identiškus rezultatus tam pačiam duomenų rinkiniui
Paieška su kanoniniu lygiavertiškumu
Parinktys yra rinkinys, todėl kanoninis lygiavertiškumas derinamas su esamomis elgsenomis, tokiomis kaip viso žodžio atitikimas, pakaitos simboliai ir diakritinius ženklus ignoruojantis sulyginimas:
uses
PDFlibrary;
var
Lib: TPDFlib;
Hits: array of TPDFlibSearchHit;
Found, I: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('contracts.pdf', '');
SetLength(Hits, 500);
Found := Lib.SearchText('Bäcker', [soCanonicalEquivalent, soWholeWord],
'', Hits); // tuščias puslapių diapazonas = visas dokumentas
for I := 0 to Found - 1 do
Log(Format('page %d: "%s" at %d (%d chars)',
[Hits[I].Page, Hits[I].MatchText, Hits[I].MatchStart,
Hits[I].MatchLength]));
finally
Lib.Free;
end;
end;
Normalizavimas yra sąmoningai neprivalomas (opt-in) dėl priežasties. NFD teksto ir jo pozicijų susiejimo sukūrimas kainuoja darbo, o dauguma paieškų dokumentuose, turinčiuose tik ASCII simbolius, jo niekada nereikalauja. Kai parinktis naudojama, kiekvienas teksto blokas talpina dvi transformuotas formas – vieną be jungiamųjų ženklų ir vieną su jais, todėl užklausų partija tame pačiame bloke normalizuojama vieną kartą, o ne kiekvienai užklausai atskirai. Registro sulyginimas ir toliau eina tuo pačiu pigesniu vienas-su-vienu keliu, nepakitęs
Kas sugenda be grafemos klasterio ribų?
Kodo vienetai nėra simboliai, o simboliai nėra tai, ką suvokia vartotojai. Vėliavos jaustukas yra du regioninio indikatoriaus kodo taškai. Šeimos jaustukas yra keli kodo taškai, sujungti nulinio pločio jungtukais. Indų raštų junginys yra priebalsė, virama ir kita priebalsė. Raidė su dviem sudėtais akcentais yra trys kodo taškai. Atitikimas ar kirtimas bet kurio iš jų viduryje duoda fragmentą, kuris atvaizduojamas kaip šiukšlės
soGraphemeClusters apriboja abu kiekvieno rezultato, literalaus ar pakaitos simbolio, galus iki pilnų išplėstinių grafemos klasterių ribų. Segmentavimas įgyvendina išplėstas taisykles: CR ir LF poravimą, valdymo simbolius, korėjiečių skiemenų klases, Extend ir SpacingMark, Prepend, jaustukų ZWJ sekas, regioninio indikatoriaus poravimą ir Indijos raštų junginių lūžius. Riba niekada nesukuriama pakaitinės poros viduje, o vien tai pašalina visą klasę sugadintų rezultatų bet kokiam turiniui, esančiam už bazinės daugiakalbės plokštumos ribų
Parinktis taip pat valdo pakaitos simbolio suvartojimą – čia naivi realizacija vis tiek kirstų neteisingai. Vieno simbolio pakaitos simbolis pasistumia lygiai per vieną pilną klasterį, o grįžimo atgal (backtracking) paieška su pakartojimo pakaitos simboliu juda tik tarp klasterių ribų:
// Be soGraphemeClusters "?" gali suvartoti pusę klasterio ir
// grąžinti rezultatą, kurio tekstas baigiasi pakibusiu jungiamuoju ženklu
Found := Lib.SearchText('c?té',
[soWildcards, soCanonicalEquivalent, soGraphemeClusters], '', Hits);
// Tos pačios ribos apsaugo ir pakeitimą, todėl redagavimas ir
// turinio perrašymas niekada nesuskaido jaustuko ar raidės su akcentu
Replaced := Lib.SearchAndReplaceText('naïve', 'plain',
[soCanonicalEquivalent, soGraphemeClusters], '1-20');
Parinkčių pasirinkimas realiam darbo krūviui
Trys derinys apima daugumą atvejų. Vidinei dokumentų paieškos juostai soCanonicalEquivalent plius soDiacriticInsensitive duoda atlaidžią elgseną, kurios tikisi vartotojai, atitinkant abi koduotes bei rašybą su akcentais ir be jų. Teisinei ar atitikties paieškai, kur klaidingas teigiamas rezultatas kainuoja, naudokite soCanonicalEquivalent su soCaseSensitive ir soWholeWord ir palikite akcentų sulyginimą išjungtą, kad lygiavertiškumas būtų tikslus ir nepriklausomas nuo koduotės
Viskam, kas keičia dokumentą, be išimčių pridėkite soGraphemeClusters. Paieška, grąžinanti šiek tiek neteisingą diapazoną, tik suklaidina skaitytoją; pakeitimas ar redagavimas, naudojantis tą patį neteisingą diapazoną, įrašo klaidą į patį failą. Pasekmės, kylančios dėl neteisingų šalinimo diapazonų, aprašytos straipsnyje tikras redagavimas ir turinio šalinimas
Kai svarbi pralaida, rinkitės partinius įėjimo taškus. SearchTextBatch vykdo kiekvieną netuščią užklausą, kol kiekvieno puslapio teksto blokai yra atmintyje, taip išvengiant pakartotinio puslapio ištraukimo kiekvienai užklausai ir pakartotinai naudojant talpinamą normalizavimą, o srautiniai variantai perduoda rezultatus be kviečiančiojo dydžio buferio. Ištraukimo modelis apačioje aprašytas straipsnyje teksto paieška ir puslapio elementų išvardijimas
Raštai, kuriuose tai nėra pasirenkama
Korėjiečių kalbai kanoninis lygiavertiškumas yra skirtumas tarp vardo suradimo ir nesuradimo, nes iš anksto sudaryti skiemenys ir išskaidytas jamo abu dažni realiuose dokumentuose. Vietnamiečių kalbai sudėti diakritiniai ženklai daro sudėties formą visiškai priklausomą nuo kūrėjo. Indų raštams junginių apdorojimas nusprendžia, ar rezultato riba atsidurs skaitomoje vietoje. Japonų ir kinų kalboms paieškos pusė santykinai paprasta, nors maketo pusė – ne, kaip aprašyta straipsnyje vertikalus rašymas japonų ir kinų kalboms
Bendra taisyklė trumpa: jei duomenų rinkinyje yra bet kokia kalba, ne tik anglų, įjunkite kanoninį lygiavertiškumą ir įvertinkite kainą prieš nuspręsdami, kad ji per didelė. Daugumoje dokumentų rinkinių ji nėra, o alternatyva – paieškos funkcija, kuri tyliai neveikia būtent su tais vardais, kuriuos jūsų vartotojams svarbiausia rasti
Unicode suvokianti paieška, ištraukimas, redagavimas ir teksto perrašymas dalijasi vienu varikliu Delphi, C++Builder ir Free Pascal aplinkoms; pilnas funkcijų sąrašas pateikiamas PDF Library for Delphi puslapyje