Tabellen aus PDF in Python extrahieren
Das Extrahieren von Tabellen aus PDF-Dokumenten gehört zu den häufigsten Aufgaben in Dokumentenverarbeitungs-Pipelines. Ob Sie Finanzdaten aus Jahresberichten abrufen, Produktkataloge auslesen oder strukturierte Daten in einen LLM einspeisen möchten — zuverlässige Tabellenextraktion ist unerlässlich. Dieser Leitfaden deckt alles ab, was Sie zur Extraktion von Tabellen aus PDFs in Python wissen müssen: von einfachen Einzeilern bis hin zu produktionsreifen Workflows für mehrseitige Tabellen.
Erkennungsmodul
PDF Oxide verwendet die universelle Kanten → Ausrichten/Zusammenführen → Schnittpunkte → Zellen → Gruppen-Erkennungspipeline — denselben Ansatz wie Tabula, pdfplumber und PyMuPDF, implementiert in reinem Rust.
Erkennungsfähigkeiten:
- Schnittpunktbasiert — findet H×V-Linienkreuzungen, erstellt Zellen aus Vier-Ecken-Rechtecken, gruppiert per Union-Find zu Tabellen.
- Erweitertes Raster — wenn horizontale und vertikale Linien in verschiedenen Seitenbereichen liegen, wird ein virtuelles Raster aus dem kartesischen Produkt aller Koordinaten aufgebaut.
- Spaltenorientierte Texterkennung — segmentiert zweispaltige Layouts per X-Projektions-Histogramm und erkennt dann textbasierte Tabellen je Spalte.
- Durch horizontale Linien begrenzte Texttabellen — erkennt Tabellen, die durch horizontale Linien ohne vertikale Linien begrenzt sind (häufig in Fachaufsätzen).
- Hybride Zeilenerkennung — leitet Zeilengrenzen aus Text-Y-Positionen ab, wenn nur vertikale Ränder vorhanden sind (Rechnungspositionen).
- Rekonstruktion gepunkteter/gestrichelter Linien — fügt kurze Liniensegmente zu durchgehenden Kanten zusammen.
- Aufteilung durch Abschnittstrennlinien — teilt mehrabschnittliche Formulare an vollbreiten horizontalen Trennlinien auf.
- Kantenabdeckungsfilterung — entfernt verwaiste Kanten, die an keinem potenziellen Raster beteiligt sind.
Konfiguration
TableDetectionConfig bietet einstellbare Parameter:
| Feld | Standard | Beschreibung |
|---|---|---|
horizontal_strategy |
"lines_strict" |
"lines_strict", "lines", "text" oder "explicit" |
vertical_strategy |
"lines_strict" |
Gleiche Optionen |
v_split_gap |
20.0 pt |
Abstand zwischen vertikalen Linien, der die Aufteilung in separate Tabellen auslöst (vor v0.3.20 fest auf 4pt kodiert) |
snap_tolerance |
3.0 pt |
Toleranz für Kantenausrichtzusammenführung |
text_tolerance |
3.0 pt |
Toleranz für Textzeilenzusammenführung |
Verhaltensänderung
Ab v0.3.20 ist die Standardstrategie für Python extract_tables() Both (erkennt über Linien und Text). Seiten, die auf den alten reinen Textstandard angewiesen waren, müssen horizontal_strategy="text" und vertical_strategy="text" explizit übergeben.
Die Python-Bindung liest vertical_strategy jetzt korrekt aus dem table_settings-Dictionary — zuvor wurde der Parameter stillschweigend ignoriert.
Rendering
Extrahierte Tabellen werden mit leerzeichengefüllter Spaltenausrichtung ausgegeben (ersetzt die ASCII-Boxzeichnungszeichen früherer Versionen). Währungs- und Zahlenspalten werden automatisch rechtsbündig ausgerichtet. Formularnum-mernpräfixe ("1 Apr 11" → "Apr 11") und dekorative Strich/Unterstrich-Zellen ("------") werden beim Rendering entfernt.
Tabellendaten aus einem PDF per Markdown-Konvertierung extrahieren:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("invoice.pdf")
md = doc.to_markdown(0, detect_headings=True)
print(md)
# Output includes tables in GFM format:
# | Item | Qty | Price |
# |------|-----|-------|
# | Widget | 10 | $9.99 |
WASM
import { WasmPdfDocument } from "pdf-oxide-wasm";
const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdown(0);
console.log(md);
// Output includes tables in GFM format:
// | Item | Qty | Price |
// |------|-----|-------|
// | Widget | 10 | $9.99 |
doc.free();
Rust
use pdf_oxide::PdfDocument;
let mut doc = PdfDocument::open("invoice.pdf")?;
let md = doc.to_markdown(0, true)?;
println!("{}", md);
Go
package main
import (
"fmt"
"log"
pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)
func main() {
doc, err := pdfoxide.Open("invoice.pdf")
if err != nil { log.Fatal(err) }
defer doc.Close()
md, err := doc.ToMarkdown(0)
if err != nil { log.Fatal(err) }
fmt.Println(md)
}
C#
using PdfOxide;
using var doc = PdfDocument.Open("invoice.pdf");
Console.WriteLine(doc.ToMarkdown(0));
Java
import fyi.oxide.pdf.PdfDocument;
try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("invoice.pdf"))) {
System.out.println(doc.toMarkdown(0));
}
PHP
use PdfOxide\PdfDocument;
$doc = PdfDocument::open('invoice.pdf');
echo $doc->toMarkdown(0);
$doc->close();
Ruby
require 'pdf_oxide'
PdfOxide::PdfDocument.open('invoice.pdf') do |doc|
puts doc.to_markdown(0)
end
C++
#include <pdf_oxide/pdf_oxide.hpp>
#include <iostream>
auto doc = pdf_oxide::Document::open("invoice.pdf");
std::cout << doc.to_markdown(0) << '\n';
Swift
import PdfOxide
let doc = try Document.open("invoice.pdf")
print(try doc.toMarkdown(0))
Kotlin
import fyi.oxide.pdf.PdfDocument
PdfDocument.open(java.nio.file.Path.of("invoice.pdf")).use { doc ->
println(doc.toMarkdown(0))
}
Dart
import 'package:pdf_oxide/pdf_oxide.dart';
final doc = PdfDocument.open('invoice.pdf');
print(doc.toMarkdown(0));
doc.close();
R
library(pdfoxide)
doc <- pdf_open("invoice.pdf")
cat(pdf_to_markdown(doc, 0))
Julia
using PdfOxide
doc = open_document("invoice.pdf")
println(to_markdown(doc, 0))
Zig
const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;
var doc = try pdf_oxide.Document.open("invoice.pdf");
const md = try doc.toMarkdown(a, 0);
defer a.free(md);
std.debug.print("{s}\n", .{md});
Scala
import fyi.oxide.pdf.PdfDocument
import scala.util.Using
Using.resource(PdfDocument.open("invoice.pdf")) { doc =>
println(doc.toMarkdown(0))
}
Clojure
(require '[pdf-oxide.core :as pdf])
(with-open [d (pdf/open "invoice.pdf")]
(println (pdf/to-markdown d 0)))
Objective-C
#import "POXPdfOxide.h"
NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"invoice.pdf" error:&err];
NSLog(@"%@", [doc toMarkdown:0 error:&err]);
Elixir
{:ok, doc} = PdfOxide.open("invoice.pdf")
{:ok, md} = PdfOxide.to_markdown(doc, 0)
IO.puts(md)
PDF Oxide erkennt tabellarische Layouts durch räumliche Analyse ausgerichteter Textblöcke und gibt GitHub Flavored Markdown-Tabellen aus.
Warum Tabellenextraktion aus PDFs so schwierig ist
Wer schon einmal versucht hat, eine Tabelle aus einem PDF zu kopieren und in eine Tabellenkalkulation einzufügen, weiß, dass das Ergebnis meistens chaotisch aussieht. Das ist kein Fehler des PDF-Betrachters — es spiegelt eine grundlegende Einschränkung des PDF-Formats selbst wider.
PDFs kennen kein Konzept einer „Tabelle". Im Gegensatz zu HTML, das <table>-, <tr>- und <td>-Tags zur Definition tabellarischer Struktur verwendet, speichert eine PDF-Datei nur Zeichenanweisungen: Dieses Zeichen an Koordinate (x, y) platzieren, eine Linie von Punkt A nach Punkt B zeichnen. Es gibt keine semantische Schicht, die aussagt: „Diese Zeichen gehören zu einer Zelle in Zeile 3, Spalte 2." Jede Tabellenextraktionsbibliothek muss diese Struktur durch Analyse der räumlichen Positionen von Text und Linien auf der Seite rekonstruieren.
Diese Rekonstruktion ist aus mehreren Gründen schwierig:
-
Tabellen mit und ohne Rahmen. Hat eine Tabelle sichtbare Rasterlinien, können Extraktionstools diese als Zellgrenzen nutzen. Rahmenlose Tabellen — häufig in Finanzberichten, Behördendokumenten und wissenschaftlichen Aufsätzen — haben überhaupt keine Linien. Die Bibliothek muss Spaltengrenzen allein aus den Leerraumabständen zwischen Textblöcken ableiten, was fehleranfällig ist, wenn Spalten unterschiedlich breit sind oder Zahlen rechtsbündig ausgerichtet sind.
-
Verbundene Zellen und überspannende Überschriften. Eine Kopfzelle, die drei Spalten überspannt, sieht wie ein einzelner breiter Textblock aus. Ohne Rasterlinien zur Abgrenzung hat ein Parser keine zuverlässige Möglichkeit zu erkennen, welche Spalten die Überschrift umfasst. Manche Bibliotheken handhaben das gut; viele erzeugen stillschweigend unlesbaren Output.
-
Mehrzeiliger Zelleninhalt. Wenn eine Zelle einen Absatz mit Zeilenumbruch enthält, behandelt naives zeilenbasiertes Parsen jede umgebrochene Zeile als eigene Tabellenzeile. Um diese Zeilen korrekt einer einzigen Zelle zuzuordnen, muss die vertikale Ausdehnung jeder Zeile verstanden werden.
-
Mehrseitige Tabellen. Große Tabellen erstrecken sich häufig über zwei oder mehr Seiten. Die Kopfzeile kann auf jeder Seite wiederholt werden oder auch nicht, und Seitenfußzeilen, Wasserzeichen oder Seitenzahlen können zwischen Tabellenzeilen erscheinen. Das Zusammenfügen dieser Fragmente zu einer einzigen kohärenten Tabelle erfordert seitenorientierte Logik.
-
Rotierter Text und nicht-standardmäßige Layouts. Manche PDFs verwenden rotierten Text für Spaltenüberschriften oder platzieren Tabellen in mehrspaltige Seitenlayouts. Solche Randfälle brechen die Annahmen, die die meisten Parser über die Links-nach-rechts-, Oben-nach-unten-Lesereihenfolge treffen.
Das Verstehen dieser Herausforderungen hilft bei der Wahl des richtigen Tools für spezifische Dokumente. Für einfache ausgerichtete Tabellen — die Mehrheit von Rechnungen, Auftragsbestätigungen und einfachen Berichten — funktioniert ein schneller räumlicher Analyseansatz wie PDF Oxide gut. Für Dokumente mit komplexen Zusammenführungen, rahmenlosen Layouts oder ungewöhnlicher Formatierung ist möglicherweise eine Bibliothek mit ausgefeilterer Heuristik erforderlich.
Tabellenextraktion: PDF Oxide vs. andere Bibliotheken
Die Wahl einer Bibliothek für PDF-Tabellenextraktion in Python hängt von den Dokumenten, den Leistungsanforderungen und dem benötigten Ausgabeformat ab. So vergleichen sich die wichtigsten Optionen:
| Bibliothek | Tabellenerkennung | Tabellen mit Rahmen | Tabellen ohne Rahmen | Ausgabeformat | Geschwindigkeit |
|---|---|---|---|---|---|
| PDF Oxide | Integriert | Ja | Grundlegend | Markdown/HTML | 0,8ms |
| pdfplumber | Integriert | Ja | Erweitert | Python-Listen | 23,2ms |
| Camelot | Integriert | Ja | Ja (lattice/stream) | DataFrames | ~50ms+ |
| PyMuPDF | Grundlegend (v1.23+) | Ja | Begrenzt | DataFrames | 4,6ms |
| pypdf | Nein | Nein | Nein | N/A | N/A |
| tabula-py | Integriert | Ja | Ja | DataFrames | ~100ms+ (Java) |
PDF Oxide ist mit großem Abstand die schnellste Option. Es erkennt Tabellen durch räumliche Analyse ausgerichteter Textblöcke und gibt saubere GitHub Flavored Markdown-Tabellen aus. Bei einer mittleren Extraktionszeit von 0,8ms ist es 29× schneller als pdfplumber und über 100× schneller als tabula-py. Es verarbeitet Tabellen mit Rahmen und einfache ausgerichtete rahmenlose Tabellen gut. Für LLM-Pipelines, in denen ohnehin Markdown-Ausgabe benötigt wird, ist es die natürliche Wahl.
pdfplumber verfügt über die ausgefeilteste Erkennung rahmelloser Tabellen. Die find_tables()-Methode verwendet konfigurierbare Strategien zur zeilenbasierten Spalten- und Zeilenerkennung aus Textausrichtung und verarbeitet verbundene Zellen und mehrzeiligen Zelleninhalt besser als die meisten Alternativen. Der Kompromiss ist Geschwindigkeit: 23,2ms pro Seite ist für Batch-Verarbeitung deutlich langsamer.
Camelot bietet zwei Erkennungsmodi — lattice (für Tabellen mit Rahmen) und stream (für rahmenlose Tabellen). Es erzeugt direkt pandas DataFrames, was für Datenanalyse-Workflows praktisch ist. Allerdings hängt es von Ghostscript und OpenCV ab, was die Installation schwerer macht, und seine Geschwindigkeit ist die langsamste unter den reinen Python-Optionen.
PyMuPDF (fitz) hat in Version 1.23 grundlegende Tabellenextraktion hinzugefügt. Es ist schnell (4,6ms) und eignet sich gut für einfache Tabellen mit Rahmen, aber die Unterstützung rahmelloser Tabellen ist im Vergleich zu pdfplumber oder Camelot begrenzt.
pypdf hat keine Tabellenerkennung. Es extrahiert reinen Text, weshalb Sie eigene Parsing-Logik schreiben müssten, um die Tabellenstruktur zu rekonstruieren.
tabula-py ist ein Python-Wrapper um die Java-basierte Tabula-Bibliothek. Es bietet gute Tabellenerkennung für Tabellen mit und ohne Rahmen, benötigt jedoch eine Java-Laufzeitumgebung und ist aufgrund des JVM-Startaufwands die langsamste Option. Es eignet sich besser für einmalige Extraktionsaufgaben als für Hochdurchsatz-Pipelines.
Für die meisten Produktions-Anwendungsfälle empfiehlt sich PDF Oxide als primären Extraktor für Geschwindigkeit und Einfachheit zu verwenden, und für die Teilmenge von Dokumenten mit komplexen Tabellenlayouts auf pdfplumber zurückzugreifen.
Installation
pip install pdf_oxide
Grundlegende Tabellenextraktion
Als Markdown-Tabellen
Der einfachste Ansatz — die Seite in Markdown konvertieren, das Tabellen in GFM-Syntax enthält:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("report.pdf")
for i in range(doc.page_count()):
md = doc.to_markdown(i, detect_headings=True)
if "|" in md: # Page contains a table
print(f"--- Page {i + 1} ---")
print(md)
WASM
const doc = new WasmPdfDocument(bytes);
for (let i = 0; i < doc.pageCount(); i++) {
const md = doc.toMarkdown(i);
if (md.includes("|")) { // Page contains a table
console.log(`--- Page ${i + 1} ---`);
console.log(md);
}
}
doc.free();
Rust
let mut doc = PdfDocument::open("report.pdf")?;
for i in 0..doc.page_count()? {
let md = doc.to_markdown(i, true)?;
if md.contains("|") {
println!("--- Page {} ---", i + 1);
println!("{}", md);
}
}
Go
doc, _ := pdfoxide.Open("report.pdf")
defer doc.Close()
n, _ := doc.PageCount()
for i := 0; i < n; i++ {
md, _ := doc.ToMarkdown(i)
if strings.Contains(md, "|") {
fmt.Printf("--- Page %d ---\n%s\n", i+1, md)
}
}
C#
using var doc = PdfDocument.Open("report.pdf");
for (int i = 0; i < doc.PageCount; i++)
{
var md = doc.ToMarkdown(i);
if (md.Contains("|"))
Console.WriteLine($"--- Page {i + 1} ---\n{md}");
}
Java
try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("report.pdf"))) {
for (int i = 0; i < doc.pageCount(); i++) {
String md = doc.toMarkdown(i);
if (md.contains("|")) { // Page contains a table
System.out.println("--- Page " + (i + 1) + " ---");
System.out.println(md);
}
}
}
PHP
$doc = PdfDocument::open('report.pdf');
for ($i = 0; $i < $doc->pageCount(); $i++) {
$md = $doc->toMarkdown($i);
if (str_contains($md, '|')) { // Page contains a table
echo "--- Page " . ($i + 1) . " ---\n";
echo $md;
}
}
$doc->close();
Ruby
PdfOxide::PdfDocument.open('report.pdf') do |doc|
doc.page_count.times do |i|
md = doc.to_markdown(i)
if md.include?('|') # Page contains a table
puts "--- Page #{i + 1} ---"
puts md
end
end
end
C++
auto doc = pdf_oxide::Document::open("report.pdf");
for (int i = 0; i < doc.page_count(); i++) {
auto md = doc.to_markdown(i);
if (md.find('|') != std::string::npos) { // Page contains a table
std::cout << "--- Page " << (i + 1) << " ---\n" << md << '\n';
}
}
Swift
let doc = try Document.open("report.pdf")
for i in 0..<(try doc.pageCount()) {
let md = try doc.toMarkdown(i)
if md.contains("|") { // Page contains a table
print("--- Page \(i + 1) ---")
print(md)
}
}
Kotlin
PdfDocument.open(java.nio.file.Path.of("report.pdf")).use { doc ->
for (i in 0 until doc.pageCount()) {
val md = doc.toMarkdown(i)
if (md.contains("|")) { // Page contains a table
println("--- Page ${i + 1} ---")
println(md)
}
}
}
Dart
final doc = PdfDocument.open('report.pdf');
for (var i = 0; i < doc.pageCount; i++) {
final md = doc.toMarkdown(i);
if (md.contains('|')) { // Page contains a table
print('--- Page ${i + 1} ---');
print(md);
}
}
doc.close();
R
doc <- pdf_open("report.pdf")
for (i in 0:(pdf_page_count(doc) - 1)) {
md <- pdf_to_markdown(doc, i)
if (grepl("\\|", md)) { # Page contains a table
cat(sprintf("--- Page %d ---\n%s\n", i + 1, md))
}
}
Julia
doc = open_document("report.pdf")
for i in 0:(page_count(doc) - 1)
md = to_markdown(doc, i)
if occursin("|", md) # Page contains a table
println("--- Page $(i + 1) ---")
println(md)
end
end
Zig
var doc = try pdf_oxide.Document.open("report.pdf");
const n = try doc.pageCount();
var i: i32 = 0;
while (i < n) : (i += 1) {
const md = try doc.toMarkdown(a, i);
defer a.free(md);
if (std.mem.indexOfScalar(u8, md, '|') != null) { // Page contains a table
std.debug.print("--- Page {d} ---\n{s}\n", .{ i + 1, md });
}
}
Scala
Using.resource(PdfDocument.open("report.pdf")) { doc =>
for (i <- 0 until doc.pageCount()) {
val md = doc.toMarkdown(i)
if (md.contains("|")) { // Page contains a table
println(s"--- Page ${i + 1} ---")
println(md)
}
}
}
Clojure
(with-open [d (pdf/open "report.pdf")]
(doseq [i (range (pdf/page-count d))]
(let [md (pdf/to-markdown d i)]
(when (.contains md "|") ; Page contains a table
(println (str "--- Page " (inc i) " ---"))
(println md)))))
Objective-C
NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"report.pdf" error:&err];
for (NSInteger i = 0; i < [doc pageCountError:&err]; i++) {
NSString *md = [doc toMarkdown:i error:&err];
if ([md containsString:@"|"]) { // Page contains a table
NSLog(@"--- Page %ld ---\n%@", (long)(i + 1), md);
}
}
Elixir
{:ok, doc} = PdfOxide.open("report.pdf")
{:ok, n} = PdfOxide.page_count(doc)
for i <- 0..(n - 1) do
{:ok, md} = PdfOxide.to_markdown(doc, i)
if String.contains?(md, "|") do # Page contains a table
IO.puts("--- Page #{i + 1} ---")
IO.puts(md)
end
end
Strukturierte Tabellenextraktion (v0.3.34)
Für typisierten Zugriff auf Zeilen und Bounding Boxes ohne Markdown-Parsing rufen Sie ExtractTables(pageIndex) (Go, C#) / extract_tables(page) (Python, Rust) auf. Jede Tabelle stellt strukturierte Zellen bereit, sodass Sie Ergebnisse direkt ohne Regex in eine Datenbank oder ein DataFrame leiten können.
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("invoice.pdf")
for table in doc.extract_tables(0):
for row in table.rows:
print(row)
Rust
let mut doc = PdfDocument::open("invoice.pdf")?;
for table in doc.extract_tables(0)? {
for row in &table.rows {
println!("{:?}", row);
}
}
Go
doc, _ := pdfoxide.Open("invoice.pdf")
defer doc.Close()
tables, _ := doc.ExtractTables(0)
for _, t := range tables {
for _, row := range t.Rows {
fmt.Println(row)
}
}
C#
using var doc = PdfDocument.Open("invoice.pdf");
foreach (var table in doc.ExtractTables(0))
foreach (var row in table.Rows)
Console.WriteLine(string.Join(" | ", row));
Java
import fyi.oxide.pdf.PdfDocument;
import fyi.oxide.pdf.table.Table;
import fyi.oxide.pdf.table.TableCell;
try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("invoice.pdf"))) {
for (Table table : doc.page(0).tables()) {
String[][] grid = new String[table.rows()][table.cols()];
for (TableCell c : table.cells()) grid[c.row()][c.col()] = c.text();
for (String[] row : grid) System.out.println(String.join(" | ", row));
}
}
C++
auto doc = pdf_oxide::Document::open("invoice.pdf");
for (const auto& table : doc.extract_tables(0)) {
for (int r = 0; r < table.row_count; r++) {
for (int c = 0; c < table.col_count; c++) {
std::cout << table.cell(r, c);
if (c + 1 < table.col_count) std::cout << " | ";
}
std::cout << '\n';
}
}
Swift
let doc = try Document.open("invoice.pdf")
for table in try doc.extractTables(0) {
for r in 0..<table.rowCount {
let row = (0..<table.colCount).map { table.cell(r, $0) }
print(row.joined(separator: " | "))
}
}
Kotlin
import fyi.oxide.pdf.PdfDocument
PdfDocument.open(java.nio.file.Path.of("invoice.pdf")).use { doc ->
for (table in doc.page(0).tables()) {
val grid = Array(table.rows()) { arrayOfNulls<String>(table.cols()) }
table.cells().forEach { grid[it.row()][it.col()] = it.text() }
grid.forEach { println(it.joinToString(" | ")) }
}
}
Dart
final doc = PdfDocument.open('invoice.pdf');
for (final table in doc.extractTables(0)) {
for (var r = 0; r < table.rowCount; r++) {
final row = [for (var c = 0; c < table.colCount; c++) table.cell(r, c)];
print(row.join(' | '));
}
}
doc.close();
R
doc <- pdf_open("invoice.pdf")
for (table in pdf_extract_tables(doc, 0)) {
for (r in seq_len(table$row_count)) {
cat(paste(table$cells[r, ], collapse = " | "), "\n")
}
}
Julia
doc = open_document("invoice.pdf")
for table in extract_tables(doc, 0)
for r in 1:table.row_count
println(join(table.cells[r, :], " | "))
end
end
Zig
var doc = try pdf_oxide.Document.open("invoice.pdf");
const tables = try doc.extractTables(a, 0);
defer pdf_oxide.Document.freeTables(a, tables);
for (tables) |table| {
var r: i32 = 0;
while (r < table.rowCount) : (r += 1) {
var c: i32 = 0;
while (c < table.colCount) : (c += 1) {
std.debug.print("{s}", .{table.cell(r, c)});
if (c + 1 < table.colCount) std.debug.print(" | ", .{});
}
std.debug.print("\n", .{});
}
}
Scala
import fyi.oxide.pdf.PdfDocument
import scala.jdk.CollectionConverters._
import scala.util.Using
Using.resource(PdfDocument.open("invoice.pdf")) { doc =>
for (table <- doc.page(0).tables().asScala) {
val grid = Array.ofDim[String](table.rows(), table.cols())
table.cells().asScala.foreach(c => grid(c.row())(c.col()) = c.text())
grid.foreach(row => println(row.mkString(" | ")))
}
}
Clojure
(with-open [d (pdf/open "invoice.pdf")]
(doseq [table (pdf/tables (pdf/page d 0))]
(let [grid (make-array String (.rows table) (.cols table))]
(doseq [c (.cells table)]
(aset grid (.row c) (.col c) (.text c)))
(doseq [row grid]
(println (clojure.string/join " | " row))))))
Objective-C
NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"invoice.pdf" error:&err];
for (POXTable *table in [doc extractTables:0 error:&err]) {
for (NSInteger r = 0; r < table.rowCount; r++) {
NSMutableArray<NSString *> *row = [NSMutableArray array];
for (NSInteger c = 0; c < table.colCount; c++)
[row addObject:([table cellTextAtRow:r col:c] ?: @"")];
NSLog(@"%@", [row componentsJoinedByString:@" | "]);
}
}
Elixir
{:ok, doc} = PdfOxide.open("invoice.pdf")
{:ok, tables} = PdfOxide.extract_tables(doc, 0)
for table <- tables do
for r <- 0..(table.row_count - 1) do
row = for c <- 0..(table.col_count - 1), do: PdfOxide.cell(table, r, c)
IO.puts(Enum.join(row, " | "))
end
end
Markdown-Tabellen in Zeilen einlesen
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("invoice.pdf")
md = doc.to_markdown(0)
# Extract table rows from Markdown
rows = []
for line in md.split("\n"):
line = line.strip()
if line.startswith("|") and not line.startswith("|--"):
cells = [cell.strip() for cell in line.split("|")[1:-1]]
rows.append(cells)
header = rows[0] if rows else []
data = rows[1:] if len(rows) > 1 else []
print(f"Columns: {header}")
for row in data:
print(row)
WASM
const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdown(0);
const rows = [];
for (const line of md.split("\n")) {
const trimmed = line.trim();
if (trimmed.startsWith("|") && !trimmed.startsWith("|--")) {
const cells = trimmed.split("|").slice(1, -1).map(c => c.trim());
rows.push(cells);
}
}
const header = rows[0] || [];
const data = rows.slice(1);
console.log("Columns:", header);
data.forEach(row => console.log(row));
doc.free();
Rust
let mut doc = PdfDocument::open("invoice.pdf")?;
let md = doc.to_markdown(0, false)?;
let rows: Vec<Vec<String>> = md.lines()
.map(|l| l.trim())
.filter(|l| l.starts_with('|') && !l.starts_with("|--"))
.map(|l| l.split('|').skip(1).map(|c| c.trim().to_string())
.take_while(|c| !c.is_empty()).collect())
.collect();
if let Some(header) = rows.first() {
println!("Columns: {:?}", header);
for row in &rows[1..] {
println!("{:?}", row);
}
}
Java
import fyi.oxide.pdf.PdfDocument;
import java.util.*;
try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("invoice.pdf"))) {
String md = doc.toMarkdown(0);
List<List<String>> rows = new ArrayList<>();
for (String line : md.split("\n")) {
line = line.strip();
if (line.startsWith("|") && !line.startsWith("|--")) {
String[] parts = line.substring(1, line.length() - 1).split("\\|");
List<String> cells = new ArrayList<>();
for (String p : parts) cells.add(p.strip());
rows.add(cells);
}
}
System.out.println("Columns: " + (rows.isEmpty() ? List.of() : rows.get(0)));
for (int i = 1; i < rows.size(); i++) System.out.println(rows.get(i));
}
PHP
$doc = PdfDocument::open('invoice.pdf');
$md = $doc->toMarkdown(0);
$rows = [];
foreach (explode("\n", $md) as $line) {
$line = trim($line);
if (str_starts_with($line, '|') && !str_starts_with($line, '|--')) {
$cells = array_map('trim', array_slice(explode('|', $line), 1, -1));
$rows[] = $cells;
}
}
$header = $rows[0] ?? [];
echo "Columns: " . implode(', ', $header) . "\n";
foreach (array_slice($rows, 1) as $row) {
echo implode(' | ', $row) . "\n";
}
$doc->close();
Ruby
PdfOxide::PdfDocument.open('invoice.pdf') do |doc|
md = doc.to_markdown(0)
rows = md.lines.map(&:strip)
.select { |l| l.start_with?('|') && !l.start_with?('|--') }
.map { |l| l.split('|')[1..-2].map(&:strip) }
header = rows.first || []
puts "Columns: #{header.inspect}"
rows.drop(1).each { |row| puts row.inspect }
end
C++
#include <pdf_oxide/pdf_oxide.hpp>
#include <sstream>
#include <vector>
auto doc = pdf_oxide::Document::open("invoice.pdf");
auto md = doc.to_markdown(0);
std::vector<std::vector<std::string>> rows;
std::istringstream stream(md);
for (std::string line; std::getline(stream, line);) {
auto s = line.find_first_not_of(" \t");
if (s == std::string::npos) continue;
line = line.substr(s);
if (line.rfind("|", 0) != 0 || line.rfind("|--", 0) == 0) continue;
std::vector<std::string> cells;
std::istringstream cs(line.substr(1, line.size() - 2));
for (std::string cell; std::getline(cs, cell, '|');) cells.push_back(cell);
rows.push_back(cells);
}
Swift
let doc = try Document.open("invoice.pdf")
let md = try doc.toMarkdown(0)
let rows = md.split(separator: "\n").map { $0.trimmingCharacters(in: .whitespaces) }
.filter { $0.hasPrefix("|") && !$0.hasPrefix("|--") }
.map { line -> [String] in
line.dropFirst().dropLast().split(separator: "|", omittingEmptySubsequences: false)
.map { $0.trimmingCharacters(in: .whitespaces) }
}
if let header = rows.first {
print("Columns:", header)
for row in rows.dropFirst() { print(row) }
}
Kotlin
PdfDocument.open(java.nio.file.Path.of("invoice.pdf")).use { doc ->
val md = doc.toMarkdown(0)
val rows = md.split("\n").map { it.trim() }
.filter { it.startsWith("|") && !it.startsWith("|--") }
.map { it.removeSurrounding("|").split("|").map(String::trim) }
rows.firstOrNull()?.let { println("Columns: $it") }
rows.drop(1).forEach { println(it) }
}
Dart
final doc = PdfDocument.open('invoice.pdf');
final md = doc.toMarkdown(0);
final rows = md.split('\n').map((l) => l.trim())
.where((l) => l.startsWith('|') && !l.startsWith('|--'))
.map((l) => l.substring(1, l.length - 1).split('|').map((c) => c.trim()).toList())
.toList();
if (rows.isNotEmpty) {
print('Columns: ${rows.first}');
for (final row in rows.skip(1)) print(row);
}
doc.close();
R
doc <- pdf_open("invoice.pdf")
md <- pdf_to_markdown(doc, 0)
lines <- trimws(strsplit(md, "\n")[[1]])
lines <- lines[startsWith(lines, "|") & !startsWith(lines, "|--")]
rows <- lapply(lines, function(l) {
cells <- strsplit(l, "\\|")[[1]]
trimws(cells[2:(length(cells) - 1)])
})
if (length(rows) > 0) {
cat("Columns:", rows[[1]], "\n")
for (row in rows[-1]) cat(row, "\n")
}
Julia
doc = open_document("invoice.pdf")
md = to_markdown(doc, 0)
rows = [strip.(split(l, "|")[2:end-1])
for l in strip.(split(md, "\n"))
if startswith(l, "|") && !startswith(l, "|--")]
if !isempty(rows)
println("Columns: ", rows[1])
for row in rows[2:end]
println(row)
end
end
Zig
var doc = try pdf_oxide.Document.open("invoice.pdf");
const md = try doc.toMarkdown(a, 0);
defer a.free(md);
var lines = std.mem.splitScalar(u8, md, '\n');
while (lines.next()) |raw| {
const line = std.mem.trim(u8, raw, " \t\r");
if (!std.mem.startsWith(u8, line, "|") or std.mem.startsWith(u8, line, "|--")) continue;
const inner = line[1 .. line.len - 1];
var cells = std.mem.splitScalar(u8, inner, '|');
while (cells.next()) |cell| {
std.debug.print("{s}\t", .{std.mem.trim(u8, cell, " \t")});
}
std.debug.print("\n", .{});
}
Scala
Using.resource(PdfDocument.open("invoice.pdf")) { doc =>
val md = doc.toMarkdown(0)
val rows = md.split("\n").map(_.trim)
.filter(l => l.startsWith("|") && !l.startsWith("|--"))
.map(_.stripPrefix("|").stripSuffix("|").split("\\|").map(_.trim).toList)
.toList
rows.headOption.foreach(h => println(s"Columns: $h"))
rows.drop(1).foreach(println)
}
Clojure
(with-open [d (pdf/open "invoice.pdf")]
(let [md (pdf/to-markdown d 0)
rows (->> (clojure.string/split-lines md)
(map clojure.string/trim)
(filter #(and (.startsWith % "|") (not (.startsWith % "|--"))))
(map #(->> (clojure.string/split % #"\|")
(drop 1) (butlast) (map clojure.string/trim) vec)))]
(when-let [header (first rows)]
(println "Columns:" header)
(doseq [row (rest rows)] (println row)))))
Objective-C
NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"invoice.pdf" error:&err];
NSString *md = [doc toMarkdown:0 error:&err];
NSMutableArray<NSArray<NSString *> *> *rows = [NSMutableArray array];
for (NSString *raw in [md componentsSeparatedByString:@"\n"]) {
NSString *line = [raw stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceCharacterSet]];
if (![line hasPrefix:@"|"] || [line hasPrefix:@"|--"]) continue;
NSArray<NSString *> *parts = [line componentsSeparatedByString:@"|"];
NSMutableArray<NSString *> *cells = [NSMutableArray array];
for (NSUInteger i = 1; i + 1 < parts.count; i++)
[cells addObject:[parts[i] stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceCharacterSet]]];
[rows addObject:cells];
}
if (rows.count > 0) NSLog(@"Columns: %@", rows[0]);
Elixir
{:ok, doc} = PdfOxide.open("invoice.pdf")
{:ok, md} = PdfOxide.to_markdown(doc, 0)
rows =
md
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.filter(&(String.starts_with?(&1, "|") and not String.starts_with?(&1, "|--")))
|> Enum.map(fn line ->
line |> String.split("|") |> Enum.slice(1..-2//1) |> Enum.map(&String.trim/1)
end)
case rows do
[header | data] ->
IO.puts("Columns: #{inspect(header)}")
Enum.each(data, &IO.inspect/1)
[] ->
:ok
end
Als CSV exportieren
import csv
from pdf_oxide import PdfDocument
doc = PdfDocument("invoice.pdf")
md = doc.to_markdown(0)
rows = []
for line in md.split("\n"):
line = line.strip()
if line.startswith("|") and not line.startswith("|--"):
cells = [cell.strip() for cell in line.split("|")[1:-1]]
rows.append(cells)
with open("table.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerows(rows)
Als Pandas DataFrame exportieren
import pandas as pd
from pdf_oxide import PdfDocument
doc = PdfDocument("report.pdf")
md = doc.to_markdown(0)
rows = []
for line in md.split("\n"):
line = line.strip()
if line.startswith("|") and not line.startswith("|--"):
cells = [cell.strip() for cell in line.split("|")[1:-1]]
rows.append(cells)
if rows:
df = pd.DataFrame(rows[1:], columns=rows[0])
print(df)
Zeichenpositionen für benutzerdefiniertes Tabellen-Parsing nutzen
Für detaillierte Kontrolle können Sie Extraktion auf Zeichenebene und räumliche Analyse verwenden:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("financial.pdf")
chars = doc.extract_chars(0)
# Group characters by Y position (rows)
rows = {}
for ch in chars:
row_key = round(ch.y / 2) * 2 # Snap to 2pt grid
rows.setdefault(row_key, []).append(ch)
# Sort rows top-to-bottom, characters left-to-right
for y in sorted(rows.keys(), reverse=True):
line_chars = sorted(rows[y], key=lambda c: c.x)
text = "".join(c.char for c in line_chars)
print(text)
WASM
const doc = new WasmPdfDocument(bytes);
const chars = doc.extractChars(0);
// Group characters by Y position (rows)
const rows = new Map();
for (const ch of chars) {
const rowKey = Math.round(ch.y / 2) * 2; // Snap to 2pt grid
if (!rows.has(rowKey)) rows.set(rowKey, []);
rows.get(rowKey).push(ch);
}
// Sort rows top-to-bottom, characters left-to-right
const sortedKeys = [...rows.keys()].sort((a, b) => b - a);
for (const y of sortedKeys) {
const lineChars = rows.get(y).sort((a, b) => a.x - b.x);
const text = lineChars.map(c => c.char).join("");
console.log(text);
}
doc.free();
Rust
use std::collections::BTreeMap;
let mut doc = PdfDocument::open("financial.pdf")?;
let chars = doc.extract_chars(0)?;
let mut rows: BTreeMap<i32, Vec<_>> = BTreeMap::new();
for ch in &chars {
let row_key = ((ch.y / 2.0).round() * 2.0) as i32;
rows.entry(row_key).or_default().push(ch);
}
for (_, line_chars) in rows.iter().rev() {
let mut sorted = line_chars.clone();
sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
let text: String = sorted.iter().map(|c| c.char).collect();
println!("{}", text);
}
Go
doc, _ := pdfoxide.Open("financial.pdf")
defer doc.Close()
chars, _ := doc.ExtractChars(0)
rows := map[int][]pdfoxide.Char{}
for _, ch := range chars {
key := int(math.Round(float64(ch.Y)/2) * 2)
rows[key] = append(rows[key], ch)
}
keys := make([]int, 0, len(rows))
for k := range rows { keys = append(keys, k) }
sort.Sort(sort.Reverse(sort.IntSlice(keys)))
for _, y := range keys {
line := rows[y]
sort.Slice(line, func(i, j int) bool { return line[i].X < line[j].X })
var b strings.Builder
for _, c := range line { b.WriteString(c.Char) }
fmt.Println(b.String())
}
C#
using var doc = PdfDocument.Open("financial.pdf");
var chars = doc.ExtractChars(0);
var rows = chars
.GroupBy(c => (int)(Math.Round(c.Y / 2) * 2))
.OrderByDescending(g => g.Key);
foreach (var row in rows)
{
var line = string.Concat(row.OrderBy(c => c.X).Select(c => c.Char));
Console.WriteLine(line);
}
C++
#include <pdf_oxide/pdf_oxide.hpp>
#include <map>
#include <vector>
#include <algorithm>
auto doc = pdf_oxide::Document::open("financial.pdf");
auto chars = doc.extract_chars(0);
// Group characters by Y position (rows)
std::map<int, std::vector<pdf_oxide::Char>> rows;
for (const auto& ch : chars) {
int key = static_cast<int>(std::lround(ch.bbox.y / 2.0) * 2);
rows[key].push_back(ch);
}
// Sort rows top-to-bottom, characters left-to-right
for (auto it = rows.rbegin(); it != rows.rend(); ++it) {
auto& line = it->second;
std::sort(line.begin(), line.end(),
[](const auto& a, const auto& b) { return a.bbox.x < b.bbox.x; });
std::string text;
for (const auto& c : line) text += static_cast<char>(c.character);
std::cout << text << '\n';
}
Swift
let doc = try Document.open("financial.pdf")
let chars = try doc.extractChars(0)
// Group characters by Y position (rows)
var rows: [Int: [Char]] = [:]
for ch in chars {
let key = Int((ch.bbox.y / 2).rounded()) * 2 // Snap to 2pt grid
rows[key, default: []].append(ch)
}
// Sort rows top-to-bottom, characters left-to-right
for y in rows.keys.sorted(by: >) {
let line = rows[y]!.sorted { $0.bbox.x < $1.bbox.x }
let text = String(line.compactMap { Unicode.Scalar($0.character).map(Character.init) })
print(text)
}
Dart
final doc = PdfDocument.open('financial.pdf');
final chars = doc.extractChars(0);
// Group characters by Y position (rows)
final rows = <int, List<Char>>{};
for (final ch in chars) {
final key = (ch.bbox.y / 2).round() * 2; // Snap to 2pt grid
rows.putIfAbsent(key, () => []).add(ch);
}
// Sort rows top-to-bottom, characters left-to-right
final keys = rows.keys.toList()..sort((a, b) => b - a);
for (final y in keys) {
final line = rows[y]!..sort((a, b) => a.bbox.x.compareTo(b.bbox.x));
final text = String.fromCharCodes(line.map((c) => c.character));
print(text);
}
doc.close();
R
doc <- pdf_open("financial.pdf")
chars <- pdf_extract_chars(doc, 0)
# Group characters by Y position (rows), snapped to a 2pt grid
keys <- sapply(chars, function(ch) round(ch$bbox$y / 2) * 2)
for (y in sort(unique(keys), decreasing = TRUE)) {
line <- chars[keys == y]
line <- line[order(sapply(line, function(c) c$bbox$x))]
text <- paste(intToUtf8(sapply(line, function(c) c$character), multiple = TRUE),
collapse = "")
cat(text, "\n")
}
Julia
doc = open_document("financial.pdf")
chars = extract_chars(doc, 0)
# Group characters by Y position (rows), snapped to a 2pt grid
rows = Dict{Int,Vector}()
for ch in chars
key = round(Int, ch.bbox.y / 2) * 2
push!(get!(rows, key, []), ch)
end
for y in sort(collect(keys(rows)), rev = true)
line = sort(rows[y], by = c -> c.bbox.x)
text = join(Char.(getfield.(line, :character)))
println(text)
end
Zig
var doc = try pdf_oxide.Document.open("financial.pdf");
const chars = try doc.extractChars(a, 0);
defer pdf_oxide.Document.freeChars(a, chars);
// Group characters by Y position (rows)
var rows = std.AutoArrayHashMap(i32, std.ArrayList(pdf_oxide.Char)).init(a);
for (chars) |ch| {
const key: i32 = @intFromFloat(@round(ch.bbox.y / 2.0) * 2.0);
const gop = try rows.getOrPut(key);
if (!gop.found_existing) gop.value_ptr.* = std.ArrayList(pdf_oxide.Char).init(a);
try gop.value_ptr.append(ch);
}
// Sort keys descending (top-to-bottom), characters left-to-right
const keys = rows.keys();
std.mem.sort(i32, keys, {}, comptime std.sort.desc(i32));
for (keys) |y| {
var line = rows.get(y).?;
std.mem.sort(pdf_oxide.Char, line.items, {}, struct {
fn lt(_: void, x: pdf_oxide.Char, z: pdf_oxide.Char) bool { return x.bbox.x < z.bbox.x; }
}.lt);
for (line.items) |c| {
var buf: [4]u8 = undefined;
const len = std.unicode.utf8Encode(@intCast(c.character), &buf) catch 0;
std.debug.print("{s}", .{buf[0..len]});
}
std.debug.print("\n", .{});
}
Objective-C
NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"financial.pdf" error:&err];
NSArray<POXChar *> *chars = [doc extractChars:0 error:&err];
// Group characters by Y position (rows)
NSMutableDictionary<NSNumber *, NSMutableArray<POXChar *> *> *rows =
[NSMutableDictionary dictionary];
for (POXChar *ch in chars) {
NSNumber *key = @((NSInteger)(round(ch.bbox.y / 2.0) * 2));
if (!rows[key]) rows[key] = [NSMutableArray array];
[rows[key] addObject:ch];
}
// Sort rows top-to-bottom, characters left-to-right
NSArray<NSNumber *> *keys = [[rows allKeys]
sortedArrayUsingSelector:@selector(compare:)];
for (NSNumber *y in [keys reverseObjectEnumerator]) {
NSArray<POXChar *> *line = [rows[y] sortedArrayUsingComparator:
^(POXChar *x, POXChar *z) { return [@(x.bbox.x) compare:@(z.bbox.x)]; }];
NSMutableString *text = [NSMutableString string];
for (POXChar *c in line)
[text appendString:[[NSString alloc] initWithBytes:&(uint32_t){c.character}
length:4 encoding:NSUTF32LittleEndianStringEncoding]];
NSLog(@"%@", text);
}
Elixir
{:ok, doc} = PdfOxide.open("financial.pdf")
{:ok, chars} = PdfOxide.extract_chars(doc, 0)
# Group characters by Y position (rows), snapped to a 2pt grid
chars
|> Enum.group_by(fn ch -> round(ch.bbox.y / 2) * 2 end)
|> Enum.sort_by(fn {y, _} -> -y end)
|> Enum.each(fn {_y, line} ->
text =
line
|> Enum.sort_by(fn c -> c.bbox.x end)
|> Enum.map(fn c -> <<c.character::utf8>> end)
|> Enum.join()
IO.puts(text)
end)
Tabellen als Markdown exportieren
Markdown ist das ideale Ausgabeformat, wenn Sie PDF-Inhalt an ein großes Sprachmodell weitergeben, eine RAG-Pipeline aufbauen oder extrahierte Daten in einem Format speichern möchten, das sowohl menschenlesbar als auch maschinenverarbeitbar ist. PDF Oxide gibt Tabellen nativ im GitHub Flavored Markdown (GFM)-Format aus, sodass kein zusätzlicher Konvertierungsschritt erforderlich ist.
from pdf_oxide import PdfDocument
doc = PdfDocument("quarterly-report.pdf")
# Extract all tables across all pages as Markdown
all_tables = []
for i in range(doc.page_count()):
md = doc.to_markdown(i, detect_headings=True)
# Split the markdown into sections and find table blocks
in_table = False
current_table = []
for line in md.split("\n"):
if line.strip().startswith("|"):
in_table = True
current_table.append(line)
else:
if in_table and current_table:
all_tables.append("\n".join(current_table))
current_table = []
in_table = False
if current_table:
all_tables.append("\n".join(current_table))
print(f"Found {len(all_tables)} tables")
for idx, table in enumerate(all_tables):
print(f"\n--- Table {idx + 1} ---")
print(table)
Die GFM-Tabellenausgabe ist direkt mit LLM-Prompts kompatibel. Sie können sie direkt in einen OpenAI- oder Anthropic-API-Aufruf übergeben, und das Modell versteht die tabellarische Struktur ohne zusätzliche Formatierung:
# Feed extracted table to an LLM for analysis
prompt = f"""Analyze the following financial table and summarize the key trends:
{all_tables[0]}
"""
Dieser Ansatz ist erheblich schneller, als Tabellen mit pdfplumber zu extrahieren und sie anschließend selbst in Markdown umzuwandeln.
Umgang mit mehrseitigen Tabellen
Tabellen, die sich über mehrere Seiten erstrecken, sind eine häufige Herausforderung bei der PDF-Extraktion. Jahresabschlüsse, Bestandslisten und behördliche Dokumente enthalten oft Tabellen, die sich über zwei, fünf oder sogar Dutzende von Seiten erstrecken. Der Schlüssel liegt darin, die Tabelle seitenweise zu extrahieren und die Zeilen dann zusammenzufügen, wobei wiederkehrende Kopfzeilen und Seitenartefakte sorgfältig behandelt werden müssen.
from pdf_oxide import PdfDocument
doc = PdfDocument("long-report.pdf")
def extract_table_rows(md_text):
"""Extract table rows from markdown text, returning header and data separately."""
header = None
data_rows = []
for line in md_text.split("\n"):
line = line.strip()
if not line.startswith("|") or line.startswith("|--"):
continue
cells = [cell.strip() for cell in line.split("|")[1:-1]]
if header is None:
header = cells
else:
data_rows.append(cells)
return header, data_rows
# Collect rows across all pages
combined_header = None
combined_rows = []
for i in range(doc.page_count()):
md = doc.to_markdown(i)
header, rows = extract_table_rows(md)
if header is None:
continue # No table on this page
if combined_header is None:
combined_header = header
elif header == combined_header:
pass # Skip repeated header on subsequent pages
else:
# Different table — save current and start new
print(f"Table with {len(combined_rows)} rows found")
combined_header = header
combined_rows = []
combined_rows.extend(rows)
if combined_header and combined_rows:
print(f"Columns: {combined_header}")
print(f"Total rows: {len(combined_rows)}")
for row in combined_rows[:5]:
print(row)
if len(combined_rows) > 5:
print(f"... and {len(combined_rows) - 5} more rows")
Dieses Muster funktioniert zuverlässig für Tabellen, bei denen die Kopfzeile auf jeder Seite wiederholt wird (der häufigste Fall). Für Tabellen, bei denen die Kopfzeile nur auf der ersten Seite erscheint, können Sie die Logik vereinfachen, indem Sie den Header nur von der ersten Seite mit Tabelle erfassen und alle nachfolgenden Zeilen als Daten behandeln.
Tabellen als CSV oder DataFrame exportieren
Nach dem Extrahieren der Tabellendaten benötigen Sie diese oft in einem strukturierten Format zur weiteren Analyse. Die folgenden Beispiele zeigen, wie Sie in wenigen Zeilen von einem PDF zu einem pandas DataFrame oder einer CSV-Datei gelangen.
Batch-Export: alle Tabellen in separate CSV-Dateien
import csv
from pdf_oxide import PdfDocument
doc = PdfDocument("catalog.pdf")
table_count = 0
for i in range(doc.page_count()):
md = doc.to_markdown(i)
rows = []
for line in md.split("\n"):
line = line.strip()
if line.startswith("|") and not line.startswith("|--"):
cells = [cell.strip() for cell in line.split("|")[1:-1]]
rows.append(cells)
if len(rows) > 1: # At least header + one data row
table_count += 1
filename = f"table_page{i + 1}_{table_count}.csv"
with open(filename, "w", newline="") as f:
writer = csv.writer(f)
writer.writerows(rows)
print(f"Saved {filename} ({len(rows) - 1} data rows)")
print(f"Exported {table_count} tables total")
Mehrseitige Tabelle als DataFrame
Für Tabellen über mehrere Seiten kombinieren Sie das Seiten-Zusammenfügungs-Muster mit pandas:
import pandas as pd
from pdf_oxide import PdfDocument
doc = PdfDocument("financial-statement.pdf")
header = None
all_rows = []
for i in range(doc.page_count()):
md = doc.to_markdown(i)
for line in md.split("\n"):
line = line.strip()
if not line.startswith("|") or line.startswith("|--"):
continue
cells = [cell.strip() for cell in line.split("|")[1:-1]]
if header is None:
header = cells
elif cells == header:
continue # Skip repeated header
else:
all_rows.append(cells)
if header and all_rows:
df = pd.DataFrame(all_rows, columns=header)
# Clean up numeric columns
for col in df.columns:
# Try to convert columns that look numeric
cleaned = df[col].str.replace(r"[$,%]", "", regex=True).str.strip()
try:
df[col] = pd.to_numeric(cleaned)
except (ValueError, TypeError):
pass # Keep as string
print(df.dtypes)
print(df.head(10))
df.to_csv("financial_data.csv", index=False)
Dieser Workflow liefert ein sauberes DataFrame mit richtigen numerischen Typen, bereit für die Analyse mit pandas, die Visualisierung mit matplotlib oder das Laden in eine Datenbank.
Komplexe Tabellen: Wann pdfplumber sinnvoll ist
Die Tabellenerkennung von PDF Oxide verarbeitet standardmäßige ausgerichtete Tabellen gut. Für komplexe Fälle — verbundene Zellen, überspannende Überschriften, rahmenlose Tabellen oder mehrzeiligen Zelleninhalt — sind die dedizierten Tabellenextraktionsalgorithmen von pdfplumber robuster:
import pdfplumber
with pdfplumber.open("complex-report.pdf") as pdf:
page = pdf.pages[0]
tables = page.extract_tables()
for table in tables:
for row in table:
print(row)
Wann welches Tool verwenden
| Szenario | Empfehlung |
|---|---|
| Einfache ausgerichtete Tabellen | PDF Oxide (29× schneller) |
| Tabellen als Teil von seitenweitem Markdown | PDF Oxide |
| Komplexe verbundene Zellen / überspannende Überschriften | pdfplumber |
| Rahmenlose Tabellen | pdfplumber |
| Geschwindigkeitskritische Batch-Verarbeitung | PDF Oxide |
Beide zusammen verwenden
Schnelle Textextraktion mit PDF Oxide, komplexe Tabellenextraktion mit pdfplumber:
from pdf_oxide import PdfDocument
import pdfplumber
# Fast full-text extraction
doc = PdfDocument("report.pdf")
text = doc.extract_text(0)
# Targeted table extraction for complex pages
with pdfplumber.open("report.pdf") as pdf:
tables = pdf.pages[0].extract_tables()
Verwandte Seiten
- Markdown-Konvertierung — vollständige Markdown-API-Referenz
- Textextraktion — Plaintext- und Zeichenextraktion
- PDF Oxide vs. pdfplumber — detaillierter Vergleich
- PDF zu Markdown — Markdown-Konvertierungsleitfaden