Skip to content

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