Stanza ist eine Open Source NLP Bibliothek der Stanford University, die auf modernen neuronalen Netzen basiert. Sie ermöglicht die umfassende linguistische Analyse von Texten in über 70 Sprachen. Ziel von Stanza ist es, ein vollständiges Pipeline System bereitzustellen, das alle gängigen Verarbeitungsschritte umfasst: Tokenisierung, Wortartenbestimmung (POS), Lemmatisierung, syntaktische Analyse (Abhängigkeiten und Konstituenten) sowie Named Entity Recognition (NER).
Stanza eignet sich sowohl für Forschungszwecke als auch für produktive Anwendungen, etwa bei der Textklassifikation, Informationsextraktion oder dem Vorverarbeiten von Texten für Retrieval Augmented Generation (RAG). Die Modelle sind vortrainiert, können aber auch feinjustiert werden. Intern basiert Stanza auf dem PyTorch Framework.
Eine Pipeline in der Sprachverarbeitung (NLP) bezeichnet eine festgelegte Abfolge von Verarbeitungsschritten, mit denen ein Text analysiert und strukturiert wird. Jeder Schritt nimmt den Output des vorherigen als Input und reichert ihn um weitere linguistische Informationen an. Ziel ist es, aus rohem Text schrittweise eine tiefere sprachliche Repräsentation zu erzeugen, die für weitere Anwendungen genutzt werden kann, z. B. Textklassifikation, Informationsextraktion oder Fragebeantwortung.
Schritte einer typischen Stanza Pipeline
Die Standardpipeline von Stanza besteht aus den folgenden Modulen (auch „Prozessoren“ genannt), die in dieser Reihenfolge arbeiten:
Tokenisierung (tokenize)
Mehrworterkennung (mwt)
Wortartenbestimmung (pos)
Lemmatisierung (lemma)
Syntaktische Abhängigkeitsanalyse (depparse)
Konstituentenanalyse (constituency) – Nur für Englisch
Benannte Entitäten erkennen (ner)
Stimmungsanalyse (sentiment) – Nur für Englisch
1. Tokenisierung
Die Tokenisierung ist der erste Schritt in der Verarbeitung eines Textes durch eine NLP Pipeline. Dabei wird der Eingabetext in einzelne Einheiten zerlegt. Diese Einheiten nennt man Tokens. Ein Token kann ein Wort, eine Zahl, ein Satzzeichen oder ein Symbol sein. Die Tokenisierung legt fest, wo ein Token anfängt und wo es endet. Das ist wichtig, weil alle weiteren Verarbeitungsschritte diese Einheiten verwenden.
Stanza verwendet für die Tokenisierung sprachspezifische Modelle. Diese Modelle sind auf reale Sprachdaten trainiert und berücksichtigen Besonderheiten der jeweiligen Sprache. Im Englischen erkennt Stanza zum Beispiel, dass „U.S.A.“ ein einzelnes Token ist und nicht drei. Auch Abkürzungen, Zahlenformate und Emojis werden korrekt behandelt.
Was Stanza in diesem Schritt liefert:
Eine Aufteilung des Textes in Sätze
Eine Liste von Tokens pro Satz
Zu jedem Token werden Start- und Endposition im Originaltext gespeichert
2. Mehrworterkennung (MWT)
Die Mehrworterkennung oder Multi Word Token Expansion ist ein optionaler Schritt in der Stanza Pipeline. Er wird nur für bestimmte Sprachen aktiviert, bei denen einzelne Tokens aus mehreren Wörtern bestehen können. Dazu gehören vor allem morphologisch komplexe Sprachen wie Arabisch oder Französisch. In Sprachen wie Deutsch oder Englisch ist dieser Schritt standardmäßig deaktiviert, da Wörter dort bereits einzeln geschrieben werden.
Bei aktivierter MWT Komponente wird ein Token, das mehrere Wörter enthält, in seine Bestandteile zerlegt. Die ursprüngliche Tokenstruktur bleibt erhalten, aber es werden zusätzliche Wörter erzeugt. Diese Wörter sind die tatsächlichen Einheiten, mit denen die weiteren Module wie POS oder Lemmatisierung arbeiten.
Die Wortartenbestimmung ist ein zentraler Schritt in der Verarbeitung natürlicher Sprache. Dabei wird jedem Wort eine grammatische Kategorie zugewiesen. Beispiele für solche Kategorien sind Nomen, Verb, Adjektiv, Adverb, Artikel oder Präposition. Diese Informationen sind für fast alle weiteren Schritte erforderlich, da sie grammatische Strukturen sichtbar machen.
Stanza verwendet neuronale Modelle, um diese Kategorisierung automatisch vorzunehmen. Für jedes Wort wird sowohl eine universelle Wortart (UPOS) als auch eine sprachspezifische, detaillierte Wortart (XPOS) vergeben. Zusätzlich werden morphologische Merkmale erfasst, wie Genus, Numerus, Kasus, Tempus oder Verbform.
UPOS bedeutet „Universal Part of Speech“. Das ist eine Einteilung von Wörtern in grundlegende Wortarten wie Nomen, Verb, Adjektiv oder Artikel. Dieses System ist für alle Sprachen gleich. Ein Verb in Deutsch und ein Verb in Englisch erhalten beide das Merkmal VERB.
XPOS ist die Wortart, wie sie in einer bestimmten Sprache üblich ist. In Stanza wird sie für jede Sprache anders festgelegt. Für Englisch bedeutet das:
NN steht für ein Nomen im Singular
VBZ steht für ein Verb mit dritter Person Singular im Präsens
JJ steht für ein Adjektiv
XPOS ist also eine genauere Beschreibung der Wortart, wie sie in der jeweiligen Sprache gebraucht wird.
4. Lemmatisierung
Die Lemmatisierung ist der Prozess, bei dem ein Wort auf seine Grundform zurückgeführt wird. Diese Grundform nennt man Lemma. Ziel ist es, verschiedene grammatische Formen eines Wortes auf eine einheitliche Form zu bringen. Das ist wichtig, um Wörter unabhängig von Zeitform, Person oder Numerus vergleichen oder verarbeiten zu können.
Beispiele:
„went“ wird zu „go“
„dogs“ wird zu „dog“
Stanza verwendet für die Lemmatisierung ein Modell, das den Kontext des Wortes berücksichtigt. Dadurch kann es auch Wörter mit mehreren Bedeutungen korrekt behandeln.
5. Syntaktische Abhängigkeitsanalyse
Die syntaktische Abhängigkeitsanalyse untersucht die grammatische Struktur eines Satzes. Dabei wird für jedes Wort bestimmt, zu welchem anderen Wort es gehört und welche Rolle es dabei spielt. Das Ergebnis ist ein gerichteter Baum, in dem jedes Wort genau einem anderen untergeordnet ist. Die Verbindungen nennt man Kanten, und sie tragen grammatische Bezeichnungen wie Subjekt, Objekt oder Modifikator.
Stanza verwendet dafür ein neuronales Modell, das sogenannte Universal Dependencies erzeugt. Diese Struktur zeigt, wie die Wörter im Satz miteinander verbunden sind. Jedes Wort hat dabei einen sogenannten Head, also das übergeordnete Wort, und eine Beziehung zu diesem Head.
Wichtige Abhängigkeitsbeziehungen
nsubj: nominales Subjekt
obj: direktes Objekt
obl: adverbiale Ergänzung
root: Wurzel des Satzes, meist das Hauptverb
det: Artikel
amod: Adjektiv als Modifikator eines Nomens
case: Präposition oder Kasusanzeiger
punct: Satzzeichen
6. Konstituentenanalyse – Nur für Englisch
Die Konstituentenanalyse untersucht, aus welchen Satzteilen ein Satz besteht und wie diese Satzteile miteinander verschachtelt sind. Dabei wird erkannt, welche Wörter zusammen eine Einheit bilden, zum Beispiel ein Subjekt oder ein Objekt. Solche Einheiten nennt man Phrasen, zum Beispiel Nominalphrase oder Verbalphrase.
Die Analyse zeigt die Struktur des Satzes als Baum. Jeder Satz wird dabei in immer größere Gruppen aufgeteilt, zum Beispiel: zuerst einzelne Wörter, dann Phrasen, dann der ganze Satz.
Stanza verwendet dafür ein englisches Regelwerk namens Penn Treebank. Das funktioniert gut für Texte auf Englisch. Für deutsche Texte ist die Konstituentenanalyse in Stanza zurzeit nicht verfügbar. Die Funktion eignet sich vor allem für englische Sätze. Für Deutsch muss man zusätzlich die Berkeley Neural Parser (Benepar) Bibliothek verwenden.
Die Erkennung benannter Entitäten ist ein Schritt in der NLP Verarbeitung, bei dem bestimmte Wörter oder Wortgruppen als bedeutende Objekte erkannt werden. Diese Objekte nennt man Entitäten. Sie beziehen sich zum Beispiel auf Personen, Orte, Organisationen, Zeitangaben oder Geldbeträge.
Stanza erkennt Entitäten automatisch auf Basis eines trainierten neuronalen Modells. Jede erkannte Entität wird einem festen Typ zugeordnet. Das Modell bezieht den Kontext mit ein und kann auch mehrteilige Entitäten wie „New York City“ oder „United Nations“ korrekt erfassen.
Entitätstypen in Stanza
PERSON: Vorname oder Nachname einer Person
GPE: Geopolitische Einheit wie Land oder Stadt
ORG: Organisation wie Firma oder Behörde
DATE: Datum
TIME: Uhrzeit
MONEY: Geldbetrag
LOC: Geografische Angabe ohne politische Funktion
PRODUCT: Gegenstand oder Produkt
8. Stimmungsanalyse (Sentiment Analysis) – Nur für Englisch
Die Stimmungsanalyse bewertet, ob der Inhalt eines Satzes eher positiv, neutral oder negativ ist. Das Modell untersucht dabei nicht einzelne Wörter, sondern den gesamten Satz in seinem Zusammenhang. So kann es zum Beispiel erkennen, dass ironische oder abschwächende Formulierungen eine eigentlich positive Aussage neutral oder sogar negativ erscheinen lassen.
Stanza bietet die Stimmungsanalyse zurzeit nur für englische Texte an. Die Grundlage ist ein neuronales Modell, das auf dem SSTplus Korpus trainiert wurde. Das Modell ist in der Lage, jede Satzstruktur zu analysieren und einer von drei Kategorien zuzuordnen.
Klassifikationsstufen
0: negativ
1: neutral
2: positiv
Code Beispiel
import stanza
stanza.download('en') # download English model nlp = stanza.Pipeline('en') # initialize English neural pipeline doc = nlp("Barack Obama was born in Hawaii.") # run annotation over a sentence
print(doc)
Ausgabe
angraph/venv/python.exe c:/sources/agents/langraph/stanza-test.py Downloading https://raw.githubusercontent.com/stanfordnlp/stanza-resources/main/resources_1.10.0.json: 428kB [00:00, 27.5MB/s] 2025-06-07 12:39:46 INFO: Downloaded file to C:\Users\info\stanza_resources\resources.json 2025-06-07 12:39:46 INFO: Downloading default packages for language: en (English) ... Downloading https://huggingface.co/stanfordnlp/stanza-en/resolve/v1.10.0/models/default.zip: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 526M/526M [00:16<00:00, 31.5MB/s] 2025-06-07 12:40:04 INFO: Downloaded file to C:\Users\info\stanza_resources\en\default.zip 2025-06-07 12:40:06 INFO: Finished downloading models and saved to C:\Users\info\stanza_resources 2025-06-07 12:40:06 INFO: Checking for updates to resources.json in case models have been updated. Note: this behavior can be turned off with download_method=None or download_method=DownloadMethod.REUSE_RESOURCES Downloading https://raw.githubusercontent.com/stanfordnlp/stanza-resources/main/resources_1.10.0.json: 428kB [00:00, 27.3MB/s] 2025-06-07 12:40:06 INFO: Downloaded file to C:\Users\info\stanza_resources\resources.json 2025-06-07 12:40:07 INFO: Loading these models for language: en (English): ============================================ | Processor | Package | -------------------------------------------- | tokenize | combined | | mwt | combined | | pos | combined_charlm | | lemma | combined_nocharlm | | constituency | ptb3-revised_charlm | | depparse | combined_charlm | | sentiment | sstplus_charlm | | ner | ontonotes-ww-multi_charlm | ============================================
import stanza import fitz # PyMuPDF from pathlib import Path
# Schritt 1: PDF laden und Text extrahieren file_path = Path("test_oc.pdf") with fitz.open(file_path) as doc: text = "\n".join(page.get_text() for page in doc)
# Schritt 4: Ergebnis anzeigen (zum Beispiel Satzweise Ausgabe) for i, sentence in enumerate(doc.sentences, start=1): print(f"Satz {i}:") for word in sentence.words: print(f" Wort: {word.text}, Lemma: {word.lemma}, POS: {word.upos}")
(c:\sources\agents\langraph\venv) PS C:\sources\agents\langraph> & c:/sources/agents/langraph/venv/python.exe c:/sources/agents/langraph/stanza-test.py Downloading https://raw.githubusercontent.com/stanfordnlp/stanza-resources/main/resources_1.10.0.json: 428kB [00:00, 20.3MB/s] 2025-06-07 15:01:21 INFO: Downloaded file to C:\Users\info\stanza_resources\resources.json 2025-06-07 15:01:21 INFO: Downloading default packages for language: de (German) ... 2025-06-07 15:01:22 INFO: File exists: C:\Users\info\stanza_resources\de\default.zip 2025-06-07 15:01:25 INFO: Finished downloading models and saved to C:\Users\info\stanza_resources 2025-06-07 15:01:25 INFO: Checking for updates to resources.json in case models have been updated. Note: this behavior can be turned off with download_method=None or download_method=DownloadMethod.REUSE_RESOURCES Downloading https://raw.githubusercontent.com/stanfordnlp/stanza-resources/main/resources_1.10.0.json: 428kB [00:00, 20.5MB/s] 2025-06-07 15:01:26 INFO: Downloaded file to C:\Users\info\stanza_resources\resources.json 2025-06-07 15:01:27 INFO: Loading these models for language: de (German): ==================================== | Processor | Package | ------------------------------------ | tokenize | combined | | mwt | combined | | pos | combined_charlm | | lemma | combined_nocharlm | | constituency | spmrl_charlm | | depparse | combined_charlm | | sentiment | sb10k_charlm | | ner | germeval2014 | ====================================
Das Problem bei der obigen Ausgabe ist, dass der Text aus dem PDF ohne ausreichende Vorverarbeitung direkt und als Ganzes an die Stanza-Pipeline übergeben wurde. Dadurch werden nicht nur die eigentlichen inhaltlichen Sätze, sondern auch sämtliche Layout- und Formatierungsreste des Dokuments, wie beispielsweise Kopfzeilen, Seitennummern, einzelne Überschriften, Fußnoten oder Listenpunkte, von Stanza als eigenständige Sätze erkannt und verarbeitet. Das führt dazu, dass in der Ausgabe zahlreiche inhaltlich zusammenhanglose oder sogar bedeutungslose Fragmente als einzelne Sätze erscheinen. Auch inhaltlich zusammenhängende Sätze werden durch harte Zeilenumbrüche, wie sie bei PDFs häufig auftreten, von der Pipeline unterbrochen und als getrennte Einheiten analysiert. Die Folge ist, dass die linguistische Auswertung nicht die eigentliche Satzstruktur widerspiegelt, sondern vom Layout und den technischen Besonderheiten des PDFs dominiert wird. Für eine sinnvolle und präzise Analyse müssen solche Artefakte bereits vor der Übergabe an die Stanza-Pipeline bereinigt, Wörter an Zeilenumbrüchen korrekt zusammengesetzt und der Text in vollständige, grammatisch vollständige Einzelsätze segmentiert werden. Nur so lässt sich mit Stanza eine semantisch aussagekräftige und für nachgelagerte Verarbeitungsschritte brauchbare linguistische Analyse erzielen.
Extraktion und linguistische Vorverarbeitung via DeepSeek & Stanza
stanza torch pdfplumber requests
requirements.txt
# Vorbereitung: Bibliotheken und Hilfsfunktionen (außerhalb der Stanza-Pipeline) import pdfplumber import requests from pathlib import Path import stanza
# Vorbereitung: Datei- und Modellpfade pdf_path = Path("test_oc.pdf")
# Vorbereitung: PDF-Text extrahieren und bereinigen def extract_clean_text(path: Path, top_margin: float = 0.15, bottom_margin: float = 0.15) -> str: """ Diese Funktion ist Teil der Vorbereitung und **kein Schritt der Stanza-Pipeline**. - PDF-Datei öffnen und jede Seite durchgehen. - Obere und untere Seitenränder abschneiden. - Text extrahieren, Zeilen normalisieren. - Worttrennungen am Zeilenende auflösen. - Textblöcke zu einem Fließtext zusammenfassen. """ paragraphs = [] with pdfplumber.open(path) as pdf: for page in pdf.pages: height = page.height top = height * top_margin bottom = height * (1 - bottom_margin) cropped = page.within_bbox((0, top, page.width, bottom)) raw = cropped.extract_text() if not raw: continue current_line = "" for line in raw.splitlines(): line = line.strip() if not line: continue if line.isupper(): continue if line.endswith("-") and len(line) > 1 and line[-2].isalpha(): current_line = line[:-1] else: current_line = " " line if line.endswith((".", "!", "?")): paragraphs.append(current_line.strip()) current_line = "" if current_line: paragraphs.append(current_line.strip()) return " ".join(paragraphs)
# Zwischenschritt (kein Bestandteil der Stanza-Pipeline): Satzsegmentierung via DeepSeek def deepseek_sentence_split(text: str) -> list: """ Diese Funktion ist KEIN Teil der Stanza-Pipeline. Sie dient der Aufteilung des Fließtexts in vollständige Sätze (für die nachfolgende Verarbeitung). """ url = "http://localhost:11434/api/generate" prompt = ( "Teile den folgenden deutschen Text in grammatisch vollständige, möglichst kurze Einzelsätze. " "Jede Zeile enthält genau einen abgeschlossenen Satz. " "Keine Nummerierung, keine Einleitung, keine Leerzeilen.\n\n" text.strip() ) data = { "model": "deepseek-Coder-V2:latest", "prompt": prompt, "stream": False } response = requests.post(url, json=data, timeout=120) response.raise_for_status() raw = response.json()["response"] sentences = [line.strip() for line in raw.splitlines() if line.strip()] return sentences
# Vorbereitung: Stanza-Modelle laden und Pipeline konfigurieren # Aktivierte Prozessoren für Deutsch: tokenize (1), mwt (2), pos (3), lemma (4), depparse (5), ner (7) # Schritt 6 (constituency) und Schritt 8 (sentiment) werden für Deutsch ausgelassen. stanza.download('de', processors='tokenize,mwt,pos,lemma,depparse,ner') nlp = stanza.Pipeline( 'de', processors='tokenize,mwt,pos,lemma,depparse,ner', tokenize_no_ssplit=True )
# Vorbereitung: PDF bereinigen und in Fließtext umwandeln text = extract_clean_text(pdf_path)
Ein aufmerksamer Blick auf die Ausgabe von Schritt 7, der Erkennung benannter Entitäten (NER), könnte für eine kurze Verwirrung sorgen. Obwohl Entitäten wie OPITZ CONSULTING und STACKIT korrekt als Organisationen (ORG) erkannt werden, fehlen erwartete Zuweisungen wie zum Beispiel für das Datum „1. März 2024“ oder für Produktbezeichnungen wie „STACKIT Cloud“.
Der Grund hierfür liegt nicht in einem Fehler des Skripts, sondern im zugrundeliegenden Sprachmodell, das Stanza standardmäßig für Deutsch verwendet. Wie die Protokollausgabe beim Start des Skripts verrät, kommt hier das Paket germeval2014 zum Einsatz. Dieses Modell wurde auf Basis des „GermEval 2014 Shared Task“ trainiert und kennt daher hauptsächlich die vier Entitätstypen, die in diesem Wettbewerb im Fokus standen: PER (Person), LOC (Ort), ORG (Organisation) und OTH (Sonstiges).
Kategorien wie DATE (Datum), MONEY (Geldbetrag) oder PRODUCT (Produkt) sind in diesem spezifischen Modell schlichtweg nicht vorgesehen. Es kann also nur das erkennen, wofür es trainiert wurde. Dies ist eine wichtige Erkenntnis bei der Arbeit mit vorgefertigten KI-Modellen: Ihre Leistungsfähigkeit und ihr „Wissen“ sind immer durch die Daten und die Zielsetzung ihres ursprünglichen Trainings begrenzt.
Hybrid-Pipeline: Verbesserung von Named Entity Recognition (NER)
Das ursprüngliche Skript wurde zu einer robusten, modularen Verarbeitungspipeline weiterentwickelt. Die wichtigsten Änderungen sind:
Austausch der NER-Komponente: Der entscheidende Schritt war, die standardmäßige Erkennung benannter Entitäten (NER) von Stanza, die auf dem älteren germeval2014-Modell basiert, zu entfernen. Stattdessen wird nun ein modernes, auf der Hugging Face transformers-Bibliothek basierendes Modell verwendet. Dieses neue Modell (domischwimmbeck/bert-base-german-cased-fine-tuned-ner) erkennt nicht nur mehr Entitätstypen, sondern bietet in der Regel auch eine höhere Genauigkeit.
Einführung einer Hybrid-Pipeline: Anstatt sich auf eine einzige Bibliothek zu verlassen, kombiniert das Skript nun die Stärken von zwei spezialisierten Werkzeugen.
# Für die PDF-Verarbeitung pdfplumber
# Für API-Anfragen an das lokale DeepSeek-Modell requests
# Für die linguistische Analyse (Tokenisierung, POS, Lemma, Depparse) stanza
# Kern-Bibliothek für Stanza und Transformers # Wichtig: Muss mit torchvision kompatibel sein torch
# Wird von torch im Hintergrund genutzt torchvision
# Für die moderne NER-Analyse mit Hugging Face transformers sentencepiece
# Wichtig: Version fixiert für Kompatibilität mit Stanza/Torch numpy<2.0
requirements.txt
# Vorbereitung: Bibliotheken importieren import pdfplumber import requests from pathlib import Path import stanza from transformers import pipeline # Hinzugefügt für Hugging Face
# #################################################################### # HILFSFUNKTIONEN (aus dem ursprünglichen Skript) # ####################################################################
def extract_clean_text(path: Path, top_margin: float = 0.15, bottom_margin: float = 0.15) -> str: """ Diese Funktion ist Teil der Vorbereitung und **kein Schritt der Stanza-Pipeline**. - PDF-Datei öffnen und jede Seite durchgehen. - Obere und untere Seitenränder abschneiden. - Text extrahieren, Zeilen normalisieren. - Worttrennungen am Zeilenende auflösen. - Textblöcke zu einem Fließtext zusammenfassen. """ paragraphs = [] try: with pdfplumber.open(path) as pdf: for page in pdf.pages: height = page.height top = height * top_margin bottom = height * (1 - bottom_margin) cropped = page.within_bbox((0, top, page.width, bottom)) raw = cropped.extract_text() if not raw: continue current_line = "" for line in raw.splitlines(): line = line.strip() if not line: continue # Vereinfachte Annahme: Zeilen in Großbuchstaben sind oft Titel und werden ignoriert if line.isupper() and len(line.split()) < 5: continue if line.endswith("-") and len(line) > 1 and line[-2].isalpha(): current_line = line[:-1] else: current_line = " " line if line.endswith((".", "!", "?")): paragraphs.append(current_line.strip()) current_line = "" if current_line: paragraphs.append(current_line.strip()) except Exception as e: print(f"Fehler beim Lesen der PDF-Datei {path}: {e}") return "" return " ".join(paragraphs)
def deepseek_sentence_split(text: str) -> list: """ Diese Funktion ist KEIN Teil der Stanza-Pipeline. Sie dient der Aufteilung des Fließtexts in vollständige Sätze (für die nachfolgende Verarbeitung). """ if not text: return [] url = "http://localhost:11434/api/generate" prompt = ( "Teile den folgenden deutschen Text in grammatisch vollständige, möglichst kurze Einzelsätze. " "Jede Zeile enthält genau einen abgeschlossenen Satz. " "Keine Nummerierung, keine Einleitung, keine Leerzeilen.\n\n" text.strip() ) data = { "model": "deepseek-Coder-V2:latest", # Stellen Sie sicher, dass dieses Modell lokal verfügbar ist (z.B. via Ollama) "prompt": prompt, "stream": False } try: response = requests.post(url, json=data, timeout=120) response.raise_for_status() raw = response.json()["response"] sentences = [line.strip() for line in raw.splitlines() if line.strip()] return sentences except requests.exceptions.RequestException as e: print(f"Fehler bei der Anfrage an das lokale Sprachmodell unter {url}: {e}") print("Stellen Sie sicher, dass der lokale Server (z.B. Ollama) läuft und das Modell 'deepseek-Coder-V2:latest' geladen ist.") # Fallback: Einfache Satzteilung als Notlösung return text.split('.')
# --- SCHRITT 1: Stanza-Pipeline OHNE NER konfigurieren --- print("Lade Stanza-Modelle (ohne NER)...") try: stanza.download('de', processors='tokenize,mwt,pos,lemma,depparse') nlp_stanza = stanza.Pipeline( 'de', processors='tokenize,mwt,pos,lemma,depparse', # 'ner' wurde hier entfernt tokenize_no_ssplit=True ) print("Stanza-Modelle geladen.") except Exception as e: print(f"Fehler beim Laden der Stanza-Modelle: {e}") exit()
# --- SCHRITT 2: Hugging Face NER-Pipeline mit automatischem Download laden --- print("Lade Hugging Face NER-Modell (automatischer Download)...") try: # Ihr vorgeschlagenes Modell - das ist der Standardweg! # Die Bibliothek lädt das Modell automatisch herunter und speichert es im Cache. ner_pipeline_hf = pipeline( "ner", # "ner" ist der Task, "token-classification" ist ein Alias und funktioniert auch model="domischwimmbeck/bert-base-german-cased-fine-tuned-ner", aggregation_strategy="simple" ) print("Hugging Face NER-Modell erfolgreich geladen.") except Exception as e: print(f"Fehler beim Laden des Hugging Face Modells: {e}") print("Stellen Sie sicher, dass eine stabile Internetverbindung besteht.") exit()
# Vorbereitung: PDF bereinigen und in Sätze umwandeln pdf_path = Path("test_oc.pdf") # Stellen Sie sicher, dass diese Datei existiert text = extract_clean_text(pdf_path) if not text: print("Konnte keinen Text aus der PDF-Datei extrahieren. Das Skript wird beendet.") exit()
sentences = deepseek_sentence_split(text) if not sentences: print("Konnte den Text nicht in Sätze unterteilen. Das Skript wird beendet.") exit()
# --- HYBRIDE VERARBEITUNGSSCHLEIFE --- for i, sentence in enumerate(sentences, start=1): print(f"\nSatz {i}:") print(f" [Original] {sentence}")
# Ausgabe der Stanza-Ergebnisse (Token-Ebene) for s in doc_stanza.sentences: print(" Pipeline-Ausgabe (Stanza, Schritte 1-5):") for word in s.words: print( f" 1-4) Token/POS/Lemma: {word.text:<20} | {word.upos:<8} | {word.lemma:<20}" f" | 5) Depparse: Head={word.head}, DepRel={word.deprel}" )
In der neuen Version des Skripts wurde die komplette linguistische Basisanalyse, die zuvor von der Bibliothek Stanza durchgeführt wurde, durch die Bibliothek spaCy ersetzt. spaCy ist eine sehr populäre und auf Geschwindigkeit optimierte Bibliothek, die oft in produktiven Anwendungen zum Einsatz kommt. Während Stanza für seine hohe akademische Genauigkeit bekannt ist, bietet spaCy eine hervorragende Balance aus Performance und Präzision und liefert Analyseergebnisse, die für die Weiterverarbeitung in Softwareprojekten oft als besonders intuitiv empfunden werden. Die Kernlogik der hybriden Pipeline – die Kombination einer Basis-Analyse mit einem spezialisierten NER-Modell – bleibt dabei identisch. Die folgenden Analyse-Schritte werden nun nicht mehr von Stanza, sondern von spaCy übernommen:
Tokenisierung: Das Aufteilen der Sätze in einzelne Wörter (Tokens).
Wortartbestimmung (POS-Tagging): Die Zuweisung einer grammatikalischen Kategorie zu jedem Wort (z.B. Substantiv, Verb, Adjektiv).
Lemmatisierung: Das Zurückführen jedes Wortes auf seine Grundform (z.B. „ging“ → „gehen“).
Dependenz-Analyse: Die Analyse der syntaktischen Satzstruktur, also welche Wörter im Satz grammatikalisch voneinander abhängen.
def extract_clean_text(path: Path, top_margin: float = 0.15, bottom_margin: float = 0.15) -> str: """ Diese Funktion extrahiert und bereinigt den Text aus einer PDF-Datei. """ paragraphs = [] try: with pdfplumber.open(path) as pdf: for page in pdf.pages: height = page.height top = height * top_margin bottom = height * (1 - bottom_margin) cropped = page.within_bbox((0, top, page.width, bottom)) raw = cropped.extract_text() if not raw: continue current_line = "" for line in raw.splitlines(): line = line.strip() if not line: continue if line.isupper() and len(line.split()) < 5: continue if line.endswith("-") and len(line) > 1 and line[-2].isalpha(): current_line = line[:-1] else: current_line = " " line if line.endswith((".", "!", "?")): paragraphs.append(current_line.strip()) current_line = "" if current_line: paragraphs.append(current_line.strip()) except Exception as e: print(f"Fehler beim Lesen der PDF-Datei {path}: {e}") return "" return " ".join(paragraphs)
def deepseek_sentence_split(text: str) -> list: """ Nutzt ein lokales LLM zur intelligenten Satzzerlegung. """ if not text: return [] url = "http://localhost:11434/api/generate" prompt = ( "Teile den folgenden deutschen Text in grammatisch vollständige, möglichst kurze Einzelsätze. " "Jede Zeile enthält genau einen abgeschlossenen Satz. " "Keine Nummerierung, keine Einleitung, keine Leerzeilen.\n\n" text.strip() ) data = { "model": "deepseek-Coder-V2:latest", "prompt": prompt, "stream": False } try: response = requests.post(url, json=data, timeout=120) response.raise_for_status() raw = response.json()["response"] sentences = [line.strip() for line in raw.splitlines() if line.strip()] return sentences except requests.exceptions.RequestException as e: print(f"Fehler bei der Anfrage an das lokale Sprachmodell unter {url}: {e}") print("Stellen Sie sicher, dass der lokale Server (z.B. Ollama) läuft.") return text.split('.')
# #################################################################### # HAUPTSKRIPT mit OPTIMIERTER REIHENFOLGE # ####################################################################
# --- Modelle laden --- print("Lade spaCy-Modell...") try: nlp_spacy = spacy.load('de_core_news_lg') print("spaCy-Modell geladen.") except OSError: print("spaCy-Modell 'de_core_news_lg' nicht gefunden.") print("Bitte installieren Sie es mit dem Befehl: python -m spacy download de_core_news_lg") exit()
print("Lade Hugging Face NER-Modell...") try: ner_pipeline_hf = pipeline( "ner", model="domischwimmbeck/bert-base-german-cased-fine-tuned-ner", aggregation_strategy="simple" ) print("Hugging Face NER-Modell erfolgreich geladen.") except Exception as e: print(f"Fehler beim Laden des Hugging Face Modells: {e}") exit()
# --- Schritt 1: PDF-Text extrahieren --- pdf_path = Path("test_oc.pdf") full_text = extract_clean_text(pdf_path) if not full_text: print("Konnte keinen Text aus der PDF-Datei extrahieren. Das Skript wird beendet.") exit()
# --- Schritt 2: NER auf dem GESAMTEN Text für maximale Qualität --- print("\n--- Globale Entitäten (im gesamten Text erkannt) ---") all_entities = ner_pipeline_hf(full_text) if all_entities: for entity in all_entities: print(f" - Entity: {entity['word']} ({entity['entity_group']}, Score: {entity['score']:.4f})") else: print(" Keine Entitäten im gesamten Text erkannt.") print("--------------------------------------------------\n")
# --- Schritt 3: Text in Sätze aufteilen --- sentences = deepseek_sentence_split(full_text) if not sentences: print("Konnte den Text nicht in Sätze unterteilen. Das Skript wird beendet.") exit()
# --- Schritt 4: Detaillierte linguistische Analyse pro Satz --- for i, sentence in enumerate(sentences, start=1): print(f"Satz {i}:") print(f" [Original] {sentence}")
# --- spaCy-Analyse pro Satz --- doc_spacy = nlp_spacy(sentence)
# Ausgabe der spaCy-Ergebnisse (Token-Ebene) print(" Linguistische Analyse (spaCy):") for token in doc_spacy: print( f" - Token: {token.text:<20} | POS: {token.pos_:<8} | Lemma: {token.lemma_:<20}" f" | Dep: {token.dep_} -> {token.head.text}" ) print("-" * 20) # Trennlinie für bessere Lesbarkeit
app.py
(c:\sources\agents\langraph\venv) PS C:\sources\agents\langraph> & c:/sources/agents/langraph/venv/python.exe c:/sources/agents/langraph/spacy-test.py Lade spaCy-Modell... spaCy-Modell geladen. Lade Hugging Face NER-Modell... Hugging Face NER-Modell erfolgreich geladen.