Technický článek

Extrakce tabulek ze stránek PDF v Delphi s PDFium

PDFium Component detekuje tabulky na stránce PDF a vrací je jako mřížku buněk s rozpětím řádků a sloupců, záhlavními řádky a hodnotou spolehlivosti, přes ExtractTables pro jednu stránku a ExtractDocumentTables pro celý dokument. Každá tabulka se jedním voláním převede na CSV nebo JSON a tabulky pokračující přes zalomení stránky lze propojit do řetězce pokračování

PDF nemá objekt tabulky. Tabulka v PDF je sada textových běhů umístěných tak, aby je člověk vnímal jako mřížku, někdy s liniemi nakreslenými kolem nich a často bez nich. Rekonstrukce mřížky znamená znovu vytvořit záměr, který soubor nikdy nezaznamenal, což je důvod, proč každý extrakční nástroj vyprodukuje mírně odlišné výsledky a proč je nástroj, který vám řekne svou spolehlivost, užitečnější než ten, který to neudělá

Dva režimy detekce pro dva druhy tabulek

Detekce podle linek používá nakreslené čáry. Každý natažený segment cesty se transformuje do souřadnic stránky přes matici objektu stránky, vodorovné a svislé čáry se protnou a průsečíky vytvoří spojené komponenty. Každá komponenta se stane vlastní seřazenou mřížkou pozic X a Y, což udržuje dvě samostatné tabulky na stejné stránce od sloučení do jedné nesmyslné mřížky

Detekce podle mezer zpracovává tabulky nakreslené zarovnáním místo liniemi. Rámečky slov se seskupí do vizuálních řádků, mezery uvnitř řádku ho rozdělí na kandidátní sloupce a tabulka se přijme jen tehdy, když se alespoň MinRows řádků opakuje alespoň s MinColumns zleva zarovnanými kotvami v rámci AlignmentTolerance. Faktor mezery mezi řádky má výchozí hodnotu 3, což pokrývá zhruba 30bodové rozestupy základní čáry typické pro 12bodový text, aniž by dovolil jednomu řádku s několika textovými běhy vydávat se za tabulku

Diagram pipeline detekce tabulek PDFium Component v Delphi, kde průsečíky nakreslených linek a zarovnané řádky rámečků slov zásobují jeden ohodnocený záznam tabulky s exportem do CSV a JSON
Detekce podle linek protíná nakreslené tahy, zatímco detekce podle mezer počítá zarovnané řádky rámečků slov; kandidáti splňující MinRows a MinColumns vycházejí se skóre spolehlivosti a přiřazeným 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;                    // číslováno od 1

    Options := TPdfTableExtractionOptions.Default;
    Options.DetectRuledTables := True;
    Options.DetectWhitespaceTables := True;
    Options.MinConfidence := 0.6;            // výchozí je 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;

Jak se rozpoznávají sloučené buňky?

Toto je část, kterou naivní extraktory dělají špatně. Sloučenou buňku nelze identifikovat jen z globální mřížky, protože mřížka je odvozena ze všech čar na stránce a sloučená oblast prostě postrádá vnitřní čáru, která by je oddělila

Zde použité pravidlo je lokální: dvě sousední základní buňky se sloučí, když žádná ohraničující čára nepokrývá interval mezi nimi. Union-find je spojí, výsledné obdélníkové komponenty se stanou hodnotami RowSpan a ColumnSpan a text se přiřadí základní buňce podle jejího středu a pak sleduje tuto buňku až ke kořeni sloučení. Tento postup také udržuje náklady lineární vzhledem ke slovům plus buňkám, místo kvadratického skenu, který dostanete testováním každého slova proti každé buňce

Diagram rozpoznávání sloučených buněk při extrakci tabulek PDFium pro Delphi, kde union-find spojuje sousední základní buňky, kdykoli žádná ohraničující čára nepokrývá interval mezi nimi, čímž vznikají hodnoty RowSpan a ColumnSpan
Union-find slučuje sousedící základní buňky, jejichž sdílený interval nenese žádnou nakreslenou hranici, takže sloučené záhlaví se vrátí jako jedna buňka s nastaveným ColumnSpan místo jedné vyplněné buňky obklopené prázdnými

Praktickým efektem je, že finanční tabulka se sloučeným záhlavím "Celkem" napříč třemi sloupci vyjde jako jedna buňka s rozpětím tři, ne jako jedna vyplněná buňka a dvě záhadně prázdné

Pokračování přes stránky

