Techninis straipsnis

Lentelių ištraukimas iš PDF puslapių Delphi aplinkoje su PDFium

PDFium Component PDF puslapyje aptinka lenteles ir grąžina jas kaip langelių tinklelį su eilučių ir stulpelių apimtimis (spans), antraštės eilutėmis ir pasitikėjimo verte, per ExtractTables vienam puslapiui ir ExtractDocumentTables visam dokumentui. Kiekviena lentelė vienu iškvietimu konvertuojama į CSV arba JSON, o lentelės, tęsiamos per puslapio lūžį, gali būti susietos į tęsinio grandinę

PDF neturi lentelės objekto. PDF faile lentelė yra teksto atkarpų (text runs) rinkinys, pozicionuotas taip, kad žmogus jas skaitytų kaip tinklelį, kartais su aplink nubrėžtomis linijomis, o dažnai be jų. Tinklelio atkūrimas reiškia ketinimo, kurio failas niekada neužfiksavo, atstatymą, todėl kiekvienas ištraukimo įrankis pateikia šiek tiek skirtingus rezultatus, ir todėl įrankis, praneša savo pasitikėjimo laipsnį, yra naudingesnis už tą, kuris to nedaro

Du aptikimo režimai dviem lentelių tipams

Linijomis pagrįstas aptikimas naudoja nubrėžtas linijas. Kiekvienas kontūro segmentas per puslapio objekto matricą transformuojamas į puslapio koordinates, horizontalios ir vertikalios linijos sankirtos, o sankirtos sudaro susietus komponentus. Kiekvienas komponentas tampa savo rūšiuotu X ir Y pozicijų tinkleliu, ir būtent tai neleidžia dviem atskiroms to paties puslapio lentelėms susilieti į vieną beprasmį tinklelį

Tarpais pagrįstas aptikimas apdoroja lenteles, nubraižytas lygiavimu, o ne linijomis. Žodžių dėžutės (word boxes) grupuojamos į vizualias eilutes, tarpai eilutės viduje ją padalija į kandidatinius stulpelius, o lentelė priimama tik tada, kai bent MinRows eilučių pakartoja bent MinColumns kairėje lygiuotus inkarus, esančius AlignmentTolerance ribose. Eilutės tarpo koeficientas pagal numatymą yra 3, ir tai apima maždaug 30 taškų bazinės linijos tarpą, būdingą 12 taškų dydžio tekstui, neleidžiant vienai eilutei, turinčiai kelias teksto atkarpas, apsimesti lentele

Diagrama apie PDFium Component lentelių aptikimo konvejerį Delphi aplinkoje, kur linijų sankirtos ir tarpais lygiuotos žodžių eilutės maitina vieną įvertintą lentelės įrašą su CSV ir JSON eksportu
Linijomis pagrįstas aptikimas kerta nubrėžtus kontūrus, o tarpais pagrįstas aptikimas skaičiuoja lygiuotas žodžių dėžučių eilutes; kandidatai, praėję MinRows ir MinColumns, gauna pasitikėjimo balą ir pridėtą DetectionMode
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;                    // numeracija nuo 1

    Options := TPdfTableExtractionOptions.Default;
    Options.DetectRuledTables := True;
    Options.DetectWhitespaceTables := True;
    Options.MinConfidence := 0.6;            // numatytoji reikšmė yra 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;

Kaip atkuriami sujungti langeliai?

Tai dalis, kurioje naivūs ištraukimo įrankiai klysta. Sujungto langelio negalima nustatyti vien iš bendro tinklelio, nes tinklelis kildinamas iš visų puslapio linijų, o sujungtai sričiai tiesiog trūksta vidinės linijos, kuri ją būtų atskyrusi

Čia naudojama taisyklė yra lokali: du gretimi baziniai langeliai sujungiami, kai jokia ribinė linija neapima intervalo tarp jų. Union-find juos sujungia, gauti stačiakampiai komponentai tampa RowSpan ir ColumnSpan reikšmėmis, o tekstas priskiriamas baziniam langeliui pagal jo centro tašką ir tada seka tą langelį iki jo sujungimo šaknies. Tai atliekant tokiu būdu, sąnaudos taip pat išlieka tiesinės žodžių plius langelių atžvilgiu, o ne kvadratinės, kaip būtų testuojant kiekvieną žodį prieš kiekvieną langelį

Diagrama apie sujungtų langelių atkūrimą PDFium lentelių ištraukime Delphi aplinkoje, kur union-find sujungia gretimus bazinius langelius, kai jokia ribinė linija neapima intervalo tarp jų, gaunant RowSpan ir ColumnSpan
Union-find sujungia kaimyninius bazinius langelius, kurių bendras intervalas neturi nubrėžtos ribos, todėl sujungta antraštė grąžinama kaip vienas langelis su nustatytu ColumnSpan, o ne kaip vienas užpildytas langelis, apsuptas tuščiais

Praktinis poveikis yra tas, kad finansinė lentelė su sujungta „Total“ antrašte, apimančia tris stulpelius, gaunama kaip vienas langelis, apimties trys, o ne kaip vienas užpildytas langelis ir du paslaptingi tušti

Tęsinys per puslapius

