PDF Library for Delphi יכולה להתאים טקסט לפי שוויון-ערך קנוני במקום לפי יחידת קוד, כך ששאילתה שהוקלדה כתו מורכב-מראש מוצאת תוכן שמאוחסן כאות בסיס בתוספת סימן משלב, ולהפך. שתי אפשרויות חיפוש שולטות בכך: soCanonicalEquivalent מפעילה נרמול יוניקוד במהלך התאמה, ו-soGraphemeClusters מגבילה כל תוצאה וכל צעד תבנית לאשכולות גרפמות שלמים
הבאג שזה מתקן הוא אחד מהמדווחים ביותר והמובנים פחות ביותר בחיפוש מסמכים. משתמש מחפש שם, לא רואה תוצאות, מעתיק את השם מהמסמך, מדביק אותו בתיבת החיפוש, ומוצא אותו. שום דבר לא שבור באופן ברור: שתי המחרוזות נראות זהות, מודפסות זהה, ומושוות כלא-שוות, מכיוון שאחת היא U+00E9 והשנייה היא U+0065 ואחריה U+0301
למה אותה מילה מושווית כלא-שווה?
יוניקוד מתיר כמה קידודים לאותו תו מופשט. אותיות לטיניות עם דיאקריטיקה קיימות כנקודות קוד מורכבות-מראש וכרצפים של בסיס בתוספת משלב. הברות הנגול קיימות כהברות מורכבות-מראש וכג'אמו מפורק. איזה מהם PDF מכיל תלוי במפיק, בפלטפורמה, ולפעמים בגופן, ושום דבר מזה לא גלוי לאדם שמבצע את החיפוש
הסיבה שקיפול רישיות פשוט לא פותר את זה היא מבנית ולא מקרית. קיפול רישיות וקיפול הטעמות הם אחד-לאחד ברמת יחידת הקוד: המחרוזת המקופלת נושאת את אותו אורך כמו המקורית, כך שמיקום התאמה בטקסט המקופל הוא מיקום התאמה במקור. נרמול אינו אחד-לאחד. תו מורכב-מראש אחד הופך לשתי או שלוש יחידות קוד, רצף מפורק מתקפל בחזרה לאחד, ואחרי הטרנספורמציה הזו, המיקומים כבר לא מתיישרים עם הטקסט שחילצתם
שמירה על קואורדינטות תוצאה שמצביעות על הטקסט המקורי
זהו החלק שקובע אם חיפוש מנורמל שמיש ולא רק נכון. כל יחידת קוד שנרמול מפיק רושמת את מיקום ההתחלה והסוף של טקסט ה-UTF-16 המקורי שהפיק אותה. פירוקים רקורסיביים יורשים את טווח המקור של ההורה שלהם, הרכבות ממזגות את הטווחים של הקלטים שלהן, וכשנמצאת התאמה, הספרייה סורקת את מרווח המיפוי עבור ההתחלה הקטנה ביותר והסוף הגדול ביותר
האפקט הוא ש-MatchStart, MatchLength, מחרוזות ההקשר ושתי נקודות הכניסה להחלפה כולן ממשיכות להתייחס לטקסט שחולץ המקורי, לא לביניים המנורמל. בלי המיפוי הזה, חיפוש מנורמל היה יכול לומר לכם שתוצאה קיימת אבל לא באמינות איפה היא הייתה, מה שהופך הדגשה לשגויה וגריעה למסוכנת
המנרמל עצמו עצמאי: טבלאות קומפקטיות לפירוק קנוני, הרכבה ומחלקת שילוב קנונית מיוניקוד 15.1, כשהנגול מטופל על ידי הכללים האלגוריתמיים ולא על ידי רשומות טבלה. שום דבר לא נטען מקובץ נתונים חיצוני ושום API נרמול פלטפורמה לא נקרא, כך ששירות Windows, דמון Linux ובנייה של 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 של אמוג'י, זיווג מחווני אזורי ושברי צירוף הודי. גבול לעולם לא מופק בתוך זוג שליח, מה שלבדו מבטל מחלקה שלמה של תוצאות מושחתות בכל תוכן מעבר למישור הרב-לשוני הבסיסי
האפשרות גם שולטת בצריכת תבנית, שזה המקום שבו מימוש נאיבי היה עדיין חותך באופן שגוי. תבנית התו-הבודד מתקדמת בדיוק אשכול שלם אחד, ומעקב-אחורי עבור תבנית הרצף זז רק בין גבולות אשכול:
// בלי 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