Dlouhé tabulky se zalamují přes stránky a zacházení s fragmentem každé stránky jako se samostatnou tabulkou nutí volajícího je sešívat. ExtractDocumentTables je místo toho dokáže propojit, ale jen za přísných podmínek: fragment musí být nejnižší tabulka na dřívější stránce, další musí být nejvyšší tabulka na následující stránce, čísla stránek musí být sousední a hranice sloupců se musí shodovat

Všechny čtyři podmínky dohromady zabraňují zjevné chybě, tedy zřetězení každé čtyřsloupcové tabulky v dokumentu do jedné imaginární megatabulky jen proto, že mají shodou okolností stejný počet sloupců. Když podmínky platí, tabulky sdílejí identifikátor skupiny pokračování a nesou metadata pokračování; když neplatí, dostanete samostatné tabulky a rozhodnete se sami

Diagram pokračování tabulky přes stránky PDF v Delphi, kde čtyři přísné brány rozhodují, zda se nejnižší fragment na jedné stránce spojí s nejvyšším fragmentem na další
Extrakce na úrovni dokumentu propojí fragmenty jen tehdy, když platí všechny čtyři brány, čímž brání nesouvisejícím čtyřsloupcovým tabulkám splynout do jedné imaginární megatabulky

Extrakce na úrovni dokumentu sdílí rozpočty MaxCells a MaxTables napříč stránkami místo jejich resetování na stránku a obnoví aktivní stránku v bloku finally, takže extrakce spuštěná v prohlížeči zanechá uživatele koukajícího na stránku, na které byl

Export bez poškození dat

Oba exportéry jsou v otázce escapování promyšlené. CSV vždy uzavře pole do uvozovek a zdvojí vnitřní uvozovky, čímž se vyhne klasickému selhání, kdy se buňka s čárkou tiše promění ve dva sloupce. U sloučených buněk se obsah emituje jen na kotvě vlevo nahoře, takže zpáteční cyklus CSV nezdvojuje záhlaví přesahující přes sloupce, které pokrývá

JSON zachovává Unicode místo jeho escapování do ASCII, escapuje řídicí znaky a obsahuje metadata, která spotřebitel potřebuje k posouzení kvality: režim detekce, spolehlivost, hranice, hodnoty rozpětí, příznaky záhlaví a informace o pokračování. Pokud extrahované tabulky posíláte do navazujícího systému, upřednostněte JSON, protože řádek CSV vám nemůže říct, že tabulka, ze které pochází, měla spolehlivost 0,51:

// Extrakce napříč celým dokumentem, přičemž se ponechají jen důvěryhodné tabulky
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;

Ladění, a kdy přestat

Tři nastavení mají větší význam než zbytek. MinConfidence je brána kvality a 0,5 je záměrně shovívavá; zvyšte ji pro automatizované zpracování a snižte pro UI pro kontrolu, kde člověk potvrzuje každý výsledek. MinColumnGap rozhoduje, co se počítá jako hranice sloupce v režimu mezer, a hustě sázené tabulky v nabitých reportech mohou potřebovat snížit ji z výchozích 12 bodů. MaxRowGapFactor rozhoduje, kdy svislá vzdálenost ukončí tabulku, což má význam pro tabulky s občasnými prázdnými řádky

Buďte upřímní ohledně limitů. Tabulky s linkami se extrahují spolehlivě. Čistě zarovnané tabulky s mezerami se extrahují dobře. Tabulky s otočeným textem, vnořené tabulky nebo buňky, jejichž obsah se zalamuje do podoby, která vypadá jako další řádek, budou potřebovat kontrolu bez ohledu na to, jak nastavíte parametry. Pro ně vám model strukturovaného textu dává surový materiál pro stavbu doménově specifického čtenáře, popsaný v článku bloky strukturovaného textu a pořadí čtení

Jedno užitečné spárování: když skenovaný dokument nemá vůbec žádný text, detekce tabulek nemá s čím pracovat, dokud neexistuje textová vrstva. Přidejte ji nejdřív, jak popisuje článek přidání prohledávatelné textové vrstvy do skenovaných PDF, a pak extrahujte. Rámečky slov, které vrátí poskytovatel OCR, jsou přesně ten vstup, který detekce podle mezer potřebuje

Extrakce tabulek, strukturovaný text a reflow čtou v Delphi, C++Builder a Lazarus ze stejného modelu stránky; úplné API popisuje stránka PDFium Component pro Delphi