PDFium Component erkennt Tabellen auf einer PDF-Seite und liefert sie als Zellenraster mit Zeilen- und Spalten-Spans, Kopfzeilen und einem Konfidenzwert zurück, über ExtractTables für eine Seite und ExtractDocumentTables für ein ganzes Dokument. Jede Tabelle lässt sich mit einem Aufruf in CSV oder JSON umwandeln, und Tabellen, die über einen Seitenumbruch hinweg fortlaufen, können zu einer Fortsetzungskette verknüpft werden
PDF kennt kein Tabellenobjekt. Eine Tabelle in einem PDF ist eine Menge von Textläufen, die so positioniert sind, dass ein Mensch sie als Raster liest, manchmal mit gezeichneten Linien darum, oft ohne. Das Raster zu rekonstruieren bedeutet, eine Absicht zu rekonstruieren, die die Datei nie festgehalten hat, weshalb jedes Extraktionswerkzeug leicht unterschiedliche Ergebnisse liefert und weshalb ein Werkzeug, das seine Konfidenz mitteilt, nützlicher ist als eines, das das nicht tut
Zwei Erkennungsmodi für zwei Arten von Tabellen
Die liniierte Erkennung nutzt die gezeichneten Linien. Jedes gestrichene Pfadsegment wird über die Matrix des Seitenobjekts in Seitenkoordinaten transformiert, horizontale und vertikale Linien werden geschnitten, und die Schnittpunkte bilden zusammenhängende Komponenten. Jede Komponente wird zu ihrem eigenen sortierten Raster aus X- und Y-Positionen, was verhindert, dass zwei getrennte Tabellen auf derselben Seite zu einem unsinnigen Raster verschmolzen werden
Die Leerraum-Erkennung behandelt Tabellen, die mit Ausrichtung statt mit Linien gezeichnet sind. Wortboxen werden zu visuellen Zeilen gruppiert, Lücken innerhalb einer Zeile teilen sie in Kandidatenspalten auf, und eine Tabelle wird nur akzeptiert, wenn mindestens MinRows Zeilen mindestens MinColumns linksbündige Anker innerhalb von AlignmentTolerance wiederholen. Der Zeilenlückenfaktor liegt standardmäßig bei 3, was den für 12-Punkt-Text typischen Grundlinienabstand von etwa 30 Punkt abdeckt, ohne dass eine einzelne Zeile mit mehreren Textläufen sich als Tabelle ausgeben kann
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-basiert
Options := TPdfTableExtractionOptions.Default;
Options.DetectRuledTables := True;
Options.DetectWhitespaceTables := True;
Options.MinConfidence := 0.6; // Standard ist 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;
Wie werden verbundene Zellen wiederhergestellt?
Das ist der Teil, den naive Extraktoren falsch machen. Eine verbundene Zelle lässt sich nicht allein aus dem globalen Raster erkennen, denn das Raster wird aus allen Linien auf der Seite abgeleitet, und einem verbundenen Bereich fehlt schlicht die innere Linie, die ihn getrennt hätte
Die hier verwendete Regel ist lokal: Zwei benachbarte Basiszellen werden verbunden, wenn keine Begrenzungslinie das Intervall zwischen ihnen abdeckt. Union-Find verbindet sie, die entstehenden rechteckigen Komponenten werden zu RowSpan- und ColumnSpan-Werten, und Text wird anhand seines Mittelpunkts einer Basiszelle zugeordnet und folgt dann dieser Zelle zu ihrer Verschmelzungswurzel. So bleiben die Kosten außerdem linear in Wörtern plus Zellen, statt des quadratischen Scans, den man erhält, wenn man jedes Wort gegen jede Zelle prüft
Der praktische Effekt ist, dass eine Finanztabelle mit einer verbundenen „Gesamt“-Kopfzelle, die drei Spalten überspannt, mit einer Zelle mit Span drei herauskommt, statt mit einer befüllten Zelle und zwei mysteriös leeren
Fortsetzung über Seiten hinweg
Lange Tabellen brechen über Seiten hinweg um, und jedes Seitenfragment als eigenständige Tabelle zu behandeln zwingt den Aufrufer, sie zusammenzunähen. ExtractDocumentTables kann sie stattdessen verknüpfen, jedoch nur unter strengen Bedingungen: Das Fragment muss die unterste Tabelle auf der früheren Seite sein, das nächste muss die oberste Tabelle auf der folgenden Seite sein, die Seitenzahlen müssen benachbart sein, und die Spaltengrenzen müssen übereinstimmen
Alle vier Bedingungen zusammen verhindern den naheliegenden Fehler, nämlich jede vierspaltige Tabelle eines Dokuments zu einer imaginären Mega-Tabelle zu verketten, nur weil sie zufällig dieselbe Spaltenzahl haben. Gelten die Bedingungen, teilen sich die Tabellen eine Fortsetzungsgruppenkennung und tragen Fortsetzungsmetadaten; gelten sie nicht, erhalten Sie getrennte Tabellen und können selbst entscheiden
Die dokumentweite Extraktion teilt sich die Budgets MaxCells und MaxTables über Seiten hinweg, statt sie pro Seite zurückzusetzen, und stellt die aktive Seite in einem finally-Block wieder her, sodass ein Extraktionslauf in einem Betrachter den Benutzer auf der Seite belässt, auf der er war
Exportieren, ohne die Daten zu beschädigen
Beide Exporter gehen bewusst mit dem Escaping um. CSV setzt Felder immer in Anführungszeichen und verdoppelt interne Anführungszeichen, was den klassischen Fehler vermeidet, bei dem eine Zelle mit einem Komma stillschweigend zu zwei Spalten wird. Bei verbundenen Zellen wird Inhalt nur am oben-links-Anker ausgegeben, sodass ein CSV-Roundtrip eine übergreifende Kopfzelle nicht über die von ihr abgedeckten Spalten dupliziert
JSON bewahrt Unicode statt es nach ASCII zu escapen, escapt Steuerzeichen und enthält die Metadaten, die ein Konsument zur Qualitätsbeurteilung braucht: Erkennungsmodus, Konfidenz, Grenzen, Span-Werte, Kopfzeilen-Flags und Fortsetzungsinformationen. Speisen Sie extrahierte Tabellen in ein nachgelagertes System ein, bevorzugen Sie JSON, denn eine CSV-Zeile kann Ihnen nicht sagen, dass die Tabelle, aus der sie stammt, mit 0,51 Konfidenz bewertet wurde:
// Dokumentweite Extraktion, nur Tabellen behalten, denen man vertrauen kann
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;
Feinabstimmung, und wissen, wann man aufhören sollte
Drei Einstellungen zählen mehr als der Rest. MinConfidence ist das Qualitätstor, und 0,5 ist bewusst nachsichtig gewählt; erhöhen Sie es für automatisierte Verarbeitung und senken Sie es für eine Review-UI, in der ein Mensch jedes Ergebnis bestätigt. MinColumnGap entscheidet, was im Leerraum-Modus als Spaltengrenze zählt, und eng gesetzte Tabellen in dichten Berichten benötigen möglicherweise eine Reduzierung gegenüber dem 12-Punkt-Standard. MaxRowGapFactor entscheidet, wann vertikaler Abstand eine Tabelle beendet, was bei Tabellen mit gelegentlichen leeren Zeilen eine Rolle spielt
Seien Sie ehrlich über die Grenzen. Liniierte Tabellen extrahieren zuverlässig. Sauber ausgerichtete Leerraum-Tabellen extrahieren gut. Tabellen mit gedrehtem Text, verschachtelte Tabellen oder Zellen, deren Inhalt in etwas umbricht, das wie eine weitere Zeile aussieht, benötigen unabhängig von den Parametern eine Überprüfung. Für diese liefert das Structured-Text-Modell das Rohmaterial, um einen domänenspezifischen Reader zu bauen, beschrieben unter strukturierte Textblöcke und Lesereihenfolge
Eine nützliche Kombination: Hat ein gescanntes Dokument überhaupt keinen Text, hat die Tabellenerkennung nichts, womit sie arbeiten kann, bis eine Textebene existiert. Fügen Sie zuerst eine hinzu, wie unter Hinzufügen einer durchsuchbaren Textebene zu gescannten PDFs beschrieben, und extrahieren Sie dann. Die Wortboxen, die ein OCR-Provider liefert, sind genau die Eingabe, die die Leerraum-Erkennung braucht
Tabellenextraktion, strukturierter Text und Reflow lesen in Delphi, C++Builder und Lazarus alle aus demselben Seitenmodell; die vollständige API ist auf der Seite der PDFium Component für Delphi beschrieben