This commit is contained in:
2025-04-19 17:23:36 +00:00
parent 6c1fd91a69
commit 0b51a11aef

View File

@@ -69,7 +69,7 @@ PATTERNS_FILE_JSON = "technician_patterns.json" # Optional
# ==================== KONFIGURATION ==================== # ==================== KONFIGURATION ====================
class Config: class Config:
# ... (Alle deine bisherigen Config-Einstellungen) ... # ... (Alle deine bisherigen Config-Einstellungen) ...
VERSION = "v1.6.4" # Versionsnummer erhöhen VERSION = "v1.6.5" # Versionsnummer erhöhen
LANG = "de" LANG = "de"
SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo"
MAX_RETRIES = 3 MAX_RETRIES = 3
@@ -3814,67 +3814,85 @@ class DataProcessor:
def process_reevaluation_rows(self, row_limit=None, clear_flag=True): def process_reevaluation_rows(self, row_limit=None, clear_flag=True):
""" """
Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind. Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind.
Ruft _process_single_row für jede dieser Zeilen auf. Ruft _process_single_row für jede dieser Zeilen auf.
Verarbeitet maximal row_limit Zeilen. Verarbeitet maximal row_limit Zeilen.
Löscht optional das 'x'-Flag nach erfolgreicher Verarbeitung. Löscht optional das 'x'-Flag nach erfolgreicher Verarbeitung.
""" """
debug_print(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}") logging.info(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}")
# Lade Daten frisch, um aktuelle Flags zu sehen # Lade Daten frisch
if not self.sheet_handler.load_data(): return if not self.sheet_handler.load_data(): return
all_data = self.sheet_handler.get_all_data_with_headers() all_data = self.sheet_handler.get_all_data_with_headers()
if not all_data or len(all_data) <= 5: return if not all_data or len(all_data) <= 5:
header_rows = 5 logging.warning("Keine Daten für Re-Evaluation gefunden.")
data_rows = all_data[header_rows:] return
header_rows = 5
data_rows = all_data[header_rows:]
reeval_col_idx = COLUMN_MAP.get("ReEval Flag") reeval_col_idx = COLUMN_MAP.get("ReEval Flag")
if reeval_col_idx is None: return debug_print("FEHLER: 'ReEval Flag' nicht in COLUMN_MAP.") if reeval_col_idx is None:
logging.error("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden.")
return
rows_to_process = [] # Finde zuerst alle Kandidaten (Zeilennummer im Sheet und Rohdaten)
# Finde zuerst alle Kandidaten rows_to_process = []
for idx, row in enumerate(data_rows): for idx, row in enumerate(data_rows):
if len(row) > reeval_col_idx and row[reeval_col_idx].strip().lower() == "x": if len(row) > reeval_col_idx and row[reeval_col_idx].strip().lower() == "x":
row_num_in_sheet = idx + header_rows + 1 row_num_in_sheet = idx + header_rows + 1
rows_to_process.append({'row_num': row_num_in_sheet, 'data': row}) rows_to_process.append({'row_num': row_num_in_sheet, 'data': row})
debug_print(f"{len(rows_to_process)} Zeilen mit ReEval-Flag 'x' gefunden.") found_count = len(rows_to_process)
logging.info(f"{found_count} Zeilen mit ReEval-Flag 'x' gefunden.")
processed_count = 0 if found_count == 0:
updates_clear_flag = [] logging.info("Keine Zeilen zur Re-Evaluation markiert.")
return
# Verarbeite die gefundenen Kandidaten bis zum Limit processed_count = 0
for task in rows_to_process: updates_clear_flag = []
if row_limit is not None and processed_count >= row_limit: rows_actually_processed = [] # Liste der Zeilennummern, die verarbeitet wurden
debug_print(f"Zeilenlimit ({row_limit}) erreicht. Breche Re-Evaluation ab.")
break
row_num = task['row_num'] # Verarbeite die gefundenen Kandidaten bis zum Limit
row_data = task['data'] # WICHTIG: Iteriere nur über die Kandidatenliste, nicht die gesamten Daten
debug_print(f"--- Re-Evaluiere Zeile {row_num} ---") for task in rows_to_process:
try: # --- KORRIGIERTE LIMIT-PRÜFUNG ---
# Führe volle Verarbeitung für diese Zeile durch # Prüfe das Limit *bevor* die Verarbeitung beginnt
# _process_single_row prüft intern Timestamps AN, AT, AO if row_limit is not None and processed_count >= row_limit:
self._process_single_row(row_num, row_data, process_wiki=True, process_chatgpt=True, process_website=True) logging.info(f"Zeilenlimit ({row_limit}) für Re-Evaluation erreicht. Breche weitere Verarbeitung ab.")
processed_count += 1 break # Verlasse die Schleife
# Optional: Flag nach Verarbeitung löschen row_num = task['row_num']
if clear_flag: row_data = task['data']
flag_col_letter = self.sheet_handler._get_col_letter(reeval_col_idx + 1) logging.info(f"--- Re-Evaluiere Zeile {row_num} ---")
updates_clear_flag.append({'range': f'{flag_col_letter}{row_num}', 'values': [['']]}) try:
# Führe volle Verarbeitung für diese Zeile durch
# _process_single_row prüft intern Timestamps AN, AT, AO etc.
self._process_single_row(row_num, row_data, process_wiki=True, process_chatgpt=True, process_website=True)
processed_count += 1
rows_actually_processed.append(row_num) # Füge zur Liste der verarbeiteten hinzu
except Exception as e_proc: # Optional: Flag nach *erfolgreicher* Verarbeitung löschen (Sammeln)
debug_print(f"FEHLER bei Re-Evaluation von Zeile {row_num}: {e_proc}") if clear_flag:
# Flag hier nicht löschen, damit es beim nächsten Mal versucht wird flag_col_letter = self.sheet_handler._get_col_letter(reeval_col_idx + 1)
if flag_col_letter: # Nur wenn Buchstabe gültig
updates_clear_flag.append({'range': f'{flag_col_letter}{row_num}', 'values': [['']]})
# Lösche Flags am Ende gebündelt except Exception as e_proc:
if clear_flag and updates_clear_flag: # Logge Fehler, aber mache mit der nächsten Zeile weiter
debug_print(f"Lösche ReEval-Flags für {len(updates_clear_flag)} verarbeitete Zeilen...") logging.exception(f"FEHLER bei Re-Evaluation von Zeile {row_num}: {e_proc}")
success = self.sheet_handler.batch_update_cells(updates_clear_flag) # Flag hier nicht löschen, damit es beim nächsten Mal versucht wird
if not success: debug_print("FEHLER beim Löschen der ReEval-Flags.")
debug_print(f"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Limit: {row_limit}).") # Lösche Flags am Ende gebündelt für die *erfolgreich* verarbeiteten Zeilen
if clear_flag and updates_clear_flag:
# Logge, welche Flags gelöscht werden
logging.info(f"Lösche ReEval-Flags für {len(updates_clear_flag)} erfolgreich verarbeitete Zeilen ({rows_actually_processed})...")
success = self.sheet_handler.batch_update_cells(updates_clear_flag)
if not success:
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}).")
def process_website_details_for_marked_rows(self): def process_website_details_for_marked_rows(self):
@@ -4088,214 +4106,346 @@ class DataProcessor:
return None return None
# ==================== MAIN FUNCTION ====================
# ==================== MAIN FUNCTION ==================== # ==================== MAIN FUNCTION ====================
def main(): def main():
# WICHTIG: Global LOG_FILE wird benötigt, aber erst nach Arg-Parsing gesetzt.
global LOG_FILE global LOG_FILE
# --- Logging Setup (Konfiguration von Level und Format) ---
# Diese Konfiguration wird wirksam, sobald die Handler hinzugefügt werden.
import logging import logging
# Ganz am Anfang von main, vor dem ersten debug_print log_level = logging.DEBUG # Explizit DEBUG setzen für detaillierte Logs
log_level = logging.DEBUG # Explizit setzen log_format = '%(asctime)s - %(levelname)-8s - %(name)-15s - %(message)s' # Angepasstes Format
logging.basicConfig(level=log_level,
format='%(asctime)s - %(levelname)s - %(name)s - %(message)s', # Name des Loggers hinzufügen # Root-Logger konfigurieren (noch ohne File Handler)
filename=LOG_FILE, # In Datei schreiben logging.basicConfig(level=log_level, format=log_format, handlers=[]) # WICHTIG: handlers=[] verhindert default Console Handler
filemode='a') # Anfügen
# Optional: Auch auf Konsole ausgeben # Console Handler explizit hinzufügen
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
console_handler.setLevel(log_level) console_handler.setLevel(log_level) # Nimm das globale Level
console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')) console_handler.setFormatter(logging.Formatter(log_format))
logging.getLogger('').addHandler(console_handler) logging.getLogger('').addHandler(console_handler) # Füge zum Root-Logger hinzu
# --- Ende Initial Logging Setup ---
# Testnachricht nach Logging-Setup # Testnachricht (geht nur an Konsole, da File Handler noch fehlt)
logging.debug("DEBUG Logging ist konfiguriert.") logging.debug("DEBUG Logging initial konfiguriert (nur Konsole).")
logging.info("INFO Logging ist konfiguriert.") logging.info("INFO Logging initial konfiguriert (nur Konsole).")
# --- Initialisierung --- # --- Initialisierung (Argument Parser etc.) ---
parser = argparse.ArgumentParser(description="Firmen-Datenanreicherungs-Skript") parser = argparse.ArgumentParser(description="Firmen-Datenanreicherungs-Skript v1.6.5") # Version aktualisiert
# NEU: 'update_wiki' hinzugefügt
valid_modes = ["combined", "wiki", "website", "branch", "summarize", "reeval", valid_modes = ["combined", "wiki", "website", "branch", "summarize", "reeval",
"website_lookup", "website_details", "contacts", "full_run", "website_lookup", "website_details", "contacts", "full_run",
"alignment", "train_technician_model", "update_wiki"] "alignment", "train_technician_model", "update_wiki"]
parser.add_argument("--mode", type=str, help=f"Betriebsmodus ({', '.join(valid_modes)})") 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("--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", default=None) # Optionaler Startpunkt
parser.add_argument("--model_out", type=str, default=MODEL_FILE, help=f"Pfad für Modell (.pkl)") 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("--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)") parser.add_argument("--patterns_out", type=str, default=PATTERNS_FILE_TXT, help=f"Pfad für Regeln (.txt)")
args = parser.parse_args() args = parser.parse_args()
Config.load_api_keys() # Lade API Keys direkt am Anfang
Config.load_api_keys() # Nutzt jetzt logging.debug intern
# Betriebsmodus ermitteln # Betriebsmodus ermitteln
mode = None mode = None
if args.mode and args.mode.lower() in valid_modes: mode = args.mode.lower(); print(f"Betriebsmodus (aus Kommandozeile): {mode}") if args.mode and args.mode.lower() in valid_modes:
mode = args.mode.lower()
logging.info(f"Betriebsmodus (aus Kommandozeile): {mode}")
else: # Interaktive Abfrage else: # Interaktive Abfrage
print("Bitte wählen Sie den Betriebsmodus:") print("\nBitte wählen Sie den Betriebsmodus:")
# ... (print-Anweisungen für Modi bleiben gleich) ...
print(" combined: Wiki(AX), Website-Scrape(AR), Summarize(AS), Branch(AO) (Batch, Start bei leerem AO)") print(" combined: Wiki(AX), Website-Scrape(AR), Summarize(AS), Branch(AO) (Batch, Start bei leerem AO)")
print(" wiki: Nur Wikipedia-Verifizierung (AX) (Batch, Start bei leerem AX)") print(" wiki: Nur Wikipedia-Verifizierung (AX) (Batch, Start bei leerem AX)")
print(" website: Nur Website-Scraping Rohtext (AR) (Batch, Start bei leerem AR)") print(" website: Nur Website-Scraping Rohtext (AR) (Batch, Start bei leerem AR)")
print(" summarize: Nur Website-Zusammenfassung (AS) (Batch, Start bei leerem AS)") print(" summarize: Nur Website-Zusammenfassung (AS) (Batch, Start bei leerem AS)")
print(" branch: Nur Branchen-Einschätzung (AO) (Batch, Start bei leerem AO)") print(" branch: Nur Branchen-Einschätzung (AO) (Batch, Start bei leerem AO)")
print(" update_wiki: Wiki-URL aus Spalte T übernehmen & Reparse/Re-Branch") # NEU print(" update_wiki: Wiki-URL aus Spalte U nach M übernehmen & ReEval-Flag setzen")
print(" reeval: Verarbeitet Zeilen mit 'x' (volle Verarbeitung, alle TS prüfen)") print(" reeval: Verarbeitet Zeilen mit 'x' in A (volle Verarbeitung)")
print(" website_lookup: Sucht fehlende Websites (D)") print(" website_lookup: Sucht fehlende Websites (D)")
print(" website_details:Extrahiert Details für Zeilen mit 'x' (AR)") print(" website_details:Extrahiert Details für Zeilen mit 'x' (AR) - EXPERIMENTELL") # Ggf. anpassen
print(" contacts: Sucht LinkedIn Kontakte (AM)") print(" contacts: Sucht LinkedIn Kontakte (AM)")
print(" full_run: Verarbeitet sequentiell ab erster Zeile ohne AO (alle TS prüfen)") print(" full_run: Verarbeitet sequentiell ab erster Zeile ohne AO (alle TS prüfen)")
print(" alignment: Schreibt Header A1:AX5 (!)") print(" alignment: Schreibt Header A1:AX5 (!)")
print(" train_technician_model: Trainiert Decision Tree zur Technikerschätzung") print(" train_technician_model: Trainiert Decision Tree zur Technikerschätzung")
try: try:
mode_input = input(f"Geben Sie den Modus ein ({', '.join(valid_modes)}): ").strip().lower() mode_input = input(f"Geben Sie den Modus ein ({', '.join(valid_modes)}): ").strip().lower()
if mode_input in valid_modes: mode = mode_input if mode_input in valid_modes:
else: print("Ungültige Eingabe -> combined"); mode = "combined" mode = mode_input
except Exception as e: print(f"Fehler Modus-Eingabe ({e}) -> combined"); mode = "combined" 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}")
# Zeilenlimit ermitteln # Zeilenlimit ermitteln
row_limit = None row_limit = None
if args.limit is not None: if args.limit is not None:
if args.limit >= 0: row_limit = args.limit; print(f"Zeilenlimit (aus Kommandozeile): {row_limit}") if args.limit >= 0:
else: print("Warnung: Negatives Limit ignoriert."); row_limit = None row_limit = args.limit
elif mode in ["combined", "wiki", "website", "branch", "summarize", "full_run"]: # Nur für relevante Modi fragen logging.info(f"Zeilenlimit (aus Kommandozeile): {row_limit}")
try: else:
limit_input = input("Max Zeilen? (Enter=alle): "); logging.warning("Warnung: Negatives Limit ignoriert.")
if limit_input.strip(): row_limit = None
try: elif mode in ["combined", "wiki", "website", "branch", "summarize", "full_run", "reeval"]: # Relevante Modi
limit_val = int(limit_input) try:
if limit_val >= 0: row_limit = limit_val; print(f"Zeilenlimit: {row_limit}") limit_input = input(f"Maximale Anzahl Zeilen für Modus '{mode}'? (Enter=alle): ")
else: print("Negatives Limit -> Kein Limit"); row_limit = None if limit_input.strip():
except ValueError: print("Ungültige Zahl -> Kein Limit"); row_limit = None try:
else: row_limit = None; print("Kein Zeilenlimit.") limit_val = int(limit_input)
except Exception as e: print(f"Fehler Limit-Eingabe ({e}) -> Kein Limit"); row_limit = None if limit_val >= 0:
row_limit = limit_val
logging.info(f"Zeilenlimit (interaktiv): {row_limit}")
else:
logging.warning("Negatives Limit -> Kein Limit")
row_limit = None
except ValueError:
logging.warning("Ungültige Zahl -> Kein Limit")
row_limit = None
else:
logging.info("Kein Zeilenlimit angegeben.")
row_limit = None
except Exception as e:
logging.error(f"Fehler Limit-Eingabe ({e}) -> Kein Limit")
row_limit = None
# --- Logdatei-Konfiguration abschließen ---
LOG_FILE = create_log_filename(mode) # Annahme: Funktion existiert und gibt Pfad zurück
try:
file_handler = logging.FileHandler(LOG_FILE, mode='a', encoding='utf-8')
file_handler.setLevel(log_level) # Nimm das globale Level
file_handler.setFormatter(logging.Formatter(log_format))
logging.getLogger('').addHandler(file_handler) # Füge zum Root-Logger hinzu
logging.info(f"Logging wird jetzt auch in Datei geschrieben: {LOG_FILE}")
except Exception as e:
logging.error(f"Konnte FileHandler für Logdatei '{LOG_FILE}' nicht erstellen: {e}")
# Programm kann weiterlaufen, loggt aber nur auf Konsole
# --- Ende Logdatei-Konfiguration ---
# Logfile initialisieren # --- Logge finale Startinfos (jetzt auch in Datei) ---
LOG_FILE = create_log_filename(mode) logging.info(f"===== Skript gestartet =====")
debug_print(f"===== Skript gestartet ====="); debug_print(f"Version: {Config.VERSION}") logging.info(f"Version: {Config.VERSION}") # Sollte jetzt v1.6.5 sein
debug_print(f"Betriebsmodus: {mode}"); logging.info(f"Betriebsmodus: {mode}")
limit_log_text = str(row_limit) if row_limit is not None else 'N/A für diesen Modus' 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"]: if mode in ["combined", "wiki", "website", "branch", "summarize", "full_run", "reeval"]:
limit_log_text = str(row_limit) if row_limit is not None else 'Unbegrenzt' 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)' if row_limit == 0: limit_log_text = '0 (Keine Verarbeitung geplant)'
debug_print(f"Zeilenlimit: {limit_log_text}") logging.info(f"Zeilenlimit: {limit_log_text}")
debug_print(f"Logdatei: {LOG_FILE}") logging.info(f"Logdatei: {LOG_FILE}")
# --- Ende finale Startinfos ---
# --- Vorbereitung --- # --- Vorbereitung (Sheet Handler etc.) ---
load_target_schema() load_target_schema() # Nutzt jetzt logging intern
try: sheet_handler = GoogleSheetHandler(); try:
except Exception as e: debug_print(f"FATAL: Init GSheet: {e}"); print(f"FEHLER GSheet. Log: {LOG_FILE}"); return sheet_handler = GoogleSheetHandler() # Nutzt jetzt logging intern
data_processor = DataProcessor(sheet_handler) 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
data_processor = DataProcessor(sheet_handler) # Nutzt jetzt logging intern
# --- Modusausführung --- # --- Modusausführung ---
start_time = time.time() start_time = time.time()
debug_print(f"Starte Verarbeitung um {datetime.now().strftime('%H:%M:%S')}...") logging.info(f"Starte Verarbeitung um {datetime.now().strftime('%H:%M:%S')}...")
try: try:
# Batch-Modi über Dispatcher # Batch-Modi über Dispatcher
if mode in ["wiki", "website", "branch", "summarize", "combined"]: if mode in ["wiki", "website", "branch", "summarize", "combined"]:
if row_limit == 0: debug_print("Limit 0 -> Skip Dispatcher.") if row_limit == 0:
else: run_dispatcher(mode, sheet_handler, row_limit) logging.info("Limit 0 angegeben -> Überspringe Dispatcher für Batch-Modus.")
else:
run_dispatcher(mode, sheet_handler, row_limit) # Nutzt jetzt logging intern
# Einzelne Zeilen Modi (kein Batch-Dispatcher) # Einzelne Zeilen Modi (kein Batch-Dispatcher)
elif mode == "reeval": data_processor.process_reevaluation_rows() elif mode == "reeval":
elif mode == "website_lookup": data_processor.process_serp_website_lookup_for_empty() data_processor.process_reevaluation_rows(row_limit=row_limit) # Nutzt jetzt logging intern
elif mode == "website_details": data_processor.process_website_details_for_marked_rows()
elif mode == "contacts": process_contact_research(sheet_handler) elif mode == "website_lookup":
data_processor.process_serp_website_lookup_for_empty() # Nutzt jetzt logging intern
elif mode == "website_details":
logging.warning("Modus 'website_details' ist experimentell.")
data_processor.process_website_details_for_marked_rows() # Nutzt jetzt logging intern
elif mode == "contacts":
process_contact_research(sheet_handler) # Nutzt jetzt logging intern
elif mode == "full_run": elif mode == "full_run":
if row_limit == 0: debug_print("Limit 0 -> Skip full_run.") if row_limit == 0:
else: logging.info("Limit 0 angegeben -> Überspringe full_run.")
start_index = sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Prüfung") else:
if start_index != -1 and start_index < len(sheet_handler.get_data()): # Prüfe, ob eine explizite Startzeile übergeben wurde
num_available = len(sheet_handler.get_data()) - start_index if args.start_row and args.start_row > 5:
num_to_process = min(row_limit, num_available) if row_limit is not None and row_limit >= 0 else num_available start_data_index = args.start_row - 5 - 1 # Konvertiere zu 0-basiertem Datenindex
if num_to_process > 0: logging.info(f"Nutze expliziten Start-Datenindex {start_data_index} (Sheet Zeile {args.start_row}) für 'full_run'.")
# Übergebe Flags an process_rows_sequentially else:
data_processor.process_rows_sequentially(start_index, num_to_process, process_wiki=True, process_chatgpt=True, process_website=True) logging.info("Ermittle Startindex für 'full_run' (erste Zeile ohne AO)...")
else: debug_print("Keine Zeilen für 'full_run' zu verarbeiten.") start_data_index = sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Prüfung")
else: debug_print(f"Startindex {start_index} für 'full_run' ungültig.")
if start_data_index != -1 and start_data_index < len(sheet_handler.get_data()):
num_available = len(sheet_handler.get_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}.")
# Übergebe Flags an process_rows_sequentially
data_processor.process_rows_sequentially(
start_data_index,
num_to_process,
process_wiki=True,
process_chatgpt=True,
process_website=True
) # Nutzt jetzt logging intern
else:
logging.info("Keine Zeilen für 'full_run' zu verarbeiten (Limit/Startindex).")
else:
logging.warning(f"Startindex {start_data_index} für 'full_run' ungültig oder keine Zeilen mehr.")
elif mode == "alignment": elif mode == "alignment":
print("\nACHTUNG: Überschreibt A1:AX5!"); # AX statt AS print("\nACHTUNG: Dieser Modus überschreibt die Header-Zeilen A1:AX5!")
try: confirm = input("Fortfahren? (j/N): ").strip().lower() try:
except Exception as e_input: print(f"Input-Fehler: {e_input}"); confirm = 'n' confirm = input("Möchten Sie wirklich fortfahren? (j/N): ").strip().lower()
if confirm == 'j': alignment_demo(sheet_handler.sheet) except Exception as e_input:
else: print("Abgebrochen.") logging.error(f"Input-Fehler bei Bestätigung: {e_input}")
confirm = 'n'
if confirm == 'j':
logging.info("Starte Alignment Demo...")
alignment_demo(sheet_handler.sheet) # Annahme: nutzt logging intern
else:
logging.info("Alignment Demo abgebrochen.")
# --- NEU: Wiki Update Modus --- # --- Wiki Update Modus ---
elif mode == "update_wiki": elif mode == "update_wiki":
process_wiki_updates_from_chatgpt(sheet_handler, data_processor) logging.info("Starte Modus 'update_wiki'...")
# --- Ende Wiki Update Modus --- process_wiki_updates_from_chatgpt(sheet_handler, data_processor, row_limit=row_limit) # Limit optional übergeben
# Block für Modelltraining (wie von dir bereitgestellt) # Block für Modelltraining
elif mode == "train_technician_model": elif mode == "train_technician_model":
debug_print(f"Starte Modus: {mode}") logging.info(f"Starte Modus: {mode}")
# (Dieser Block verwendet jetzt logging intern, keine Anpassungen hier nötig,
# außer sicherzustellen, dass prepare_data_for_modeling logging nutzt)
prepared_df = data_processor.prepare_data_for_modeling() prepared_df = data_processor.prepare_data_for_modeling()
if prepared_df is not None and not prepared_df.empty: if prepared_df is not None and not prepared_df.empty:
debug_print("Aufteilen der Daten...") logging.info("Aufteilen der Daten für das Modelltraining...")
try: try:
# Definition von X und y (wie im Original, Annahme: prepare_data... hat Spalten 'name' etc.)
X = prepared_df.drop(columns=['Techniker_Bucket', 'name', 'Anzahl_Servicetechniker_Numeric']) X = prepared_df.drop(columns=['Techniker_Bucket', 'name', 'Anzahl_Servicetechniker_Numeric'])
y = prepared_df['Techniker_Bucket'] 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) 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 split_successful = True
except Exception as e: debug_print(f"FEHLER Split: {e}"); split_successful = False logging.info(f"Train/Test Split: {len(X_train)} Train, {len(X_test)} Test samples.")
except Exception as e:
logging.error(f"FEHLER beim Train/Test Split: {e}")
split_successful = False
if split_successful: if split_successful:
debug_print("Imputation...") logging.info("Imputation fehlender numerischer Werte (Median)...")
numeric_features = ['Finaler_Umsatz', 'Finaler_Mitarbeiter'] numeric_features = ['Finaler_Umsatz', 'Finaler_Mitarbeiter']
try: try:
imputer = SimpleImputer(strategy='median') imputer = SimpleImputer(strategy='median')
X_train[numeric_features] = imputer.fit_transform(X_train[numeric_features]) # Stelle sicher, dass Spalten existieren bevor Transformation
X_test[numeric_features] = imputer.transform(X_test[numeric_features]) if all(nf in X_train.columns for nf in numeric_features):
imputer_filename = args.imputer_out; pickle.dump(imputer, open(imputer_filename, 'wb')) X_train[numeric_features] = imputer.fit_transform(X_train[numeric_features])
debug_print(f"Imputer gespeichert: '{imputer_filename}'.") X_test[numeric_features] = imputer.transform(X_test[numeric_features])
imputation_successful = True imputer_filename = args.imputer_out
except Exception as e: debug_print(f"FEHLER Imputation: {e}"); imputation_successful = False 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
if imputation_successful: if imputation_successful:
debug_print("Starte Training/GridSearchCV...") 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]} # Parameter Grid (wie im Original)
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] # Cost-Complexity Pruning
}
dtree = DecisionTreeClassifier(random_state=42, class_weight='balanced') 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) # GridSearchCV (wie im Original)
grid_search = GridSearchCV(estimator=dtree, param_grid=param_grid, cv=5,
scoring='f1_weighted', n_jobs=-1, verbose=1)
try: try:
grid_search.fit(X_train, y_train) grid_search.fit(X_train, y_train)
best_estimator = grid_search.best_estimator_ best_estimator = grid_search.best_estimator_
debug_print(f"GridSearchCV fertig. Beste Params: {grid_search.best_params_}, Score: {grid_search.best_score_:.4f}") logging.info(f"GridSearchCV abgeschlossen.")
model_filename = args.model_out; pickle.dump(best_estimator, open(model_filename, 'wb')) logging.info(f"Beste Parameter: {grid_search.best_params_}")
debug_print(f"Modell gespeichert: '{model_filename}'.") logging.info(f"Bester F1-Score (gewichtet, CV): {grid_search.best_score_:.4f}")
training_successful = True
except Exception as e_train: debug_print(f"FEHLER Training: {e_train}"); training_successful = False; import traceback; debug_print(traceback.format_exc()) # Speichere das beste Modell
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.error(f"FEHLER während des Trainings: {e_train}")
logging.exception("Traceback Training:") # Loggt Traceback
training_successful = False
if training_successful: if training_successful:
debug_print("Evaluiere Test-Set..."); y_pred = best_estimator.predict(X_test) logging.info("Evaluiere Modell auf dem Test-Set...")
test_accuracy = accuracy_score(y_test, y_pred)
report = classification_report(y_test, y_pred, zero_division=0, labels=best_estimator.classes_, target_names=best_estimator.classes_)
conf_matrix = confusion_matrix(y_test, y_pred, labels=best_estimator.classes_)
conf_matrix_df = pd.DataFrame(conf_matrix, index=best_estimator.classes_, columns=best_estimator.classes_)
debug_print(f"\n--- Evaluation Test-Set ---\nGenauigkeit: {test_accuracy:.4f}\nBericht:\n{report}\nMatrix:\n{conf_matrix_df}"); print(f"\nModell Genauigkeit (Test): {test_accuracy:.4f}")
debug_print("\nExtrahiere Regeln...");
try: try:
feature_names = list(X_train.columns); class_names = best_estimator.classes_ y_pred = best_estimator.predict(X_test)
rules_text = export_text(best_estimator, feature_names=feature_names, class_names=class_names, show_weights=True, spacing=3) test_accuracy = accuracy_score(y_test, y_pred)
patterns_filename = args.patterns_out; report = classification_report(y_test, y_pred, zero_division=0,
with open(patterns_filename, 'w', encoding='utf-8') as f: f.write(rules_text) labels=best_estimator.classes_, target_names=best_estimator.classes_)
debug_print(f"Regeln gespeichert: '{patterns_filename}'.") conf_matrix = confusion_matrix(y_test, y_pred, labels=best_estimator.classes_)
except Exception as e_export: debug_print(f"Fehler Export Regeln: {e_export}") conf_matrix_df = pd.DataFrame(conf_matrix, index=best_estimator.classes_, columns=best_estimator.classes_)
else: debug_print("Datenvorbereitung fehlgeschlagen -> Abbruch ML Training.")
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}") # Auch auf Konsole
logging.info("Extrahiere Baumregeln...")
try:
feature_names = list(X_train.columns)
class_names = list(best_estimator.classes_) # Sicherstellen, dass es eine Liste ist
rules_text = export_text(best_estimator, feature_names=feature_names,
show_weights=True, spacing=3) # class_names optional
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.error(f"Fehler bei der Evaluation des Test-Sets: {e_eval}")
else:
logging.warning("Datenvorbereitung für Modelltraining fehlgeschlagen oder ergab keine Daten.")
else: else:
debug_print(f"Unbekannter Modus '{mode}'.") logging.error(f"Unbekannter Modus '{mode}' wurde zur Ausführung übergeben.")
except KeyboardInterrupt:
logging.warning("Skript durch Benutzer unterbrochen (KeyboardInterrupt).")
print("\n! Skript wurde manuell beendet.")
except Exception as e: except Exception as e:
debug_print(f"FATAL: Unerwarteter Fehler in main try-Block: {e}") # Fange alle unerwarteten Fehler im Hauptblock ab
import traceback; debug_print(traceback.format_exc()) logging.critical(f"FATAL: Unerwarteter Fehler im Haupt-Ausführungsblock des Modus '{mode}': {e}")
logging.exception("Traceback des kritischen Fehlers:") # Loggt den Traceback
# --- Abschluss --- # --- Abschluss ---
end_time = time.time(); duration = end_time - start_time end_time = time.time()
debug_print(f"Verarbeitung abgeschlossen um {datetime.now().strftime('%H:%M:%S')}.") duration = end_time - start_time
debug_print(f"Gesamtdauer: {duration:.2f} Sekunden.") logging.info(f"Verarbeitung abgeschlossen um {datetime.now().strftime('%H:%M:%S')}.")
debug_print(f"===== Skript beendet =====") logging.info(f"Gesamtdauer: {duration:.2f} Sekunden.")
if LOG_FILE: logging.info(f"===== Skript beendet =====")
try:
# 'with' startet in der nächsten Zeile
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ===== Skript wirklich beendet =====\n")
except Exception as e:
# Optional: Warnung ausgeben, wenn das finale Schreiben fehlschlägt
print(f"[WARNUNG] Konnte letzte Log-Nachricht nicht schreiben: {e}")
pass # Programm soll trotzdem normal beenden
# --- ENDE KORRIGIERTER BLOCK ---
print(f"Verarbeitung abgeschlossen. Logfile: {LOG_FILE}") # Schließe Logging Handler explizit (optional, aber sauber)
logging.shutdown()
print(f"\nVerarbeitung abgeschlossen. Logfile: {LOG_FILE}")
# Führt die main-Funktion aus, wenn das Skript direkt gestartet wird # Führt die main-Funktion aus, wenn das Skript direkt gestartet wird