QAPdfAnalyzer: Automatisierte Dokumentenanalyse mit moderner GUI

Einführung

Das manuelle Durchsuchen von Dokumenten nach Antworten auf spezifische Fragen ist oft mühsam und zeitaufwendig – sei es in einem Forschungsbericht, einem Unternehmenshandbuch oder einem Lehrbuch. Der Python-Code „QAPdfAnalyzer“ automatisiert diesen Prozess, indem er Texte aus verschiedenen Dateiformaten einliest, Fragen verarbeitet und Antworten mithilfe von maschinellem Lernen generiert. Mit einer modernen Benutzeroberfläche (GUI) basierend auf PyQt6 hebt sich dieser Ansatz von einfachen Kommandozeilen-Skripten ab und bietet eine intuitive Bedienung. In diesem Beitrag stelle ich den Code vor, erkläre, was er leistet, wo er eingesetzt werden kann, und gehe auf technische Details sowie mögliche Weiterentwicklungen ein.

Was macht der QAPdfAnalyzer?

Der „QAPdfAnalyzer“ ist ein Werkzeug zur automatischen Frage-Antwort-Analyse von Dokumenten. Seine Kernfunktionen lassen sich wie folgt zusammenfassen:

  1. Dokumentenunterstützung: Er liest Texte aus PDF-, DOC-, DOCX- und TXT-Dateien ein. Mehrere Dateien können gleichzeitig analysiert werden, was die Flexibilität erhöht.
  2. Fragenverarbeitung: Nutzer geben Fragen in einem Textfeld ein – eine pro Zeile. Diese Fragen werden dann auf alle geladenen Dokumente angewendet.
  3. Antwortgenerierung: Mithilfe eines vortrainierten Frage-Antwort-Modells („deepset/gelectra-base-germanquad“) sucht der Code passende Antworten im Text. Antworten mit einem Konfidenzwert über 0.5 werden akzeptiert.
  4. Ergebnisdarstellung: Die Antworten erscheinen in Echtzeit in der GUI, inklusive der Quelle (Dateiname und Seitennummer), einem Konfidenzwert und einem relevanten Textausschnitt.
  5. Berichtserstellung: Ergebnisse werden in drei Formaten gespeichert: HTML (mit modernem Design), CSV und XLSX, sodass sie weiterverarbeitet oder archiviert werden können.

Die GUI, entwickelt mit PyQt6, macht den Prozess zugänglich: Nutzer wählen Dateien aus, geben Fragen ein, bestimmen einen Ausgabeordner und starten die Analyse per Knopfdruck. Die Ergebnisse werden übersichtlich präsentiert, während die Verarbeitung im Hintergrund läuft, um die Benutzeroberfläche reaktionsfähig zu halten.

Technische Grundlagen

Der Code kombiniert mehrere Python-Bibliotheken:

  • PyMuPDF (fitz): Für die Verarbeitung von PDF-Dateien.
  • python-docx: Zum Einlesen von Word-Dokumenten.
  • Transformers: Stellt das maschinelle Lernmodell und den Tokenizer bereit.
  • Pandas: Ermöglicht die Erstellung und Speicherung der Berichte.
  • PyQt6: Liefert die moderne Benutzeroberfläche.
  • re: Für die Textbereinigung, z. B. das Entfernen von Tausendertrennzeichen.

