Documents
Documents
Ein Bekannter, der für ein Personalvermittlungsunternehmen arbeitete, schilderte mir den allgemeinen Ablauf der Kandidatenauswahl und der Überprüfung von Lebensläufen. Ich war sehr überrascht von seinen Ausführungen; ich hatte ihm daraufhin ein Skript geschrieben, aber er kündigte und orientierte sich beruflich komplett neu. Jetzt braucht niemand mehr dieses Skript – vielleicht aber Sie. Natürlich können Sie nicht nur Lebensläufe mit Stellenanzeigen vergleichen, sondern generell alle Dokumente.
1. virtual environment (venv)
Am besten arbeitet man in einer isolierten Umgebung. Ich persönlich bevorzuge venv, aber man kann natürlich auch conda, miniconda usw. verwenden.
python3 -m venv .venv
source .venv/bin/activate
2. requirements.txt
Alle notwendigen Bibliotheken befinden sich hier.
numpy==1.26.4
pandas==2.2.2
sentence-transformers==3.0.1
pymupdf==1.24.5
docx2pdf==0.1.8
python-docx==1.1.2
spacy==3.7.5
scikit-learn==1.5.0
weasyprint==62.1
3. Installation
Ich empfehle, genau die in der Datei recommendations.txt angegebenen Versionen zu installieren.
pip install -r requirements.txt
4. Model-Installation
Selbstverständlich müssen Sie dasselbe Modell angeben. Wir verwenden hier "de_core_news_lg" (German-large), aber Sie können selbstverständlich auch andere Modelle verwenden, sowohl hinsichtlich der Sprache (en, fr, ...) als auch der Größe (sm, md).
python -m spacy download de_core_news_lg
5. analyser.py
Zum Vergleich und zur Bewertung verwenden wir das Modell de_core_news_lg, das sich sehr gut für die Verarbeitung deutscher Texte eignet.
import os
import sys
import argparse
import csv
from datetime import datetime
# Unterdrücke Hugging Face Warnungen beim Laden von BERT
os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
os.environ["TRANSFORMERS_VERBOSITY"] = "error"
import pandas as pd
import fitz
import spacy
from weasyprint import HTML
from docx2pdf import convert as docx_to_pdf
from sentence_transformers import SentenceTransformer, util
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
# Standardmäßig breite Spaltenausgabe für die Konsole aktivieren
pd.set_option('display.max_columns', None)
pd.set_option('display.width', 1000)
def ensure_pdf(file_path: str) -> str:
"""Konvertiert HTML oder DOCX in PDF, falls notwendig."""
ext = os.path.splitext(file_path)[1].lower()
if ext == ".pdf":
return file_path
base_name = os.path.splitext(file_path)[0]
output_pdf = f"{base_name}.pdf"
print(f"Konvertiere {os.path.basename(file_path)} zu PDF...")
try:
if ext == ".html" or ext == ".htm":
HTML(file_path).write_pdf(output_pdf)
return output_pdf
elif ext == ".docx" or ext == ".doc":
docx_to_pdf(file_path, output_pdf)
return output_pdf
elif ext == ".txt":
# Einfaches Text-zu-PDF Fallback via WeasyPrint
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
html_content = f"<html><body style='font-family: sans-serif; white-space: pre-wrap;'>{content}</body></html>"
HTML(string=html_content).write_pdf(output_pdf)
return output_pdf
except Exception as e:
print(f"Fehler bei Konvertierung von {file_path}: {e}", file=sys.stderr)
return file_path
def extract_text_from_pdf(pdf_path: str) -> str:
"""Extrahiert den Rohtext aus einer PDF-Datei."""
text = ""
try:
with fitz.open(pdf_path) as doc:
for page in doc:
text += page.get_text()
except Exception as e:
print(f"Fehler beim Lesen der PDF {pdf_path}: {e}", file=sys.stderr)
return text
def get_clean_lemmas(text: str, nlp) -> list:
"""Verarbeitet Text mit de_core_news_lg, entfernt Stopwörter/Interpunktion und liefert Lemmata."""
doc = nlp(text)
return [
token.lemma_.lower() for token in doc
if not token.is_punct and not token.is_stop and not token.is_space and len(token.lemma_.strip()) > 1
]
def calculate_shared_words_matrix(file_names: list, lemma_lists: list) -> pd.DataFrame:
"""Berechnet die Matrix der gemeinsamen Wörter inklusive Prozentanteil (absteigend sortiert nach Relevanz)."""
n = len(file_names)
matrix = []
for i in range(n):
row = []
set_i = set(lemma_lists[i])
total_i = len(set_i)
for j in range(n):
if i == j:
row.append("100.0%")
elif total_i == 0:
row.append("0.0%")
else:
set_j = set(lemma_lists[j])
intersection = set_i.intersection(set_j)
# Prozentualer Anteil der gemeinsamen Wörter bezogen auf Dokument i
percentage = (len(intersection) / total_i) * 100
row.append(f"{percentage:.1f}%")
matrix.append(row)
return pd.DataFrame(matrix, columns=file_names, index=file_names)
def main():
parser = argparse.ArgumentParser(
description="Konsolen-Utility zur Dokumenten-Analyse mittels spaCy de_core_news_lg und BERT.")
parser.add_argument("files", nargs="+", help="Pfade zu den zu analysierenden Dokumenten.")
parser.add_argument("-o", "--output",
help="Zielverzeichnis für den CSV-Export. Wenn weggelassen, erfolgt Konsolenausgabe.",
default=None)
args = parser.parse_args()
if len(args.files) < 2:
print("Fehler: Bitte mindestens 2 Dokumente für den Vergleich angeben.", file=sys.stderr)
sys.exit(1)
print("Lade spaCy Modell 'de_core_news_lg'...")
try:
nlp = spacy.load("de_core_news_lg")
except OSError:
print("Fehler: Das Modell 'de_core_news_lg' ist nicht installiert.", file=sys.stderr)
print("Bitte ausführen: python -m spacy download de_core_news_lg", file=sys.stderr)
sys.exit(1)
# 1. Alle Dokumente zu PDF konvertieren und Texte extrahieren
pdf_paths = []
raw_texts = []
file_basenames = []
for f_path in args.files:
if not os.path.exists(f_path):
print(f"Warnung: Datei nicht gefunden: {f_path}", file=sys.stderr)
continue
pdf_p = ensure_pdf(f_path)
pdf_paths.append(pdf_p)
txt = extract_text_from_pdf(pdf_p)
raw_texts.append(txt if txt.strip() else "Leeres Dokument")
file_basenames.append(os.path.basename(f_path))
if len(raw_texts) < 2:
print("Fehler: Nicht genügend gültige Dokumente zur Analyse vorhanden.", file=sys.stderr)
sys.exit(1)
# 2. NLP Lemmatisierung für TF-IDF und gemeinsame Wörter
print("Analysiere Text-Lemmata...")
processed_lemmas_lists = [get_clean_lemmas(text, nlp) for text in raw_texts]
joined_cleaned_texts = [" ".join(lemmas) for lemmas in processed_lemmas_lists]
# Matrix A: TF-IDF Kosinus-Ähnlichkeit
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(joined_cleaned_texts)
tfidf_sim = cosine_similarity(tfidf_matrix)
df_tfidf = pd.DataFrame(tfidf_sim, columns=file_basenames, index=file_basenames)
# Matrix B: BERT Semantischer Sinnvergleich
print("Berechne semantische BERT-Einbettungen (auf CPU)...")
bert_model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2', device="cpu")
embeddings = bert_model.encode(raw_texts, convert_to_tensor=True, device="cpu")
bert_sim = util.cos_sim(embeddings, embeddings).cpu().numpy()
df_bert = pd.DataFrame(bert_sim, columns=file_basenames, index=file_basenames)
# Matrix C: Gemeinsame Wörter Matrix mit Prozenten
df_shared = calculate_shared_words_matrix(file_basenames, processed_lemmas_lists)
# 3. Ausgabe-Logik (Konsole vs. CSV)
if args.output is None:
print("\n" + "=" * 60)
print("1. TF-IDF KOSINUS-ÄHNLICHKEITSMATRIX")
print("=" * 60)
print(df_tfidf.round(4))
print("\n" + "=" * 60)
print("2. KI SEMANTIK-SINNVERGLEICH (BERT MATRIX)")
print("=" * 60)
print(df_bert.round(4))
print("\n" + "=" * 60)
print("3. MATRIX GEMEINSAMER WÖRTER (PROZENTUALER ANTEIL IN REIHE)")
print("=" * 60)
print(df_shared)
# Liste der am häufigsten geteilten Wörter über alle Dokumente hinweg ausgeben
print("\n" + "=" * 60)
print("TOP GEMEINSAME KERNBEGRIFFE (ABSTEIGEND NACH VORKOMMEN):")
print("=" * 60)
all_words = []
for lemmas in processed_lemmas_lists:
all_words.extend(list(set(lemmas))) # Nur einzigartige Wörter pro Dokument zählen
word_counts = {}
for w in all_words:
word_counts[w] = word_counts.get(w, 0) + 1
sorted_words = sorted(word_counts.items(), key=lambda x: x[1], reverse=True)
top_words = [f"{w} ({c}x)" for w, c in sorted_words if c > 1][:25]
if top_words:
print(", ".join(top_words))
else:
print("Keine gemeinsamen Wörter zwischen den Dokumenten gefunden.")
else:
# CSV Export mit aktuellem Zeitstempel generieren
if not os.path.exists(args.output):
os.makedirs(args.output)
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
csv_filename = os.path.join(args.output, f"analysis_{timestamp}.csv")
print(f"Schreibe Ergebnisse in CSV-Datei: {csv_filename}")
with open(csv_filename, mode="w", newline="", encoding="utf-8") as f:
writer = csv.writer(f, delimiter=";")
# Sektion 1: TF-IDF
writer.writerow(["# 1. TF-IDF KOSINUS-ÄHNLICHKEITSMATRIX"])
writer.writerow(["Datei"] + file_basenames)
for idx, row in df_tfidf.iterrows():
writer.writerow([idx] + list(row.round(4)))
writer.writerow([])
# Sektion 2: BERT
writer.writerow(["# 2. KI SEMANTIK-SINNVERGLEICH (BERT MATRIX)"])
writer.writerow(["Datei"] + file_basenames)
for idx, row in df_bert.iterrows():
writer.writerow([idx] + list(row.round(4)))
writer.writerow([])
# Sektion 3: Shared Words
writer.writerow(["# 3. MATRIX GEMEINSAMER WÖRTER"])
writer.writerow(["Datei"] + file_basenames)
for idx, row in df_shared.iterrows():
writer.writerow([idx] + list(row))
print("Export erfolgreich abgeschlossen.")
if __name__ == "__main__":
main()
6. Verwendung
Wir übergeben die Argumente (Dokumente zum Vergleich) nacheinander. Bei Verwendung der Option -o werden die Ergebnisse als CSV-Datei exportiert.
python analyzer.py dokument1.docx dokument2.pdf dokument3.html
python analyzer.py dokument1.docx dokument2.pdf -o ./results