HotPDF משחזרת טבלאות מתוך PDF קיים דרך ExtractLoadedTypedTables, API של Delphi שממזג את מקטעי השורות שמייצר מעבר הפריסה, בונה רשת עמודות קנונית אחת לכל טבלה, ממשיך את הטבלה מעבר למעבר עמוד כאשר הגיאומטריה תומכת בכך ומחזיר כל תא כערך מטופס שנושא provenance של עמוד, span של עמודות וגבולות. ExportLoadedTypedTables כותבת את אותה תוצאה ישירות ל-CSV או JSON. התרחיש שהופך את זה לשווה בנייה משעמם ונפוץ מאוד. רשם חשבוניות בן ארבעים עמודים, טבלה אחת מבחינה לוגית, מודפס עם כותרת שחוזרת בראש כל עמוד. מריצים עליו מעבר reading-order נאיבי ומקבלים ארבעים טבלאות, 39 שורות כותרת מזויפות ועמודת מטבע שמחליקה מיקום אחד שמאלה בכל שורה שבה התא האמצעי במקרה היה ריק. ניקוי זה downstream, בתוך האפליקציה הקוראת, הוא המקום שבו פרויקטי יבוא מסמכים מתים
מדוע עמוד PDF נותן לכם מקטעים ולא טבלה
מפני שלעמוד PDF אין סמנטיקה של טבלה כלל, אלא אם המסמך מתויג. זרם התוכן מחזיק אופרטורים שמציגים טקסט ומטריצות מיקום (ISO 32000-1 §9.4.3) ולא דבר נוסף; תיבת הקווים שרואים על המסך היא ציור path שאינו קשור לטקסט ואף מחלץ אינו חייב לקשר ביניהם. סוגי רכיבי המבנה Table, TR, TH ו-TD חיים רק בהיררכיית המבנה הלוגי של PDF מתויג (ISO 32000-1 §14.8.4), והרוב המכריע של מסמכים עסקיים שבמחזור אינו מתויג. כל מה שמתואר להלן הוא שחזור גיאומטרי ולא parsing, וכדאי לומר זאת בקול לפני שמישהו בונה עליו דוח reconciliation
לכן HotPDF מריצה קודם ניתוח layout סמנטי על הגליפים שחולצו, אותו מעבר שמאחורי חילוץ טקסט לפי סדר מבני מ-PDF טעון וייצואי HTML ו-XML מובנים. המעבר מקבץ קווי בסיס לריצות שהתאים בהן מיושרים אנכית, והוא ממשיך ריצה רק כל עוד לשורות עוקבות יש אותו מספר תאים. עבור מנוע layout זה כלל נכון וזול. עבור caller זה shape שגוי: שורה יחידה עם תא פנימי ריק מפצלת טבלה חזותית אחת לשתי טבלאות מקור. שכבת הטבלאות המטופסות יושבת מעל המעבר הזה בדיוק כדי לחבר את החלקים בחזרה
רשתות עמודות קנוניות וכפתור ColumnTolerance
ExtractLoadedTypedTables ממזגת מקטעים באותו עמוד לפני שהיא עושה דבר אחר, והיא ממזגת לפי גיאומטריית עמודות ולא לפי טקסט שורות. שתי טבלאות מקור סמוכות באותו עמוד מתחברות כאשר לכל אחת יש לפחות שתי עמודות, כאשר הפער האנכי בין השורה האחרונה של הראשונה לשורה הראשונה של השנייה נשאר בתוך רצועת הסבילות וכאשר מיקומי תחילת העמודות שלהן מיושרים. התחלות עמודה שבטווח ColumnTolerance זו מזו מתכווצות לעמודה קנונית אחת וממוצעות בזמן האיחוד. סבילות ברירת המחדל היא 12 יחידות user-space, מה שמתאים לטיפוגרפיה עסקית רגילה ודורש העלאה בפריסות בעלות tracking רחב או indentation עמוק
מה שקורה לשורה שחסר בה ערך פנימי הוא החלק החשוב. HotPDF מצמידה כל תא להתחלת העמודה הקנונית הקרובה ביותר ואז קובעת את ColumnSpan לפי המרחק מהעמודה הזו לעמודה התפוסה הבאה, במקום להזיז את התאים הנותרים שמאלה. שורה בת שלושה תאים ברשת של חמש עמודות משאירה את הערכים מתחת לכותרות הנכונות ומתעדת בדיוק היכן נמצאים הפערים. זהו ההבדל בין טבלה שאפשר לבצע לה reconciliation לבין טבלה שמייחסת כסף בשקט לעמודה הלא נכונה
var
Pdf: THotPDF;
Options: THPDFTypedTableExtractionOptions;
Tables: THPDFTypedTables;
Info: THPDFTypedTableExtractionInfo;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('register.pdf', '') <= 0 then
Exit;
Options := THPDFTypedTableExtractionOptions.Default;
Options.ColumnTolerance := 12; // יחידות user-space
Options.MinimumTableConfidence := 0.55; // מתחת לזה הטבלאות מושמטות
Options.DateOrder := ttdoDMY; // 03/04/2026 הוא 3 באפריל
Options.DecimalSeparator := ',';
Options.ThousandsSeparator := '.';
if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
// Info.TableCount מול Info.SourceTableCount מראה כמה מוזג
ProcessTables(Tables)
else if Info.Status = ttesBudgetExceeded then
Log(string(Info.Diagnostic));
finally
Pdf.Free;
end;
end;
מה באמת מבטיח מיזוג בין עמודים
הוא מבטיח שמרנות, בכוונה. HotPDF מחברת שתי טבלאות מעבר לגבול עמוד רק כאשר MergeAcrossPages מופעל, כאשר הטבלה השנייה מתחילה בדיוק באינדקס העמוד שאחרי סוף הראשונה, כאשר לשתיהן לפחות שתי עמודות וכאשר לפחות שתי התחלות עמודה קנוניות מיושרות בתוך ColumnTolerance. תנאי העמודים העוקבים הוא נושא המשקל. Callers מעבירים את PageIndices כ-open array בכל סדר שירצו, וללא הבדיקה הזו בקשה לעמודים 3, 9 ו-14 יכולה לרתך שלוש טבלאות לא קשורות לתוצאה אחת שנראית סבירה לחלוטין. המחיר הוא שהמשך אמיתי שמדלג על עמוד, נספח משולב או סריקה דו-צדדית עם צד אחורי ריק חוזרים כשתי טבלאות, ושום option אינו מרפה את זה. חיבור מחדש של אלה הוא החלטת policy שרק האפליקציה הקוראת יכולה לקבל, ולכן ה-API חושפת FirstPageIndex, LastPageIndex, SourceTableCount ו-PageIndex לכל שורה ומשאירה את ההחלטה היכן שהיא שייכת
כותרות חוזרות מסומנות ולעולם לא נמחקות
ExtractLoadedTypedTables לעולם אינה מסירה שורת כותרת חוזרת מהתוצאה. כאשר מיזוג בין עמודים מוצא שהטבלה הנכנסת נפתחת בטקסט כותרת זהה לטבלה שנצברה, לאחר trim ו-case folding, היא מסמנת את השורות כ-IsHeader וכ-IsRepeatedHeader ומוסיפה אותן בכל זאת בסדר המקור. מחיקה היא החלטה מאבדת ובלתי הפיכה, ולצרכנים שונים נדרשות תשובות שונות: יבוא CSV רוצה שהחזרות ייעלמו, audit trail רוצה שהן יישארו עם מספרי העמודים שלהן וכלי diff רוצה שסדר המקור יישמר בייט-לבייט. לכן הספרייה מדווחת וה-caller מחליט
var
T, R, C: Integer;
Row: THPDFTypedTableRow;
Total: Double;
begin
Total := 0;
for T := 0 to High(Tables) do
for R := 0 to High(Tables[T].Rows) do
begin
Row := Tables[T].Rows[R];
if Row.IsRepeatedHeader then
Continue; // שמור רק את בלוק הכותרת הראשון
for C := 0 to High(Row.Cells) do
if Row.Cells[C].ValueKind = ttvkCurrency then
Total := Total + Row.Cells[C].NumberValue;
end;
end;
ערכים מטופסים והמפרידים שאתם חייבים לספק
הסקת הטיפוס רצה בסדר קבוע שפותר את העמימויות בכיוון ההגיוני היחיד: boolean קודם, אחריו date, אחריו percentage, אחריו currency, אחריו מספר רגיל, וכל מה שלא התאים נשאר מחרוזת. הסדר הוא מה שמונע מ-2026 בעמודת תאריך להיקבע על ידי parser של מספר לפני שה-parser של תאריך רואה אותו. מטבע מזוהה מ-$, £, ¥ או € מובילים, או מקוד ISO 4217 בן שלוש אותיות שאחריו רווח, והקוד נשמר ב-CurrencyCode. חשוב מכך, HotPDF אינה מנחשת את ה-locale שלכם. DecimalSeparator, ThousandsSeparator ו-DateOrder מגיעים מה-options, מפני ש-1.234 הוא או מספר אחד או אלף מאתיים ושלושים וארבעה, לפי עובדה שה-PDF אינו מכיל. ה-Text הגולמי ב-Unicode נשמר בכל תא לצד הערך המטופס, ולכן ניחוש שגוי תמיד ניתן לשחזור ללא מעבר חילוץ נוסף
var
Stream: TFileStream;
Info: THPDFTypedTableExtractionInfo;
begin
Stream := TFileStream.Create('tables.json', fmCreate);
try
if not Pdf.ExportLoadedTypedTables([0, 1, 2], ttefJSON,
Stream, Options, Info) then
case Info.Status of
ttesInvalidOptions: ReportBadConfiguration;
ttesBudgetExceeded: ReportOversizedDocument;
ttesCancelled: ReportUserCancelled;
ttesWriteFailed: ReportDestinationProblem;
else
ReportExtractionFailure;
end;
finally
Stream.Free;
end;
end;
שני פורמטי הייצוא עונים על שאלות שונות ובכוונה אינם שקולים. CSV כותב את עמודות ההמשך של span ממוזג כשדות ריקים, וזה מה שגיליון אלקטרוני או loader המוני מצפים לו. JSON שומר כל מה שהחילוץ ידע: את הערך המטופס תחת הסוג שלו, columnSpan, confidence לכל תא ולכל שורה, גבולות התא ו-provenance של העמוד ושל טבלת המקור. שני הפורמטים מכינים את כל המסמך במאגר זיכרון מוגבל ורק אז מפרסמים לזרם היעד, תוך שחזור הבתים, האורך והמיקום המקוריים אם הכתיבה נכשלת באמצע, כך שייצוא כושל לעולם אינו משאיר קובץ שנכתב למחצה. תקציבים לעמודים, גליפים לעמוד, טבלאות, שורות, תאים, תווים ובתי פלט נספרים בנפרד, ושורות נספרות לפני ההקצאה מפני ש-SetLength לכל שורה מידרדר להעתקה ריבועית הרבה לפני תקרת ברירת המחדל של מיליון שורות
היכן שחזור טבלאות גיאומטרי מוותר
הבהירות לגבי מצבי הכשל שימושית יותר מרשימת תכונות, מפני שכל אחד מאלה הוא מקום שבו caller זקוק למדיניות משלו ולא לערך option טוב יותר
- מיזוגים אנכיים אינם משוחזרים. HotPDF מדווחת על
ColumnSpanעבור spans אופקיים ומשאירה אתRowSpanעל 1, לכן תא שמשתרע על שלוש שורות בטבלה המודפסת מגיע כתא אחד ועוד שני gaps - זיהוי כותרת מונחה-נתונים ולא חזותי. בלוק הכותרת הוא ריצת השורות שלפני השורה הראשונה שמכילה ערך מטופס שאינו מחרוזת, ולכן טבלה שגופה כולו טקסט מדווחת
HeaderRowCountכאפס בלי קשר לעיצוב שלה - טבלאות שמתחת ל-
MinimumTableConfidenceמושמטות מהתוצאה ללא שגיאה. השוו אתInfo.TableCountל-Info.SourceTableCountכאשר אתם צריכים לדעת שמשהו הושלך - ריצה זקוקה לפחות לשתי שורות ולפחות לשתי עמודות לפני שמעבר ה-layout יקרא לה טבלה בכלל, לכן pseudo-table של שורה אחת או פריסת שתי עמודות של prose ארוך אינן טבלה, בצדק ובאופן לא שימושי
- עמודים סרוקים אינם מכילים אופרטורים של טקסט, לכן אין דבר לשחזר גיאומטרית עד שקיימת בעמוד שכבת טקסט של OCR
אם קובצי ה-PDF שלכם יוצאים ממחסנית דוחות שבניתם בעצמכם, התיקון הזול ביותר לכל זה הוא upstream: להפיק טבלאות מתויגות או לשמור את נתוני המקור, ולהתייחס לחילוץ כ-fallback עבור מסמכים שלא אתם הפקתם. עבור כל השאר כדאי ללמוד את הצינור בסדר הזה, מפני שכל שכבה נבנית על התחתונה: מתחילים בחילוץ טקסט פשוט מ-PDF טעון, עולים ל-API של טבלאות מטופסות כאשר צריך לשמר את הגיאומטריה ומסתכלים על רינדור טבלת נתונים ל-PDF חדש כאשר אתם בצד המפיק ויכולים להחליט עד כמה הפלט יהיה ניתן לשחזור
ExtractLoadedTypedTables ו-ExportLoadedTypedTables נשלחות כחלק מHotPDF Delphi PDF Component הטבעי עבור Delphi ו-C++Builder, ללא DLL חיצוני וללא תלות runtime; דף המוצר נושא את ההפניה המלאה לאפשרויות, לסטטוסים ולרשומות של API הטבלאות המטופסות