Technisch artikel

Tabellen extraheren uit PDF-pagina's in Delphi met PDFium

PDFium Component detecteert tabellen op een PDF-pagina en geeft ze terug als een raster van cellen met rij- en kolomspans, headerrijen en een betrouwbaarheidswaarde, via ExtractTables voor één pagina en ExtractDocumentTables voor een heel document. Elke tabel wordt met één aanroep omgezet naar CSV of JSON, en tabellen die doorlopen over een paginabreek kunnen worden gekoppeld tot een vervolgketen

PDF heeft geen tabelobject. Een tabel in een PDF is een verzameling tekstruns die zo gepositioneerd is dat een mens ze als een raster leest, soms met lijnen eromheen getekend en vaak zonder. Het raster herstellen betekent een intentie reconstrueren die het bestand nooit heeft vastgelegd, wat de reden is waarom elke extractietool net iets andere resultaten oplevert en waarom een tool die u zijn betrouwbaarheid vertelt, nuttiger is dan een die dat niet doet

Twee detectiemodi voor twee soorten tabellen

Detectie op basis van lijnen gebruikt de getekende lijnen. Elk gestreken padsegment wordt via de matrix van het pagina-object omgezet naar paginacoördinaten, horizontale en verticale lijnen worden gesneden, en de snijpunten vormen samenhangende componenten. Elke component wordt zijn eigen gesorteerde raster van X- en Y-posities, wat voorkomt dat twee afzonderlijke tabellen op dezelfde pagina worden samengevoegd tot één onzinnig raster

Witruimtedetectie behandelt tabellen die met uitlijning zijn getekend in plaats van met lijnen. Woordkaders worden gegroepeerd in visuele rijen, hiaten binnen een rij splitsen die op in kandidaat-kolommen, en een tabel wordt alleen geaccepteerd wanneer ten minste MinRows rijen ten minste MinColumns links uitgelijnde ankers herhalen binnen AlignmentTolerance. De rijhiaatfactor staat standaard op 3, wat de ongeveer 30-punts baseline-afstand dekt die typisch is voor tekst van 12 punt, zonder dat één regel met meerdere tekstruns zich kan voordoen als een tabel

Diagram van de tabeldetectiepijplijn van PDFium Component in Delphi, waarbij lijnsnijpunten en op witruimte uitgelijnde woordrijen samenkomen in één gescoord tabelrecord met CSV- en JSON-export
Lijndetectie snijdt getekende streken, terwijl witruimtedetectie uitgelijnde woordkaderrijen telt; kandidaten die MinRows en MinColumns halen, komen naar voren met een betrouwbaarheidsscore en een gekoppelde 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;                    // gebaseerd op 1

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

Hoe worden samengevoegde cellen hersteld?

Dit is het onderdeel dat naïeve extractors fout doen. Een samengevoegde cel kan niet alleen uit het globale raster worden herkend, omdat het raster is afgeleid van alle lijnen op de pagina en een samengevoegde regio simpelweg de interne lijn mist die haar zou hebben gescheiden

De regel die hier wordt gebruikt is lokaal: twee aangrenzende basiscellen worden samengevoegd wanneer geen grenslijn het interval ertussen dekt. Union-find voegt ze samen, de resulterende rechthoekige componenten worden RowSpan- en ColumnSpan-waarden, en tekst wordt op basis van zijn middelpunt toegewezen aan een basiscel en volgt vervolgens die cel naar haar samenvoegingswortel. Dit op deze manier doen houdt de kosten ook lineair in woorden plus cellen, in plaats van de kwadratische scan die u krijgt door elk woord tegen elke cel te testen

Diagram van het herstellen van samengevoegde cellen bij tabelextractie met PDFium voor Delphi, waarbij union-find aangrenzende basiscellen samenvoegt telkens wanneer geen grenslijn het interval ertussen dekt, wat RowSpan en ColumnSpan oplevert
Union-find voegt naburige basiscellen samen waarvan het gedeelde interval geen getekende grens draagt, zodat een samengevoegde header terugkomt als één cel met ColumnSpan ingesteld, in plaats van één gevulde cel geflankeerd door lege cellen

Het praktische effect is dat een financiële tabel met een samengevoegde "Totaal"-header over drie kolommen naar voren komt als één cel met span drie, in plaats van één gevulde cel en twee mysterieuze lege cellen

Doorloop over pagina's heen

