Dokumenten-Extraktion lokal: Rechnungen und Belege mit Vision-LLM und OCR DSGVO-konform auslesen
Rechnungen und Belege DSGVO-konform lokal verarbeiten: Mit PaddleOCR v3, Docling und Vision-LLMs wie Qwen2.5-VL extrahierst du strukturierte JSON-Daten aus PDFs und Scans – vollständig on-premises, mit Pydantic-Validierung und Paperless-ngx-Anbindung.

Jede Rechnung, jeder Lieferschein, jede Quittung – wer diese Dokumente manuell in die Buchhaltungssoftware tippt, verliert Zeit und riskiert Fehler. Wer dabei auf Cloud-Dienste setzt, riskiert DSGVO-Probleme. Dieser Artikel zeigt dir, wie du einen vollständig lokalen Workflow aufbaust, der aus PDFs und Scan-Bildern strukturierte JSON-Daten extrahiert: mit klassischem OCR (PaddleOCR v3), modernen Vision-LLMs (Qwen2.5-VL, Docling) und zweistufiger Pydantic-Validierung – ohne einen einzigen Byte in die Cloud zu schicken.
Voraussetzungen
- Python 3.10 oder neuer (3.11/3.12 empfohlen)
- Für GPU-Beschleunigung: NVIDIA-GPU mit CUDA 12.x und mindestens 8 GB VRAM (Qwen2.5-VL-7B), 40 GB für das 72B-Modell ohne Quantisierung
- Ollama (ollama.com) für einfaches lokales LLM-Deployment ohne tiefe Python-Abhängigkeiten
- Paperless-ngx ab v2.0 (Docker-Compose, mindestens 4 GB RAM)
- Für Mistral OCR: API-Key + abgeschlossenes Data Processing Addendum (DPA) unter console.mistral.ai
- Tesseract OCR 5.x optional als Fallback (
apt install tesseract-ocr tesseract-ocr-deu) - Python-Pakete:
docling paddleocr paddlepaddle transformers accelerate qwen-vl-utils pydantic mistralai opencv-python pdf2image pdfplumber
Schritt 1: Tool-Auswahl – lokal vs. Cloud
Bevor du loslegst, lege fest, welcher Stack für dein KMU passt. Die folgende Tabelle fasst die wichtigsten Optionen zusammen:
| Tool | Typ | CPU-tauglich | VRAM | Lizenz | DSGVO lokal |
|---|---|---|---|---|---|
| PaddleOCR v3.6 (PP-StructureV3) | OCR + Layout | Ja (1–1,5 s/Seite) | Keiner | Apache 2.0 | Ja |
| Docling v2.96.1 (IBM) | PDF/Bild zu JSON | Ja | Keiner | Apache 2.0 | Ja |
| GraniteDocling-258M | Spezialisiertes Dok.-Modell | Ja (bis 30× schneller) | Gering | Apache 2.0 | Ja |
| Qwen2.5-VL-7B (Ollama) | Vision-LLM | Langsam (10–30×) | ~8 GB (4-bit: ~5 GB) | Apache 2.0 | Ja |
| Qwen2.5-VL-72B | Vision-LLM | Nein | ~40 GB (4-bit: ~20 GB) | Apache 2.0 | Ja |
| GLM-4.5V / GLM-OCR | Vision-LLM + Thinking | Langsam | Modellabhängig | Offen | Ja |
| Mistral OCR API | Cloud-SaaS | N/A | N/A | Proprietär, DPA | Nur mit DPA |
Empfehlung ohne GPU: PaddleOCR v3 (PP-StructureV3) für OCR + Layout, danach ein kleines lokales LLM (z. B. Qwen2.5:3B via Ollama) für die JSON-Strukturierung. Mit GPU (≥8 GB VRAM): Qwen2.5-VL-7B übernimmt beides in einem Schritt. Cloud als Ergänzung (z. B. für Handschrift-Belege): Mistral OCR mit abgeschlossenem DPA.
Schritt 2: Abhängigkeiten installieren
# Basis-Pakete (alle Varianten)
pip install docling paddlepaddle paddleocr pydantic opencv-python pdf2image pdfplumber
# Nur wenn du Qwen2.5-VL direkt via Transformers nutzt (GPU empfohlen)
pip install transformers accelerate qwen-vl-utils
# Nur wenn du Mistral OCR (Cloud) nutzen möchtest
pip install mistralai
# Ollama separat installieren: https://ollama.com
# Danach Qwen2.5-VL lokal herunterladen:
ollama pull qwen2.5vl:7b
Schritt 3: PDF vorprüfen und Bild vorverarbeiten
Nicht jedes PDF enthält einen Textlayer. Bild-PDFs (z. B. eingescannte Rechnungen) liefern mit Standard-Bibliotheken leeren Text. Prüfe das zuerst – und bereite Scans mit schlechter Qualität per OpenCV auf:
pip install pdfplumber pdf2image opencv-python
# text_layer_check.py
import pdfplumber
from pdf2image import convert_from_path
import cv2
import numpy as np
def check_text_layer(pdf_path: str) -> bool:
with pdfplumber.open(pdf_path) as pdf:
text = pdf.pages[0].extract_text() or ''
return len(text.strip()) > 50 # Heuristik: weniger = Bild-PDF
def preprocess_scan(image_path: str, output_path: str, target_dpi: int = 300):
img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
# Otsu-Binarisierung
_, binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# Deskewing (Schieflagen-Korrektur)
coords = np.column_stack(np.where(binary < 255))
angle = cv2.minAreaRect(coords)[-1]
if angle < -45:
angle = -(90 + angle)
else:
angle = -angle
(h, w) = binary.shape[:2]
M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0)
rotated = cv2.warpAffine(binary, M, (w, h), flags=cv2.INTER_CUBIC,
borderMode=cv2.BORDER_REPLICATE)
cv2.imwrite(output_path, rotated)
print(f'Vorverarbeitung abgeschlossen: {output_path}')
# Beispielaufruf
if not check_text_layer('rechnung.pdf'):
images = convert_from_path('rechnung.pdf', dpi=300)
images[0].save('rechnung_seite1.png')
preprocess_scan('rechnung_seite1.png', 'rechnung_clean.png')
Schritt 4a: OCR + Layout-Analyse mit PaddleOCR PP-StructureV3
PaddleOCR v3.6 bringt mit PP-StructureV3 eine integrierte Layout-Analyse: Es erkennt Textblöcke, Tabellen und Schlüssel-Wert-Paare in einem Durchgang. Für rein CPU-basierte KMU-Umgebungen die erste Wahl.
pip install paddlepaddle paddleocr
# paddleocr_extract.py
from paddleocr import PPStructure
import json
engine = PPStructure(table=True, ocr=True, show_log=False, lang='german')
result = engine('rechnung_clean.png')
# Ergebnis strukturieren
extracted = {'tables': [], 'text_blocks': []}
for block in result:
if block['type'] == 'table':
extracted['tables'].append(block['res'])
elif block['type'] == 'text':
extracted['text_blocks'].append(block['res'])
print(json.dumps(extracted, ensure_ascii=False, indent=2))
Das Ergebnis enthält getrennte Listen für Tabellenzeilen und Textblöcke. Im nächsten Schritt übernimmt ein LLM die Strukturierung in das finale JSON-Schema.
Schritt 4b: PDF-Extraktion mit Docling (IBM)
Für native PDF-Dokumente mit Textlayer ist Docling v2.96.1 die elegantere Lösung – kein Umweg über Bilder nötig:
pip install docling
# docling_extract.py
from docling.document_converter import DocumentConverter
import json
converter = DocumentConverter()
result = converter.convert('rechnung.pdf')
# Als JSON exportieren
doc_json = result.document.export_to_json()
print(json.dumps(json.loads(doc_json), ensure_ascii=False, indent=2))
# Alternativ als Markdown (für LLM-Weiterverarbeitung)
print(result.document.export_to_markdown())
Schritt 4c: Vision-LLM mit Qwen2.5-VL-7B via Ollama
Wenn du Qwen2.5-VL bereits via Ollama installiert hast, ist dieser Weg am unkompliziertesten – keine Python-Modell-Verwaltung, einfache REST-API. Das Modell extrahiert direkt strukturierten JSON aus dem Rechnungsbild:
# Modell laden (einmalig, ca. 4-5 GB Download)
ollama pull qwen2.5vl:7b
# ollama_invoice.py
import base64, json, httpx
def image_to_base64(path: str) -> str:
with open(path, 'rb') as f:
return base64.b64encode(f.read()).decode()
prompt = """Extrahiere alle Felder aus dieser Rechnung als valides JSON-Objekt.
Pflichtfelder: rechnungsnummer, rechnungsdatum (ISO 8601: YYYY-MM-DD),
lieferant, waehrung (ISO-4217), positionen (Liste mit beschreibung/menge/einzelpreis/gesamtpreis),
nettobetrag, mwst_betrag, bruttobetrag.
Optionale Felder: iban, bestellnummer.
Nur das JSON-Objekt ausgeben, kein erklärender Text."""
payload = {
'model': 'qwen2.5vl:7b',
'prompt': prompt,
'images': [image_to_base64('rechnung_clean.png')],
'stream': False,
'format': 'json'
}
resp = httpx.post('http://localhost:11434/api/generate', json=payload, timeout=120)
result = resp.json()
invoice_data = json.loads(result['response'])
print(json.dumps(invoice_data, ensure_ascii=False, indent=2))
Alternativ kannst du Qwen2.5-VL direkt über die Transformers-Bibliothek laden – das bietet mehr Kontrolle über Quantisierung und Batch-Verarbeitung, erfordert aber eine GPU mit mindestens 8 GB VRAM für das 7B-Modell (4-Bit: ~5 GB).
Schritt 4d: Mistral OCR API als Cloud-Ergänzung
Für Teilmengen, bei denen lokale Modelle versagen – insbesondere Handschrift – bietet Mistral OCR eine DSGVO-konforme Cloud-Option, sofern du das Data Processing Addendum abgeschlossen hast. Kosten: ca. 1.000 Seiten pro US-Dollar (Batch: ~2.000 Seiten/USD).
# mistral_ocr.py
from mistralai import Mistral
import json, base64, pathlib
client = Mistral(api_key='DEIN_API_KEY')
# Dokument als Base64
doc_bytes = pathlib.Path('rechnung.pdf').read_bytes()
doc_b64 = base64.b64encode(doc_bytes).decode()
response = client.ocr.process({
'model': 'mistral-ocr-latest',
'document': {
'type': 'document_url',
'document_url': f'data:application/pdf;base64,{doc_b64}'
},
'document_annotation_format': {'type': 'json_schema'},
'include_image_base64': False,
'extract_header': True
})
for page in response.pages:
print(page.markdown)
Schritt 5: Zweistufige Validierung mit Pydantic v2
Das extrahierte JSON ist nur so gut wie seine Validierung. Pydantic v2 (Rust-Core, 5–50× schneller als v1) prüft in zwei Stufen: zuerst Struktur und Pflichtfelder, dann Business-Logik (Summencheck auf Cent-Genauigkeit). Wichtig: Beträge immer als decimal.Decimal – nie als float, da float(19.99) * 3 = 59.97000000000001 zu falschen Summenprüfungen führt.
# pydantic_invoice.py
from pydantic import BaseModel, field_validator, model_validator
from decimal import Decimal, ROUND_HALF_UP
from datetime import date
from typing import Optional, List
import json
class LineItem(BaseModel):
beschreibung: str
menge: Decimal
einzelpreis: Decimal
gesamtpreis: Decimal
@model_validator(mode='after')
def check_line_total(self):
expected = (self.menge * self.einzelpreis).quantize(
Decimal('0.01'), rounding=ROUND_HALF_UP
)
if expected != self.gesamtpreis:
raise ValueError(
f'Zeilenpreis falsch: {self.menge} x {self.einzelpreis} != {self.gesamtpreis}'
)
return self
class Invoice(BaseModel):
rechnungsnummer: str # Pflichtfeld
rechnungsdatum: date # Pflichtfeld
lieferant: str # Pflichtfeld
waehrung: str = 'EUR' # ISO-4217
positionen: List[LineItem]
nettobetrag: Decimal
mwst_betrag: Decimal
bruttobetrag: Decimal
iban: Optional[str] = None
@field_validator('waehrung')
@classmethod
def validate_currency(cls, v):
if len(v) != 3 or not v.isalpha():
raise ValueError(f'Ungültiger ISO-4217-Code: {v}')
return v.upper()
@model_validator(mode='after')
def check_total(self):
summe = sum(p.gesamtpreis for p in self.positionen)
expected_brutto = (summe + self.mwst_betrag).quantize(
Decimal('0.01'), rounding=ROUND_HALF_UP
)
if expected_brutto != self.bruttobetrag:
raise ValueError(
f'Bruttosumme stimmt nicht: {expected_brutto} != {self.bruttobetrag}'
)
return self
# Verwendung
raw_data = {
'rechnungsnummer': 'RE-2025-0042',
'rechnungsdatum': '2025-03-15',
'lieferant': 'Muster GmbH',
'waehrung': 'EUR',
'positionen': [
{'beschreibung': 'Servermiete', 'menge': '1', 'einzelpreis': '99.00', 'gesamtpreis': '99.00'}
],
'nettobetrag': '99.00',
'mwst_betrag': '18.81',
'bruttobetrag': '117.81'
}
try:
invoice = Invoice.model_validate(raw_data)
print('Validierung erfolgreich:', invoice.model_dump_json(indent=2))
except Exception as e:
print('Validierungsfehler:', e)
Schritt 6: Paperless-ngx automatisch befüllen
Paperless-ngx unterstützt ab v2.0 Custom Fields (Rechnungsnummer, Datum, Betrag) und Post-Consumption-Skripte. Das Skript wird nach jeder OCR-Verarbeitung automatisch aufgerufen und schreibt die extrahierten Werte per REST-API zurück. Mehr zum Setup findest du in der Anleitung Paperless-ngx Dokumentenverwaltung mit OCR und Docker einrichten.
# In paperless.conf hinzufügen:
# PAPERLESS_POST_CONSUME_SCRIPT=/opt/scripts/post-consume.sh
#!/bin/bash
# /opt/scripts/post-consume.sh
# Umgebungsvariablen von Paperless: DOCUMENT_ID, DOCUMENT_FILE_NAME, DOCUMENT_CONTENT
# Rechnungsnummer per Regex aus OCR-Text extrahieren
INV_NR=$(echo "$DOCUMENT_CONTENT" | grep -oP 'Rechnungsnr[.:\s]+\K[A-Z0-9-]+')
if [ -n "$INV_NR" ]; then
curl -s -X PATCH \
-H "Authorization: Token $PAPERLESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"custom_fields\": [{\"field\": 1, \"value\": \"$INV_NR\"}]}" \
"http://localhost:8000/api/documents/$DOCUMENT_ID/"
echo "Rechnungsnummer gesetzt: $INV_NR"
fi
Custom Field IDs ermittelst du per GET /api/custom_fields/. Die Workflow-Aktionen in Paperless-ngx können Felder zusätzlich regelbasiert befüllen – kombiniert mit dem Post-Consumption-Skript erhältst du eine vollständige Automatisierung.
Troubleshooting / Typische Fehler
- Leerer OCR-Text bei Bild-PDFs: PyPDF2 und pdfminer liefern für Scan-PDFs ohne Textlayer leeren String. Lösung: Immer zuerst
pdfplumberprüfen (siehe Schritt 3), bei leerem Ergebnis auf OCR-Pipeline umschalten. - Schlechte Erkennungsrate bei Scans unter 150 DPI: PaddleOCR und Vision-LLMs versagen bei niedrigem DPI und Hintergrundrauschen. Lösung: OpenCV-Vorverarbeitung (Graustufen, Otsu-Binarisierung, Deskewing) +
convert_from_path(..., dpi=300). - Zusammengefügte Tabellenzellen (Merged Cells): PP-StructureV3 und Tesseract verlieren bei komplexen Multi-Spalten-Layouts Zellgrenzen. Lösung: Docling oder Qwen2.5-VL mit explizitem Tabellen-Prompt einsetzen; alternativ GraniteDocling-258M.
- Float-Rundungsfehler bei Summenprüfung:
float(19.99) * 3ergibt59.97000000000001– Pydantic-Validierung schlägt fehl. Lösung: Ausschließlichdecimal.DecimalmitROUND_HALF_UPverwenden. - Datumsformat-Ambiguität: „01.03.2025" kann 1. März oder 3. Januar (US) sein. Lösung: Mehrere Formate probieren (
%d.%m.%Y,%Y-%m-%d,%m/%d/%Y); bei Ambiguität manuell flaggen. - VRAM-Engpass bei 72B-Modell: Qwen2.5-VL-72B benötigt ~40 GB VRAM (4-Bit: ~20 GB). Lösung: 7B-Modell verwenden oder 4-Bit-Quantisierung mit
bitsandbytes. - Handschrift und Unterschriften: Kein lokales Open-Source-Modell erreicht zuverlässig über 85 % Genauigkeit. Lösung: Separat flaggen, zur manuellen Prüfung leiten oder Mistral OCR Cloud für diese Teilmenge nutzen (nur mit DPA).
- Mehrseitige PDFs mit AGB-Seiten: Alle Seiten zu verarbeiten erhöht Latenz und kann Extraktion verfälschen. Lösung: Seitenselektion per
pages-Parameter (Mistral OCR) oder erst Seite 1–2 verarbeiten. - Lieferantenvielfalt (verschiedene Layouts): „Rechnungsnr.", „Invoice No.", „Re.-Nr." – jeder Lieferant ist anders. Lösung: Few-Shot-Prompting (3–5 Beispiele im System-Prompt) oder Fine-Tuning auf 50–200 eigenen Rechnungen (Roboflow-Notebook verfügbar).
Häufige Fragen
Welches Tool ist für KMU ohne GPU am besten geeignet?
PaddleOCR v3 (PP-StructureV3) läuft effizient auf CPU (ca. 1–1,5 Sekunden pro Seite) und benötigt nur 150–200 MB Speicher. Für strukturierten JSON-Output schalte ein kleines lokales LLM nach – z. B. Ollama mit qwen2.5:3b oder llama3.2:3b, das den OCR-Text in das gewünschte JSON-Schema umwandelt.
Wie bleibe ich vollständig DSGVO-konform ohne Cloud?
Vollständig lokale Pipeline: PaddleOCR oder Docling für OCR + Qwen2.5-VL-7B via Ollama für Extraktion + Pydantic für Validierung. Alle Komponenten laufen on-premises; kein Byte verlässt deinen Server. Mistral OCR gilt als Cloud-Lösung und ist nur DSGVO-konform, wenn ein Data Processing Addendum (DPA) abgeschlossen ist. Für hochsensible Daten (Personaldaten, Patientendaten) ist ausschließlich die lokale Pipeline vertretbar. Mehr zur DSGVO-Praxis in der Anleitung DSGVO TOMs und Auftragsverarbeitung in der Praxis.
Wie integriere ich die extrahierten Daten in Paperless-ngx?
Paperless-ngx Custom Fields (ab v2.0) können per REST-API (PATCH /api/documents/{id}/) mit Werten befüllt werden. Post-Consumption-Skripte werden automatisch nach der OCR-Verarbeitung ausgeführt und können Rechnungsnummer, Datum und Betrag per Regex aus dem OCR-Text extrahieren und als Custom Fields speichern (siehe Schritt 6). Workflow-Aktionen in Paperless-ngx ergänzen das regelbasiert.
Wie gut funktioniert das bei deutschen Rechnungen mit Umlauten?
PaddleOCR unterstützt Deutsch nativ – beim Laden lang='german' angeben. Qwen2.5-VL versteht Deutsch ohne Sonderkonfiguration. Das Hauptproblem sind spezifische deutsche Datumsformate (TT.MM.JJJJ) und Tausendertrennzeichen (1.234,56 EUR), die im Validierungsschritt normalisiert werden müssen.
Lohnt sich Fine-Tuning auf eigene Rechnungen?
Ab ca. 50–200 annotierten Rechnungsbeispielen verbessert sich die Extraktionsgenauigkeit deutlich. Roboflow stellt ein öffentliches Fine-Tuning-Notebook für Qwen2.5-VL bereit (how-to-finetune-qwen2-5-vl-for-json-data-extraction.ipynb). Ohne GPU ist Fine-Tuning nicht praktikabel – dann lieber Few-Shot-Prompting mit 3–5 Beispielen im System-Prompt.
Was kostet Mistral OCR für 1.000 Rechnungen pro Monat?
Bei ca. 1.000 Seiten/USD und angenommenen 2 Seiten pro Rechnung kostet die Verarbeitung von 1.000 Rechnungen (2.000 Seiten) ca. 2 USD/Monat. Mit Batch-Inferenz ca. 1 USD/Monat. Ab ca. 10.000 Rechnungen pro Monat lohnt sich die lokale Lösung schon mit einem mittleren Server.
Fazit
Ein vollständig lokaler Rechnungs-Extraktions-Workflow ist heute auch für KMU ohne großes IT-Budget realisierbar. PaddleOCR v3 (PP-StructureV3) liefert auf normaler CPU in ca. 1–1,5 Sekunden pro Seite gute Ergebnisse für Layout und Text; Docling (IBM) ist die bessere Wahl für native PDFs. Wer eine GPU zur Hand hat, setzt Qwen2.5-VL-7B via Ollama ein und bekommt strukturierten JSON in einem Schritt. Pydantic v2 schützt vor Tippfehlern und Rundungsfehlern, Paperless-ngx speichert alles strukturiert. Die wichtigste Erkenntnis: Float nie für Geldbeträge verwenden – decimal.Decimal ist Pflicht. Mistral OCR ist eine valide Cloud-Ergänzung für schwierige Dokumente, aber kein Ersatz für sensible Daten ohne abgeschlossenes DPA.
Weiterführende Anleitungen und Quellen
- Paperless-ngx: Dokumentenverwaltung mit OCR und Docker einrichten
- Ollama + Open WebUI: Lokale LLMs mit Docker betreiben
- Qdrant: Lokales RAG-System mit Embeddings aufbauen
- DSGVO TOMs und Auftragsverarbeitung in der Praxis
Quellen: Docling Projektseite (docling-project.github.io) · PaddleOCR GitHub Repository · Qwen2.5-VL-7B-Instruct Modellkarte (HuggingFace) · Mistral OCR API Dokumentation (docs.mistral.ai) · IBM GraniteDocling-258M Ankündigung · Roboflow Fine-Tuning Notebook · Paperless-ngx Advanced Usage (docs.paperless-ngx.com)