Teknisk artikel

Extrahera tabeller från PDF-sidor i Delphi med PDFium

PDFium Component upptäcker tabeller på en PDF-sida och returnerar dem som ett rutnät av celler med rad- och kolumnspann, rubrikrader och ett konfidensvärde, via ExtractTables för en sida och ExtractDocumentTables för ett helt dokument. Varje tabell konverteras till CSV eller JSON med ett anrop, och tabeller som fortsätter över en sidbrytning kan länkas till en fortsättningskedja

PDF har inget tabellobjekt. En tabell i en PDF är en uppsättning textkörningar positionerade så att en människa läser dem som ett rutnät, ibland med linjer ritade runt dem och ofta utan. Att återskapa rutnätet innebär att rekonstruera en avsikt som filen aldrig registrerade, vilket är varför varje extraktionsverktyg ger något olika resultat och varför ett verktyg som talar om sin konfidens är mer användbart än ett som inte gör det

Två detekteringslägen för två sorters tabeller

Linjerad detektering använder de ritade linjerna. Varje strukna bansegment transformeras till sidkoordinater via sidobjektets matris, horisontella och vertikala linjer korsas, och korsningarna bildar sammanhängande komponenter. Varje komponent blir sitt eget sorterade rutnät av X- och Y-positioner, vilket är det som hindrar två separata tabeller på samma sida från att slås samman till ett obegripligt rutnät

Blanksteg-detektering hanterar tabeller ritade med justering istället för linjer. Ordrutor grupperas i visuella rader, luckor inom en rad delar upp den i kandidatkolumner, och en tabell accepteras bara när minst MinRows rader upprepar minst MinColumns vänsterjusterade ankare inom AlignmentTolerance. Radluckefaktorn är som standard 3, vilket täcker det ungefär 30 punkter stora baslinjeavståndet typiskt för 12-punkterstext utan att låta en enda rad som innehåller flera textkörningar utge sig för att vara en tabell

Diagram över PDFium Components tabelldetekteringspipeline i Delphi, där korsningar av linjerade linjer och blanksteg-justerade ordrader matar en enda poängsatt tabellpost med CSV- och JSON-export
Linjerad detektering korsar ritade streck medan blanksteg-detektering räknar justerade ordruterader; kandidater som klarar MinRows och MinColumns kommer ut med ett konfidenspoäng och DetectionMode kopplat
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;                    // 1-baserat

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

Hur återställs sammanslagna celler?

Det här är delen naiva extraktorer gör fel. En sammanslagen cell kan inte identifieras enbart från det globala rutnätet, eftersom rutnätet härleds från alla linjer på sidan och en sammanslagen region helt enkelt saknar den inre linje som skulle ha separerat den

Regeln som används här är lokal: två intilliggande baceller slås samman när ingen gränslinje täcker intervallet mellan dem. Union-find sammanfogar dem, de resulterande rektangulära komponenterna blir RowSpan- och ColumnSpan-värden, och text tilldelas en bascell efter dess mittpunkt och följer sedan den cellen till dess sammanslagningsrot. Att göra det på det här sättet håller också kostnaden linjär i ord plus celler, istället för den kvadratiska skanning du får av att testa varje ord mot varje cell

Diagram över återställning av sammanslagna celler i PDFium-tabellextraktion för Delphi, där union-find sammanfogar intilliggande baceller varje gång ingen gränslinje täcker intervallet mellan dem, vilket ger RowSpan och ColumnSpan
Union-find slår samman angränsande baceller vars delade intervall inte bär någon ritad gräns, så en sammanslagen rubrik returneras som en cell med ColumnSpan satt istället för en fylld cell flankerad av tomma

Den praktiska effekten är att en finansiell tabell med en sammanslagen "Total"-rubrik som spänner över tre kolumner kommer ut med en cell med spann tre, snarare än en fylld cell och två mystiskt tomma

Fortsättning över sidor

