PDFium Component מזהה טבלאות בעמוד PDF ומחזיר אותן כרשת תאים עם פרישות שורה ועמודה, שורות כותרת וערך ביטחון, דרך ExtractTables עבור עמוד אחד ו-ExtractDocumentTables עבור מסמך שלם. כל טבלה מומרת ל-CSV או JSON בקריאה אחת, וטבלאות שנמשכות מעבר למעבר עמוד ניתנות לקישור לשרשרת המשך
ל-PDF אין אובייקט טבלה. טבלה ב-PDF היא קבוצת ריצות טקסט הממוקמות כך שבן אדם קורא אותן כרשת, לפעמים עם קווים מצוירים סביבן ולעיתים קרובות בלעדיהם. שחזור הרשת פירושו לבנות מחדש כוונה שהקובץ מעולם לא רשם, וזו הסיבה שכל כלי חילוץ מייצר תוצאות מעט שונות ולמה כלי שאומר לך את הביטחון שלו שימושי יותר מכלי שלא
שני מצבי זיהוי לשני סוגי טבלה
זיהוי מבוסס-קווים משתמש בקווים המצוירים. כל מקטע נתיב מקווקו עובר טרנספורמציה למרחב קואורדינטות העמוד דרך מטריצת אובייקט העמוד, קווים אופקיים ואנכיים נחתכים, וההצטלבויות יוצרות רכיבים מחוברים. כל רכיב הופך לרשת מיוינת משלו של מיקומי X ו-Y, וזה מה ששומר על שתי טבלאות נפרדות באותו עמוד מלהתמזג לרשת חסרת היגיון אחת
זיהוי רווח-לבן מטפל בטבלאות שמצוירות עם יישור במקום קווים. תיבות מילים מקובצות לשורות חזותיות, פערים בתוך שורה מפצלים אותה לעמודות מועמדות, וטבלה מתקבלת רק כאשר לפחות MinRows שורות חוזרות עם לפחות MinColumns עוגני יישור-שמאלה בתוך AlignmentTolerance. גורם פער השורה כברירת מחדל הוא 3, מה שמכסה את המרווח בין שורות בסיס של כ-30 נקודות האופייני לטקסט בגודל 12 נקודות בלי לאפשר לשורה בודדת שמכילה כמה ריצות טקסט להתחזות לטבלה
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfTableExtractionOptions;
Tables: TPdfTables;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'annual-report.pdf';
Pdf.LoadDocument;
Pdf.PageNumber := 12; // 1-based
Options := TPdfTableExtractionOptions.Default;
Options.DetectRuledTables := True;
Options.DetectWhitespaceTables := True;
Options.MinConfidence := 0.6; // default is 0.5
Options.HeaderRowCount := 1;
Tables := Pdf.ExtractTables(Options);
for I := 0 to High(Tables) do
Writeln(Format('table %d: %dx%d cells, confidence %.2f, mode %d',
[I, Tables[I].RowCount, Tables[I].ColumnCount,
Tables[I].Confidence, Ord(Tables[I].DetectionMode)]));
if Length(Tables) > 0 then
SaveText('page12-table0.csv', Tables[0].ToCsv);
finally
Pdf.Free;
end;
end;
איך תאים ממוזגים משוחזרים?
זה החלק שכלי חילוץ נאיביים טועים בו. תא ממוזג לא ניתן לזיהוי מהרשת הגלובלית בלבד, מכיוון שהרשת נגזרת מכל הקווים בעמוד, ואזור ממוזג פשוט חסר את הקו הפנימי שהיה מפריד אותו
הכלל שמשמש כאן מקומי: שני תאי בסיס סמוכים ממוזגים כאשר שום קו גבול לא מכסה את המרווח ביניהם. Union-find מחבר אותם, הרכיבים המלבניים המתקבלים הופכים לערכי RowSpan ו-ColumnSpan, וטקסט מוקצה לתא בסיס לפי נקודת המרכז שלו ואז עוקב אחר התא הזה לשורש המיזוג שלו. עשיית זה בדרך הזו גם שומרת על העלות ליניארית במילים בתוספת תאים, במקום הסריקה הריבועית שמתקבלת מבדיקת כל מילה כנגד כל תא
ההשפעה המעשית היא שטבלה פיננסית עם כותרת "Total" ממוזגת הפרושה על פני שלוש עמודות יוצאת עם תא אחד בעל פרישה של שלוש, ולא תא מאוכלס אחד ושני תאים ריקים מסתוריים
המשכיות בין עמודים
טבלאות ארוכות נשברות בין עמודים, וטיפול בקטע של כל עמוד כטבלה עצמאית מחייב את הקורא לתפור אותם. ExtractDocumentTables יכול לקשר אותם במקום זאת, אך רק תחת תנאים מחמירים: הקטע חייב להיות הטבלה התחתונה ביותר בעמוד המוקדם יותר, הבא חייב להיות הטבלה העליונה ביותר בעמוד הבא, מספרי העמודים חייבים להיות סמוכים, וגבולות העמודות חייבים להתאים
כל ארבעת התנאים יחד הם מה שמונע את הטעות הברורה, שהיא שרשור כל טבלה בת ארבע עמודות במסמך לתוך מגה-טבלה דמיונית אחת מכיוון שהן במקרה חולקות מספר עמודות. כאשר התנאים מתקיימים, הטבלאות חולקות מזהה קבוצת המשך ונושאות מטא-נתוני המשך; כאשר הם לא, אתה מקבל טבלאות נפרדות ויכול להחליט בעצמך
חילוץ ברמת מסמך חולק את תקציבי MaxCells ו-MaxTables על פני עמודים במקום לאפס אותם לכל עמוד, והוא משחזר את העמוד הפעיל בבלוק finally, כך שחילוץ שרץ במציג משאיר את המשתמש מסתכל על העמוד שהיה בו
ייצוא בלי לשבש את הנתונים
שני המייצאים מדוקדקים לגבי בריחה (escaping). CSV תמיד נותן מירכאות לשדות ומכפיל מירכאות פנימיות, מה שנמנע מהכשל הקלאסי שבו תא המכיל פסיק הופך בשקט לשתי עמודות. עבור תאים ממוזגים, תוכן נפלט רק בעוגן הפינה השמאלית-עליונה, כך שסבב הלוך-חזור של CSV לא משכפל כותרת פרושה על פני העמודות שהיא מכסה
JSON משמר יוניקוד במקום לתת לו בריחה ל-ASCII, נותן בריחה לתווי בקרה, וכולל את המטא-נתונים שצרכן זקוק להם כדי לשפוט איכות: מצב זיהוי, ביטחון, גבולות, ערכי פרישה, דגלי כותרת ומידע המשכיות. אם אתה מזין טבלאות מחולצות למערכת במורד הזרם, העדף JSON, מכיוון ששורת CSV לא יכולה לומר לך שהטבלה שממנה היא הגיעה קיבלה ציון ביטחון של 0.51:
// Document-wide extraction, keeping only tables worth trusting
Tables := Pdf.ExtractDocumentTables(Options);
for I := 0 to High(Tables) do
begin
if Tables[I].Confidence < 0.75 then
begin
Log(Format('page %d table needs review (%.2f)',
[Tables[I].PageNumber, Tables[I].Confidence]));
Continue;
end;
if Tables[I].ContinuationGroup > 0 then
AppendToGroup(Tables[I].ContinuationGroup, Tables[I].ToJson)
else
EmitStandalone(Tables[I].ToJson);
end;
כוונון, וידיעה מתי להפסיק
שלוש הגדרות חשובות יותר מהשאר. MinConfidence הוא שער האיכות, ו-0.5 סלחני במכוון; העלה אותו עבור קליטה אוטומטית והנמך אותו עבור ממשק בדיקה שבו בן אדם מאשר כל תוצאה. MinColumnGap מחליט מה נחשב לגבול עמודה במצב רווח-לבן, וטבלאות עם מרווח צמוד בדוחות צפופים עשויות לזקוק לו מונמך מברירת המחדל של 12 נקודות. MaxRowGapFactor מחליט מתי מרחק אנכי מסיים טבלה, וזה חשוב לטבלאות עם שורות ריקות מדי פעם
היה כן לגבי המגבלות. טבלאות מבוססות-קווים מחולצות באופן אמין. טבלאות רווח-לבן מיושרות בניקיון מחולצות היטב. טבלאות עם טקסט מסובב, טבלאות מקוננות, או תאים שהתוכן שלהם עוטף למה שנראה כמו שורה נוספת יזקקו לבדיקה ללא תלות באיך הפרמטרים מוגדרים. עבור אלה, מודל הטקסט המובנה נותן לך את חומר הגלם לבניית קורא ספציפי-לתחום, המתואר בבלוקי טקסט מובנה וסדר קריאה
צימוד שימושי אחד: כאשר מסמך סרוק אין בו טקסט כלל, לזיהוי טבלאות אין עם מה לעבוד עד שקיימת שכבת טקסט. הוסף אחת תחילה, כמתואר בהוספת שכבת טקסט ניתנת לחיפוש ל-PDF סרוקים, ואז חלץ. תיבות המילים שספק OCR מחזיר הן בדיוק הקלט שזיהוי רווח-לבן זקוק לו
חילוץ טבלאות, טקסט מובנה וזרימה מחדש כולם קוראים מאותו מודל עמוד ב-Delphi, C++Builder ו-Lazarus; ה-API המלא מתואר בעמוד PDFium Component ל-Delphi