Teknik Makale

Delphi'de Unicode Güvenli PDF Metin Arama: NFC ve NFD

PDF Library for Delphi, metni kod birimine göre değil kurallı eşdeğerliğe (canonical equivalence) göre eşleştirebilir, bu yüzden önceden birleştirilmiş (precomposed) bir karakter olarak yazılan bir sorgu, bir temel harf artı birleştirici işaret olarak saklanan içeriği bulur ve tersi de geçerlidir. Bunu iki arama seçeneği kontrol eder: soCanonicalEquivalent, eşleştirme sırasında Unicode normalleştirmesini etkinleştirir ve soGraphemeClusters, her isabeti ve her joker karakter adımını tam grafem kümelerine kısıtlar

Bunun düzelttiği hata, belge aramasında en çok bildirilen ve en az anlaşılan hatalardan biridir. Bir kullanıcı bir ad arar, hiç sonuç görmez, adı belgeden kopyalar, arama kutusuna yapıştırır ve bulur. Açık bir biçimde hiçbir şey bozuk değildir: iki dize aynı görünür, aynı şekilde yazdırılır ve eşit değil olarak karşılaştırılır, çünkü biri U+00E9, diğeri ise ardından U+0301 gelen U+0065'tir

Aynı sözcük neden eşit değil olarak karşılaştırılıyor?

Unicode, aynı soyut karakter için birkaç kodlamaya izin verir. Aksanlı Latin harfleri, önceden birleştirilmiş kod noktaları olarak ve temel artı birleştirici diziler olarak var olur. Hangul heceleri, önceden birleştirilmiş heceler olarak ve ayrıştırılmış jamo olarak var olur. Bir PDF'nin hangisini içerdiği üreticiye, platforma ve bazen yazı tipine bağlıdır ve bunların hiçbiri aramayı yapan kişiye görünmez

Basit büyük/küçük harf katlamanın (case folding) bunu çözmemesinin nedeni tesadüfi değil yapısaldır. Büyük/küçük harf katlama ve aksan katlama, kod birimi düzeyinde bire birdir: katlanmış dize, orijinaliyle aynı uzunluğa sahiptir, bu yüzden katlanmış metindeki bir eşleşme konumu, orijinaldeki bir eşleşme konumudur. Normalleştirme ise bire bir değildir. Önceden birleştirilmiş bir karakter iki veya üç kod birimine dönüşür, ayrıştırılmış bir dizi tekrar bire çöker ve bu dönüşümden sonra konumlar artık çıkardığınız metinle hizalanmaz

İsabet koordinatlarını orijinal metne işaret eder halde tutmak

Bu, normalleştirilmiş aramanın yalnızca doğru değil aynı zamanda kullanılabilir olup olmadığını belirleyen kısımdır. Normalleştirme tarafından üretilen her kod birimi, kendisini üreten orijinal UTF-16 metninin başlangıç ve bitiş konumunu kaydeder. Özyinelemeli ayrıştırmalar, ebeveynlerinin kaynak aralığını devralır, birleştirmeler girdilerinin aralıklarını birleştirir ve bir eşleşme bulunduğunda kütüphane, eşleme aralığını en küçük başlangıç ve en büyük bitiş için tarar

Bunun sonucu, MatchStart, MatchLength, bağlam dizeleri ve her iki değiştirme giriş noktasının hepsinin, normalleştirilmiş ara biçimi değil orijinal çıkarılmış metni adreslemeye devam etmesidir. Bu eşleme olmadan, normalleştirilmiş bir arama size bir isabetin var olduğunu ama nerede olduğunu güvenilir bir şekilde söyleyemezdi; bu da vurgulamayı yanlış ve redaksiyonu tehlikeli kılar

Normalleştiricinin kendisi kendi kendine yeterlidir: Unicode 15.1'den kurallı ayrıştırma, birleştirme ve kurallı birleştirme sınıfı için kompakt tablolar, Hangul ise tablo girdileri yerine algoritmik kurallarla ele alınır. Hiçbir şey harici bir veri dosyasından yüklenmez ve hiçbir platform normalleştirme API'si çağrılmaz, bu yüzden bir Windows hizmeti, bir Linux arka plan programı (daemon) ve bir FPC derlemesi aynı girdide özdeş sonuçlar üretir

Kurallı eşdeğerlikle arama yapmak

Seçenekler bir kümedir, bu yüzden kurallı eşdeğerlik, tam sözcük eşleştirme, joker karakterler ve aksana duyarsız katlama gibi mevcut davranışlarla birleşir:

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);                       // boş sayfa aralığı = tüm belge

    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;

Normalleştirme, bir nedenden dolayı katılımı seçime bağlıdır (opt-in). NFD metnini ve konum eşlemesini oluşturmak iş gerektirir ve yalnızca ASCII içeren belgeler üzerindeki çoğu arama buna hiç ihtiyaç duymaz. Seçenek kullanıldığında, her metin bloğu iki dönüştürülmüş biçimi önbelleğe alır — biri birleştirici işaretler kaldırılmış, diğeri kaldırılmamış — bu yüzden aynı blok üzerindeki bir sorgu grubu, sorgu başına değil bir kez normalleştirilir. Büyük/küçük harf katlama, daha ucuz bire bir yolda değişmeden ilerlemeye devam eder

Grafem küme sınırları olmadan ne bozulur?

