Articol tehnic

Extragere de text PDF structurat în Delphi cu PDFium VCL

PDFiumPas returnează textul unei pagini ca structură, nu ca string. GetStructuredText produce un TPdfStructuredTextPage conținând blocuri, fiecare purtând linii, fiecare purtând span-uri stilizate, cu limite în spațiul paginii la fiecare nivel și indicii de caractere sursă păstrați, astfel încât orice fragment poate fi mapat înapoi la pagina de text de dedesubt

Extragerea ca string plat, de la care pornește majoritatea codului, este încă acolo și încă corectă pentru scopul ei. Încetează să mai fie suficientă în momentul în care ai nevoie să știi ce cuvinte au fost un titlu, ce a aparținut coloanei din stânga, sau unde pe pagină se află efectiv o potrivire

De ce este un string plat output-ul greșit pentru majoritatea sarcinilor?

Pentru că întrebările pe care oamenii le pun despre text extras aproape niciodată nu sunt „ce caractere sunt pe această pagină”. Sunt „care este titlul”, „este acesta un tabel”, „acest paragraf aparține secțiunii 4”, „unde desenez evidențierea”. Un singur string nu răspunde la niciuna dintre ele, iar fiecare răspuns pe care îl reconstruiești din el este o euristică pe care acum o deții tu

Layout-urile pe două coloane fac afirmația concretă. Extrage un articol pe două coloane ca string și, în funcție de cum a scris producătorul content stream-ul, poți obține coloana unu urmată de coloana doi, sau poți obține linia unu a coloanei unu, linia unu a coloanei doi, linia doi a coloanei unu, și așa mai departe pe pagină. Ambele ies dintr-un PDF conform. Niciuna nu este greșită la nivel de format, pentru că PDF descrie marcaje pe o pagină, nu un contur de document. Un model bazat pe blocuri permite extractorului să ia decizia de ordonare explicit și să îți spună ce decizie a luat

Ordinea conținutului sau layout-ul fizic?

TPdfStructuredTextOptions.ReadingOrder alege între roContentOrder și roPhysicalLayout, iar răspunsul corect depinde de în ce ai mai multă încredere, producătorul sau geometria

Ordinea conținutului returnează textul în secvența în care content stream-ul îl desenează. Asta este rapid, și pentru documente generate de un producător bine-crescut, de obicei ordinea de citire intenționată. Layout-ul fizic ignoră secvența stream-ului și reconstruiește ordinea din locul unde caracterele stau efectiv, grupându-le în linii și apoi în coloane. Asta îți dorești pentru pagini scanate și trecute prin OCR, pentru output din instrumente care emit text în ordinea fontului, nu în ordinea de citire, și pentru orice unde rezultatul vizual este singurul lucru pe care te poți baza

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;                     // bazat pe 1

    Options := TPdfStructuredTextOptions.Default;
    Options.ReadingOrder := roPhysicalLayout;
    Options.IncludeFontInfo := True;
    Options.IncludeSemantics := True;
    Options.MaxCharacters := 200000;         // buget fail-closed

    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;

Ce adaugă tagging-ul pe care geometria nu îl poate oferi?

Intenția. Cu IncludeSemantics activat, blocurile dintr-un PDF tagged poartă un Kind extras din arborele de structură, așa că un titlu este un titlu pentru că producătorul a spus asta, nu pentru că fontul lui era mai mare decât media. Tipurile acoperă formele care contează pentru reutilizare: cfParagraph, cfHeading cu un HeadingLevel, cfListItem, cfTableCell, cfCaption, cfFigure și fallback-ul netagged cfPlain

Câmpul Source înregistrează de unde a venit fiecare clasificare, rosStructure pentru arborele de structură și rosHeuristic pentru inferență, care este câmpul de logat atunci când decizi cât de mult să ai încredere într-un pipeline de extracție de-a lungul unui set de documente. Figurile sunt un caz special demn de știut: pentru un bloc cfFigure, textul vine din descrierea alternativă, nu din vreo glifă, întrucât o figură nu are propriile caractere. Textul alternativ nepotrivit este încă reprezentat, în loc să fie eliminat, ceea ce permite unui audit de accesibilitate să vadă că există o descriere chiar și atunci când nimic de pe pagină nu o desenează. Modelul de tagging în sine este acoperit în validarea arborelui de structură PDF/UA