Långa tabeller bryts över sidor, och att behandla varje sidas fragment som en oberoende tabell tvingar anroparen att sy ihop dem. ExtractDocumentTables kan länka dem istället, men bara under strikta villkor: fragmentet måste vara den understa tabellen på den tidigare sidan, nästa måste vara den översta tabellen på följande sida, sidnumren måste vara intilliggande, och kolumngränserna måste stämma överens

Alla fyra villkor tillsammans är det som förhindrar det uppenbara felet, vilket är att kedja samman varje fyrkolumnstabell i ett dokument till en enda inbillad megatabell bara för att de råkar dela kolumnantal. När villkoren håller delar tabellerna en fortsättningsgruppidentifierare och bär fortsättningsmetadata; när de inte gör det får du separata tabeller och kan bestämma själv

Diagram över tabellfortsättning över PDF-sidor i Delphi, där fyra strikta grindar avgör om det understa fragmentet på en sida ansluter till det översta fragmentet på nästa
Extraktion på dokumentnivå länkar fragment bara när alla fyra grindar håller, vilket hindrar orelaterade fyrkolumnstabeller från att smälta samman till en inbillad megatabell

Extraktion på dokumentnivå delar budgetarna MaxCells och MaxTables över sidor istället för att återställa dem per sida, och den återställer den aktiva sidan i ett finally-block, så en extraktionskörning i en visare lämnar användaren tittandes på sidan de var på

Exportera utan att korrumpera datan

Båda exportörerna är noggranna med escaping. CSV citerar alltid fält och dubblerar interna citattecken, vilket undviker det klassiska felet där en cell som innehåller ett kommatecken tyst blir två kolumner. För sammanslagna celler avges innehåll bara vid det övre vänstra ankaret, så en CSV-tur-och-retur duplicerar inte en spännande rubrik över de kolumner den täcker

JSON bevarar Unicode snarare än att escapa det till ASCII, escapar styrtecken, och inkluderar den metadata en konsument behöver för att bedöma kvalitet: detekteringsläge, konfidens, gränser, spannvärden, rubrikflaggor och fortsättningsinformation. Om du matar extraherade tabeller in i ett nedströms system, föredra JSON, eftersom en CSV-rad inte kan tala om för dig att tabellen den kom från hade en konfidenspoäng på 0,51:

// Dokumentomfattande extraktion, behåller bara tabeller värda att lita på
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;

Finjustering, och att veta när man ska sluta

Tre inställningar spelar större roll än resten. MinConfidence är kvalitetsgrinden, och 0,5 är avsiktligt tillåtande; höj den för automatiserad inmatning och sänk den för ett granskningsgränssnitt där en människa bekräftar varje resultat. MinColumnGap avgör vad som räknas som en kolumngräns i blanksteg-läge, och tätt satta tabeller i täta rapporter kan behöva den sänkt från standardvärdet på 12 punkter. MaxRowGapFactor avgör när vertikalt avstånd avslutar en tabell, vilket spelar roll för tabeller med enstaka tomma rader

Var ärlig om begränsningarna. Linjerade tabeller extraheras tillförlitligt. Rent justerade blanksteg-tabeller extraheras väl. Tabeller med roterad text, nästlade tabeller, eller celler vars innehåll radbryts till vad som ser ut som en annan rad kommer att behöva granskning oavsett hur parametrarna är satta. För dessa ger den strukturerade textmodellen dig råmaterialet för att bygga en domänspecifik läsare, beskriven i strukturerade textblock och läsordning

En användbar kombination: när ett skannat dokument helt saknar text har tabelldetektering inget att arbeta med förrän ett textlager finns. Lägg till ett först, som beskrivs i att lägga till ett sökbart textlager i skannade PDF-filer, och extrahera sedan. Ordrutorna en OCR-leverantör returnerar är exakt den indata blanksteg-detektering behöver

Tabellextraktion, strukturerad text och flödesomformatering läser alla från samma sidmodell i Delphi, C++Builder och Lazarus; det fullständiga API:et beskrivs på sidan för PDFium Component för Delphi