PDFium Component rileva le tabelle in una pagina PDF e le restituisce come una griglia di celle con span di riga e colonna, righe di intestazione e un valore di confidenza, tramite ExtractTables per una pagina ed ExtractDocumentTables per un intero documento. Ogni tabella si converte in CSV o JSON con una sola chiamata, e le tabelle che continuano oltre un'interruzione di pagina possono essere collegate in una catena di continuazione
Il PDF non ha un oggetto tabella. Una tabella in un PDF è un insieme di text run posizionati in modo che un essere umano li legga come una griglia, a volte con linee disegnate attorno e spesso senza. Recuperare la griglia significa ricostruire un'intenzione che il file non ha mai registrato, motivo per cui ogni strumento di estrazione produce risultati leggermente diversi e perché uno strumento che vi dice la propria confidenza è più utile di uno che non lo fa
Due modalità di rilevamento per due tipi di tabella
Il rilevamento a righe usa le linee disegnate. Ogni segmento di percorso tracciato viene trasformato in coordinate di pagina attraverso la matrice dell'oggetto pagina, le linee orizzontali e verticali vengono intersecate, e le intersezioni formano componenti connesse. Ogni componente diventa la propria griglia ordinata di posizioni X e Y, il che è ciò che impedisce a due tabelle separate sulla stessa pagina di venire unite in un'unica griglia insensata
Il rilevamento a spazi bianchi gestisce tabelle disegnate con allineamento invece di linee. Le word box vengono raggruppate in righe visive, gli spazi vuoti all'interno di una riga la suddividono in colonne candidate, e una tabella viene accettata solo quando almeno MinRows righe ripetono almeno MinColumns ancoraggi allineati a sinistra entro AlignmentTolerance. Il fattore di distanza tra righe è predefinito a 3, che copre la spaziatura di linea di base di circa 30 punti tipica del testo a 12 punti senza permettere a una singola riga contenente più text run di travestirsi da tabella
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; // basato su 1
Options := TPdfTableExtractionOptions.Default;
Options.DetectRuledTables := True;
Options.DetectWhitespaceTables := True;
Options.MinConfidence := 0.6; // il default è 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;
Come vengono recuperate le celle unite?
Questa è la parte in cui gli estrattori ingenui sbagliano. Una cella unita non può essere identificata dalla sola griglia globale, perché la griglia deriva da tutte le linee della pagina e una regione unita manca semplicemente della linea interna che l'avrebbe separata
La regola usata qui è locale: due celle base adiacenti vengono unite quando nessuna linea di confine copre l'intervallo tra loro. Union-find le unisce, le componenti rettangolari risultanti diventano i valori RowSpan e ColumnSpan, e il testo viene assegnato a una cella base in base al suo punto centrale e poi segue quella cella fino alla sua radice di unione. Farlo in questo modo mantiene anche il costo lineare in parole più celle, invece della scansione quadratica che si ottiene testando ogni parola contro ogni cella
L'effetto pratico è che una tabella finanziaria con un'intestazione "Total" unita che copre tre colonne esce con una cella di span tre, invece di una cella popolata e due misteriose celle vuote
Continuazione tra pagine
Le tabelle lunghe si interrompono attraverso le pagine, e trattare il frammento di ogni pagina come una tabella indipendente costringe il chiamante a ricucirle. ExtractDocumentTables può invece collegarle, ma solo in condizioni rigorose: il frammento deve essere la tabella più in basso sulla pagina precedente, la successiva deve essere la tabella più in alto sulla pagina seguente, i numeri di pagina devono essere adiacenti, e i confini delle colonne devono corrispondere
Tutte e quattro le condizioni insieme sono ciò che previene l'errore ovvio, ovvero concatenare ogni tabella a quattro colonne di un documento in un'unica mega-tabella immaginaria solo perché condividono un numero di colonne. Quando le condizioni sono soddisfatte, le tabelle condividono un identificatore di gruppo di continuazione e portano metadati di continuazione; quando non lo sono, ottenete tabelle separate e potete decidere da soli
L'estrazione a livello documento condivide i budget MaxCells e MaxTables tra le pagine invece di azzerarli per pagina, e ripristina la pagina attiva in un blocco finally, così un'estrazione eseguita in un visualizzatore lascia l'utente a guardare la pagina in cui si trovava
Esportare senza corrompere i dati
Entrambi gli esportatori sono deliberati riguardo l'escaping. Il CSV mette sempre i campi tra virgolette e raddoppia le virgolette interne, il che evita il classico fallimento in cui una cella contenente una virgola diventa silenziosamente due colonne. Per le celle unite, il contenuto viene emesso solo all'ancoraggio in alto a sinistra, così un round trip CSV non duplica un'intestazione che copre più colonne attraverso le colonne che copre
Il JSON preserva Unicode invece di trasformarlo in escape ASCII, esegue l'escape dei caratteri di controllo, e include i metadati di cui un consumatore ha bisogno per giudicare la qualità: modalità di rilevamento, confidenza, limiti, valori di span, flag di intestazione e informazioni di continuazione. Se state alimentando tabelle estratte in un sistema a valle, preferite JSON, perché una riga CSV non può dirvi che la tabella da cui proviene ha ottenuto una confidenza di 0,51:
// Estrazione a livello di documento, mantenendo solo le tabelle degne di fiducia
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;
Regolazione, e sapere quando fermarsi
Tre impostazioni contano più delle altre. MinConfidence è il gate di qualità, e 0,5 è deliberatamente permissivo; alzatelo per l'ingestione automatizzata e abbassatelo per un'interfaccia di revisione in cui un essere umano conferma ogni risultato. MinColumnGap decide cosa conta come confine di colonna in modalità spazi bianchi, e le tabelle strettamente impostate in report densi potrebbero richiedere di ridurlo rispetto al default di 12 punti. MaxRowGapFactor decide quando la distanza verticale termina una tabella, il che conta per tabelle con righe vuote occasionali
Siate onesti riguardo i limiti. Le tabelle a righe si estraggono in modo affidabile. Le tabelle allineate in modo pulito a spazi bianchi si estraggono bene. Le tabelle con testo ruotato, tabelle annidate, o celle il cui contenuto va a capo in quella che sembra un'altra riga richiederanno revisione indipendentemente da come sono impostati i parametri. Per queste, il modello di testo strutturato vi fornisce la materia prima per costruire un lettore specifico per il dominio, descritto in blocchi di testo strutturato e ordine di lettura
Un abbinamento utile: quando un documento scansionato non ha alcun testo, il rilevamento delle tabelle non ha nulla su cui lavorare finché non esiste un livello di testo. Aggiungetene uno prima, come descritto in aggiungere un livello di testo ricercabile a PDF scansionati, poi estraete. Le word box che un provider OCR restituisce sono esattamente l'input di cui il rilevamento a spazi bianchi ha bisogno
L'estrazione delle tabelle, il testo strutturato e il reflow leggono tutti dallo stesso modello di pagina in Delphi, C++Builder e Lazarus; l'API completa è descritta nella pagina di PDFium Component per Delphi