A PDF Library for Delphi kanonikus egyenértékűség szerint tud szöveget illeszteni kódegység szerint való illesztés helyett, így egy előre összetett karakterként begépelt lekérdezés megtalálja az alapbetű plusz kombinálójel formában tárolt tartalmat, és fordítva. Két keresési opció vezérli ezt: a soCanonicalEquivalent bekapcsolja a Unicode-normalizálást illesztés közben, a soGraphemeClusters pedig minden találatot és minden helyettesítő karakteres lépést teljes grafémaklaszterekre korlátoz
Az a hiba, amelyet ez kijavít, a dokumentumkeresés egyik leggyakrabban jelentett és legkevésbé megértett hibája. Egy felhasználó rákeres egy névre, nem lát eredményt, kimásolja a nevet a dokumentumból, beilleszti a keresőmezőbe, és megtalálja. Semmi nem tűnik nyilvánvalóan hibásnak: a két string azonosnak néz ki, azonosan nyomtatódik, mégis egyenlőtlennek bizonyul, mert az egyik U+00E9, a másik pedig U+0065, amelyet U+0301 követ
Miért nem egyenlő az összehasonlításban ugyanaz a szó?
A Unicode több kódolást is megenged ugyanahhoz az absztrakt karakterhez. A diakritikus jelekkel ellátott latin betűk léteznek előre összetett kódpontként és alapbetű plusz kombinálósorozatként is. A hangul szótagok léteznek előre összetett szótagként és lebontott dzsamóként is. Hogy egy PDF melyiket tartalmazza, az a producertől, a platformtól és néha a betűtípustól függ, és mindez láthatatlan a keresést végző személy számára
Az ok, amiért az egyszerű kis-nagybetű-hajtogatás nem oldja meg ezt, strukturális, nem esetleges. A kis-nagybetű- és ékezethajtogatás kódegység szinten egy-egyértelmű: a hajtogatott string ugyanolyan hosszú, mint az eredeti, így egy találati pozíció a hajtogatott szövegben egy találati pozíció az eredetiben is. A normalizálás nem egy-egyértelmű. Egy előre összetett karakterből két vagy három kódegység lesz, egy lebontott sorozat visszaomlik eggyé, és e transzformáció után a pozíciók már nem esnek egybe a kinyert szöveggel
A találati koordináták az eredeti szövegre mutatva tartása
Ez az a rész, amely eldönti, hogy a normalizált keresés használható-e, nem csupán helyes. A normalizálás által előállított minden kódegység rögzíti az azt előállító eredeti UTF-16 szöveg kezdő- és végpozícióját. A rekurzív lebontások öröklik szülőjük forrástartományát, az összetételek egyesítik bemeneteik tartományait, egy találat esetén pedig a könyvtár végigpásztázza a leképezési intervallumot a legkisebb kezdő- és legnagyobb végpontért
Ennek eredményeként a MatchStart, a MatchLength, a kontextus-stringek és mindkét csere-belépési pont továbbra is az eredeti kinyert szövegre hivatkozik, nem a normalizált köztes formára. E leképezés nélkül egy normalizált keresés megmondaná, hogy létezik találat, de nem megbízhatóan azt, hol volt, ami hibássá teszi a kiemelést, és veszélyessé a feketítést
Maga a normalizáló önálló: kompakt táblák a kanonikus lebontáshoz, összetételhez és a Unicode 15.1-ből származó kanonikus kombinálási osztályhoz, a hangult pedig algoritmikus szabályokkal kezeli, nem táblabejegyzésekkel. Semmi nem töltődik be külső adatfájlból, és semmilyen platform normalizálási API-t nem hív, így egy Windows-szolgáltatás, egy Linux-démon és egy FPC-build mind azonos eredményt ad ugyanarra a bemenetre
Keresés kanonikus egyenértékűséggel
Az opciók egy halmazt alkotnak, így a kanonikus egyenértékűség kombinálódik a meglévő viselkedésekkel, mint a teljes szó szerinti illesztés, a helyettesítő karakterek és az ékezet-érzéketlen hajtogatás:
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); // üres oldaltartomány = teljes dokumentum
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;
A normalizálás nem véletlenül opcionális. Az NFD-szöveg és a pozíciólekepezés felépítése munkába kerül, a legtöbb tisztán ASCII dokumentum feletti keresésnek pedig soha nincs rá szüksége. Amikor az opciót használják, minden szövegblokk két átalakított formát gyorsítótáraz, egyet kombinálójelek eltávolításával és egyet anélkül, így ugyanazon blokk feletti lekérdezések kötege egyszer normalizál, nem lekérdezésenként. A kis-nagybetű-hajtogatás továbbra is az olcsóbb egy-egyértelmű útvonalon halad, változatlanul
Mi törik el grafémaklaszter-határok nélkül?
A kódegységek nem karakterek, a karakterek pedig nem azok, amit a felhasználók érzékelnek. Egy zászló-emodzsi két regionálisjelző-kódpont. Egy családot ábrázoló emodzsi több kódpont, nulla szélességű összekötőkkel összefűzve. Egy indiai írásrendszerbeli ligatúra egy mássalhangzó, egy virama és egy másik mássalhangzó. Egy két egymásra rakott ékezettel rendelkező betű három kódpont. Bármelyik közepén való illesztés vagy vágás olyan töredéket ad, amely szemétként jelenik meg
A soGraphemeClusters minden találat mindkét végét, legyen az literál vagy helyettesítő karakteres, teljes kiterjesztett grafémaklaszter-határokra korlátozza. A szegmentálás a kiterjesztett szabályokat implementálja: CR és LF párosítás, vezérlőkarakterek, hangul szótagosztályok, Extend és SpacingMark, Prepend, emodzsi ZWJ-sorozatok, regionálisjelző-párosítás és indiai ligatúra-törések. Határ soha nem keletkezik egy helyettesítőpár közepén, ami önmagában megszünteti a hibás eredmények egy egész osztályát bármilyen, az alap többnyelvű síkon túli tartalomnál
Az opció a helyettesítő karakterek felhasználását is szabályozza, ami az a pont, ahol egy naiv implementáció még mindig helytelenül vágna. Az egykarakteres helyettesítő pontosan egy teljes klasztert lép előre, a szakasz-helyettesítő visszalépéses keresése pedig csak klaszterhatárok között mozog:
// soGraphemeClusters nélkül a „?” elfogyaszthat egy klaszter felét,
// és olyan találatot adhat vissza, amelynek szövege egy lógó kombinálójellel végződik
Found := Lib.SearchText('c?té',
[soWildcards, soCanonicalEquivalent, soGraphemeClusters], '', Hits);
// Ugyanezek a határok védik a cserét is, így a feketítés és
// a tartalomátírás soha nem vág ketté emodzsit vagy ékezetes betűt
Replaced := Lib.SearchAndReplaceText('naïve', 'plain',
[soCanonicalEquivalent, soGraphemeClusters], '1-20');
Opciók kiválasztása egy valódi terheléshez
Három kombináció fedi le a legtöbb esetet. Egy belső dokumentumkereső mezőhöz a soCanonicalEquivalent plusz soDiacriticInsensitive adja azt a megbocsátó viselkedést, amelyet a felhasználók elvárnak, mindkét kódolási formát és mind az ékezetes, mind az ékezet nélküli írásmódot illesztve. Jogi vagy megfelelőségi kereséshez, ahol egy hamis pozitívnak ára van, használd a soCanonicalEquivalent-et a soCaseSensitive-dal és a soWholeWord-dal, és hagyd kikapcsolva az ékezethajtogatást, hogy az egyenértékűség pontos és kódolásfüggetlen legyen
Minden olyasmihez, ami módosítja a dokumentumot, kivétel nélkül add hozzá a soGraphemeClusters-t. Egy keresés, amely kissé rossz tartományt ad vissza, csak félrevezeti az olvasót; egy csere vagy feketítés, amely ugyanazt a rossz tartományt használja, beleírja a hibát a fájlba. Az eltávolítási tartományok elrontásának következményeit a valódi feketítés és tartalomeltávolítás ismerteti
Amikor az átviteli sebesség számít, részesítsd előnyben a kötegelt belépési pontokat. A SearchTextBatch minden nem üres lekérdezést lefuttat, miközben az egyes oldalak szövegblokkjai rezidensek, ami elkerüli egy oldal lekérdezésenkénti újrakinyerését, és újrahasznosítja a gyorsítótárazott normalizálást, a streamelő változatok pedig hívó által méretezett puffer nélkül adnak ki találatokat. A mögöttes kinyerési modellt a szövegkeresés és oldalelem-felsorolás ismerteti
Írásrendszerek, ahol ez nem opcionális
Koreai esetén a kanonikus egyenértékűség a különbség egy név megtalálása és a meg nem találása között, mert az előre összetett szótagok és a lebontott dzsamó egyaránt gyakori a valódi dokumentumokban. Vietnami esetén az egymásra rakott diakritikus jelek teljesen a producertől teszik függővé az összetételi formát. Indiai írásrendszereknél a ligatúrakezelés dönti el, hogy egy találathatár olvasható helyre esik-e. Japán és kínai esetén a keresési oldal viszonylag egyszerű, bár az elrendezési oldal nem, ahogy azt a függőleges írás japánhoz és kínaihoz ismerteti
Az ökölszabály rövid: ha a korpusz bármilyen, angoltól eltérő nyelvet tartalmaz, kapcsold be a kanonikus egyenértékűséget, és mérd meg a költséget, mielőtt eldöntenéd, hogy túl drága. A legtöbb dokumentumkészletnél nem az, az alternatíva pedig egy keresési funkció, amely csendben pontosan azoknál a neveknél bukik el, amelyeknek megtalálása a felhasználóidnak a leginkább számít
A Unicode-tudatos keresés, kinyerés, feketítés és szövegátírás egyetlen motoron osztozik Delphihez, C++Builderhez és Free Pascalhoz; a teljes funkciólista a PDF Library for Delphi oldalán található