Skip to content

PDF-Extraktion für RAG-Pipelines in Python

PDFs in strukturiertes Markdown für Ihre RAG-Pipeline umwandeln:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("paper.pdf")
md = doc.to_markdown_all(detect_headings=True, include_images=False)
# In Chunks zerlegen, einbetten und in der Vektordatenbank ablegen

WASM

import { WasmPdfDocument } from "pdf-oxide-wasm";

const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdownAll();
// In Chunks zerlegen, einbetten und in der Vektordatenbank ablegen
doc.free();

Rust

use pdf_oxide::PdfDocument;

let mut doc = PdfDocument::open("paper.pdf")?;
let md = doc.to_markdown_all(true)?;
// In Chunks zerlegen, einbetten und in der Vektordatenbank ablegen

Go

package main

import (
    "log"
    pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

func main() {
    doc, err := pdfoxide.Open("paper.pdf")
    if err != nil { log.Fatal(err) }
    defer doc.Close()

    md, _ := doc.ToMarkdownAll()
    _ = md // In Chunks zerlegen, einbetten und in der Vektordatenbank ablegen
}

C#

using PdfOxide;

using var doc = PdfDocument.Open("paper.pdf");
var md = doc.ToMarkdownAll();
// In Chunks zerlegen, einbetten und in der Vektordatenbank ablegen

PDF Oxide verarbeitet 3.830 PDFs in 3,1 Sekunden — 0,8 ms pro Seite bei 100 % Trefferquote. Kein einziges Dokument fehlt in Ihrem Index.

Warum die Qualität der Extraktion für RAG entscheidend ist

Ihre Retrieval-Qualität ist immer nur so gut wie die Extraktion davor:

  • Fehlender Text = fehlende Antworten. Eine Bibliothek mit 98,4 % Trefferquote (pypdf) verliert aus einem Korpus mit 3.823 Dateien stillschweigend 61 Dokumente. PDF Oxide schafft 100 %.
  • Verlorene Struktur = schlechtes Chunking. Reiner Text wirft Überschriften, Tabellen und Formatierung weg, die semantisches Chunking erst ermöglichen. Markdown erhält sie.
  • Langsame Extraktion = Engpass in der Pipeline. Bei 12,1 ms pro Seite (pypdf) oder 23,2 ms (pdfplumber) dauert die Verarbeitung von 100.000 Seiten Minuten. Bei 0,8 ms sind es 80 Sekunden.

Installation

pip install pdf_oxide

Schnellstart: vom PDF in die Vektordatenbank

from pdf_oxide import PdfDocument, PdfError
from pathlib import Path

def extract_documents(pdf_dir: str) -> list[dict]:
    """Alle PDFs in einem Verzeichnis zu strukturierten Chunks extrahieren."""
    documents = []
    for pdf_path in Path(pdf_dir).glob("*.pdf"):
        try:
            doc = PdfDocument(str(pdf_path))
            for i in range(doc.page_count()):
                md = doc.to_markdown(i,
                    detect_headings=True,
                    include_images=False
                )
                if md.strip():
                    documents.append({
                        "content": md,
                        "source": pdf_path.name,
                        "page": i,
                    })
        except PdfError as e:
            print(f"{pdf_path.name} übersprungen: {e}")
    return documents

docs = extract_documents("research-papers/")
print(f"{len(docs)} Chunks aus den PDFs extrahiert")
# Chunks an das Embedding-Modell und den Vektorspeicher übergeben

Chunking-Strategien

Nach Überschrift (semantisches Chunking)

Teilen Sie die Markdown-Ausgabe an Überschriften, um semantisch sinnvolle Chunks zu erhalten:

Python

import re
from pdf_oxide import PdfDocument

doc = PdfDocument("paper.pdf")
md = doc.to_markdown_all(detect_headings=True, include_images=False)

# An ##-Überschriften teilen
chunks = re.split(r'\n(?=## )', md)
chunks = [c.strip() for c in chunks if c.strip()]

WASM

const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdownAll();

// An ##-Überschriften teilen
const chunks = md.split(/\n(?=## )/).filter(c => c.trim());
doc.free();

Rust

let mut doc = PdfDocument::open("paper.pdf")?;
let md = doc.to_markdown_all(true)?;

let chunks: Vec<&str> = md.split("\n## ")
    .map(|c| c.trim())
    .filter(|c| !c.is_empty())
    .collect();

Go

doc, _ := pdfoxide.Open("paper.pdf")
defer doc.Close()

md, _ := doc.ToMarkdownAll()

var chunks []string
for _, c := range strings.Split(md, "\n## ") {
    c = strings.TrimSpace(c)
    if c != "" { chunks = append(chunks, c) }
}

C#

using var doc = PdfDocument.Open("paper.pdf");
var md = doc.ToMarkdownAll();

var chunks = md.Split("\n## ")
    .Select(c => c.Trim())
    .Where(c => c.Length > 0)
    .ToList();

Nach Seite

Ein Chunk pro Seite — simpel und seitenbezogener Kontext bleibt erhalten:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("manual.pdf")
chunks = []
for i in range(doc.page_count()):
    md = doc.to_markdown(i, detect_headings=True, include_images=False)
    if md.strip():
        chunks.append({"content": md, "page": i})

WASM

const doc = new WasmPdfDocument(bytes);
const chunks = [];
for (let i = 0; i < doc.pageCount(); i++) {
    const md = doc.toMarkdown(i);
    if (md.trim()) {
        chunks.push({ content: md, page: i });
    }
}
doc.free();

Rust

let mut doc = PdfDocument::open("manual.pdf")?;
let mut chunks = Vec::new();
for i in 0..doc.page_count()? {
    let md = doc.to_markdown(i, true)?;
    if !md.trim().is_empty() {
        chunks.push((i, md));
    }
}

Go

doc, _ := pdfoxide.Open("manual.pdf")
defer doc.Close()

type Chunk struct{ Page int; Content string }
var chunks []Chunk

n, _ := doc.PageCount()
for i := 0; i < n; i++ {
    md, _ := doc.ToMarkdown(i)
    if strings.TrimSpace(md) != "" {
        chunks = append(chunks, Chunk{Page: i, Content: md})
    }
}

C#

using var doc = PdfDocument.Open("manual.pdf");
var chunks = Enumerable.Range(0, doc.PageCount)
    .Select(i => new { Page = i, Content = doc.ToMarkdown(i) })
    .Where(c => !string.IsNullOrWhiteSpace(c.Content))
    .ToList();

Fester Blockgröße mit Überlappung

Langen Text in Chunks fester Größe mit Überlappung zerlegen:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("book.pdf")
full_text = doc.to_markdown_all(detect_headings=True, include_images=False)

chunk_size = 1000  # Zeichen
overlap = 200
chunks = []

for start in range(0, len(full_text), chunk_size - overlap):
    chunk = full_text[start:start + chunk_size]
    if chunk.strip():
        chunks.append(chunk)

WASM

const doc = new WasmPdfDocument(bytes);
const fullText = doc.toMarkdownAll();

const chunkSize = 1000;
const overlap = 200;
const chunks = [];

for (let start = 0; start < fullText.length; start += chunkSize - overlap) {
    const chunk = fullText.slice(start, start + chunkSize);
    if (chunk.trim()) chunks.push(chunk);
}
doc.free();

Rust

let mut doc = PdfDocument::open("book.pdf")?;
let full_text = doc.to_markdown_all(true)?;

let chunk_size = 1000;
let overlap = 200;
let mut chunks = Vec::new();
let mut start = 0;

while start < full_text.len() {
    let end = (start + chunk_size).min(full_text.len());
    let chunk = &full_text[start..end];
    if !chunk.trim().is_empty() {
        chunks.push(chunk.to_string());
    }
    start += chunk_size - overlap;
}

Go

doc, _ := pdfoxide.Open("book.pdf")
defer doc.Close()

full, _ := doc.ToMarkdownAll()

const chunkSize, overlap = 1000, 200
var chunks []string
for start := 0; start < len(full); start += chunkSize - overlap {
    end := start + chunkSize
    if end > len(full) { end = len(full) }
    chunk := full[start:end]
    if strings.TrimSpace(chunk) != "" {
        chunks = append(chunks, chunk)
    }
}

C#

using var doc = PdfDocument.Open("book.pdf");
var full = doc.ToMarkdownAll();

const int chunkSize = 1000, overlap = 200;
var chunks = new List<string>();
for (int start = 0; start < full.Length; start += chunkSize - overlap)
{
    var end = Math.Min(start + chunkSize, full.Length);
    var chunk = full[start..end];
    if (!string.IsNullOrWhiteSpace(chunk))
        chunks.Add(chunk);
}

Stapelverarbeitung von Tausenden PDFs

Mit 0,8 ms pro Seite bewältigt PDF Oxide große Korpora im Handumdrehen:

from pdf_oxide import PdfDocument, PdfError
from pathlib import Path

pdf_files = list(Path("corpus/").glob("**/*.pdf"))
print(f"{len(pdf_files)} PDFs werden verarbeitet...")

all_chunks = []
errors = 0

for pdf_path in pdf_files:
    try:
        doc = PdfDocument(str(pdf_path))
        md = doc.to_markdown_all(
            detect_headings=True,
            include_images=False
        )
        if md.strip():
            all_chunks.append({
                "content": md,
                "source": str(pdf_path),
                "pages": doc.page_count(),
            })
    except PdfError:
        errors += 1

print(f"{len(all_chunks)} Dokumente extrahiert, {errors} Fehler")

Gescannte PDFs in der Pipeline abfangen

In jedem Korpus gibt es einige eingescannte PDFs. Als Fallback greift OCR:

from pdf_oxide import PdfDocument

doc = PdfDocument("mixed-corpus-file.pdf")
text = doc.extract_text(0)

if len(text.strip()) < 50:
    # Wahrscheinlich eine gescannte Seite — OCR verwenden
    text = doc.extract_text_ocr(0)

Einrichtungsdetails finden Sie im OCR-Leitfaden.

Warum Markdown statt reinem Text

Eigenschaft Reiner Text Markdown
Überschriftenhierarchie Verloren Erhalten (#, ##, ###)
Tabellen Flachgeklopft GFM-Tabellen-Syntax
Fett/Kursiv Verloren **fett**, *kursiv*
Semantisches Chunking Schwierig Teilen an Überschriften
LLM-Verständnis Geringer Höher (strukturierte Eingabe)

Markdown liefert Ihrem LLM mehr Kontext zur Dokumentstruktur — und damit bessere Retrieval- und Generierungsqualität.

Performance im großen Maßstab

Korpusgröße PDF Oxide pypdf pdfplumber
1.000 Seiten 0,8 s 12,1 s 23,2 s
10.000 Seiten 8 s 121 s 232 s
100.000 Seiten 80 s 1.210 s 2.320 s
Trefferquote 100 % 98,4 % 98,8 %

Bei 100 % Trefferquote müssen Sie nie manuell recherchieren, warum Dokumente in Ihrem Index fehlen.

Verwandte Seiten