مقاله فنی

جست‌وجوی متن PDF ایمن نسبت به یونیکد در دلفی: NFC و NFD

PDF Library for Delphi می‌تواند متن را بر اساس تعادل قانونی (canonical equivalence) به‌جای واحد کد مطابقت دهد، بنابراین یک پرس‌وجو که به‌صورت یک نویسه پیش‌ترکیب‌شده تایپ شده، محتوایی را می‌یابد که به‌صورت یک حرف پایه به‌علاوه یک نشانه ترکیبی ذخیره شده، و برعکس. دو گزینه جست‌وجو آن را کنترل می‌کنند: soCanonicalEquivalent نرمال‌سازی یونیکد را در حین تطبیق فعال می‌کند، و soGraphemeClusters هر نتیجه و هر گام حرف‌جای‌خالی (wildcard) را به خوشه‌های گرافیم کامل محدود می‌کند

اشکالی که این حل می‌کند یکی از پرگزارش‌ترین و کم‌فهمیده‌شده‌ترین اشکال‌ها در جست‌وجوی سند است. یک کاربر یک نام را جست‌وجو می‌کند، هیچ نتیجه‌ای نمی‌بیند، نام را از سند کپی می‌کند، آن را در جعبه جست‌وجو می‌چسباند، و آن را می‌یابد. هیچ چیزی به شکل آشکاری خراب نیست: دو رشته یکسان به نظر می‌رسند، یکسان چاپ می‌شوند، و نابرابر مقایسه می‌شوند، زیرا یکی U+00E9 است و دیگری U+0065 است که با U+0301 دنبال می‌شود

چرا همان واژه نابرابر مقایسه می‌شود؟

یونیکد چندین رمزگذاری را برای همان نویسه انتزاعی مجاز می‌داند. حروف لاتین با علامت‌های تشخیصی هم به‌صورت نقطه‌های کد پیش‌ترکیب‌شده وجود دارند و هم به‌صورت دنباله‌های پایه به‌علاوه ترکیبی. هجاهای هانگول هم به‌صورت هجاهای پیش‌ترکیب‌شده وجود دارند و هم به‌صورت جاموی تجزیه‌شده. اینکه کدام یک را یک PDF دارد به تولیدکننده، سکو، و گاهی فونت بستگی دارد، و هیچ‌کدام از این‌ها برای شخصی که جست‌وجو می‌کند قابل‌مشاهده نیست

دلیلی که تاخوردگی حروف کوچک/بزرگ ساده این را حل نمی‌کند، ساختاری است نه اتفاقی. تاخوردگی حروف کوچک/بزرگ و تاخوردگی تکیه یک‌به‌یک در سطح واحد کد هستند: رشته تاخورده همان طول رشته اصلی را دارد، بنابراین یک موقعیت تطبیق در متن تاخورده، یک موقعیت تطبیق در اصل است. نرمال‌سازی یک‌به‌یک نیست. یک نویسه پیش‌ترکیب‌شده به دو یا سه واحد کد تبدیل می‌شود، یک دنباله تجزیه‌شده به یکی برمی‌گردد، و پس از آن تبدیل، موقعیت‌ها دیگر با متنی که استخراج کرده‌اید هم‌تراز نیستند

نگه‌داشتن مختصات نتیجه به‌سمت متن اصلی

این همان بخشی است که تعیین می‌کند آیا جست‌وجوی نرمال‌شده قابل‌استفاده است، نه فقط درست. هر واحد کدی که نرمال‌سازی تولید می‌کند، موقعیت شروع و پایان متن UTF-16 اصلی‌ای را که آن را تولید کرده ثبت می‌کند. تجزیه‌های بازگشتی، بازه منبع والد خود را به ارث می‌برند، ترکیب‌ها بازه‌های ورودی‌های خود را ادغام می‌کنند، و وقتی یک تطبیق یافت می‌شود، کتابخانه بازه نگاشت را برای کوچک‌ترین شروع و بزرگ‌ترین پایان می‌پیماید

اثر آن این است که MatchStart، MatchLength، رشته‌های بافت و هر دو نقطه ورودی جایگزینی، همگی همچنان به متن استخراج‌شده اصلی خطاب می‌شوند، نه به میانی نرمال‌شده. بدون آن نگاشت، یک جست‌وجوی نرمال‌شده می‌توانست به شما بگوید یک نتیجه وجود دارد اما نه به‌طور قابل‌اتکایی بگوید کجاست، که هایلایت‌کردن را نادرست و ردکشن (redaction) را خطرناک می‌کند

