Teknisk artikkel

Trekk ut tabeller fra PDF-sider i Delphi med PDFium

PDFium Component oppdager tabeller på en PDF-side og returnerer dem som et rutenett av celler med rad- og kolonnespenn, topptekstrader og en tillitsverdi, gjennom ExtractTables for én side og ExtractDocumentTables for et helt dokument. Hver tabell konverteres til CSV eller JSON med ett kall, og tabeller som fortsetter over et sideskift kan lenkes inn i en fortsettelseskjede

PDF har ikke noe tabellobjekt. En tabell i en PDF er et sett med tekst-runninger posisjonert slik at et menneske leser dem som et rutenett, noen ganger med linjer tegnet rundt dem og ofte uten. Å gjenopprette rutenettet betyr å rekonstruere en hensikt filen aldri registrerte, som er grunnen til at hvert uttrekksverktøy produserer litt forskjellige resultater, og hvorfor et verktøy som forteller deg tilliten sin er mer nyttig enn ett som ikke gjør det

To deteksjonsmodi for to typer tabeller

Linjert deteksjon bruker de tegnede linjene. Hvert strøkne banesegment transformeres til sidekoordinater gjennom sideobjektets matrise, horisontale og vertikale linjer skjæres, og skjæringspunktene danner sammenhengende komponenter. Hver komponent blir sitt eget sorterte rutenett av X- og Y-posisjoner, som er det som hindrer to separate tabeller på samme side fra å bli slått sammen til ett meningsløst rutenett

Mellomrom-deteksjon håndterer tabeller tegnet med justering i stedet for linjer. Ordbokser grupperes inn i visuelle rader, mellomrom innenfor en rad splitter den inn i kandidatkolonner, og en tabell godtas bare når minst MinRows rader gjentar minst MinColumns venstrejusterte ankere innenfor AlignmentTolerance. Radmellomrom-faktoren er som standard 3, som dekker den omtrent 30-punkts grunnlinjeavstanden typisk for 12-punkts tekst uten å la en enkelt linje med flere tekst-runninger utgi seg for en tabell

Diagram av PDFium Components tabelldeteksjonspipeline i Delphi, der linjeskjæringspunkter og mellomromsjusterte ordrader mater inn i én poengsatt tabellpost med CSV- og JSON-eksport
Linjert deteksjon skjærer tegnede strøk mens mellomrom-deteksjon teller justerte ordboks-rader; kandidater som passerer MinRows og MinColumns kommer ut med en konfidensskår 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-based

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

Hvordan gjenopprettes sammenslåtte celler?

Dette er delen naive uttrekkere gjør feil. En sammenslått celle kan ikke identifiseres fra det globale rutenettet alene, fordi rutenettet er avledet fra alle linjene på siden, og et sammenslått område mangler rett og slett den interne linjen som ville ha skilt det

Regelen brukt her er lokal: to tilstøtende basisceller slås sammen når ingen grenselinje dekker intervallet mellom dem. Union-find slår dem sammen, de resulterende rektangulære komponentene blir RowSpan- og ColumnSpan-verdier, og tekst tildeles en basiscelle etter dens senterpunkt og følger deretter den cellen til sin sammenslåingsrot. Å gjøre det på denne måten holder også kostnaden lineær i ord pluss celler, i stedet for den kvadratiske skanningen du får av å teste hvert ord mot hver celle

Diagram av gjenoppretting av sammenslåtte celler i PDFium tabelluttrekk for Delphi, der union-find slår sammen tilstøtende basisceller når ingen grenselinje dekker intervallet mellom dem, og gir RowSpan og ColumnSpan
Union-find slår sammen nabo-basisceller hvis delte intervall ikke bærer noen tegnet grense, slik at en sammenslått header returneres som én celle med ColumnSpan satt, i stedet for én utfylt celle flankert av tomme

Den praktiske effekten er at en finansiell tabell med en sammenslått «Total»-header som spenner over tre kolonner, kommer ut med én celle med spenn tre, heller enn én utfylt celle og to mystisk tomme

Fortsettelse over sider

