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
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
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
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