Die Analyse läuft in einem separaten Thread (via QThread), um die GUI nicht zu blockieren. Das Design der Oberfläche nutzt ein helles Farbschema (z. B. #f4f5f7 als Hintergrund, #1a73e8 für Buttons) mit abgerundeten Ecken und Schatten, was einen zeitgemäßen Look erzeugt.

Installation und Nutzung

Die Installation ist unkompliziert, erfordert jedoch einige Abhängigkeiten. Führen Sie in der Kommandozeile aus:

bash

pip install pymupdf transformers pandas openpyxl python-docx torch pyqt6

Speichern Sie den Code in einer Datei (z. B. qa_pdf_analyzer.py) und starten Sie ihn mit:

bash

python qa_pdf_analyzer.py

Die GUI öffnet sich automatisch:

  1. Klicken Sie auf „Dokumente hinzufügen“, um Dateien auszuwählen (PDF, DOC, DOCX, TXT).
  2. Geben Sie Fragen in das Textfeld ein, z. B.:Wie hoch war der Umsatz 2022? Wer ist der Autor des Berichts?
  3. Wählen Sie einen Ausgabeordner (Standard: „output“) oder lassen Sie ihn unverändert.
  4. Klicken Sie auf „Analyse starten“. Die Ergebnisse erscheinen im unteren Bereich, und Berichte werden im gewählten Ordner gespeichert.

Ein Beispiel für eine Ausgabe könnte so aussehen:

Frage: Wie hoch war der Umsatz 2022?
Antwort: 1.500.000 €
Datei: finanzbericht.pdf
Seite: 5
Score: 0.87
Textstelle: ...Der Umsatz im Jahr 2022 betrug 1.500.000 €, ein Anstieg von...

Einsatzmöglichkeiten

Der „QAPdfAnalyzer“ ist vielseitig einsetzbar. Hier sind einige konkrete Szenarien:

  1. Bildung und Lehre: Lehrkräfte könnten Lehrmaterialien oder Skripte analysieren, um schnell Antworten auf Prüfungsfragen zu finden. Studenten könnten ihn nutzen, um Schlüsselinformationen aus Texten zu extrahieren.
  2. Forschung: Wissenschaftler könnten große Mengen an Literatur (z. B. Studien oder Berichte) durchsuchen, um spezifische Daten wie Statistiken oder Methoden zu identifizieren.
  3. Unternehmen: Teams könnten Handbücher, Verträge oder Geschäftsberichte analysieren, um Details wie Umsatzzahlen, Vertragsbedingungen oder Projektpläne zu finden.
  4. Archivierung: Bibliotheken oder Archive könnten historische Dokumente digital durchsuchen, um Inhalte zu katalogisieren.
  5. Persönliche Nutzung: Privatpersonen könnten ihn verwenden, um alte Dokumente wie Briefe oder Notizen zu durchforsten, etwa um wichtige Termine oder Namen zu finden.

Die Möglichkeit, mehrere Dokumente gleichzeitig zu analysieren, macht ihn besonders nützlich für Projekte mit umfangreichem Material.

Stärken und Grenzen

Stärken:

  • Benutzerfreundlichkeit: Die PyQt6-GUI ist intuitiv und optisch ansprechend, was den Einstieg erleichtert.
  • Flexibilität: Unterstützt verschiedene Dateiformate und mehrere Dokumente.
  • Automatisierung: Spart Zeit bei der Suche nach Informationen.
  • Exportoptionen: HTML, CSV und XLSX decken unterschiedliche Bedürfnisse ab.

Grenzen:

  • Modellgenauigkeit: Das verwendete Modell ist auf deutsche Texte optimiert und kann bei mehrdeutigen oder schlecht formulierten Fragen ungenaue Ergebnisse liefern.
  • Performance: Bei sehr großen Dokumenten oder vielen Fragen kann die Analyse Zeit in Anspruch nehmen, da keine Parallelisierung implementiert ist.
  • Eingeschränkte Formate: Unterstützt keine Bilder oder gescannten Dokumente ohne OCR.
  • Abhängigkeit: Erfordert eine stabile Internetverbindung für den initialen Modell-Download und ausreichend Speicher für die Transformer-Bibliothek.

Mögliche Erweiterungen

Der Code bietet Potenzial für Weiterentwicklungen:

  • OCR-Unterstützung: Integration von Tesseract oder ähnlichen Tools, um gescannte Dokumente zu verarbeiten.
  • Modellwechsel: Ein spezialisierteres oder mehrsprachiges Modell könnte die Genauigkeit erhöhen.
  • Parallelisierung: Die Analyse könnte auf mehrere Threads oder Prozesse verteilt werden, um die Geschwindigkeit zu steigern.
  • Filteroptionen: Nutzer könnten Schwellenwerte für den Konfidenzwert anpassen oder nach Dateien filtern.
  • Cloud-Integration: Ergebnisse könnten direkt in eine Cloud wie Google Drive hochgeladen werden.

Praktisches Beispiel

Stellen Sie sich vor, ein Unternehmen hat mehrere Jahresberichte (PDF) und möchte wissen, wie sich der Umsatz über die Jahre entwickelt hat. Der Nutzer lädt die Berichte hoch, gibt Fragen wie „Was war der Umsatz 2020?“, „Was war der Umsatz 2021?“ ein und startet die Analyse. Innerhalb weniger Minuten erhält er eine Liste mit Antworten, inklusive Quellenangaben, und kann die Ergebnisse als Excel-Datei exportieren – ein Prozess, der manuell Stunden dauern könnte.

Fazit

Der „QAPdfAnalyzer“ ist ein leistungsfähiges Werkzeug für alle, die schnell und effizient Informationen aus Dokumenten extrahieren möchten. Die Kombination aus moderner PyQt6-Oberfläche und maschinellem Lernen macht ihn zugänglich und praktisch, sei es für berufliche, akademische oder private Zwecke. Während er in seiner aktuellen Form bereits solide Ergebnisse liefert, bietet er Raum für Anpassungen, um spezifische Anforderungen zu erfüllen. Für Nutzer mit grundlegenden Python-Kenntnissen ist er ein idealer Ausgangspunkt, um Dokumentenanalyse zu automatisieren und den Arbeitsalltag zu erleichtern.

Quellcode:

import fitz  # PyMuPDF
from transformers import pipeline, AutoModelForQuestionAnswering, AutoTokenizer
import re
import pandas as pd
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
                             QPushButton, QFileDialog, QListWidget, QTextEdit, QLabel, 
                             QMessageBox, QLineEdit)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import QFont
