Documents

Documents

Ein Bekannter, der für ein Personal­vermittlungs­unternehmen 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
              
Diese Website verwendet den Browser-Cache für den Offline-Modus und verarbeitet Ihre Daten aus dem Kontaktformular gemäß unserer Datenschutzerklärung.