خود نرمال‌ساز خودکفاست: جدول‌های فشرده برای تجزیه قانونی، ترکیب و کلاس ترکیبی قانونی از یونیکد ۱۵.۱، با هانگول که با قواعد الگوریتمی به‌جای ورودی‌های جدول مدیریت می‌شود. هیچ چیزی از یک فایل داده خارجی بارگذاری نمی‌شود و هیچ API نرمال‌سازی سکو فراخوانی نمی‌شود، بنابراین یک سرویس ویندوز، یک دیمون لینوکس و یک ساخت FPC همگی روی همان ورودی نتایج یکسانی تولید می‌کنند

جست‌وجو با تعادل قانونی

گزینه‌ها یک مجموعه‌اند، بنابراین تعادل قانونی با رفتارهای موجود مانند تطبیق کل‌واژه، حرف‌جای‌خالی‌ها و تاخوردگی نسبت به علامت تشخیصی بی‌تفاوت ترکیب می‌شود:

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);                       // بازه صفحه خالی = کل سند

    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;

نرمال‌سازی به دلیلی opt-in است. ساخت متن NFD و نگاشت موقعیت آن هزینه دارد، و بیشتر جست‌وجوها روی اسناد فقط-ASCII هرگز به آن نیاز ندارند. وقتی گزینه استفاده می‌شود، هر بلوک متنی دو شکل تبدیل‌شده را کش می‌کند، یکی با نشانه‌های ترکیبی حذف‌شده و یکی بدون آن، بنابراین یک دسته از پرس‌وجوها روی همان بلوک یک‌بار نرمال می‌شود نه یک‌بار به ازای هر پرس‌وجو. تاخوردگی حروف کوچک/بزرگ همچنان مسیر ارزان‌تر یک‌به‌یک را بدون تغییر طی می‌کند

بدون مرزهای خوشه گرافیم چه چیزی می‌شکند؟

واحدهای کد نویسه نیستند، و نویسه‌ها آن چیزی نیستند که کاربران درک می‌کنند. یک اموجی پرچم دو نقطه‌کد نشانگر منطقه‌ای است. یک اموجی خانواده چندین نقطه‌کد است که با پیونددهنده‌های عرض-صفر به هم پیوسته‌اند. یک ترکیب هندی یک صامت، یک ویراما و صامت دیگری است. یک حرف با دو تکیه انباشته سه نقطه‌کد است. تطبیق یا برش‌دادن در میانه هر یک از این‌ها یک قطعه تولید می‌کند که مثل آشغال رندر می‌شود

soGraphemeClusters هر دو انتهای هر نتیجه، لفظی یا حرف‌جای‌خالی، را به مرزهای کامل خوشه گرافیم گسترش‌یافته محدود می‌کند. بخش‌بندی، قواعد گسترش‌یافته را پیاده می‌کند: جفت‌شدن CR و LF، نویسه‌های کنترلی، کلاس‌های هجای هانگول، Extend و SpacingMark، Prepend، دنباله‌های اموجی ZWJ، جفت‌شدن نشانگر منطقه‌ای و شکست‌های ترکیب هندی. یک مرز هرگز درون یک جفت جانشین (surrogate pair) تولید نمی‌شود، که به‌تنهایی یک کلاس کامل از نتایج خراب‌شده روی هر محتوایی فراتر از صفحه چندزبانه پایه را حذف می‌کند

این گزینه همچنین مصرف حرف‌جای‌خالی را حاکم می‌کند، که همان‌جایی است که یک پیاده‌سازی ساده‌لوحانه همچنان به‌شکل نادرستی برش می‌داد. حرف‌جای‌خالی تک‌نویسه دقیقاً یک خوشه کامل را پیش می‌برد، و بازگشت برای حرف‌جای‌خالی دنباله‌ای فقط بین مرزهای خوشه حرکت می‌کند:

// بدون soGraphemeClusters، "?" می‌تواند نیمی از یک خوشه را مصرف کند و
// نتیجه‌ای برگرداند که متنش به یک نشانه ترکیبی آویزان ختم می‌شود
Found := Lib.SearchText('c?té',
  [soWildcards, soCanonicalEquivalent, soGraphemeClusters], '', Hits);

