يكتشف 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
Options := TPdfTableExtractionOptions.Default;
Options.DetectRuledTables := True;
Options.DetectWhitespaceTables := True;
Options.MinConfidence := 0.6; // الافتراضي 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، ويُخصَّص النص إلى خلية أساسية بنقطة مركزها ثم يتبع تلك الخلية إلى جذر دمجها. وفعل ذلك بهذه الطريقة يُبقي أيضًا التكلفة خطية في عدد الكلمات زائد الخلايا، بدلاً من المسح التربيعي الذي تحصل عليه من اختبار كل كلمة مقابل كل خلية
والأثر العملي هو أن جدولاً ماليًا بترويسة "الإجمالي" مدمجة تمتد عبر ثلاثة أعمدة يخرج بخلية واحدة بامتداد ثلاثة، بدلاً من خلية واحدة مملوءة وخليتين فارغتين غامضتين
الاستمرار عبر الصفحات
تنكسر الجداول الطويلة عبر الصفحات، ومعاملة جزء كل صفحة كجدول مستقل يُجبر المستدعي على خياطتها معًا. ويمكن لـ ExtractDocumentTables ربطها بدلاً من ذلك، لكن بشروط صارمة فقط: يجب أن يكون الجزء أدنى جدول في الصفحة السابقة، ويجب أن يكون التالي أعلى جدول في الصفحة اللاحقة، ويجب أن تكون أرقام الصفحات متجاورة، ويجب أن تتطابق حدود الأعمدة
والشروط الأربعة معًا هي ما يمنع الخطأ الواضح، وهو ربط كل جدول من أربعة أعمدة في مستند في جدول عملاق وهمي واحد لمجرد أنها تصادف تشارك عدد الأعمدة. وعندما تتحقق الشروط، تشترك الجداول في معرّف مجموعة استمرار وتحمل بيانات وصفية للاستمرار؛ وعندما لا تتحقق، تحصل على جداول منفصلة ويمكنك أن تقرر بنفسك
ويشترك الاستخراج على مستوى المستند في ميزانيتي MaxCells و MaxTables عبر الصفحات بدلاً من إعادة ضبطهما لكل صفحة، ويستعيد الصفحة النشطة في كتلة finally، بحيث يترك تشغيل استخراج في عارض المستخدم ينظر إلى الصفحة التي كان عليها
التصدير دون إفساد البيانات
وكلا المُصدِّرين حريص عمدًا على الهروب من الرموز الخاصة. يضع CSV دائمًا الحقول بين علامات اقتباس ويضاعف علامات الاقتباس الداخلية، وهذا يتجنب الفشل الكلاسيكي حيث تصبح خلية تحتوي فاصلة عمودين بصمت. وبالنسبة للخلايا المدمجة، يُصدَّر المحتوى فقط عند المرساة العلوية اليسرى، بحيث لا تكرر دورة CSV ترويسة ممتدة عبر الأعمدة التي تغطيها
ويحافظ JSON على Unicode بدلاً من الهروب منه إلى ASCII، ويهرب من أحرف التحكم، ويتضمن البيانات الوصفية التي يحتاجها المستهلك للحكم على الجودة: وضع الاكتشاف، والثقة، والحدود، وقيم الامتداد، وأعلام الترويسة، ومعلومات الاستمرار. وإذا كنت تُغذّي جداول مستخرَجة إلى نظام لاحق، فضّل JSON، لأن صف CSV لا يمكنه إخبارك بأن الجدول الذي جاء منه سجّل ثقة 0.51:
// استخراج على مستوى المستند بأكمله، مع الاحتفاظ فقط بالجداول الجديرة بالثقة
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؛ وتُوصف الواجهة البرمجية الكاملة على صفحة PDFium Component لـ Delphi