import sys
import docx
import os
from datetime import datetime

class AnalysisWorker(QThread):
    update_signal = pyqtSignal(str)
    finished_signal = pyqtSignal(list)

    def __init__(self, analyzer, files, questions, output_folder):
        super().__init__()
        self.analyzer = analyzer
        self.files = files
        self.questions = questions
        self.output_folder = output_folder

    def run(self):
        self.update_signal.emit("Analyse läuft...\n")
        answers = self.analyzer.analyze(self.files, self.questions)
        self.analyzer.generate_reports(answers, self.output_folder)
        self.finished_signal.emit(answers)

class QAPdfAnalyzer:
    def __init__(self, model_name: str = "deepset/gelectra-base-germanquad"):
        self.model = AutoModelForQuestionAnswering.from_pretrained(model_name)
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.nlp = pipeline("question-answering", model=self.model, tokenizer=self.tokenizer)

    def read_text_from_file(self, file_path: str) -> list[tuple[str, int]]:
        ext = os.path.splitext(file_path)[1].lower()
        pages = []
        if ext == '.pdf':
            with fitz.open(file_path) as doc:
                pages = [(page.get_text(), i + 1) for i, page in enumerate(doc)]
        elif ext in ['.doc', '.docx']:
            doc = docx.Document(file_path)
            text = "\n".join([para.text for para in doc.paragraphs])
            pages = [(text, 1)]
        elif ext == '.txt':
            with open(file_path, 'r', encoding='utf-8') as file:
                pages = [(file.read(), 1)]
        else:
            raise ValueError(f"Unsupported file format: {ext}")
        return pages

    def clean_number_format(self, text: str) -> str:
        return re.sub(r'(\d)(?=(\d{3})+(\.|\b))', r'\1', text)

    def find_relevant_text(self, context: str, answer_start: int, answer_end: int) -> str:
        start = max(0, answer_start - 100)
        end = min(len(context), answer_end + 100)
        return context[start:end]

    def analyze(self, files: list[str], questions: list[str]) -> list[tuple]:
        answers = []
        for file_path in files:
            contexts = self.read_text_from_file(file_path)
            for question in questions:
                for context, page_num in contexts:
                    cleaned_context = self.clean_number_format(context)
                    result = self.nlp(question=question, context=cleaned_context, max_answer_len=100)
                    if result['score'] > 0.5:
                        relevant_text = self.find_relevant_text(cleaned_context, result['start'], result['end'])
                        answers.append((question, result['answer'], os.path.basename(file_path), page_num, result['score'], relevant_text))
        return answers

    def generate_reports(self, answers: list[tuple], output_folder: str) -> None:
        if not answers:
            return
        df = pd.DataFrame(answers, columns=["Frage", "Antwort", "Datei", "Seite", "Score", "Textstelle"])
        os.makedirs(output_folder, exist_ok=True)

        html_content = df.to_html(index=False, escape=False, 
                                formatters={'Textstelle': lambda x: x.replace('\n', '<br>')})
        html_template = f"""<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <title>Frage-Antwort-Bericht</title>
    <style>
        body {{ font-family: 'Segoe UI', sans-serif; margin: 20px; background-color: #f4f5f7; }}
        h1 {{ color: #1a73e8; }}
        table {{ width: 100%; border-collapse: collapse; margin-top: 20px; background: white; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }}
        th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #e0e0e0; }}
        th {{ background-color: #1a73e8; color: white; }}
        tr:hover {{ background-color: #f1f3f4; }}
    </style>
</head>
<body>
    <h1>Frage-Antwort-Bericht</h1>
    <p>Erstellt am: {datetime.now().strftime("%d.%m.%Y %H:%M:%S")}</p>
    {html_content}
</body>
</html>"""
        with open(os.path.join(output_folder, "report.html"), 'w', encoding='utf-8') as f:
            f.write(html_template)
        df.to_csv(os.path.join(output_folder, "report.csv"), index=False, encoding='utf-8')
        df.to_excel(os.path.join(output_folder, "report.xlsx"), index=False, engine='openpyxl')

