مقال تقني

استخراج نص PDF الهيكلي في Delphi باستخدام PDFium VCL

تُعيد PDFiumPas نص الصفحة كبنية بدلاً من سلسلة نصية. تُنتج GetStructuredText صفحة TPdfStructuredTextPage تحتوي على كتل، تحمل كل واحدة منها أسطراً، ويحمل كل سطر مقاطع منسَّقة، مع حدود بمساحة الصفحة عند كل مستوى، وفهارس الأحرف المصدرية محفوظة بحيث يمكن ربط أي جزء بصفحة النص الأساسية

استخراج السلسلة النصية المسطَّحة الذي يبدأ به معظم الكود لا يزال موجوداً وصحيحاً لغرضه. يتوقف عن الكفاية في اللحظة التي تحتاج فيها إلى معرفة أي الكلمات كانت عنواناً، أو أيها ينتمي إلى العمود الأيسر، أو أين على الصفحة تقع مطابقة ما فعلياً

لماذا تكون السلسلة النصية المسطَّحة الإخراج الخاطئ لمعظم المهام؟

لأن الأسئلة التي يطرحها الناس على النص المستخرَج نادراً ما تكون «ما الأحرف الموجودة في هذه الصفحة». إنها «ما العنوان»، «هل هذا جدول»، «هل تنتمي هذه الفقرة إلى القسم 4»، «أين أرسم التظليل». لا تجيب سلسلة نصية واحدة عن أي منها، وكل إجابة تعيد بناءها منها هي أسلوب تقريبي تملكه أنت الآن

تجسّد التخطيطات ذات العمودين هذه النقطة. استخرج مقالاً بعمودين كسلسلة نصية، وحسب الطريقة التي كتب بها المولّد تيار المحتوى، قد تحصل على العمود الأول متبوعاً بالعمود الثاني، أو قد تحصل على السطر الأول من العمود الأول، ثم السطر الأول من العمود الثاني، ثم السطر الثاني من العمود الأول، وهكذا نزولاً في الصفحة. كلاهما يخرج من ملف PDF مطابق. لا شيء منهما خطأ على مستوى الصيغة، لأن PDF يصف علامات على صفحة، لا مخطط مستند. يسمح النموذج القائم على الكتل للمستخرِج باتخاذ قرار الترتيب صراحة وإخبارك بأي قرار اتخذه

ترتيب المحتوى أم التخطيط الفيزيائي؟

يختار TPdfStructuredTextOptions.ReadingOrder بين roContentOrder وroPhysicalLayout، وتعتمد الإجابة الصحيحة على ما تثق به أكثر، المولّد أم الهندسة

يُعيد ترتيب المحتوى النص بالتسلسل الذي يرسمه به تيار المحتوى. هذا سريع، وبالنسبة للمستندات المولَّدة من قِبل مولّد حسن السلوك، هو عادة ترتيب القراءة المقصود. يتجاهل التخطيط الفيزيائي تسلسل التيار ويعيد بناء الترتيب من مكان جلوس الأحرف فعلياً، متجمِّعة في أسطر ثم في أعمدة. هذا ما تريده للصفحات الممسوحة ضوئياً ثم المُخضعة لـ OCR، ولمخرجات الأدوات التي تصدر النص بترتيب الخط بدلاً من ترتيب القراءة، ولأي شيء تكون فيه النتيجة البصرية هي الشيء الوحيد الذي يمكنك الاعتماد عليه

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfStructuredTextOptions;
  Page: TPdfStructuredTextPage;
  B, L: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'article.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 1;                     // يبدأ من 1

    Options := TPdfStructuredTextOptions.Default;
    Options.ReadingOrder := roPhysicalLayout;
    Options.IncludeFontInfo := True;
    Options.IncludeSemantics := True;
    Options.MaxCharacters := 200000;         // ميزانية بمبدأ الفشل المغلق

    Page := Pdf.GetStructuredText(Options);

    for B := 0 to High(Page.Blocks) do
    begin
      if Page.Blocks[B].Kind = cfHeading then
        Emit(Format('H%d: %s',
          [Page.Blocks[B].HeadingLevel, Page.Blocks[B].Text]))
      else
        for L := 0 to High(Page.Blocks[B].Lines) do
          Emit(Page.Blocks[B].Lines[L].Text);
    end;
  finally
    Pdf.Free;
  end;
end;

ماذا يضيف الوسم غير قادر عليه الهندسة؟

النية. مع تفعيل IncludeSemantics، تحمل الكتل من مستند PDF موسوم Kind مستمدة من شجرة البنية، بحيث يكون العنوان عنواناً لأن المولّد قال ذلك، لا لأن خطه كان أكبر من المتوسط. تغطي الأنواع الأشكال المهمة لإعادة الاستخدام: cfParagraph، وcfHeading مع HeadingLevel، وcfListItem، وcfTableCell، وcfCaption، وcfFigure، والقيمة الاحتياطية غير الموسومة cfPlain

يسجّل حقل Source من أين جاء كل تصنيف، rosStructure لشجرة البنية وrosHeuristic للاستنتاج، وهو الحقل الذي تسجّله عندما تقرر إلى أي مدى تثق بخط أنابيب استخراج عبر مجموعة مستندات. الأشكال التوضيحية حالة خاصة تستحق المعرفة: بالنسبة لكتلة cfFigure يأتي النص من الوصف البديل بدلاً من أي غليفات، إذ لا يملك الشكل التوضيحي أحرفاً خاصة به. لا يزال النص البديل غير المطابَق يُمثَّل بدلاً من إسقاطه، وهو ما يتيح لتدقيق إمكانية الوصول رؤية وجود وصف حتى عندما لا يرسم شيء على الصفحة شيئاً. نموذج الوسم نفسه موضح في التحقق من شجرة بنية PDF/UA

تحمل المقاطع التنسيق والمنشأ

يحمل كل TPdfStructuredTextSpan نصه، وحدوده بمساحة الصفحة، وFontName وFontSize وFontWeight وAngle، بالإضافة إلى SourceStartIndex وSourceCharacterCount. تنكسر المقاطع حيث يتغير التنسيق، بحيث تصبح جملة بها ثلاث كلمات عريضة ثلاثة مقاطع، وإعادة بناء التشديد في HTML أو Markdown مسألة قراءة خصائص بدلاً من التخمين من أسماء الخطوط

حقلا فهرس المصدر هما ما يحوّلان الاستخراج إلى ميزة لا مجرد تقرير. يشيران إلى الخلف نحو تسلسل أحرف الصفحة، مما يعني أن كتلة طابقتها في بحث يمكن تحويلها إلى هندسة تحديد على مستوى الحرف أو مستطيل تظليل دون تمريرة ثانية مختلفة الترتيب على النص؛ الآلية موضحة في تحديد سطر النص البصري بمربعات الأحرف. حقل Angle أهم مما يبدو: النص المُدار في ختم أو علامة مائية يقع في مساحة الإحداثيات نفسها لنص المتن، وخط أنابيب يتجاهل الزاوية سيدمج بسعادة كلمة «DRAFT» قطرية في منتصف فقرة

الميزانية، وعدادا الجودة

MaxCharacters ميزانية بمبدأ الفشل المغلق، لا إعداد اقتطاع: تتوقف الصفحة التي تتجاوزها بدلاً من إعادة جزء من المحتوى بصمت. في مسار استيعاب غير موثوق هذا هو السلوك الذي تريده، لأن صفحة بمليون حرف إما وحش مولَّد آلياً أو محاولة لجعل مستخرِجك أبطأ جزء في النظام