Span-urile poartă stilizarea și proveniența

Fiecare TPdfStructuredTextSpan poartă textul lui, limitele lui în spațiul paginii, FontName, FontSize, FontWeight și Angle, plus SourceStartIndex și SourceCharacterCount. Span-urile se întrerup unde se schimbă stilizarea, așa că o propoziție cu trei cuvinte bold devine trei span-uri, iar reconstruirea accentuării în HTML sau Markdown este o chestiune de citire a proprietăților, nu de ghicit din nume de font

Cele două câmpuri de index sursă sunt cele care transformă extragerea într-o funcționalitate, nu doar un raport. Indică înapoi în secvența de caractere a paginii, ceea ce înseamnă că un bloc pe care l-ai potrivit într-o căutare poate fi convertit în geometrie de selecție la nivel de caracter sau într-un dreptunghi de evidențiere fără o a doua trecere, ordonată diferit, peste text; mecanica este descrisă în selecția vizuală de linie de text cu cutii de caractere. Câmpul Angle contează mai mult decât pare: textul rotit dintr-o ștampilă sau un filigran ajunge în același spațiu de coordonate ca textul din corp, iar un pipeline care ignoră unghiul va îmbina cu bucurie un „DRAFT” diagonal în mijlocul unui paragraf

Buget, și cele două contoare de calitate

MaxCharacters este un buget fail-closed, nu o setare de trunchiere: o pagină care îl depășește se oprește în loc să returneze silențios doar o parte din conținut. Pe un flux de ingestie nesigur, acesta este comportamentul pe care îl vrei, pentru că o pagină cu un milion de caractere este fie un monstru generat de mașină, fie o încercare de a face din extractorul tău cea mai lentă parte a sistemului

Două contoare de pe pagina returnată descriu direct calitatea extragerii. UnmappedCharacterCount numără caracterele fără o mapare Unicode utilizabilă, care este simptomul clasic al unui font subset inclus fără un CMap /ToUnicode; un text ca acesta se randează perfect și se extrage ca nimic util. GeometryFailureCount numără caracterele a căror casetă de delimitare nu a putut fi determinată, ceea ce degradează ordonarea de layout fizic. Loghează pe amândouă. Un set de documente unde aceste numere sunt constant aproape de zero poate fi indexat cu încredere, iar unul unde nu sunt îți spune că unii producători din pipeline-ul tău au nevoie de atenție înainte ca vreun rezultat din aval să fie de încredere

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;

Performanță pe pagini reale

Extragerea cu layout fizic este modul costisitor, iar implementarea este construită pentru pagini care sunt cu adevărat mari: ordonarea caracterelor rulează în O(n log n) în loc de scanare repetată, buffer-ele de linii și span-uri cresc geometric în loc să realoce per caracter, textul Unicode este construit în buffere, nu prin concatenare de string-uri, iar căutările de font pentru obiecte de text adiacente sunt puse în cache. Această combinație este ce menține o pagină densă de 5.000 de caractere previzibilă, nu quadratică

Pentru o sarcină încărcată cu multe pagini, tot merită să alegi modul mai ieftin unde poți. Folosește roContentOrder cu semantică activată pentru documente tagged în care ai încredere, și rezervă roPhysicalLayout pentru materialul scanat și cel legacy unde geometria este singurul semnal. Dacă tot ce ai nevoie este un string simplu, API-ul mai simplu descris în extragerea de text din documente PDF rămâne traseul mai rapid, iar când ai nevoie să urmărești textul înapoi la identificatori de conținut marcat, citirea și scrierea conținutului marcat BDC și MCID acoperă acel strat

Modelul de blocuri se mapează de asemenea curat pe ce își doresc pipeline-urile de retrieval: un titlu cu paragrafele lui este un chunk cu un titlu, iar limitele permit unei citări să indice o locație pe o pagină, nu doar un document. PDFiumPas este o componentă Delphi și Lazarus în jurul motorului PDFium, documentată cu exemple pe pagina componentei PDFium Delphi