Teknisk artikel

Udtræk tabeller fra PDF-sider i Delphi med PDFium

PDFium Component registrerer tabeller på en PDF-side og returnerer dem som et gitter af celler med række- og kolonnespans, overskriftsrækker og en tillidsværdi, via ExtractTables for én side og ExtractDocumentTables for et helt dokument. Hver tabel konverteres til CSV eller JSON med ét kald, og tabeller, der fortsætter over et sideskift, kan kædes sammen i en fortsættelseskæde

PDF har intet tabelobjekt. En tabel i en PDF er et sæt tekst-runs placeret, så et menneske læser dem som et gitter, nogle gange med linjer tegnet omkring dem og ofte uden. At genskabe gitteret betyder at rekonstruere en hensigt, filen aldrig registrerede, hvilket er grunden til, at ethvert udtrækningsværktøj giver lidt forskellige resultater, og hvorfor et værktøj, der fortæller dig dets tillidsværdi, er mere nyttigt end ét, der ikke gør

To detektionstilstande til to slags tabeller

Linjebaseret detektion bruger de tegnede linjer. Hvert stroked path-segment transformeres til sidekoordinater via sideobjektets matrix, vandrette og lodrette linjer skæres, og skæringspunkterne danner forbundne komponenter. Hver komponent bliver sit eget sorterede gitter af X- og Y-positioner, hvilket er, hvad der forhindrer to separate tabeller på samme side i at blive sammenlagt til ét meningsløst gitter

Whitespace-detektion håndterer tabeller tegnet med justering i stedet for linjer. Ordbokse grupperes i visuelle rækker, mellemrum inde i en række opdeler den i kandidatkolonner, og en tabel accepteres kun, når mindst MinRows rækker gentager mindst MinColumns venstrejusterede ankre inden for AlignmentTolerance. Rækkemellemrums-faktoren er som standard 3, hvilket dækker den cirka 30-punkts baseline-afstand, der er typisk for 12-punkts tekst, uden at lade en enkelt linje med flere tekst-runs udgive sig for en tabel

Diagram af PDFium Components tabeldetektionspipeline i Delphi, hvor skæringer mellem tegnede linjer og whitespace-justerede ordrækker fodrer én scoret tabelpost med CSV- og JSON-eksport
Linjebaseret detektion skærer tegnede streger, mens whitespace-detektion tæller justerede ordboks-rækker; kandidater, der klarer MinRows og MinColumns, kommer ud med en tillidsværdi og DetectionMode tilknyttet
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-indekseret

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

Hvordan genskabes sammenlagte celler?

Dette er den del, naive udtrækkere tager fejl af. En sammenlagt celle kan ikke identificeres ud fra det globale gitter alene, fordi gitteret er udledt af alle linjerne på siden, og en sammenlagt region mangler ganske enkelt den indre linje, der ville have adskilt den

Reglen, der bruges her, er lokal: to tilstødende basisceller sammenlægges, når ingen grænselinje dækker intervallet mellem dem. Union-find sammenslutter dem, de resulterende rektangulære komponenter bliver RowSpan- og ColumnSpan-værdier, og tekst tildeles en basiscelle efter dens centerpunkt og følger derefter den celle til dens sammenlægningsrod. At gøre det på denne måde holder også omkostningen lineær i ord plus celler, i stedet for den kvadratiske scanning, du får ved at teste hvert ord mod hver celle

Diagram af genskabelse af sammenlagte celler i PDFium tabeludtrækning til Delphi, hvor union-find sammenslutter tilstødende basisceller, når ingen grænselinje dækker intervallet mellem dem, hvilket giver RowSpan og ColumnSpan
Union-find sammenlægger nabo-basisceller, hvis fælles interval ikke bærer nogen tegnet grænse, så en sammenlagt overskrift returneres som én celle med ColumnSpan sat, i stedet for én udfyldt celle flankeret af tomme

Den praktiske effekt er, at en finansiel tabel med en sammenlagt "Total"-overskrift, der spænder over tre kolonner, kommer ud med én celle med span tre, i stedet for én udfyldt celle og to mystisk tomme

Fortsættelse på tværs af sider