class QAGui(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QA Document Analyzer")
        self.setGeometry(100, 100, 800, 600)
        self.analyzer = QAPdfAnalyzer()
        self.init_ui()

    def init_ui(self):
        widget = QWidget()
        self.setCentralWidget(widget)
        layout = QVBoxLayout()
        widget.setLayout(layout)

        # Stil
        self.setStyleSheet("""
            QWidget { background-color: #f4f5f7; font-family: 'Segoe UI', sans-serif; }
            QPushButton { background-color: #1a73e8; color: white; padding: 8px; border: none; border-radius: 4px; }
            QPushButton:hover { background-color: #1557b0; }
            QLineEdit { padding: 6px; border: 1px solid #dadce0; border-radius: 4px; }
            QListWidget { border: 1px solid #dadce0; border-radius: 4px; padding: 5px; }
            QTextEdit { border: 1px solid #dadce0; border-radius: 4px; padding: 5px; }
            QLabel { font-size: 14px; color: #202124; }
        """)

        # Dateiauswahl
        layout.addWidget(QLabel("Dokumente auswählen:"))
        self.file_list = QListWidget()
        layout.addWidget(self.file_list)
        add_file_btn = QPushButton("Dokumente hinzufügen")
        add_file_btn.clicked.connect(self.add_files)
        layout.addWidget(add_file_btn)

        # Fragenfeld
        layout.addWidget(QLabel("Fragen eingeben (eine pro Zeile):"))
        self.questions_input = QTextEdit()
        layout.addWidget(self.questions_input)

        # Ausgabeordner
        output_layout = QHBoxLayout()
        output_layout.addWidget(QLabel("Ausgabeordner:"))
        self.output_folder = QLineEdit("output")
        output_layout.addWidget(self.output_folder)
        choose_folder_btn = QPushButton("Ordner wählen")
        choose_folder_btn.clicked.connect(self.choose_output_folder)
        output_layout.addWidget(choose_folder_btn)
        layout.addLayout(output_layout)

        # Analyse-Button
        analyze_btn = QPushButton("Analyse starten")
        analyze_btn.clicked.connect(self.start_analysis)
        analyze_btn.setFont(QFont("Segoe UI", 12, QFont.Weight.Bold))
        layout.addWidget(analyze_btn)

        # Ergebnisanzeige
        layout.addWidget(QLabel("Ergebnisse:"))
        self.result_output = QTextEdit()
        self.result_output.setReadOnly(True)
        layout.addWidget(self.result_output)

    def add_files(self):
        files, _ = QFileDialog.getOpenFileNames(self, "Dokumente auswählen", "", 
                                               "Supported Files (*.pdf *.doc *.docx *.txt)")
        for file in files:
            self.file_list.addItem(file)

    def choose_output_folder(self):
        folder = QFileDialog.getExistingDirectory(self, "Ausgabeordner wählen")
        if folder:
            self.output_folder.setText(folder)

    def start_analysis(self):
        files = [self.file_list.item(i).text() for i in range(self.file_list.count())]
        questions = [q.strip() for q in self.questions_input.toPlainText().splitlines() if q.strip()]
        output_folder = self.output_folder.text()

        if not files or not questions:
            QMessageBox.warning(self, "Eingabe fehlt", "Bitte wählen Sie Dateien und geben Sie Fragen ein.")
            return

        self.result_output.clear()
        self.worker = AnalysisWorker(self.analyzer, files, questions, output_folder)
        self.worker.update_signal.connect(self.update_result)
        self.worker.finished_signal.connect(self.analysis_finished)
        self.worker.start()

    def update_result(self, text):
        self.result_output.append(text)

    def analysis_finished(self, answers):
        questions = set(a[0] for a in answers)
        for question in [q.strip() for q in self.questions_input.toPlainText().splitlines() if q.strip()]:
            self.result_output.append(f"Frage: {question}")
            answers_found = [a[1:] for a in answers if a[0] == question]
            if answers_found:
                for answer, file, page, score, text in answers_found:
                    self.result_output.append(f"Antwort: {answer}\nDatei: {file}\nSeite: {page}\nScore: {score:.2f}\nTextstelle: {text}\n")
            else:
                self.result_output.append("Antwort: Keine Antwort gefunden\n")
        self.result_output.append(f"Berichte wurden in {self.output_folder.text()} gespeichert.")
        QMessageBox.information(self, "Fertig", "Analyse abgeschlossen!")

def main():
    app = QApplication(sys.argv)
    window = QAGui()
    window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert