Tehnički članak

Pretraga PDF teksta sigurna za Unicode u Delphiju: NFC i NFD

PDF Library for Delphi može podudarati tekst prema kanonskoj ekvivalenciji umjesto prema kodnoj jedinici, pa upit upisan kao unaprijed sastavljen znak pronalazi sadržaj pohranjen kao osnovno slovo plus kombinirajući znak, i obrnuto. Time upravljaju dvije opcije pretrage: soCanonicalEquivalent omogućuje Unicode normalizaciju tijekom podudaranja, a soGraphemeClusters ograničava svaki pogodak i svaki korak zamjenskog znaka na cijele grafemske klastere

Greška koju ovo rješava jedna je od najprijavljivanijih i najmanje shvaćenih u pretraživanju dokumenata. Korisnik pretraži ime, ne vidi rezultate, kopira ime iz dokumenta, zalijepi ga u polje za pretragu i pronađe ga. Ništa nije očito pokvareno: oba niza izgledaju identično, ispisuju se identično, a pri usporedbi ispadaju nejednaka, jer je jedan U+00E9, a drugi U+0065 iza kojeg slijedi U+0301

Zašto se ista riječ pri usporedbi ispostavi nejednakom?

Unicode dopušta nekoliko kodiranja za isti apstraktni znak. Latinska slova s dijakritičkim znakovima postoje kao unaprijed sastavljene kodne točke i kao nizovi osnova plus kombinirajući znak. Hangul slogovi postoje kao unaprijed sastavljeni slogovi i kao rastavljeni jamo. Koji od njih PDF sadrži ovisi o proizvođaču, platformi, a ponekad i fontu, i ništa od toga nije vidljivo osobi koja provodi pretragu

Razlog zašto jednostavno preslikavanje velikih/malih slova ovo ne rješava strukturan je, ne slučajan. Preslikavanje velikih/malih slova i preslikavanje naglasaka su jedan-na-jedan na razini kodne jedinice: preslikani niz iste je duljine kao izvorni, pa pozicija podudaranja u preslikanom tekstu odgovara poziciji podudaranja u izvorniku. Normalizacija nije jedan-na-jedan. Jedan unaprijed sastavljen znak postaje dvije ili tri kodne jedinice, rastavljen niz se sažima natrag u jednu, a nakon te transformacije pozicije se više ne poklapaju s izdvojenim tekstom

Zadržavanje koordinata pogotka usmjerenih na izvorni tekst

Ovo je dio koji određuje je li normalizirana pretraga upotrebljiva, a ne samo ispravna. Svaka kodna jedinica proizvedena normalizacijom bilježi početnu i završnu poziciju izvornog UTF-16 teksta koji ju je proizveo. Rekurzivna rastavljanja nasljeđuju izvorni raspon svog roditelja, sastavljanja spajaju raspone svojih ulaza, a kada se pronađe podudaranje, knjižnica pretražuje interval mapiranja za najmanji početak i najveći kraj

Učinak je da MatchStart, MatchLength, kontekstualni nizovi i obje ulazne točke zamjene i dalje adresiraju izvorni izdvojeni tekst, a ne normalizirani posrednik. Bez tog mapiranja, normalizirana pretraga mogla bi vam reći da pogodak postoji, ali ne pouzdano gdje je bio, što isticanje čini pogrešnim, a redakciju opasnom

Sam normalizator je samostalan: kompaktne tablice za kanonsko rastavljanje, sastavljanje i kanonsku kombinirajuću klasu iz Unicode 15.1, uz Hangul obrađen algoritamskim pravilima, a ne unosima u tablici. Ništa se ne učitava iz vanjske datoteke podataka i ne poziva se nikakav platformski API za normalizaciju, pa Windows servis, Linux daemon i FPC build svi proizvode identične rezultate na istom ulazu

Pretraga uz kanonsku ekvivalenciju

Opcije čine skup, pa se kanonska ekvivalencija kombinira s postojećim ponašanjima poput podudaranja cijelih riječi, zamjenskih znakova i preslikavanja neosjetljivog na dijakritičke znakove:

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);                       // prazan raspon stranica = cijeli dokument

    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;

Normalizacija je namjerno opcionalna. Izgradnja NFD teksta i njegovog mapiranja pozicija zahtijeva rad, a većina pretraga nad dokumentima koji sadrže samo ASCII to nikada ne treba. Kada se opcija koristi, svaki tekstualni blok predmemorira dva transformirana oblika, jedan s uklonjenim kombinirajućim znakovima i jedan bez njih, tako da se paket upita nad istim blokom normalizira jednom, a ne po svakom upitu. Preslikavanje velikih/malih slova i dalje putuje jeftinijim jedan-na-jedan putem, nepromijenjeno

Što se pokvari bez granica grafemskih klastera?