Kod birimleri karakter değildir ve karakterler de kullanıcıların algıladığı şey değildir. Bir bayrak emojisi iki bölgesel gösterge kod noktasıdır. Bir aile emojisi, sıfır genişlikli birleştiricilerle (zero-width joiner) bağlanmış birkaç kod noktasıdır. Bir Hint dili bitişiği (conjunct), bir ünsüz, bir virama ve başka bir ünsüzdür. Üst üste iki aksanı olan bir harf üç kod noktasıdır. Bunlardan herhangi birinin ortasında eşleştirmek veya kesmek, çöp olarak render edilen bir parça üretir

soGraphemeClusters, harfi harfine veya joker karakterli, her isabetin her iki ucunu da eksiksiz genişletilmiş grafem küme sınırlarına kısıtlar. Bölümleme, genişletilmiş kuralları uygular: CR ve LF eşleştirmesi, kontrol karakterleri, Hangul hece sınıfları, Extend ve SpacingMark, Prepend, emoji ZWJ dizileri, bölgesel gösterge eşleştirmesi ve Hint dili bitişik kesmeleri. Hiçbir zaman bir vekil (surrogate) çift içinde bir sınır üretilmez; bu tek başına, temel çok dilli düzlemin ötesindeki herhangi bir içerikte tüm bir bozuk sonuç sınıfını ortadan kaldırır

Seçenek ayrıca, saf (naive) bir uygulamanın hâlâ yanlış kesebileceği yer olan joker karakter tüketimini de yönetir. Tek karakterli joker karakter tam olarak bir eksiksiz kümeyi ilerletir ve çalışma (run) joker karakteri için geri izleme yalnızca küme sınırları arasında hareket eder:

// soGraphemeClusters olmadan, "?" bir kümenin yarısını tüketebilir ve
// metni sarkan bir birleştirici işaretle biten bir isabet döndürebilir
Found := Lib.SearchText('c?té',
  [soWildcards, soCanonicalEquivalent, soGraphemeClusters], '', Hits);

// Aynı sınırlar değiştirmeyi de korur, bu yüzden redaksiyon ve
// içerik yeniden yazma hiçbir zaman bir emojiyi veya aksanlı bir harfi bölmez
Replaced := Lib.SearchAndReplaceText('naïve', 'plain',
  [soCanonicalEquivalent, soGraphemeClusters], '1-20');

Gerçek bir iş yükü için seçenekleri seçmek

Üç kombinasyon çoğu durumu kapsar. Bir dahili belge arama kutusu için, soCanonicalEquivalent artı soDiacriticInsensitive, kullanıcıların beklediği hoşgörülü davranışı verir; hem kodlama biçimlerini hem de aksanlı ve aksansız yazımları eşleştirir. Yanlış pozitifin bir maliyeti olduğu hukuki veya uyumluluk (compliance) araması için, soCanonicalEquivalent'i soCaseSensitive ve soWholeWord ile kullanın ve aksan katlamayı kapalı bırakın, böylece eşdeğerlik kesin ve kodlamadan bağımsız olur

Belgeyi değiştiren herhangi bir şey için, istisnasız soGraphemeClusters ekleyin. Hafif yanlış bir aralık döndüren bir arama yalnızca bir okuyucuyu yanıltır; aynı yanlış aralığı kullanan bir değiştirme veya redaksiyon ise hatayı dosyanın içine yazar. Kaldırma aralıklarının yanlış alınmasının sonuçları, gerçek redaksiyon ve içerik kaldırma yazısında ele alınmıştır

Verimin önemli olduğu durumlarda toplu giriş noktalarını tercih edin. SearchTextBatch, her sayfanın metin blokları belleğe alınmışken boş olmayan her sorguyu çalıştırır; bu, sorgu başına bir sayfayı yeniden çıkarmaktan kaçınır ve önbelleğe alınmış normalleştirmeyi yeniden kullanır ve akış varyantları, çağıran boyutlu bir arabellek olmadan isabetleri yayınlar. Altındaki çıkarma modeli, metin arama ve sayfa öğesi numaralandırması yazısında anlatılmıştır

Bunun isteğe bağlı olmadığı yazı sistemleri

Korece için, kurallı eşdeğerlik bir adı bulmakla bulmamak arasındaki farktır, çünkü önceden birleştirilmiş heceler ve ayrıştırılmış jamo gerçek belgelerde ikisi de yaygındır. Vietnamca için, üst üste binen aksanlar birleştirme biçimini tamamen üreticiye bağımlı kılar. Hint dili yazı sistemleri için, bitişik ele alma, bir isabet sınırının okunaklı bir yere inip inmediğine karar verir. Japonca ve Çince için, arama tarafı görece basittir, ancak Japonca ve Çince için dikey yazı yazısında anlatıldığı gibi yerleşim tarafı öyle değildir

Genel kural kısadır: kaynak (corpus) İngilizce dışında herhangi bir dil içeriyorsa, kurallı eşdeğerliği açın ve çok pahalı olduğuna karar vermeden önce maliyeti ölçün. Çoğu belge kümesinde öyle değildir ve alternatif, kullanıcılarınızın bulmayı en çok önemsediği adlarda tam olarak sessizce başarısız olan bir arama özelliğidir

Unicode farkında arama, çıkarma, redaksiyon ve metin yeniden yazma, Delphi, C++Builder ve Free Pascal için tek bir motoru paylaşır; eksiksiz özellik listesi Delphi için PDF Library sayfasındadır