From 552fb3e372eaeb903e1614ff0dab8a7656363881 Mon Sep 17 00:00:00 2001 From: Floke Date: Wed, 23 Apr 2025 12:27:01 +0000 Subject: [PATCH] bugfix --- brancheneinstufung.py | 637 +++++++++++++++++++----------------------- 1 file changed, 287 insertions(+), 350 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index a62b6b23..809e2717 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -4383,97 +4383,95 @@ class DataProcessor: # --- Methode für den Re-Eval Modus --- # Diese Methode gehört in die Klasse - def process_reevaluation_rows(self, row_limit=None, clear_flag=True): + def process_reevaluation_rows(self, row_limit=None, clear_flag=True, + # NEUE PARAMETER hinzugefügt: + process_wiki_steps=True, + process_chatgpt_steps=True, + process_website_steps=True): """ Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind. Ruft _process_single_row für jede dieser Zeilen auf mit force_reeval=True. Verarbeitet maximal row_limit Zeilen. Löscht optional das 'x'-Flag nach erfolgreicher Verarbeitung. + Erlaubt die Auswahl spezifischer Verarbeitungsschritte. Args: row_limit (int, optional): Maximale Anzahl zu verarbeitender Zeilen. Defaults to None. clear_flag (bool, optional): Flag 'x' nach erfolgreicher Verarbeitung löschen. Defaults to True. + process_wiki_steps (bool, optional): Soll der Wiki-Schritt in _process_single_row ausgeführt werden?. Defaults to True. + process_chatgpt_steps (bool, optional): Sollen ChatGPT-Schritte in _process_single_row ausgeführt werden?. Defaults to True. + process_website_steps (bool, optional): Soll der Website-Schritt in _process_single_row ausgeführt werden?. Defaults to True. + # Fügen Sie hier ggf. weitere Parameter hinzu, wenn Sie granularere Schritte in _process_single_row haben. """ logging.info(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}") + # Logge, welche Schritte für Re-Eval ausgewählt wurden + selected_steps_log = [] + if process_wiki_steps: selected_steps_log.append("Wiki") + if process_chatgpt_steps: selected_steps_log.append("ChatGPT") + if process_website_steps: selected_steps_log.append("Website") + logging.info(f"Ausgewählte Schritte für Re-Eval: {', '.join(selected_steps_log) if selected_steps_log else 'Keine ausgewählt!'} (force_reeval=True)") + # Daten neu laden vor der Verarbeitung - if not self.sheet_handler.load_data(): - logging.error("Fehler beim Laden der Daten für Re-Evaluation.") - return - + # ... (Code zum Laden der Daten, Finden der x-markierten Zeilen wie gehabt) ... + if not self.sheet_handler.load_data(): return logging.error("Fehler beim Laden der Daten für Re-Evaluation.") all_data = self.sheet_handler.get_all_data_with_headers() - if not all_data or len(all_data) <= 5: - logging.warning("Keine Daten für Re-Evaluation gefunden.") - return header_rows = 5 - # data_rows wird hier nicht direkt benötigt, wir nutzen all_data und den sheet index - - # Annahme: COLUMN_MAP ist global verfügbar + if not all_data or len(all_data) <= header_rows: return logging.warning("Keine Daten für Re-Evaluation gefunden.") reeval_col_idx = COLUMN_MAP.get("ReEval Flag") - if reeval_col_idx is None: - logging.error("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden.") - return + if reeval_col_idx is None: return logging.critical("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden.") rows_to_process = [] - # Iteriere über alle Datenzeilen, um 'x' zu finden for idx_in_list in range(header_rows, len(all_data)): row_data = all_data[idx_in_list] - row_num_in_sheet = idx_in_list + 1 # 1-basierte Zeilennummer + row_num_in_sheet = idx_in_list + 1 if len(row_data) > reeval_col_idx and str(row_data[reeval_col_idx]).strip().lower() == "x": rows_to_process.append({'row_num': row_num_in_sheet, 'data': row_data}) found_count = len(rows_to_process) logging.info(f"{found_count} Zeilen mit ReEval-Flag 'x' gefunden.") + if found_count == 0: return logging.info("Keine Zeilen zur Re-Evaluation markiert.") - if found_count == 0: - logging.info("Keine Zeilen zur Re-Evaluation markiert.") - return processed_count = 0 updates_clear_flag = [] - rows_actually_processed = [] # Liste der Zeilennummern, die wirklich verarbeitet wurden + rows_actually_processed = [] for task in rows_to_process: - # Limit-Prüfung VOR der Verarbeitung if row_limit is not None and processed_count >= row_limit: logging.info(f"Zeilenlimit ({row_limit}) für Re-Evaluation erreicht. Breche weitere Verarbeitung ab.") break row_num = task['row_num'] - row_data = task['data'] # Verwende die direkt geladenen Daten für die Zeile + row_data = task['data'] try: - # Rufe _process_single_row mit force_reeval=True auf - self._process_single_row(row_num, row_data, - process_wiki=True, process_chatgpt=True, process_website=True, - force_reeval=True) # WICHTIG: Erzwingt Verarbeitung aller Schritte! + # RUFE _process_single_row MIT DEN NEUEN PARAMETERN AUF: + self._process_single_row( + row_num_in_sheet = row_num, + row_data = row_data, + process_wiki = process_wiki_steps, # <<< ÜBERGIBT DIE STEUERUNG + process_chatgpt = process_chatgpt_steps, # <<< ÜBERGIBT DIE STEUERUNG + process_website = process_website_steps, # <<< ÜBERGIBT DIE STEUERUNG + force_reeval = True # <<< BLEIBT HIER TRUE FÜR RE-EVAL MODUS + ) processed_count += 1 - rows_actually_processed.append(row_num) # Füge Zeilennummer hinzu + rows_actually_processed.append(row_num) - # Vorbereiten des Updates zum Löschen des 'x'-Flags + # Vorbereiten des Updates zum Löschen des 'x'-Flags (wie gehabt) if clear_flag: flag_col_letter = self.sheet_handler._get_col_letter(reeval_col_idx + 1) - if flag_col_letter: # Prüfe, ob der Spaltenbuchstabe ermittelt werden konnte - updates_clear_flag.append({'range': f'{flag_col_letter}{row_num}', 'values': [['']]}) - else: - logging.error(f"Fehler: Konnte Spaltenbuchstaben für 'ReEval Flag' nicht ermitteln.") - + if flag_col_letter: updates_clear_flag.append({'range': f'{flag_col_letter}{row_num}', 'values': [['']]}) + else: logging.error(f"Fehler: Konnte Spaltenbuchstaben für 'ReEval Flag' ({reeval_col_idx+1}) nicht ermitteln.") except Exception as e_proc: - # Logge den spezifischen Fehler für diese Zeile logging.exception(f"FEHLER bei Re-Evaluation von Zeile {row_num}: {e_proc}") - # Das 'x'-Flag wird in diesem Fall NICHT gelöscht, damit die Zeile erneut versucht werden kann. - # Lösche Flags am Ende in einem Batch-Update + # Lösche Flags am Ende (wie gehabt) if clear_flag and updates_clear_flag: logging.info(f"Lösche ReEval-Flags für {len(updates_clear_flag)} erfolgreich verarbeitete Zeilen ({rows_actually_processed})...") - # Annahme: sheet_handler.batch_update_cells existiert und nutzt logging/retry success = self.sheet_handler.batch_update_cells(updates_clear_flag) - if success: - logging.info("ReEval-Flags erfolgreich gelöscht.") - else: - # Fehlermeldung wird von batch_update_cells geloggt - logging.error("FEHLER beim Löschen der ReEval-Flags.") - + if success: logging.info("ReEval-Flags erfolgreich gelöscht.") + else: logging.error("FEHLER beim Löschen der ReEval-Flags.") logging.info(f"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Limit war: {row_limit}, Gefunden: {found_count}).") @@ -5057,18 +5055,21 @@ class DataProcessor: # ==================== MAIN FUNCTION ==================== # ==================== MAIN FUNCTION ==================== +# Diese Funktion ist der Haupteinstiegspunkt des Skripts. + def main(): # WICHTIG: Global LOG_FILE wird benötigt, aber erst nach Arg-Parsing gesetzt. global LOG_FILE - # --- Logging Setup (Konfiguration von Level und Format) --- + # --- Initial Logging Setup (Konfiguration von Level und Format) --- # Diese Konfiguration wird wirksam, sobald die Handler hinzugefügt werden. import logging log_level = logging.DEBUG # Explizit DEBUG setzen für detaillierte Logs log_format = '%(asctime)s - %(levelname)-8s - %(name)-15s - %(message)s' # Angepasstes Format # Root-Logger konfigurieren (noch ohne File Handler) - logging.basicConfig(level=log_level, format=log_format, handlers=[]) # WICHTIG: handlers=[] verhindert default Console Handler + # handlers=[] verhindert default Console Handler, wir fügen ihn manuell hinzu + logging.basicConfig(level=log_level, format=log_format, handlers=[]) # Console Handler explizit hinzufügen console_handler = logging.StreamHandler() @@ -5082,96 +5083,53 @@ def main(): logging.info("INFO Logging initial konfiguriert (nur Konsole).") # --- Initialisierung (Argument Parser etc.) --- - parser = argparse.ArgumentParser(description="Firmen-Datenanreicherungs-Skript v1.6.5") # Version aktualisiert - valid_modes = ["combined", "wiki", "website", "branch", "summarize", "reeval", - "website_lookup", "website_details", "contacts", "full_run", - "alignment", "train_technician_model", "update_wiki", - "find_wiki_serp"] # <-- NEUER MODUS HINZUGEFÜGT + # Version hier (sollte mit Config.VERSION übereinstimmen) + current_script_version = "v1.6.6" # <-- ANPASSEN, wenn Config.VERSION geändert wird + + parser = argparse.ArgumentParser(description=f"Firmen-Datenanreicherungs-Skript {current_script_version}") + # Liste der gültigen Modi (basierend auf Ihrer aktuellen v1.6.6 + dem neuen Modus) + valid_modes = [ + "combined", "wiki", "website", "branch", "summarize", "reeval", + "website_lookup", "website_details", "contacts", "full_run", + "alignment", "train_technician_model", "update_wiki", + "find_wiki_serp", "wiki_reextract" # <<< NEUER MODUS HIER HINZUGEFÜGT + ] + # Stellen Sie sicher, dass diese Liste mit denelif-Zweigen unten übereinstimmt. + parser.add_argument("--mode", type=str, help=f"Betriebsmodus ({', '.join(valid_modes)})") parser.add_argument("--limit", type=int, help="Maximale Anzahl zu verarbeitender Zeilen", default=None) - parser.add_argument("--start_row", type=int, help="Startzeile im Sheet (1-basiert) für sequenzielle Modi (full_run)", default=None) # Optionaler Startpunkt für full_run + # start_row wird primär für full_run verwendet, kann aber generell hilfreich sein + parser.add_argument("--start_row", type=int, help="Startzeile im Sheet (1-basiert) für sequenzielle Modi", default=None) + + # NEUES ARGUMENT für den Re-Eval Modus zur Auswahl der Schritte + # Standard ist "wiki,chat,web", um das bisherige Verhalten zu imitieren + # Mögliche Werte für die Schritte: 'wiki', 'chat', 'web' (entsprechend den Parametern in _process_single_row) + parser.add_argument("--steps", type=str, help="Komma-getrennte Liste der Schritte im 'reeval' Modus (z.B. 'wiki,chat,web'). Mögliche Schritte: wiki, chat, web.", default="wiki,chat,web") + + # Argumente für find_wiki_serp (falls über CLI gesteuert) + parser.add_argument("--min_umsatz", type=int, help="Mindestumsatz in Mio € für find_wiki_serp", default=200) + parser.add_argument("--min_employees", type=int, help="Mindestmitarbeiterzahl für find_wiki_serp", default=500) + + # Argumente für train_technician_model parser.add_argument("--model_out", type=str, default=MODEL_FILE, help=f"Pfad für Modell (.pkl)") parser.add_argument("--imputer_out", type=str, default=IMPUTER_FILE, help=f"Pfad für Imputer (.pkl)") parser.add_argument("--patterns_out", type=str, default=PATTERNS_FILE_TXT, help=f"Pfad für Regeln (.txt)") + + # TODO: Fügen Sie hier weitere CLI-Argumente hinzu, falls andere Modi Parameter benötigen (z.B. für Kriterien-Modus) + args = parser.parse_args() + # Lade API Keys direkt am Anfang - Config.load_api_keys() # Nutzt jetzt logging.debug/info/warning intern - - # Betriebsmodus ermitteln - mode = None - if args.mode and args.mode.lower() in valid_modes: - mode = args.mode.lower() - # Logge erst NACHDEM FileHandler konfiguriert ist - # logging.info(f"Betriebsmodus (aus Kommandozeile): {mode}") # Wird später geloggt - print(f"Betriebsmodus (aus Kommandozeile): {mode}") # Frühes Feedback für User - else: # Interaktive Abfrage - print("\nBitte wählen Sie den Betriebsmodus:") - print(" combined: Wiki(AX), Website(AR), Summarize(AS), Branch(AO) (Batch, Start bei leerem AO)") - print(" wiki: Nur Wikipedia-Verifizierung (AX) (Batch, Start bei leerem AX)") - print(" website: Nur Website-Scraping Rohtext (AR) (Batch, Start bei leerem AR)") - print(" summarize: Nur Website-Zusammenfassung (AS) (Batch, Start bei leerem AS)") - print(" branch: Nur Branchen-Einschätzung (AO) (Batch, Start bei leerem AO)") - print(" update_wiki: Wiki-URL aus Spalte U nach M übernehmen & ReEval-Flag setzen") - print(" reeval: Verarbeitet Zeilen mit 'x' in A (volle Verarbeitung)") - print(" find_wiki_serp: Sucht fehlende Wiki-URLs (M=k.A.) für große Firmen (>500 MA) via SerpAPI") # Neuer Modus erklärt - print(" website_lookup: Sucht fehlende Websites (D) via SerpAPI") - # print(" website_details:Extrahiert Details für Zeilen mit 'x' (AR) - EXPERIMENTELL") # Ggf. ausblenden - print(" contacts: Sucht LinkedIn Kontakte (AM)") - print(" full_run: Verarbeitet sequenziell ab erster Zeile ohne AO (alle TS prüfen)") - print(" alignment: Schreibt Header A1:AX5 (!)") - print(" train_technician_model: Trainiert Decision Tree zur Technikerschätzung") - - try: - mode_input = input(f"Geben Sie den Modus ein ({', '.join(valid_modes)}): ").strip().lower() - if mode_input in valid_modes: - mode = mode_input - else: - print("Ungültige Eingabe -> Standard: combined") - mode = "combined" - except Exception as e: - print(f"Fehler Modus-Eingabe ({e}) -> Standard: combined") - mode = "combined" - # logging.info(f"Betriebsmodus (interaktiv gewählt): {mode}") # Wird später geloggt - - # Zeilenlimit ermitteln - row_limit = None - if args.limit is not None: - if args.limit >= 0: - row_limit = args.limit - # logging.info(f"Zeilenlimit (aus Kommandozeile): {row_limit}") # Wird später geloggt - print(f"Zeilenlimit (aus Kommandozeile): {row_limit}") # Frühes Feedback - else: - print("Warnung: Negatives Limit ignoriert.") - # logging.warning("Warnung: Negatives Limit ignoriert.") # Wird später geloggt - row_limit = None - # Frage nur bei Modi, wo es sinnvoll ist (inkl. neuer Modus) - elif mode in ["combined", "wiki", "website", "branch", "summarize", "full_run", "reeval", "update_wiki", "find_wiki_serp"]: - try: - limit_input = input(f"Maximale Anzahl Zeilen für Modus '{mode}'? (Enter=alle): ") - if limit_input.strip(): - try: - limit_val = int(limit_input) - if limit_val >= 0: - row_limit = limit_val - print(f"Zeilenlimit (interaktiv): {row_limit}") - else: - print("Negatives Limit -> Kein Limit") - row_limit = None - except ValueError: - print("Ungültige Zahl -> Kein Limit") - row_limit = None - else: - print("Kein Zeilenlimit angegeben.") - row_limit = None - except Exception as e: - print(f"Fehler Limit-Eingabe ({e}) -> Kein Limit") - row_limit = None - # Logging der Limit-Info erfolgt nach FileHandler-Setup + Config.load_api_keys() # Nutzt jetzt logging intern # --- Logdatei-Konfiguration abschließen --- - # Annahme: Funktion existiert und gibt Pfad zurück - LOG_FILE = create_log_filename(mode) + # Bestimmen Sie den Log-Modus Namen basierend auf CLI oder Interaktion + # Wir nutzen den CLI Modus Namen, wenn er gesetzt ist, sonst einen Platzhalter. + # Der tatsächliche Modus wird unten ermittelt und geloggt. + log_mode_name = args.mode if args.mode else "interactive" + LOG_FILE = create_log_filename(log_mode_name) # Annahme: create_log_filename ist global + try: file_handler = logging.FileHandler(LOG_FILE, mode='a', encoding='utf-8') file_handler.setLevel(log_level) # Nimm das globale Level @@ -5185,233 +5143,230 @@ def main(): logging.getLogger('').handlers = [h for h in logging.getLogger('').handlers if not isinstance(h, logging.FileHandler)] # Entferne evtl. defekten Handler logging.error(f"Konnte FileHandler für Logdatei '{LOG_FILE}' nicht erstellen: {e}") + # --- JETZT die Startmeldungen loggen (gehen jetzt in Konsole UND Datei) --- logging.info(f"===== Skript gestartet =====") - logging.info(f"Version: {Config.VERSION}") # Sollte jetzt v1.6.5 sein - logging.info(f"Betriebsmodus: {mode}") - limit_log_text = str(row_limit) if row_limit is not None else 'N/A für diesen Modus' - if mode in ["combined", "wiki", "website", "branch", "summarize", "full_run", "reeval", "update_wiki", "find_wiki_serp"]: - limit_log_text = str(row_limit) if row_limit is not None else 'Unbegrenzt' - if row_limit == 0: limit_log_text = '0 (Keine Verarbeitung geplant)' - logging.info(f"Zeilenlimit: {limit_log_text}") + logging.info(f"Version: {Config.VERSION}") # Sollte jetzt v1.6.6 sein + # Der Modus wird später vom Dispatcher geloggt logging.info(f"Logdatei: {LOG_FILE}") - # --- Ende finale Startinfos --- + # Loggen Sie auch die Re-Eval Schritte, wenn das Argument gesetzt ist (unabhängig vom gewählten Modus, zur Info) + if 'steps' in args and args.steps: + logging.info(f"CLI Argument --steps: '{args.steps}' (relevant für 'reeval' Modus)") + if 'min_umsatz' in args: logging.info(f"CLI Argument --min_umsatz: {args.min_umsatz}") + if 'min_employees' in args: logging.info(f"CLI Argument --min_employees: {args.min_employees}") + if 'model_out' in args: logging.info(f"CLI Argument --model_out: '{args.model_out}'") + # ... loggen Sie weitere relevante CLI Argumente + # --- Vorbereitung (Schema, Sheet Handler etc.) --- - # Annahme: Diese Funktionen verwenden jetzt logging intern - load_target_schema() + load_target_schema() # Annahme: load_target_schema ist global definiert + try: - sheet_handler = GoogleSheetHandler() + sheet_handler = GoogleSheetHandler() # Annahme: GoogleSheetHandler ist global definiert except Exception as e: logging.critical(f"FATAL: Initialisierung des GoogleSheetHandlers fehlgeschlagen: {e}") logging.critical(f"Bitte Logdatei prüfen: {LOG_FILE}") return # Beende Skript, wenn Sheet nicht geladen werden kann - # Annahme: DataProcessor verwendet jetzt logging intern - data_processor = DataProcessor(sheet_handler) - # --- Modusausführung --- + try: + # Initialisiere WikipediaScraper hier, da er an DataProcessor übergeben werden muss + wiki_scraper = WikipediaScraper() # Annahme: WikipediaScraper ist global definiert und benötigt keine Parameter oder nutzt Config + except Exception as e: + logging.critical(f"FATAL: Initialisierung des WikipediaScrapers fehlgeschlagen: {e}") + logging.critical(f"Bitte Logdatei prüfen: {LOG_FILE}") + # Das Skript kann ohne Wiki Scraper nicht sinnvoll laufen + return + + # Initialisiere DataProcessor Instanz mit Handlern + # PASSEN SIE DIESEN AUFRUF AN DIE TATSÄCHLICHE __init__ SIGNATUR IHRER DataProcessor Klasse an + # In v1.6.6 nahm sie nur sheet_handler entgegen. Für den Refactoring-Plan soll sie wiki_scraper auch nehmen. + # Für diese Übergangsversion halten wir uns an die v1.6.6 Signatur (nur sheet_handler) + # ABER: Methoden IN DataProcessor (wie _process_single_row) brauchen den wiki_scraper! + # Das bedeutet, wiki_scraper muss in __init__ übergeben und als self.wiki_scraper gespeichert werden. + # KORRIGIEREN SIE DataProcessor.__init__ ZU: def __init__(self, sheet_handler, wiki_scraper): + data_processor = DataProcessor(sheet_handler, wiki_scraper) # <<< KORRIGIERTER AUFRUF + + # --- Modusauswahl und Ausführung --- start_time = time.time() logging.info(f"Starte Verarbeitung um {datetime.now().strftime('%H:%M:%S')}...") - try: - # Batch-Modi über Dispatcher - if mode in ["wiki", "website", "branch", "summarize", "combined"]: - if row_limit == 0: - logging.info("Limit 0 angegeben -> Überspringe Dispatcher für Batch-Modus.") - else: - # Annahme: run_dispatcher verwendet logging intern - run_dispatcher(mode, sheet_handler, row_limit) - # Einzelne Zeilen Modi (kein Batch-Dispatcher) - elif mode == "reeval": - # Annahme: process_reevaluation_rows verwendet logging intern - data_processor.process_reevaluation_rows(row_limit=row_limit) + mode = None # Wird aus CLI oder Interaktion ermittelt + + # --- Ermitteln des zu führenden Modus (CLI hat Priorität) --- + if args.mode: + mode = args.mode.lower() + if mode not in valid_modes: + logging.error(f"Ungültiger Modus '{args.mode}' über Kommandozeile angegeben. Gültige Modi: {', '.join(valid_modes)}") + print(f"Fehler: Ungültiger Modus '{args.mode}'. Siehe --help.") + return # Skript beenden + logging.info(f"Betriebsmodus (CLI gewählt): {mode}") + else: + # --- Interaktive Modusauswahl --- + print("\nBitte wählen Sie den Betriebsmodus:") + # Zeigen Sie die Liste der validen Modi an + for i, m in enumerate(valid_modes): + print(f" {i+1}: {m}") + + while mode is None: # Schleife, bis ein gültiger Modus gewählt wurde + try: + mode_input = input(f"Geben Sie den Modusnamen oder die Zahl ein: ").strip().lower() + try: + mode_index = int(mode_input) + if 1 <= mode_index <= len(valid_modes): mode = valid_modes[mode_index - 1] + else: print("Ungültige Zahl.") + except ValueError: + if mode_input in valid_modes: mode = mode_input + else: print("Ungültige Eingabe.") + + if mode: logging.info(f"Betriebsmodus (interaktiv gewählt): {mode}") + # Wenn mode None bleibt, Schleife läuft weiter + + except Exception as e: + logging.error(f"Fehler bei interaktiver Modus-Eingabe: {e}"); return # Skript beenden + print(f"Fehler Modus-Eingabe ({e}).") + + + # --- Ausführung des gewählten Modus --- + try: + # Rufen Sie die entsprechenden Funktionen/Methoden auf basierend auf dem gewählten 'mode' + # Die Aufrufe hier werden auf die 'data_processor' Instanz umgestellt, + # da die Funktionen jetzt Methoden dieser Klasse sind (oder es sein sollten). + + if mode == "combined": + # Der combined Mode war ein globaler run_dispatcher Aufruf. + # run_dispatcher sollte eine Methode in DataProcessor sein. + data_processor.run_batch_dispatcher(mode="combined", limit=args.limit) # Annahme: run_batch_dispatcher existiert in DataProcessor + + elif mode == "wiki": # Entspricht dem Batch-Modus Wiki Verifizierung (AX) + # process_verification_only sollte jetzt data_processor.process_verification_batch sein + data_processor.process_verification_batch(limit=args.limit) + + elif mode == "website": # Entspricht dem Batch-Modus Website Scraping (AT) + # process_website_batch sollte jetzt data_processor.process_website_batch sein + data_processor.process_website_batch(limit=args.limit) + + elif mode == "summarize": # Entspricht dem Batch-Modus Website Summarization (AS) + # process_website_summarization_batch sollte jetzt data_processor.process_summarization_batch sein + data_processor.process_summarization_batch(limit=args.limit) + + elif mode == "branch": # Entspricht dem Batch-Modus Branchen-Einstufung (AO) + # process_branch_batch sollte jetzt data_processor.process_branch_batch sein + data_processor.process_branch_batch(limit=args.limit) + + elif mode == "reeval": # process_reevaluation_rows + if args.limit is not None and args.limit <= 0: + logging.info(f"Limit {args.limit} angegeben im Re-Eval Modus. Überspringe Verarbeitung.") + else: + # Parse das neue --steps Argument + steps_list = [step.strip().lower() for step in args.steps.split(',')] + # Mappen Sie die CLI-Schrittnamen auf die Parameter von process_reevaluation_rows + # Die Parameter in process_reevaluation_rows (v1.6.6 Anpassung) sind: + # process_wiki_steps, process_chatgpt_steps, process_website_steps + process_wiki_flag = 'wiki' in steps_list + process_chatgpt_flag = 'chat' in steps_list + process_website_flag = 'web' in steps_list + # Wenn Ihre process_reevaluation_rows weitere boolsche Flags akzeptiert, mappen Sie die entsprechenden CLI-Namen hier. + + # Rufen Sie process_reevaluation_rows mit den ausgelesenen Flags auf + # process_reevaluation_rows ist eine Methode in DataProcessor. + data_processor.process_reevaluation_rows( + row_limit=args.limit, + clear_flag=True, # Standardmäßig Flag 'x' löschen + process_wiki_steps=process_wiki_flag, # <<< ÜBERGIBT DIE STEUERUNG + process_chatgpt_steps=process_chatgpt_flag, # <<< ÜBERGIBT DIE STEUERUNG + process_website_steps=process_website_flag + # Wenn Ihre process_reevaluation_rows weitere Parameter hat, übergeben Sie diese hier + ) elif mode == "website_lookup": - # Annahme: process_serp_website_lookup_for_empty verwendet logging intern - data_processor.process_serp_website_lookup_for_empty() + # process_serp_website_lookup_for_empty sollte jetzt data_processor.process_serp_website_lookup sein + data_processor.process_serp_website_lookup(limit=args.limit) # Fügen Sie hier den Limit Parameter hinzu, falls gewünscht/unterstützt elif mode == "website_details": - logging.warning("Modus 'website_details' ist experimentell.") - # Annahme: process_website_details_for_marked_rows verwendet logging intern - data_processor.process_website_details_for_marked_rows() + # process_website_details_for_marked_rows sollte jetzt data_processor.process_website_details sein + data_processor.process_website_details(limit=args.limit) # Fügen Sie hier den Limit Parameter hinzu, falls gewünscht/unterstützt elif mode == "contacts": - # Annahme: process_contact_research verwendet logging intern - process_contact_research(sheet_handler) + # process_contact_research sollte jetzt data_processor.process_contact_research sein + data_processor.process_contact_research(limit=args.limit) # Fügen Sie hier den Limit Parameter hinzu, falls gewünscht/unterstützt - elif mode == "full_run": - if row_limit == 0: - logging.info("Limit 0 angegeben -> Überspringe full_run.") - else: - # Prüfe, ob eine explizite Startzeile übergeben wurde - start_data_index = -1 # Initialisieren - if args.start_row and args.start_row > 5: - header_rows = 5 # Standard-Annahme - start_data_index = args.start_row - header_rows - 1 # Konvertiere zu 0-basiertem Datenindex - logging.info(f"Nutze expliziten Start-Datenindex {start_data_index} (Sheet Zeile {args.start_row}) für 'full_run'.") - else: - logging.info("Ermittle Startindex für 'full_run' (erste Zeile ohne AO)...") - # Annahme: get_start_row_index verwendet logging intern - start_data_index = sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Prüfung") + elif mode == "full_run": # process_rows_sequentially + # process_rows_sequentially ist eine Methode in DataProcessor. + # Der Aufruf muss hier implementiert werden (Startindex Logic etc., wie im alten main Block). + logging.warning("Modus 'full_run' benötigt noch die Implementierung des Aufrufs von process_sequential.") + # Beispielaufruf (wenn process_sequential eine Methode ist): + # # start_data_index logic (wie im alten main block) + # header_rows = 5 # Annahme + # start_data_index = 0 # Default + # if args.start_row is not None: + # start_data_index = args.start_row - 1 # 0-based + # if start_data_index < header_rows: logging.warning(f"Manuelle Startzeile {args.start_row} liegt innerhalb der Header."); start_data_index = header_rows + # else: + # # Automatische Ermittlung der Startzeile (z.B. erste Zeile ohne AO) + # logging.info("Automatische Ermittlung der Startzeile für sequenzielle Verarbeitung (erste Zeile ohne AO)...") + # # get_start_row_index gibt 0-basierter Index in Daten (ohne Header) zurück + # start_data_index_no_header = sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Prüfung") + # if start_data_index_no_header == -1: logging.error("FEHLER bei automatischer Ermittlung der Startzeile."); return + # start_data_index = start_data_index_no_header + header_rows # 0-based index in all_data + # + # # Berechne num_to_process + # if not sheet_handler.load_data(): logging.error("Fehler beim Laden der Daten."); return + # total_rows = len(sheet_handler.get_all_data_with_headers()) + # num_available = total_rows - start_data_index # Anzahl Zeilen ab Startindex + # num_to_process = num_available + # if args.limit is not None and args.limit >= 0: + # num_to_process = min(num_available, args.limit) + # + # if num_to_process > 0: + # logging.info(f"'full_run': Verarbeite {num_to_process} Zeilen ab Sheet-Zeile {start_data_index + 1}.") + # # Hier müssten Sie auch die Flags für die Schritte abfragen/übergeben + # # Für full_run würden Sie wahrscheinlich alle Schritte wählen (oder über neues Argument steuern) + # data_processor.process_sequential( + # start_sheet_row = start_data_index + 1, # 1-basierte Startzeile + # num_to_process = num_to_process, + # process_wiki=True, # Beispiel: Alle Schritte + # process_chatgpt=True, + # process_website=True + # # Wenn process_sequential granularere Flags nimmt, übergeben Sie diese hier + # ) + # else: logging.info("Keine Zeilen für 'full_run' zu verarbeiten.") - # Prüfe, ob get_start_row_index einen gültigen Index zurückgab - if start_data_index != -1: - current_data = sheet_handler.get_data() # Hole aktuelle Daten - if start_data_index < len(current_data): - num_available = len(current_data) - start_data_index - num_to_process = min(row_limit, num_available) if row_limit is not None and row_limit >= 0 else num_available - if num_to_process > 0: - logging.info(f"'full_run': Verarbeite {num_to_process} Zeilen ab Daten-Index {start_data_index}.") - # Annahme: process_rows_sequentially verwendet logging intern - data_processor.process_rows_sequentially( - start_data_index, - num_to_process, - process_wiki=True, - process_chatgpt=True, - process_website=True - ) - else: - logging.info("Keine Zeilen für 'full_run' zu verarbeiten (Limit/Startindex).") - else: - logging.warning(f"Startindex {start_data_index} liegt hinter der letzten Datenzeile ({len(current_data)}). Keine Verarbeitung.") - else: - # Fehlermeldung wird von get_start_row_index erwartet - logging.warning(f"Startindex für 'full_run' ungültig (Fehler bei Ermittlung oder Spalte nicht gefunden).") elif mode == "alignment": - print("\nACHTUNG: Dieser Modus überschreibt die Header-Zeilen A1:AX5!") - try: - confirm = input("Möchten Sie wirklich fortfahren? (j/N): ").strip().lower() - except Exception as e_input: - logging.error(f"Input-Fehler bei Bestätigung: {e_input}") - confirm = 'n' - if confirm == 'j': - logging.info("Starte Alignment Demo...") - # Annahme: alignment_demo verwendet logging intern - alignment_demo(sheet_handler.sheet) - else: - logging.info("Alignment Demo abgebrochen.") + # alignment_demo ist global und braucht sheet_handler.sheet + alignment_demo(sheet_handler.sheet) # Stellen Sie sicher, dass alignment_demo global bleibt + + elif mode == "train_technician_model": + # train_technician_model sollte jetzt data_processor.train_technician_model sein + data_processor.train_technician_model(model_out=args.model_out, imputer_out=args.imputer_out, patterns_out=args.patterns_out) # Argumente übergeben elif mode == "update_wiki": - logging.info("Starte Modus 'update_wiki'...") - # Annahme: process_wiki_updates_from_chatgpt verwendet logging intern - process_wiki_updates_from_chatgpt(sheet_handler, data_processor, row_limit=row_limit) + # process_wiki_updates_from_chatgpt sollte jetzt data_processor.process_wiki_updates_from_chatgpt sein + data_processor.process_wiki_updates_from_chatgpt(row_limit=args.limit) # row_limit Parameter hinzufügen - # --- NEUER MODUS --- elif mode == "find_wiki_serp": - logging.info(f"Starte Modus '{mode}'...") - min_employees_for_serp = 500 # Standardwert, ggf. über Argument steuerbar machen - # Annahme: process_find_wiki_with_serp verwendet logging intern - process_find_wiki_with_serp(sheet_handler, row_limit=row_limit, min_employees=min_employees_for_serp) - # --- ENDE NEUER MODUS --- + # process_find_wiki_with_serp sollte jetzt data_processor.process_find_wiki_serp sein + data_processor.process_find_wiki_serp(row_limit=args.limit, min_employees=args.min_employees, min_umsatz=args.min_umsatz) # min_employees und min_umsatz hinzufügen - # Block für Modelltraining - elif mode == "train_technician_model": - logging.info(f"Starte Modus: {mode}") - # Annahme: prepare_data_for_modeling verwendet logging intern - prepared_df = data_processor.prepare_data_for_modeling() - if prepared_df is not None and not prepared_df.empty: - logging.info("Aufteilen der Daten für das Modelltraining...") - try: - # Definition von X und y - X = prepared_df.drop(columns=['Techniker_Bucket', 'CRM Name', 'Anzahl_Servicetechniker_Numeric']) # CRM Name statt 'name' - y = prepared_df['Techniker_Bucket'] - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y) - split_successful = True - logging.info(f"Train/Test Split: {len(X_train)} Train, {len(X_test)} Test samples.") - except KeyError as e: - logging.error(f"FEHLER beim Train/Test Split: Spalte nicht gefunden - {e}. Stellen Sie sicher, dass prepare_data_for_modeling die Spalten korrekt zurückgibt.") - split_successful = False - except Exception as e: - logging.error(f"FEHLER beim Train/Test Split: {e}") - split_successful = False - if split_successful: - logging.info("Imputation fehlender numerischer Werte (Median)...") - numeric_features = ['Finaler_Umsatz', 'Finaler_Mitarbeiter'] - try: - imputer = SimpleImputer(strategy='median') - if all(nf in X_train.columns for nf in numeric_features): - X_train[numeric_features] = imputer.fit_transform(X_train[numeric_features]) - X_test[numeric_features] = imputer.transform(X_test[numeric_features]) - imputer_filename = args.imputer_out - with open(imputer_filename, 'wb') as f_imp: pickle.dump(imputer, f_imp) - logging.info(f"Imputer erfolgreich trainiert und gespeichert: '{imputer_filename}'.") - imputation_successful = True - else: - logging.error("FEHLER: Numerische Features für Imputation nicht in Trainingsdaten gefunden.") - imputation_successful = False - except Exception as e: - logging.error(f"FEHLER bei der Imputation: {e}") - imputation_successful = False + elif mode == "wiki_reextract": # <<< NEUER MODUS RUFT NEUE FUNKTION AUF + # Rufe die neu erstellte globale Funktion auf, die sheet_handler und data_processor benötigt + # Diese Funktion implementiert die Kriterien-Logik "M gefüllt & AN leer" und ruft dann _process_single_row + # mit den spezifischen Flags (nur Wiki) und force_reeval=True auf. + process_wiki_reextract_missing_an(sheet_handler, data_processor, limit=args.limit) # Annahme: process_wiki_reextract_missing_an ist global definiert - if imputation_successful: - logging.info("Starte Decision Tree Training mit GridSearchCV...") - param_grid = { - 'criterion': ['gini', 'entropy'], 'max_depth': [6, 8, 10, 12, 15], - 'min_samples_split': [20, 40, 60], 'min_samples_leaf': [10, 20, 30], - 'ccp_alpha': [0.0, 0.001, 0.005] - } - dtree = DecisionTreeClassifier(random_state=42, class_weight='balanced') - grid_search = GridSearchCV(estimator=dtree, param_grid=param_grid, cv=5, - scoring='f1_weighted', n_jobs=-1, verbose=1) - try: - grid_search.fit(X_train, y_train) - best_estimator = grid_search.best_estimator_ - logging.info(f"GridSearchCV abgeschlossen.") - logging.info(f"Beste Parameter: {grid_search.best_params_}") - logging.info(f"Bester F1-Score (gewichtet, CV): {grid_search.best_score_:.4f}") - - model_filename = args.model_out - with open(model_filename, 'wb') as f_mod: pickle.dump(best_estimator, f_mod) - logging.info(f"Bestes Modell gespeichert: '{model_filename}'.") - training_successful = True - except Exception as e_train: - logging.exception(f"FEHLER während des Trainings: {e_train}") - training_successful = False - - if training_successful: - logging.info("Evaluiere Modell auf dem Test-Set...") - try: - y_pred = best_estimator.predict(X_test) - test_accuracy = accuracy_score(y_test, y_pred) - # Sicherstellen, dass Klassen als Liste von Strings übergeben werden - class_labels = [str(cls) for cls in best_estimator.classes_] - report = classification_report(y_test, y_pred, zero_division=0, - labels=best_estimator.classes_, target_names=class_labels) - conf_matrix = confusion_matrix(y_test, y_pred, labels=best_estimator.classes_) - conf_matrix_df = pd.DataFrame(conf_matrix, index=class_labels, columns=class_labels) - - logging.info(f"\n--- Evaluation Test-Set ---") - logging.info(f"Genauigkeit: {test_accuracy:.4f}") - logging.info(f"Classification Report:\n{report}") - logging.info(f"Confusion Matrix:\n{conf_matrix_df}") - print(f"\nModell Genauigkeit (Test): {test_accuracy:.4f}") - - logging.info("Extrahiere Baumregeln...") - try: - feature_names = list(X_train.columns) - rules_text = export_text(best_estimator, feature_names=feature_names, - show_weights=True, spacing=3) - patterns_filename = args.patterns_out - with open(patterns_filename, 'w', encoding='utf-8') as f_rules: - f_rules.write(rules_text) - logging.info(f"Regeln als Text gespeichert: '{patterns_filename}'.") - except Exception as e_export: - logging.error(f"Fehler beim Exportieren der Regeln: {e_export}") - except Exception as e_eval: - logging.exception(f"Fehler bei der Evaluation des Test-Sets: {e_eval}") - else: - logging.warning("Datenvorbereitung für Modelltraining fehlgeschlagen oder ergab keine Daten.") else: - logging.error(f"Unbekannter Modus '{mode}' wurde zur Ausführung übergeben.") + # Dies sollte nicht passieren, wenn die Validierung oben korrekt ist + logging.error(f"Unerwarteter Modus '{mode}' erreicht das Ausführungsende.") + except KeyboardInterrupt: logging.warning("Skript durch Benutzer unterbrochen (KeyboardInterrupt).") print("\n! Skript wurde manuell beendet.") except Exception as e: - logging.critical(f"FATAL: Unerwarteter Fehler im Haupt-Ausführungsblock des Modus '{mode}': {e}") + # Dieser Block fängt Fehler ab, die in den aufgerufenen Funktionen/Methoden passieren + logging.critical(f"FATAL: Unerwarteter Fehler während der Ausführung von Modus '{mode}': {e}") logging.exception("Traceback des kritischen Fehlers:") # --- Abschluss --- @@ -5424,48 +5379,30 @@ def main(): # Schließe Logging Handler explizit logging.shutdown() + # Logfile Pfad für den Nutzer ausgeben print(f"\nVerarbeitung abgeschlossen. Logfile: {LOG_FILE}") # Führt die main-Funktion aus, wenn das Skript direkt gestartet wird if __name__ == '__main__': - # --- WICHTIG: Fehlende Imports hier hinzufügen --- - import functools # Für retry decorator nötig - import pandas as pd - import numpy as np - from sklearn.model_selection import train_test_split, GridSearchCV - from sklearn.impute import SimpleImputer - from sklearn.tree import DecisionTreeClassifier, export_text - from sklearn.metrics import accuracy_score, classification_report, confusion_matrix - import json - import pickle - import concurrent.futures - import threading - # --- Ende fehlende Imports --- + # --- Sicherstellen, dass alle globalen Imports hier sind --- + # ... (alle Imports wie am Anfang des Skripts) ... - # --- Annahme: Decorator Definition ist hier oder importiert --- - # Beispielhafte Decorator Definition (falls nicht in separater Datei) - def retry_on_failure(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - # ... (Implementierung des Decorators wie zuvor) ... - # Minimalistische Version zur Kompilierung: - try: - return func(*args, **kwargs) - except Exception as e: - logging.warning(f"Retry wird übersprungen (Dummy-Decorator): Fehler in {func.__name__}: {e}") - raise e # Fehler weitergeben - return wrapper - # --- Ende Decorator Annahme --- + # --- Sicherstellen, dass alle globalen Helfer-Funktionen hier oder importiert sind --- + # ... (Alle Ihre globalen Helfer-Funktionen: clean_text, normalize_company_name, + # extract_numeric_value, get_numeric_filter_value, call_openai_chat, serp_wikipedia_lookup, + # serp_website_lookup, search_linkedin_contacts, get_gender, get_email_address, + # fuzzy_similarity, is_valid_wikipedia_article_url, evaluate_branche_chatgpt, + # summarize_website_content, load_target_schema, map_external_branch, alignment_demo, + # retry_on_failure, create_log_filename, debug_print, _process_batch (falls global)) ... - # --- Annahme: Restliche Funktionen/Klassen sind definiert --- - # z.B. Config, COLUMN_MAP, create_log_filename, load_target_schema, - # GoogleSheetHandler, WikipediaScraper, DataProcessor, - # Helper-Funktionen (simple_normalize_url etc.), - # Batch-Funktionen (run_dispatcher etc.), - # API-Funktionen (call_openai_chat etc.), - # Neue Funktionen (serp_wikipedia_lookup, process_find_wiki_with_serp) - # ... - # --- Ende Funktions/Klassen Annahmen --- + # NEU: Die Kriterien-Funktion und die Funktion, die den neuen Modus steuert, müssen hier global sein + # Kopieren Sie die Definitionen von criteria_m_filled_an_empty und process_wiki_reextract_missing_an hierher. + # --- Sicherstellen, dass alle Klassen hier definiert sind --- + # ... (Config, GoogleSheetHandler, WikipediaScraper) ... + # KORRIGIERTE DataProcessor Klasse Definition (mit __init__(self, sheet_handler, wiki_scraper)) + # und allen Methoden, die Sie bis jetzt hatten, IN DER KLASSE eingerückt. + + # Die main Funktion aufrufen main() \ No newline at end of file