Articol tehnic

Extragerea tabelelor din pagini PDF în Delphi cu PDFium

PDFium Component detectează tabele într-o pagină PDF și le returnează ca o grilă de celule cu span-uri de rând și coloană, rânduri de antet și o valoare de încredere, prin ExtractTables pentru o pagină și ExtractDocumentTables pentru un document întreg. Fiecare tabel se convertește în CSV sau JSON cu un singur apel, iar tabelele care continuă peste o întrerupere de pagină pot fi legate într-un lanț de continuare

PDF nu are un obiect tabel. Un tabel într-un PDF este un set de run-uri de text poziționate astfel încât un om le citește ca pe o grilă, uneori cu linii desenate în jurul lor și adesea fără. Recuperarea grilei înseamnă reconstruirea unei intenții pe care fișierul nu a înregistrat-o niciodată, motiv pentru care fiecare instrument de extracție produce rezultate ușor diferite și motiv pentru care un instrument care vă spune nivelul lui de încredere este mai util decât unul care nu o face

Două moduri de detecție pentru două tipuri de tabel

Detecția pe bază de linii folosește liniile desenate. Fiecare segment de cale trasat este transformat în coordonate de pagină prin matricea obiectului de pagină, liniile orizontale și verticale sunt intersectate, iar intersecțiile formează componente conexe. Fiecare componentă devine propria ei grilă sortată de poziții X și Y, ceea ce este cel care împiedică două tabele separate de pe aceeași pagină să fie îmbinate într-o grilă absurdă

Detecția prin spații albe gestionează tabelele desenate prin aliniere, nu prin linii. Casetele de cuvinte sunt grupate în rânduri vizuale, golurile din interiorul unui rând îl împart în coloane candidate, iar un tabel este acceptat doar când cel puțin MinRows rânduri repetă cel puțin MinColumns ancore aliniate la stânga, în limita AlignmentTolerance. Factorul de gol de rând este implicit 3, ceea ce acoperă spațierea de bază de aproximativ 30 de puncte, tipică textului de 12 puncte, fără a permite ca o singură linie ce conține mai multe run-uri de text să treacă drept tabel

Diagramă a pipeline-ului de detecție a tabelelor din PDFium Component în Delphi, în care intersecțiile de linii desenate și rândurile de cuvinte aliniate prin spații albe alimentează o singură înregistrare de tabel notată, cu export CSV și JSON
Detecția pe bază de linii intersectează trasările desenate, în timp ce detecția prin spații albe numără rândurile de casete de cuvinte aliniate; candidații care depășesc MinRows și MinColumns ies cu un scor de încredere și DetectionMode atașat
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;                    // bazat pe 1

    Options := TPdfTableExtractionOptions.Default;
    Options.DetectRuledTables := True;
    Options.DetectWhitespaceTables := True;
    Options.MinConfidence := 0.6;            // implicit este 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;

Cum sunt recuperate celulele îmbinate?

Aceasta este partea pe care extractoarele naive o greșesc. O celulă îmbinată nu poate fi identificată doar din grila globală, pentru că grila este derivată din toate liniile de pe pagină, iar unei regiuni îmbinate pur și simplu îi lipsește linia interioară care ar fi separat-o

Regula folosită aici este locală: două celule de bază adiacente sunt îmbinate atunci când nicio linie de graniță nu acoperă intervalul dintre ele. Union-find le unește, componentele rectangulare rezultate devin valori RowSpan și ColumnSpan, iar textul este atribuit unei celule de bază după punctul ei central, apoi urmărește acea celulă până la rădăcina ei de îmbinare. Procedând astfel, costul rămâne și el liniar în cuvinte plus celule, în loc de scanarea pătratică pe care o obțineți testând fiecare cuvânt față de fiecare celulă

Diagramă a recuperării celulelor îmbinate în extracția de tabele PDFium pentru Delphi, în care union-find unește celulele de bază adiacente ori de câte ori nicio linie de graniță nu acoperă intervalul dintre ele, producând RowSpan și ColumnSpan
Union-find îmbină celulele de bază vecine al căror interval comun nu poartă nicio graniță desenată, astfel încât un antet îmbinat este returnat ca o singură celulă cu ColumnSpan setat, nu ca o celulă completată flancată de celule goale

Efectul practic este că un tabel financiar cu un antet îmbinat „Total”, care se întinde pe trei coloane, iese cu o celulă cu span trei, nu cu o celulă populată și două goale, misterioase