Kodne jedinice nisu znakovi, a znakovi nisu ono što korisnici percipiraju. Emoji zastave dvije su kodne točke regionalnog indikatora. Emoji obitelji nekoliko je kodnih točaka spojenih spojnicama nulte širine (zero-width joiner). Indijski konjunkt suglasnik je, virama i drugi suglasnik. Slovo s dva naslagana naglaska tri su kodne točke. Podudaranje ili rezanje na sredini bilo čega od toga proizvodi fragment koji se prikazuje kao smeće

soGraphemeClusters ograničava oba kraja svakog pogotka, doslovnog ili sa zamjenskim znakom, na cjelovite granice proširenog grafemskog klastera. Segmentacija implementira proširena pravila: uparivanje CR i LF, kontrolne znakove, klase Hangul slogova, Extend i SpacingMark, Prepend, emoji ZWJ nizove, uparivanje regionalnih indikatora i prekide indijskih konjunkta. Granica se nikada ne stvara unutar surogatnog para, što samo po sebi eliminira čitavu klasu oštećenih rezultata na bilo kojem sadržaju izvan osnovne višejezične ravnine

Opcija također upravlja potrošnjom zamjenskih znakova, gdje bi naivna implementacija i dalje pogrešno rezala. Zamjenski znak za jedan znak pomiče se točno za jedan cjelovit klaster, a povratno traženje za zamjenski znak niza kreće se samo između granica klastera:

// Bez soGraphemeClusters, "?" može potrošiti pola klastera i
// vratiti pogodak čiji tekst završava visećim kombinirajućim znakom
Found := Lib.SearchText('c?té',
  [soWildcards, soCanonicalEquivalent, soGraphemeClusters], '', Hits);

// Iste granice štite zamjenu, pa redakcija i
// prepisivanje sadržaja nikada ne razdvoje emoji ili naglašeno slovo
Replaced := Lib.SearchAndReplaceText('naïve', 'plain',
  [soCanonicalEquivalent, soGraphemeClusters], '1-20');

Odabir opcija za stvarno radno opterećenje

Tri kombinacije pokrivaju većinu slučajeva. Za internu okvir za pretragu dokumenata, soCanonicalEquivalent plus soDiacriticInsensitive daje popustljivo ponašanje koje korisnici očekuju, podudarajući oba oblika kodiranja i oba pravopisa, s naglascima i bez njih. Za pravnu ili usklađenu pretragu, gdje lažni pozitiv ima cijenu, koristite soCanonicalEquivalent uz soCaseSensitive i soWholeWord i ostavite preslikavanje naglasaka isključenim, tako da je ekvivalencija točna i neovisna o kodiranju

Za sve što mijenja dokument, dodajte soGraphemeClusters bez iznimke. Pretraga koja vrati malo pogrešan raspon samo zavarava čitatelja; zamjena ili redakcija koja koristi isti pogrešan raspon upisuje pogrešku u datoteku. Posljedice pogrešnih raspona uklanjanja opisane su u pravoj redakciji i uklanjanju sadržaja

Kada je propusnost bitna, dajte prednost paketnim ulaznim točkama. SearchTextBatch pokreće svaki neprazan upit dok su tekstualni blokovi svake stranice rezidentni, čime se izbjegava ponovno izdvajanje stranice po upitu i ponovno koristi predmemorirana normalizacija, a strujne varijante emitiraju pogotke bez međuspremnika koji poziva definira po veličini. Model izdvajanja u pozadini opisan je u pretrazi teksta i nabrajanju elemenata stranice

Pisma za koje ovo nije opcionalno

Za korejski, kanonska ekvivalencija je razlika između pronalaska imena i nepronalaska, jer su i unaprijed sastavljeni slogovi i rastavljeni jamo uobičajeni u stvarnim dokumentima. Za vijetnamski, naslagani dijakritički znakovi čine oblik sastavljanja potpuno ovisnim o proizvođaču. Za indijska pisma, obrada konjunkta odlučuje slijeće li granica pogotka na čitljivo mjesto. Za japanski i kineski, strana pretrage relativno je jednostavna, iako strana rasporeda nije, kako je opisano u okomitom pisanju za japanski i kineski

Praktično pravilo kratko je: ako korpus sadrži bilo koji jezik osim engleskog, uključite kanonsku ekvivalenciju i izmjerite trošak prije nego što zaključite da je preskup. U većini skupova dokumenata nije, a alternativa je značajka pretrage koja tiho zakaže upravo na imenima koja su vašim korisnicima najvažnija za pronaći

Pretraga svjesna Unicodea, izdvajanje, redakcija i prepisivanje teksta dijele jedan mehanizam za Delphi, C++Builder i Free Pascal; cjelokupni popis značajki nalazi se na stranici PDF Library for Delphi