// همان مرزها از جایگزینی محافظت می‌کنند، بنابراین ردکشن و
// بازنویسی محتوا هرگز یک اموجی یا یک حرف تکیه‌دار را نمی‌شکافند
Replaced := Lib.SearchAndReplaceText('naïve', 'plain',
  [soCanonicalEquivalent, soGraphemeClusters], '1-20');

انتخاب گزینه‌ها برای یک بار کاری واقعی

سه ترکیب بیشتر موارد را پوشش می‌دهند. برای یک جعبه جست‌وجوی سند داخلی، soCanonicalEquivalent به‌علاوه soDiacriticInsensitive رفتار بخشنده‌ای را می‌دهد که کاربران انتظار دارند، و هم اشکال رمزگذاری و هم املاهای تکیه‌دار و بدون تکیه را تطبیق می‌دهد. برای جست‌وجوی حقوقی یا انطباق، جایی که یک نتیجه مثبت کاذب هزینه دارد، از soCanonicalEquivalent با soCaseSensitive و soWholeWord استفاده کنید و تاخوردگی تکیه را خاموش نگه دارید، تا تعادل دقیق و مستقل از رمزگذاری باشد

برای هر چیزی که سند را تغییر می‌دهد، soGraphemeClusters را بدون استثنا اضافه کنید. جست‌وجویی که یک بازه اندکی نادرست برمی‌گرداند فقط خواننده را گمراه می‌کند؛ جایگزینی یا ردکشنی که از همان بازه نادرست استفاده می‌کند اشتباه را درون فایل می‌نویسد. پیامدهای نادرست‌گرفتن بازه‌های حذف در ردکشن واقعی و حذف محتوا پوشش داده شده

وقتی توان عملیاتی اهمیت دارد، نقاط ورودی دسته‌ای را ترجیح دهید. SearchTextBatch هر پرس‌وجوی غیرخالی را درحالی‌که بلوک‌های متنی هر صفحه مقیم هستند اجرا می‌کند، که از استخراج دوباره یک صفحه به ازای هر پرس‌وجو اجتناب می‌کند و نرمال‌سازی کش‌شده را دوباره استفاده می‌کند، و گونه‌های جریانی نتایج را بدون یک بافر با اندازه فراخوان منتشر می‌کنند. مدل استخراج زیرین در جست‌وجوی متن و شمارش عنصر صفحه شرح داده شده

رسم‌الخط‌هایی که در آن‌ها این اختیاری نیست

برای کره‌ای، تعادل قانونی همان تفاوت بین یافتن یک نام و نیافتن آن است، زیرا هجاهای پیش‌ترکیب‌شده و جاموی تجزیه‌شده هر دو در اسناد واقعی رایج‌اند. برای ویتنامی، علامت‌های تشخیصی انباشته، شکل ترکیب را کاملاً وابسته‌به‌تولیدکننده می‌کنند. برای رسم‌الخط‌های هندی، مدیریت ترکیب تعیین می‌کند که آیا یک مرز نتیجه در جایی خوانا فرود می‌آید. برای ژاپنی و چینی، سمت جست‌وجو نسبتاً ساده است، هرچند سمت چیدمان چنین نیست، همان‌طور که در نگارش عمودی برای ژاپنی و چینی شرح داده شده

قاعده سرانگشتی کوتاه است: اگر مجموعه‌داده هر زبانی غیر از انگلیسی داشته باشد، تعادل قانونی را روشن کنید و هزینه را پیش از تصمیم‌گیری درباره گران‌بودن آن اندازه‌گیری کنید. در بیشتر مجموعه‌های سند این‌طور نیست، و جایگزین آن یک ویژگی جست‌وجوست که به‌طور خاموش دقیقاً روی همان نام‌هایی شکست می‌خورد که کاربران شما بیشترین اهمیت را برای یافتن آن‌ها قائل‌اند

جست‌وجوی آگاه از یونیکد، استخراج، ردکشن و بازنویسی متن، یک موتور واحد را برای دلفی، C++Builder و Free Pascal به اشتراک می‌گذارند؛ فهرست کامل ویژگی‌ها در صفحه PDF Library for Delphi قرار دارد