عدادان في الصفحة المُعادة يصفان جودة الاستخراج مباشرة. يحسب UnmappedCharacterCount الأحرف التي لا تملك تخطيط يونيكود قابلاً للاستخدام، وهو العرَض الكلاسيكي لخط جزئي مضمَّن دون /ToUnicode CMap؛ نص من هذا النوع يُعرض بشكل مثالي ويُستخرج كشيء عديم الفائدة. يحسب GeometryFailureCount الأحرف التي تعذَّر تحديد مربعها المحيط، مما يُضعف ترتيب التخطيط الفيزيائي. سجّل كليهما. مجموعة مستندات تكون فيها هذه الأرقام قريبة من الصفر باستمرار يمكن فهرستها بثقة، ومجموعة لا تكون كذلك تخبرك أن بعض المولّدات في خط أنابيبك تحتاج إلى اهتمام قبل أن تكون أي نتيجة لاحقة موثوقة

var
  Page: TPdfStructuredTextPage;
  B, S, L: Integer;
  Emphasised: Boolean;
begin
  Page := Pdf.GetStructuredText(Options);

  if Page.UnmappedCharacterCount > 0 then
    Log(Format('page %d: %d characters without a Unicode mapping',
      [Page.PageNumber, Page.UnmappedCharacterCount]));
  if Page.GeometryFailureCount > 0 then
    Log(Format('page %d: %d characters without geometry',
      [Page.PageNumber, Page.GeometryFailureCount]));

  for B := 0 to High(Page.Blocks) do
    for L := 0 to High(Page.Blocks[B].Lines) do
      for S := 0 to High(Page.Blocks[B].Lines[L].Spans) do
      begin
        Emphasised := Page.Blocks[B].Lines[L].Spans[S].FontWeight >= 600;
        AppendRun(Page.Blocks[B].Lines[L].Spans[S].Text, Emphasised,
          Page.Blocks[B].Lines[L].Spans[S].SourceStartIndex);
      end;
end;

الأداء على صفحات حقيقية

استخراج التخطيط الفيزيائي هو الوضع المكلف، والتنفيذ مبني لصفحات كبيرة فعلاً: يعمل ترتيب الأحرف بتعقيد O(n log n) بدلاً من المسح المتكرر، وتنمو مخازن الأسطر والمقاطع هندسياً بدلاً من إعادة التخصيص لكل حرف، ويُبنى نص اليونيكود في مخازن بدلاً من ضم السلاسل، وتُخزَّن مؤقتاً عمليات البحث عن الخطوط للكائنات النصية المتجاورة. هذا المزيج هو ما يبقي صفحة كثيفة من 5000 حرف قابلة للتنبؤ بدلاً من تربيعية

بالنسبة لمهمة ثقيلة العدد من الصفحات، لا يزال يستحق اختيار الوضع الأرخص حيثما أمكن. استخدم roContentOrder مع تفعيل الدلالات للمستندات الموسومة التي تثق بها، واحجز roPhysicalLayout للمواد الممسوحة ضوئياً والقديمة حيث تكون الهندسة الإشارة الوحيدة. إذا كان كل ما تحتاجه سلسلة نصية بسيطة، فإن واجهة البرمجة الأبسط الموضحة في استخراج النص من مستندات PDF تبقى المسار الأسرع، وعندما تحتاج إلى تتبع النص عائداً إلى معرِّفات المحتوى المُعلَّم، يغطي قراءة وكتابة محتوى BDC وMCID المُعلَّم تلك الطبقة

يتطابق نموذج الكتل أيضاً بشكل نظيف مع ما تريده خطوط أنابيب الاسترجاع: العنوان مع فقراته جزء له عنوان، وتتيح الحدود لاستشهاد أن يشير إلى موقع على صفحة بدلاً من الإشارة إلى مستند. PDFiumPas مكون لـ Delphi وLazarus مبني حول محرك PDFium، موثَّق بأمثلة على صفحة مكون PDFium لـ Delphi