PDF Library for Delphi kan tekst matchen op canonieke equivalentie in plaats van op code-eenheid, zodat een zoekopdracht getypt als een vooraf samengesteld teken inhoud vindt die is opgeslagen als een basisletter plus een combinerend teken, en omgekeerd. Twee zoekopties bepalen dit: soCanonicalEquivalent schakelt Unicode-normalisatie in tijdens het matchen, en soGraphemeClusters beperkt elke treffer en elke wildcardstap tot volledige grafeemclusters
De bug die dit oplost, is een van de meest gerapporteerde en minst begrepen in documentzoekfunctionaliteit. Een gebruiker zoekt naar een naam, ziet geen resultaten, kopieert de naam uit het document, plakt deze in het zoekvak, en vindt hem wel. Er is niets op een voor de hand liggende manier kapot: de twee strings zien er identiek uit, drukken identiek af, en vergelijken ongelijk, omdat de ene U+00E9 is en de andere U+0065 gevolgd door U+0301
Waarom vergelijkt hetzelfde woord ongelijk?
Unicode staat meerdere coderingen toe voor hetzelfde abstracte teken. Latijnse letters met diakritische tekens bestaan als vooraf samengestelde codepunten en als basis-plus-combinerende reeksen. Hangul-lettergrepen bestaan als vooraf samengestelde lettergrepen en als ontlede jamo. Welke een PDF bevat, hangt af van de producent, het platform, en soms het lettertype, en niets daarvan is zichtbaar voor de persoon die zoekt
De reden dat eenvoudige case folding dit niet oplost, is structureel, niet toevallig. Case folding en accentfolding zijn één-op-één op het niveau van de code-eenheid: de gefoldde string heeft dezelfde lengte als het origineel, dus een matchpositie in de gefoldde tekst is een matchpositie in het origineel. Normalisatie is niet één op één. Eén vooraf samengesteld teken wordt twee of drie code-eenheden, een ontlede reeks valt terug tot één, en na die transformatie lopen posities niet meer gelijk met de tekst die u hebt geëxtraheerd
Trefcoördinaten laten wijzen naar de originele tekst
Dit is het deel dat bepaalt of genormaliseerd zoeken bruikbaar is en niet alleen correct. Elke code-eenheid die door normalisatie wordt geproduceerd, legt de start- en eindpositie vast van de originele UTF-16-tekst die haar produceerde. Recursieve ontledingen erven het bronbereik van hun ouder, samenstellingen voegen de bereiken van hun invoer samen, en wanneer een match wordt gevonden, doorzoekt de bibliotheek het mappinginterval naar de kleinste start en grootste einde
Het effect is dat MatchStart, MatchLength, de contextstrings en beide vervangingstoegangspunten allemaal de originele geëxtraheerde tekst blijven aanspreken, niet het genormaliseerde tussenresultaat. Zonder die mapping zou een genormaliseerde zoekopdracht u kunnen vertellen dat een treffer bestaat, maar niet betrouwbaar waar die was, wat markering fout maakt en redactie gevaarlijk
De normalisator zelf is zelfstandig: compacte tabellen voor canonieke ontleding, samenstelling en canonieke combineerklasse uit Unicode 15.1, waarbij Hangul wordt afgehandeld door de algoritmische regels in plaats van door tabelitems. Er wordt niets geladen uit een extern gegevensbestand en geen platform-normalisatie-API wordt aangeroepen, dus een Windows-service, een Linux-daemon en een FPC-build produceren allemaal identieke resultaten op dezelfde invoer
Zoeken met canonieke equivalentie
Opties vormen een set, dus canonieke equivalentie combineert met het bestaande gedrag, zoals matching op hele woorden, wildcards en diakritisch-ongevoelige folding:
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); // lege paginareeks = hele document
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;
Normalisatie is om een reden opt-in. Het opbouwen van de NFD-tekst en de positiemapping ervan kost werk, en de meeste zoekopdrachten over documenten met alleen ASCII hebben dat nooit nodig. Wanneer de optie wordt gebruikt, cacht elk tekstblok twee getransformeerde vormen, één met combinerende tekens verwijderd en één zonder, zodat een batch queries over hetzelfde blok eenmaal normaliseert in plaats van eenmaal per query. Case folding blijft ongewijzigd het goedkopere één-op-één-pad volgen
Wat gaat er kapot zonder grafeemclustergrenzen?
Code-eenheden zijn geen tekens, en tekens zijn niet wat gebruikers waarnemen. Een vlagemoji is twee regional-indicator-codepunten. Een gezinsemoji is verschillende codepunten verbonden door zero-width joiners. Een Indische conjunct is een medeklinker, een virama en nog een medeklinker. Een letter met twee gestapelde accenten is drie codepunten. Matchen of afsnijden in het midden van een van deze produceert een fragment dat als rommel wordt weergegeven
soGraphemeClusters beperkt beide uiteinden van elke treffer, letterlijk of wildcard, tot volledige uitgebreide grafeemclustergrenzen. De segmentatie implementeert de uitgebreide regels: CR-en-LF-koppeling, controletekens, Hangul-lettergreepklassen, Extend en SpacingMark, Prepend, emoji-ZWJ-reeksen, regional-indicator-koppeling en Indische conjunct-breukpunten. Er wordt nooit een grens geproduceerd binnen een surrogaatpaar, wat op zichzelf al een hele klasse beschadigde resultaten elimineert bij elke inhoud voorbij het basis-meertalige vlak
De optie bepaalt ook de wildcardconsumptie, wat de plek is waar een naïeve implementatie nog steeds verkeerd zou afsnijden. De wildcard voor één teken schuift precies één volledige cluster op, en backtracking voor de run-wildcard beweegt alleen tussen clustergrenzen:
// Zonder soGraphemeClusters kan "?" een halve cluster consumeren en
// een treffer retourneren waarvan de tekst eindigt in een loshangend combinerend teken
Found := Lib.SearchText('c?té',
[soWildcards, soCanonicalEquivalent, soGraphemeClusters], '', Hits);
// Dezelfde grenzen beschermen vervanging, zodat redactie en
// het herschrijven van inhoud nooit een emoji of een letter met accent splitsen
Replaced := Lib.SearchAndReplaceText('naïve', 'plain',
[soCanonicalEquivalent, soGraphemeClusters], '1-20');
Opties kiezen voor een echte workload
Drie combinaties dekken de meeste gevallen. Voor een intern zoekvak in documenten geeft soCanonicalEquivalent plus soDiacriticInsensitive het vergevingsgezinde gedrag dat gebruikers verwachten, en matcht het beide coderingsvormen en zowel geaccentueerde als niet-geaccentueerde spellingen. Voor juridisch of compliance-zoeken, waar een fout-positief een kostenpost is, gebruikt u soCanonicalEquivalent met soCaseSensitive en soWholeWord en laat u accentfolding uit, zodat equivalentie exact en coderingsonafhankelijk is
Voor alles wat het document wijzigt, voegt u zonder uitzondering soGraphemeClusters toe. Een zoekopdracht die een iets verkeerd bereik retourneert, misleidt alleen een lezer; een vervanging of redactie die hetzelfde verkeerde bereik gebruikt, schrijft de fout in het bestand. De gevolgen van verkeerde verwijderingsbereiken worden behandeld in echte redactie en contentverwijdering
Wanneer doorvoer ertoe doet, geeft u de voorkeur aan de batchtoegangspunten. SearchTextBatch voert elke niet-lege query uit terwijl de tekstblokken van elke pagina resident zijn, wat het opnieuw extraheren van een pagina per query vermijdt en de gecachte normalisatie hergebruikt, en de streamingvarianten geven treffers uit zonder een buffer van de aanroeper. Het onderliggende extractiemodel wordt beschreven in tekstzoeken en enumeratie van pagina-elementen
Schriftsystemen waarbij dit niet optioneel is
Voor Koreaans is canonieke equivalentie het verschil tussen een naam wel of niet vinden, omdat vooraf samengestelde lettergrepen en ontlede jamo beide veel voorkomen in echte documenten. Voor Vietnamees maken gestapelde diakritische tekens de samenstellingsvorm volledig producentafhankelijk. Voor Indische schriftsystemen bepaalt conjunct-afhandeling of een trefgrens op een leesbare plek terechtkomt. Voor Japans en Chinees is de zoekkant relatief eenvoudig, hoewel de lay-outkant dat niet is, zoals beschreven in verticaal schrijven voor Japans en Chinees
De vuistregel is kort: als het corpus enige andere taal dan Engels bevat, schakel dan canonieke equivalentie in en meet de kosten voordat u beslist dat die te hoog zijn. In de meeste documentensets is dat niet zo, en het alternatief is een zoekfunctie die stilzwijgend faalt op precies de namen die uw gebruikers het liefst willen vinden
Unicode-bewust zoeken, extractie, redactie en het herschrijven van tekst delen één engine voor Delphi, C++Builder en Free Pascal; de volledige functielijst staat op de PDF Library for Delphi-pagina