Lange tabeller brydes over sider, og at behandle hver sides fragment som en uafhængig tabel tvinger den kaldende part til at sy dem sammen. ExtractDocumentTables kan i stedet kæde dem, men kun under strenge betingelser: fragmentet skal være den nederste tabel på den tidligere side, den næste skal være den øverste tabel på den følgende side, sidetallene skal være tilstødende, og kolonnegrænserne skal matche

Alle fire betingelser tilsammen er, hvad der forhindrer den oplagte fejl, som er at kæde hver fire-kolonnes tabel i et dokument sammen til én imaginær megatabel, fordi de tilfældigvis deler et kolonneantal. Når betingelserne er opfyldt, deler tabellerne en fortsættelsesgruppe-identifikator og bærer fortsættelsesmetadata; når de ikke er, får du separate tabeller og kan selv beslutte

Diagram af tabelfortsættelse på tværs af PDF-sider i Delphi, hvor fire strenge porte afgør, om det nederste fragment på én side kobles sammen med det øverste fragment på den næste
Udtrækning på dokumentniveau kæder kun fragmenter sammen, når alle fire porte er opfyldt, hvilket forhindrer urelaterede fire-kolonnes tabeller i at smelte sammen til én imaginær megatabel

Udtrækning på dokumentniveau deler MaxCells- og MaxTables-budgetterne på tværs af sider frem for at nulstille dem pr. side, og den gendanner den aktive side i en finally-blok, så et udtrækningskørsel i en fremviser efterlader brugeren kigge på den side, de var på

Eksport uden at korrumpere dataene

Begge eksportører er bevidste om escaping. CSV citerer altid felter og fordobler interne citationstegn, hvilket undgår den klassiske fejl, hvor en celle, der indeholder et komma, stiltiende bliver to kolonner. For sammenlagte celler udsendes indhold kun ved det øverste venstre anker, så en CSV-tur frem og tilbage ikke duplikerer en spændende overskrift på tværs af de kolonner, den dækker

JSON bevarer Unicode frem for at escape det til ASCII, escaper kontroltegn, og inkluderer den metadata, en forbruger har brug for til at vurdere kvaliteten: detektionstilstand, tillidsværdi, grænser, span-værdier, overskriftsflag og fortsættelsesinformation. Hvis du fodrer udtrukne tabeller ind i et downstream-system, foretræk JSON, fordi en CSV-række ikke kan fortælle dig, at tabellen, den kom fra, scorede 0,51 i tillidsværdi:

// Dokumentbred udtrækning, der kun beholder tabeller, det er værd at stole 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, og at vide hvornår man skal stoppe

Tre indstillinger betyder mere end resten. MinConfidence er kvalitetsporten, og 0,5 er bevidst tilladende; hæv den til automatiseret indtag, og sænk den til en gennemgangs-UI, hvor et menneske bekræfter hvert resultat. MinColumnGap afgør, hvad der tæller som en kolonnegrænse i whitespace-tilstand, og tætsatte tabeller i tætte rapporter kan have brug for, at den sænkes fra 12-punkts standarden. MaxRowGapFactor afgør, hvornår lodret afstand afslutter en tabel, hvilket betyder noget for tabeller med lejlighedsvise blanke rækker

Vær ærlig om grænserne. Linjebaserede tabeller udtrækkes pålideligt. Rent justerede whitespace-tabeller udtrækkes godt. Tabeller med roteret tekst, indlejrede tabeller, eller celler hvis indhold ombrydes til noget, der ligner endnu en række, vil have brug for gennemgang, uanset hvordan parametrene er sat. Til disse giver den strukturerede tekstmodel dig råmaterialet til at bygge en domænespecifik læser, beskrevet i strukturerede tekstblokke og læserækkefølge

Én nyttig kombination: når et scannet dokument slet ingen tekst har, har tabeldetektion intet at arbejde med, før et tekstlag findes. Tilføj ét først, som beskrevet i tilføjelse af et søgbart tekstlag til scannede PDF'er, og udtræk så. De ordbokse, en OCR-udbyder returnerer, er præcis det input, whitespace-detektion har brug for

Tabeludtrækning, struktureret tekst og reflow læser alle fra den samme sidemodel i Delphi, C++Builder og Lazarus; det fulde API er beskrevet på PDFium Component for Delphi-siden