Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Künstliche Intelligenz 04.06.2026 · 11 min Lesezeit

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.

Server mit schwebenden Rechnungsdokumenten und JSON-Datenextraktion per KI

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:

ToolTypCPU-tauglichVRAMLizenzDSGVO lokal
PaddleOCR v3.6 (PP-StructureV3)OCR + LayoutJa (1–1,5 s/Seite)KeinerApache 2.0Ja
Docling v2.96.1 (IBM)PDF/Bild zu JSONJaKeinerApache 2.0Ja
GraniteDocling-258MSpezialisiertes Dok.-ModellJa (bis 30× schneller)GeringApache 2.0Ja
Qwen2.5-VL-7B (Ollama)Vision-LLMLangsam (10–30×)~8 GB (4-bit: ~5 GB)Apache 2.0Ja
Qwen2.5-VL-72BVision-LLMNein~40 GB (4-bit: ~20 GB)Apache 2.0Ja
GLM-4.5V / GLM-OCRVision-LLM + ThinkingLangsamModellabhängigOffenJa
Mistral OCR APICloud-SaaSN/AN/AProprietär, DPANur 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 pdfplumber prü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) * 3 ergibt 59.97000000000001 – Pydantic-Validierung schlägt fehl. Lösung: Ausschließlich decimal.Decimal mit ROUND_HALF_UP verwenden.
  • 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 verwendendecimal.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

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)