Lange tabeller brytes over sider, og å behandle hver sides fragment som en uavhengig tabell tvinger den kallende parten til å sy dem sammen. ExtractDocumentTables kan lenke dem i stedet, men bare under strenge betingelser: fragmentet må være den nederste tabellen på den tidligere siden, den neste må være den øverste tabellen på den følgende siden, sidetallene må være tilstøtende, og kolonnegrensene må matche

Alle fire betingelsene sammen er det som forhindrer den åpenbare feilen, som er å kjede hver fire-kolonners tabell i et dokument sammen til én innbilt mega-tabell fordi de tilfeldigvis deler et kolonneantall. Når betingelsene holder, deler tabellene en fortsettelsesgruppe-identifikator og bærer fortsettelsesmetadata; når de ikke gjør det, får du separate tabeller og kan bestemme selv

Diagram av tabellfortsettelse over PDF-sider i Delphi, der fire strenge porter avgjør om det nederste fragmentet på én side kobles til det øverste fragmentet på den neste
Dokumentnivå-uttrekk kobler fragmenter bare når alle fire portene holder, noe som hindrer urelaterte fire-kolonners tabeller fra å smelte sammen til én innbilt mega-tabell

Uttrekk på dokumentnivå deler budsjettene MaxCells og MaxTables på tvers av sider heller enn å nullstille dem per side, og det gjenoppretter den aktive siden i en finally-blokk, slik at en uttrekkskjøring i en visning etterlater brukeren på siden de var på

Eksport uten å korrumpere dataene

Begge eksportørene er bevisste om escaping. CSV siterer alltid felt og dobler interne anførselstegn, som unngår den klassiske svikten der en celle som inneholder et komma stille blir to kolonner. For sammenslåtte celler gis innhold kun ut ved øverste-venstre-ankeret, slik at en CSV-rundtur ikke dupliserer en spennende header på tvers av kolonnene den dekker

JSON bevarer Unicode heller enn å escape det til ASCII, escaper kontrolltegn, og inkluderer metadataen en konsument trenger for å vurdere kvalitet: deteksjonsmodus, tillit, grenser, spennverdier, header-flagg og fortsettelsesinformasjon. Hvis du mater uttrukne tabeller inn i et nedstrøms system, foretrekk JSON, fordi en CSV-rad ikke kan fortelle deg at tabellen den kom fra scoret 0,51 i tillit:

// Dokumentomfattende uttrekk, som kun beholder tabeller verdt å 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 å vite når man skal stoppe

Tre innstillinger betyr mer enn resten. MinConfidence er kvalitetsporten, og 0,5 er bevisst tillatende; øk den for automatisert inntak og senk den for et gjennomgangs-UI der et menneske bekrefter hvert resultat. MinColumnGap bestemmer hva som teller som en kolonnegrense i mellomrom-modus, og tett satte tabeller i tette rapporter kan trenge den redusert fra 12-punkts standarden. MaxRowGapFactor bestemmer når vertikal avstand avslutter en tabell, som betyr noe for tabeller med sporadiske tomme rader

Vær ærlig om grensene. Linjerte tabeller trekkes ut pålitelig. Rent justerte mellomrom-tabeller trekkes ut godt. Tabeller med rotert tekst, nestede tabeller, eller celler hvis innhold brytes inn i noe som ser ut som en annen rad, vil trenge gjennomgang uansett hvordan parameterne er satt. For disse gir den strukturerte tekstmodellen deg råmaterialet til å bygge en domenespesifikk leser, beskrevet i strukturerte tekstblokker og leserekkefølge

Én nyttig sammenkobling: når et skannet dokument ikke har noen tekst i det hele tatt, har tabelldeteksjon ingenting å jobbe med før et tekstlag eksisterer. Legg til ett først, som beskrevet i å legge til et søkbart tekstlag i skannede PDF-er, og trekk deretter ut. Ordboksene en OCR-leverandør returnerer er nøyaktig inndataen mellomrom-deteksjon trenger

Tabelluttrekk, strukturert tekst og omflyt leser alle fra den samme sidemodellen i Delphi, C++Builder og Lazarus; det fullstendige API-et er beskrevet på PDFium Component for Delphi-siden