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
- PDF zu Markdown — Details zur Markdown-Konvertierung
- Stapelverarbeitung — Muster für parallele Verarbeitung
- Gescannte PDFs per OCR — Einrichtung und Einsatz von OCR
- Text aus PDF extrahieren — reine Textextraktion
- Performance-Benchmarks — vollständige Benchmark-Ergebnisse