Ilgos lentelės lūžta per puslapius, o kiekvieno puslapio fragmento traktavimas kaip nepriklausomos lentelės verčia iškvietėją juos susiūti. ExtractDocumentTables vietoj to gali juos susieti, bet tik esant griežtoms sąlygoms: fragmentas turi būti žemiausia ankstesnio puslapio lentelė, kita turi būti aukščiausia sekančio puslapio lentelė, puslapių numeriai turi būti gretimi, o stulpelių ribos turi sutapti

Būtent visos keturios sąlygos kartu apsaugo nuo akivaizdžios klaidos — sugrandinti kiekvieną keturių stulpelių lentelę dokumente į vieną įsivaizduojamą mega-lentelę vien todėl, kad joms atsitiktinai sutampa stulpelių skaičius. Kai sąlygos tenkinamos, lentelės dalijasi tęsinio grupės identifikatoriumi ir neša tęsinio metaduomenis; kai netenkinamos, gaunate atskiras lenteles ir galite spręsti patys

Diagrama apie lentelės tęsinį per PDF puslapius Delphi aplinkoje, kur keturi griežti vartai nusprendžia, ar žemiausias vieno puslapio fragmentas jungiasi su aukščiausiu sekančio puslapio fragmentu
Dokumento lygio ištraukimas fragmentus sujungia tik tada, kai visi keturi vartai tenkinami, taip apsaugant nesusijusias keturių stulpelių lenteles nuo susiliejimo į vieną įsivaizduojamą mega-lentelę

Dokumento lygio ištraukimas MaxCells ir MaxTables biudžetus dalijasi tarp puslapių, o ne juos atstato kiekvienam puslapiui, ir aktyvų puslapį atkuria finally bloke, todėl ištraukimas, paleistas žiūryklėje, vartotoją palieka žiūrint puslapį, kuriame jis buvo

Eksportavimas nesugadinant duomenų

Abu eksportuotojai apgalvotai tvarko escape simbolius. CSV visada apgaubia laukus kabutėmis ir dubliuoja vidines kabutes, taip išvengiant klasikinės klaidos, kai langelis su kableliu tyliai virsta dviem stulpeliais. Sujungtiems langeliams turinys išvedamas tik viršutiniame kairiajame inkare, todėl CSV apvažiavimas (round-trip) nedubliuoja apimties antraštės per stulpelius, kuriuos ji apima

JSON išsaugo Unicode, o ne konvertuoja jį į ASCII, escape'ina valdymo simbolius ir įtraukia metaduomenis, kurių vartotojui reikia kokybei įvertinti: aptikimo režimą, pasitikėjimą, ribas, apimties reikšmes, antraštės žymes ir tęsinio informaciją. Jei ištrauktas lenteles perduodate tolesnei sistemai, rinkitės JSON, nes CSV eilutė negali pasakyti, kad lentelė, iš kurios ji atėjo, gavo 0,51 pasitikėjimo balą:

// Viso dokumento ištraukimas, paliekant tik lenteles, kuriomis verta pasitikėti
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;

Derinimas ir žinojimas, kada sustoti

Trys nustatymai svarbesni už likusius. MinConfidence yra kokybės vartai, ir 0,5 yra sąmoningai liberalus; padidinkite jį automatiniam įkėlimui ir sumažinkite peržiūros sąsajai, kur žmogus patvirtina kiekvieną rezultatą. MinColumnGap nusprendžia, kas laikoma stulpelio riba tarpų režime, o tankiai išdėstytoms lentelėms glaustose ataskaitose gali prireikti jį sumažinti nuo numatytųjų 12 taškų. MaxRowGapFactor nusprendžia, kada vertikalus atstumas baigia lentelę, ir tai svarbu lentelėms su pavienėmis tuščiomis eilutėmis

Būkite sąžiningi dėl ribų. Linijomis pagrįstos lentelės ištraukiamos patikimai. Švariai lygiuotos tarpais pagrįstos lentelės ištraukiamos gerai. Lentelėms su pasuktu tekstu, įdėtomis lentelėmis arba langeliais, kurių turinys persikelia į kitą eilutę primenančią formą, prireiks peržiūros nepriklausomai nuo to, kaip nustatyti parametrai. Joms struktūrizuoto teksto modelis suteikia žaliavą specifinio domeno skaitytuvui sukurti, aprašytą straipsnyje struktūrizuoto teksto blokai ir skaitymo tvarka

Vienas naudingas derinys: kai nuskaitytas dokumentas apskritai neturi teksto, lentelių aptikimui neturi su kuo dirbti, kol neatsiranda teksto sluoksnis. Pirmiausia jį pridėkite, kaip aprašyta straipsnyje ieškomo teksto sluoksnio pridėjimas prie nuskaitytų PDF failų, tada ištraukite. OCR tiekėjo grąžinamos žodžių dėžutės yra būtent tokia įvestis, kurios reikia tarpais pagrįstam aptikimui

Lentelių ištraukimas, struktūrizuotas tekstas ir perkomponavimas (reflow) visi skaito iš to paties puslapio modelio Delphi, C++Builder ir Lazarus aplinkoms; pilnas API aprašytas PDFium Component for Delphi puslapyje