This commit is contained in:
2025-04-23 12:27:01 +00:00
parent 9ae750d6bd
commit 552fb3e372

View File

@@ -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()