Lange tabellen breken over pagina's heen, en elk paginafragment als een onafhankelijke tabel behandelen dwingt de aanroeper ze aan elkaar te naaien. ExtractDocumentTables kan ze in plaats daarvan koppelen, maar alleen onder strikte voorwaarden: het fragment moet de onderste tabel op de eerdere pagina zijn, de volgende moet de bovenste tabel op de daaropvolgende pagina zijn, de paginanummers moeten aangrenzend zijn, en de kolomgrenzen moeten overeenkomen

Alle vier de voorwaarden samen voorkomen de voor de hand liggende fout, namelijk elke tabel met vier kolommen in een document aaneenschakelen tot één denkbeeldige megatabel omdat ze toevallig hetzelfde aantal kolommen delen. Wanneer de voorwaarden gelden, delen de tabellen een vervolggroep-identifier en dragen ze vervolgmetadata; wanneer dat niet zo is, krijgt u afzonderlijke tabellen en kunt u zelf beslissen

Diagram van tabeldoorloop over PDF-pagina's in Delphi, waarbij vier strikte poorten bepalen of het onderste fragment op één pagina zich voegt bij het bovenste fragment op de volgende
Extractie op documentniveau koppelt fragmenten alleen wanneer alle vier de poorten gelden, waardoor niet-gerelateerde tabellen met vier kolommen niet samensmelten tot één denkbeeldige megatabel

Extractie op documentniveau deelt de budgetten MaxCells en MaxTables over pagina's in plaats van ze per pagina te resetten, en herstelt de actieve pagina in een finally-blok, zodat een extractierun in een viewer de gebruiker laat kijken naar de pagina waar hij was

Exporteren zonder de data te beschadigen

Beide exporters zijn bewust in hun escaping. CSV zet velden altijd tussen aanhalingstekens en verdubbelt interne aanhalingstekens, wat de klassieke fout voorkomt waarbij een cel met een komma stilzwijgend twee kolommen wordt. Bij samengevoegde cellen wordt de inhoud alleen bij het linkerboven-anker uitgevoerd, zodat een CSV-rondgang een overspannende header niet dupliceert over de kolommen die ze dekt

JSON behoudt Unicode in plaats van het naar ASCII te escapen, escapet controletekens, en bevat de metadata die een afnemer nodig heeft om de kwaliteit te beoordelen: detectiemodus, betrouwbaarheid, grenzen, spanwaarden, headervlaggen en vervolginformatie. Als u geëxtraheerde tabellen doorvoert naar een downstream-systeem, geef dan de voorkeur aan JSON, want een CSV-rij kan u niet vertellen dat de tabel waar ze vandaan komt 0,51 betrouwbaarheid scoorde:

// Documentbrede extractie, waarbij alleen tabellen worden bewaard die het vertrouwen waard zijn
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;

Afstemmen, en weten wanneer te stoppen

Drie instellingen zijn belangrijker dan de rest. MinConfidence is de kwaliteitspoort, en 0,5 is bewust permissief; verhoog deze voor geautomatiseerde inname en verlaag deze voor een review-UI waarin een mens elk resultaat bevestigt. MinColumnGap bepaalt wat telt als een kolomgrens in witruimtemodus, en strak gezette tabellen in dichte rapporten hebben deze mogelijk verlaagd nodig ten opzichte van de standaard van 12 punt. MaxRowGapFactor bepaalt wanneer verticale afstand een tabel beëindigt, wat van belang is voor tabellen met af en toe een lege rij

Wees eerlijk over de grenzen. Tabellen met lijnen worden betrouwbaar geëxtraheerd. Netjes uitgelijnde witruimtetabellen worden goed geëxtraheerd. Tabellen met gedraaide tekst, geneste tabellen, of cellen waarvan de inhoud doorloopt naar wat eruitziet als nog een rij, hebben altijd review nodig, ongeacht hoe de parameters zijn ingesteld. Voor die gevallen geeft het gestructureerde-tekstmodel u het ruwe materiaal om een domeinspecifieke lezer te bouwen, beschreven in gestructureerde tekstblokken en leesvolgorde

Eén nuttige combinatie: wanneer een gescand document helemaal geen tekst heeft, heeft tabeldetectie niets om mee te werken totdat er een tekstlaag bestaat. Voeg er eerst één toe, zoals beschreven in het toevoegen van een doorzoekbare tekstlaag aan gescande PDF's, en extraheer dan pas. De woordkaders die een OCR-provider teruggeeft, zijn precies de input die witruimtedetectie nodig heeft

Tabelextractie, gestructureerde tekst en reflow lezen allemaal uit hetzelfde paginamodel in Delphi, C++Builder en Lazarus; de volledige API wordt beschreven op de PDFium Component voor Delphi-pagina