Continuarea peste pagini

Tabelele lungi se întrerup peste pagini, iar tratarea fragmentului fiecărei pagini ca pe un tabel independent forțează apelantul să le coasă manual. ExtractDocumentTables le poate lega, în schimb, dar doar în condiții stricte: fragmentul trebuie să fie cel mai de jos tabel de pe pagina anterioară, următorul trebuie să fie cel mai de sus tabel de pe pagina următoare, numerele de pagină trebuie să fie adiacente, iar granițele de coloană trebuie să se potrivească

Toate cele patru condiții împreună sunt cele care previn greșeala evidentă, adică înlănțuirea fiecărui tabel cu patru coloane dintr-un document într-un mega-tabel imaginar, doar pentru că se întâmplă să aibă același număr de coloane. Când condițiile se îndeplinesc, tabelele partajează un identificator de grup de continuare și poartă metadate de continuare; când nu se îndeplinesc, obțineți tabele separate și puteți decide singur

Diagramă a continuării tabelelor peste pagini PDF în Delphi, în care patru condiții stricte decid dacă cel mai de jos fragment de pe o pagină se leagă de cel mai de sus fragment de pe pagina următoare
Extracția la nivel de document leagă fragmentele doar atunci când toate cele patru condiții se îndeplinesc, împiedicând tabelele nelegate cu patru coloane să fuzioneze într-un mega-tabel imaginar

Extracția la nivel de document partajează bugetele MaxCells și MaxTables pe toate paginile, în loc să le reseteze per pagină, și restaurează pagina activă într-un bloc finally, astfel încât o extracție rulată într-un viewer lasă utilizatorul privind pagina pe care se afla

Exportul fără a corupe datele

Ambele exportatoare sunt deliberate în privința escaping-ului. CSV pune întotdeauna câmpurile între ghilimele și dublează ghilimelele interne, ceea ce evită eșecul clasic în care o celulă ce conține o virgulă devine silențios două coloane. Pentru celulele îmbinate, conținutul este emis doar la ancora din stânga sus, astfel încât un round-trip CSV nu duplică un antet care se întinde pe mai multe coloane pe toate coloanele pe care le acoperă

JSON păstrează Unicode, nu îl transformă prin escaping în ASCII, aplică escaping caracterelor de control și include metadatele de care are nevoie un consumator pentru a judeca calitatea: mod de detecție, încredere, limite, valori de span, flag-uri de antet și informații de continuare. Dacă alimentați tabele extrase într-un sistem din aval, preferați JSON, pentru că un rând CSV nu vă poate spune că tabelul din care provine a obținut un scor de încredere de 0,51:

// Extracție la nivel de document, păstrând doar tabelele demne de încredere
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;

Reglaje fine, și cum știți când să vă opriți

Trei setări contează mai mult decât restul. MinConfidence este poarta de calitate, iar 0,5 este deliberat permisiv; ridicați-l pentru ingestie automată și coborâți-l pentru o interfață de revizuire unde un om confirmă fiecare rezultat. MinColumnGap decide ce contează drept graniță de coloană în modul de spații albe, iar tabelele setate strâns în rapoarte dense pot avea nevoie să fie redusă de la valoarea implicită de 12 puncte. MaxRowGapFactor decide când distanța verticală încheie un tabel, ceea ce contează pentru tabelele cu rânduri goale ocazionale

Fiți onest în privința limitelor. Tabelele cu linii se extrag fiabil. Tabelele cu spații albe, aliniate curat, se extrag bine. Tabelele cu text rotit, tabele imbricate, sau celule al căror conținut se întinde pe ce arată ca un alt rând vor avea nevoie de revizuire indiferent de cum sunt setați parametrii. Pentru acestea, modelul de text structurat vă oferă materialul brut pentru a construi un cititor specific domeniului, descris în blocuri de text structurat și ordinea de citire

O combinație utilă: când un document scanat nu are deloc text, detecția de tabele nu are cu ce să lucreze până când nu există un strat de text. Adăugați unul mai întâi, așa cum este descris în adăugarea unui strat de text căutabil la PDF-uri scanate, apoi extrageți. Casetele de cuvinte pe care le returnează un furnizor OCR sunt exact intrarea de care are nevoie detecția prin spații albe

Extracția de tabele, textul structurat și reflow-ul citesc toate din același model de pagină în Delphi, C++Builder și Lazarus; API-ul complet este descris pe pagina PDFium Component for Delphi