diff --git a/data_processor.py b/data_processor.py index 14f870a2..f3282c30 100644 --- a/data_processor.py +++ b/data_processor.py @@ -31,11 +31,19 @@ from imblearn.pipeline import Pipeline as ImbPipeline # Import der abhängigen Module from config import Config, COLUMN_MAP, MODEL_FILE, IMPUTER_FILE, PATTERNS_FILE_JSON from helpers import ( - serp_website_lookup, get_website_raw, scrape_website_details, summarize_website_content, - URL_CHECK_MARKER, evaluate_branche_chatgpt, get_numeric_filter_value, - summarize_batch_openai, load_target_schema, ALLOWED_TARGET_BRANCHES, - serp_wikipedia_lookup, search_linkedin_contacts, is_valid_wikipedia_article_url -) + serp_website_lookup, + get_website_raw, + scrape_website_details, + summarize_website_content, + URL_CHECK_MARKER, + evaluate_branche_chatgpt, + get_numeric_filter_value, + summarize_batch_openai, + load_target_schema, + ALLOWED_TARGET_BRANCHES, + serp_wikipedia_lookup, + search_linkedin_contacts, + is_valid_wikipedia_article_url) # Klassen-Imports from google_sheet_handler import GoogleSheetHandler from wikipedia_scraper import WikipediaScraper @@ -45,6 +53,7 @@ class DataProcessor: """ Zentrale Klasse zur Orchestrierung und Verarbeitung von Unternehmensdaten. """ + def __init__(self, sheet_handler, wiki_scraper): """ Initialisiert den DataProcessor mit Instanzen von Handler-Klassen. @@ -53,11 +62,15 @@ class DataProcessor: self.logger.info("Initialisiere DataProcessor...") if not isinstance(sheet_handler, GoogleSheetHandler): - self.logger.critical("DataProcessor Init FEHLER: Kein gueltiger GoogleSheetHandler uebergeben!") - raise ValueError("DataProcessor benoetigt eine gueltige GoogleSheetHandler Instanz.") + self.logger.critical( + "DataProcessor Init FEHLER: Kein gueltiger GoogleSheetHandler uebergeben!") + raise ValueError( + "DataProcessor benoetigt eine gueltige GoogleSheetHandler Instanz.") if not isinstance(wiki_scraper, WikipediaScraper): - self.logger.critical("DataProcessor Init FEHLER: Kein gueltiger WikipediaScraper uebergeben!") - raise ValueError("DataProcessor benoetigt eine gueltige WikipediaScraper Instanz.") + self.logger.critical( + "DataProcessor Init FEHLER: Kein gueltiger WikipediaScraper uebergeben!") + raise ValueError( + "DataProcessor benoetigt eine gueltige WikipediaScraper Instanz.") self.sheet_handler = sheet_handler self.wiki_scraper = wiki_scraper @@ -84,12 +97,14 @@ class DataProcessor: """ idx = COLUMN_MAP.get(column_key) if idx is None: - self.logger.error(f"_get_cell_value_safe: Schluessel '{column_key}' nicht in COLUMN_MAP gefunden.") + self.logger.error( + f"_get_cell_value_safe: Schluessel '{column_key}' nicht in COLUMN_MAP gefunden.") return '' if len(row) > idx: return row[idx] if row[idx] is not None else '' else: - self.logger.debug(f"_get_cell_value_safe: Index {idx} fuer '{column_key}' ist gueltig, aber Zeile ist zu kurz (Laenge {len(row)}).") + self.logger.debug( + f"_get_cell_value_safe: Index {idx} fuer '{column_key}' ist gueltig, aber Zeile ist zu kurz (Laenge {len(row)}).") return '' def _needs_website_processing(self, row_data, force_reeval): @@ -98,7 +113,8 @@ class DataProcessor: """ if force_reeval: return True - at_value = self._get_cell_value_safe(row_data, "Website Scrape Timestamp").strip() + at_value = self._get_cell_value_safe( + row_data, "Website Scrape Timestamp").strip() if not at_value: return True return False @@ -109,34 +125,43 @@ class DataProcessor: """ if force_reeval: return True - an_value = self._get_cell_value_safe(row_data, "Wikipedia Timestamp").strip() + an_value = self._get_cell_value_safe( + row_data, "Wikipedia Timestamp").strip() if not an_value: return True - s_value = self._get_cell_value_safe(row_data, "Chat Wiki Konsistenzpruefung").strip().upper() + s_value = self._get_cell_value_safe( + row_data, "Chat Wiki Konsistenzpruefung").strip().upper() if s_value == "X (URL COPIED)": - return True + return True return False def _needs_wiki_verification(self, row_data, force_reeval): - """ - Prueft, ob Wikipedia-Verifizierung fuer diese Zeile noetig ist. - """ - if force_reeval: - return True - ax_value = self._get_cell_value_safe(row_data, "Wiki Verif. Timestamp").strip() - if not ax_value: - m_value = self._get_cell_value_safe(row_data, "Wiki URL").strip() - if m_value and m_value.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"]: - return True - return False + """ + Prueft, ob Wikipedia-Verifizierung fuer diese Zeile noetig ist. + """ + if force_reeval: + return True + ax_value = self._get_cell_value_safe( + row_data, "Wiki Verif. Timestamp").strip() + if not ax_value: + m_value = self._get_cell_value_safe(row_data, "Wiki URL").strip() + if m_value and m_value.lower() not in [ + "k.a.", "kein artikel gefunden", "fehler bei suche", "http:"]: + return True + return False - def _needs_chat_evaluations(self, row_data, force_reeval, wiki_data_just_updated): + def _needs_chat_evaluations( + self, + row_data, + force_reeval, + wiki_data_just_updated): """ Prueft, ob ChatGPT-Evaluationen fuer diese Zeile noetig sind. """ if force_reeval: return True - ao_value = self._get_cell_value_safe(row_data, "Timestamp letzte Pruefung").strip() + ao_value = self._get_cell_value_safe( + row_data, "Timestamp letzte Pruefung").strip() if not ao_value: return True if wiki_data_just_updated: @@ -144,28 +169,38 @@ class DataProcessor: return False def _needs_ml_prediction(self, row_data, force_reeval, chat_eval_just_ran): - """ - Prueft, ob die ML-Schaetzung fuer diese Zeile noetig ist. - """ - if force_reeval: - return True - au_value = self._get_cell_value_safe(row_data, "Geschaetzter Techniker Bucket").strip() - if not au_value or au_value.lower() in ["k.a.", "fehler schaetzung"]: - if chat_eval_just_ran: - return True - av_value = self._get_cell_value_safe(row_data, "Finaler Umsatz (Wiki>CRM)").strip() - aw_value = self._get_cell_value_safe(row_data, "Finaler Mitarbeiter (Wiki>CRM)").strip() - if av_value != "k.A." and not av_value.startswith("FEHLER") and aw_value != "k.A." and not aw_value.startswith("FEHLER"): - return True - return False + """ + Prueft, ob die ML-Schaetzung fuer diese Zeile noetig ist. + """ + if force_reeval: + return True + au_value = self._get_cell_value_safe( + row_data, "Geschaetzter Techniker Bucket").strip() + if not au_value or au_value.lower() in ["k.a.", "fehler schaetzung"]: + if chat_eval_just_ran: + return True + av_value = self._get_cell_value_safe( + row_data, "Finaler Umsatz (Wiki>CRM)").strip() + aw_value = self._get_cell_value_safe( + row_data, "Finaler Mitarbeiter (Wiki>CRM)").strip() + if av_value != "k.A." and not av_value.startswith( + "FEHLER") and aw_value != "k.A." and not aw_value.startswith("FEHLER"): + return True + return False - def _process_single_row(self, row_num_in_sheet, row_data, - steps_to_run, force_reeval=False, clear_x_flag=False): + def _process_single_row( + self, + row_num_in_sheet, + row_data, + steps_to_run, + force_reeval=False, + clear_x_flag=False): """ Verarbeitet die Daten fuer eine einzelne Zeile im Sheet. Fuehrt ausgewaehlte Anreicherungs- und Analyseprozesse durch. """ - self.logger.info(f"--- Starte Verarbeitung fuer Zeile {row_num_in_sheet} {'(Re-Eval)' if force_reeval else ''} (Schritte: {', '.join(steps_to_run) if steps_to_run else 'Keine'}) ---") + self.logger.info( + f"--- Starte Verarbeitung fuer Zeile {row_num_in_sheet} {'(Re-Eval)' if force_reeval else ''} (Schritte: {', '.join(steps_to_run) if steps_to_run else 'Keine'}) ---") updates = [] now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") any_processing_done = False @@ -174,11 +209,16 @@ class DataProcessor: # --- Initiale Werte lesen --- company_name = self._get_cell_value_safe(row_data, "CRM Name").strip() - website_url = self._get_cell_value_safe(row_data, "CRM Website").strip() - crm_kurzform = self._get_cell_value_safe(row_data, "CRM Kurzform").strip() - crm_branche = self._get_cell_value_safe(row_data, "CRM Branche").strip() - crm_beschreibung = self._get_cell_value_safe(row_data, "CRM Beschreibung").strip() - parent_account_name_d = self._get_cell_value_safe(row_data, "Parent Account Name").strip() + website_url = self._get_cell_value_safe( + row_data, "CRM Website").strip() + crm_kurzform = self._get_cell_value_safe( + row_data, "CRM Kurzform").strip() + crm_branche = self._get_cell_value_safe( + row_data, "CRM Branche").strip() + crm_beschreibung = self._get_cell_value_safe( + row_data, "CRM Beschreibung").strip() + parent_account_name_d = self._get_cell_value_safe( + row_data, "Parent Account Name").strip() current_wiki_data = { 'url': self._get_cell_value_safe(row_data, "Wiki URL") or 'k.A.', @@ -192,36 +232,51 @@ class DataProcessor: } final_wiki_data = current_wiki_data.copy() - website_raw = self._get_cell_value_safe(row_data, "Website Rohtext") or 'k.A.' - website_summary = self._get_cell_value_safe(row_data, "Website Zusammenfassung") or 'k.A.' - website_meta_details = self._get_cell_value_safe(row_data, "Website Meta-Details") or 'k.A.' - url_pruefstatus = self._get_cell_value_safe(row_data, "URL Prüfstatus") or '' + website_raw = self._get_cell_value_safe( + row_data, "Website Rohtext") or 'k.A.' + website_summary = self._get_cell_value_safe( + row_data, "Website Zusammenfassung") or 'k.A.' + website_meta_details = self._get_cell_value_safe( + row_data, "Website Meta-Details") or 'k.A.' + url_pruefstatus = self._get_cell_value_safe( + row_data, "URL Prüfstatus") or '' # --- 1. Website Handling (Lookup, Scraping, Summarization, Meta) --- run_website_step = 'web' in steps_to_run - website_processing_needed = self._needs_website_processing(row_data, force_reeval) + website_processing_needed = self._needs_website_processing( + row_data, force_reeval) if run_website_step and website_processing_needed: any_processing_done = True grund_message = "Re-Eval" if force_reeval else "Timestamp (AJ) leer" - self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre WEBSITE Schritte aus (Grund: {grund_message})...") + self.logger.info( + f"Zeile {row_num_in_sheet}: Fuehre WEBSITE Schritte aus (Grund: {grund_message})...") if not website_url or website_url.lower() == "k.a.": - self.logger.debug(" -> Website URL (E) leer, suche ueber SERP...") + self.logger.debug( + " -> Website URL (E) leer, suche ueber SERP...") try: new_website = serp_website_lookup(company_name) - if new_website and new_website.lower() != "k.a." and not new_website.startswith("k.A. (Fehler"): + if new_website and new_website.lower( + ) != "k.a." and not new_website.startswith("k.A. (Fehler"): website_url = new_website - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["CRM Website"] + 1)}{row_num_in_sheet}', 'values': [[website_url]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["CRM Website"] + 1)}{row_num_in_sheet}', + 'values': [ + [website_url]]}) url_pruefstatus = "URL_OK_SERP" else: - url_pruefstatus = "URL_SERP_FAILED" if not website_url or website_url.lower() == "k.a." else url_pruefstatus + url_pruefstatus = "URL_SERP_FAILED" if not website_url or website_url.lower( + ) == "k.a." else url_pruefstatus except Exception as e_serp_lookup_web: - self.logger.error(f"FEHLER bei SERP Website Lookup für '{company_name}': {e_serp_lookup_web}") + self.logger.error( + f"FEHLER bei SERP Website Lookup für '{company_name}': {e_serp_lookup_web}") url_pruefstatus = "URL_SERP_ERROR" if website_url and website_url.lower() not in ["k.a.", "http:"]: - self.logger.debug(f" -> Scrape Rohtext & Meta von {website_url[:100]}...") + self.logger.debug( + f" -> Scrape Rohtext & Meta von {website_url[:100]}...") try: website_raw = get_website_raw(website_url) if website_raw == URL_CHECK_MARKER: @@ -229,51 +284,115 @@ class DataProcessor: website_summary, website_meta_details = "k.A. (URL prüfen)", "k.A. (URL prüfen)" elif website_raw and str(website_raw).strip().lower() not in ["k.a.", "k.a. (nur cookie-banner erkannt)", "k.a. (fehler)"]: url_pruefstatus = "URL_OK_SCRAPED" - website_meta_details = scrape_website_details(website_url) or "k.A. (Keine Meta-Details)" - website_summary = summarize_website_content(website_raw) or "k.A. (Keine Zusammenfassung erhalten)" - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Meta-Details"] + 1)}{row_num_in_sheet}', 'values': [[website_meta_details]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [[website_summary]]}) + website_meta_details = scrape_website_details( + website_url) or "k.A. (Keine Meta-Details)" + website_summary = summarize_website_content( + website_raw) or "k.A. (Keine Zusammenfassung erhalten)" + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Meta-Details"] + 1)}{row_num_in_sheet}', + 'values': [ + [website_meta_details]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', + 'values': [ + [website_summary]]}) else: if not str(website_raw).startswith("k.A. (Fehler"): url_pruefstatus = "URL_SCRAPE_EMPTY_OR_BANNER" website_summary, website_meta_details = "k.A.", "k.A." - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [[website_summary]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Meta-Details"] + 1)}{row_num_in_sheet}', 'values': [[website_meta_details]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', + 'values': [ + [website_summary]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Meta-Details"] + 1)}{row_num_in_sheet}', + 'values': [ + [website_meta_details]]}) except Exception as e_scrape_web: - self.logger.error(f"FEHLER beim Website Scraping für '{company_name}': {e_scrape_web}") + self.logger.error( + f"FEHLER beim Website Scraping für '{company_name}': {e_scrape_web}") website_raw, website_summary, website_meta_details = f"k.A. (Fehler Scraping: {str(e_scrape_web)[:50]})", "k.A.", "k.A." url_pruefstatus = "URL_SCRAPE_ERROR" - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [[website_summary]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Meta-Details"] + 1)}{row_num_in_sheet}', 'values': [[website_meta_details]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [[website_raw]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', + 'values': [ + [website_summary]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Meta-Details"] + 1)}{row_num_in_sheet}', + 'values': [ + [website_meta_details]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', + 'values': [ + [website_raw]]}) else: - self.logger.debug(f" -> Keine gültige Website URL für '{company_name}'. Web-Verarbeitung übersprungen.") + self.logger.debug( + f" -> Keine gültige Website URL für '{company_name}'. Web-Verarbeitung übersprungen.") website_raw, website_summary, website_meta_details = "k.A. (Keine URL)", "k.A.", "k.A." - if not url_pruefstatus: url_pruefstatus = "URL_MISSING" - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [[website_raw]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [[website_summary]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Meta-Details"] + 1)}{row_num_in_sheet}', 'values': [[website_meta_details]]}) + if not url_pruefstatus: + url_pruefstatus = "URL_MISSING" + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', + 'values': [ + [website_raw]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', + 'values': [ + [website_summary]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Meta-Details"] + 1)}{row_num_in_sheet}', + 'values': [ + [website_meta_details]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["URL Prüfstatus"] + 1)}{row_num_in_sheet}', 'values': [[url_pruefstatus]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Scrape Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["URL Prüfstatus"] + 1)}{row_num_in_sheet}', + 'values': [ + [url_pruefstatus]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Scrape Timestamp"] + 1)}{row_num_in_sheet}', + 'values': [ + [now_timestamp]]}) # ====================================================================== - # === 2. Wikipedia Handling (Search, Extraction, Status Reset) ========== + # === 2. Wikipedia Handling (Search, Extraction, Status Reset) ======== # ====================================================================== run_wiki_step = 'wiki' in steps_to_run - wiki_processing_needed = self._needs_wiki_processing(row_data, force_reeval) + wiki_processing_needed = self._needs_wiki_processing( + row_data, force_reeval) if run_wiki_step and wiki_processing_needed: any_processing_done = True grund_message_parts_wiki = [] - if force_reeval: grund_message_parts_wiki.append('Re-Eval') - if not self._get_cell_value_safe(row_data, "Wikipedia Timestamp").strip(): grund_message_parts_wiki.append('Z leer') - if self._get_cell_value_safe(row_data, "Chat Wiki Konsistenzpruefung").strip().upper() == "X (URL COPIED)": grund_message_parts_wiki.append("AC='X (URL COPIED)'") - grund_message_wiki = ", ".join(filter(None, grund_message_parts_wiki)) or "Bedingung erfüllt" - self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre WIKI Schritte aus (Grund: {grund_message_wiki})...") + if force_reeval: + grund_message_parts_wiki.append('Re-Eval') + if not self._get_cell_value_safe( + row_data, "Wikipedia Timestamp").strip(): + grund_message_parts_wiki.append('Z leer') + if self._get_cell_value_safe( + row_data, + "Chat Wiki Konsistenzpruefung").strip().upper() == "X (URL COPIED)": + grund_message_parts_wiki.append("AC='X (URL COPIED)'") + grund_message_wiki = ", ".join( + filter(None, grund_message_parts_wiki)) or "Bedingung erfüllt" + self.logger.info( + f"Zeile {row_num_in_sheet}: Fuehre WIKI Schritte aus (Grund: {grund_message_wiki})...") - current_wiki_url_r = self._get_cell_value_safe(row_data, "Wiki URL").strip() - system_suggested_parent_o = self._get_cell_value_safe(row_data, "System Vorschlag Parent Account").strip() + current_wiki_url_r = self._get_cell_value_safe( + row_data, "Wiki URL").strip() + system_suggested_parent_o = self._get_cell_value_safe( + row_data, "System Vorschlag Parent Account").strip() url_for_extraction = None source_of_wiki_data_origin_log_msg = "Tochter (Initial)" @@ -281,123 +400,269 @@ class DataProcessor: if not current_wiki_url_r or current_wiki_url_r.lower() == 'k.a.': if parent_account_name_d and parent_account_name_d.lower() != 'k.a.': - self.logger.info(f" Zeile {row_num_in_sheet}: R leer, D ('{parent_account_name_d}') gesetzt. Suche Wiki für Parent D.") + self.logger.info( + f" Zeile {row_num_in_sheet}: R leer, D ('{parent_account_name_d}') gesetzt. Suche Wiki für Parent D.") try: - potential_url = serp_wikipedia_lookup(parent_account_name_d) - if potential_url and not str(potential_url).startswith("FEHLER"): + potential_url = serp_wikipedia_lookup( + parent_account_name_d) + if potential_url and not str( + potential_url).startswith("FEHLER"): url_for_extraction = potential_url source_of_wiki_data_origin_log_msg = f"Parent D ('{parent_account_name_d}')" - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Wiki Konsistenzpruefung"] + 1)}{row_num_in_sheet}', 'values': [["INFO_PARENT_AUS_D"]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Wiki Konsistenzpruefung"] + 1)}{row_num_in_sheet}', + 'values': [ + ["INFO_PARENT_AUS_D"]]}) additional_info_for_af_col = f"INFO: Wiki-URL von Parent (D): {parent_account_name_d}. " else: additional_info_for_af_col = f"WARN: Kein Wiki für Parent D '{parent_account_name_d}' gefunden. " except Exception as e_d_lookup: - self.logger.error(f"Fehler bei Wiki-Suche für Parent D '{parent_account_name_d}': {e_d_lookup}") + self.logger.error( + f"Fehler bei Wiki-Suche für Parent D '{parent_account_name_d}': {e_d_lookup}") additional_info_for_af_col = f"ERR: Suche Parent D fehlgeschlagen. " if url_for_extraction is None and system_suggested_parent_o and system_suggested_parent_o.lower() != 'k.a.': - self.logger.info(f" Zeile {row_num_in_sheet}: R leer, D nicht erfolgreich. O ('{system_suggested_parent_o}') gesetzt. Suche Wiki für Parent O.") + self.logger.info( + f" Zeile {row_num_in_sheet}: R leer, D nicht erfolgreich. O ('{system_suggested_parent_o}') gesetzt. Suche Wiki für Parent O.") try: - potential_url = serp_wikipedia_lookup(system_suggested_parent_o) - if potential_url and not str(potential_url).startswith("FEHLER"): + potential_url = serp_wikipedia_lookup( + system_suggested_parent_o) + if potential_url and not str( + potential_url).startswith("FEHLER"): url_for_extraction = potential_url source_of_wiki_data_origin_log_msg = f"Parent O ('{system_suggested_parent_o}')" - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Wiki Konsistenzpruefung"] + 1)}{row_num_in_sheet}', 'values': [["INFO_PARENT_AUS_O"]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Wiki Konsistenzpruefung"] + 1)}{row_num_in_sheet}', + 'values': [ + ["INFO_PARENT_AUS_O"]]}) additional_info_for_af_col += f"INFO: Wiki-URL von Parent (O): {system_suggested_parent_o}. " else: additional_info_for_af_col += f"WARN: Kein Wiki für Parent O '{system_suggested_parent_o}' gefunden. " except Exception as e_o_lookup: - self.logger.error(f"Fehler bei Wiki-Suche für Parent O '{system_suggested_parent_o}': {e_o_lookup}") + self.logger.error( + f"Fehler bei Wiki-Suche für Parent O '{system_suggested_parent_o}': {e_o_lookup}") additional_info_for_af_col += f"ERR: Suche Parent O fehlgeschlagen. " if url_for_extraction is None: search_for_daughter_needed = False - status_ac_reparse = self._get_cell_value_safe(row_data, "Chat Wiki Konsistenzpruefung").strip().upper() == "X (URL COPIED)" - ts_z_empty = not self._get_cell_value_safe(row_data, "Wikipedia Timestamp").strip() + status_ac_reparse = self._get_cell_value_safe( + row_data, "Chat Wiki Konsistenzpruefung").strip().upper() == "X (URL COPIED)" + ts_z_empty = not self._get_cell_value_safe( + row_data, "Wikipedia Timestamp").strip() r_url_valid_looking = current_wiki_url_r and "wikipedia.org/wiki/" in current_wiki_url_r.lower() if status_ac_reparse or force_reeval or ts_z_empty or not r_url_valid_looking: - if r_url_valid_looking and not (status_ac_reparse or force_reeval): - self.logger.info(f" Zeile {row_num_in_sheet}: Nutze vorhandene Tochter-URL (R): {current_wiki_url_r}") + if r_url_valid_looking and not ( + status_ac_reparse or force_reeval): + self.logger.info( + f" Zeile {row_num_in_sheet}: Nutze vorhandene Tochter-URL (R): {current_wiki_url_r}") url_for_extraction = current_wiki_url_r source_of_wiki_data_origin_log_msg = "Tochter (aus R)" else: - self.logger.info(f" Zeile {row_num_in_sheet}: Starte neue Suche für Tochter '{company_name}'.") + self.logger.info( + f" Zeile {row_num_in_sheet}: Starte neue Suche für Tochter '{company_name}'.") search_for_daughter_needed = True if search_for_daughter_needed: try: - page_obj = self.wiki_scraper.search_company_article(company_name, website_url) + page_obj = self.wiki_scraper.search_company_article( + company_name, website_url) if page_obj: url_for_extraction = page_obj.url source_of_wiki_data_origin_log_msg = "Tochter (Suche erfolgreich)" else: url_for_extraction = "Kein Artikel gefunden" except Exception as e_tochter_suche: - self.logger.error(f"Fehler bei Wiki-Suche für Tochter '{company_name}': {e_tochter_suche}") + self.logger.error( + f"Fehler bei Wiki-Suche für Tochter '{company_name}': {e_tochter_suche}") url_for_extraction = f"Fehler Suche Tochter: {str(e_tochter_suche)[:50]}" - if url_for_extraction and isinstance(url_for_extraction, str) and url_for_extraction.lower() not in ["k.a.", "kein artikel gefunden"] and not url_for_extraction.startswith("FEHLER"): - self.logger.info(f" -> Extrahiere Wiki-Daten von URL ({source_of_wiki_data_origin_log_msg}): {url_for_extraction[:100]}...") + if url_for_extraction and isinstance( + url_for_extraction, + str) and url_for_extraction.lower() not in [ + "k.a.", + "kein artikel gefunden"] and not url_for_extraction.startswith("FEHLER"): + self.logger.info( + f" -> Extrahiere Wiki-Daten von URL ({source_of_wiki_data_origin_log_msg}): {url_for_extraction[:100]}...") try: - extracted_data = self.wiki_scraper.extract_company_data(url_for_extraction) + extracted_data = self.wiki_scraper.extract_company_data( + url_for_extraction) if extracted_data and extracted_data.get('url') != 'k.A.': final_wiki_data = extracted_data wiki_data_updated_in_this_run = True - current_ac_val = self._get_cell_value_safe(row_data, "Chat Wiki Konsistenzpruefung").strip() - if source_of_wiki_data_origin_log_msg.startswith("Parent") and current_ac_val not in ["INFO_PARENT_AUS_D", "INFO_PARENT_AUS_O"]: - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Verif. Timestamp"] + 1)}{row_num_in_sheet}', 'values': [['']]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Wiki Inkonsistenz"] + 1)}{row_num_in_sheet}', 'values': [['']]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Wiki Artikel"] + 1)}{row_num_in_sheet}', 'values': [['']]}) + current_ac_val = self._get_cell_value_safe( + row_data, "Chat Wiki Konsistenzpruefung").strip() + if source_of_wiki_data_origin_log_msg.startswith("Parent") and current_ac_val not in [ + "INFO_PARENT_AUS_D", "INFO_PARENT_AUS_O"]: + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Verif. Timestamp"] + 1)}{row_num_in_sheet}', + 'values': [ + ['']]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Wiki Inkonsistenz"] + 1)}{row_num_in_sheet}', + 'values': [ + ['']]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Wiki Artikel"] + 1)}{row_num_in_sheet}', + 'values': [ + ['']]}) elif not source_of_wiki_data_origin_log_msg.startswith("Parent"): - self.logger.info(f" -> Setze AC auf '?' für Tochter-Wiki-Update.") - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Wiki Konsistenzpruefung"] + 1)}{row_num_in_sheet}', 'values': [['?']]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Verif. Timestamp"] + 1)}{row_num_in_sheet}', 'values': [['']]}) + self.logger.info( + f" -> Setze AC auf '?' für Tochter-Wiki-Update.") + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Wiki Konsistenzpruefung"] + 1)}{row_num_in_sheet}', + 'values': [ + ['?']]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Verif. Timestamp"] + 1)}{row_num_in_sheet}', + 'values': [ + ['']]}) else: final_wiki_data['url'] = url_for_extraction - for key in ['sitz_stadt', 'sitz_land', 'first_paragraph', 'branche', 'umsatz', 'mitarbeiter', 'categories']: + for key in [ + 'sitz_stadt', + 'sitz_land', + 'first_paragraph', + 'branche', + 'umsatz', + 'mitarbeiter', + 'categories']: final_wiki_data[key] = 'k.A. (Extraktion fehlgeschlagen)' wiki_data_updated_in_this_run = True except Exception as e_extract: - self.logger.error(f"FEHLER bei Wikipedia Datenextraktion von {url_for_extraction[:100]}...: {e_extract}") + self.logger.error( + f"FEHLER bei Wikipedia Datenextraktion von {url_for_extraction[:100]}...: {e_extract}") final_wiki_data['url'] = url_for_extraction - for key in ['sitz_stadt', 'sitz_land', 'first_paragraph', 'branche', 'umsatz', 'mitarbeiter', 'categories']: + for key in [ + 'sitz_stadt', + 'sitz_land', + 'first_paragraph', + 'branche', + 'umsatz', + 'mitarbeiter', + 'categories']: final_wiki_data[key] = 'k.A. (FEHLER Extr.)' wiki_data_updated_in_this_run = True elif url_for_extraction: final_wiki_data['url'] = url_for_extraction - for key in ['sitz_stadt', 'sitz_land', 'first_paragraph', 'branche', 'umsatz', 'mitarbeiter', 'categories']: + for key in [ + 'sitz_stadt', + 'sitz_land', + 'first_paragraph', + 'branche', + 'umsatz', + 'mitarbeiter', + 'categories']: final_wiki_data[key] = 'k.A.' wiki_data_updated_in_this_run = True if wiki_data_updated_in_this_run: - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki URL"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('url', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Sitz Stadt"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('sitz_stadt', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Sitz Land"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('sitz_land', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Absatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('first_paragraph', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Branche"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('branche', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('umsatz', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('mitarbeiter', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Kategorien"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('categories', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wikipedia Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki URL"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + final_wiki_data.get( + 'url', + 'k.A.')]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Sitz Stadt"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + final_wiki_data.get( + 'sitz_stadt', + 'k.A.')]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Sitz Land"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + final_wiki_data.get( + 'sitz_land', + 'k.A.')]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Absatz"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + final_wiki_data.get( + 'first_paragraph', + 'k.A.')]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Branche"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + final_wiki_data.get( + 'branche', + 'k.A.')]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Umsatz"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + final_wiki_data.get( + 'umsatz', + 'k.A.')]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Mitarbeiter"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + final_wiki_data.get( + 'mitarbeiter', + 'k.A.')]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Kategorien"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + final_wiki_data.get( + 'categories', + 'k.A.')]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wikipedia Timestamp"] + 1)}{row_num_in_sheet}', + 'values': [ + [now_timestamp]]}) if additional_info_for_af_col: - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Begruendung bei Abweichung"] + 1)}{row_num_in_sheet}', 'values': [[additional_info_for_af_col]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Begruendung bei Abweichung"] + 1)}{row_num_in_sheet}', + 'values': [ + [additional_info_for_af_col]]}) if source_of_wiki_data_origin_log_msg.startswith("Parent"): - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["SerpAPI Wiki Search Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["SerpAPI Wiki Search Timestamp"] + 1)}{row_num_in_sheet}', + 'values': [ + [now_timestamp]]}) # --- 3. ChatGPT Evaluationen (Branch, FSM, etc.) & Plausi --- run_chat_step = 'chat' in steps_to_run - chat_processing_needed = self._needs_chat_evaluations(row_data, force_reeval, wiki_data_updated_in_this_run) + chat_processing_needed = self._needs_chat_evaluations( + row_data, force_reeval, wiki_data_updated_in_this_run) if run_chat_step and chat_processing_needed: any_processing_done = True chat_eval_just_ran = True - grund_message_chat = "Re-Eval" if force_reeval else ("Wiki-Daten aktualisiert" if wiki_data_updated_in_this_run else "Timestamp (BN) leer") - self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre CHATGPT Evaluationen & Plausi aus (Grund: {grund_message_chat})...") + grund_message_chat = "Re-Eval" if force_reeval else ( + "Wiki-Daten aktualisiert" if wiki_data_updated_in_this_run else "Timestamp (BN) leer") + self.logger.info( + f"Zeile {row_num_in_sheet}: Fuehre CHATGPT Evaluationen & Plausi aus (Grund: {grund_message_chat})...") # 3a. Branchen-Einstufung - self.logger.info(f" Zeile {row_num_in_sheet}: Starte Branchen-Einstufung ueber ChatGPT...") + self.logger.info( + f" Zeile {row_num_in_sheet}: Starte Branchen-Einstufung ueber ChatGPT...") try: branch_result = evaluate_branche_chatgpt( crm_branche, @@ -406,175 +671,352 @@ class DataProcessor: final_wiki_data.get('categories', 'k.A.'), website_summary ) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get("branch", "FEHLER BRANCH")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Branche Konfidenz"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get("confidence", "N/A CONF")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenz Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get("consistency", "error CONS")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get("justification", "No JUST")]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + branch_result.get( + "branch", + "FEHLER BRANCH")]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Branche Konfidenz"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + branch_result.get( + "confidence", + "N/A CONF")]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenz Branche"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + branch_result.get( + "consistency", + "error CONS")]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Branche"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + branch_result.get( + "justification", + "No JUST")]]}) except Exception as e_branch_eval: - self.logger.error(f"FEHLER bei Branchen-Einstufung für Zeile {row_num_in_sheet}: {e_branch_eval}") + self.logger.error( + f"FEHLER bei Branchen-Einstufung für Zeile {row_num_in_sheet}: {e_branch_eval}") # 3b, 3c, 3d: Weitere ChatGPT-Evaluationen (hier nicht detailliert implementiert, aber Platzhalter) # ... Logik für FSM-Relevanz, Mitarbeiter-Schätzung, Umsatz-Schätzung, etc. ... # 3e. Konsolidierung Umsatz/Mitarbeiter (BD, BE) - self.logger.debug(f" Zeile {row_num_in_sheet}: Konsolidiere Umsatz (BD) und Mitarbeiter (BE)...") + self.logger.debug( + f" Zeile {row_num_in_sheet}: Konsolidiere Umsatz (BD) und Mitarbeiter (BE)...") final_umsatz_str_konsolidiert = "k.A." final_ma_str_konsolidiert = "k.A." try: - crm_umsatz_val_str = self._get_cell_value_safe(row_data, "CRM Umsatz") + crm_umsatz_val_str = self._get_cell_value_safe( + row_data, "CRM Umsatz") wiki_umsatz_val_str = final_wiki_data.get('umsatz', 'k.A.') - crm_ma_val_str = self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter") + crm_ma_val_str = self._get_cell_value_safe( + row_data, "CRM Anzahl Mitarbeiter") wiki_ma_val_str = final_wiki_data.get('mitarbeiter', 'k.A.') - num_crm_umsatz = get_numeric_filter_value(crm_umsatz_val_str, is_umsatz=True) - num_wiki_umsatz = get_numeric_filter_value(wiki_umsatz_val_str, is_umsatz=True) - num_crm_ma = get_numeric_filter_value(crm_ma_val_str, is_umsatz=False) - num_wiki_ma = get_numeric_filter_value(wiki_ma_val_str, is_umsatz=False) + num_crm_umsatz = get_numeric_filter_value( + crm_umsatz_val_str, is_umsatz=True) + num_wiki_umsatz = get_numeric_filter_value( + wiki_umsatz_val_str, is_umsatz=True) + num_crm_ma = get_numeric_filter_value( + crm_ma_val_str, is_umsatz=False) + num_wiki_ma = get_numeric_filter_value( + wiki_ma_val_str, is_umsatz=False) if parent_account_name_d and parent_account_name_d.lower() != 'k.a.': - self.logger.info(f" -> Parent D ('{parent_account_name_d}') gesetzt. Konsolidiere primär mit CRM-Daten der Tochter.") + self.logger.info( + f" -> Parent D ('{parent_account_name_d}') gesetzt. Konsolidiere primär mit CRM-Daten der Tochter.") final_num_umsatz = num_crm_umsatz if num_crm_umsatz > 0 else num_wiki_umsatz final_num_ma = num_crm_ma if num_crm_ma > 0 else num_wiki_ma else: - self.logger.debug(f" -> Parent D leer. Standardkonsolidierung Wiki > CRM.") + self.logger.debug( + f" -> Parent D leer. Standardkonsolidierung Wiki > CRM.") final_num_umsatz = num_wiki_umsatz if num_wiki_umsatz > 0 else num_crm_umsatz final_num_ma = num_wiki_ma if num_wiki_ma > 0 else num_crm_ma - final_umsatz_str_konsolidiert = str(int(round(final_num_umsatz))) if final_num_umsatz > 0 else 'k.A.' - final_ma_str_konsolidiert = str(int(round(final_num_ma))) if final_num_ma > 0 else 'k.A.' - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [[final_umsatz_str_konsolidiert]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [[final_ma_str_konsolidiert]]}) + final_umsatz_str_konsolidiert = str( + int(round(final_num_umsatz))) if final_num_umsatz > 0 else 'k.A.' + final_ma_str_konsolidiert = str( + int(round(final_num_ma))) if final_num_ma > 0 else 'k.A.' + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_in_sheet}', + 'values': [ + [final_umsatz_str_konsolidiert]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_in_sheet}', + 'values': [ + [final_ma_str_konsolidiert]]}) except Exception as e_consolidate: - self.logger.error(f"FEHLER bei Konsolidierung (BD/BE) für Zeile {row_num_in_sheet}: {e_consolidate}") + self.logger.error( + f"FEHLER bei Konsolidierung (BD/BE) für Zeile {row_num_in_sheet}: {e_consolidate}") final_umsatz_str_konsolidiert, final_ma_str_konsolidiert = "FEHLER_KONSO", "FEHLER_KONSO" - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER_KONSO']]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER_KONSO']]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_in_sheet}', + 'values': [ + ['FEHLER_KONSO']]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_in_sheet}', + 'values': [ + ['FEHLER_KONSO']]}) # 3f. Plausibilitäts-Checks (BG-BM) - self.logger.debug(f" Zeile {row_num_in_sheet}: Führe Plausibilitäts-Checks durch...") + self.logger.debug( + f" Zeile {row_num_in_sheet}: Führe Plausibilitäts-Checks durch...") try: plausi_input_data = { "Finaler Umsatz (Wiki>CRM)": final_umsatz_str_konsolidiert, "Finaler Mitarbeiter (Wiki>CRM)": final_ma_str_konsolidiert, - "CRM Umsatz": self._get_cell_value_safe(row_data, "CRM Umsatz"), - "Wiki Umsatz": final_wiki_data.get('umsatz', 'k.A.'), - "CRM Anzahl Mitarbeiter": self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter"), - "Wiki Mitarbeiter": final_wiki_data.get('mitarbeiter', 'k.A.'), + "CRM Umsatz": self._get_cell_value_safe( + row_data, + "CRM Umsatz"), + "Wiki Umsatz": final_wiki_data.get( + 'umsatz', + 'k.A.'), + "CRM Anzahl Mitarbeiter": self._get_cell_value_safe( + row_data, + "CRM Anzahl Mitarbeiter"), + "Wiki Mitarbeiter": final_wiki_data.get( + 'mitarbeiter', + 'k.A.'), "Parent Account Name": parent_account_name_d, - "System Vorschlag Parent Account": self._get_cell_value_safe(row_data, "System Vorschlag Parent Account"), - "Parent Vorschlag Status": self._get_cell_value_safe(row_data, "Parent Vorschlag Status") - } - plausi_results = self._check_financial_plausibility(plausi_input_data) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plaus_umsatz_flag", "ERR_FLAG")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plaus_ma_flag", "ERR_FLAG")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz/MA Ratio"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plaus_ratio_flag", "ERR_FLAG")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung Umsatz CRM/Wiki"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("abweichung_umsatz_flag", "ERR_FLAG")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung MA CRM/Wiki"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("abweichung_ma_flag", "ERR_FLAG")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plausi_begruendung_final", "Fehler Begr.")]]}) + "System Vorschlag Parent Account": self._get_cell_value_safe( + row_data, + "System Vorschlag Parent Account"), + "Parent Vorschlag Status": self._get_cell_value_safe( + row_data, + "Parent Vorschlag Status")} + plausi_results = self._check_financial_plausibility( + plausi_input_data) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + plausi_results.get( + "plaus_umsatz_flag", + "ERR_FLAG")]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Mitarbeiter"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + plausi_results.get( + "plaus_ma_flag", + "ERR_FLAG")]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz/MA Ratio"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + plausi_results.get( + "plaus_ratio_flag", + "ERR_FLAG")]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung Umsatz CRM/Wiki"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + plausi_results.get( + "abweichung_umsatz_flag", + "ERR_FLAG")]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung MA CRM/Wiki"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + plausi_results.get( + "abweichung_ma_flag", + "ERR_FLAG")]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_in_sheet}', + 'values': [ + [ + plausi_results.get( + "plausi_begruendung_final", + "Fehler Begr.")]]}) except Exception as e_plausi: - self.logger.error(f"FEHLER bei Plausi-Checks für Zeile {row_num_in_sheet}: {e_plausi}") + self.logger.error( + f"FEHLER bei Plausi-Checks für Zeile {row_num_in_sheet}: {e_plausi}") - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Prüfdatum"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Pruefung"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Prüfdatum"] + 1)}{row_num_in_sheet}', + 'values': [ + [now_timestamp]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Pruefung"] + 1)}{row_num_in_sheet}', + 'values': [ + [now_timestamp]]}) # --- 4. Servicetechniker Schaetzung (ML Modell) --- run_ml_step = 'ml_predict' in steps_to_run - ml_processing_needed = self._needs_ml_prediction(row_data, force_reeval, chat_eval_just_ran) + ml_processing_needed = self._needs_ml_prediction( + row_data, force_reeval, chat_eval_just_ran) if run_ml_step and ml_processing_needed: any_processing_done = True - self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre ML-Schaetzung aus...") + self.logger.info( + f"Zeile {row_num_in_sheet}: Fuehre ML-Schaetzung aus...") try: - predicted_bucket = self._predict_technician_bucket(row_data) - if predicted_bucket and isinstance(predicted_bucket, str) and not predicted_bucket.startswith("FEHLER"): - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', 'values': [[predicted_bucket]]}) - self.logger.info(f" -> ML-Schaetzung erfolgreich: Bucket '{predicted_bucket}'.") - else: - self.logger.warning(f" -> ML-Schaetzung lieferte kein gueltiges Ergebnis: '{predicted_bucket}'.") - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', 'values': [['k.A. (Schaetzung fehlgeschlagen)']]}) + predicted_bucket = self._predict_technician_bucket(row_data) + if predicted_bucket and isinstance( + predicted_bucket, + str) and not predicted_bucket.startswith("FEHLER"): + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', + 'values': [ + [predicted_bucket]]}) + self.logger.info( + f" -> ML-Schaetzung erfolgreich: Bucket '{predicted_bucket}'.") + else: + self.logger.warning( + f" -> ML-Schaetzung lieferte kein gueltiges Ergebnis: '{predicted_bucket}'.") + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', + 'values': [ + ['k.A. (Schaetzung fehlgeschlagen)']]}) except Exception as e_ml: - self.logger.error(f"FEHLER bei ML-Schaetzung fuer Zeile {row_num_in_sheet}: {e_ml}") - self.logger.debug(traceback.format_exc()) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', 'values': [[f'FEHLER Schaetzung: {str(e_ml)[:50]}...']]}) + self.logger.error( + f"FEHLER bei ML-Schaetzung fuer Zeile {row_num_in_sheet}: {e_ml}") + self.logger.debug(traceback.format_exc()) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', + 'values': [ + [f'FEHLER Schaetzung: {str(e_ml)[:50]}...']]}) # ====================================================================== - # === Abschluss der _process_single_row Verarbeitung =================== + # === Abschluss der _process_single_row Verarbeitung ================== # ====================================================================== # --- 5. Abschliessende Updates (Version, Tokens, ReEval Flag) --- if any_processing_done: version_col_idx = COLUMN_MAP.get("Version") if version_col_idx is not None: - updates.append({'range': f'{self.sheet_handler._get_col_letter(version_col_idx + 1)}{row_num_in_sheet}', 'values': [[getattr(Config, 'VERSION', 'unknown')]]}) + updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(version_col_idx + 1)}{row_num_in_sheet}', + 'values': [ + [ + getattr( + Config, + 'VERSION', + 'unknown')]]}) else: - self.logger.error("FEHLER: Spaltenschluessel 'Version' nicht in COLUMN_MAP gefunden.") + self.logger.error( + "FEHLER: Spaltenschluessel 'Version' nicht in COLUMN_MAP gefunden.") if force_reeval and clear_x_flag: - reeval_col_idx = COLUMN_MAP.get("ReEval Flag") - if reeval_col_idx is not None: - flag_col_letter = self.sheet_handler._get_col_letter(reeval_col_idx + 1) - if flag_col_letter: - updates.append({'range': f'{flag_col_letter}{row_num_in_sheet}', 'values': [['']]}) - self.logger.debug(f" -> Update zum Loeschen des ReEval-Flags (A{row_num_in_sheet}) vorgemerkt.") - else: - self.logger.error(f"FEHLER: Konnte Spaltenbuchstaben fuer 'ReEval Flag' nicht ermitteln.") - else: - self.logger.error("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden.") + reeval_col_idx = COLUMN_MAP.get("ReEval Flag") + if reeval_col_idx is not None: + flag_col_letter = self.sheet_handler._get_col_letter( + reeval_col_idx + 1) + if flag_col_letter: + updates.append( + {'range': f'{flag_col_letter}{row_num_in_sheet}', 'values': [['']]}) + self.logger.debug( + f" -> Update zum Loeschen des ReEval-Flags (A{row_num_in_sheet}) vorgemerkt.") + else: + self.logger.error( + f"FEHLER: Konnte Spaltenbuchstaben fuer 'ReEval Flag' nicht ermitteln.") + else: + self.logger.error( + "FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden.") # --- 6. Batch Update fuer diese Zeile --- if updates: - self.logger.info(f"Zeile {row_num_in_sheet}: Sende Batch-Update mit {len(updates)} Operationen...") - success = self.sheet_handler.batch_update_cells(updates) - if not success: - self.logger.error(f"Zeile {row_num_in_sheet}: ENDGUELTIGER FEHLER beim Batch-Update nach Retries.") + self.logger.info( + f"Zeile {row_num_in_sheet}: Sende Batch-Update mit {len(updates)} Operationen...") + success = self.sheet_handler.batch_update_cells(updates) + if not success: + self.logger.error( + f"Zeile {row_num_in_sheet}: ENDGUELTIGER FEHLER beim Batch-Update nach Retries.") else: - if not any_processing_done: - self.logger.debug(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben.") + if not any_processing_done: + self.logger.debug( + f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben.") pause_duration = max(0.05, getattr(Config, 'RETRY_DELAY', 5) / 20.0) time.sleep(pause_duration) - self.logger.info(f"--- Verarbeitung fuer Zeile {row_num_in_sheet} abgeschlossen ---") - + self.logger.info( + f"--- Verarbeitung fuer Zeile {row_num_in_sheet} abgeschlossen ---") # ========================================================================== - # === Prozess Methoden (Sequentiell & Re-Evaluation) ======================= + # === Prozess Methoden (Sequentiell & Re-Evaluation) ===================== # ========================================================================== - def process_rows_sequentially(self, start_sheet_row, num_to_process, - process_wiki_steps=True, process_chatgpt_steps=True, - process_website_steps=True, process_ml_steps=True, - force_reeval_in_single_row=False): + def process_rows_sequentially( + self, + start_sheet_row, + num_to_process, + process_wiki_steps=True, + process_chatgpt_steps=True, + process_website_steps=True, + process_ml_steps=True, + force_reeval_in_single_row=False): """ Verarbeitet eine feste Anzahl von Zeilen beginnend bei einer bestimmten Sheet-Zeilennummer sequentiell. """ header_rows = self.sheet_handler._header_rows - if num_to_process is None or not isinstance(num_to_process, int) or num_to_process <= 0: - self.logger.info("Sequentielle Verarbeitung uebersprungen: num_to_process ist ungueltig oder <= 0.") - return + if num_to_process is None or not isinstance( + num_to_process, int) or num_to_process <= 0: + self.logger.info( + "Sequentielle Verarbeitung uebersprungen: num_to_process ist ungueltig oder <= 0.") + return - self.logger.info(f"Starte sequentielle Verarbeitung von {num_to_process} Zeilen ab Sheet-Zeile {start_sheet_row}...") + self.logger.info( + f"Starte sequentielle Verarbeitung von {num_to_process} Zeilen ab Sheet-Zeile {start_sheet_row}...") selected_steps_log = [] - if process_wiki_steps: selected_steps_log.append("Wiki (wiki)") - if process_chatgpt_steps: selected_steps_log.append("ChatGPT (chat)") - if process_website_steps: selected_steps_log.append("Website (web)") - if process_ml_steps: selected_steps_log.append("ML Predict (ml_predict)") - self.logger.info(f" Ausgewaehlte Schritte: {', '.join(selected_steps_log) if selected_steps_log else 'Keine'}") + if process_wiki_steps: + selected_steps_log.append("Wiki (wiki)") + if process_chatgpt_steps: + selected_steps_log.append("ChatGPT (chat)") + if process_website_steps: + selected_steps_log.append("Website (web)") + if process_ml_steps: + selected_steps_log.append("ML Predict (ml_predict)") + self.logger.info( + f" Ausgewaehlte Schritte: {', '.join(selected_steps_log) if selected_steps_log else 'Keine'}") if force_reeval_in_single_row: - self.logger.warning(" !!! force_reeval=True wird fuer alle Zeilen in _process_single_row gesetzt !!!") + self.logger.warning( + " !!! force_reeval=True wird fuer alle Zeilen in _process_single_row gesetzt !!!") steps_to_run_set = set() - if process_wiki_steps: steps_to_run_set.add('wiki') - if process_chatgpt_steps: steps_to_run_set.add('chat') - if process_website_steps: steps_to_run_set.add('web') - if process_ml_steps: steps_to_run_set.add('ml_predict') + if process_wiki_steps: + steps_to_run_set.add('wiki') + if process_chatgpt_steps: + steps_to_run_set.add('chat') + if process_website_steps: + steps_to_run_set.add('web') + if process_ml_steps: + steps_to_run_set.add('ml_predict') if not steps_to_run_set: - self.logger.warning("Keine Verarbeitungsschritte ausgewaehlt. Modus wird uebersprungen.") + self.logger.warning( + "Keine Verarbeitungsschritte ausgewaehlt. Modus wird uebersprungen.") return if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer sequentielle Verarbeitung.") + self.logger.error( + "Fehler beim Laden der Daten fuer sequentielle Verarbeitung.") return all_data = self.sheet_handler.get_all_data_with_headers() @@ -582,27 +1024,37 @@ class DataProcessor: start_index_in_all_data = start_sheet_row - 1 if start_index_in_all_data >= total_sheet_rows: - self.logger.warning(f"Start-Sheet-Zeile {start_sheet_row} liegt ausserhalb der Daten. Keine Verarbeitung.") - return + self.logger.warning( + f"Start-Sheet-Zeile {start_sheet_row} liegt ausserhalb der Daten. Keine Verarbeitung.") + return if start_index_in_all_data < header_rows: - self.logger.warning(f"Start-Sheet-Zeile {start_sheet_row} liegt in Headern. Starte ab Zeile {header_rows + 1}.") - start_index_in_all_data = header_rows + self.logger.warning( + f"Start-Sheet-Zeile {start_sheet_row} liegt in Headern. Starte ab Zeile {header_rows + 1}.") + start_index_in_all_data = header_rows - end_index_in_all_data = min(start_index_in_all_data + num_to_process, total_sheet_rows) - self.logger.info(f"Sequentielle Verarbeitung: Index-Bereich [{start_index_in_all_data}, {end_index_in_all_data}).") + end_index_in_all_data = min( + start_index_in_all_data + + num_to_process, + total_sheet_rows) + self.logger.info( + f"Sequentielle Verarbeitung: Index-Bereich [{start_index_in_all_data}, {end_index_in_all_data}).") if start_index_in_all_data >= end_index_in_all_data: - self.logger.info(f"Keine Zeilen im definierten Bereich zu verarbeiten.") - return + self.logger.info( + f"Keine Zeilen im definierten Bereich zu verarbeiten.") + return processed_count = 0 for i in range(start_index_in_all_data, end_index_in_all_data): row_num_in_sheet = i + 1 row_data = all_data[i] - if row_num_in_sheet <= header_rows: continue - if not any(cell and isinstance(cell, str) and cell.strip() for cell in row_data): - self.logger.debug(f"Ueberspringe leere Zeile {row_num_in_sheet}.") - continue + if row_num_in_sheet <= header_rows: + continue + if not any(cell and isinstance(cell, str) and cell.strip() + for cell in row_data): + self.logger.debug( + f"Ueberspringe leere Zeile {row_num_in_sheet}.") + continue try: self._process_single_row( row_num_in_sheet=row_num_in_sheet, @@ -613,66 +1065,92 @@ class DataProcessor: ) processed_count += 1 except Exception as e_proc: - self.logger.exception(f"FEHLER bei sequentieller Verarbeitung von Zeile {row_num_in_sheet}: {e_proc}") + self.logger.exception( + f"FEHLER bei sequentieller Verarbeitung von Zeile {row_num_in_sheet}: {e_proc}") - self.logger.info(f"Sequentielle Verarbeitung abgeschlossen. {processed_count} Zeilen bearbeitet.") + self.logger.info( + f"Sequentielle Verarbeitung abgeschlossen. {processed_count} Zeilen bearbeitet.") - def process_reevaluation_rows(self, row_limit=None, clear_flag=True, - process_wiki_steps=True, process_chatgpt_steps=True, - process_website_steps=True, process_ml_steps=True): + def process_reevaluation_rows( + self, + row_limit=None, + clear_flag=True, + process_wiki_steps=True, + process_chatgpt_steps=True, + process_website_steps=True, + process_ml_steps=True): """ Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind. """ - self.logger.info(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}") + self.logger.info( + f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}") 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") - if process_ml_steps: selected_steps_log.append("ML Predict") - self.logger.info(f"Ausgewaehlte Schritte fuer Re-Eval: {', '.join(selected_steps_log) if selected_steps_log else 'Keine'}") + 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") + if process_ml_steps: + selected_steps_log.append("ML Predict") + self.logger.info( + f"Ausgewaehlte Schritte fuer Re-Eval: {', '.join(selected_steps_log) if selected_steps_log else 'Keine'}") steps_to_run_set = set() - if process_wiki_steps: steps_to_run_set.add('wiki') - if process_chatgpt_steps: steps_to_run_set.add('chat') - if process_website_steps: steps_to_run_set.add('web') - if process_ml_steps: steps_to_run_set.add('ml_predict') + if process_wiki_steps: + steps_to_run_set.add('wiki') + if process_chatgpt_steps: + steps_to_run_set.add('chat') + if process_website_steps: + steps_to_run_set.add('web') + if process_ml_steps: + steps_to_run_set.add('ml_predict') if not steps_to_run_set: - self.logger.warning("Keine Verarbeitungsschritte fuer Re-Eval ausgewaehlt. Modus wird uebersprungen.") + self.logger.warning( + "Keine Verarbeitungsschritte fuer Re-Eval ausgewaehlt. Modus wird uebersprungen.") return if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer Re-Evaluation.") + self.logger.error( + "Fehler beim Laden der Daten fuer Re-Evaluation.") return all_data = self.sheet_handler.get_all_data_with_headers() header_rows = self.sheet_handler._header_rows if not all_data or len(all_data) <= header_rows: - self.logger.warning("Keine Datenzeilen fuer Re-Evaluation gefunden.") - return + self.logger.warning( + "Keine Datenzeilen fuer Re-Evaluation gefunden.") + return reeval_col_idx = COLUMN_MAP.get("ReEval Flag") if reeval_col_idx is None: - self.logger.critical("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden. Breche ab.") + self.logger.critical( + "FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden. Breche ab.") return rows_to_process = [] for idx, row_data in enumerate(all_data): - if idx < header_rows: continue - row_num_in_sheet = idx + 1 - cell_a_value = self._get_cell_value_safe(row_data, "ReEval Flag").strip().lower() - if cell_a_value == "x": - rows_to_process.append({'row_num': row_num_in_sheet, 'data': row_data}) + if idx < header_rows: + continue + row_num_in_sheet = idx + 1 + cell_a_value = self._get_cell_value_safe( + row_data, "ReEval Flag").strip().lower() + if cell_a_value == "x": + rows_to_process.append( + {'row_num': row_num_in_sheet, 'data': row_data}) found_count = len(rows_to_process) self.logger.info(f"{found_count} Zeilen mit ReEval-Flag 'x' gefunden.") - if found_count == 0: return + if found_count == 0: + return processed_count_actual = 0 for task in rows_to_process: if row_limit is not None and processed_count_actual >= row_limit: - self.logger.info(f"Zeilenlimit ({row_limit}) fuer Re-Evaluation erreicht.") + self.logger.info( + f"Zeilenlimit ({row_limit}) fuer Re-Evaluation erreicht.") break - + self.logger.info(f"Bearbeite Re-Eval Zeile {task['row_num']}...") processed_count_actual += 1 try: @@ -684,12 +1162,14 @@ class DataProcessor: clear_x_flag=clear_flag ) except Exception as e_proc_reval: - self.logger.exception(f"FEHLER bei Re-Evaluation von Zeile {task['row_num']}: {e_proc_reval}") + self.logger.exception( + f"FEHLER bei Re-Evaluation von Zeile {task['row_num']}: {e_proc_reval}") - self.logger.info(f"Re-Evaluierung abgeschlossen. {processed_count_actual} Zeilen verarbeitet.") + self.logger.info( + f"Re-Evaluierung abgeschlossen. {processed_count_actual} Zeilen verarbeitet.") # ========================================================================== - # === Batch Processing Methods ============================================= + # === Batch Processing Methods =========================================== # ========================================================================== @retry_on_failure @@ -697,8 +1177,10 @@ class DataProcessor: """ Verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen ueber OpenAI. """ - if not batch_data: return {} - self.logger.debug(f"Sende OpenAI-Batch fuer Wiki-Verifizierung ({len(batch_data)} Zeilen)...") + if not batch_data: + return {} + self.logger.debug( + f"Sende OpenAI-Batch fuer Wiki-Verifizierung ({len(batch_data)} Zeilen)...") aggregated_prompt = ( "Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln fuer Unternehmen. " @@ -709,8 +1191,7 @@ class DataProcessor: "- 'OK' (wenn der Artikel gut passt)\n" "- 'X | Alternativer Artikel: | Begruendung: '\n" "- 'X | Kein passender Artikel gefunden | Begruendung: '\n\n" - "Eintraege zur Pruefung:\n--------------------\n" - ) + "Eintraege zur Pruefung:\n--------------------\n") max_desc_length = 200 for item in batch_data: entry_text = ( @@ -719,16 +1200,21 @@ class DataProcessor: f" CRM-Beschreibung: {str(item.get('crm_desc', 'k.A.'))[:max_desc_length]}\n" f" Wikipedia-URL: {str(item.get('wiki_url', 'k.A.'))}\n" f" Wiki-Absatz: {str(item.get('wiki_paragraph', 'k.A.'))[:max_desc_length]}\n" - f" Wiki-Kategorien: {str(item.get('wiki_categories', 'k.A.'))[:max_desc_length]}\n----\n" - ) + f" Wiki-Kategorien: {str(item.get('wiki_categories', 'k.A.'))[:max_desc_length]}\n----\n") aggregated_prompt += entry_text try: - chat_response = summarize_batch_openai(aggregated_prompt, temperature=0.0) # Using summarize helper, but it's just a call_openai_chat wrapper - if not chat_response: raise openai.error.APIError("Keine Antwort von OpenAI erhalten.") + # Using summarize helper, but it's just a call_openai_chat wrapper + chat_response = summarize_batch_openai( + aggregated_prompt, temperature=0.0) + if not chat_response: + raise openai.error.APIError( + "Keine Antwort von OpenAI erhalten.") except Exception as e: - self.logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Wiki-Verifizierung: {e}") - return {item['row_num']: f"FEHLER API: {str(e)[:100]}" for item in batch_data} + self.logger.error( + f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Wiki-Verifizierung: {e}") + return { + item['row_num']: f"FEHLER API: {str(e)[:100]}" for item in batch_data} answers = {} original_batch_row_nums = {item['row_num'] for item in batch_data} @@ -736,34 +1222,47 @@ class DataProcessor: for line in lines: match = re.match(r"Eintrag (\d+): (.*)", line.strip()) if match: - row_num, answer_text = int(match.group(1)), match.group(2).strip() - if row_num in original_batch_row_nums: answers[row_num] = answer_text + row_num, answer_text = int( + match.group(1)), match.group(2).strip() + if row_num in original_batch_row_nums: + answers[row_num] = answer_text for row_num in original_batch_row_nums: - if row_num not in answers: answers[row_num] = "FEHLER: Antwort nicht geparst" + if row_num not in answers: + answers[row_num] = "FEHLER: Antwort nicht geparst" return answers - - def process_verification_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None): + def process_verification_batch( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None): """ Batch-Prozess nur fuer Wikipedia-Verifizierung. """ - self.logger.info(f"Starte Wikipedia-Verifizierungsmodus (Batch). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") + self.logger.info( + f"Starte Wikipedia-Verifizierungsmodus (Batch). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") if start_sheet_row is None: - start_data_index = self.sheet_handler.get_start_row_index(check_column_key="Wiki Verif. Timestamp") - if start_data_index == -1: return - start_sheet_row = start_data_index + self.sheet_handler._header_rows + 1 + start_data_index = self.sheet_handler.get_start_row_index( + check_column_key="Wiki Verif. Timestamp") + if start_data_index == -1: + return + start_sheet_row = start_data_index + self.sheet_handler._header_rows + 1 else: - 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() header_rows = self.sheet_handler._header_rows total_sheet_rows = len(all_data) - if end_sheet_row is None: end_sheet_row = total_sheet_rows - if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: return + if end_sheet_row is None: + end_sheet_row = total_sheet_rows + if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: + return # ... (Implementation of task collection, batching, and updating remains similar to the original code) ... - # This is a simplified placeholder as the logic is very long and already captured in the original file. + # This is a simplified placeholder as the logic is very long and + # already captured in the original file. """ Verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen ueber OpenAI. Sammelt die Ergebnisse und gibt sie zurueck. Aktualisiert NICHT das Sheet direkt. @@ -781,9 +1280,10 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist if not batch_data: - return {} # Gebe leeres Dictionary zurueck, wenn keine Tasks da sind + return {} # Gebe leeres Dictionary zurueck, wenn keine Tasks da sind - self.logger.debug(f"Sende OpenAI-Batch fuer Wiki-Verifizierung ({len(batch_data)} Zeilen, Start: {batch_data[0]['row_num'] if batch_data else 'N/A'})...") # <<< GEÄNDERT + self.logger.debug( + f"Sende OpenAI-Batch fuer Wiki-Verifizierung ({len(batch_data)} Zeilen, Start: {batch_data[0]['row_num'] if batch_data else 'N/A'})...") # <<< GEÄNDERT # --- Prompt Erstellung --- # Verwenden Sie klare Anweisungen und das definierte Antwortformat. @@ -806,22 +1306,27 @@ class DataProcessor: # Fuegen Sie die Daten fuer jeden Eintrag im Batch hinzu. # Kuerzen Sie die Beschreibungen und Kategorien, um das Prompt-Limit zu reduzieren. - # Stellen Sie sicher, dass die Werte Strings sind und "k.A." richtig behandelt wird. - max_desc_length = 200 # Maximale Laenge fuer Beschreibungsteile im Prompt + # Stellen Sie sicher, dass die Werte Strings sind und "k.A." richtig + # behandelt wird. + max_desc_length = 200 # Maximale Laenge fuer Beschreibungsteile im Prompt for item in batch_data: row_num = item['row_num'] - # Holen und Kuerzen Sie die Werte sicher. Ersetzen Sie None durch "k.A.". + # Holen und Kuerzen Sie die Werte sicher. Ersetzen Sie None durch + # "k.A.". company_name = str(item.get('company_name', 'k.A.')) crm_desc = str(item.get('crm_desc', 'k.A.')) wiki_url = str(item.get('wiki_url', 'k.A.')) wiki_paragraph = str(item.get('wiki_paragraph', 'k.A.')) wiki_categories = str(item.get('wiki_categories', 'k.A.')) - # Kuerzen Sie die Laengen und fuegen Sie "..." hinzu, wenn gekuerzt wurde. - crm_desc_short = crm_desc[:max_desc_length] + '...' if len(crm_desc) > max_desc_length else crm_desc - wiki_paragraph_short = wiki_paragraph[:max_desc_length] + '...' if len(wiki_paragraph) > max_desc_length else wiki_paragraph - wiki_categories_short = wiki_categories[:max_desc_length] + '...' if len(wiki_categories) > max_desc_length else wiki_categories - + # Kuerzen Sie die Laengen und fuegen Sie "..." hinzu, wenn gekuerzt + # wurde. + crm_desc_short = crm_desc[:max_desc_length] + \ + '...' if len(crm_desc) > max_desc_length else crm_desc + wiki_paragraph_short = wiki_paragraph[:max_desc_length] + '...' if len( + wiki_paragraph) > max_desc_length else wiki_paragraph + wiki_categories_short = wiki_categories[:max_desc_length] + '...' if len( + wiki_categories) > max_desc_length else wiki_categories entry_text = ( f"Eintrag {row_num}:\n" @@ -834,46 +1339,54 @@ class DataProcessor: ) aggregated_prompt += entry_text - # Fuegen Sie den Abschluss des Prompts hinzu. aggregated_prompt += "--------------------\nBitte nur die 'Eintrag X: Antwort'-Zeilen ausgeben." # Optional: Token zaehlen fuer den Prompt. # try: prompt_tokens = token_count(aggregated_prompt, model=getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')); self.logger.debug(f"Geschaetzt Prompt-Tokens fuer Batch: {prompt_tokens}."); - # except Exception as e_tc: self.logger.debug(f"Fehler beim Token-Zaehlen: {e_tc}"); - + # except Exception as e_tc: self.logger.debug(f"Fehler beim + # Token-Zaehlen: {e_tc}"); # --- ChatGPT Aufruf --- # call_openai_chat (Block 8) nutzt den retry_on_failure Decorator und wirft bei endgueltigem Fehler eine Exception. # Der retry_on_failure Decorator auf dieser Funktion faengt die Exception - # von call_openai_chat und fuehrt die Retries fuer die GESAMTE Batch-Funktion durch. + # von call_openai_chat und fuehrt die Retries fuer die GESAMTE + # Batch-Funktion durch. chat_response = None try: # Rufe die zentrale OpenAI Chat API Funktion auf (Block 8). # Standard Temperatur 0.0 fuer Klassifizierung/Verifizierung. - chat_response = call_openai_chat(aggregated_prompt, temperature=0.0) + chat_response = call_openai_chat( + aggregated_prompt, temperature=0.0) # Wenn call_openai_chat erfolgreich ist, gibt es den String zurueck. - # Exceptions werden nach Retries von call_openai_chat geworfen und vom aeusseren retry_on_failure dieser Funktion gefangen. + # Exceptions werden nach Retries von call_openai_chat geworfen und + # vom aeusseren retry_on_failure dieser Funktion gefangen. if not chat_response: - # Dieser Fall sollte nach der Aenderung in call_openai_chat (wirft Exception) nicht mehr auftreten. - self.logger.error("call_openai_chat gab unerwarteterweise None zurueck fuer Wiki-Verifizierungs-Batch.") # <<< GEÄNDERT - # Werfen Sie eine spezifische Exception, damit der aeussere Decorator sie faengt. - raise openai.error.APIError("Keine Antwort von OpenAI erhalten fuer Wiki-Verifizierungs-Batch.") - + # Dieser Fall sollte nach der Aenderung in call_openai_chat + # (wirft Exception) nicht mehr auftreten. + self.logger.error( + "call_openai_chat gab unerwarteterweise None zurueck fuer Wiki-Verifizierungs-Batch.") # <<< GEÄNDERT + # Werfen Sie eine spezifische Exception, damit der aeussere + # Decorator sie faengt. + raise openai.error.APIError( + "Keine Antwort von OpenAI erhalten fuer Wiki-Verifizierungs-Batch.") except Exception as e: # Wenn call_openai_chat oder der aeussere retry_on_failure eine Exception wirft (nach Retries) - # Die Exception wird hier gefangen, bevor sie an den Aufrufer (process_verification_batch) weitergeleitet wird. - self.logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Wiki-Verifizierung (innerhalb Batch Decorator): {e}") # <<< GEÄNDERT + # Die Exception wird hier gefangen, bevor sie an den Aufrufer + # (process_verification_batch) weitergeleitet wird. + self.logger.error( + f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Wiki-Verifizierung (innerhalb Batch Decorator): {e}") # <<< GEÄNDERT # Logge den Traceback - self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT - # Geben Sie ein Dictionary zurueck, das signalisiert, dass fuer alle Zeilen im Batch ein Fehler aufgetreten ist - return {item['row_num']: f"FEHLER API: {str(e)[:100]}" for item in batch_data} - + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT + # Geben Sie ein Dictionary zurueck, das signalisiert, dass fuer + # alle Zeilen im Batch ein Fehler aufgetreten ist + return { + item['row_num']: f"FEHLER API: {str(e)[:100]}" for item in batch_data} # --- Antwort parsen --- - answers = {} # Initialisieren Sie das Ergebnis-Dictionary + answers = {} # Initialisieren Sie das Ergebnis-Dictionary # Liste der Zeilennummern, die im ursprünglichen Batch angefragt wurden original_batch_row_nums = {item['row_num'] for item in batch_data} lines = chat_response.strip().split('\n') @@ -884,28 +1397,34 @@ class DataProcessor: if match: row_num = int(match.group(1)) answer_text = match.group(2).strip() - # Pruefen Sie, ob die Zeilennummer im urspruenglichen Batch angefragt wurde + # Pruefen Sie, ob die Zeilennummer im urspruenglichen Batch + # angefragt wurde if row_num in original_batch_row_nums: - answers[row_num] = answer_text - parsed_count += 1 - # else: self.logger.debug(f"Warnung: Antwort fuer unerwartete Zeilennummer {row_num} im Batch erhalten: {answer_text[:100]}...") # Zu viel Laerm (gekuerzt loggen) + answers[row_num] = answer_text + parsed_count += 1 + # else: self.logger.debug(f"Warnung: Antwort fuer unerwartete + # Zeilennummer {row_num} im Batch erhalten: + # {answer_text[:100]}...") # Zu viel Laerm (gekuerzt loggen) # Logge das Ergebnis des Parsens - self.logger.debug(f"OpenAI-Batch-Antwort geparst: {parsed_count} von {len(original_batch_row_nums)} Zeilen erfolgreich zugeordnet.") # <<< GEÄNDERT + self.logger.debug( + f"OpenAI-Batch-Antwort geparst: {parsed_count} von {len(original_batch_row_nums)} Zeilen erfolgreich zugeordnet.") # <<< GEÄNDERT - # Fuegen Sie einen Fehlerwert fuer Zeilen hinzu, die nicht geparst werden konnten (z.B. falsches Antwortformat) + # Fuegen Sie einen Fehlerwert fuer Zeilen hinzu, die nicht geparst + # werden konnten (z.B. falsches Antwortformat) if parsed_count < len(original_batch_row_nums): - self.logger.warning(f"Nicht alle Zeilen aus dem Batch ({len(original_batch_row_nums)}) konnten in der OpenAI-Antwort ({len(lines)} Zeilen) geparst werden.") # <<< GEÄNDERT - # Logge den Anfang der unvollstaendigen Antwort auf Debug - self.logger.debug(f"Unerwartete Antwortteile (erste 500 Zeichen): {chat_response[:500]}") # <<< GEÄNDERT - for row_num in original_batch_row_nums: - if row_num not in answers: - answers[row_num] = "FEHLER: Antwort nicht geparst" - - - # Die 'answers' Dictionary enthaelt nun Ergebnisse fuer alle Zeilen, entweder geparst oder mit einem Fehlerstring. - return answers # Rueckgabe des Dictionarys mit Ergebnissen oder Fehlern + self.logger.warning( + f"Nicht alle Zeilen aus dem Batch ({len(original_batch_row_nums)}) konnten in der OpenAI-Antwort ({len(lines)} Zeilen) geparst werden.") # <<< GEÄNDERT + # Logge den Anfang der unvollstaendigen Antwort auf Debug + self.logger.debug( + f"Unerwartete Antwortteile (erste 500 Zeichen): {chat_response[:500]}") # <<< GEÄNDERT + for row_num in original_batch_row_nums: + if row_num not in answers: + answers[row_num] = "FEHLER: Antwort nicht geparst" + # Die 'answers' Dictionary enthaelt nun Ergebnisse fuer alle Zeilen, + # entweder geparst oder mit einem Fehlerstring. + return answers # Rueckgabe des Dictionarys mit Ergebnissen oder Fehlern # --- Methode fuer den Wiki-Verifizierungs-Batchmodus (AX) --- # Diese Methode koordiniert die Auswahl der Zeilen, die Batch-Verarbeitung durch OpenAI, @@ -914,7 +1433,12 @@ class DataProcessor: # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter, _process_verification_openai_batch (derselben Block). # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, Config (Block 1), datetime, time. # Nutzt die uebergeordnete sheet_handler Instanz (Block 14). - def process_verification_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None): + + def process_verification_batch( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None): """ Batch-Prozess nur fuer Wikipedia-Verifizierung (Spalten S-U, V-Y werden geleert). Laedt Daten neu, prueft fuer jede Zeile im Bereich, ob Timestamp AX (Wiki Verif.) @@ -929,203 +1453,264 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Batch-Laufs - self.logger.info(f"Starte Wikipedia-Verifizierungsmodus (Batch S-U, AX). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT - + self.logger.info( + f"Starte Wikipedia-Verifizierungsmodus (Batch S-U, AX). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden und Startzeile ermitteln --- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AX...") # <<< GEÄNDERT - # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AX (Block 1 Column Map). - # Standardmaessig ab Zeile 7 - start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wiki Verif. Timestamp", min_sheet_row=7) + self.logger.info( + "Automatische Ermittlung der Startzeile basierend auf leeren AX...") # <<< GEÄNDERT + # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AX (Block 1 Column Map). + # Standardmaessig ab Zeile 7 + start_data_index_no_header = self.sheet_handler.get_start_row_index( + check_column_key="Wiki Verif. Timestamp", min_sheet_row=7) - # Wenn get_start_row_index -1 zurueckgibt (Fehler) - if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT - return # Beende die Methode + # Wenn get_start_row_index -1 zurueckgibt (Fehler) + if start_data_index_no_header == -1: + self.logger.error( + "FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT + return # Beende die Methode - # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index - start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AX Zelle): {start_sheet_row}") # <<< GEÄNDERT + # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten + # Daten-Index + start_sheet_row = start_data_index_no_header + \ + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut + self.logger.info( + f"Automatisch ermittelte Startzeile (erste leere AX Zelle): {start_sheet_row}") # <<< GEÄNDERT else: # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. - # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). + # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block + # 2). if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_verification_batch.") # <<< GEÄNDERT - return # Beende die Methode, wenn das Laden fehlschlaegt + self.logger.error( + "FEHLER beim Laden der Daten fuer process_verification_batch.") # <<< GEÄNDERT + return # Beende die Methode, wenn das Laden fehlschlaegt - - # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. + # Holen Sie die gesamte Datenliste (inklusive Header) aus dem + # SheetHandler. all_data = self.sheet_handler.get_all_data_with_headers() - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). + # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar + # (Block 14). header_rows = self.sheet_handler._header_rows - total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet + total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet # Berechne Endzeile, wenn nicht manuell gesetzt if end_sheet_row is None: - end_sheet_row = total_sheet_rows # Bis zur letzten Zeile + end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den verarbeitungsbereich - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT + self.logger.info( + f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT - # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) + # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht + # ueber Gesamtzeilen) if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT - return # Beende die Methode, wenn der Bereich leer ist - + self.logger.info( + "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT + return # Beende die Methode, wenn der Bereich leer ist # --- Indizes und Buchstaben --- - # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind + # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP + # (Block 1) vorhanden sind required_keys = [ - "Wiki Verif. Timestamp", "Wiki URL", "Chat Wiki Konsistenzpruefung", # Pruefkriterien / Timestamp (AX, M, S) - "CRM Name", "CRM Beschreibung", "Wiki Absatz", "Wiki Kategorien", # Daten fuer Prompt (B, F, N, R) - "Chat Begruendung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", # Ergebnisspalten (T, U) - "Begruendung bei Abweichung", "Chat Begruendung Abweichung Branche", # Spalten V-Y zum Leeren - "Wikipedia Timestamp", "Timestamp letzte Pruefung", # Spalten AN, AO zum Leeren - "Version", "SerpAPI Wiki Search Timestamp" # Spalten AP, AY zum Leeren + # Pruefkriterien / Timestamp (AX, M, S) + "Wiki Verif. Timestamp", "Wiki URL", "Chat Wiki Konsistenzpruefung", + # Daten fuer Prompt (B, F, N, R) + "CRM Name", "CRM Beschreibung", "Wiki Absatz", "Wiki Kategorien", + # Ergebnisspalten (T, U) + "Chat Begruendung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", + # Spalten V-Y zum Leeren + "Begruendung bei Abweichung", "Chat Begruendung Abweichung Branche", + "Wikipedia Timestamp", "Timestamp letzte Pruefung", # Spalten AN, AO zum Leeren + "Version", "SerpAPI Wiki Search Timestamp" # Spalten AP, AY zum Leeren ] # Erstellen Sie ein Dictionary mit Schluesseln und Indizes col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} - # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden + # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden + # wurden if None in col_indices.values(): - missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_verification_batch: {missing}. Breche ab.") # <<< GEÄNDERT - return # Beende die Methode bei kritischem Fehler + missing = [k for k, v in col_indices.items() if v is None] + self.logger.critical( + f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_verification_batch: {missing}. Breche ab.") # <<< GEÄNDERT + return # Beende die Methode bei kritischem Fehler - - # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt interne Helfer _get_col_letter Block 14) - ts_ax_letter = self.sheet_handler._get_col_letter(col_indices["Wiki Verif. Timestamp"] + 1) # Timestamp zu setzen (AX) - s_letter = self.sheet_handler._get_col_letter(col_indices["Chat Wiki Konsistenzpruefung"] + 1) # Status S - t_letter = self.sheet_handler._get_col_letter(col_indices["Chat Begruendung Wiki Inkonsistenz"] + 1) # Begruendung T - u_letter = self.sheet_handler._get_col_letter(col_indices["Chat Vorschlag Wiki Artikel"] + 1) # Vorschlag U + # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt + # interne Helfer _get_col_letter Block 14) + ts_ax_letter = self.sheet_handler._get_col_letter( + col_indices["Wiki Verif. Timestamp"] + 1) # Timestamp zu setzen (AX) + s_letter = self.sheet_handler._get_col_letter( + col_indices["Chat Wiki Konsistenzpruefung"] + 1) # Status S + t_letter = self.sheet_handler._get_col_letter( + col_indices["Chat Begruendung Wiki Inkonsistenz"] + 1) # Begruendung T + u_letter = self.sheet_handler._get_col_letter( + col_indices["Chat Vorschlag Wiki Artikel"] + 1) # Vorschlag U # Spalten V-Y leeren (werden in diesem Modus nicht neu befuellt). # V ist Begruendung bei Abweichung (von Wiki-URL Pruefung CRM vs Wiki). # Y ist Begruendung Abweichung Branche (von Chat). v_idx = col_indices["Begruendung bei Abweichung"] - y_idx = col_indices["Chat Begruendung Abweichung Branche"] # Block 1 Column Map + # Block 1 Column Map + y_idx = col_indices["Chat Begruendung Abweichung Branche"] # Erstellen Sie den Bereichsnamen (z.B. "V:Y") v_letter = self.sheet_handler._get_col_letter(v_idx + 1) y_letter = self.sheet_handler._get_col_letter(y_idx + 1) - v_y_range_letter = f'{v_letter}:{y_letter}' # z.B. V:Y + v_y_range_letter = f'{v_letter}:{y_letter}' # z.B. V:Y # Erstellen Sie eine Liste von leeren Strings fuer diesen Bereich - empty_vy_values = [''] * (y_idx - v_idx + 1) # Anzahl der Spalten = Y_Index - V_Index + 1 - + # Anzahl der Spalten = Y_Index - V_Index + 1 + empty_vy_values = [''] * (y_idx - v_idx + 1) # Timestamps AN, AO, AP, AY leeren. # Diese werden von anderen Schritten gesetzt und sollen hier zurueckgesetzt werden, - # um sicherzustellen, dass die Zeile bei Bedarf von diesen anderen Schritten erneut bearbeitet wird. - an_letter = self.sheet_handler._get_col_letter(col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS) - ao_letter = self.sheet_handler._get_col_letter(col_indices["Timestamp letzte Pruefung"] + 1) # AO (Chat Evaluation TS) - ap_letter = self.sheet_handler._get_col_letter(col_indices["Version"] + 1) # AP (Version) - ay_letter = self.sheet_handler._get_col_letter(col_indices["SerpAPI Wiki Search Timestamp"] + 1) # AY (SerpAPI Wiki TS) - + # um sicherzustellen, dass die Zeile bei Bedarf von diesen anderen + # Schritten erneut bearbeitet wird. + an_letter = self.sheet_handler._get_col_letter( + col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS) + ao_letter = self.sheet_handler._get_col_letter( + col_indices["Timestamp letzte Pruefung"] + + 1) # AO (Chat Evaluation TS) + ap_letter = self.sheet_handler._get_col_letter( + col_indices["Version"] + 1) # AP (Version) + ay_letter = self.sheet_handler._get_col_letter( + col_indices["SerpAPI Wiki Search Timestamp"] + + 1) # AY (SerpAPI Wiki TS) # --- Verarbeitung --- # Holen Sie die Batch-Groesse fuer OpenAI-Aufrufe aus Config (Block 1) - openai_batch_size = getattr(Config, 'PROCESSING_BATCH_SIZE', 20) # Nutzt dieselbe Batch-Groesse wie Scraping/Summarization + # Nutzt dieselbe Batch-Groesse wie Scraping/Summarization + openai_batch_size = getattr(Config, 'PROCESSING_BATCH_SIZE', 20) # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1) update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) + # Daten fuer den aktuellen OpenAI Batch (Liste von Dicts) + current_openai_batch_data = [] + # 1-basierte Zeilennummern im aktuellen OpenAI Batch + rows_in_current_openai_batch = [] + # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) + all_sheet_updates = [] - current_openai_batch_data = [] # Daten fuer den aktuellen OpenAI Batch (Liste von Dicts) - rows_in_current_openai_batch = [] # 1-basierte Zeilennummern im aktuellen OpenAI Batch - all_sheet_updates = [] # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) + # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den + # Batch aufgenommen werden (im Rahmen des Limits). + processed_count = 0 + # Zaehlt Zeilen, die uebersprungen wurden (wegen Status, fehlender + # Daten etc.). + skipped_count = 0 + # Zaehlt Zeilen, die speziell wegen fehlender M-URL uebersprungen + # wurden. + skipped_no_wiki_url = 0 - - processed_count = 0 # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den Batch aufgenommen werden (im Rahmen des Limits). - skipped_count = 0 # Zaehlt Zeilen, die uebersprungen wurden (wegen Status, fehlender Daten etc.). - skipped_no_wiki_url = 0 # Zaehlt Zeilen, die speziell wegen fehlender M-URL uebersprungen wurden. - - - # Iteriere ueber die Sheet-Zeilen im definierten Bereich (1-basierte Sheet-Zeilennummer) + # Iteriere ueber die Sheet-Zeilen im definierten Bereich (1-basierte + # Sheet-Zeilennummer) for i in range(start_sheet_row, end_sheet_row + 1): - row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste + row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste # Pruefen Sie, ob das Ende des Sheets erreicht wurde - if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht - - - row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile + if row_index_in_list >= total_sheet_rows: + break # Ende des Sheets erreicht + row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile # Stellen Sie sicher, dass die Zeile nicht leer ist (mindestens Name vorhanden) # Nutzt interne Helfer _get_cell_value_safe - company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map + company_name = self._get_cell_value_safe( + row, "CRM Name").strip() # Block 1 Column Map if not company_name: - self.logger.debug(f"Zeile {i}: Uebersprungen (Kein Firmenname in Spalte B).") # <<< GEÄNDERT - skipped_count += 1 # Zaehlen als uebersprungen - continue # Springe zur naechsten Zeile + self.logger.debug( + f"Zeile {i}: Uebersprungen (Kein Firmenname in Spalte B).") # <<< GEÄNDERT + skipped_count += 1 # Zaehlen als uebersprungen + continue # Springe zur naechsten Zeile # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist --- # Kriterium: Wiki Verif. Timestamp (AX) ist leer # UND Wiki URL (M) ist gefuellt und gueltig aussehend (nicht k.A., Fehler etc.) - # UND Status S ist NICHT bereits in einem Endzustand (OK, X (UPDATED/COPIED/INVALID)). + # UND Status S ist NICHT bereits in einem Endzustand (OK, X + # (UPDATED/COPIED/INVALID)). - # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne Helfer) - ax_value = self._get_cell_value_safe(row, "Wiki Verif. Timestamp").strip() # Block 1 Column Map - m_value = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map - s_value_upper = self._get_cell_value_safe(row, "Chat Wiki Konsistenzpruefung").strip().upper() # Block 1 Column Map + # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne + # Helfer) + ax_value = self._get_cell_value_safe( + row, "Wiki Verif. Timestamp").strip() # Block 1 Column Map + m_value = self._get_cell_value_safe( + row, "Wiki URL").strip() # Block 1 Column Map + s_value_upper = self._get_cell_value_safe( + row, "Chat Wiki Konsistenzpruefung").strip().upper() # Block 1 Column Map # Pruefen Sie, ob die Wiki URL (M) gueltig aussieht - is_wiki_url_valid_looking = m_value and isinstance(m_value, str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu basierend auf Log - + is_wiki_url_valid_looking = m_value and isinstance( + m_value, + str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in [ + "k.a.", + "kein artikel gefunden", + "fehler bei suche", + "http:"] # Fuege "http:" hinzu basierend auf Log # Definieren Sie die Endzustaende von Status S (Grossbuchstaben) - s_end_states = ["OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID SUGGESTION)"] + s_end_states = [ + "OK", + "X (UPDATED)", + "X (URL COPIED)", + "X (INVALID SUGGESTION)"] # Pruefen Sie, ob Status S in einem Endzustand ist - is_s_in_endstate = s_value_upper in s_end_states # Bugfix: Korrekte Zuweisung + is_s_in_endstate = s_value_upper in s_end_states # Bugfix: Korrekte Zuweisung - - # Verarbeitung ist noetig, wenn AX leer UND M gefuellt/gueltig aussieht UND S NICHT im Endzustand ist. + # Verarbeitung ist noetig, wenn AX leer UND M gefuellt/gueltig + # aussieht UND S NICHT im Endzustand ist. processing_needed_for_row = not ax_value and is_wiki_url_valid_looking and not is_s_in_endstate - # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level - log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) + log_check = ( + i < start_sheet_row + + 5) or ( + i % + 100 == 0) or (processing_needed_for_row) if log_check: - self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Verif. Check): AX leer? {not ax_value}, M gueltig? {is_wiki_url_valid_looking}, S ('{s_value_upper}') Endzustand? {is_s_in_endstate}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT - + self.logger.debug( + f"Zeile {i} ({company_name[:50]}... Wiki Verif. Check): AX leer? {not ax_value}, M gueltig? {is_wiki_url_valid_looking}, S ('{s_value_upper}') Endzustand? {is_s_in_endstate}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist if not processing_needed_for_row: - skipped_count += 1 # Zaehlen als uebersprungene Zeile - # Zaehlen Sie separat, wenn die Zeile speziell wegen fehlender M-URL uebersprungen wurde - if not is_wiki_url_valid_looking: skipped_no_wiki_url += 1 - continue # Springe zur naechsten Zeile - + skipped_count += 1 # Zaehlen als uebersprungene Zeile + # Zaehlen Sie separat, wenn die Zeile speziell wegen fehlender + # M-URL uebersprungen wurde + if not is_wiki_url_valid_looking: + skipped_no_wiki_url += 1 + continue # Springe zur naechsten Zeile # --- Wenn Verarbeitung noetig: Fuege zur Batch-Liste fuer OpenAI hinzu --- - processed_count += 1 # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im Rahmen des Limits zaehlen) + # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im + # Rahmen des Limits zaehlen) + processed_count += 1 # Pruefe das Limit fuer verarbeitete Zeilen - if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: - # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_verification_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT - break # Brich die Schleife ab - + if limit is not None and isinstance( + limit, int) and limit > 0 and processed_count > limit: + # Wenn das Limit erreicht ist und es ein positives Limit gibt + self.logger.info( + f"Verarbeitungslimit ({limit}) fuer process_verification_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT + break # Brich die Schleife ab # Sammle die benoetigten Daten fuer den OpenAI Prompt (Block 26 - _process_verification_openai_batch). # Diese Daten werden in einem Dictionary fuer den Batch gesammelt. - crm_desc = self._get_cell_value_safe(row, "CRM Beschreibung") # Block 1 Column Map - wiki_paragraph = self._get_cell_value_safe(row, "Wiki Absatz") # Block 1 Column Map - wiki_categories = self._get_cell_value_safe(row, "Wiki Kategorien") # Block 1 Column Map + crm_desc = self._get_cell_value_safe( + row, "CRM Beschreibung") # Block 1 Column Map + wiki_paragraph = self._get_cell_value_safe( + row, "Wiki Absatz") # Block 1 Column Map + wiki_categories = self._get_cell_value_safe( + row, "Wiki Kategorien") # Block 1 Column Map - - # Fuege die Daten dieser Zeile zur aktuellen Batch-Liste fuer OpenAI hinzu + # Fuege die Daten dieser Zeile zur aktuellen Batch-Liste fuer + # OpenAI hinzu current_openai_batch_data.append({ - 'row_num': i, # Die 1-basierte Sheet-Zeilennummer - 'company_name': company_name, # Nutzt den initial geladenen Namen + 'row_num': i, # Die 1-basierte Sheet-Zeilennummer + 'company_name': company_name, # Nutzt den initial geladenen Namen 'crm_desc': crm_desc, - 'wiki_url': m_value, # Nutzt die M-URL aus dem Sheet + 'wiki_url': m_value, # Nutzt die M-URL aus dem Sheet 'wiki_paragraph': wiki_paragraph, 'wiki_categories': wiki_categories }) # Fuege die Zeilennummer zur Liste der Zeilennummern im Batch hinzu rows_in_current_openai_batch.append(i) - # --- Verarbeite den Batch, wenn voll --- # Pruefe, ob die aktuelle Batch-Liste die definierte Groesse erreicht hat. # openai_batch_size wird aus Config geholt (Block 1). @@ -1133,103 +1718,132 @@ class DataProcessor: # Logge den Start der Batch-Verarbeitung batch_start_row = current_openai_batch_data[0]['row_num'] batch_end_row = current_openai_batch_data[-1]['row_num'] - self.logger.debug(f"\n--- Starte Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT - + self.logger.debug( + f"\n--- Starte Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT # Rufe die interne Methode auf, die den OpenAI Call fuer den Batch macht. # _process_verification_openai_batch (Block 26) ist mit retry_on_failure dekoriert. - # Wenn _process_verification_openai_batch eine Exception wirft (nach Retries), wird diese hier gefangen. - batch_results = self._process_verification_openai_batch(current_openai_batch_data) - # Ergebnisse sollten ein Dictionary {row_num: raw_chatgpt_answer} sein, auch bei Fehlern. - + # Wenn _process_verification_openai_batch eine Exception wirft + # (nach Retries), wird diese hier gefangen. + batch_results = self._process_verification_openai_batch( + current_openai_batch_data) + # Ergebnisse sollten ein Dictionary {row_num: + # raw_chatgpt_answer} sein, auch bei Fehlern. # Sammle Sheet Updates basierend auf den Batch-Ergebnissen. # Setze immer den Timestamp AX und die Werte in S, T, U und V-Y. # Der aktuelle Zeitstempel fuer den Batch current_batch_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen + batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen - - # Iteriere ueber die Zeilennummern, die in DIESEM OpenAI Batch waren + # Iteriere ueber die Zeilennummern, die in DIESEM OpenAI Batch + # waren for row_num in rows_in_current_openai_batch: # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary. - # Fallback auf einen Fehlerstring, wenn das Ergebnis fehlt (sollte nicht passieren, wenn _process_verification_openai_batch korrekt ist). - answer = batch_results.get(row_num, "FEHLER: Batch-Ergebnis fehlt") - # self.logger.debug(f"Zeile {row_num} Verifizierungsantwort: '{answer[:100]}...'") # Zu viel Laerm (gekuerzt) + # Fallback auf einen Fehlerstring, wenn das Ergebnis fehlt + # (sollte nicht passieren, wenn + # _process_verification_openai_batch korrekt ist). + answer = batch_results.get( + row_num, "FEHLER: Batch-Ergebnis fehlt") + # self.logger.debug(f"Zeile {row_num} + # Verifizierungsantwort: '{answer[:100]}...'") # Zu viel + # Laerm (gekuerzt) - - # Logik zur Bestimmung der Werte fuer S, T, U basierend auf 'answer' (aehnlich wie in altem _process_batch) - wiki_confirm, alt_article, wiki_explanation = "", "", "" # Initialisiere mit leeren Strings + # Logik zur Bestimmung der Werte fuer S, T, U basierend auf + # 'answer' (aehnlich wie in altem _process_batch) + # Initialisiere mit leeren Strings + wiki_confirm, alt_article, wiki_explanation = "", "", "" # Pruefe auf Standard-Antworten und Fehler-Antworten if isinstance(answer, str) and answer.upper() == "OK": wiki_confirm = "OK" - wiki_explanation = "Passt laut KI zur Firma." # Standard Begruendung bei OK + wiki_explanation = "Passt laut KI zur Firma." # Standard Begruendung bei OK elif isinstance(answer, str) and answer.startswith("X |"): - # Parse die Antwort im Format "X | | " - parts = answer.split("|", 2) # Teile maximal in 3 Teile - wiki_confirm = "X" # Status ist X - if len(parts) > 1: - detail = parts[1].strip() # Zweiter Teil ist Detail (Alternative URL oder "Kein passender Artikel gefunden") - if detail.lower().startswith("alternativer artikel:"): - alt_article = detail.split(":", 1)[1].strip() # Extrahiere URL - elif detail.lower() == "kein passender artikel gefunden": - alt_article = detail # Text "Kein passender Artikel gefunden" - else: - alt_article = detail # Unbekanntes Detail + # Parse die Antwort im Format "X | | + # " + # Teile maximal in 3 Teile + parts = answer.split("|", 2) + wiki_confirm = "X" # Status ist X + if len(parts) > 1: + # Zweiter Teil ist Detail (Alternative URL oder + # "Kein passender Artikel gefunden") + detail = parts[1].strip() + if detail.lower().startswith("alternativer artikel:"): + alt_article = detail.split( + ":", 1)[1].strip() # Extrahiere URL + elif detail.lower() == "kein passender artikel gefunden": + alt_article = detail # Text "Kein passender Artikel gefunden" + else: + alt_article = detail # Unbekanntes Detail - if len(parts) > 2: - reason_part = parts[2].strip() # Dritter Teil ist Begruendung - if reason_part.lower().startswith("begruendung:"): - wiki_explanation = reason_part.split(":", 1)[1].strip() # Extrahiere Begruendungstext - else: - wiki_explanation = reason_part # Unbekannte Begruendung - - # Fuege ggf. den rohen Antworttext zur Begruendung hinzu, wenn Parsing unvollstaendig war - if not alt_article or not wiki_explanation: - wiki_explanation += f" (Rohantwort: {answer[:100]}...)" + if len(parts) > 2: + # Dritter Teil ist Begruendung + reason_part = parts[2].strip() + if reason_part.lower().startswith("begruendung:"): + wiki_explanation = reason_part.split( + ":", 1)[1].strip() # Extrahiere Begruendungstext + else: + wiki_explanation = reason_part # Unbekannte Begruendung + # Fuege ggf. den rohen Antworttext zur Begruendung + # hinzu, wenn Parsing unvollstaendig war + if not alt_article or not wiki_explanation: + wiki_explanation += f" (Rohantwort: {answer[:100]}...)" elif isinstance(answer, str) and answer.startswith("FEHLER"): - # Wenn die Batch-Verarbeitung einen Fehler zurueckgegeben hat - wiki_confirm = "FEHLER" - wiki_explanation = answer # Fehlermeldung in Begruendung schreiben - alt_article = "Siehe Begruendung" # Verweis auf Begruendung + # Wenn die Batch-Verarbeitung einen Fehler + # zurueckgegeben hat + wiki_confirm = "FEHLER" + wiki_explanation = answer # Fehlermeldung in Begruendung schreiben + alt_article = "Siehe Begruendung" # Verweis auf Begruendung - else: # Unerwartetes Format der Antwort (weder OK noch X | noch FEHLER) - wiki_confirm = "?" # Setze Status auf unbekannt - wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..." # Speichere Anfang der Antwort in Begruendung (gekuerzt) - alt_article = "Siehe Begruendung" # Verweis auf Begruendung + # Unerwartetes Format der Antwort (weder OK noch X | noch + # FEHLER) + else: + wiki_confirm = "?" # Setze Status auf unbekannt + # Speichere Anfang der Antwort in Begruendung + # (gekuerzt) + wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..." + alt_article = "Siehe Begruendung" # Verweis auf Begruendung # Spalten V-Y (Begruendung bei Abweichung etc.) werden in diesem Modus geleert # Fuer jede Zeile im Batch fuegen wir das Update hinzu. # empty_vy_values wurde oben vorbereitet. - v_y_values = empty_vy_values # Liste von leeren Strings - # Fuege Update zum Leeren von V-Y hinzu, falls Index gefunden wurde - if v_y_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte - batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [v_y_values]}) # Block 1 Column Map, interne Helfer + v_y_values = empty_vy_values # Liste von leeren Strings + # Fuege Update zum Leeren von V-Y hinzu, falls Index + # gefunden wurde + if v_y_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte + batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [ + v_y_values]}) # Block 1 Column Map, interne Helfer - - # Fuege Updates fuer S, T, U und AX hinzu (nutzt interne Helfer) - batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [[wiki_confirm]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [[alt_article]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [[wiki_explanation]]}) # Block 1 Column Map + # Fuege Updates fuer S, T, U und AX hinzu (nutzt interne + # Helfer) + batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [ + [wiki_confirm]]}) # Block 1 Column Map + batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [ + [alt_article]]}) # Block 1 Column Map + batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [ + [wiki_explanation]]}) # Block 1 Column Map # Setze AX Timestamp fuer diese Zeile - batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [[current_batch_timestamp]]}) # Block 1 Column Map - + batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [ + [current_batch_timestamp]]}) # Block 1 Column Map # --- Sende gesammelte Updates fuer diesen Batch --- # Sammle die Updates fuer diesen Batch in der globalen Liste. - # all_sheet_updates.extend(batch_sheet_updates) # Nicht hier sammeln, sondern direkt senden + # all_sheet_updates.extend(batch_sheet_updates) # Nicht hier + # sammeln, sondern direkt senden # Sende die gesammelten Updates fuer DIESEN Batch sofort. if batch_sheet_updates: - self.logger.debug(f" Sende Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...") # <<< GEÄNDERT + self.logger.debug( + f" Sende Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. # Wenn es fehlschlaegt, wird es intern geloggt. - success = self.sheet_handler.batch_update_cells(batch_sheet_updates) + success = self.sheet_handler.batch_update_cells( + batch_sheet_updates) if success: - self.logger.info(f" Sheet-Update fuer Wiki-Verifizierungs-Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.") # <<< GEÄNDERT + self.logger.info( + f" Sheet-Update fuer Wiki-Verifizierungs-Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Setze Batch-Listen zurueck fuer die naechste Iteration @@ -1239,71 +1853,102 @@ class DataProcessor: # Pause nach jedem OpenAI Batch (nutzt Config Block 1). # Dies ist wichtig, um Rate Limits zu vermeiden. # Nutze Config.RETRY_DELAY, ggf. kuerzer, da es ein Batch war - pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.5 # 50% der Retry-Wartezeit - self.logger.debug(f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---") # <<< GEÄNDERT + pause_duration = getattr( + Config, 'RETRY_DELAY', 5) * 0.5 # 50% der Retry-Wartezeit + self.logger.debug( + f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---") # <<< GEÄNDERT time.sleep(pause_duration) - # --- Verarbeitung des letzten unvollstaendigen Batches nach der Schleife --- # Wenn nach der Hauptschleife noch Tasks in der Batch-Liste sind if current_openai_batch_data: - # Logge den Start des finalen Batches - batch_start_row = current_openai_batch_data[0]['row_num'] - batch_end_row = current_openai_batch_data[-1]['row_num'] - self.logger.debug(f"\n--- Starte FINALEN Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT + # Logge den Start des finalen Batches + batch_start_row = current_openai_batch_data[0]['row_num'] + batch_end_row = current_openai_batch_data[-1]['row_num'] + self.logger.debug( + f"\n--- Starte FINALEN Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT - # Rufe die interne Methode auf, die den OpenAI Call macht - batch_results = self._process_verification_openai_batch(current_openai_batch_data) - # Ergebnisse sollten ein Dictionary {row_num: raw_chatgpt_answer} sein, auch bei Fehlern. + # Rufe die interne Methode auf, die den OpenAI Call macht + batch_results = self._process_verification_openai_batch( + current_openai_batch_data) + # Ergebnisse sollten ein Dictionary {row_num: raw_chatgpt_answer} + # sein, auch bei Fehlern. - # Sammle Sheet Updates (S, T, U, V-Y, AX) fuer diesen finalen Batch - current_batch_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen + # Sammle Sheet Updates (S, T, U, V-Y, AX) fuer diesen finalen Batch + current_batch_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen - # Iteriere ueber die Zeilennummern, die in DIESEM finalen OpenAI Batch waren - for row_num in rows_in_current_openai_batch: - # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary. - answer = batch_results.get(row_num, "FEHLER: Batch-Ergebnis fehlt") # Fallback + # Iteriere ueber die Zeilennummern, die in DIESEM finalen OpenAI + # Batch waren + for row_num in rows_in_current_openai_batch: + # Hole das Ergebnis fuer diese Zeile aus dem + # Ergebnis-Dictionary. + answer = batch_results.get( + row_num, "FEHLER: Batch-Ergebnis fehlt") # Fallback - # Logik zur Bestimmung der Werte fuer S, T, U basierend auf 'answer' - wiki_confirm, alt_article, wiki_explanation = "", "", "" - # Leere V-Y Spalten - v_y_values = empty_vy_values # Liste von leeren Strings - if isinstance(answer, str) and answer.upper() == "OK": wiki_confirm = "OK"; wiki_explanation = "Passt laut KI zur Firma." - elif isinstance(answer, str) and answer.startswith("X |"): - parts = answer.split("|", 2); wiki_confirm = "X" - if len(parts) > 1: detail = parts[1].strip(); alt_article = detail.split(":", 1)[1].strip() if detail.lower().startswith("alternativer artikel:") else detail - if len(parts) > 2: reason_part = parts[2].strip(); wiki_explanation = reason_part.split(":", 1)[1].strip() if reason_part.lower().startswith("begruendung:") else reason_part - if not alt_article or not wiki_explanation: wiki_explanation += f" (Rohantwort: {answer[:100]}...)" - elif isinstance(answer, str) and answer.startswith("FEHLER"): wiki_confirm = "FEHLER"; wiki_explanation = answer; alt_article = "Siehe Begruendung" - else: wiki_confirm = "?"; wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..."; alt_article = "Siehe Begruendung" + # Logik zur Bestimmung der Werte fuer S, T, U basierend auf + # 'answer' + wiki_confirm, alt_article, wiki_explanation = "", "", "" + # Leere V-Y Spalten + v_y_values = empty_vy_values # Liste von leeren Strings + if isinstance(answer, str) and answer.upper() == "OK": + wiki_confirm = "OK" + wiki_explanation = "Passt laut KI zur Firma." + elif isinstance(answer, str) and answer.startswith("X |"): + parts = answer.split("|", 2) + wiki_confirm = "X" + if len(parts) > 1: + detail = parts[1].strip() + alt_article = detail.split(":", 1)[1].strip() if detail.lower( + ).startswith("alternativer artikel:") else detail + if len(parts) > 2: + reason_part = parts[2].strip() + wiki_explanation = reason_part.split(":", 1)[1].strip( + ) if reason_part.lower().startswith("begruendung:") else reason_part + if not alt_article or not wiki_explanation: + wiki_explanation += f" (Rohantwort: {answer[:100]}...)" + elif isinstance(answer, str) and answer.startswith("FEHLER"): + wiki_confirm = "FEHLER" + wiki_explanation = answer + alt_article = "Siehe Begruendung" + else: + wiki_confirm = "?" + wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..." + alt_article = "Siehe Begruendung" + # Fuege Updates fuer S, T, U und AX hinzu + batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [ + [wiki_confirm]]}) # Block 1 Column Map + batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [ + [alt_article]]}) # Block 1 Column Map + batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [ + [wiki_explanation]]}) # Block 1 Column Map + # Setze AX Timestamp + batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [ + [current_batch_timestamp]]}) # Block 1 Column Map - # Fuege Updates fuer S, T, U und AX hinzu - batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [[wiki_confirm]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [[alt_article]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [[wiki_explanation]]}) # Block 1 Column Map - # Setze AX Timestamp - batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [[current_batch_timestamp]]}) # Block 1 Column Map - - # Fuege Update zum Leeren von V-Y hinzu, falls Index gefunden wurde - if v_y_range_letter: - batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [v_y_values]}) # Block 1 Column Map, interne Helfer - - - # Sende die gesammelten Updates fuer DIESEN finalen Batch. - if batch_sheet_updates: - self.logger.debug(f" Sende FINALES Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen)...") # <<< GEÄNDERT - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. - success = self.sheet_handler.batch_update_cells(batch_sheet_updates) - if success: - self.logger.info(f" FINALES Sheet-Update fuer Wiki-Verifizierungs-Batch erfolgreich.") # <<< GEÄNDERT - # Der Fehlerfall wird von batch_update_cells geloggt + # Fuege Update zum Leeren von V-Y hinzu, falls Index gefunden + # wurde + if v_y_range_letter: + batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [ + v_y_values]}) # Block 1 Column Map, interne Helfer + # Sende die gesammelten Updates fuer DIESEN finalen Batch. + if batch_sheet_updates: + self.logger.debug( + f" Sende FINALES Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen)...") # <<< GEÄNDERT + # Nutzt die batch_update_cells Methode des Sheet Handlers + # (Block 14) mit Retry. + success = self.sheet_handler.batch_update_cells( + batch_sheet_updates) + if success: + self.logger.info( + f" FINALES Sheet-Update fuer Wiki-Verifizierungs-Batch erfolgreich.") # <<< GEÄNDERT + # Der Fehlerfall wird von batch_update_cells geloggt # Logge den Abschluss des Modus - self.logger.info(f"Wikipedia-Verifizierungs-Batch abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen ({skipped_no_wiki_url} wegen fehlender M-URL).") # <<< GEÄNDERT - + self.logger.info( + f"Wikipedia-Verifizierungs-Batch abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen ({skipped_no_wiki_url} wegen fehlender M-URL).") # <<< GEÄNDERT def _scrape_raw_text_task(self, task_info, get_website_raw_func): """ @@ -1314,28 +1959,37 @@ class DataProcessor: raw_text, error = "k.A.", None try: raw_text = get_website_raw_func(url) - if isinstance(raw_text, str) and (raw_text.startswith("k.A. (Fehler") or raw_text.startswith("FEHLER:")): - error = f"Scraping Fehler: {raw_text[:100]}..." + if isinstance(raw_text, str) and (raw_text.startswith( + "k.A. (Fehler") or raw_text.startswith("FEHLER:")): + error = f"Scraping Fehler: {raw_text[:100]}..." elif not isinstance(raw_text, str) or not raw_text.strip(): - error = "Scraping Task Fehler: Funktion gab keinen gueltigen String zurueck." - raw_text = "k.A. (Extraktion fehlgeschlagen)" + error = "Scraping Task Fehler: Funktion gab keinen gueltigen String zurueck." + raw_text = "k.A. (Extraktion fehlgeschlagen)" except Exception as e: error = f"Unerwarteter Fehler im Scraping Task Zeile {row_num}: {e}" logger.error(error) raw_text = "k.A. (Unerwarteter Fehler Task)" return {"row_num": row_num, "raw_text": raw_text, "error": error} - def process_website_scraping_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None): + def process_website_scraping_batch( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None): """ Batch-Prozess NUR fuer Website-Scraping. """ - self.logger.info(f"Starte Website-Scraping (Batch). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") + self.logger.info( + f"Starte Website-Scraping (Batch). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") if start_sheet_row is None: - start_data_index = self.sheet_handler.get_start_row_index(check_column_key="Website Scrape Timestamp") - if start_data_index == -1: return - start_sheet_row = start_data_index + self.sheet_handler._header_rows + 1 + start_data_index = self.sheet_handler.get_start_row_index( + check_column_key="Website Scrape Timestamp") + if start_data_index == -1: + return + start_sheet_row = start_data_index + self.sheet_handler._header_rows + 1 else: - if not self.sheet_handler.load_data(): return + if not self.sheet_handler.load_data(): + return """ Batch-Prozess NUR fuer Website-Scraping (Rohtext AR). Laedt Daten neu, prueft Spalte AR auf Inhalt ('', 'k.A.', etc.) und ueberspringt Zeilen mit Inhalt. @@ -1348,66 +2002,80 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Batch-Laufs - self.logger.info(f"Starte Website-Scraping (Batch AR, AT, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT - + self.logger.info( + f"Starte Website-Scraping (Batch AR, AT, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden und Startzeile ermitteln --- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AT...") # <<< GEÄNDERT - # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AT (Block 1 Column Map). - # Standardmaessig ab Zeile 7 - start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Website Scrape Timestamp", min_sheet_row=7) + self.logger.info( + "Automatische Ermittlung der Startzeile basierend auf leeren AT...") # <<< GEÄNDERT + # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AT (Block 1 Column Map). + # Standardmaessig ab Zeile 7 + start_data_index_no_header = self.sheet_handler.get_start_row_index( + check_column_key="Website Scrape Timestamp", min_sheet_row=7) - # Wenn get_start_row_index -1 zurueckgibt (Fehler) - if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT - return # Beende die Methode + # Wenn get_start_row_index -1 zurueckgibt (Fehler) + if start_data_index_no_header == -1: + self.logger.error( + "FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT + return # Beende die Methode - # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index - start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AT Zelle): {start_sheet_row}") # <<< GEÄNDERT + # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten + # Daten-Index + start_sheet_row = start_data_index_no_header + \ + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut + self.logger.info( + f"Automatisch ermittelte Startzeile (erste leere AT Zelle): {start_sheet_row}") # <<< GEÄNDERT else: # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. - # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). + # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block + # 2). if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_website_scraping_batch.") # <<< GEÄNDERT - return # Beende die Methode, wenn das Laden fehlschlaegt + self.logger.error( + "FEHLER beim Laden der Daten fuer process_website_scraping_batch.") # <<< GEÄNDERT + return # Beende die Methode, wenn das Laden fehlschlaegt - - # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. + # Holen Sie die gesamte Datenliste (inklusive Header) aus dem + # SheetHandler. all_data = self.sheet_handler.get_all_data_with_headers() - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). + # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar + # (Block 14). header_rows = self.sheet_handler._header_rows - total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet - + total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet # Berechne Endzeile, wenn nicht manuell gesetzt if end_sheet_row is None: - end_sheet_row = total_sheet_rows # Bis zur letzten Zeile + end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den verarbeitungsbereich - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT + self.logger.info( + f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT - # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) + # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht + # ueber Gesamtzeilen) if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT - return # Beende die Methode, wenn der Bereich leer ist - + self.logger.info( + "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT + return # Beende die Methode, wenn der Bereich leer ist # --- Indizes und Buchstaben --- - # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind + # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP + # (Block 1) vorhanden sind required_keys = [ - "Website Rohtext", "CRM Website", "Version", "Website Scrape Timestamp", "CRM Name" # AR, D, AP, AT, B + # AR, D, AP, AT, B + "Website Rohtext", "CRM Website", "Version", "Website Scrape Timestamp", "CRM Name" ] # Erstellen Sie ein Dictionary mit Schluesseln und Indizes col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} - # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden + # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden + # wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_website_scraping_batch: {missing}. Breche ab.") # <<< GEÄNDERT - return # Beende die Methode bei kritischem Fehler + self.logger.critical( + f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_website_scraping_batch: {missing}. Breche ab.") # <<< GEÄNDERT + return # Beende die Methode bei kritischem Fehler # Ermitteln Sie die Indizes und Buchstaben fuer Updates (AR, AT, AP) rohtext_col_idx = col_indices["Website Rohtext"] @@ -1416,98 +2084,133 @@ class DataProcessor: timestamp_col_idx = col_indices["Website Scrape Timestamp"] name_col_idx = col_indices["CRM Name"] - rohtext_col_letter = self.sheet_handler._get_col_letter(rohtext_col_idx + 1) # Block 14 _get_col_letter - version_col_letter = self.sheet_handler._get_col_letter(version_col_idx + 1) - timestamp_col_letter = self.sheet_handler._get_col_letter(timestamp_col_idx + 1) - + rohtext_col_letter = self.sheet_handler._get_col_letter( + rohtext_col_idx + 1) # Block 14 _get_col_letter + version_col_letter = self.sheet_handler._get_col_letter( + version_col_idx + 1) + timestamp_col_letter = self.sheet_handler._get_col_letter( + timestamp_col_idx + 1) # --- Hauptlogik: Iteriere und sammle Batches --- - # Holen Sie die Batch-Groesse fuer Verarbeitung (Threading) aus Config (Block 1) + # Holen Sie die Batch-Groesse fuer Verarbeitung (Threading) aus Config + # (Block 1) processing_batch_size = getattr(Config, 'PROCESSING_BATCH_SIZE', 20) # Holen Sie die maximale Anzahl Worker aus Config (Block 1) max_scraping_workers = getattr(Config, 'MAX_SCRAPING_WORKERS', 10) # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1) update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) + # Tasks fuer den aktuellen Scraping-Batch (Liste von Dicts) + tasks_for_processing_batch = [] + # 1-basierte Zeilennummern im aktuellen Batch + rows_in_current_scraping_batch = [] + # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) + all_sheet_updates = [] - tasks_for_processing_batch = [] # Tasks fuer den aktuellen Scraping-Batch (Liste von Dicts) - rows_in_current_scraping_batch = [] # 1-basierte Zeilennummern im aktuellen Batch - all_sheet_updates = [] # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) + # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den + # Batch aufgenommen werden (im Rahmen des Limits). + processed_count = 0 + # Zaehlt Zeilen, die uebersprungen wurden (wegen Inhalt oder fehlender + # URL). + skipped_count = 0 + # Zaehlt Zeilen, die speziell wegen fehlender URL uebersprungen wurden. + skipped_no_url = 0 - - processed_count = 0 # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den Batch aufgenommen werden (im Rahmen des Limits). - skipped_count = 0 # Zaehlt Zeilen, die uebersprungen wurden (wegen Inhalt oder fehlender URL). - skipped_no_url = 0 # Zaehlt Zeilen, die speziell wegen fehlender URL uebersprungen wurden. - - - # Iteriere ueber die Sheet-Zeilen im definierten Bereich (1-basierte Sheet-Zeilennummer) + # Iteriere ueber die Sheet-Zeilen im definierten Bereich (1-basierte + # Sheet-Zeilennummer) for i in range(start_sheet_row, end_sheet_row + 1): - row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste + row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste # Pruefen Sie, ob das Ende des Sheets erreicht wurde - if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht - - - row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile + if row_index_in_list >= total_sheet_rows: + break # Ende des Sheets erreicht + row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile # Stellen Sie sicher, dass die Zeile nicht leer ist - if not any(cell and isinstance(cell, str) and cell.strip() for cell in row): - #self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") # Zu viel Laerm im Debug - skipped_count += 1 # Zaehlen als uebersprungen - continue # Springe zur naechsten Zeile - + if not any(cell and isinstance(cell, str) and cell.strip() + for cell in row): + # self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") + # # Zu viel Laerm im Debug + skipped_count += 1 # Zaehlen als uebersprungen + continue # Springe zur naechsten Zeile # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist --- # Kriterium: Website Rohtext (AR) ist leer oder ein Standard-Fehlerwert. # UND Website URL (D) ist vorhanden und gueltig aussehend. - # Holen Sie den Wert aus Spalte AR (Website Rohtext) (nutzt interne Helfer _get_cell_value_safe) - cell_value_ar = self._get_cell_value_safe(row, "Website Rohtext") # Block 1 Column Map - # Pruefen Sie, ob AR leer ist oder einen Standard-Fehlerwert enthaelt. - ar_is_empty_or_default = not cell_value_ar or (isinstance(cell_value_ar, str) and str(cell_value_ar).strip().lower() in ["k.a.", "k.a. (nur cookie-banner erkannt)", "k.a. (fehler)"]) + # Holen Sie den Wert aus Spalte AR (Website Rohtext) (nutzt interne + # Helfer _get_cell_value_safe) + cell_value_ar = self._get_cell_value_safe( + row, "Website Rohtext") # Block 1 Column Map + # Pruefen Sie, ob AR leer ist oder einen Standard-Fehlerwert + # enthaelt. + ar_is_empty_or_default = not cell_value_ar or ( + isinstance( + cell_value_ar, + str) and str(cell_value_ar).strip().lower() in [ + "k.a.", + "k.a. (nur cookie-banner erkannt)", + "k.a. (fehler)"]) + # Holen Sie den Wert aus Spalte D (CRM Website) (nutzt interne + # Helfer _get_cell_value_safe) + website_url = self._get_cell_value_safe( + row, "CRM Website").strip() # Block 1 Column Map + # Pruefen Sie, ob die Website URL (D) vorhanden und gueltig + # aussehend ist. + website_url_is_valid_looking = website_url and isinstance( + website_url, + str) and website_url.lower() not in [ + "k.a.", + "kein artikel gefunden", + "fehler bei suche", + "http:"] # Fuege "http:" hinzu basierend auf Log - # Holen Sie den Wert aus Spalte D (CRM Website) (nutzt interne Helfer _get_cell_value_safe) - website_url = self._get_cell_value_safe(row, "CRM Website").strip() # Block 1 Column Map - # Pruefen Sie, ob die Website URL (D) vorhanden und gueltig aussehend ist. - website_url_is_valid_looking = website_url and isinstance(website_url, str) and website_url.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu basierend auf Log - - - # Verarbeitung ist noetig, wenn AR leer/default ist UND D gefuellt/gueltig aussieht. + # Verarbeitung ist noetig, wenn AR leer/default ist UND D + # gefuellt/gueltig aussieht. processing_needed_for_row = ar_is_empty_or_default and website_url_is_valid_looking - # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level - log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) + log_check = ( + i < start_sheet_row + + 5) or ( + i % + 100 == 0) or (processing_needed_for_row) if log_check: - company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Scraping Check): AR leer/default? {ar_is_empty_or_default}, D gueltig? {website_url_is_valid_looking}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT - + company_name = self._get_cell_value_safe( + row, "CRM Name").strip() # Block 1 Column Map + self.logger.debug( + f"Zeile {i} ({company_name[:50]}... Website Scraping Check): AR leer/default? {ar_is_empty_or_default}, D gueltig? {website_url_is_valid_looking}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist if not processing_needed_for_row: - skipped_count += 1 # Zaehlen als uebersprungene Zeile - # Zaehlen Sie speziell, wenn die Zeile wegen fehlender gueltiger URL uebersprungen wurde. - if not website_url_is_valid_looking: skipped_no_url += 1 - continue # Springe zur naechsten Zeile - + skipped_count += 1 # Zaehlen als uebersprungene Zeile + # Zaehlen Sie speziell, wenn die Zeile wegen fehlender + # gueltiger URL uebersprungen wurde. + if not website_url_is_valid_looking: + skipped_no_url += 1 + continue # Springe zur naechsten Zeile # --- Wenn Verarbeitung noetig: Fuege zur Batch-Liste hinzu --- - processed_count += 1 # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im Rahmen des Limits zaehlen) + # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im + # Rahmen des Limits zaehlen) + processed_count += 1 # Pruefe das Limit fuer verarbeitete Zeilen - if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: - # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_website_scraping_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT - break # Brich die Schleife ab + if limit is not None and isinstance( + limit, int) and limit > 0 and processed_count > limit: + # Wenn das Limit erreicht ist und es ein positives Limit gibt + self.logger.info( + f"Verarbeitungslimit ({limit}) fuer process_website_scraping_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT + break # Brich die Schleife ab - - # Fuege die benoetigten Daten fuer den Task hinzu (Zeilennummer und URL) - tasks_for_processing_batch.append({"row_num": i, "url": website_url}) + # Fuege die benoetigten Daten fuer den Task hinzu (Zeilennummer und + # URL) + tasks_for_processing_batch.append( + {"row_num": i, "url": website_url}) # Fuege die Zeilennummer zur Liste der Zeilennummern im Batch hinzu rows_in_current_scraping_batch.append(i) - # --- Verarbeite den Batch, wenn voll --- # Pruefe, ob die aktuelle Batch-Liste die definierte Groesse erreicht hat. # scraping_batch_size wird aus Config geholt (Block 1). @@ -1515,176 +2218,229 @@ class DataProcessor: # Logge den Start der Batch-Verarbeitung batch_start_row = tasks_for_processing_batch[0]['row_num'] batch_end_row = tasks_for_processing_batch[-1]['row_num'] - self.logger.debug(f"\n--- Starte Website-Scraping Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT + self.logger.debug( + f"\n--- Starte Website-Scraping Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT - scraping_results = {} # Dictionary zum Speichern der Ergebnisse {row_num: raw_text} - batch_error_count = 0 # Fehlerzaehler fuer diesen spezifischen Batch + scraping_results = {} # Dictionary zum Speichern der Ergebnisse {row_num: raw_text} + batch_error_count = 0 # Fehlerzaehler fuer diesen spezifischen Batch - self.logger.debug(f" Scrape {len(tasks_for_processing_batch)} Websites parallel (max {max_scraping_workers} worker)...") # <<< GEÄNDERT + self.logger.debug( + f" Scrape {len(tasks_for_processing_batch)} Websites parallel (max {max_scraping_workers} worker)...") # <<< GEÄNDERT # Nutzt concurrent.futures.ThreadPoolExecutor fuer paralleles Scraping. # max_workers wird aus Config geholt (Block 1). with concurrent.futures.ThreadPoolExecutor(max_workers=max_scraping_workers) as executor: # Map tasks to futures. Ruft die INTERNE Worker-Funktion auf. - # Uebergibt das task_info Dictionary und die globale Funktion get_website_raw (Block 11) als Argument. - future_to_task = {executor.submit(self._scrape_raw_text_task, task, get_website_raw): task for task in tasks_for_processing_batch} # <<< Korrigiert: interne Methode + # Uebergibt das task_info Dictionary und die globale + # Funktion get_website_raw (Block 11) als Argument. + future_to_task = { + executor.submit( + self._scrape_raw_text_task, + task, + get_website_raw): task for task in tasks_for_processing_batch} # <<< Korrigiert: interne Methode # Verarbeite die Ergebnisse, sobald sie fertig sind. - for future in concurrent.futures.as_completed(future_to_task): - task = future_to_task[future] # Holen Sie die urspruenglichen Task-Daten (Dict) + for future in concurrent.futures.as_completed( + future_to_task): + # Holen Sie die urspruenglichen Task-Daten (Dict) + task = future_to_task[future] try: - # Holen Sie das Ergebnis vom Future. Wenn die Worker-Funktion eine Exception wirft, wird diese hier gefangen. - result = future.result() # Ergebnis ist ein Dictionary {'row_num': ..., 'raw_text': ..., 'error': ...} - # Speichere das Ergebnis im scraping_results Dictionary - scraping_results[result['row_num']] = result['raw_text'] - # Wenn der Worker einen Fehler gemeldet hat (z.B. durch Fehlerstring im raw_text oder error-Feld) + # Holen Sie das Ergebnis vom Future. Wenn die + # Worker-Funktion eine Exception wirft, wird diese + # hier gefangen. + # Ergebnis ist ein Dictionary {'row_num': ..., + # 'raw_text': ..., 'error': ...} + result = future.result() + # Speichere das Ergebnis im scraping_results + # Dictionary + scraping_results[result['row_num'] + ] = result['raw_text'] + # Wenn der Worker einen Fehler gemeldet hat (z.B. + # durch Fehlerstring im raw_text oder error-Feld) if result.get('error'): - batch_error_count += 1 # Erhoehe den Fehlerzaehler fuer diesen Batch + batch_error_count += 1 # Erhoehe den Fehlerzaehler fuer diesen Batch except Exception as exc: # Dieser Block faengt unerwartete Fehler ab, die waehrend der Future-Ergebnis-Abfrage auftreten. - # Die meisten Fehler sollten von get_website_raws retry/logging behandelt werden. - row_num = task['row_num'] # Zeilennummer aus den Task-Daten - err_msg = f"Unerwarteter Fehler bei Ergebnisabfrage Scraping Task Zeile {row_num} ({task['url'][:100]}): {type(exc).__name__} - {exc}" # Gekuerzt loggen - self.logger.error(err_msg) # <<< GEÄNDERT - # Setze einen Standard-Fehlerwert fuer diese Zeile im Ergebnis + # Die meisten Fehler sollten von get_website_raws + # retry/logging behandelt werden. + # Zeilennummer aus den Task-Daten + row_num = task['row_num'] + # Gekuerzt loggen + err_msg = f"Unerwarteter Fehler bei Ergebnisabfrage Scraping Task Zeile {row_num} ({task['url'][:100]}): {type(exc).__name__} - {exc}" + self.logger.error(err_msg) # <<< GEÄNDERT + # Setze einen Standard-Fehlerwert fuer diese Zeile + # im Ergebnis scraping_results[row_num] = "k.A. (Unerwarteter Fehler Task)" - batch_error_count += 1 # Erhoehe den Fehlerzaehler - - - self.logger.debug(f" Scraping fuer Batch beendet. {len(scraping_results)} Ergebnisse erhalten ({batch_error_count} Fehler in diesem Batch).") # <<< GEÄNDERT + batch_error_count += 1 # Erhoehe den Fehlerzaehler + self.logger.debug( + f" Scraping fuer Batch beendet. {len(scraping_results)} Ergebnisse erhalten ({batch_error_count} Fehler in diesem Batch).") # <<< GEÄNDERT # Sammle Sheet Updates (AR, AT, AP) fuer diesen Batch. # Dies geschieht jetzt nach der parallelen Verarbeitung. if scraping_results: # Aktueller Zeitstempel und Version current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - current_version = getattr(Config, 'VERSION', 'unknown') # Block 1 Config Attribut - batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen - + current_version = getattr( + Config, 'VERSION', 'unknown') # Block 1 Config Attribut + batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen # Iteriere ueber die Zeilennummern im Batch, fuer die Ergebnisse vorliegen. # Ergebnisse koennen Fehlerwerte enthalten. for row_num, raw_text_res in scraping_results.items(): # Fuege Updates fuer AR, AT und AP hinzu (nutzt interne Helfer) # AR: Roh extrahierter Text (kann auch Fehlerwert sein) - batch_sheet_updates.append({'range': f'{rohtext_col_letter}{row_num}', 'values': [[raw_text_res]]}) # Block 1 Column Map - # AT: Timestamp des Scraping-Versuchs (immer setzen, wenn versucht wurde) - batch_sheet_updates.append({'range': f'{timestamp_col_letter}{row_num}', 'values': [[current_timestamp]]}) # Block 1 Column Map + batch_sheet_updates.append({'range': f'{rohtext_col_letter}{row_num}', 'values': [ + [raw_text_res]]}) # Block 1 Column Map + # AT: Timestamp des Scraping-Versuchs (immer setzen, + # wenn versucht wurde) + batch_sheet_updates.append({'range': f'{timestamp_col_letter}{row_num}', 'values': [ + [current_timestamp]]}) # Block 1 Column Map # AP: Version des Skripts - batch_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]}) # Block 1 Column Map - + batch_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [ + [current_version]]}) # Block 1 Column Map # Sammle diese Batch-Updates fuer das groessere Batch-Update am Ende oder bei Limit. # update_batch_row_limit wird aus Config geholt (Block 1). all_sheet_updates.extend(batch_sheet_updates) - # Leere den Scraping-Batch fuer die naechste Iteration tasks_for_processing_batch = [] rows_in_current_scraping_batch = [] - # Sende gesammelte Sheet Updates, wenn das Update-Batch-Limit erreicht ist. - # Updates pro Zeile sind 3 (AR, AT, AP). Anzahl der Zeilen = len(all_sheet_updates) / 3. - rows_in_update_batch = len(all_sheet_updates) // 3 # Ganzzahl-Division + # Updates pro Zeile sind 3 (AR, AT, AP). Anzahl der Zeilen = + # len(all_sheet_updates) / 3. + rows_in_update_batch = len( + all_sheet_updates) // 3 # Ganzzahl-Division if rows_in_update_batch >= update_batch_row_limit: - self.logger.debug(f" Sende gesammelte Sheet-Updates ({rows_in_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT + self.logger.debug( + f" Sende gesammelte Sheet-Updates ({rows_in_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. # Wenn es fehlschlaegt, wird es intern geloggt. - success = self.sheet_handler.batch_update_cells(all_sheet_updates) + success = self.sheet_handler.batch_update_cells( + all_sheet_updates) if success: - self.logger.info(f" Sheet-Update fuer {rows_in_update_batch} Zeilen erfolgreich.") # <<< GEÄNDERT + self.logger.info( + f" Sheet-Update fuer {rows_in_update_batch} Zeilen erfolgreich.") # <<< GEÄNDERT # Der Fehlerfall wird von batch_update_cells geloggt # Leere die gesammelten Updates nach dem Senden. all_sheet_updates = [] - # rows_in_update_batch muss nicht explizit zurueckgesetzt werden, da es aus len(all_sheet_updates) berechnet wird. - + # rows_in_update_batch muss nicht explizit zurueckgesetzt + # werden, da es aus len(all_sheet_updates) berechnet wird. # Keine Pause hier nach jedem kleinen Scraping-Batch, da wir auf batch_update warten. # Die Pause kommt erst nach dem Batch-Update (oder am Ende des Modus). # time.sleep(0.1) # Optionale kurze Pause - # --- Verarbeitung des letzten unvollstaendigen Scraping-Batches nach der Schleife --- - # Fuehre den letzten Batch aus, wenn nach der Hauptschleife noch Tasks in der Batch-Liste sind. + # Fuehre den letzten Batch aus, wenn nach der Hauptschleife noch Tasks + # in der Batch-Liste sind. if tasks_for_processing_batch: # Logge den Start des finalen Batches batch_start_row = tasks_for_processing_batch[0]['row_num'] batch_end_row = tasks_for_processing_batch[-1]['row_num'] - self.logger.debug(f"\n--- Starte FINALEN Website-Scraping Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT + self.logger.debug( + f"\n--- Starte FINALEN Website-Scraping Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT - scraping_results = {} # Dictionary fuer die Ergebnisse - batch_error_count = 0 # Fehlerzaehler + scraping_results = {} # Dictionary fuer die Ergebnisse + batch_error_count = 0 # Fehlerzaehler - self.logger.debug(f" Scrape {len(tasks_for_processing_batch)} Websites parallel (max {max_scraping_workers} worker)...") # <<< GEÄNDERT + self.logger.debug( + f" Scrape {len(tasks_for_processing_batch)} Websites parallel (max {max_scraping_workers} worker)...") # <<< GEÄNDERT with concurrent.futures.ThreadPoolExecutor(max_workers=max_scraping_workers) as executor: - # Map tasks to futures. Ruft die INTERNE Worker-Funktion auf. - future_to_task = {executor.submit(self._scrape_raw_text_task, task, get_website_raw): task for task in tasks_for_processing_batch} # <<< Korrigiert: interne Methode + # Map tasks to futures. Ruft die INTERNE Worker-Funktion auf. + future_to_task = { + executor.submit( + self._scrape_raw_text_task, + task, + get_website_raw): task for task in tasks_for_processing_batch} # <<< Korrigiert: interne Methode - # Verarbeite die Ergebnisse - for future in concurrent.futures.as_completed(future_to_task): - task = future_to_task[future] # Holen Sie die urspruenglichen Task-Daten - try: - result = future.result() # Holen Sie das Ergebnis - scraping_results[result['row_num']] = result['raw_text'] - # Pruefe, ob der Worker einen Fehler gemeldet hat - if result.get('error'): batch_error_count += 1 - except Exception as exc: - # Faengt unerwartete Fehler bei der Ergebnisabfrage ab - row_num = task['row_num'] - err_msg = f"Unerwarteter Fehler bei Ergebnisabfrage Scraping Task Zeile {row_num} ({task['url'][:100]}): {type(exc).__name__} - {exc}" # Gekuerzt loggen - self.logger.error(err_msg) # <<< GEÄNDERT - # Setze einen Standard-Fehlerwert - scraping_results[row_num] = "k.A. (Unerwarteter Fehler Task)" - batch_error_count += 1 + # Verarbeite die Ergebnisse + for future in concurrent.futures.as_completed(future_to_task): + # Holen Sie die urspruenglichen Task-Daten + task = future_to_task[future] + try: + result = future.result() # Holen Sie das Ergebnis + scraping_results[result['row_num'] + ] = result['raw_text'] + # Pruefe, ob der Worker einen Fehler gemeldet hat + if result.get('error'): + batch_error_count += 1 + except Exception as exc: + # Faengt unerwartete Fehler bei der Ergebnisabfrage ab + row_num = task['row_num'] + # Gekuerzt loggen + err_msg = f"Unerwarteter Fehler bei Ergebnisabfrage Scraping Task Zeile {row_num} ({task['url'][:100]}): {type(exc).__name__} - {exc}" + self.logger.error(err_msg) # <<< GEÄNDERT + # Setze einen Standard-Fehlerwert + scraping_results[row_num] = "k.A. (Unerwarteter Fehler Task)" + batch_error_count += 1 - - self.logger.debug(f" FINALER Scraping Batch beendet. {len(scraping_results)} Ergebnisse erhalten ({batch_error_count} Fehler).") # <<< GEÄNDERT + self.logger.debug( + f" FINALER Scraping Batch beendet. {len(scraping_results)} Ergebnisse erhalten ({batch_error_count} Fehler).") # <<< GEÄNDERT # Sammle Sheet Updates (AR, AT, AP) fuer diesen finalen Batch. if scraping_results: current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - current_version = getattr(Config, 'VERSION', 'unknown') # Block 1 Config Attribut - batch_sheet_updates = [] # Updates fuer diesen spezifischen Batch - # Iteriere ueber die Zeilennummern im Batch, fuer die Ergebnisse vorliegen. + current_version = getattr( + Config, 'VERSION', 'unknown') # Block 1 Config Attribut + batch_sheet_updates = [] # Updates fuer diesen spezifischen Batch + # Iteriere ueber die Zeilennummern im Batch, fuer die + # Ergebnisse vorliegen. for row_num, raw_text_res in scraping_results.items(): # Fuege Updates fuer AR, AT und AP hinzu - batch_sheet_updates.append({'range': f'{rohtext_col_letter}{row_num}', 'values': [[raw_text_res]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{timestamp_col_letter}{row_num}', 'values': [[current_timestamp]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]}) # Block 1 Column Map - # Fuege diese Updates zur globalen Liste hinzu (wird dann nur noch einmal gesendet) + batch_sheet_updates.append({'range': f'{rohtext_col_letter}{row_num}', 'values': [ + [raw_text_res]]}) # Block 1 Column Map + batch_sheet_updates.append({'range': f'{timestamp_col_letter}{row_num}', 'values': [ + [current_timestamp]]}) # Block 1 Column Map + batch_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [ + [current_version]]}) # Block 1 Column Map + # Fuege diese Updates zur globalen Liste hinzu (wird dann nur + # noch einmal gesendet) all_sheet_updates.extend(batch_sheet_updates) - # --- Finale Sheet Updates senden --- - # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update. + # Sende alle verbleibenden gesammelten Updates in einem letzten + # Batch-Update. if all_sheet_updates: - rows_in_final_update_batch = len(all_sheet_updates) // 3 # Updates pro Zeile ist 3 - self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({rows_in_final_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. + rows_in_final_update_batch = len( + all_sheet_updates) // 3 # Updates pro Zeile ist 3 + self.logger.info( + f"Sende FINALE gesammelte Sheet-Updates ({rows_in_final_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT + # Nutzt die batch_update_cells Methode des Sheet Handlers (Block + # 14) mit Retry. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT + # <<< GEÄNDERT + self.logger.info(f"FINALES Sheet-Update erfolgreich.") # Der Fehlerfall wird von batch_update_cells geloggt - # Logge den Abschluss des Modus - self.logger.info(f"Website-Scraping (Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT + self.logger.info( + f"Website-Scraping (Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT - def process_summarization_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None): + def process_summarization_batch( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None): """ Batch-Prozess NUR fuer Website-Zusammenfassung. """ - self.logger.info(f"Starte Website-Zusammenfassung (Batch). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") + self.logger.info( + f"Starte Website-Zusammenfassung (Batch). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") if start_sheet_row is None: - start_data_index = self.sheet_handler.get_start_row_index(check_column_key="Website Zusammenfassung") - if start_data_index == -1: return - start_sheet_row = start_data_index + self.sheet_handler._header_rows + 1 + start_data_index = self.sheet_handler.get_start_row_index( + check_column_key="Website Zusammenfassung") + if start_data_index == -1: + return + start_sheet_row = start_data_index + self.sheet_handler._header_rows + 1 else: - if not self.sheet_handler.load_data(): return + if not self.sheet_handler.load_data(): + return """ Batch-Prozess NUR fuer Website-Zusammenfassung (AS). Laedt Daten neu, prueft, ob Rohtext (AR) vorhanden und Zusammenfassung (AS) fehlt. @@ -1697,76 +2453,90 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Batch-Laufs - self.logger.info(f"Starte Website-Zusammenfassung (Batch AS, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT - + self.logger.info( + f"Starte Website-Zusammenfassung (Batch AS, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden und Startzeile ermitteln --- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AS...") # <<< GEÄNDERT - # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AS (Block 1 Column Map). - # Standardmaessig ab Zeile 7 - start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Website Zusammenfassung", min_sheet_row=7) + self.logger.info( + "Automatische Ermittlung der Startzeile basierend auf leeren AS...") # <<< GEÄNDERT + # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AS (Block 1 Column Map). + # Standardmaessig ab Zeile 7 + start_data_index_no_header = self.sheet_handler.get_start_row_index( + check_column_key="Website Zusammenfassung", min_sheet_row=7) - # Wenn get_start_row_index -1 zurueckgibt (Fehler) - if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT - return # Beende die Methode + # Wenn get_start_row_index -1 zurueckgibt (Fehler) + if start_data_index_no_header == -1: + self.logger.error( + "FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT + return # Beende die Methode - # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index - start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AS Zelle): {start_sheet_row}") # <<< GEÄNDERT + # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten + # Daten-Index + start_sheet_row = start_data_index_no_header + \ + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut + self.logger.info( + f"Automatisch ermittelte Startzeile (erste leere AS Zelle): {start_sheet_row}") # <<< GEÄNDERT else: # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. - # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). + # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block + # 2). if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_summarization_batch.") # <<< GEÄNDERT - return # Beende die Methode, wenn das Laden fehlschlaegt - - - # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. - all_data = self.sheet_handler.get_all_data_with_headers(); - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). - header_rows = self.sheet_handler._header_rows; - total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet + self.logger.error( + "FEHLER beim Laden der Daten fuer process_summarization_batch.") # <<< GEÄNDERT + return # Beende die Methode, wenn das Laden fehlschlaegt + # Holen Sie die gesamte Datenliste (inklusive Header) aus dem + # SheetHandler. + all_data = self.sheet_handler.get_all_data_with_headers() + # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar + # (Block 14). + header_rows = self.sheet_handler._header_rows + total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet # Berechne Endzeile, wenn nicht manuell gesetzt if end_sheet_row is None: - end_sheet_row = total_sheet_rows # Bis zur letzten Zeile + end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den verarbeitungsbereich - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT + self.logger.info( + f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT - # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) + # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht + # ueber Gesamtzeilen) if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT - return # Beende die Methode, wenn der Bereich leer ist - + self.logger.info( + "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT + return # Beende die Methode, wenn der Bereich leer ist # --- Indizes und Buchstaben --- - # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind + # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP + # (Block 1) vorhanden sind required_keys = [ - "Website Rohtext", "Website Zusammenfassung", "Version", "CRM Name" # AR, AS, AP, B + "Website Rohtext", "Website Zusammenfassung", "Version", "CRM Name" # AR, AS, AP, B ] # Erstellen Sie ein Dictionary mit Schluesseln und Indizes col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} - # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden + # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden + # wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_summarization_batch: {missing}. Breche ab.") # <<< GEÄNDERT - return # Beende die Methode bei kritischem Fehler + self.logger.critical( + f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_summarization_batch: {missing}. Breche ab.") # <<< GEÄNDERT + return # Beende die Methode bei kritischem Fehler # Ermitteln Sie die Indizes und Buchstaben fuer Updates (AS, AP) rohtext_col_idx = col_indices["Website Rohtext"] summary_col_idx = col_indices["Website Zusammenfassung"] version_col_idx = col_indices["Version"] - name_col_idx = col_indices["CRM Name"] # Benoetigt fuer Logging - - summary_col_letter = self.sheet_handler._get_col_letter(summary_col_idx + 1) # Block 14 _get_col_letter - version_col_letter = self.sheet_handler._get_col_letter(version_col_idx + 1) + name_col_idx = col_indices["CRM Name"] # Benoetigt fuer Logging + summary_col_letter = self.sheet_handler._get_col_letter( + summary_col_idx + 1) # Block 14 _get_col_letter + version_col_letter = self.sheet_handler._get_col_letter( + version_col_idx + 1) # --- Verarbeitung --- # Holen Sie die Batch-Groesse fuer OpenAI-Aufrufe aus Config (Block 1) @@ -1774,238 +2544,315 @@ class DataProcessor: # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1) update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) + # Tasks fuer den aktuellen OpenAI Batch (Liste von Dicts) + tasks_for_openai_batch = [] + # 1-basierte Zeilennummern im aktuellen OpenAI Batch + rows_in_current_openai_batch = [] + # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) + all_sheet_updates = [] - tasks_for_openai_batch = [] # Tasks fuer den aktuellen OpenAI Batch (Liste von Dicts) - rows_in_current_openai_batch = [] # 1-basierte Zeilennummern im aktuellen OpenAI Batch - all_sheet_updates = [] # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) + # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den + # Batch aufgenommen werden (im Rahmen des Limits). + processed_count = 0 + # Zaehlt Zeilen, die uebersprungen wurden (wegen fehlendem Rohtext oder + # vorhandener Zusammenfassung). + skipped_count = 0 - - processed_count = 0 # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den Batch aufgenommen werden (im Rahmen des Limits). - skipped_count = 0 # Zaehlt Zeilen, die uebersprungen wurden (wegen fehlendem Rohtext oder vorhandener Zusammenfassung). - - - # Iteriere ueber die Sheet-Zeilen im definierten Bereich (1-basierte Sheet-Zeilennummer) + # Iteriere ueber die Sheet-Zeilen im definierten Bereich (1-basierte + # Sheet-Zeilennummer) for i in range(start_sheet_row, end_sheet_row + 1): - row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste + row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste # Pruefen Sie, ob das Ende des Sheets erreicht wurde - if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht - - - row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile + if row_index_in_list >= total_sheet_rows: + break # Ende des Sheets erreicht + row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile # Stellen Sie sicher, dass die Zeile nicht leer ist - if not any(cell and isinstance(cell, str) and cell.strip() for cell in row): - #self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") # Zu viel Laerm im Debug - skipped_count += 1 # Zaehlen als uebersprungen - continue # Springe zur naechsten Zeile - + if not any(cell and isinstance(cell, str) and cell.strip() + for cell in row): + # self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") + # # Zu viel Laerm im Debug + skipped_count += 1 # Zaehlen als uebersprungen + continue # Springe zur naechsten Zeile # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist --- # Kriterium: Website Rohtext (AR) ist vorhanden und gueltig (nicht k.A. oder Fehlerwerte). - # UND Website Zusammenfassung (AS) ist leer oder ein Standard-Fehlerwert. + # UND Website Zusammenfassung (AS) ist leer oder ein + # Standard-Fehlerwert. - # Holen Sie den Wert aus Spalte AR (Website Rohtext) (nutzt interne Helfer _get_cell_value_safe) - raw_text = self._get_cell_value_safe(row, "Website Rohtext") # Block 1 Column Map + # Holen Sie den Wert aus Spalte AR (Website Rohtext) (nutzt interne + # Helfer _get_cell_value_safe) + raw_text = self._get_cell_value_safe( + row, "Website Rohtext") # Block 1 Column Map # Pruefen Sie, ob AR gefuellt und gueltig ist. - raw_text_is_valid = raw_text and isinstance(raw_text, str) and str(raw_text).strip().lower() not in ["k.a.", "k.a. (nur cookie-banner erkannt)", "k.a. (fehler)"] + raw_text_is_valid = raw_text and isinstance( + raw_text, str) and str(raw_text).strip().lower() not in [ + "k.a.", "k.a. (nur cookie-banner erkannt)", "k.a. (fehler)"] + # Holen Sie den Wert aus Spalte AS (Website Zusammenfassung) (nutzt + # interne Helfer _get_cell_value_safe) + summary_value = self._get_cell_value_safe( + row, "Website Zusammenfassung") # Block 1 Column Map + # Pruefen Sie, ob AS leer ist oder einen Standard-Fehlerwert + # enthaelt. + summary_is_empty_or_default = not summary_value or ( + isinstance( + summary_value, + str) and str(summary_value).strip().lower() in [ + "k.a.", + "k.a. (keine zusammenfassung erhalten)"]) - # Holen Sie den Wert aus Spalte AS (Website Zusammenfassung) (nutzt interne Helfer _get_cell_value_safe) - summary_value = self._get_cell_value_safe(row, "Website Zusammenfassung") # Block 1 Column Map - # Pruefen Sie, ob AS leer ist oder einen Standard-Fehlerwert enthaelt. - summary_is_empty_or_default = not summary_value or (isinstance(summary_value, str) and str(summary_value).strip().lower() in ["k.a.", "k.a. (keine zusammenfassung erhalten)"]) - - - # Verarbeitung ist noetig, wenn AR gueltig ist UND AS leer/default ist. + # Verarbeitung ist noetig, wenn AR gueltig ist UND AS leer/default + # ist. processing_needed_for_row = raw_text_is_valid and summary_is_empty_or_default - # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level - log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) + log_check = ( + i < start_sheet_row + + 5) or ( + i % + 100 == 0) or (processing_needed_for_row) if log_check: - company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Summarization Check): AR gueltig? {raw_text_is_valid} (len={len(str(raw_text))}), AS leer/default? {summary_is_empty_or_default}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT - + company_name = self._get_cell_value_safe( + row, "CRM Name").strip() # Block 1 Column Map + self.logger.debug( + f"Zeile {i} ({company_name[:50]}... Website Summarization Check): AR gueltig? {raw_text_is_valid} (len={len(str(raw_text))}), AS leer/default? {summary_is_empty_or_default}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist if not processing_needed_for_row: - skipped_count += 1 # Zaehlen als uebersprungene Zeile - continue # Springe zur naechsten Zeile - + skipped_count += 1 # Zaehlen als uebersprungene Zeile + continue # Springe zur naechsten Zeile # --- Wenn Verarbeitung noetig: Fuege zur Batch-Liste fuer OpenAI hinzu --- - processed_count += 1 # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im Rahmen des Limits zaehlen) + # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im + # Rahmen des Limits zaehlen) + processed_count += 1 # Pruefe das Limit fuer verarbeitete Zeilen - if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: - # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_summarization_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT - break # Brich die Schleife ab + if limit is not None and isinstance( + limit, int) and limit > 0 and processed_count > limit: + # Wenn das Limit erreicht ist und es ein positives Limit gibt + self.logger.info( + f"Verarbeitungslimit ({limit}) fuer process_summarization_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT + break # Brich die Schleife ab - - # Fuege die benoetigten Daten fuer den OpenAI Batch hinzu (Zeilennummer und Rohtext) + # Fuege die benoetigten Daten fuer den OpenAI Batch hinzu + # (Zeilennummer und Rohtext) tasks_for_openai_batch.append({'row_num': i, 'raw_text': raw_text}) # Fuege die Zeilennummer zur Liste der Zeilennummern im Batch hinzu rows_in_current_openai_batch.append(i) - # --- Verarbeite den Batch, wenn voll --- # Pruefe, ob die aktuelle Batch-Liste die definierte Groesse erreicht hat. # openai_batch_size wird aus Config geholt (Block 1). if len(tasks_for_openai_batch) >= openai_batch_size: - # Logge den Start der Batch-Verarbeitung - batch_start_row = tasks_for_openai_batch[0]['row_num'] - batch_end_row = tasks_for_openai_batch[-1]['row_num'] - self.logger.debug(f"\n--- Starte Website-Summarization Batch ({len(tasks_for_openai_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT + # Logge den Start der Batch-Verarbeitung + batch_start_row = tasks_for_openai_batch[0]['row_num'] + batch_end_row = tasks_for_openai_batch[-1]['row_num'] + self.logger.debug( + f"\n--- Starte Website-Summarization Batch ({len(tasks_for_openai_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT - # Rufe die globale Funktion auf, die den OpenAI Call fuer den Batch macht (Block 9). - # summarize_batch_openai ist mit retry_on_failure dekoriert (Block 2). - # Wenn summarize_batch_openai eine Exception wirft (nach Retries), wird diese hier gefangen. - # !!! KORRIGIERTER AUFRUF !!! - try: - # Rufen Sie die korrekte globale Funktion auf - batch_results = summarize_batch_openai(tasks_for_openai_batch) # <<< Korrigierter Aufruf (vorher war fälschlicherweise _process_verification_openai_batch) - # Ergebnisse sollten ein Dictionary {row_num: summary_text} sein, auch bei Fehlern. + # Rufe die globale Funktion auf, die den OpenAI Call fuer den Batch macht (Block 9). + # summarize_batch_openai ist mit retry_on_failure dekoriert (Block 2). + # Wenn summarize_batch_openai eine Exception wirft (nach Retries), wird diese hier gefangen. + # !!! KORRIGIERTER AUFRUF !!! + try: + # Rufen Sie die korrekte globale Funktion auf + # <<< Korrigierter Aufruf (vorher war fälschlicherweise _process_verification_openai_batch) + batch_results = summarize_batch_openai( + tasks_for_openai_batch) + # Ergebnisse sollten ein Dictionary {row_num: summary_text} + # sein, auch bei Fehlern. - # Sammle Sheet Updates (AS, AP) fuer diesen Batch - current_version = getattr(Config, 'VERSION', 'unknown') # Block 1 Config Attribut - batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen + # Sammle Sheet Updates (AS, AP) fuer diesen Batch + current_version = getattr( + Config, 'VERSION', 'unknown') # Block 1 Config Attribut + batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen - # Iteriere ueber die Zeilennummern, die in DIESEM OpenAI Batch waren - for row_num in rows_in_current_openai_batch: - # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary. - # Fallback auf einen Fehlerstring, wenn das Ergebnis fehlt (sollte nicht passieren, wenn summarize_batch_openai korrekt ist). - summary = batch_results.get(row_num, "k.A. (Batch Ergebnis fehlte)") - # Stelle sicher, dass 'k.A.' bei leeren/kurzen Summaries gesetzt wird - if not summary or (isinstance(summary, str) and summary.strip().lower() in ["k.a.", "k.a. (keine zusammenfassung erhalten)"]): - summary = "k.A. (Keine Zusammenfassung erhalten)" - # Fuege "k.A." oder Fehler an, wenn der Wert von summarize_batch_openai ein Fehlerstring ist - elif isinstance(summary, str) and (summary.startswith("k.A. (Fehler") or summary.startswith("FEHLER:")): - pass # Behalte den Fehlerstring von summarize_batch_openai + # Iteriere ueber die Zeilennummern, die in DIESEM OpenAI + # Batch waren + for row_num in rows_in_current_openai_batch: + # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary. + # Fallback auf einen Fehlerstring, wenn das Ergebnis + # fehlt (sollte nicht passieren, wenn + # summarize_batch_openai korrekt ist). + summary = batch_results.get( + row_num, "k.A. (Batch Ergebnis fehlte)") + # Stelle sicher, dass 'k.A.' bei leeren/kurzen + # Summaries gesetzt wird + if not summary or ( + isinstance( + summary, + str) and summary.strip().lower() in [ + "k.a.", + "k.a. (keine zusammenfassung erhalten)"]): + summary = "k.A. (Keine Zusammenfassung erhalten)" + # Fuege "k.A." oder Fehler an, wenn der Wert von + # summarize_batch_openai ein Fehlerstring ist + elif isinstance(summary, str) and (summary.startswith("k.A. (Fehler") or summary.startswith("FEHLER:")): + pass # Behalte den Fehlerstring von summarize_batch_openai - # Fuege Updates fuer AS und AP hinzu (nutzt interne Helfer) - batch_sheet_updates.append({'range': f'{summary_col_letter}{row_num}', 'values': [[summary]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]}) # Block 1 Column Map + # Fuege Updates fuer AS und AP hinzu (nutzt interne + # Helfer) + batch_sheet_updates.append({'range': f'{summary_col_letter}{row_num}', 'values': [ + [summary]]}) # Block 1 Column Map + batch_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [ + [current_version]]}) # Block 1 Column Map + # Sammle diese Batch-Updates fuer das groessere + # Batch-Update + all_sheet_updates.extend(batch_sheet_updates) - # Sammle diese Batch-Updates fuer das groessere Batch-Update - all_sheet_updates.extend(batch_sheet_updates) + except Exception as e_openai_batch: + # Wenn summarize_batch_openai eine Exception wirft (nach Retries) + # Der Fehler wird bereits vom retry_on_failure Decorator + # auf summarize_batch_openai geloggt. + self.logger.error( + f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Zusammenfassung: {e_openai_batch}") # <<< GEÄNDERT + # Logge den Traceback + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT + # Fügen Sie Fehlerwerte für alle Zeilen im Batch hinzu + current_version = getattr( + Config, 'VERSION', 'unknown') # Block 1 Config Attribut + for row_num in rows_in_current_openai_batch: + # Gekuerzt + error_summary = f"FEHLER OpenAI Batch: {str(e_openai_batch)[:100]}..." + # Fuege Updates mit Fehlerwerten fuer AS und AP hinzu + all_sheet_updates.append({'range': f'{summary_col_letter}{row_num}', 'values': [ + [error_summary]]}) # Block 1 Column Map + all_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [ + [current_version]]}) # Block 1 Column Map + # Leere den OpenAI-Batch zurueck + tasks_for_openai_batch = [] + rows_in_current_openai_batch = [] - except Exception as e_openai_batch: - # Wenn summarize_batch_openai eine Exception wirft (nach Retries) - # Der Fehler wird bereits vom retry_on_failure Decorator auf summarize_batch_openai geloggt. - self.logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Zusammenfassung: {e_openai_batch}") # <<< GEÄNDERT - # Logge den Traceback - self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT - # Fügen Sie Fehlerwerte für alle Zeilen im Batch hinzu - current_version = getattr(Config, 'VERSION', 'unknown') # Block 1 Config Attribut - for row_num in rows_in_current_openai_batch: - error_summary = f"FEHLER OpenAI Batch: {str(e_openai_batch)[:100]}..." # Gekuerzt - # Fuege Updates mit Fehlerwerten fuer AS und AP hinzu - all_sheet_updates.append({'range': f'{summary_col_letter}{row_num}', 'values': [[error_summary]]}) # Block 1 Column Map - all_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]}) # Block 1 Column Map + # Sende gesammelte Sheet Updates, wenn das Update-Batch-Limit erreicht ist. + # Updates pro Zeile sind 2 (AS, AP). Anzahl der Zeilen = + # len(all_sheet_updates) / 2. + rows_in_update_batch = len(all_sheet_updates) // 2 + if rows_in_update_batch >= update_batch_row_limit: + self.logger.debug( + f" Sende gesammelte Sheet-Updates ({rows_in_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT + # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. + # Wenn es fehlschlaegt, wird es intern geloggt. + success = self.sheet_handler.batch_update_cells( + all_sheet_updates) + if success: + self.logger.info( + f" Sheet-Update fuer {rows_in_update_batch} Zeilen erfolgreich.") # <<< GEÄNDERT + # Der Fehlerfall wird von batch_update_cells geloggt - # Leere den OpenAI-Batch zurueck - tasks_for_openai_batch = [] - rows_in_current_openai_batch = [] - - - # Sende gesammelte Sheet Updates, wenn das Update-Batch-Limit erreicht ist. - # Updates pro Zeile sind 2 (AS, AP). Anzahl der Zeilen = len(all_sheet_updates) / 2. - rows_in_update_batch = len(all_sheet_updates) // 2 - - if rows_in_update_batch >= update_batch_row_limit: - self.logger.debug(f" Sende gesammelte Sheet-Updates ({rows_in_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. - # Wenn es fehlschlaegt, wird es intern geloggt. - success = self.sheet_handler.batch_update_cells(all_sheet_updates) - if success: - self.logger.info(f" Sheet-Update fuer {rows_in_update_batch} Zeilen erfolgreich.") # <<< GEÄNDERT - # Der Fehlerfall wird von batch_update_cells geloggt - - # Leere die gesammelten Updates nach dem Senden. - all_sheet_updates = [] - - # Kurze Pause nach jedem OpenAI Batch (nutzt Config Block 1). - # Dies ist wichtig, um Rate Limits zu vermeiden. - pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.5 # 50% der Retry-Wartezeit - self.logger.debug(f"Warte {pause_duration:.2f}s vor naechstem Batch...") # <<< GEÄNDERT - time.sleep(pause_duration) + # Leere die gesammelten Updates nach dem Senden. + all_sheet_updates = [] + # Kurze Pause nach jedem OpenAI Batch (nutzt Config Block 1). + # Dies ist wichtig, um Rate Limits zu vermeiden. + pause_duration = getattr( + Config, 'RETRY_DELAY', 5) * 0.5 # 50% der Retry-Wartezeit + self.logger.debug( + f"Warte {pause_duration:.2f}s vor naechstem Batch...") # <<< GEÄNDERT + time.sleep(pause_duration) # --- Verarbeitung des letzten unvollstaendigen OpenAI Batches nach der Schleife --- # Wenn nach der Hauptschleife noch Tasks in der Batch-Liste sind. - if tasks_for_openai_batch: # Korrektur: War vorher `current_openai_batch_data` - # Logge den Start des finalen Batches - batch_start_row = tasks_for_openai_batch[0]['row_num'] # Korrektur: War vorher `current_openai_batch_data` - batch_end_row = tasks_for_openai_batch[-1]['row_num'] # Korrektur: War vorher `current_openai_batch_data` - self.logger.debug(f"\n--- Starte FINALEN Website-Summarization Batch ({len(tasks_for_openai_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT + if tasks_for_openai_batch: # Korrektur: War vorher `current_openai_batch_data` + # Logge den Start des finalen Batches + # Korrektur: War vorher `current_openai_batch_data` + batch_start_row = tasks_for_openai_batch[0]['row_num'] + # Korrektur: War vorher `current_openai_batch_data` + batch_end_row = tasks_for_openai_batch[-1]['row_num'] + self.logger.debug( + f"\n--- Starte FINALEN Website-Summarization Batch ({len(tasks_for_openai_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT - # Rufe die globale Funktion auf, die den OpenAI Call fuer den Batch macht (Block 9). - # summarize_batch_openai ist mit retry_on_failure dekoriert (Block 2). - # Wenn summarize_batch_openai eine Exception wirft (nach Retries), wird diese hier gefangen. - batch_results = None - try: - batch_results = summarize_batch_openai(tasks_for_openai_batch) # <<< Korrekter Aufruf Block 9, Korrektur: War vorher `current_openai_batch_data` - # Ergebnisse sollten ein Dictionary {row_num: summary_text} sein, auch bei Fehlern. + # Rufe die globale Funktion auf, die den OpenAI Call fuer den Batch macht (Block 9). + # summarize_batch_openai ist mit retry_on_failure dekoriert (Block 2). + # Wenn summarize_batch_openai eine Exception wirft (nach Retries), + # wird diese hier gefangen. + batch_results = None + try: + # <<< Korrekter Aufruf Block 9, Korrektur: War vorher `current_openai_batch_data` + batch_results = summarize_batch_openai(tasks_for_openai_batch) + # Ergebnisse sollten ein Dictionary {row_num: summary_text} + # sein, auch bei Fehlern. - # Sammle Sheet Updates (AS, AP) fuer diesen finalen Batch - current_version = getattr(Config, 'VERSION', 'unknown') # Block 1 Config Attribut - batch_sheet_updates = [] # Updates fuer diesen spezifischen Batch - # Iteriere ueber die Zeilennummern im Batch, fuer die Ergebnisse vorliegen. - for row_num in rows_in_current_openai_batch: - # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary. - summary = batch_results.get(row_num, "k.A. (Batch Ergebnis fehlte)") # Fallback + # Sammle Sheet Updates (AS, AP) fuer diesen finalen Batch + current_version = getattr( + Config, 'VERSION', 'unknown') # Block 1 Config Attribut + batch_sheet_updates = [] # Updates fuer diesen spezifischen Batch + # Iteriere ueber die Zeilennummern im Batch, fuer die + # Ergebnisse vorliegen. + for row_num in rows_in_current_openai_batch: + # Hole das Ergebnis fuer diese Zeile aus dem + # Ergebnis-Dictionary. + summary = batch_results.get( + row_num, "k.A. (Batch Ergebnis fehlte)") # Fallback - # Stelle sicher, dass 'k.A.' bei leeren/kurzen Summaries gesetzt wird - if not summary or (isinstance(summary, str) and summary.strip().lower() in ["k.a.", "k.a. (keine zusammenfassung erhalten)"]): - summary = "k.A. (Keine Zusammenfassung erhalten)" - # Fuege "k.A." oder Fehler an, wenn der Wert von summarize_batch_openai ein Fehlerstring ist - elif isinstance(summary, str) and (summary.startswith("k.A. (Fehler") or summary.startswith("FEHLER:")): - pass # Behalte den Fehlerstring von summarize_batch_openai + # Stelle sicher, dass 'k.A.' bei leeren/kurzen Summaries + # gesetzt wird + if not summary or ( + isinstance( + summary, + str) and summary.strip().lower() in [ + "k.a.", + "k.a. (keine zusammenfassung erhalten)"]): + summary = "k.A. (Keine Zusammenfassung erhalten)" + # Fuege "k.A." oder Fehler an, wenn der Wert von + # summarize_batch_openai ein Fehlerstring ist + elif isinstance(summary, str) and (summary.startswith("k.A. (Fehler") or summary.startswith("FEHLER:")): + pass # Behalte den Fehlerstring von summarize_batch_openai + # Fuege Updates fuer AS und AP hinzu + batch_sheet_updates.append({'range': f'{summary_col_letter}{row_num}', 'values': [ + [summary]]}) # Block 1 Column Map + batch_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [ + [current_version]]}) # Block 1 Column Map - # Fuege Updates fuer AS und AP hinzu - batch_sheet_updates.append({'range': f'{summary_col_letter}{row_num}', 'values': [[summary]]}) # Block 1 Column Map - batch_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]}) # Block 1 Column Map - - - # Fuege diese Updates zur globalen Liste hinzu (wird dann nur noch einmal gesendet) - all_sheet_updates.extend(batch_sheet_updates) - - - except Exception as e_openai_batch: - # Wenn summarize_batch_openai eine Exception wirft (nach Retries) - # Der Fehler wird bereits vom retry_on_failure Decorator auf summarize_batch_openai geloggt. - self.logger.error(f"Endgueltiger FEHLER beim FINALEN OpenAI-Batch-Aufruf fuer Zusammenfassung: {e_openai_batch}") # <<< GEÄNDERT - # Logge den Traceback - self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT - # Fügen Sie Fehlerwerte für alle Zeilen im Batch hinzu - current_version = getattr(Config, 'VERSION', 'unknown') # Block 1 Config Attribut - for row_num in rows_in_current_openai_batch: - error_summary = f"FEHLER OpenAI Batch: {str(e_openai_batch)[:100]}..." # Gekuerzt - # Fuege Updates mit Fehlerwerten fuer AS und AP hinzu - all_sheet_updates.append({'range': f'{summary_col_letter}{row_num}', 'values': [[error_summary]]}) # Block 1 Column Map - all_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [[current_version]]}) # Block 1 Column Map + # Fuege diese Updates zur globalen Liste hinzu (wird dann nur + # noch einmal gesendet) + all_sheet_updates.extend(batch_sheet_updates) + except Exception as e_openai_batch: + # Wenn summarize_batch_openai eine Exception wirft (nach Retries) + # Der Fehler wird bereits vom retry_on_failure Decorator auf + # summarize_batch_openai geloggt. + self.logger.error( + f"Endgueltiger FEHLER beim FINALEN OpenAI-Batch-Aufruf fuer Zusammenfassung: {e_openai_batch}") # <<< GEÄNDERT + # Logge den Traceback + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT + # Fügen Sie Fehlerwerte für alle Zeilen im Batch hinzu + current_version = getattr( + Config, 'VERSION', 'unknown') # Block 1 Config Attribut + for row_num in rows_in_current_openai_batch: + # Gekuerzt + error_summary = f"FEHLER OpenAI Batch: {str(e_openai_batch)[:100]}..." + # Fuege Updates mit Fehlerwerten fuer AS und AP hinzu + all_sheet_updates.append({'range': f'{summary_col_letter}{row_num}', 'values': [ + [error_summary]]}) # Block 1 Column Map + all_sheet_updates.append({'range': f'{version_col_letter}{row_num}', 'values': [ + [current_version]]}) # Block 1 Column Map # --- Finale Sheet Updates senden --- - # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update. + # Sende alle verbleibenden gesammelten Updates in einem letzten + # Batch-Update. if all_sheet_updates: - rows_in_final_update_batch = len(all_sheet_updates) // 2 # Updates pro Zeile ist 2 - self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({rows_in_final_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. + rows_in_final_update_batch = len( + all_sheet_updates) // 2 # Updates pro Zeile ist 2 + self.logger.info( + f"Sende FINALE gesammelte Sheet-Updates ({rows_in_final_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT + # Nutzt die batch_update_cells Methode des Sheet Handlers (Block + # 14) mit Retry. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT + # <<< GEÄNDERT + self.logger.info(f"FINALES Sheet-Update erfolgreich.") # Der Fehlerfall wird von batch_update_cells geloggt - # Logge den Abschluss des Modus - self.logger.info(f"Website-Zusammenfassung (Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT + self.logger.info( + f"Website-Zusammenfassung (Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT def evaluate_branch_task(self, task_data, openai_semaphore): """ @@ -2013,7 +2860,10 @@ class DataProcessor: """ logger = logging.getLogger(__name__ + ".evaluate_branch_task") row_num = task_data['row_num'] - result = {"branch": "k.A. (Fehler Task)", "consistency": "error", "justification": "Fehler in Worker-Task"} + result = { + "branch": "k.A. (Fehler Task)", + "consistency": "error", + "justification": "Fehler in Worker-Task"} error = None try: with openai_semaphore: @@ -2026,101 +2876,147 @@ class DataProcessor: error = f"Fehler bei Branchenevaluation Zeile {row_num}: {e}" logger.error(error) logger.debug(traceback.format_exc()) - result = {"branch": "FEHLER", "consistency": "error_task", "justification": error[:500]} + result = {"branch": "FEHLER", + "consistency": "error_task", + "justification": error[:500]} return {"row_num": row_num, "result": result, "error": error} - def process_branch_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None): + def process_branch_batch( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None): """ Batch-Prozess NUR fuer Brancheneinschaetzung. """ - self.logger.info(f"Starte Brancheneinschaetzung (Parallel Batch). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") - if not ALLOWED_TARGET_BRANCHES: load_target_schema() + self.logger.info( + f"Starte Brancheneinschaetzung (Parallel Batch). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") if not ALLOWED_TARGET_BRANCHES: - self.logger.critical("FEHLER: Ziel-Branchenschema konnte nicht geladen werden. Breche Batch ab.") + load_target_schema() + if not ALLOWED_TARGET_BRANCHES: + self.logger.critical( + "FEHLER: Ziel-Branchenschema konnte nicht geladen werden. Breche Batch ab.") return - self.logger.info(f"Starte Brancheneinschaetzung (Parallel Batch). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") + self.logger.info( + f"Starte Brancheneinschaetzung (Parallel Batch). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # --- Daten laden und Startzeile ermitteln --- if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren Timestamp letzte Pruefung (BC)...") - start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Pruefung", min_sheet_row=7) - if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") - return - start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere BC Zelle): {start_sheet_row}") + self.logger.info( + "Automatische Ermittlung der Startzeile basierend auf leeren Timestamp letzte Pruefung (BC)...") + start_data_index_no_header = self.sheet_handler.get_start_row_index( + check_column_key="Timestamp letzte Pruefung", min_sheet_row=7) + if start_data_index_no_header == -1: + self.logger.error( + "FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") + return + start_sheet_row = start_data_index_no_header + \ + self.sheet_handler._header_rows + 1 + self.logger.info( + f"Automatisch ermittelte Startzeile (erste leere BC Zelle): {start_sheet_row}") else: if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_branch_batch.") - return + self.logger.error( + "FEHLER beim Laden der Daten fuer process_branch_batch.") + return all_data = self.sheet_handler.get_all_data_with_headers() header_rows = self.sheet_handler._header_rows total_sheet_rows = len(all_data) if end_sheet_row is None: - end_sheet_row = total_sheet_rows - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") + end_sheet_row = total_sheet_rows + self.logger.info( + f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") - return + self.logger.info( + "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") + return # --- Indizes und Buchstaben --- required_keys = [ - "Timestamp letzte Pruefung", "CRM Branche", "CRM Beschreibung", "Wiki Branche", - "Wiki Kategorien", "Website Zusammenfassung", "Version", "Chat Vorschlag Branche", - "Chat Branche Konfidenz", "Chat Konsistenz Branche", "Chat Begruendung Abweichung Branche", - "CRM Name" - ] + "Timestamp letzte Pruefung", + "CRM Branche", + "CRM Beschreibung", + "Wiki Branche", + "Wiki Kategorien", + "Website Zusammenfassung", + "Version", + "Chat Vorschlag Branche", + "Chat Branche Konfidenz", + "Chat Konsistenz Branche", + "Chat Begruendung Abweichung Branche", + "CRM Name"] col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_branch_batch: {missing}. Breche ab.") + self.logger.critical( + f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_branch_batch: {missing}. Breche ab.") return MAX_BRANCH_WORKERS = getattr(Config, 'MAX_BRANCH_WORKERS', 10) - OPENAI_CONCURRENCY_LIMIT = getattr(Config, 'OPENAI_CONCURRENCY_LIMIT', 3) - processing_batch_size = getattr(Config, 'PROCESSING_BRANCH_BATCH_SIZE', 20) + OPENAI_CONCURRENCY_LIMIT = getattr( + Config, 'OPENAI_CONCURRENCY_LIMIT', 3) + processing_batch_size = getattr( + Config, 'PROCESSING_BRANCH_BATCH_SIZE', 20) tasks_for_current_batch = [] - processed_tasks_count = 0 # Zählt Tasks, die tatsächlich verarbeitet wurden - skipped_count = 0 + processed_tasks_count = 0 # Zählt Tasks, die tatsächlich verarbeitet wurden + skipped_count = 0 global ALLOWED_TARGET_BRANCHES if not ALLOWED_TARGET_BRANCHES: - load_target_schema() - if not ALLOWED_TARGET_BRANCHES: - self.logger.critical("FEHLER: Ziel-Branchenschema konnte nicht geladen werden. Breche Batch ab.") - return + load_target_schema() + if not ALLOWED_TARGET_BRANCHES: + self.logger.critical( + "FEHLER: Ziel-Branchenschema konnte nicht geladen werden. Breche Batch ab.") + return - # Funktion zum Verarbeiten eines einzelnen Batches (um Code-Duplikation zu reduzieren) + # Funktion zum Verarbeiten eines einzelnen Batches (um Code-Duplikation + # zu reduzieren) def _execute_and_write_batch(batch_tasks_to_run): - nonlocal processed_tasks_count # Zugriff auf die äußere Variable + nonlocal processed_tasks_count # Zugriff auf die äußere Variable if not batch_tasks_to_run: return batch_start_log = batch_tasks_to_run[0]['row_num'] batch_end_log = batch_tasks_to_run[-1]['row_num'] - self.logger.debug(f"\n--- Verarbeite Branch-Evaluation Batch ({len(batch_tasks_to_run)} Tasks, Zeilen {batch_start_log}-{batch_end_log}) ---") + self.logger.debug( + f"\n--- Verarbeite Branch-Evaluation Batch ({len(batch_tasks_to_run)} Tasks, Zeilen {batch_start_log}-{batch_end_log}) ---") current_batch_results = [] current_batch_errors = 0 openai_sem = threading.Semaphore(OPENAI_CONCURRENCY_LIMIT) with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_BRANCH_WORKERS) as executor: - future_map = {executor.submit(self.evaluate_branch_task, task, openai_sem): task for task in batch_tasks_to_run} + future_map = { + executor.submit( + self.evaluate_branch_task, + task, + openai_sem): task for task in batch_tasks_to_run} for future in concurrent.futures.as_completed(future_map): task_info = future_map[future] try: res_data = future.result() current_batch_results.append(res_data) - if res_data.get('error'): current_batch_errors +=1 + if res_data.get('error'): + current_batch_errors += 1 except Exception as exc_future: - self.logger.error(f"Exception im Future für Zeile {task_info['row_num']}: {exc_future}") - current_batch_results.append({"row_num": task_info['row_num'], "result": {"branch": "FEHLER FUTURE", "consistency": "error_task", "justification": str(exc_future)[:100]}, "error": str(exc_future)}) + self.logger.error( + f"Exception im Future für Zeile {task_info['row_num']}: {exc_future}") + current_batch_results.append( + { + "row_num": task_info['row_num'], + "result": { + "branch": "FEHLER FUTURE", + "consistency": "error_task", + "justification": str(exc_future)[ + :100]}, + "error": str(exc_future)}) current_batch_errors += 1 - - self.logger.debug(f" Batch ({batch_start_log}-{batch_end_log}) beendet. {len(current_batch_results)} Ergebnisse, {current_batch_errors} Fehler.") + + self.logger.debug( + f" Batch ({batch_start_log}-{batch_end_log}) beendet. {len(current_batch_results)} Ergebnisse, {current_batch_errors} Fehler.") if current_batch_results: ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -2130,105 +3026,196 @@ class DataProcessor: for item in current_batch_results: rn, res = item['row_num'], item['result'] self.logger.debug(f" Zeile {rn} (Ergebnis): {res}") - updates_this_batch.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{rn}', 'values': [[res.get("branch", "ERR BR")]]}) - updates_this_batch.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Branche Konfidenz"] + 1)}{rn}', 'values': [[res.get("confidence", "N/A CO")]]}) - updates_this_batch.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenz Branche"] + 1)}{rn}', 'values': [[res.get("consistency", "err CO")]]}) - updates_this_batch.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Branche"] + 1)}{rn}', 'values': [[res.get("justification", "No JU")]]}) - updates_this_batch.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Pruefung"] + 1)}{rn}', 'values': [[ts]]}) - updates_this_batch.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Version"] + 1)}{rn}', 'values': [[ver]]}) - + updates_this_batch.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{rn}', + 'values': [ + [ + res.get( + "branch", + "ERR BR")]]}) + updates_this_batch.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Branche Konfidenz"] + 1)}{rn}', + 'values': [ + [ + res.get( + "confidence", + "N/A CO")]]}) + updates_this_batch.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenz Branche"] + 1)}{rn}', + 'values': [ + [ + res.get( + "consistency", + "err CO")]]}) + updates_this_batch.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Branche"] + 1)}{rn}', + 'values': [ + [ + res.get( + "justification", + "No JU")]]}) + updates_this_batch.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Pruefung"] + 1)}{rn}', + 'values': [ + [ts]]}) + updates_this_batch.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Version"] + 1)}{rn}', + 'values': [ + [ver]]}) + if updates_this_batch: - self.logger.debug(f" Sende Sheet-Update für {len(current_batch_results)} Zeilen dieses Batches...") - s_upd = self.sheet_handler.batch_update_cells(updates_this_batch) - if s_upd: self.logger.info(f" Sheet-Update für Batch Zeilen {batch_start_log}-{batch_end_log} erfolgreich.") - - processed_tasks_count += len(batch_tasks_to_run) # Zähle verarbeitete Tasks - + self.logger.debug( + f" Sende Sheet-Update für {len(current_batch_results)} Zeilen dieses Batches...") + s_upd = self.sheet_handler.batch_update_cells( + updates_this_batch) + if s_upd: + self.logger.info( + f" Sheet-Update für Batch Zeilen {batch_start_log}-{batch_end_log} erfolgreich.") + + # Zähle verarbeitete Tasks + processed_tasks_count += len(batch_tasks_to_run) + pause_dur = getattr(Config, 'RETRY_DELAY', 5) * 0.8 - self.logger.debug(f"--- Batch ({batch_start_log}-{batch_end_log}) abgeschlossen. Warte {pause_dur:.2f}s ---") + self.logger.debug( + f"--- Batch ({batch_start_log}-{batch_end_log}) abgeschlossen. Warte {pause_dur:.2f}s ---") time.sleep(pause_dur) # Ende der Hilfsfunktion _execute_and_write_batch # Hauptschleife über die Zeilen for i in range(start_sheet_row, end_sheet_row + 1): - if limit is not None and processed_tasks_count >= limit: # Prüfe Limit für *tatsächlich verarbeitete* Tasks - self.logger.info(f"Verarbeitungslimit ({limit}) erreicht. Stoppe weitere Zeilenprüfung.") - break + # Prüfe Limit für *tatsächlich verarbeitete* Tasks + if limit is not None and processed_tasks_count >= limit: + self.logger.info( + f"Verarbeitungslimit ({limit}) erreicht. Stoppe weitere Zeilenprüfung.") + break - row_index_in_list = i - 1 - if row_index_in_list >= total_sheet_rows: break + row_index_in_list = i - 1 + if row_index_in_list >= total_sheet_rows: + break - row = all_data[row_index_in_list] - if not any(cell and isinstance(cell, str) and cell.strip() for cell in row): - skipped_count += 1 - continue - - company_name_log = self._get_cell_value_safe(row, "CRM Name").strip() - ao_value = self._get_cell_value_safe(row, "Timestamp letzte Pruefung").strip() - if ao_value: # Wenn Timestamp gesetzt ist, überspringen - skipped_count += 1 - continue + row = all_data[row_index_in_list] + if not any(cell and isinstance(cell, str) and cell.strip() + for cell in row): + skipped_count += 1 + continue + + company_name_log = self._get_cell_value_safe( + row, "CRM Name").strip() + ao_value = self._get_cell_value_safe( + row, "Timestamp letzte Pruefung").strip() + if ao_value: # Wenn Timestamp gesetzt ist, überspringen + skipped_count += 1 + continue # --- DEBUG BLOCK für info_sources_count (wie gehabt) --- - crm_branche_val = self._get_cell_value_safe(row, "CRM Branche").strip(); crm_beschreibung_val = self._get_cell_value_safe(row, "CRM Beschreibung").strip() - wiki_branche_val = self._get_cell_value_safe(row, "Wiki Branche").strip(); wiki_kategorien_val = self._get_cell_value_safe(row, "Wiki Kategorien").strip() - website_summary_val = self._get_cell_value_safe(row, "Website Zusammenfassung").strip() + crm_branche_val = self._get_cell_value_safe( + row, "CRM Branche").strip() + crm_beschreibung_val = self._get_cell_value_safe( + row, "CRM Beschreibung").strip() + wiki_branche_val = self._get_cell_value_safe( + row, "Wiki Branche").strip() + wiki_kategorien_val = self._get_cell_value_safe( + row, "Wiki Kategorien").strip() + website_summary_val = self._get_cell_value_safe( + row, "Website Zusammenfassung").strip() # ... (kompletter detaillierter Debug-Block für info_sources_count hier einfügen) ... - self.logger.debug(f"Zeile {i} ({company_name_log[:30]}...) - Rohwerte für Info-Quellen:") - sources_to_check = {"CRM Branche": crm_branche_val, "CRM Beschreibung": crm_beschreibung_val, "Wiki Branche": wiki_branche_val, "Wiki Kategorien": wiki_kategorien_val, "Website Zusammenfassung": website_summary_val} - info_sources_count = 0; counted_sources = [] + self.logger.debug( + f"Zeile {i} ({company_name_log[:30]}...) - Rohwerte für Info-Quellen:") + sources_to_check = { + "CRM Branche": crm_branche_val, + "CRM Beschreibung": crm_beschreibung_val, + "Wiki Branche": wiki_branche_val, + "Wiki Kategorien": wiki_kategorien_val, + "Website Zusammenfassung": website_summary_val} + info_sources_count = 0 + counted_sources = [] for source_name, val in sources_to_check.items(): - cond1 = bool(val); cond2 = isinstance(val, str); cond3 = False; cond4 = False; cond5 = False + cond1 = bool(val) + cond2 = isinstance(val, str) + cond3 = False + cond4 = False + cond5 = False if cond1 and cond2: - stripped_val = val.strip(); cond3 = bool(stripped_val) - if cond3: lower_stripped_val = stripped_val.lower(); cond4 = lower_stripped_val != "k.a."; cond5 = not stripped_val.upper().startswith("FEHLER") + stripped_val = val.strip() + cond3 = bool(stripped_val) + if cond3: + lower_stripped_val = stripped_val.lower() + cond4 = lower_stripped_val != "k.a." + cond5 = not stripped_val.upper().startswith("FEHLER") is_valid_source = cond1 and cond2 and cond3 and cond4 and cond5 - if is_valid_source: info_sources_count += 1; counted_sources.append(source_name) - self.logger.debug(f" Prüfe Quelle '{source_name}': Wert='{str(val)[:30]}...', c1?{cond1}, c2?{cond2}, c3?{cond3}, c4?{cond4}, c5?{cond5} -> Gültig? {is_valid_source}") - self.logger.debug(f"Zeile {i} ({company_name_log[:30]}...) - Gezählte valide Quellen: {info_sources_count} - {counted_sources}") - - if info_sources_count < 2: - self.logger.info(f"Zeile {i} ({company_name_log[:30]}...) (Branch Check): Uebersprungen (Timestamp BC leer, aber nur {info_sources_count} Informationsquellen verfuegbar: {counted_sources}). Mindestens 2 benoetigt.") - skipped_count += 1 - continue - - # Task zur Liste hinzufügen, wenn alle Kriterien erfüllt sind UND Limit noch nicht erreicht - if limit is None or (processed_tasks_count + len(tasks_for_current_batch)) < limit: - tasks_for_current_batch.append({ - "row_num": i, "crm_branche": crm_branche_val, "beschreibung": crm_beschreibung_val, - "wiki_branche": wiki_branche_val, "wiki_kategorien": wiki_kategorien_val, - "website_summary": website_summary_val - }) - elif limit is not None and (processed_tasks_count + len(tasks_for_current_batch)) >= limit : - # Wenn das Hinzufügen dieses Tasks das Limit erreichen oder überschreiten würde, - # füge ihn noch hinzu (wird im nächsten Batch-Check gekürzt) und beende dann die Schleife - tasks_for_current_batch.append({ - "row_num": i, "crm_branche": crm_branche_val, "beschreibung": crm_beschreibung_val, - "wiki_branche": wiki_branche_val, "wiki_kategorien": wiki_kategorien_val, - "website_summary": website_summary_val - }) - self.logger.info(f"Zeile {i} wurde als letzter Task vor Erreichen des Limits ({limit}) gesammelt.") - # Die execute_and_write_batch Logik wird getriggert, wenn die Schleife endet oder der Batch voll ist. + if is_valid_source: + info_sources_count += 1 + counted_sources.append(source_name) + self.logger.debug( + f" Prüfe Quelle '{source_name}': Wert='{str(val)[:30]}...', c1?{cond1}, c2?{cond2}, c3?{cond3}, c4?{cond4}, c5?{cond5} -> Gültig? {is_valid_source}") + self.logger.debug( + f"Zeile {i} ({company_name_log[:30]}...) - Gezählte valide Quellen: {info_sources_count} - {counted_sources}") + if info_sources_count < 2: + self.logger.info( + f"Zeile {i} ({company_name_log[:30]}...) (Branch Check): Uebersprungen (Timestamp BC leer, aber nur {info_sources_count} Informationsquellen verfuegbar: {counted_sources}). Mindestens 2 benoetigt.") + skipped_count += 1 + continue + + # Task zur Liste hinzufügen, wenn alle Kriterien erfüllt sind UND + # Limit noch nicht erreicht + if limit is None or ( + processed_tasks_count + + len(tasks_for_current_batch)) < limit: + tasks_for_current_batch.append({ + "row_num": i, "crm_branche": crm_branche_val, "beschreibung": crm_beschreibung_val, + "wiki_branche": wiki_branche_val, "wiki_kategorien": wiki_kategorien_val, + "website_summary": website_summary_val + }) + elif limit is not None and (processed_tasks_count + len(tasks_for_current_batch)) >= limit: + # Wenn das Hinzufügen dieses Tasks das Limit erreichen oder überschreiten würde, + # füge ihn noch hinzu (wird im nächsten Batch-Check gekürzt) + # und beende dann die Schleife + tasks_for_current_batch.append({ + "row_num": i, "crm_branche": crm_branche_val, "beschreibung": crm_beschreibung_val, + "wiki_branche": wiki_branche_val, "wiki_kategorien": wiki_kategorien_val, + "website_summary": website_summary_val + }) + self.logger.info( + f"Zeile {i} wurde als letzter Task vor Erreichen des Limits ({limit}) gesammelt.") + # Die execute_and_write_batch Logik wird getriggert, wenn die + # Schleife endet oder der Batch voll ist. # Batch ausführen, wenn voll ODER es die letzte Zeile ist - if len(tasks_for_current_batch) >= processing_batch_size or i == end_sheet_row: + if len( + tasks_for_current_batch) >= processing_batch_size or i == end_sheet_row: _execute_and_write_batch(tasks_for_current_batch) - tasks_for_current_batch = [] # Batch-Liste für den nächsten Durchlauf leeren - + tasks_for_current_batch = [] # Batch-Liste für den nächsten Durchlauf leeren + # Sicherstellen, dass ein eventuell nicht voller letzter Batch auch noch verarbeitet wird, - # falls die Schleife durch das Limit beendet wurde und noch Tasks übrig sind. + # falls die Schleife durch das Limit beendet wurde und noch Tasks übrig + # sind. if tasks_for_current_batch: - self.logger.debug(f"Verarbeite verbleibenden Rest-Batch von {len(tasks_for_current_batch)} Tasks...") + self.logger.debug( + f"Verarbeite verbleibenden Rest-Batch von {len(tasks_for_current_batch)} Tasks...") _execute_and_write_batch(tasks_for_current_batch) - self.logger.info(f"Brancheneinschaetzung (Parallel Batch) abgeschlossen. {processed_tasks_count} Zeilen verarbeitet, {skipped_count} Zeilen uebersprungen.") + self.logger.info( + f"Brancheneinschaetzung (Parallel Batch) abgeschlossen. {processed_tasks_count} Zeilen verarbeitet, {skipped_count} Zeilen uebersprungen.") - def process_find_wiki_serp(self, start_sheet_row=None, end_sheet_row=None, limit=None, min_employees=500, min_umsatz=200): + def process_find_wiki_serp( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None, + min_employees=500, + min_umsatz=200): """ Sucht fehlende Wikipedia-URLs ueber SerpAPI. """ - self.logger.info(f"Starte Modus 'find_wiki_serp'. Filter: (Umsatz > {min_umsatz} MIO € ODER MA > {min_employees}). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") + self.logger.info( + f"Starte Modus 'find_wiki_serp'. Filter: (Umsatz > {min_umsatz} MIO € ODER MA > {min_employees}). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") """ Sucht fehlende Wikipedia-URLs (Spalte M = k.A.) ueber SerpAPI fuer Unternehmen mit (Umsatz CRM > min_umsatz MIO € ODER Mitarbeiter CRM > min_employees) @@ -2246,76 +3233,99 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Batch-Laufs - self.logger.info(f"Starte Modus 'find_wiki_serp' (AY, M, A). Filter: (Umsatz CRM > {min_umsatz} MIO € ODER Mitarbeiter CRM > {min_employees}). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT - + self.logger.info( + f"Starte Modus 'find_wiki_serp' (AY, M, A). Filter: (Umsatz CRM > {min_umsatz} MIO € ODER Mitarbeiter CRM > {min_employees}). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden und Startzeile ermitteln --- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AY...") # <<< GEÄNDERT - # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AY (Block 1 Column Map). - # Standardmaessig ab Zeile 7 - start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="SerpAPI Wiki Search Timestamp", min_sheet_row=7) + self.logger.info( + "Automatische Ermittlung der Startzeile basierend auf leeren AY...") # <<< GEÄNDERT + # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AY (Block 1 Column Map). + # Standardmaessig ab Zeile 7 + start_data_index_no_header = self.sheet_handler.get_start_row_index( + check_column_key="SerpAPI Wiki Search Timestamp", min_sheet_row=7) - # Wenn get_start_row_index -1 zurueckgibt (Fehler) - if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT - return # Beende die Methode + # Wenn get_start_row_index -1 zurueckgibt (Fehler) + if start_data_index_no_header == -1: + self.logger.error( + "FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT + return # Beende die Methode - # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index - start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AY Zelle): {start_sheet_row}") # <<< GEÄNDERT + # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten + # Daten-Index + start_sheet_row = start_data_index_no_header + \ + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut + self.logger.info( + f"Automatisch ermittelte Startzeile (erste leere AY Zelle): {start_sheet_row}") # <<< GEÄNDERT else: # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. - # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). + # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block + # 2). if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_find_wiki_serp.") # <<< GEÄNDERT - return # Beende die Methode, wenn das Laden fehlschlaegt - - - # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. - all_data = self.sheet_handler.get_all_data_with_headers(); - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). - header_rows = self.sheet_handler._header_rows; - total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet + self.logger.error( + "FEHLER beim Laden der Daten fuer process_find_wiki_serp.") # <<< GEÄNDERT + return # Beende die Methode, wenn das Laden fehlschlaegt + # Holen Sie die gesamte Datenliste (inklusive Header) aus dem + # SheetHandler. + all_data = self.sheet_handler.get_all_data_with_headers() + # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar + # (Block 14). + header_rows = self.sheet_handler._header_rows + total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet # Berechne Endzeile, wenn nicht manuell gesetzt if end_sheet_row is None: - end_sheet_row = total_sheet_rows # Bis zur letzten Zeile + end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den verarbeitungsbereich - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT + self.logger.info( + f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT - # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) + # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht + # ueber Gesamtzeilen) if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT - return # Beende die Methode, wenn der Bereich leer ist - + self.logger.info( + "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT + return # Beende die Methode, wenn der Bereich leer ist # --- Indizes und Buchstaben --- - # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind + # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP + # (Block 1) vorhanden sind required_keys = [ - "SerpAPI Wiki Search Timestamp", "Wiki URL", "CRM Umsatz", "CRM Anzahl Mitarbeiter", # AY, M, J, K (Pruefkriterien / Timestamp) - "ReEval Flag", "CRM Name", "CRM Website", # A, B, D (Daten fuer Suche / Updates) - "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", # N-R (Spalten zum Leeren) - "Chat Wiki Konsistenzpruefung", "Chat Begruendung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", # S-U (Spalten zum Leeren) - "Begruendung bei Abweichung", "Wikipedia Timestamp", "Timestamp letzte Pruefung", # V, AN, AO (Spalten zum Leeren) - "Version", "Wiki Verif. Timestamp" # AP, AX (Spalten zum Leeren) + # AY, M, J, K (Pruefkriterien / Timestamp) + "SerpAPI Wiki Search Timestamp", "Wiki URL", "CRM Umsatz", "CRM Anzahl Mitarbeiter", + # A, B, D (Daten fuer Suche / Updates) + "ReEval Flag", "CRM Name", "CRM Website", + # N-R (Spalten zum Leeren) + "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", + # S-U (Spalten zum Leeren) + "Chat Wiki Konsistenzpruefung", "Chat Begruendung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", + # V, AN, AO (Spalten zum Leeren) + "Begruendung bei Abweichung", "Wikipedia Timestamp", "Timestamp letzte Pruefung", + "Version", "Wiki Verif. Timestamp" # AP, AX (Spalten zum Leeren) ] # Erstellen Sie ein Dictionary mit Schluesseln und Indizes col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} - # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden + # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden + # wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_find_wiki_serp: {missing}. Breche ab.") # <<< GEÄNDERT - return # Beende die Methode bei kritischem Fehler + self.logger.critical( + f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_find_wiki_serp: {missing}. Breche ab.") # <<< GEÄNDERT + return # Beende die Methode bei kritischem Fehler - # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt interne Helfer _get_col_letter Block 14) - ts_ay_letter = self.sheet_handler._get_col_letter(col_indices["SerpAPI Wiki Search Timestamp"] + 1) # Timestamp zu setzen (AY) - m_letter = self.sheet_handler._get_col_letter(col_indices["Wiki URL"] + 1) # Wiki URL Spalte (M) - a_letter = self.sheet_handler._get_col_letter(col_indices["ReEval Flag"] + 1) # ReEval Flag (A) + # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt + # interne Helfer _get_col_letter Block 14) + ts_ay_letter = self.sheet_handler._get_col_letter( + col_indices["SerpAPI Wiki Search Timestamp"] + + 1) # Timestamp zu setzen (AY) + m_letter = self.sheet_handler._get_col_letter( + col_indices["Wiki URL"] + 1) # Wiki URL Spalte (M) + a_letter = self.sheet_handler._get_col_letter( + col_indices["ReEval Flag"] + 1) # ReEval Flag (A) # Spalten N-V leeren. # N ist Wiki Absatz, V ist Begruendung bei Abweichung. @@ -2324,229 +3334,294 @@ class DataProcessor: # Erstellen Sie den Bereichsnamen (z.B. "N:V") n_letter = self.sheet_handler._get_col_letter(n_idx + 1) v_letter = self.sheet_handler._get_col_letter(v_idx + 1) - nv_range_letter = f'{n_letter}:{v_letter}' # z.B. N:V + nv_range_letter = f'{n_letter}:{v_letter}' # z.B. N:V # Erstellen Sie eine Liste von leeren Strings fuer diesen Bereich - empty_nv_values = [''] * (v_idx - n_idx + 1) # Anzahl der Spalten = V_Index - N_Index + 1 - + # Anzahl der Spalten = V_Index - N_Index + 1 + empty_nv_values = [''] * (v_idx - n_idx + 1) # Timestamps AN, AO, AP, AX leeren. - an_letter = self.sheet_handler._get_col_letter(col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS) - ao_letter = self.sheet_handler._get_col_letter(col_indices["Timestamp letzte Pruefung"] + 1) # AO (Chat Evaluation TS) - ap_letter = self.sheet_handler._get_col_letter(col_indices["Version"] + 1) # AP (Version) - ax_letter = self.sheet_handler._get_col_letter(col_indices["Wiki Verif. Timestamp"] + 1) # AX (Wiki Verif. TS) - + an_letter = self.sheet_handler._get_col_letter( + col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS) + ao_letter = self.sheet_handler._get_col_letter( + col_indices["Timestamp letzte Pruefung"] + + 1) # AO (Chat Evaluation TS) + ap_letter = self.sheet_handler._get_col_letter( + col_indices["Version"] + 1) # AP (Version) + ax_letter = self.sheet_handler._get_col_letter( + col_indices["Wiki Verif. Timestamp"] + 1) # AX (Wiki Verif. TS) # --- Verarbeitung --- # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1) update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) + # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) + all_sheet_updates = [] - all_sheet_updates = [] # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) - - - processed_count = 0 # Zaehlt Zeilen, fuer die SerpAPI versucht wurde (im Rahmen des Limits zaehlen). - skipped_count = 0 # Zaehlt Zeilen, die uebersprungen wurden (verschiedene Gruende). - found_urls_count = 0 # Zaehlt Zeilen, wo eine URL gefunden und eingetragen wurde. - + # Zaehlt Zeilen, fuer die SerpAPI versucht wurde (im Rahmen des Limits + # zaehlen). + processed_count = 0 + # Zaehlt Zeilen, die uebersprungen wurden (verschiedene Gruende). + skipped_count = 0 + # Zaehlt Zeilen, wo eine URL gefunden und eingetragen wurde. + found_urls_count = 0 # Aktueller Zeitstempel fuer den AY Timestamp now_timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte Sheet-Zeilennummer) + # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte + # Sheet-Zeilennummer) for i in range(start_sheet_row, end_sheet_row + 1): - row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste + row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste # Pruefen Sie, ob das Ende des Sheets erreicht wurde - if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht - - - row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile + if row_index_in_list >= total_sheet_rows: + break # Ende des Sheets erreicht + row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile # Stellen Sie sicher, dass die Zeile nicht leer ist - if not any(cell and isinstance(cell, str) and cell.strip() for cell in row): - #self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") # Zu viel Laerm im Debug - skipped_count += 1 # Zaehlen als uebersprungen - continue # Springe zur naechsten Zeile - + if not any(cell and isinstance(cell, str) and cell.strip() + for cell in row): + # self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") + # # Zu viel Laerm im Debug + skipped_count += 1 # Zaehlen als uebersprungen + continue # Springe zur naechsten Zeile # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist --- # Kriterium: SerpAPI Wiki Search Timestamp (AY) ist leer. # UND Wiki URL (M) ist leer oder "k.A.". - # UND (Umsatz CRM (J) > min_umsatz MIO € ODER Mitarbeiter CRM (K) > min_employees). - - # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne Helfer _get_cell_value_safe) - ay_value = self._get_cell_value_safe(row, "SerpAPI Wiki Search Timestamp").strip() # Block 1 Column Map - m_value = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map - umsatz_val_str = self._get_cell_value_safe(row, "CRM Umsatz") # Block 1 Column Map - ma_val_str = self._get_cell_value_safe(row, "CRM Anzahl Mitarbeiter") # Block 1 Column Map + # UND (Umsatz CRM (J) > min_umsatz MIO € ODER Mitarbeiter CRM (K) > + # min_employees). + # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne + # Helfer _get_cell_value_safe) + ay_value = self._get_cell_value_safe( + row, "SerpAPI Wiki Search Timestamp").strip() # Block 1 Column Map + m_value = self._get_cell_value_safe( + row, "Wiki URL").strip() # Block 1 Column Map + umsatz_val_str = self._get_cell_value_safe( + row, "CRM Umsatz") # Block 1 Column Map + ma_val_str = self._get_cell_value_safe( + row, "CRM Anzahl Mitarbeiter") # Block 1 Column Map # Pruefen Sie, ob AY leer ist. is_ay_empty = not ay_value # Pruefen Sie, ob M leer oder "k.A." ist. - is_m_empty_or_ka = not m_value or (isinstance(m_value, str) and m_value.lower() == "k.a.") + is_m_empty_or_ka = not m_value or (isinstance( + m_value, str) and m_value.lower() == "k.a.") # Nutze die globale Hilfsfunktion (Block 5), um die Werte fuer den Groessen-Filter zu bekommen. - # get_numeric_filter_value gibt 0 fuer ungueltige/leere Werte zurueck. - umsatz_val_mio = get_numeric_filter_value(umsatz_val_str, is_umsatz=True) + # get_numeric_filter_value gibt 0 fuer ungueltige/leere Werte + # zurueck. + umsatz_val_mio = get_numeric_filter_value( + umsatz_val_str, is_umsatz=True) ma_val_num = get_numeric_filter_value(ma_val_str, is_umsatz=False) # Pruefen Sie, ob das Groessen-Kriterium erfuellt ist. - size_criteria_met = (umsatz_val_mio > min_umsatz) or (ma_val_num > min_employees) + size_criteria_met = ( + umsatz_val_mio > min_umsatz) or ( + ma_val_num > min_employees) - - # Verarbeitung ist noetig, wenn AY leer ist UND M leer/k.A. ist UND das Groessen-Kriterium erfuellt ist. + # Verarbeitung ist noetig, wenn AY leer ist UND M leer/k.A. ist UND + # das Groessen-Kriterium erfuellt ist. processing_needed_for_row = is_ay_empty and is_m_empty_or_ka and size_criteria_met - # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level - log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) + log_check = ( + i < start_sheet_row + + 5) or ( + i % + 100 == 0) or (processing_needed_for_row) if log_check: - company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - self.logger.debug(f"Zeile {i} ({company_name[:50]}... SerpAPI Wiki Search Check): AY leer? {is_ay_empty}, M leer/k.A.? {is_m_empty_or_ka}, Groesse ({umsatz_val_mio:.1f} Mio, {ma_val_num} MA) Kriterium ({min_umsatz} Mio, {min_employees} MA)? {size_criteria_met}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT - + company_name = self._get_cell_value_safe( + row, "CRM Name").strip() # Block 1 Column Map + self.logger.debug( + f"Zeile {i} ({company_name[:50]}... SerpAPI Wiki Search Check): AY leer? {is_ay_empty}, M leer/k.A.? {is_m_empty_or_ka}, Groesse ({umsatz_val_mio:.1f} Mio, {ma_val_num} MA) Kriterium ({min_umsatz} Mio, {min_employees} MA)? {size_criteria_met}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist if not processing_needed_for_row: - skipped_count += 1 # Zaehlen als uebersprungene Zeile - continue # Springe zur naechsten Zeile - + skipped_count += 1 # Zaehlen als uebersprungene Zeile + continue # Springe zur naechsten Zeile # --- Wenn Verarbeitung noetig: Fuehre SerpAPI Suche aus --- - processed_count += 1 # Zaehle die Zeile, fuer die SerpAPI versucht wird (im Rahmen des Limits zaehlen) + # Zaehle die Zeile, fuer die SerpAPI versucht wird (im Rahmen des + # Limits zaehlen) + processed_count += 1 # Pruefe das Limit fuer verarbeitete Zeilen - if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: - # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_find_wiki_serp erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT - break # Brich die Schleife ab + if limit is not None and isinstance( + limit, int) and limit > 0 and processed_count > limit: + # Wenn das Limit erreicht ist und es ein positives Limit gibt + self.logger.info( + f"Verarbeitungslimit ({limit}) fuer process_find_wiki_serp erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT + break # Brich die Schleife ab + # Hole Firmenname und Website fuer die Suche (nutzt interne Helfer + # _get_cell_value_safe) + company_name = self._get_cell_value_safe( + row, "CRM Name").strip() # Block 1 Column Map + # Block 1 Column Map (Website kann fuer SerpAPI Kontext hilfreich + # sein) + website_url = self._get_cell_value_safe(row, "CRM Website").strip() - # Hole Firmenname und Website fuer die Suche (nutzt interne Helfer _get_cell_value_safe) - company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - website_url = self._get_cell_value_safe(row, "CRM Website").strip() # Block 1 Column Map (Website kann fuer SerpAPI Kontext hilfreich sein) - - # Wenn kein Firmenname vorhanden ist, kann die Suche nicht durchgefuehrt werden + # Wenn kein Firmenname vorhanden ist, kann die Suche nicht + # durchgefuehrt werden if not company_name: - self.logger.warning(f"Zeile {i}: Uebersprungen (kein Firmenname fuer Suche vorhanden in Spalte B).") # <<< GEÄNDERT - skipped_count += 1 # Zaehlen als uebersprungene Zeile, da Suche nicht moeglich + self.logger.warning( + f"Zeile {i}: Uebersprungen (kein Firmenname fuer Suche vorhanden in Spalte B).") # <<< GEÄNDERT + skipped_count += 1 # Zaehlen als uebersprungene Zeile, da Suche nicht moeglich # Setze AY Timestamp auch hier, um nicht immer wieder zu versuchen # Erstelle leeres Update-Dict, damit extend funktioniert updates = [] - updates.append({'range': f'{ts_ay_letter}{i}', 'values': [[now_timestamp_str]]}) # Block 1 Column Map - all_sheet_updates.extend(updates) # Fuege dieses einzelne Update zur Liste hinzu - updates = [] # Leere die lokale Liste - continue # Springe zur naechsten Zeile - - - self.logger.info(f"Zeile {i}: Suche Wiki-URL fuer '{company_name[:100]}...' (Umsatz (Mio): {umsatz_val_mio:.1f}, MA: {ma_val_num}) ueber SerpAPI...") # <<< GEÄNDERT + updates.append({'range': f'{ts_ay_letter}{i}', 'values': [ + [now_timestamp_str]]}) # Block 1 Column Map + # Fuege dieses einzelne Update zur Liste hinzu + all_sheet_updates.extend(updates) + updates = [] # Leere die lokale Liste + continue # Springe zur naechsten Zeile + self.logger.info( + f"Zeile {i}: Suche Wiki-URL fuer '{company_name[:100]}...' (Umsatz (Mio): {umsatz_val_mio:.1f}, MA: {ma_val_num}) ueber SerpAPI...") # <<< GEÄNDERT # Führe die SerpAPI Suche durch (nutzt globale Funktion Block 10 mit Retry). # serp_wikipedia_lookup ist mit retry_on_failure dekoriert (Block 2). - # Wenn serp_wikipedia_lookup nach Retries fehlschlaegt, wirft er eine Exception. - wiki_url_found = None # Initialisiere mit None + # Wenn serp_wikipedia_lookup nach Retries fehlschlaegt, wirft er + # eine Exception. + wiki_url_found = None # Initialisiere mit None try: - wiki_url_found = serp_wikipedia_lookup(company_name, website=website_url) # Nutzt globalen Helfer (Block 10) - # Wenn serp_wikipedia_lookup erfolgreich ist, gibt es die URL oder None zurueck. + wiki_url_found = serp_wikipedia_lookup( + company_name, website=website_url) # Nutzt globalen Helfer (Block 10) + # Wenn serp_wikipedia_lookup erfolgreich ist, gibt es die URL + # oder None zurueck. except Exception as e_serp_wiki: - # Wenn serp_wikipedia_lookup eine Exception wirft (nach Retries) - self.logger.error(f"FEHLER bei serp_wikipedia_lookup fuer Zeile {i} ('{company_name[:100]}...'): {e_serp_wiki}") # <<< GEÄNDERT - # wiki_url_found bleibt None. Fahren Sie fort. - pass # Fahren Sie fort, um Timestamp zu setzen und Updates vorzubereiten - + # Wenn serp_wikipedia_lookup eine Exception wirft (nach + # Retries) + self.logger.error( + f"FEHLER bei serp_wikipedia_lookup fuer Zeile {i} ('{company_name[:100]}...'): {e_serp_wiki}") # <<< GEÄNDERT + # wiki_url_found bleibt None. Fahren Sie fort. + pass # Fahren Sie fort, um Timestamp zu setzen und Updates vorzubereiten # --- Updates vorbereiten --- - # Timestamp AY IMMER setzen, nachdem der Versuch gemacht wurde, unabhaengig vom Ergebnis der Suche. - updates_for_row = [] # Lokale Liste fuer Updates dieser Zeile - - updates_for_row.append({'range': f'{ts_ay_letter}{i}', 'values': [[now_timestamp_str]]}) # Block 1 Column Map + # Timestamp AY IMMER setzen, nachdem der Versuch gemacht wurde, + # unabhaengig vom Ergebnis der Suche. + updates_for_row = [] # Lokale Liste fuer Updates dieser Zeile + updates_for_row.append({'range': f'{ts_ay_letter}{i}', 'values': [ + [now_timestamp_str]]}) # Block 1 Column Map # Wenn eine URL gefunden wurde, bereite weitere Updates vor. - # Eine gefundene URL ist ein String, der nicht None ist und nicht "k.A." oder Fehlerstring ist. - if wiki_url_found and isinstance(wiki_url_found, str) and wiki_url_found.lower() not in ["k.a.", "kein artikel gefunden"] and not wiki_url_found.startswith("FEHLER"): # Korrektur Pruefung - self.logger.info(f" -> URL gefunden: {wiki_url_found[:100]}... Bereite Update vor (Setze M, A; Loesche N-V, AN, AO, AP, AX).") # <<< GEÄNDERT - found_urls_count += 1 # Zaehle den Fund + # Eine gefundene URL ist ein String, der nicht None ist und nicht + # "k.A." oder Fehlerstring ist. + if wiki_url_found and isinstance( + wiki_url_found, + str) and wiki_url_found.lower() not in [ + "k.a.", + "kein artikel gefunden"] and not wiki_url_found.startswith("FEHLER"): # Korrektur Pruefung + self.logger.info( + f" -> URL gefunden: {wiki_url_found[:100]}... Bereite Update vor (Setze M, A; Loesche N-V, AN, AO, AP, AX).") # <<< GEÄNDERT + found_urls_count += 1 # Zaehle den Fund + # Setze M (Wiki URL) mit der gefundenen URL + updates_for_row.append({'range': f'{m_letter}{i}', 'values': [ + [wiki_url_found]]}) # Block 1 Column Map + # Setze ReEval Flag (A) auf 'x' (signalisiert, dass eine + # Re-Evaluation noetig ist) + updates_for_row.append({'range': f'{a_letter}{i}', 'values': [ + ['x']]}) # Block 1 Column Map - # Setze M (Wiki URL) mit der gefundenen URL - updates_for_row.append({'range': f'{m_letter}{i}', 'values': [[wiki_url_found]]}) # Block 1 Column Map - # Setze ReEval Flag (A) auf 'x' (signalisiert, dass eine Re-Evaluation noetig ist) - updates_for_row.append({'range': f'{a_letter}{i}', 'values': [['x']]}) # Block 1 Column Map - - - # Leere Spalten N-V. - # Fuege das Update zum Leeren des Bereichs V-Y hinzu, falls der Bereichsname ermittelt werden konnte. - if nv_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte. - updates_for_row.append({'range': f'{n_letter}{i}:{v_letter}{i}', 'values': [empty_nv_values]}) # Block 1 Column Map, lokale Variable - else: - self.logger.warning(f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.") # <<< GEÄNDERT - - - # Leere Timestamps AN, AO, AP, AX. - # Dies setzt die Zeile zurueck, damit andere Schritte sie spaeter bearbeiten. - updates_for_row.append({'range': f'{an_letter}{i}', 'values': [['']]}) # Block 1 Column Map - updates_for_row.append({'range': f'{ao_letter}{i}', 'values': [['']]}) # Block 1 Column Map - updates_for_row.append({'range': f'{ap_letter}{i}', 'values': [['']]}) # Block 1 Column Map - updates_for_row.append({'range': f'{ax_letter}{i}', 'values': [['']]}) # Block 1 Column Map + # Leere Spalten N-V. + # Fuege das Update zum Leeren des Bereichs V-Y hinzu, falls der + # Bereichsname ermittelt werden konnte. + if nv_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte. + updates_for_row.append({'range': f'{n_letter}{i}:{v_letter}{i}', 'values': [ + empty_nv_values]}) # Block 1 Column Map, lokale Variable + else: + self.logger.warning( + f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.") # <<< GEÄNDERT + # Leere Timestamps AN, AO, AP, AX. + # Dies setzt die Zeile zurueck, damit andere Schritte sie + # spaeter bearbeiten. + # Block 1 Column Map + updates_for_row.append( + {'range': f'{an_letter}{i}', 'values': [['']]}) + # Block 1 Column Map + updates_for_row.append( + {'range': f'{ao_letter}{i}', 'values': [['']]}) + # Block 1 Column Map + updates_for_row.append( + {'range': f'{ap_letter}{i}', 'values': [['']]}) + # Block 1 Column Map + updates_for_row.append( + {'range': f'{ax_letter}{i}', 'values': [['']]}) else: - # Wenn keine Wiki-URL ueber SerpAPI gefunden wurde - self.logger.debug(f" -> Keine Wiki-URL fuer '{company_name[:100]}...' ueber SerpAPI gefunden.") # <<< GEÄNDERT - # Nur AY Timestamp wird gesetzt, was bereits oben passiert ist. Keine weiteren Updates fuer M, A, N-V etc. - + # Wenn keine Wiki-URL ueber SerpAPI gefunden wurde + self.logger.debug( + f" -> Keine Wiki-URL fuer '{company_name[:100]}...' ueber SerpAPI gefunden.") # <<< GEÄNDERT + # Nur AY Timestamp wird gesetzt, was bereits oben passiert ist. + # Keine weiteren Updates fuer M, A, N-V etc. # Sammle die Updates fuer diese Zeile in der globalen Liste. all_sheet_updates.extend(updates_for_row) - # Sende gesammelte Sheet Updates, wenn das Update-Batch-Limit erreicht ist. # update_batch_row_limit wird aus Config geholt (Block 1). # Die Anzahl der Updates pro Zeile variiert (1 bei nicht gefunden, ca. 10+ bei gefunden). # Pruefen Sie einfach die Laenge der gesammelten Liste. - if len(all_sheet_updates) >= update_batch_row_limit * 5: # Grobe Schaetzung: im Schnitt 5 Updates pro Zeile - self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. - # Wenn es fehlschlaegt, wird es intern geloggt. - success = self.sheet_handler.batch_update_cells(all_sheet_updates) - if success: - self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") # <<< GEÄNDERT - # Der Fehlerfall wird von batch_update_cells geloggt + if len(all_sheet_updates) >= update_batch_row_limit * \ + 5: # Grobe Schaetzung: im Schnitt 5 Updates pro Zeile + self.logger.debug( + f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT + # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. + # Wenn es fehlschlaegt, wird es intern geloggt. + success = self.sheet_handler.batch_update_cells( + all_sheet_updates) + if success: + self.logger.info( + f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") # <<< GEÄNDERT + # Der Fehlerfall wird von batch_update_cells geloggt - # Leere die gesammelten Updates nach dem Senden. - all_sheet_updates = [] + # Leere die gesammelten Updates nach dem Senden. + all_sheet_updates = [] # Kleine Pause nach jeder SerpAPI-Suche (nutzt Config Block 1). # Der retry_on_failure Decorator (Block 2) kuemmert sich um Retries mit Backoff. - # Dies ist eine globale Rate-Limit-Vorsorge zwischen einzelnen Anfragen. + # Dies ist eine globale Rate-Limit-Vorsorge zwischen einzelnen + # Anfragen. serp_delay = getattr(Config, 'SERPAPI_DELAY', 1.5) - #self.logger.debug(f"Warte {serp_delay:.2f}s nach SerpAPI Suche...") # Zu viel Laerm im Debug + # self.logger.debug(f"Warte {serp_delay:.2f}s nach SerpAPI + # Suche...") # Zu viel Laerm im Debug time.sleep(serp_delay) - # --- Finale Sheet Updates senden --- - # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update. + # Sende alle verbleibenden gesammelten Updates in einem letzten + # Batch-Update. if all_sheet_updates: - self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. + self.logger.info( + f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT + # Nutzt die batch_update_cells Methode des Sheet Handlers (Block + # 14) mit Retry. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT + # <<< GEÄNDERT + self.logger.info(f"FINALES Sheet-Update erfolgreich.") # Der Fehlerfall wird von batch_update_cells geloggt - # Logge den Abschluss des Modus - self.logger.info(f"Modus 'find_wiki_serp' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {found_urls_count} URLs gefunden & eingetragen, {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT + self.logger.info( + f"Modus 'find_wiki_serp' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {found_urls_count} URLs gefunden & eingetragen, {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT - def process_contact_search(self, start_sheet_row=None, end_sheet_row=None, limit=None): + def process_contact_search( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None): """ Sucht LinkedIn Kontakte ueber SerpAPI. """ - self.logger.info(f"Starte Contact Research (Batch). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") + self.logger.info( + f"Starte Contact Research (Batch). Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") """ Sucht LinkedIn Kontakte ueber SerpAPI fuer Zeilen, bei denen der Contact Search Timestamp (AM) leer ist. Traegt Trefferzahlen in @@ -2559,399 +3634,542 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Batch-Laufs - self.logger.info(f"Starte Contact Research (Batch AM, AI-AL). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT - + self.logger.info( + f"Starte Contact Research (Batch AM, AI-AL). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden und Startzeile ermitteln --- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AM...") # <<< GEÄNDERT - # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AM (Block 1 Column Map). - # Standardmaessig ab Zeile 7 - start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Contact Search Timestamp", min_sheet_row=7) + self.logger.info( + "Automatische Ermittlung der Startzeile basierend auf leeren AM...") # <<< GEÄNDERT + # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AM (Block 1 Column Map). + # Standardmaessig ab Zeile 7 + start_data_index_no_header = self.sheet_handler.get_start_row_index( + check_column_key="Contact Search Timestamp", min_sheet_row=7) - # Wenn get_start_row_index -1 zurueckgibt (Fehler) - if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT - return # Beende die Methode + # Wenn get_start_row_index -1 zurueckgibt (Fehler) + if start_data_index_no_header == -1: + self.logger.error( + "FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT + return # Beende die Methode - # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index - start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AM Zelle): {start_sheet_row}") # <<< GEÄNDERT + # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten + # Daten-Index + start_sheet_row = start_data_index_no_header + \ + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut + self.logger.info( + f"Automatisch ermittelte Startzeile (erste leere AM Zelle): {start_sheet_row}") # <<< GEÄNDERT else: # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. - # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). + # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block + # 2). if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten fuer process_contact_search.") # <<< GEÄNDERT - return # Beende die Methode, wenn das Laden fehlschlaegt + self.logger.error( + "FEHLER beim Laden der Daten fuer process_contact_search.") # <<< GEÄNDERT + return # Beende die Methode, wenn das Laden fehlschlaegt - - # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. + # Holen Sie die gesamte Datenliste (inklusive Header) aus dem + # SheetHandler. all_data = self.sheet_handler.get_all_data_with_headers() - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). + # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar + # (Block 14). header_rows = self.sheet_handler._header_rows - total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet - + total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet # Berechne Endzeile, wenn nicht manuell gesetzt if end_sheet_row is None: - end_sheet_row = total_sheet_rows # Bis zur letzten Zeile + end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den verarbeitungsbereich - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT + self.logger.info( + f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT - # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) + # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht + # ueber Gesamtzeilen) if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT - return # Beende die Methode, wenn der Bereich leer ist - + self.logger.info( + "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT + return # Beende die Methode, wenn der Bereich leer ist # --- Indizes und Buchstaben --- - # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind + # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP + # (Block 1) vorhanden sind required_keys = [ - "Contact Search Timestamp", # AM - Pruefkriterium / Timestamp - "CRM Name", "CRM Kurzform", "CRM Website", # B, C, D (Daten fuer Suche) - "Linked Serviceleiter gefunden", "Linked It-Leiter gefunden", # AI, AJ (Zielspalten fuer Trefferzahlen) - "Linked Management gefunden", "Linked Disponent gefunden" # AK, AL (Zielspalten fuer Trefferzahlen) + "Contact Search Timestamp", # AM - Pruefkriterium / Timestamp + # B, C, D (Daten fuer Suche) + "CRM Name", "CRM Kurzform", "CRM Website", + # AI, AJ (Zielspalten fuer Trefferzahlen) + "Linked Serviceleiter gefunden", "Linked It-Leiter gefunden", + # AK, AL (Zielspalten fuer Trefferzahlen) + "Linked Management gefunden", "Linked Disponent gefunden" ] # Erstellen Sie ein Dictionary mit Schluesseln und Indizes col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} - # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden + # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden + # wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_contact_search: {missing}. Breche ab.") # <<< GEÄNDERT - return # Beende die Methode bei kritischem Fehler - - - # Ermitteln Sie die Spaltenbuchstaben fuer Updates (AI-AL, AM) (nutzt interne Helfer _get_col_letter Block 14) - ts_am_letter = self.sheet_handler._get_col_letter(col_indices["Contact Search Timestamp"] + 1) # AM - ai_letter = self.sheet_handler._get_col_letter(col_indices["Linked Serviceleiter gefunden"] + 1) # AI - aj_letter = self.sheet_handler._get_col_letter(col_indices["Linked It-Leiter gefunden"] + 1) # AJ - ak_letter = self.sheet_handler._get_col_letter(col_indices["Linked Management gefunden"] + 1) # AK - al_letter = self.sheet_handler._get_col_letter(col_indices["Linked Disponent gefunden"] + 1) # AL + self.logger.critical( + f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_contact_search: {missing}. Breche ab.") # <<< GEÄNDERT + return # Beende die Methode bei kritischem Fehler + # Ermitteln Sie die Spaltenbuchstaben fuer Updates (AI-AL, AM) (nutzt + # interne Helfer _get_col_letter Block 14) + ts_am_letter = self.sheet_handler._get_col_letter( + col_indices["Contact Search Timestamp"] + 1) # AM + ai_letter = self.sheet_handler._get_col_letter( + col_indices["Linked Serviceleiter gefunden"] + 1) # AI + aj_letter = self.sheet_handler._get_col_letter( + col_indices["Linked It-Leiter gefunden"] + 1) # AJ + ak_letter = self.sheet_handler._get_col_letter( + col_indices["Linked Management gefunden"] + 1) # AK + al_letter = self.sheet_handler._get_col_letter( + col_indices["Linked Disponent gefunden"] + 1) # AL # Positionen, nach denen gesucht wird (kann in Config verschoben werden Block 1) # Die Zuordnung zur Zaehlspalte (AI-AL) muss hier im Code erfolgen. positions_to_search = { "Serviceleiter": ["Serviceleiter", "Leiter Kundendienst", "Einsatzleiter"], "IT-Leiter": ["IT-Leiter", "Leiter IT"], - "Management": ["Geschäftsführer", "Vorstand", "Inhaber", "CEO", "CTO", "COO", "Kaufmännischer Leiter", "Technischer Leiter"], # Management erweitert - "Disponent": ["Disponent", "Einsatzplaner"] # Disponent erweitert + # Management erweitert + "Management": ["Geschäftsführer", "Vorstand", "Inhaber", "CEO", "CTO", "COO", "Kaufmännischer Leiter", "Technischer Leiter"], + "Disponent": ["Disponent", "Einsatzplaner"] # Disponent erweitert } # Stellen Sie sicher, dass die Schluessel im Dict den COLUMN_MAP Keys (AI-AL) entsprechen, # damit die Zaehlung korrekt zugeordnet werden kann. - # --- Kontakte-Blatt oeffnen oder erstellen --- - contacts_sheet = None # Initialisiere mit None - # Der Zugriff auf das Spreadsheet-Objekt erfolgt ueber den SheetHandler (Block 14). + contacts_sheet = None # Initialisiere mit None + # Der Zugriff auf das Spreadsheet-Objekt erfolgt ueber den SheetHandler + # (Block 14). if self.sheet_handler and self.sheet_handler.sheet and self.sheet_handler.sheet.spreadsheet: - try: - # Versuche, das Sheet "Contacts" zu oeffnen - contacts_sheet = self.sheet_handler.sheet.spreadsheet.worksheet("Contacts") - self.logger.info("Blatt 'Contacts' gefunden.") # <<< GEÄNDERT - except gspread.exceptions.WorksheetNotFound: - # Wenn nicht gefunden, erstelle es. - self.logger.info("Blatt 'Contacts' nicht gefunden, erstelle neu...") # <<< GEÄNDERT - try: - # Definieren Sie den Header fuer das neue Blatt - contacts_header = ["Firmenname", "CRM Kurzform", "Website", "Geschlecht", "Vorname", "Nachname", "Position", "Suchbegriffskategorie", "E-Mail-Adresse", "LinkedIn-Link", "Timestamp"] - # Schaetzen Sie die Anzahl der Zeilen und Spalten fuer das neue Blatt (kann angepasst werden) - num_cols_contacts_sheet = len(contacts_header) - # Erstellen Sie das neue Blatt - contacts_sheet = self.sheet_handler.sheet.spreadsheet.add_worksheet(title="Contacts", rows="5000", cols=num_cols_contacts_sheet) - # Schreiben Sie den Header in die erste Zeile des neuen Blattes - # Nutzt _get_col_letter interne Methode des SheetHandlers (Block 14) - contacts_sheet.update(values=[contacts_header], range_name=f"A1:{self.sheet_handler._get_col_letter(num_cols_contacts_sheet)}1") - self.logger.info("Neues Blatt 'Contacts' erstellt und Header eingetragen.") # <<< GEÄNDERT + try: + # Versuche, das Sheet "Contacts" zu oeffnen + contacts_sheet = self.sheet_handler.sheet.spreadsheet.worksheet( + "Contacts") + self.logger.info("Blatt 'Contacts' gefunden.") # <<< GEÄNDERT + except gspread.exceptions.WorksheetNotFound: + # Wenn nicht gefunden, erstelle es. + # <<< GEÄNDERT + self.logger.info( + "Blatt 'Contacts' nicht gefunden, erstelle neu...") + try: + # Definieren Sie den Header fuer das neue Blatt + contacts_header = [ + "Firmenname", + "CRM Kurzform", + "Website", + "Geschlecht", + "Vorname", + "Nachname", + "Position", + "Suchbegriffskategorie", + "E-Mail-Adresse", + "LinkedIn-Link", + "Timestamp"] + # Schaetzen Sie die Anzahl der Zeilen und Spalten fuer das + # neue Blatt (kann angepasst werden) + num_cols_contacts_sheet = len(contacts_header) + # Erstellen Sie das neue Blatt + contacts_sheet = self.sheet_handler.sheet.spreadsheet.add_worksheet( + title="Contacts", rows="5000", cols=num_cols_contacts_sheet) + # Schreiben Sie den Header in die erste Zeile des neuen Blattes + # Nutzt _get_col_letter interne Methode des SheetHandlers + # (Block 14) + contacts_sheet.update( + values=[contacts_header], + range_name=f"A1:{self.sheet_handler._get_col_letter(num_cols_contacts_sheet)}1") + # <<< GEÄNDERT + self.logger.info( + "Neues Blatt 'Contacts' erstellt und Header eingetragen.") - except Exception as e_create_sheet: - # Fange Fehler bei der Erstellung des Blattes ab und logge sie. - self.logger.critical(f"FEHLER: Konnte Blatt 'Contacts' nicht erstellen: {e_create_sheet}. Kontakt-Details koennen NICHT gespeichert werden.") # <<< GEÄNDERT - # Logge den Traceback. - self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT - contacts_sheet = None # Setze contacts_sheet auf None, um spaetere Schreibversuche zu verhindern + except Exception as e_create_sheet: + # Fange Fehler bei der Erstellung des Blattes ab und logge + # sie. + self.logger.critical( + f"FEHLER: Konnte Blatt 'Contacts' nicht erstellen: {e_create_sheet}. Kontakt-Details koennen NICHT gespeichert werden.") # <<< GEÄNDERT + # Logge den Traceback. + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT + # Setze contacts_sheet auf None, um spaetere + # Schreibversuche zu verhindern + contacts_sheet = None else: - # Wenn SheetHandler oder Sheet-Objekt nicht verfuegbar war. - self.logger.warning("SheetHandler oder Sheet-Objekt nicht verfuegbar. Kann Blatt 'Contacts' nicht oeffnen/erstellen. Kontakt-Details werden NICHT gespeichert.") # <<< GEÄNDERT - contacts_sheet = None # Sicherstellen, dass contacts_sheet None ist - + # Wenn SheetHandler oder Sheet-Objekt nicht verfuegbar war. + self.logger.warning( + "SheetHandler oder Sheet-Objekt nicht verfuegbar. Kann Blatt 'Contacts' nicht oeffnen/erstellen. Kontakt-Details werden NICHT gespeichert.") # <<< GEÄNDERT + contacts_sheet = None # Sicherstellen, dass contacts_sheet None ist # --- Verarbeitung --- - all_sheet_updates = [] # Gesammelte Updates fuer Batch-Schreiben ins Hauptblatt (Liste von Dicts) - all_contact_rows_to_append = [] # Gesammelte Zeilen fuer append_rows ins Contacts-Blatt (Liste von Listen) + # Gesammelte Updates fuer Batch-Schreiben ins Hauptblatt (Liste von + # Dicts) + all_sheet_updates = [] + # Gesammelte Zeilen fuer append_rows ins Contacts-Blatt (Liste von + # Listen) + all_contact_rows_to_append = [] # append_rows kann grosse Batches handhaben, wir koennen hier mehr sammeln als beim Batch-Update. # Oder wir schreiben pro Firma in das Contacts-Blatt (weniger sammelbar). - # Fuer diesen Modus sammeln wir alle Kontaktzeilen und schreiben am Ende gesammelt mit append_rows. - - - processed_count = 0 # Zaehlt Zeilen im Hauptblatt, die fuer die Verarbeitung in Frage kommen und in den Batch aufgenommen werden (im Rahmen des Limits). - skipped_count = 0 # Zaehlt Zeilen im Hauptblatt, die uebersprungen wurden (wegen AM oder fehlender Daten). + # Fuer diesen Modus sammeln wir alle Kontaktzeilen und schreiben am + # Ende gesammelt mit append_rows. + # Zaehlt Zeilen im Hauptblatt, die fuer die Verarbeitung in Frage + # kommen und in den Batch aufgenommen werden (im Rahmen des Limits). + processed_count = 0 + # Zaehlt Zeilen im Hauptblatt, die uebersprungen wurden (wegen AM oder + # fehlender Daten). + skipped_count = 0 # Aktueller Zeitstempel fuer die AM Timestamp und Kontaktzeilen now_timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte Sheet-Zeilennummer) + # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte + # Sheet-Zeilennummer) for i in range(start_sheet_row, end_sheet_row + 1): - row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste + row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste # Pruefen Sie, ob das Ende des Sheets erreicht wurde - if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht - - - row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile + if row_index_in_list >= total_sheet_rows: + break # Ende des Sheets erreicht + row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile # Stellen Sie sicher, dass die Zeile nicht leer ist - if not any(cell and isinstance(cell, str) and cell.strip() for cell in row): - #self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") # Zu viel Laerm im Debug - skipped_count += 1 # Zaehlen als uebersprungen - continue # Springe zur naechsten Zeile - + if not any(cell and isinstance(cell, str) and cell.strip() + for cell in row): + # self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") + # # Zu viel Laerm im Debug + skipped_count += 1 # Zaehlen als uebersprungen + continue # Springe zur naechsten Zeile # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist --- # Kriterium: Contact Search Timestamp (AM) ist leer. - # ZUSAETZLICH: Pruefen, ob CRM Name, Kurzform und Website vorhanden und gueltig sind. + # ZUSAETZLICH: Pruefen, ob CRM Name, Kurzform und Website vorhanden + # und gueltig sind. - # Holen Sie den Wert aus Spalte AM (Contact Search Timestamp) (nutzt interne Helfer _get_cell_value_safe) - am_value = self._get_cell_value_safe(row, "Contact Search Timestamp").strip() # Block 1 Column Map + # Holen Sie den Wert aus Spalte AM (Contact Search Timestamp) + # (nutzt interne Helfer _get_cell_value_safe) + am_value = self._get_cell_value_safe( + row, "Contact Search Timestamp").strip() # Block 1 Column Map # Pruefung basiert darauf, ob AM leer ist. processing_needed_based_on_status = not am_value - - # Holen Sie die benoetigten Daten fuer die Suche (nutzt interne Helfer _get_cell_value_safe) - company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - crm_kurzform = self._get_cell_value_safe(row, "CRM Kurzform").strip() # Block 1 Column Map - website = self._get_cell_value_safe(row, "CRM Website").strip() # Block 1 Column Map + # Holen Sie die benoetigten Daten fuer die Suche (nutzt interne + # Helfer _get_cell_value_safe) + company_name = self._get_cell_value_safe( + row, "CRM Name").strip() # Block 1 Column Map + crm_kurzform = self._get_cell_value_safe( + row, "CRM Kurzform").strip() # Block 1 Column Map + website = self._get_cell_value_safe( + row, "CRM Website").strip() # Block 1 Column Map # Pruefen Sie, ob die Mindestdaten fuer die Suche vorhanden und gueltig sind. - # Name und Kurzform duerfen nicht leer sein. Website muss vorhanden und gueltig aussehen. - has_min_data_for_search = company_name and crm_kurzform and website and isinstance(website, str) and website.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu + # Name und Kurzform duerfen nicht leer sein. Website muss vorhanden + # und gueltig aussehen. + has_min_data_for_search = company_name and crm_kurzform and website and isinstance( + website, str) and website.lower() not in [ + "k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu - - # Verarbeitung ist noetig, wenn AM leer ist UND die Mindestdaten fuer die Suche vorhanden sind. + # Verarbeitung ist noetig, wenn AM leer ist UND die Mindestdaten + # fuer die Suche vorhanden sind. processing_needed_for_row = processing_needed_based_on_status and has_min_data_for_search - # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level - log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) + log_check = ( + i < start_sheet_row + + 5) or ( + i % + 100 == 0) or (processing_needed_for_row) if log_check: - company_name_log = company_name[:50] + '...' if len(company_name) > 50 else company_name # Gekuerzt loggen - self.logger.debug(f"Zeile {i} ({company_name_log} Contact Check): AM leer? {processing_needed_based_on_status}, Mindestdaten gueltig? {has_min_data_for_search}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT - + # Gekuerzt loggen + company_name_log = company_name[:50] + \ + '...' if len(company_name) > 50 else company_name + self.logger.debug( + f"Zeile {i} ({company_name_log} Contact Check): AM leer? {processing_needed_based_on_status}, Mindestdaten gueltig? {has_min_data_for_search}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist if not processing_needed_for_row: - skipped_count += 1 # Zaehlen als uebersprungene Zeile - continue # Springe zur naechsten Zeile - + skipped_count += 1 # Zaehlen als uebersprungene Zeile + continue # Springe zur naechsten Zeile # --- Wenn Verarbeitung noetig: Fuehre LinkedIn Suche(n) aus --- - processed_count += 1 # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im Rahmen des Limits zaehlen) + # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im + # Rahmen des Limits zaehlen) + processed_count += 1 # Pruefe das Limit fuer verarbeitete Zeilen - if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: - # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_contact_search erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT - break # Brich die Schleife ab + if limit is not None and isinstance( + limit, int) and limit > 0 and processed_count > limit: + # Wenn das Limit erreicht ist und es ein positives Limit gibt + self.logger.info( + f"Verarbeitungslimit ({limit}) fuer process_contact_search erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT + break # Brich die Schleife ab + self.logger.info( + f"Zeile {i}: Suche LinkedIn Kontakte fuer '{crm_kurzform[:50]}...' ({website[:50]}...)...") # <<< GEÄNDERT - self.logger.info(f"Zeile {i}: Suche LinkedIn Kontakte fuer '{crm_kurzform[:50]}...' ({website[:50]}...)...") # <<< GEÄNDERT - - - all_found_contacts_for_row = [] # Liste zum Sammeln aller gefundenen Kontakte fuer DIESE Zeile (Liste von Dicts) - contact_counts_for_row = {key: 0 for key in positions_to_search.keys()} # Dictionary zum Zaehlen der Treffer pro Kategorie fuer diese Zeile (AI-AL) - + # Liste zum Sammeln aller gefundenen Kontakte fuer DIESE Zeile + # (Liste von Dicts) + all_found_contacts_for_row = [] + # Dictionary zum Zaehlen der Treffer pro Kategorie fuer diese Zeile + # (AI-AL) + contact_counts_for_row = { + key: 0 for key in positions_to_search.keys()} # Führe die Suche fuer jede Positionskategorie durch. # positions_to_search Dictionary ist oben definiert. for category, queries in positions_to_search.items(): # Führe die Suche fuer jede spezifische Abfrage innerhalb der Kategorie durch. # search_linkedin_contacts (Block 10) nutzt den retry_on_failure Decorator (Block 2). - # Wenn search_linkedin_contacts fehlschlaegt, wirft es eine Exception oder gibt eine leere Liste zurueck. - found_contacts_in_category = {} # Dictionary zum Sammeln eindeutiger Kontakte {linkedin_url: contact_data} fuer diese Kategorie + # Wenn search_linkedin_contacts fehlschlaegt, wirft es eine + # Exception oder gibt eine leere Liste zurueck. + # Dictionary zum Sammeln eindeutiger Kontakte {linkedin_url: + # contact_data} fuer diese Kategorie + found_contacts_in_category = {} for position_query in queries: - self.logger.debug(f" -> Suche nach Position: '{position_query}' bei '{crm_kurzform[:50]}'...") # <<< GEÄNDERT + self.logger.debug( + f" -> Suche nach Position: '{position_query}' bei '{crm_kurzform[:50]}'...") # <<< GEÄNDERT try: # Rufe die globale Funktion search_linkedin_contacts auf (Block 10). - # Limitieren Sie die Anzahl der SerpAPI Ergebnisse pro Query, um Kosten zu managen. + # Limitieren Sie die Anzahl der SerpAPI Ergebnisse pro + # Query, um Kosten zu managen. contacts_from_query = search_linkedin_contacts( - company_name=company_name, # Voller Name fuer Kontext (optional genutzt) - website=website, # Website fuer Email-Generierung spaeter - position_query=position_query, # Die spezifische Position - crm_kurzform=crm_kurzform, # Die Kurzform der Firma - num_results=getattr(Config, 'SERPAPI_LINKEDIN_RESULTS_PER_QUERY', 5) # Konfigurierbar in Config (Block 1) + company_name=company_name, + # Voller Name fuer Kontext (optional genutzt) + website=website, # Website fuer Email-Generierung spaeter + position_query=position_query, # Die spezifische Position + crm_kurzform=crm_kurzform, # Die Kurzform der Firma + num_results=getattr( + Config, + 'SERPAPI_LINKEDIN_RESULTS_PER_QUERY', + 5) # Konfigurierbar in Config (Block 1) ) - # Fuege die gefundenen Kontakte (mit Suchkategorie) zur Liste fuer diese Kategorie hinzu, dedupliziert ueber URL. + # Fuege die gefundenen Kontakte (mit Suchkategorie) zur + # Liste fuer diese Kategorie hinzu, dedupliziert ueber + # URL. for contact in contacts_from_query: linkedin_url = contact.get("LinkedInURL") - if linkedin_url and isinstance(linkedin_url, str) and linkedin_url.strip(): # Stelle sicher, dass URL gueltig ist + if linkedin_url and isinstance( + linkedin_url, str) and linkedin_url.strip(): # Stelle sicher, dass URL gueltig ist if linkedin_url not in found_contacts_in_category: - # Wenn die URL noch nicht in dieser Kategorie gefunden wurde, fuege den Kontakt hinzu. - contact["Suchbegriffskategorie"] = category # Speichere die Kategorie, die den Treffer brachte + # Wenn die URL noch nicht in dieser + # Kategorie gefunden wurde, fuege den + # Kontakt hinzu. + # Speichere die Kategorie, die den Treffer + # brachte + contact["Suchbegriffskategorie"] = category found_contacts_in_category[linkedin_url] = contact # else: Wenn die URL bereits gefunden wurde, mache nichts (erste Kategorie wird beibehalten). - # self.logger.debug(f" -> Gefunden: {contact.get('Vorname')} {contact.get('Nachname')} ({contact.get('Position')})") # Zu viel Laerm im Debug - + # self.logger.debug(f" -> Gefunden: + # {contact.get('Vorname')} + # {contact.get('Nachname')} + # ({contact.get('Position')})") # Zu viel Laerm + # im Debug except Exception as e_linkedin_search: - # Wenn search_linkedin_contacts eine Exception wirft (nach Retries) - # Der Fehler wird bereits vom retry_on_failure Decorator oder search_linkedin_contacts geloggt. - self.logger.error(f"FEHLER bei search_linkedin_contacts fuer Zeile {i} (Query: '{position_query}', Firma: '{crm_kurzform[:50]}...'): {e_linkedin_search}") # <<< GEÄNDERT - pass # Faert fort mit der naechsten Query oder Kategorie + # Wenn search_linkedin_contacts eine Exception wirft (nach Retries) + # Der Fehler wird bereits vom retry_on_failure + # Decorator oder search_linkedin_contacts geloggt. + self.logger.error( + f"FEHLER bei search_linkedin_contacts fuer Zeile {i} (Query: '{position_query}', Firma: '{crm_kurzform[:50]}...'): {e_linkedin_search}") # <<< GEÄNDERT + pass # Faert fort mit der naechsten Query oder Kategorie # Pause nach jeder SerpAPI Suche (pro position_query) # Nutzt Config.SERPAPI_DELAY (Block 1). serp_delay = getattr(Config, 'SERPAPI_DELAY', 1.5) - #self.logger.debug(f"Warte {serp_delay:.2f}s nach LinkedIn Suche fuer '{position_query}'...") # Zu viel Laerm im Debug + # self.logger.debug(f"Warte {serp_delay:.2f}s nach LinkedIn + # Suche fuer '{position_query}'...") # Zu viel Laerm im + # Debug time.sleep(serp_delay) - # Zaehle die eindeutigen Treffer in dieser Kategorie nach allen Queries innerhalb der Kategorie. - contact_counts_for_row[category] = len(found_contacts_in_category) - # Fuege die eindeutigen Kontakte DIESER Kategorie zur Gesamtliste fuer DIESE Zeile hinzu. - all_found_contacts_for_row.extend(found_contacts_in_category.values()) - + # Zaehle die eindeutigen Treffer in dieser Kategorie nach allen + # Queries innerhalb der Kategorie. + contact_counts_for_row[category] = len( + found_contacts_in_category) + # Fuege die eindeutigen Kontakte DIESER Kategorie zur + # Gesamtliste fuer DIESE Zeile hinzu. + all_found_contacts_for_row.extend( + found_contacts_in_category.values()) # --- Verarbeite gefundene Kontakte und bereite Updates vor --- - rows_to_append_to_contacts_sheet = [] # Liste von Listen fuer append_rows ins 'Contacts' Blatt - main_sheet_updates_for_row = [] # Updates fuer das Hauptblatt (AI-AL, AM) fuer DIESE Zeile - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Timestamp fuer DIESE Zeile/Kontakte - + # Liste von Listen fuer append_rows ins 'Contacts' Blatt + rows_to_append_to_contacts_sheet = [] + # Updates fuer das Hauptblatt (AI-AL, AM) fuer DIESE Zeile + main_sheet_updates_for_row = [] + # Timestamp fuer DIESE Zeile/Kontakte + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Fuegen Sie die Updates fuer die Trefferzahlen im Hauptblatt hinzu (nutzt interne Helfer _get_col_letter Block 14) - # Stellen Sie sicher, dass die Spaltenbuchstaben korrekt sind (AI, AJ, AK, AL) (aus oben ermittelt) - main_sheet_updates_for_row.append({'range': f'{ai_letter}{i}', 'values': [[str(contact_counts_for_row.get("Serviceleiter", 0))]]}) - main_sheet_updates_for_row.append({'range': f'{aj_letter}{i}', 'values': [[str(contact_counts_for_row.get("IT-Leiter", 0))]]}) - main_sheet_updates_for_row.append({'range': f'{ak_letter}{i}', 'values': [[str(contact_counts_for_row.get("Management", 0))]]}) - main_sheet_updates_for_row.append({'range': f'{al_letter}{i}', 'values': [[str(contact_counts_for_row.get("Disponent", 0))]]}) + # Stellen Sie sicher, dass die Spaltenbuchstaben korrekt sind (AI, + # AJ, AK, AL) (aus oben ermittelt) + main_sheet_updates_for_row.append({'range': f'{ai_letter}{i}', 'values': [ + [str(contact_counts_for_row.get("Serviceleiter", 0))]]}) + main_sheet_updates_for_row.append({'range': f'{aj_letter}{i}', 'values': [ + [str(contact_counts_for_row.get("IT-Leiter", 0))]]}) + main_sheet_updates_for_row.append({'range': f'{ak_letter}{i}', 'values': [ + [str(contact_counts_for_row.get("Management", 0))]]}) + main_sheet_updates_for_row.append({'range': f'{al_letter}{i}', 'values': [ + [str(contact_counts_for_row.get("Disponent", 0))]]}) # Setze den Contact Search Timestamp (AM) fuer DIESE Zeile - main_sheet_updates_for_row.append({'range': f'{ts_am_letter}{i}', 'values': [[timestamp]]}) + main_sheet_updates_for_row.append( + {'range': f'{ts_am_letter}{i}', 'values': [[timestamp]]}) - - # Sammeln Sie diese Updates fuer das Hauptblatt in der globalen Liste all_sheet_updates. + # Sammeln Sie diese Updates fuer das Hauptblatt in der globalen + # Liste all_sheet_updates. all_sheet_updates.extend(main_sheet_updates_for_row) - self.logger.info(f"Zeile {i}: Kontaktzahlen gesammelt: {contact_counts_for_row} – Timestamp AM vorgemerkt fuer Update.") # <<< GEÄNDERT - + self.logger.info( + f"Zeile {i}: Kontaktzahlen gesammelt: {contact_counts_for_row} – Timestamp AM vorgemerkt fuer Update.") # <<< GEÄNDERT # Bereiten Sie die Zeilen fuer das 'Contacts' Blatt vor (falls es existiert). - # all_found_contacts_for_row enthaelt alle gefundenen Kontakte fuer DIESE Zeile (mit evtl. Duplikaten bei URL). - if contacts_sheet: # Pruefen Sie, ob das Contacts-Sheet geoeffnet/erstellt werden konnte (siehe Initialisierung oben) - # Führen Sie eine finale Deduplizierung ueber die LinkedIn-URL durch. - # Dictionary-Comprehension behält nur das letzte Vorkommen bei Duplikaten. - unique_contacts_for_row_dict = {c['LinkedInURL']: c for c in all_found_contacts_for_row if c.get('LinkedInURL')} # Filtere Kontakte ohne URL - unique_contacts_for_row = list(unique_contacts_for_row_dict.values()) # Liste der eindeutigen Kontakte + # all_found_contacts_for_row enthaelt alle gefundenen Kontakte fuer + # DIESE Zeile (mit evtl. Duplikaten bei URL). + # Pruefen Sie, ob das Contacts-Sheet geoeffnet/erstellt werden + # konnte (siehe Initialisierung oben) + if contacts_sheet: + # Führen Sie eine finale Deduplizierung ueber die LinkedIn-URL durch. + # Dictionary-Comprehension behält nur das letzte Vorkommen bei + # Duplikaten. + unique_contacts_for_row_dict = {c['LinkedInURL']: c for c in all_found_contacts_for_row if c.get( + 'LinkedInURL')} # Filtere Kontakte ohne URL + unique_contacts_for_row = list( + unique_contacts_for_row_dict.values()) # Liste der eindeutigen Kontakte - # Iteriere ueber die eindeutigen Kontakte fuer diese Zeile - for contact in unique_contacts_for_row: - firstname = contact.get("Vorname", "") # Nutzt den extrahierten Vornamen - lastname = contact.get("Nachname", "") # Nutzt den extrahierten Nachnamen + # Iteriere ueber die eindeutigen Kontakte fuer diese Zeile + for contact in unique_contacts_for_row: + # Nutzt den extrahierten Vornamen + firstname = contact.get("Vorname", "") + # Nutzt den extrahierten Nachnamen + lastname = contact.get("Nachname", "") - # Generiere Geschlecht und E-Mail-Adresse (nutzt globale Funktionen Block 5). - # get_gender und get_email_address behandeln leere/ungueltige Eingaben. - gender_value = get_gender(firstname) - email = get_email_address(firstname, lastname, website) # Nutzt die Website der Firma (initial geladen) + # Generiere Geschlecht und E-Mail-Adresse (nutzt globale Funktionen Block 5). + # get_gender und get_email_address behandeln + # leere/ungueltige Eingaben. + gender_value = get_gender(firstname) + # Nutzt die Website der Firma (initial geladen) + email = get_email_address(firstname, lastname, website) - # Erstellen Sie die Liste der Werte fuer eine Zeile im 'Contacts' Blatt. - contact_row = [ - contact.get("Firmenname", ""), # Voller Firmenname - contact.get("CRM Kurzform", ""), # Firmenkurzform - contact.get("Website", ""), # Website der Firma - gender_value, # Generiertes Geschlecht - firstname, # Extrahierter Vorname - lastname, # Extrahierter Nachname - contact.get("Position", ""), # Extrahierte oder Fallback Position - contact.get("Suchbegriffskategorie", ""), # Kategorie, die den Treffer brachte - email, # Generierte E-Mail-Adresse - contact.get("LinkedInURL", ""), # URL des LinkedIn Profils - timestamp # Zeitstempel des Suchlaufs - ] - # Fuegen Sie diese Zeile zur Liste der Zeilen hinzu, die spaeter ins Contacts-Sheet geschrieben werden. - rows_to_append_to_contacts_sheet.append(contact_row) - - # Wenn Zeilen zum Anfuegen gefunden wurden - if rows_to_append_to_contacts_sheet: - # Fuegen Sie diese Zeilen zur globalen Liste aller Kontakte hinzu, die spaeter angefuegt werden. - all_contact_rows_to_append.extend(rows_to_append_to_contacts_sheet) - self.logger.debug(f" -> {len(rows_to_append_to_contacts_sheet)} eindeutige Kontakte fuer Zeile {i} zum Anfuegen an 'Contacts' vorgemerkt.") # <<< GEÄNDERT - else: - self.logger.debug(f" -> Keine neuen Kontakte fuer Zeile {i} gefunden.") # <<< GEÄNDERT + # Erstellen Sie die Liste der Werte fuer eine Zeile im + # 'Contacts' Blatt. + contact_row = [ + contact.get("Firmenname", ""), # Voller Firmenname + contact.get("CRM Kurzform", ""), # Firmenkurzform + contact.get("Website", ""), # Website der Firma + gender_value, # Generiertes Geschlecht + firstname, # Extrahierter Vorname + lastname, # Extrahierter Nachname + # Extrahierte oder Fallback Position + contact.get("Position", ""), + # Kategorie, die den Treffer brachte + contact.get("Suchbegriffskategorie", ""), + email, # Generierte E-Mail-Adresse + # URL des LinkedIn Profils + contact.get("LinkedInURL", ""), + timestamp # Zeitstempel des Suchlaufs + ] + # Fuegen Sie diese Zeile zur Liste der Zeilen hinzu, die + # spaeter ins Contacts-Sheet geschrieben werden. + rows_to_append_to_contacts_sheet.append(contact_row) + # Wenn Zeilen zum Anfuegen gefunden wurden + if rows_to_append_to_contacts_sheet: + # Fuegen Sie diese Zeilen zur globalen Liste aller Kontakte + # hinzu, die spaeter angefuegt werden. + all_contact_rows_to_append.extend( + rows_to_append_to_contacts_sheet) + self.logger.debug( + f" -> {len(rows_to_append_to_contacts_sheet)} eindeutige Kontakte fuer Zeile {i} zum Anfuegen an 'Contacts' vorgemerkt.") # <<< GEÄNDERT + else: + self.logger.debug( + f" -> Keine neuen Kontakte fuer Zeile {i} gefunden.") # <<< GEÄNDERT # Sende gesammelte Sheet Updates (Hauptblatt) wenn das Update-Batch-Limit erreicht ist. # update_batch_row_limit wird aus Config geholt (Block 1). - # Updates pro Zeile im Hauptblatt sind 5 (AI-AL + AM). Anzahl der Zeilen = len(all_sheet_updates) / 5. + # Updates pro Zeile im Hauptblatt sind 5 (AI-AL + AM). Anzahl der + # Zeilen = len(all_sheet_updates) / 5. rows_in_main_sheet_update_batch = len(all_sheet_updates) // 5 if rows_in_main_sheet_update_batch >= update_batch_row_limit: - self.logger.debug(f" Sende gesammelte Hauptblatt-Updates ({rows_in_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. - # Wenn es fehlschlaegt, wird es intern geloggt. - success = self.sheet_handler.batch_update_cells(all_sheet_updates) - if success: - self.logger.info(f" Hauptblatt-Update fuer {rows_in_main_sheet_update_batch} Zeilen erfolgreich.") # <<< GEÄNDERT - # Der Fehlerfall wird von batch_update_cells geloggt - - # Leere die gesammelten Updates nach dem Senden. - all_sheet_updates = [] + self.logger.debug( + f" Sende gesammelte Hauptblatt-Updates ({rows_in_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT + # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. + # Wenn es fehlschlaegt, wird es intern geloggt. + success = self.sheet_handler.batch_update_cells( + all_sheet_updates) + if success: + self.logger.info( + f" Hauptblatt-Update fuer {rows_in_main_sheet_update_batch} Zeilen erfolgreich.") # <<< GEÄNDERT + # Der Fehlerfall wird von batch_update_cells geloggt + # Leere die gesammelten Updates nach dem Senden. + all_sheet_updates = [] # Eine laengere Pause nach der Verarbeitung jeder Firma im Contact Search Modus. # Dieser Modus ist API-intensiv und sollte langsamer laufen. # Nutzt Config.RETRY_DELAY (Block 1). - pause_duration = getattr(Config, 'RETRY_DELAY', 10) * 0.8 # Laengere Pause, z.B. 80% der Retry-Wartezeit - self.logger.debug(f"Warte {pause_duration:.2f}s nach Verarbeitung von Zeile {i}...") # <<< GEÄNDERT + # Laengere Pause, z.B. 80% der Retry-Wartezeit + pause_duration = getattr(Config, 'RETRY_DELAY', 10) * 0.8 + self.logger.debug( + f"Warte {pause_duration:.2f}s nach Verarbeitung von Zeile {i}...") # <<< GEÄNDERT time.sleep(pause_duration) - # --- Finale Sheet Updates (Hauptblatt) senden --- - # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update. + # Sende alle verbleibenden gesammelten Updates in einem letzten + # Batch-Update. if all_sheet_updates: rows_in_final_main_sheet_update_batch = len(all_sheet_updates) // 5 - self.logger.info(f"Sende FINALE gesammelte Hauptblatt-Updates ({rows_in_final_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. + self.logger.info( + f"Sende FINALE gesammelte Hauptblatt-Updates ({rows_in_final_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT + # Nutzt die batch_update_cells Methode des Sheet Handlers (Block + # 14) mit Retry. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f"FINALES Hauptblatt-Update erfolgreich.") # <<< GEÄNDERT + # <<< GEÄNDERT + self.logger.info(f"FINALES Hauptblatt-Update erfolgreich.") # Der Fehlerfall wird von batch_update_cells geloggt - # --- Finale Kontakte-Zeilen (Contacts Sheet) anfuegen --- - # Fuege alle gesammelten Kontaktzeilen auf einmal ans Ende des 'Contacts' Blattes an. + # Fuege alle gesammelten Kontaktzeilen auf einmal ans Ende des + # 'Contacts' Blattes an. if contacts_sheet and all_contact_rows_to_append: - self.logger.info(f"Fuege {len(all_contact_rows_to_append)} gesammelte Kontaktzeilen an Blatt 'Contacts' an...") # <<< GEÄNDERT + self.logger.info( + f"Fuege {len(all_contact_rows_to_append)} gesammelte Kontaktzeilen an Blatt 'Contacts' an...") # <<< GEÄNDERT try: - # append_rows ist effizienter als batch_update fuer viele neue Zeilen am Ende. - # Die gspread.Worksheet.append_rows Methode kann Exceptions werfen (z.B. APIError), - # die hier gefangen werden koennen, wenn gewuenscht. - # Wenn sie eine Exception wirft, wird diese nicht von retry_on_failure auf - # process_contact_search behandelt, da append_rows nicht mit @retry_on_failure - # dekoriert ist. Sie muessten append_rows selbst in einen try/except Block packen oder - # es mit @retry_on_failure dekorieren (falls gspread es unterstuetzt). - # Fuer jetzt, fangen wir die Exception hier. - contacts_sheet.append_rows(all_contact_rows_to_append, value_input_option='USER_ENTERED') # Standard Option - self.logger.info(f"Anfuegen von {len(all_contact_rows_to_append)} Kontaktzeilen erfolgreich.") # <<< GEÄNDERT + # append_rows ist effizienter als batch_update fuer viele neue Zeilen am Ende. + # Die gspread.Worksheet.append_rows Methode kann Exceptions werfen (z.B. APIError), + # die hier gefangen werden koennen, wenn gewuenscht. + # Wenn sie eine Exception wirft, wird diese nicht von retry_on_failure auf + # process_contact_search behandelt, da append_rows nicht mit @retry_on_failure + # dekoriert ist. Sie muessten append_rows selbst in einen try/except Block packen oder + # es mit @retry_on_failure dekorieren (falls gspread es unterstuetzt). + # Fuer jetzt, fangen wir die Exception hier. + contacts_sheet.append_rows( + all_contact_rows_to_append, + value_input_option='USER_ENTERED') # Standard Option + self.logger.info( + f"Anfuegen von {len(all_contact_rows_to_append)} Kontaktzeilen erfolgreich.") # <<< GEÄNDERT except Exception as e_append: - # Fange Fehler beim Anfuegen der Zeilen ab und logge sie. - self.logger.error(f"FEHLER beim Anfuegen von Kontaktzeilen an Blatt 'Contacts': {type(e_append).__name__} - {e_append}") # <<< GEÄNDERT - # Logge den Traceback. - self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT - pass # Faert fort, der Rest des Skripts sollte nicht blockiert werden - + # Fange Fehler beim Anfuegen der Zeilen ab und logge sie. + self.logger.error( + f"FEHLER beim Anfuegen von Kontaktzeilen an Blatt 'Contacts': {type(e_append).__name__} - {e_append}") # <<< GEÄNDERT + # Logge den Traceback. + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT + pass # Faert fort, der Rest des Skripts sollte nicht blockiert werden # Logge den Abschluss des Modus - self.logger.info(f"Modus 'contact_search' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT - - def process_url_check(self, start_sheet_row=None, end_sheet_row=None, limit=None): + self.logger.info( + f"Modus 'contact_search' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT + + def process_url_check( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None): """ Sucht nach Zeilen mit URL_CHECK_MARKER und versucht, eine neue URL zu finden. """ - self.logger.info(f"Starte Modus 'check_urls'. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") + self.logger.info( + f"Starte Modus 'check_urls'. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") """ Sucht nach Zeilen, die in Spalte AR mit URL_CHECK_MARKER oder bekannten "k.A. (Fehler...)" Mustern markiert sind UND bei denen der AY-Timestamp (SerpAPI Wiki Search Timestamp) leer ist @@ -2966,55 +4184,86 @@ class DataProcessor: end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet). limit (int, optional): Maximale Anzahl ZU VERARBEITENDER Zeilen. Defaults to None (Unbegrenzt). """ - self.logger.info(f"Starte Modus 'check_urls'. Sucht nach '{URL_CHECK_MARKER}' oder 'k.A. (Fehler...)' in AR. Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") - + self.logger.info( + f"Starte Modus 'check_urls'. Sucht nach '{URL_CHECK_MARKER}' oder 'k.A. (Fehler...)' in AR. Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") + # --- Konfiguration holen --- - update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) # <<< NEUE ZEILE HINZUGEFÜGT - + update_batch_row_limit = getattr( + Config, + 'UPDATE_BATCH_ROW_LIMIT', + 50) # <<< NEUE ZEILE HINZUGEFÜGT + if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer URL Check.") - return + self.logger.error("Fehler beim Laden der Daten fuer URL Check.") + return all_data = self.sheet_handler.get_all_data_with_headers() header_rows = self.sheet_handler._header_rows total_sheet_rows = len(all_data) - if start_sheet_row is None: start_sheet_row = header_rows + 1 - if end_sheet_row is None: end_sheet_row = total_sheet_rows + if start_sheet_row is None: + start_sheet_row = header_rows + 1 + if end_sheet_row is None: + end_sheet_row = total_sheet_rows - self.logger.info(f"Suchbereich fuer URL Checks: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}.") + self.logger.info( + f"Suchbereich fuer URL Checks: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}.") if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") - return + self.logger.info( + "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") + return required_keys = [ - "Website Rohtext", "CRM Name", "CRM Website", "ReEval Flag", - "Website Scrape Timestamp", "Timestamp letzte Pruefung", - "Wikipedia Timestamp", "Wiki Verif. Timestamp", "SerpAPI Wiki Search Timestamp", - "Version" - ] + "Website Rohtext", + "CRM Name", + "CRM Website", + "ReEval Flag", + "Website Scrape Timestamp", + "Timestamp letzte Pruefung", + "Wikipedia Timestamp", + "Wiki Verif. Timestamp", + "SerpAPI Wiki Search Timestamp", + "Version"] col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_url_check: {missing}. Breche ab.") + self.logger.critical( + f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_url_check: {missing}. Breche ab.") return - ar_letter = self.sheet_handler._get_col_letter(col_indices["Website Rohtext"] + 1) - d_letter = self.sheet_handler._get_col_letter(col_indices["CRM Website"] + 1) - a_letter = self.sheet_handler._get_col_letter(col_indices["ReEval Flag"] + 1) - at_letter = self.sheet_handler._get_col_letter(col_indices["Website Scrape Timestamp"] + 1) - ao_letter = self.sheet_handler._get_col_letter(col_indices["Timestamp letzte Pruefung"] + 1) - an_letter = self.sheet_handler._get_col_letter(col_indices["Wikipedia Timestamp"] + 1) - ax_letter = self.sheet_handler._get_col_letter(col_indices["Wiki Verif. Timestamp"] + 1) - ay_letter = self.sheet_handler._get_col_letter(col_indices["SerpAPI Wiki Search Timestamp"] + 1) # Timestamp dieser Funktion - ap_letter = self.sheet_handler._get_col_letter(col_indices["Version"] + 1) + ar_letter = self.sheet_handler._get_col_letter( + col_indices["Website Rohtext"] + 1) + d_letter = self.sheet_handler._get_col_letter( + col_indices["CRM Website"] + 1) + a_letter = self.sheet_handler._get_col_letter( + col_indices["ReEval Flag"] + 1) + at_letter = self.sheet_handler._get_col_letter( + col_indices["Website Scrape Timestamp"] + 1) + ao_letter = self.sheet_handler._get_col_letter( + col_indices["Timestamp letzte Pruefung"] + 1) + an_letter = self.sheet_handler._get_col_letter( + col_indices["Wikipedia Timestamp"] + 1) + ax_letter = self.sheet_handler._get_col_letter( + col_indices["Wiki Verif. Timestamp"] + 1) + ay_letter = self.sheet_handler._get_col_letter( + col_indices["SerpAPI Wiki Search Timestamp"] + + 1) # Timestamp dieser Funktion + ap_letter = self.sheet_handler._get_col_letter( + col_indices["Version"] + 1) ka_error_patterns = [ - "k.A.", "k.A. (Extraktion leer)", "k.A. (Nur Cookie-Banner erkannt)", - "k.A. (Kein Body gefunden)", "k.A. (Fehler Parsing:", "k.A. (Unerwarteter Fehler Task)", - "k.A. (Fehler Scraping:", "k.A. (Timeout", "k.A. (SSL Fehler", - "k.A. (Connection Error", "k.A. (HTTP Error", URL_CHECK_MARKER - ] + "k.A.", + "k.A. (Extraktion leer)", + "k.A. (Nur Cookie-Banner erkannt)", + "k.A. (Kein Body gefunden)", + "k.A. (Fehler Parsing:", + "k.A. (Unerwarteter Fehler Task)", + "k.A. (Fehler Scraping:", + "k.A. (Timeout", + "k.A. (SSL Fehler", + "k.A. (Connection Error", + "k.A. (HTTP Error", + URL_CHECK_MARKER] all_sheet_updates = [] processed_count = 0 @@ -3024,123 +4273,170 @@ class DataProcessor: for i in range(start_sheet_row, end_sheet_row + 1): row_index_in_list = i - 1 - if row_index_in_list >= total_sheet_rows: break + if row_index_in_list >= total_sheet_rows: + break row = all_data[row_index_in_list] - if not any(cell and isinstance(cell, str) and cell.strip() for cell in row): - skipped_count += 1 - continue + if not any(cell and isinstance(cell, str) and cell.strip() + for cell in row): + skipped_count += 1 + continue - ar_value = self._get_cell_value_safe(row, "Website Rohtext").strip() - ay_timestamp_value = self._get_cell_value_safe(row, "SerpAPI Wiki Search Timestamp").strip() # Verwende den spezifischen Timestamp für diese Funktion + ar_value = self._get_cell_value_safe( + row, "Website Rohtext").strip() + # Verwende den spezifischen Timestamp für diese Funktion + ay_timestamp_value = self._get_cell_value_safe( + row, "SerpAPI Wiki Search Timestamp").strip() processing_needed_for_row = False is_marker_case = ar_value == URL_CHECK_MARKER - is_ka_error_case = any(pattern in ar_value for pattern in ka_error_patterns if pattern != URL_CHECK_MARKER) + is_ka_error_case = any( + pattern in ar_value for pattern in ka_error_patterns if pattern != URL_CHECK_MARKER) - if is_marker_case: # URL_CHECK_MARKER löst immer eine Suche aus + if is_marker_case: # URL_CHECK_MARKER löst immer eine Suche aus processing_needed_for_row = True - elif is_ka_error_case and not ay_timestamp_value: # Alte k.A.-Fehler nur, wenn AY-Timestamp noch nicht gesetzt wurde + # Alte k.A.-Fehler nur, wenn AY-Timestamp noch nicht gesetzt wurde + elif is_ka_error_case and not ay_timestamp_value: processing_needed_for_row = True if not processing_needed_for_row: - skipped_count += 1 - continue + skipped_count += 1 + continue processed_count += 1 - if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_url_check erreicht.") - break + if limit is not None and isinstance( + limit, int) and limit > 0 and processed_count > limit: + self.logger.info( + f"Verarbeitungslimit ({limit}) fuer process_url_check erreicht.") + break company_name = self._get_cell_value_safe(row, "CRM Name").strip() - old_crm_website_url = self._get_cell_value_safe(row, "CRM Website").strip() + old_crm_website_url = self._get_cell_value_safe( + row, "CRM Website").strip() normalized_old_crm_url = simple_normalize_url(old_crm_website_url) if not company_name: - self.logger.warning(f"Zeile {i}: Uebersprungen (kein Firmenname fuer Suche vorhanden).") + self.logger.warning( + f"Zeile {i}: Uebersprungen (kein Firmenname fuer Suche vorhanden).") skipped_count += 1 - updates_for_row_skip = [{'range': f'{ay_letter}{i}', 'values': [[now_timestamp_str]]}] # Timestamp trotzdem setzen + updates_for_row_skip = [{'range': f'{ay_letter}{i}', 'values': [ + [now_timestamp_str]]}] # Timestamp trotzdem setzen all_sheet_updates.extend(updates_for_row_skip) continue - self.logger.info(f"Zeile {i}: AR='{ar_value[:50]}...'. Suche neue URL für '{company_name[:50]}...' (Aktuell D: '{old_crm_website_url[:50]}...')...") + self.logger.info( + f"Zeile {i}: AR='{ar_value[:50]}...'. Suche neue URL für '{company_name[:50]}...' (Aktuell D: '{old_crm_website_url[:50]}...')...") updates_for_row = [] new_url_found_str = "k.A." try: - new_url_found_str = serp_website_lookup(company_name) - normalized_new_url = simple_normalize_url(new_url_found_str) + new_url_found_str = serp_website_lookup(company_name) + normalized_new_url = simple_normalize_url(new_url_found_str) - if new_url_found_str != "k.A." and normalized_new_url != "k.A.": - if normalized_new_url != normalized_old_crm_url: - self.logger.info(f" -> Neue, andere URL gefunden: {new_url_found_str}. Alte war: '{old_crm_website_url}'. Bereite Update vor.") - found_new_url_count += 1 - updates_for_row.append({'range': f'{d_letter}{i}', 'values': [[new_url_found_str]]}) - updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [['']]}) - updates_for_row.append({'range': f'{a_letter}{i}', 'values': [['x']]}) - updates_for_row.append({'range': f'{at_letter}{i}', 'values': [['']]}) - updates_for_row.append({'range': f'{ao_letter}{i}', 'values': [['']]}) - updates_for_row.append({'range': f'{an_letter}{i}', 'values': [['']]}) - updates_for_row.append({'range': f'{ax_letter}{i}', 'values': [['']]}) - updates_for_row.append({'range': f'{ay_letter}{i}', 'values': [['']]}) # Wird unten explizit neu gesetzt - updates_for_row.append({'range': f'{ap_letter}{i}', 'values': [['']]}) - else: - self.logger.info(f" -> SerpAPI fand URL '{new_url_found_str}', aber diese ist identisch mit der bereits vorhandenen URL in Spalte D. Keine Änderung in D.") - updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [["k.A. (URL via SerpAPI identisch mit alter URL)"]]}) - else: - self.logger.warning(f" -> Keine neue gueltige URL via SerpAPI für '{company_name[:50]}...' gefunden. Setze AR auf 'k.A. (Keine URL bei Neusuche)'.") - updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [["k.A. (Keine URL bei Neusuche)"]]}) + if new_url_found_str != "k.A." and normalized_new_url != "k.A.": + if normalized_new_url != normalized_old_crm_url: + self.logger.info( + f" -> Neue, andere URL gefunden: {new_url_found_str}. Alte war: '{old_crm_website_url}'. Bereite Update vor.") + found_new_url_count += 1 + updates_for_row.append( + {'range': f'{d_letter}{i}', 'values': [[new_url_found_str]]}) + updates_for_row.append( + {'range': f'{ar_letter}{i}', 'values': [['']]}) + updates_for_row.append( + {'range': f'{a_letter}{i}', 'values': [['x']]}) + updates_for_row.append( + {'range': f'{at_letter}{i}', 'values': [['']]}) + updates_for_row.append( + {'range': f'{ao_letter}{i}', 'values': [['']]}) + updates_for_row.append( + {'range': f'{an_letter}{i}', 'values': [['']]}) + updates_for_row.append( + {'range': f'{ax_letter}{i}', 'values': [['']]}) + # Wird unten explizit neu gesetzt + updates_for_row.append( + {'range': f'{ay_letter}{i}', 'values': [['']]}) + updates_for_row.append( + {'range': f'{ap_letter}{i}', 'values': [['']]}) + else: + self.logger.info( + f" -> SerpAPI fand URL '{new_url_found_str}', aber diese ist identisch mit der bereits vorhandenen URL in Spalte D. Keine Änderung in D.") + updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [ + ["k.A. (URL via SerpAPI identisch mit alter URL)"]]}) + else: + self.logger.warning( + f" -> Keine neue gueltige URL via SerpAPI für '{company_name[:50]}...' gefunden. Setze AR auf 'k.A. (Keine URL bei Neusuche)'.") + updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [ + ["k.A. (Keine URL bei Neusuche)"]]}) except Exception as e_serp_lookup: - self.logger.error(f"FEHLER bei SERP Website Lookup für Zeile {i} ('{company_name[:50]}...'): {e_serp_lookup}") - updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [[f"k.A. (Fehler URL Suche)"]]}) - pass + self.logger.error( + f"FEHLER bei SERP Website Lookup für Zeile {i} ('{company_name[:50]}...'): {e_serp_lookup}") + updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [ + [f"k.A. (Fehler URL Suche)"]]}) + pass - updates_for_row.append({'range': f'{ay_letter}{i}', 'values': [[now_timestamp_str]]}) # AY Timestamp immer setzen + updates_for_row.append({'range': f'{ay_letter}{i}', 'values': [ + [now_timestamp_str]]}) # AY Timestamp immer setzen all_sheet_updates.extend(updates_for_row) - if len(all_sheet_updates) >= update_batch_row_limit * 3 : # Angepasst, da Anzahl Updates variiert - self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Operationen)...") - success = self.sheet_handler.batch_update_cells(all_sheet_updates) - if success: self.logger.info(f" Sheet-Update für {len(all_sheet_updates)} Operationen erfolgreich.") - all_sheet_updates = [] + if len(all_sheet_updates) >= update_batch_row_limit * \ + 3: # Angepasst, da Anzahl Updates variiert + self.logger.debug( + f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Operationen)...") + success = self.sheet_handler.batch_update_cells( + all_sheet_updates) + if success: + self.logger.info( + f" Sheet-Update für {len(all_sheet_updates)} Operationen erfolgreich.") + all_sheet_updates = [] serp_delay = getattr(Config, 'SERPAPI_DELAY', 1.5) time.sleep(serp_delay) if all_sheet_updates: - self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Operationen)...") + self.logger.info( + f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Operationen)...") success = self.sheet_handler.batch_update_cells(all_sheet_updates) - if success: self.logger.info(f"FINALES Sheet-Update erfolgreich.") + if success: + self.logger.info(f"FINALES Sheet-Update erfolgreich.") - self.logger.info(f"Modus 'check_urls' abgeschlossen. {processed_count} Zeilen mit Marker/Fehler verarbeitet, {found_new_url_count} neue URLs gefunden, {skipped_count} Zeilen uebersprungen.") + self.logger.info( + f"Modus 'check_urls' abgeschlossen. {processed_count} Zeilen mit Marker/Fehler verarbeitet, {found_new_url_count} neue URLs gefunden, {skipped_count} Zeilen uebersprungen.") - def process_repair_sitz_data(self, start_sheet_row=None, end_sheet_row=None, limit=None): + def process_repair_sitz_data( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None): """ Wendet die verbesserte Sitz-Parsing-Logik auf bestehende Daten an. """ - self.logger.info(f"Starte Modus 'Sitz-Daten Reparatur'. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") + self.logger.info( + f"Starte Modus 'Sitz-Daten Reparatur'. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") """ Liest bestehende Sitz-Stadt/Land-Angaben, wendet die verbesserte Parsing-Logik an und aktualisiert das Sheet, falls sich Änderungen ergeben. """ - self.logger.info(f"Starte Modus 'Sitz-Daten Reparatur'. Bereich: {start_sheet_row if start_sheet_row is not None else 'Komplett ab Datenstart'}, End: {end_sheet_row if end_sheet_row else 'Sheet-Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") + self.logger.info( + f"Starte Modus 'Sitz-Daten Reparatur'. Bereich: {start_sheet_row if start_sheet_row is not None else 'Komplett ab Datenstart'}, End: {end_sheet_row if end_sheet_row else 'Sheet-Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") if not self.sheet_handler.load_data(): - self.logger.error("Konnte Sheet-Daten nicht laden für Sitz-Reparatur. Abbruch.") + self.logger.error( + "Konnte Sheet-Daten nicht laden für Sitz-Reparatur. Abbruch.") return all_data = self.sheet_handler.get_all_data_with_headers() header_offset = self.sheet_handler._header_rows - + stadt_col_idx = COLUMN_MAP.get("Wiki Sitz Stadt") land_col_idx = COLUMN_MAP.get("Wiki Sitz Land") # Optional: Eine Spalte für den originalen Roh-Sitz-String, falls vorhanden - # roh_sitz_col_idx = COLUMN_MAP.get("IHRE_ROH_SITZ_SPALTE") + # roh_sitz_col_idx = COLUMN_MAP.get("IHRE_ROH_SITZ_SPALTE") if stadt_col_idx is None or land_col_idx is None: - self.logger.error("Spaltenindizes für 'Wiki Sitz Stadt' oder 'Wiki Sitz Land' nicht in COLUMN_MAP. Abbruch.") + self.logger.error( + "Spaltenindizes für 'Wiki Sitz Stadt' oder 'Wiki Sitz Land' nicht in COLUMN_MAP. Abbruch.") return updates_fuer_sheet = [] @@ -3148,22 +4444,28 @@ class DataProcessor: updated_rows_count = 0 effective_start_row = start_sheet_row if start_sheet_row is not None else header_offset + 1 - effective_end_row = end_sheet_row if end_sheet_row is not None else len(all_data) + effective_end_row = end_sheet_row if end_sheet_row is not None else len( + all_data) - self.logger.info(f"Prüfe Zeilen {effective_start_row} bis {effective_end_row} für Sitz-Reparatur.") + self.logger.info( + f"Prüfe Zeilen {effective_start_row} bis {effective_end_row} für Sitz-Reparatur.") for row_num_sheet in range(effective_start_row, effective_end_row + 1): if limit is not None and processed_rows_count >= limit: - self.logger.info(f"Limit von {limit} erreichten Zeilen für Sitz-Reparatur erreicht.") + self.logger.info( + f"Limit von {limit} erreichten Zeilen für Sitz-Reparatur erreicht.") break row_list_idx = row_num_sheet - 1 - if row_list_idx >= len(all_data): break # Ende der Daten erreicht + if row_list_idx >= len(all_data): + break # Ende der Daten erreicht row_data = all_data[row_list_idx] - - aktuelle_stadt = self._get_cell_value_safe(row_data, "Wiki Sitz Stadt") - aktuelle_land = self._get_cell_value_safe(row_data, "Wiki Sitz Land") + + aktuelle_stadt = self._get_cell_value_safe( + row_data, "Wiki Sitz Stadt") + aktuelle_land = self._get_cell_value_safe( + row_data, "Wiki Sitz Land") # Erzeuge den Input-String für die Parsing-Funktion # Besser: Wenn Sie den *ursprünglichen* String aus der Wikipedia Infobox @@ -3171,28 +4473,34 @@ class DataProcessor: # Als Fallback kombinieren wir aktuelle Stadt und Land. input_sitz_string = aktuelle_stadt if aktuelle_land and aktuelle_land.lower() not in ["", "k.a."]: - if input_sitz_string and input_sitz_string.lower() not in ["", "k.a."]: - input_sitz_string += f", {aktuelle_land}" # Kombiniere mit Komma + if input_sitz_string and input_sitz_string.lower() not in [ + "", "k.a."]: + # Kombiniere mit Komma + input_sitz_string += f", {aktuelle_land}" else: - input_sitz_string = aktuelle_land # Wenn Stadt leer/kA, nimm nur Land + input_sitz_string = aktuelle_land # Wenn Stadt leer/kA, nimm nur Land - if not input_sitz_string or not input_sitz_string.strip() or input_sitz_string.lower() == 'k.a.': + if not input_sitz_string or not input_sitz_string.strip( + ) or input_sitz_string.lower() == 'k.a.': # self.logger.debug(f"Zeile {row_num_sheet}: Keine validen aktuellen Sitzdaten ('{aktuelle_stadt}', '{aktuelle_land}') zum Reparieren.") continue - + processed_rows_count += 1 - + try: # Verwende die neue Parsing-Methode des WikipediaScrapers - # Stellen Sie sicher, dass self.wiki_scraper eine Instanz von WikipediaScraper ist - parsed_sitz_info = self.wiki_scraper._parse_sitz_string_detailed(input_sitz_string) + # Stellen Sie sicher, dass self.wiki_scraper eine Instanz von + # WikipediaScraper ist + parsed_sitz_info = self.wiki_scraper._parse_sitz_string_detailed( + input_sitz_string) neue_stadt = parsed_sitz_info.get('sitz_stadt', 'k.A.') neues_land = parsed_sitz_info.get('sitz_land', 'k.A.') # Nur updaten, wenn sich etwas geändert hat - if (neue_stadt != aktuelle_stadt and not (neue_stadt == "k.A." and aktuelle_stadt == "")) or \ - (neues_land != aktuelle_land and not (neues_land == "k.A." and aktuelle_land == "")): - self.logger.info(f"Zeile {row_num_sheet}: SITZ-UPDATE. Input: '{input_sitz_string[:60]}...' Alt: '{aktuelle_stadt} / {aktuelle_land}' -> Neu: '{neue_stadt} / {neues_land}'") + if (neue_stadt != aktuelle_stadt and not (neue_stadt == "k.A." and aktuelle_stadt == "")) or ( + neues_land != aktuelle_land and not (neues_land == "k.A." and aktuelle_land == "")): + self.logger.info( + f"Zeile {row_num_sheet}: SITZ-UPDATE. Input: '{input_sitz_string[:60]}...' Alt: '{aktuelle_stadt} / {aktuelle_land}' -> Neu: '{neue_stadt} / {neues_land}'") updates_fuer_sheet.append({ 'range': f'{self.sheet_handler._get_col_letter(stadt_col_idx + 1)}{row_num_sheet}', 'values': [[neue_stadt]] @@ -3205,23 +4513,27 @@ class DataProcessor: # else: # self.logger.debug(f"Zeile {row_num_sheet}: Keine Änderung bei Sitzdaten für Input '{input_sitz_string[:60]}...'. Alt: '{aktuelle_stadt} / {aktuelle_land}', Neu: '{neue_stadt} / {neues_land}'") - except Exception as e_parse: - self.logger.error(f"Fehler beim Parsen des Sitzes für Zeile {row_num_sheet} mit Input '{input_sitz_string}': {e_parse}") + self.logger.error( + f"Fehler beim Parsen des Sitzes für Zeile {row_num_sheet} mit Input '{input_sitz_string}': {e_parse}") # Batch-Update Logik - if len(updates_fuer_sheet) >= getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) * 2: # Mal 2, da zwei Spalten pro Zeile - self.logger.info(f"Sende Batch-Update für {len(updates_fuer_sheet)//2} Sitzreparaturen...") + if len(updates_fuer_sheet) >= getattr( + Config, 'UPDATE_BATCH_ROW_LIMIT', 50) * 2: # Mal 2, da zwei Spalten pro Zeile + self.logger.info( + f"Sende Batch-Update für {len(updates_fuer_sheet)//2} Sitzreparaturen...") self.sheet_handler.batch_update_cells(updates_fuer_sheet) updates_fuer_sheet = [] # time.sleep(1) # Optionale Pause # Letzten Batch senden if updates_fuer_sheet: - self.logger.info(f"Sende finalen Batch-Update für {len(updates_fuer_sheet)//2} Sitzreparaturen...") + self.logger.info( + f"Sende finalen Batch-Update für {len(updates_fuer_sheet)//2} Sitzreparaturen...") self.sheet_handler.batch_update_cells(updates_fuer_sheet) - self.logger.info(f"Sitz-Daten Reparatur abgeschlossen. {processed_rows_count} Zeilen geprüft, {updated_rows_count} Zeilen aktualisiert.") + self.logger.info( + f"Sitz-Daten Reparatur abgeschlossen. {processed_rows_count} Zeilen geprüft, {updated_rows_count} Zeilen aktualisiert.") def _get_numeric_value_for_plausi(self, value_str, is_umsatz=False): """ @@ -3231,7 +4543,8 @@ class DataProcessor: # Diese Funktion ist relativ kurz und könnte hier stehen bleiben. from helpers import extract_numeric_value extracted_val_str = extract_numeric_value(value_str, is_umsatz) - if extracted_val_str.lower() in ['k.a.', '0']: return np.nan + if extracted_val_str.lower() in ['k.a.', '0']: + return np.nan try: num_val = float(extracted_val_str) return num_val * 1000000.0 if is_umsatz else num_val @@ -3243,29 +4556,38 @@ class DataProcessor: Führt die Plausibilitätschecks für eine Zeile durch. """ results = { - "plaus_umsatz_flag": "NICHT_PRUEFBAR", "plaus_ma_flag": "NICHT_PRUEFBAR", - "plaus_ratio_flag": "NICHT_PRUEFBAR", "abweichung_umsatz_flag": "N/A", + "plaus_umsatz_flag": "NICHT_PRUEFBAR", + "plaus_ma_flag": "NICHT_PRUEFBAR", + "plaus_ratio_flag": "NICHT_PRUEFBAR", + "abweichung_umsatz_flag": "N/A", "abweichung_ma_flag": "N/A", - "plausi_begruendung_final": "Plausibilität OK" - } - temp_begruendungen = [] + "plausi_begruendung_final": "Plausibilität OK"} + temp_begruendungen = [] - parent_account_name_d_val = row_data_dict.get("Parent Account Name", "").strip() - is_konzern_tochter_laut_d = bool(parent_account_name_d_val and parent_account_name_d_val.lower() != 'k.a.') + parent_account_name_d_val = row_data_dict.get( + "Parent Account Name", "").strip() + is_konzern_tochter_laut_d = bool( + parent_account_name_d_val and parent_account_name_d_val.lower() != 'k.a.') # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # +++ NEU/ERWEITERT: Info aus Spalte O und P für Konzernlogik heranziehen +++ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # Diese Keys müssen im row_data_dict vorhanden sein, wenn diese Funktion aufgerufen wird! - parent_o_val = row_data_dict.get("System Vorschlag Parent Account", "").strip().lower() - parent_p_val = row_data_dict.get("Parent Vorschlag Status", "").strip().lower() - is_konzern_tochter_laut_o_und_p = bool(parent_o_val and parent_o_val != 'k.a.' and parent_p_val == 'x') - - # (Das `wiki_stammt_von_parent_explizit` Flag ist optional, wenn Sie den anderen Ansatz verfolgen) - # wiki_stammt_von_parent_explizit = row_data_dict.get("Wiki Daten von Parent", False) + # Diese Keys müssen im row_data_dict vorhanden sein, wenn diese + # Funktion aufgerufen wird! + parent_o_val = row_data_dict.get( + "System Vorschlag Parent Account", + "").strip().lower() + parent_p_val = row_data_dict.get( + "Parent Vorschlag Status", "").strip().lower() + is_konzern_tochter_laut_o_und_p = bool( + parent_o_val and parent_o_val != 'k.a.' and parent_p_val == 'x') + + # (Das `wiki_stammt_von_parent_explizit` Flag ist optional, wenn Sie den anderen Ansatz verfolgen) + # wiki_stammt_von_parent_explizit = row_data_dict.get("Wiki Daten von Parent", False) + + # or wiki_stammt_von_parent_explizit + is_part_of_a_group_for_plausi = is_konzern_tochter_laut_d or is_konzern_tochter_laut_o_und_p - is_part_of_a_group_for_plausi = is_konzern_tochter_laut_d or is_konzern_tochter_laut_o_und_p # or wiki_stammt_von_parent_explizit - log_msg_group_parts = [] if is_konzern_tochter_laut_d: log_msg_group_parts.append(f"D='{parent_account_name_d_val}'") @@ -3273,135 +4595,186 @@ class DataProcessor: log_msg_group_parts.append(f"O/P='{parent_o_val}/{parent_p_val}'") # if wiki_stammt_von_parent_explizit: # log_msg_group_parts.append("WikiParentFlag=True") - - if is_part_of_a_group_for_plausi: - self.logger.debug(f" PlausiCheck: Unternehmen ist Teil einer Gruppe ({'; '.join(log_msg_group_parts)}). Abweichungs-Checks CRM/Wiki werden als INFO_KONZERN_LOGIK behandelt.") - # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # +++ ENDE NEU/ERWEITERT ++++++++++++++++++++++++++++++++++++++++++++++++++ - # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + if is_part_of_a_group_for_plausi: + self.logger.debug( + f" PlausiCheck: Unternehmen ist Teil einer Gruppe ({'; '.join(log_msg_group_parts)}). Abweichungs-Checks CRM/Wiki werden als INFO_KONZERN_LOGIK behandelt.") + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # +++ ENDE NEU/ERWEITERT ++++++++++++++++++++++++++++++++++++++++++++++ + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # --- 1. Plausibilität Finaler Umsatz (BG) --- - final_umsatz_str = row_data_dict.get("Finaler Umsatz (Wiki>CRM)", "k.A.") - umsatz_num_absolut = self._get_numeric_value_for_plausi(final_umsatz_str, is_umsatz=True) - - exclusion_list_common = ['k.a.', '', 'n/a', '-', '0', '0.0', '0,00', '0.000', '0.00'] + final_umsatz_str = row_data_dict.get( + "Finaler Umsatz (Wiki>CRM)", "k.A.") + umsatz_num_absolut = self._get_numeric_value_for_plausi( + final_umsatz_str, is_umsatz=True) + + exclusion_list_common = ['k.a.', '', 'n/a', + '-', '0', '0.0', '0,00', '0.000', '0.00'] if pd.isna(umsatz_num_absolut): - if final_umsatz_str.lower().strip() not in exclusion_list_common and not final_umsatz_str.startswith("FEHLER"): + if final_umsatz_str.lower().strip( + ) not in exclusion_list_common and not final_umsatz_str.startswith("FEHLER"): results["plaus_umsatz_flag"] = "FEHLER_FORMAT" - temp_begruendungen.append(f"Finaler Umsatz ('{final_umsatz_str}') konnte nicht als Zahl interpretiert werden.") - else: + temp_begruendungen.append( + f"Finaler Umsatz ('{final_umsatz_str}') konnte nicht als Zahl interpretiert werden.") + else: results["plaus_umsatz_flag"] = "OK" - if umsatz_num_absolut == 0 and final_umsatz_str != "0": - results["plaus_umsatz_flag"] = "WARNUNG_NULL_WERT" - temp_begruendungen.append(f"Finaler Umsatz ist numerisch 0 (aus '{final_umsatz_str}').") + if umsatz_num_absolut == 0 and final_umsatz_str != "0": + results["plaus_umsatz_flag"] = "WARNUNG_NULL_WERT" + temp_begruendungen.append( + f"Finaler Umsatz ist numerisch 0 (aus '{final_umsatz_str}').") elif umsatz_num_absolut < getattr(Config, 'PLAUSI_UMSATZ_MIN_WARNUNG', 50000): results["plaus_umsatz_flag"] = "WARNUNG_NIEDRIG" - temp_begruendungen.append(f"Finaler Umsatz ({umsatz_num_absolut:,.0f} €) < Min-Schwelle ({getattr(Config, 'PLAUSI_UMSATZ_MIN_WARNUNG', 50000):,.0f} €).") + temp_begruendungen.append( + f"Finaler Umsatz ({umsatz_num_absolut:,.0f} €) < Min-Schwelle ({getattr(Config, 'PLAUSI_UMSATZ_MIN_WARNUNG', 50000):,.0f} €).") elif umsatz_num_absolut > getattr(Config, 'PLAUSI_UMSATZ_MAX_WARNUNG', 200000000000): results["plaus_umsatz_flag"] = "WARNUNG_HOCH" - temp_begruendungen.append(f"Finaler Umsatz ({umsatz_num_absolut:,.0f} €) > Max-Schwelle ({getattr(Config, 'PLAUSI_UMSATZ_MAX_WARNUNG', 200000000000):,.0f} €).") + temp_begruendungen.append( + f"Finaler Umsatz ({umsatz_num_absolut:,.0f} €) > Max-Schwelle ({getattr(Config, 'PLAUSI_UMSATZ_MAX_WARNUNG', 200000000000):,.0f} €).") # --- 2. Plausibilität Finale Mitarbeiter (BH) --- - final_ma_str = row_data_dict.get("Finaler Mitarbeiter (Wiki>CRM)", "k.A.") - ma_num_absolut = self._get_numeric_value_for_plausi(final_ma_str, is_umsatz=False) - + final_ma_str = row_data_dict.get( + "Finaler Mitarbeiter (Wiki>CRM)", "k.A.") + ma_num_absolut = self._get_numeric_value_for_plausi( + final_ma_str, is_umsatz=False) + if pd.isna(ma_num_absolut): - if final_ma_str.lower().strip() not in exclusion_list_common and not final_ma_str.startswith("FEHLER"): + if final_ma_str.lower().strip( + ) not in exclusion_list_common and not final_ma_str.startswith("FEHLER"): results["plaus_ma_flag"] = "FEHLER_FORMAT" - temp_begruendungen.append(f"Finale MA ('{final_ma_str}') konnte nicht als Zahl interpretiert werden.") + temp_begruendungen.append( + f"Finale MA ('{final_ma_str}') konnte nicht als Zahl interpretiert werden.") else: results["plaus_ma_flag"] = "OK" if ma_num_absolut == 0 and final_ma_str != "0": - results["plaus_ma_flag"] = "WARNUNG_NULL_WERT" - temp_begruendungen.append(f"Finale MA ist numerisch 0 (aus '{final_ma_str}').") + results["plaus_ma_flag"] = "WARNUNG_NULL_WERT" + temp_begruendungen.append( + f"Finale MA ist numerisch 0 (aus '{final_ma_str}').") elif ma_num_absolut < getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_ABS', 1): results["plaus_ma_flag"] = "WARNUNG_NIEDRIG" - temp_begruendungen.append(f"Finale MA ({ma_num_absolut:.0f}) < Min-Schwelle ({getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_ABS', 1):.0f}).") + temp_begruendungen.append( + f"Finale MA ({ma_num_absolut:.0f}) < Min-Schwelle ({getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_ABS', 1):.0f}).") elif not pd.isna(umsatz_num_absolut) and umsatz_num_absolut >= getattr(Config, 'PLAUSI_UMSATZ_MIN_SCHWELLE_FUER_MA_CHECK', 1000000) and \ - ma_num_absolut < getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_BEI_UMSATZ', 3): + ma_num_absolut < getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_BEI_UMSATZ', 3): results["plaus_ma_flag"] = "WARNUNG_ZU_WENIG_MA_BEI_UMSATZ" - temp_begruendungen.append(f"Finale MA ({ma_num_absolut:.0f}) auffällig niedrig für Umsatz ({umsatz_num_absolut:,.0f} €).") + temp_begruendungen.append( + f"Finale MA ({ma_num_absolut:.0f}) auffällig niedrig für Umsatz ({umsatz_num_absolut:,.0f} €).") elif ma_num_absolut > getattr(Config, 'PLAUSI_MA_MAX_WARNUNG', 1000000): results["plaus_ma_flag"] = "WARNUNG_HOCH" - temp_begruendungen.append(f"Finale MA ({ma_num_absolut:.0f}) > Max-Schwelle ({getattr(Config, 'PLAUSI_MA_MAX_WARNUNG', 1000000):,.0f}).") + temp_begruendungen.append( + f"Finale MA ({ma_num_absolut:.0f}) > Max-Schwelle ({getattr(Config, 'PLAUSI_MA_MAX_WARNUNG', 1000000):,.0f}).") # --- 3. Plausibilität Umsatz/MA Ratio (BI) --- - if not pd.isna(umsatz_num_absolut) and not pd.isna(ma_num_absolut) and umsatz_num_absolut > 0 : + if not pd.isna(umsatz_num_absolut) and not pd.isna( + ma_num_absolut) and umsatz_num_absolut > 0: if ma_num_absolut > 0: ratio = umsatz_num_absolut / ma_num_absolut results["plaus_ratio_flag"] = "OK" - if ratio < getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MIN', 25000): + if ratio < getattr( + Config, + 'PLAUSI_RATIO_UMSATZ_PRO_MA_MIN', + 25000): results["plaus_ratio_flag"] = "WARNUNG_RATIO_NIEDRIG" - temp_begruendungen.append(f"Umsatz/MA Ratio ({ratio:,.0f} €/MA) < Min-Schwelle ({getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MIN', 25000):,.0f} €/MA).") + temp_begruendungen.append( + f"Umsatz/MA Ratio ({ratio:,.0f} €/MA) < Min-Schwelle ({getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MIN', 25000):,.0f} €/MA).") elif ratio > getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MAX', 1500000): results["plaus_ratio_flag"] = "WARNUNG_RATIO_HOCH" - temp_begruendungen.append(f"Umsatz/MA Ratio ({ratio:,.0f} €/MA) > Max-Schwelle ({getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MAX', 1500000):,.0f} €/MA).") - elif ma_num_absolut == 0: + temp_begruendungen.append( + f"Umsatz/MA Ratio ({ratio:,.0f} €/MA) > Max-Schwelle ({getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MAX', 1500000):,.0f} €/MA).") + elif ma_num_absolut == 0: results["plaus_ratio_flag"] = "FEHLER_MA_NULL_BEI_UMSATZ" - temp_begruendungen.append("Umsatz vorhanden, aber MA ist 0. Ratio nicht berechenbar.") - elif pd.isna(umsatz_num_absolut) or umsatz_num_absolut <= 0 : + temp_begruendungen.append( + "Umsatz vorhanden, aber MA ist 0. Ratio nicht berechenbar.") + elif pd.isna(umsatz_num_absolut) or umsatz_num_absolut <= 0: results["plaus_ratio_flag"] = "NICHT_PRUEFBAR_UMSATZ_FEHLT" - + # --- 4. Abgleich CRM vs. Wiki (BJ, BK) --- crm_umsatz_str = row_data_dict.get("CRM Umsatz", "k.A.") - wiki_umsatz_str = row_data_dict.get("Wiki Umsatz", "k.A.") + wiki_umsatz_str = row_data_dict.get("Wiki Umsatz", "k.A.") crm_ma_str = row_data_dict.get("CRM Anzahl Mitarbeiter", "k.A.") - wiki_ma_str = row_data_dict.get("Wiki Mitarbeiter", "k.A.") + wiki_ma_str = row_data_dict.get("Wiki Mitarbeiter", "k.A.") - crm_u_abs = self._get_numeric_value_for_plausi(crm_umsatz_str, is_umsatz=True) - wiki_u_abs = self._get_numeric_value_for_plausi(wiki_umsatz_str, is_umsatz=True) - crm_m_abs_comp = self._get_numeric_value_for_plausi(crm_ma_str, is_umsatz=False) - wiki_m_abs_comp = self._get_numeric_value_for_plausi(wiki_ma_str, is_umsatz=False) - - abweichung_prozent_config = getattr(Config, 'PLAUSI_ABWEICHUNG_CRM_WIKI_PROZENT', 30) / 100.0 + crm_u_abs = self._get_numeric_value_for_plausi( + crm_umsatz_str, is_umsatz=True) + wiki_u_abs = self._get_numeric_value_for_plausi( + wiki_umsatz_str, is_umsatz=True) + crm_m_abs_comp = self._get_numeric_value_for_plausi( + crm_ma_str, is_umsatz=False) + wiki_m_abs_comp = self._get_numeric_value_for_plausi( + wiki_ma_str, is_umsatz=False) + + abweichung_prozent_config = getattr( + Config, 'PLAUSI_ABWEICHUNG_CRM_WIKI_PROZENT', 30) / 100.0 # Umsatz Abweichung (BJ) - if pd.notna(crm_u_abs) and pd.notna(wiki_u_abs) and crm_u_abs > 0 and wiki_u_abs > 0 : - if is_part_of_a_group_for_plausi: # ERWEITERTE BEDINGUNG HIER VERWENDEN + if pd.notna(crm_u_abs) and pd.notna( + wiki_u_abs) and crm_u_abs > 0 and wiki_u_abs > 0: + if is_part_of_a_group_for_plausi: # ERWEITERTE BEDINGUNG HIER VERWENDEN results["abweichung_umsatz_flag"] = "INFO_KONZERN_LOGIK" - else: + else: diff_umsatz = abs(crm_u_abs - wiki_u_abs) - bezugswert_umsatz = max(crm_u_abs, wiki_u_abs) if max(crm_u_abs, wiki_u_abs) > 0 else 1 + bezugswert_umsatz = max( + crm_u_abs, wiki_u_abs) if max( + crm_u_abs, wiki_u_abs) > 0 else 1 if (diff_umsatz / bezugswert_umsatz) > abweichung_prozent_config: results["abweichung_umsatz_flag"] = "WARNUNG_SIGNIFIKANT" - temp_begruendungen.append(f"Umsatz CRM ({crm_u_abs:,.0f} €) vs. Wiki ({wiki_u_abs:,.0f} €) weicht >{abweichung_prozent_config*100:.0f}% ab.") + temp_begruendungen.append( + f"Umsatz CRM ({crm_u_abs:,.0f} €) vs. Wiki ({wiki_u_abs:,.0f} €) weicht >{abweichung_prozent_config*100:.0f}% ab.") else: results["abweichung_umsatz_flag"] = "OK" - elif pd.notna(crm_u_abs) and crm_u_abs > 0 and (pd.isna(wiki_u_abs) or wiki_u_abs <=0) : results["abweichung_umsatz_flag"] = "WIKI_FEHLT_ODER_NULL" - elif (pd.isna(crm_u_abs) or crm_u_abs <=0) and pd.notna(wiki_u_abs) and wiki_u_abs > 0 : results["abweichung_umsatz_flag"] = "CRM_FEHLT_ODER_NULL" - else: results["abweichung_umsatz_flag"] = "BEIDE_FEHLEN_ODER_NULL" - + elif pd.notna(crm_u_abs) and crm_u_abs > 0 and (pd.isna(wiki_u_abs) or wiki_u_abs <= 0): + results["abweichung_umsatz_flag"] = "WIKI_FEHLT_ODER_NULL" + elif (pd.isna(crm_u_abs) or crm_u_abs <= 0) and pd.notna(wiki_u_abs) and wiki_u_abs > 0: + results["abweichung_umsatz_flag"] = "CRM_FEHLT_ODER_NULL" + else: + results["abweichung_umsatz_flag"] = "BEIDE_FEHLEN_ODER_NULL" + # Mitarbeiter Abweichung (BK) - if pd.notna(crm_m_abs_comp) and pd.notna(wiki_m_abs_comp) and crm_m_abs_comp > 0 and wiki_m_abs_comp > 0: - if is_part_of_a_group_for_plausi: # ERWEITERTE BEDINGUNG HIER VERWENDEN + if pd.notna(crm_m_abs_comp) and pd.notna( + wiki_m_abs_comp) and crm_m_abs_comp > 0 and wiki_m_abs_comp > 0: + if is_part_of_a_group_for_plausi: # ERWEITERTE BEDINGUNG HIER VERWENDEN results["abweichung_ma_flag"] = "INFO_KONZERN_LOGIK" - else: + else: diff_ma = abs(crm_m_abs_comp - wiki_m_abs_comp) - bezugswert_ma = max(crm_m_abs_comp, wiki_m_abs_comp) if max(crm_m_abs_comp, wiki_m_abs_comp) > 0 else 1 + bezugswert_ma = max( + crm_m_abs_comp, + wiki_m_abs_comp) if max( + crm_m_abs_comp, + wiki_m_abs_comp) > 0 else 1 if (diff_ma / bezugswert_ma) > abweichung_prozent_config: results["abweichung_ma_flag"] = "WARNUNG_SIGNIFIKANT" - temp_begruendungen.append(f"MA CRM ({crm_m_abs_comp:.0f}) vs. Wiki ({wiki_m_abs_comp:.0f}) weicht >{abweichung_prozent_config*100:.0f}% ab.") + temp_begruendungen.append( + f"MA CRM ({crm_m_abs_comp:.0f}) vs. Wiki ({wiki_m_abs_comp:.0f}) weicht >{abweichung_prozent_config*100:.0f}% ab.") else: results["abweichung_ma_flag"] = "OK" - elif pd.notna(crm_m_abs_comp) and crm_m_abs_comp > 0 and (pd.isna(wiki_m_abs_comp) or wiki_m_abs_comp <=0): results["abweichung_ma_flag"] = "WIKI_FEHLT_ODER_NULL" - elif (pd.isna(crm_m_abs_comp) or crm_m_abs_comp <=0) and pd.notna(wiki_m_abs_comp) and wiki_m_abs_comp > 0: results["abweichung_ma_flag"] = "CRM_FEHLT_ODER_NULL" - else: results["abweichung_ma_flag"] = "BEIDE_FEHLEN_ODER_NULL" + elif pd.notna(crm_m_abs_comp) and crm_m_abs_comp > 0 and (pd.isna(wiki_m_abs_comp) or wiki_m_abs_comp <= 0): + results["abweichung_ma_flag"] = "WIKI_FEHLT_ODER_NULL" + elif (pd.isna(crm_m_abs_comp) or crm_m_abs_comp <= 0) and pd.notna(wiki_m_abs_comp) and wiki_m_abs_comp > 0: + results["abweichung_ma_flag"] = "CRM_FEHLT_ODER_NULL" + else: + results["abweichung_ma_flag"] = "BEIDE_FEHLEN_ODER_NULL" if temp_begruendungen: results["plausi_begruendung_final"] = "; ".join(temp_begruendungen) - + return results - def run_plausibility_checks_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None): + def run_plausibility_checks_batch( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None): """ Führt Konsolidierung und Plausi-Checks für einen Bereich von Zeilen aus. """ - self.logger.info(f"Starte Modus 'plausi_check_data'. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") - self.logger.info(f"Starte Modus 'plausi_check_data' (Konsolidierung & Plausi-Checks). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}") + self.logger.info( + f"Starte Modus 'plausi_check_data'. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") + self.logger.info( + f"Starte Modus 'plausi_check_data' (Konsolidierung & Plausi-Checks). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}") if not self.sheet_handler.load_data(): - self.logger.error("Konnte Sheet-Daten nicht laden für Plausi-Checks. Abbruch.") + self.logger.error( + "Konnte Sheet-Daten nicht laden für Plausi-Checks. Abbruch.") return all_data = self.sheet_handler.get_all_data_with_headers() @@ -3412,140 +4785,251 @@ class DataProcessor: effective_end_row = end_sheet_row if end_sheet_row is not None else total_sheet_rows if effective_start_row > effective_end_row or effective_start_row > total_sheet_rows: - self.logger.info("Start liegt nach Ende oder außerhalb des Sheets. Keine Zeilen zu verarbeiten.") + self.logger.info( + "Start liegt nach Ende oder außerhalb des Sheets. Keine Zeilen zu verarbeiten.") return - - self.logger.info(f"Verarbeite Zeilen {effective_start_row} bis {effective_end_row} für Konsolidierung und Plausi-Checks.") - required_keys_for_plausi_mode = [ # Schlüssel wie gehabt + self.logger.info( + f"Verarbeite Zeilen {effective_start_row} bis {effective_end_row} für Konsolidierung und Plausi-Checks.") + + required_keys_for_plausi_mode = [ # Schlüssel wie gehabt "CRM Umsatz", "Wiki Umsatz", "CRM Anzahl Mitarbeiter", "Wiki Mitarbeiter", "Parent Account Name", "Finaler Umsatz (Wiki>CRM)", "Finaler Mitarbeiter (Wiki>CRM)", "Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio", "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki", "Plausibilität Begründung", - "Plausibilität Prüfdatum", "CRM Name" # CRM Name für Logging hinzugefügt + "Plausibilität Prüfdatum", "CRM Name" # CRM Name für Logging hinzugefügt ] - if not all(key in COLUMN_MAP for key in required_keys_for_plausi_mode): - missing_k = [k for k in required_keys_for_plausi_mode if k not in COLUMN_MAP] - self.logger.error(f"Nicht alle benötigten Spalten ({missing_k}) für Modus 'plausi_check_data' in COLUMN_MAP. Abbruch.") + if not all( + key in COLUMN_MAP for key in required_keys_for_plausi_mode): + missing_k = [ + k for k in required_keys_for_plausi_mode if k not in COLUMN_MAP] + self.logger.error( + f"Nicht alle benötigten Spalten ({missing_k}) für Modus 'plausi_check_data' in COLUMN_MAP. Abbruch.") return all_sheet_updates = [] processed_rows_count = 0 now_timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - update_batch_limit_config = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) - - for row_num_sheet in range(effective_start_row, effective_end_row + 1): + update_batch_limit_config = getattr( + Config, 'UPDATE_BATCH_ROW_LIMIT', 50) + + for row_num_sheet in range( + effective_start_row, + effective_end_row + 1): if limit is not None and processed_rows_count >= limit: - self.logger.info(f"Verarbeitungslimit von {limit} Zeilen erreicht.") + self.logger.info( + f"Verarbeitungslimit von {limit} Zeilen erreicht.") break - + row_list_idx = row_num_sheet - 1 - if row_list_idx >= total_sheet_rows: break + if row_list_idx >= total_sheet_rows: + break row_data = all_data[row_list_idx] - crm_name_check = self._get_cell_value_safe(row_data, "CRM Name").strip() + crm_name_check = self._get_cell_value_safe( + row_data, "CRM Name").strip() if not crm_name_check: continue - - self.logger.debug(f"Zeile {row_num_sheet} ({crm_name_check[:30]}...): Starte Konsolidierung und Plausi-Check.") + + self.logger.debug( + f"Zeile {row_num_sheet} ({crm_name_check[:30]}...): Starte Konsolidierung und Plausi-Check.") current_row_updates = [] # 1. Konsolidierung (BD, BE) - final_umsatz_str_konsolidiert = "k.A." + final_umsatz_str_konsolidiert = "k.A." final_ma_str_konsolidiert = "k.A." - parent_account_name_d_val = self._get_cell_value_safe(row_data, "Parent Account Name").strip() - parent_o_val_plausi = self._get_cell_value_safe(row_data, "System Vorschlag Parent Account").strip() # Für Plausi-Check holen - parent_p_val_plausi = self._get_cell_value_safe(row_data, "Parent Vorschlag Status").strip() # Für Plausi-Check holen - + parent_account_name_d_val = self._get_cell_value_safe( + row_data, "Parent Account Name").strip() + parent_o_val_plausi = self._get_cell_value_safe( + row_data, "System Vorschlag Parent Account").strip() # Für Plausi-Check holen + parent_p_val_plausi = self._get_cell_value_safe( + row_data, "Parent Vorschlag Status").strip() # Für Plausi-Check holen try: - crm_umsatz_val_str = self._get_cell_value_safe(row_data, "CRM Umsatz") + crm_umsatz_val_str = self._get_cell_value_safe( + row_data, "CRM Umsatz") # Wiki-Werte direkt aus der Zeile (row_data) lesen - wiki_umsatz_val_str_sheet = self._get_cell_value_safe(row_data, "Wiki Umsatz") - crm_ma_val_str = self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter") - wiki_ma_val_str_sheet = self._get_cell_value_safe(row_data, "Wiki Mitarbeiter") - - num_crm_umsatz = get_numeric_filter_value(crm_umsatz_val_str, is_umsatz=True) - num_wiki_umsatz = get_numeric_filter_value(wiki_umsatz_val_str_sheet, is_umsatz=True) # Verwende _sheet Wert - num_crm_ma = get_numeric_filter_value(crm_ma_val_str, is_umsatz=False) - num_wiki_ma = get_numeric_filter_value(wiki_ma_val_str_sheet, is_umsatz=False) # Verwende _sheet Wert + wiki_umsatz_val_str_sheet = self._get_cell_value_safe( + row_data, "Wiki Umsatz") + crm_ma_val_str = self._get_cell_value_safe( + row_data, "CRM Anzahl Mitarbeiter") + wiki_ma_val_str_sheet = self._get_cell_value_safe( + row_data, "Wiki Mitarbeiter") + + num_crm_umsatz = get_numeric_filter_value( + crm_umsatz_val_str, is_umsatz=True) + num_wiki_umsatz = get_numeric_filter_value( + wiki_umsatz_val_str_sheet, is_umsatz=True) # Verwende _sheet Wert + num_crm_ma = get_numeric_filter_value( + crm_ma_val_str, is_umsatz=False) + num_wiki_ma = get_numeric_filter_value( + wiki_ma_val_str_sheet, is_umsatz=False) # Verwende _sheet Wert if parent_account_name_d_val and parent_account_name_d_val.lower() != 'k.a.': - self.logger.debug(f" -> Parent D ('{parent_account_name_d_val}') ist gesetzt. Konsolidiere primär mit CRM-Daten der Tochter.") + self.logger.debug( + f" -> Parent D ('{parent_account_name_d_val}') ist gesetzt. Konsolidiere primär mit CRM-Daten der Tochter.") final_num_umsatz = num_crm_umsatz if num_crm_umsatz > 0 else num_wiki_umsatz final_num_ma = num_crm_ma if num_crm_ma > 0 else num_wiki_ma else: final_num_umsatz = num_wiki_umsatz if num_wiki_umsatz > 0 else num_crm_umsatz final_num_ma = num_wiki_ma if num_wiki_ma > 0 else num_crm_ma - - final_umsatz_str_konsolidiert = str(int(round(final_num_umsatz))) if final_num_umsatz > 0 else 'k.A.' - final_ma_str_konsolidiert = str(int(round(final_num_ma))) if final_num_ma > 0 else 'k.A.' + + final_umsatz_str_konsolidiert = str( + int(round(final_num_umsatz))) if final_num_umsatz > 0 else 'k.A.' + final_ma_str_konsolidiert = str( + int(round(final_num_ma))) if final_num_ma > 0 else 'k.A.' except Exception as e_conso_batch: - self.logger.error(f"Fehler bei Konsolidierung in Plausi-Batch für Zeile {row_num_sheet}: {e_conso_batch}") + self.logger.error( + f"Fehler bei Konsolidierung in Plausi-Batch für Zeile {row_num_sheet}: {e_conso_batch}") final_umsatz_str_konsolidiert = "FEHLER_KONSO_PLAUSI" final_ma_str_konsolidiert = "FEHLER_KONSO_PLAUSI" - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_sheet}', 'values': [[final_umsatz_str_konsolidiert]]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_sheet}', 'values': [[final_ma_str_konsolidiert]]}) + current_row_updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_sheet}', + 'values': [ + [final_umsatz_str_konsolidiert]]}) + current_row_updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_sheet}', + 'values': [ + [final_ma_str_konsolidiert]]}) # 2. Plausibilitäts-Checks (BG-BM) - if not final_umsatz_str_konsolidiert.startswith("FEHLER") and not final_ma_str_konsolidiert.startswith("FEHLER"): + if not final_umsatz_str_konsolidiert.startswith( + "FEHLER") and not final_ma_str_konsolidiert.startswith("FEHLER"): try: plausi_input_data = { "Finaler Umsatz (Wiki>CRM)": final_umsatz_str_konsolidiert, "Finaler Mitarbeiter (Wiki>CRM)": final_ma_str_konsolidiert, "CRM Umsatz": self._get_cell_value_safe(row_data, "CRM Umsatz"), - "Wiki Umsatz": self._get_cell_value_safe(row_data, "Wiki Umsatz"), + "Wiki Umsatz": self._get_cell_value_safe(row_data, "Wiki Umsatz"), "CRM Anzahl Mitarbeiter": self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter"), - "Wiki Mitarbeiter": self._get_cell_value_safe(row_data, "Wiki Mitarbeiter"), - "Parent Account Name": parent_account_name_d_val, - "System Vorschlag Parent Account": parent_o_val_plausi, # Variable von oben + "Wiki Mitarbeiter": self._get_cell_value_safe(row_data, "Wiki Mitarbeiter"), + "Parent Account Name": parent_account_name_d_val, + "System Vorschlag Parent Account": parent_o_val_plausi, # Variable von oben "Parent Vorschlag Status": parent_p_val_plausi # Variable von oben } - plausi_results = self._check_financial_plausibility(plausi_input_data) + plausi_results = self._check_financial_plausibility( + plausi_input_data) + current_row_updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz"] + 1)}{row_num_sheet}', + 'values': [ + [ + plausi_results.get( + "plaus_umsatz_flag", + "ERR_FLAG")]]}) + current_row_updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Mitarbeiter"] + 1)}{row_num_sheet}', + 'values': [ + [ + plausi_results.get( + "plaus_ma_flag", + "ERR_FLAG")]]}) + current_row_updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz/MA Ratio"] + 1)}{row_num_sheet}', + 'values': [ + [ + plausi_results.get( + "plaus_ratio_flag", + "ERR_FLAG")]]}) + current_row_updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung Umsatz CRM/Wiki"] + 1)}{row_num_sheet}', + 'values': [ + [ + plausi_results.get( + "abweichung_umsatz_flag", + "ERR_FLAG")]]}) + current_row_updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung MA CRM/Wiki"] + 1)}{row_num_sheet}', + 'values': [ + [ + plausi_results.get( + "abweichung_ma_flag", + "ERR_FLAG")]]}) + current_row_updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', + 'values': [ + [ + plausi_results.get( + "plausi_begruendung_final", + "Fehler Begr.")]]}) - - - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plaus_umsatz_flag", "ERR_FLAG")]]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Mitarbeiter"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plaus_ma_flag", "ERR_FLAG")]]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz/MA Ratio"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plaus_ratio_flag", "ERR_FLAG")]]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung Umsatz CRM/Wiki"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("abweichung_umsatz_flag", "ERR_FLAG")]]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung MA CRM/Wiki"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("abweichung_ma_flag", "ERR_FLAG")]]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plausi_begruendung_final", "Fehler Begr.")]]}) - except Exception as e_plausi_run_batch: - self.logger.error(f"Fehler im Plausi-Check Aufruf (Batch-Modus) für Zeile {row_num_sheet}: {e_plausi_run_batch}") - for key_flag in ["Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio", "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki"]: - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP[key_flag] + 1)}{row_num_sheet}', 'values': [['FEHLER_CALL']]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', 'values': [[f"Systemfehler: {str(e_plausi_run_batch)[:100]}"]]}) - else: # Fehler bei Konsolidierung - self.logger.warning(f"Zeile {row_num_sheet}: Überspringe Plausi-Checks wegen Fehler bei Konsolidierung.") - for key_flag in ["Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio", "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki"]: - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP[key_flag] + 1)}{row_num_sheet}', 'values': [['INPUT_FEHLER_KONSO']]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', 'values': [["Konsolidierung fehlgeschlagen"]]}) + self.logger.error( + f"Fehler im Plausi-Check Aufruf (Batch-Modus) für Zeile {row_num_sheet}: {e_plausi_run_batch}") + for key_flag in [ + "Plausibilität Umsatz", + "Plausibilität Mitarbeiter", + "Plausibilität Umsatz/MA Ratio", + "Abweichung Umsatz CRM/Wiki", + "Abweichung MA CRM/Wiki"]: + current_row_updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP[key_flag] + 1)}{row_num_sheet}', + 'values': [ + ['FEHLER_CALL']]}) + current_row_updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', + 'values': [ + [f"Systemfehler: {str(e_plausi_run_batch)[:100]}"]]}) + else: # Fehler bei Konsolidierung + self.logger.warning( + f"Zeile {row_num_sheet}: Überspringe Plausi-Checks wegen Fehler bei Konsolidierung.") + for key_flag in [ + "Plausibilität Umsatz", + "Plausibilität Mitarbeiter", + "Plausibilität Umsatz/MA Ratio", + "Abweichung Umsatz CRM/Wiki", + "Abweichung MA CRM/Wiki"]: + current_row_updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP[key_flag] + 1)}{row_num_sheet}', + 'values': [ + ['INPUT_FEHLER_KONSO']]}) + current_row_updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', + 'values': [ + ["Konsolidierung fehlgeschlagen"]]}) + + current_row_updates.append( + { + 'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Prüfdatum"] + 1)}{row_num_sheet}', + 'values': [ + [now_timestamp_str]]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Prüfdatum"] + 1)}{row_num_sheet}', 'values': [[now_timestamp_str]]}) - all_sheet_updates.extend(current_row_updates) processed_rows_count += 1 # Batch-Update auslösen, wenn Limit erreicht if processed_rows_count % update_batch_limit_config == 0 and processed_rows_count > 0: if all_sheet_updates: - self.logger.info(f"Plausi-Batch: Sende {len(all_sheet_updates)} Operationen für {update_batch_limit_config} Zeilen...") - self.sheet_handler.batch_update_cells(all_sheet_updates) - all_sheet_updates = [] # Liste leeren nach dem Senden - time.sleep(0.5) # Kurze Pause nach Batch-Update - + self.logger.info( + f"Plausi-Batch: Sende {len(all_sheet_updates)} Operationen für {update_batch_limit_config} Zeilen...") + self.sheet_handler.batch_update_cells( + all_sheet_updates) + all_sheet_updates = [] # Liste leeren nach dem Senden + time.sleep(0.5) # Kurze Pause nach Batch-Update + # Ende der for-Schleife über die Zeilen # Sende verbleibende Updates if all_sheet_updates: - self.logger.info(f"Plausi-Batch: Sende verbleibende {len(all_sheet_updates)} Operationen...") + self.logger.info( + f"Plausi-Batch: Sende verbleibende {len(all_sheet_updates)} Operationen...") self.sheet_handler.batch_update_cells(all_sheet_updates) - self.logger.info(f"Modus 'plausi_check_data' abgeschlossen. {processed_rows_count} Zeilen verarbeitet.") + self.logger.info( + f"Modus 'plausi_check_data' abgeschlossen. {processed_rows_count} Zeilen verarbeitet.") def _suggest_parent_account_openai_task(self, task_data, openai_semaphore): """ @@ -3556,8 +5040,8 @@ class DataProcessor: Läuft in einem separaten Thread für den Parent-Suggestion-Batch. Args: - task_data (dict): Enthält die Daten für die Zeile - (row_num, crm_name, crm_website, crm_beschreibung, + task_data (dict): Enthält die Daten für die Zeile + (row_num, crm_name, crm_website, crm_beschreibung, wiki_url, wiki_absatz, wiki_kategorien, website_zusammenfassung). openai_semaphore (threading.Semaphore): Semaphore zur Begrenzung gleichzeitiger OpenAI-Calls. @@ -3569,15 +5053,19 @@ class DataProcessor: suggested_parent = "k.A." justification = "Keine Begründung erhalten." error_msg = None - + # Bereinige Input-Daten für den Prompt crm_name = str(task_data.get('crm_name', 'N/A')).strip() crm_website = str(task_data.get('crm_website', 'N/A')).strip() - crm_beschreibung = str(task_data.get('crm_beschreibung', 'N/A')).strip()[:800] # Gekürzt + crm_beschreibung = str(task_data.get( + 'crm_beschreibung', 'N/A')).strip()[:800] # Gekürzt wiki_url = str(task_data.get('wiki_url', 'N/A')).strip() - wiki_absatz = str(task_data.get('wiki_absatz', 'N/A')).strip()[:800] # Gekürzt - wiki_kategorien = str(task_data.get('wiki_kategorien', 'N/A')).strip()[:500] # Gekürzt - website_zusammenfassung = str(task_data.get('website_zusammenfassung', 'N/A')).strip()[:800] # Gekürzt + wiki_absatz = str(task_data.get('wiki_absatz', 'N/A') + ).strip()[:800] # Gekürzt + wiki_kategorien = str(task_data.get( + 'wiki_kategorien', 'N/A')).strip()[:500] # Gekürzt + website_zusammenfassung = str(task_data.get( + 'website_zusammenfassung', 'N/A')).strip()[:800] # Gekürzt prompt_parts = [ "Du bist ein Wirtschaftsanalyst und recherchierst Unternehmensstrukturen.", @@ -3599,10 +5087,13 @@ class DataProcessor: prompt_parts.append(f"Wikipedia Absatz: {wiki_absatz}") if wiki_kategorien and wiki_kategorien.lower() != "n/a": prompt_parts.append(f"Wikipedia Kategorien: {wiki_kategorien}") - if website_zusammenfassung and website_zusammenfassung.lower() != "n/a" and not website_zusammenfassung.startswith("k.A. (Fehler"): - prompt_parts.append(f"Website Zusammenfassung: {website_zusammenfassung}") - - prompt_parts.append("\nBitte gib NUR die Antwort im oben genannten Format.") + if website_zusammenfassung and website_zusammenfassung.lower( + ) != "n/a" and not website_zusammenfassung.startswith("k.A. (Fehler"): + prompt_parts.append( + f"Website Zusammenfassung: {website_zusammenfassung}") + + prompt_parts.append( + "\nBitte gib NUR die Antwort im oben genannten Format.") prompt = "\n".join(prompt_parts) # Token Count (optional, zur Info) @@ -3614,25 +5105,33 @@ class DataProcessor: try: with openai_semaphore: # call_openai_chat ist mit @retry_on_failure dekoriert - raw_chat_response = call_openai_chat(prompt, temperature=0.1, model=getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')) + raw_chat_response = call_openai_chat( + prompt, temperature=0.1, model=getattr( + Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')) if raw_chat_response: parsed_parent = "k.A." parsed_justification = "Keine Begründung extrahiert." - - parent_match = re.search(r"Vorgeschlagener Parent Account:\s*(.*)", raw_chat_response, re.IGNORECASE) + + parent_match = re.search( + r"Vorgeschlagener Parent Account:\s*(.*)", + raw_chat_response, + re.IGNORECASE) if parent_match: parsed_parent = parent_match.group(1).strip() - if not parsed_parent or parsed_parent.lower() == "k.a.": # Sicherstellen, dass "k.A." korrekt übernommen wird + if not parsed_parent or parsed_parent.lower( + ) == "k.a.": # Sicherstellen, dass "k.A." korrekt übernommen wird parsed_parent = "k.A." - justification_match = re.search(r"Begründung:\s*(.*)", raw_chat_response, re.IGNORECASE) + justification_match = re.search( + r"Begründung:\s*(.*)", raw_chat_response, re.IGNORECASE) if justification_match: parsed_justification = justification_match.group(1).strip() - + suggested_parent = parsed_parent justification = parsed_justification - logger.debug(f"Zeile {row_num}: ChatGPT Parent Vorschlag='{suggested_parent}', Begründung='{justification[:100]}...'") + logger.debug( + f"Zeile {row_num}: ChatGPT Parent Vorschlag='{suggested_parent}', Begründung='{justification[:100]}...'") else: error_msg = "Leere Antwort von OpenAI erhalten." logger.warning(f"Zeile {row_num}: {error_msg}") @@ -3642,14 +5141,24 @@ class DataProcessor: error_msg = f"Fehler bei OpenAI Call für Parent Suggestion (Zeile {row_num}): {type(e).__name__} - {str(e)[:100]}" logger.error(error_msg) justification = error_msg - - return {"row_num": row_num, "suggested_parent": suggested_parent, "justification": justification, "error": error_msg} - def process_parent_suggestion_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None, re_evaluate_question_mark=False): + return { + "row_num": row_num, + "suggested_parent": suggested_parent, + "justification": justification, + "error": error_msg} + + def process_parent_suggestion_batch( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None, + re_evaluate_question_mark=False): """ Batch-Prozess zur Generierung von Parent-Account-Vorschlägen. """ - self.logger.info(f"Starte Parent Account Suggestion Batch. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") + self.logger.info( + f"Starte Parent Account Suggestion Batch. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}") """ Batch-Prozess zur Generierung von Parent-Account-Vorschlägen mittels ChatGPT. Schreibt Ergebnisse in Spalten O, P, Q. @@ -3664,53 +5173,78 @@ class DataProcessor: in Spalte P (Parent Vorschlag Status) erneut bewertet, AUCH WENN Spalte Q bereits einen Timestamp hat. """ - self.logger.info(f"Starte Parent Account Suggestion Batch. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}, Re-Eval ?: {re_evaluate_question_mark}") + self.logger.info( + f"Starte Parent Account Suggestion Batch. Bereich: {start_sheet_row if start_sheet_row else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit else 'Unbegrenzt'}, Re-Eval ?: {re_evaluate_question_mark}") # --- Daten laden und Startzeile ermitteln --- col_o_key = "System Vorschlag Parent Account" col_p_key = "Parent Vorschlag Status" - col_q_key = "Parent Vorschlag Timestamp" # Timestamp-Spalte für die Auswahl + col_q_key = "Parent Vorschlag Timestamp" # Timestamp-Spalte für die Auswahl if start_sheet_row is None: - self.logger.info(f"Automatische Ermittlung der Startzeile basierend auf leerem '{col_q_key}'...") # Geändert: Start basierend auf leerem Q - start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key=col_q_key, min_sheet_row=7) + # Geändert: Start basierend auf leerem Q + self.logger.info( + f"Automatische Ermittlung der Startzeile basierend auf leerem '{col_q_key}'...") + start_data_index_no_header = self.sheet_handler.get_start_row_index( + check_column_key=col_q_key, min_sheet_row=7) if start_data_index_no_header == -1: - self.logger.error("FEHLER bei autom. Startzeilenermittlung. Breche ab.") + self.logger.error( + "FEHLER bei autom. Startzeilenermittlung. Breche ab.") return - start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 - self.logger.info(f"Automatisch ermittelte Startzeile: {start_sheet_row}") + start_sheet_row = start_data_index_no_header + \ + self.sheet_handler._header_rows + 1 + self.logger.info( + f"Automatisch ermittelte Startzeile: {start_sheet_row}") else: if not self.sheet_handler.load_data(): - self.logger.error("FEHLER beim Laden der Daten für Parent Suggestion Batch.") + self.logger.error( + "FEHLER beim Laden der Daten für Parent Suggestion Batch.") return all_data = self.sheet_handler.get_all_data_with_headers() header_rows = self.sheet_handler._header_rows total_sheet_rows = len(all_data) - if end_sheet_row is None: end_sheet_row = total_sheet_rows - self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}.") + if end_sheet_row is None: + end_sheet_row = total_sheet_rows + self.logger.info( + f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}.") if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Start liegt nach Ende oder außerhalb des Sheets. Keine Verarbeitung.") + self.logger.info( + "Start liegt nach Ende oder außerhalb des Sheets. Keine Verarbeitung.") return # --- Indizes --- required_keys = [ - col_o_key, col_p_key, col_q_key, "CRM Name", "CRM Website", "CRM Beschreibung", - "Wiki URL", "Wiki Absatz", "Wiki Kategorien", "Website Zusammenfassung", "Version" - ] + col_o_key, + col_p_key, + col_q_key, + "CRM Name", + "CRM Website", + "CRM Beschreibung", + "Wiki URL", + "Wiki Absatz", + "Wiki Kategorien", + "Website Zusammenfassung", + "Version"] if not all(key in COLUMN_MAP for key in required_keys): missing = [k for k in required_keys if k not in COLUMN_MAP] - self.logger.critical(f"FEHLER: Spaltenschlüssel für Parent Suggestion Batch fehlen: {missing}. Abbruch.") + self.logger.critical( + f"FEHLER: Spaltenschlüssel für Parent Suggestion Batch fehlen: {missing}. Abbruch.") return - - col_o_letter = self.sheet_handler._get_col_letter(COLUMN_MAP[col_o_key] + 1) - col_p_letter = self.sheet_handler._get_col_letter(COLUMN_MAP[col_p_key] + 1) - col_q_letter = self.sheet_handler._get_col_letter(COLUMN_MAP[col_q_key] + 1) - openai_sem = threading.Semaphore(getattr(Config, 'OPENAI_CONCURRENCY_LIMIT', 3)) + col_o_letter = self.sheet_handler._get_col_letter( + COLUMN_MAP[col_o_key] + 1) + col_p_letter = self.sheet_handler._get_col_letter( + COLUMN_MAP[col_p_key] + 1) + col_q_letter = self.sheet_handler._get_col_letter( + COLUMN_MAP[col_q_key] + 1) + + openai_sem = threading.Semaphore( + getattr(Config, 'OPENAI_CONCURRENCY_LIMIT', 3)) max_workers = getattr(Config, 'MAX_BRANCH_WORKERS', 10) - processing_batch_size = getattr(Config, 'PROCESSING_BRANCH_BATCH_SIZE', 10) + processing_batch_size = getattr( + Config, 'PROCESSING_BRANCH_BATCH_SIZE', 10) update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) tasks_for_current_openai_batch = [] @@ -3718,7 +5252,8 @@ class DataProcessor: processed_count = 0 skipped_count = 0 - # Funktion zum Verarbeiten und Schreiben eines Batches (bleibt intern gleich) + # Funktion zum Verarbeiten und Schreiben eines Batches (bleibt intern + # gleich) def _execute_and_write_openai_batch(current_tasks): # ... (Code der inneren Funktion bleibt identisch wie im vorherigen Vorschlag) ... nonlocal processed_count @@ -3727,18 +5262,25 @@ class DataProcessor: batch_start_log_row = current_tasks[0]['row_num'] batch_end_log_row = current_tasks[-1]['row_num'] - self.logger.debug(f"\n--- Starte Parent Suggestion OpenAI Batch ({len(current_tasks)} Tasks, Zeilen {batch_start_log_row}-{batch_end_log_row}) ---") + self.logger.debug( + f"\n--- Starte Parent Suggestion OpenAI Batch ({len(current_tasks)} Tasks, Zeilen {batch_start_log_row}-{batch_end_log_row}) ---") batch_results_list = [] with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: - future_to_task_map = {executor.submit(self._suggest_parent_account_openai_task, task, openai_sem): task for task in current_tasks} - for future in concurrent.futures.as_completed(future_to_task_map): + future_to_task_map = { + executor.submit( + self._suggest_parent_account_openai_task, + task, + openai_sem): task for task in current_tasks} + for future in concurrent.futures.as_completed( + future_to_task_map): task_info_orig = future_to_task_map[future] try: result_data = future.result() batch_results_list.append(result_data) except Exception as e_future: - self.logger.error(f"Exception im Future für Parent Suggestion Zeile {task_info_orig['row_num']}: {e_future}") + self.logger.error( + f"Exception im Future für Parent Suggestion Zeile {task_info_orig['row_num']}: {e_future}") batch_results_list.append({ "row_num": task_info_orig['row_num'], "suggested_parent": "FEHLER_TASK", @@ -3746,7 +5288,8 @@ class DataProcessor: "error": str(e_future) }) - self.logger.debug(f" OpenAI Batch ({batch_start_log_row}-{batch_end_log_row}) abgeschlossen. {len(batch_results_list)} Ergebnisse erhalten.") + self.logger.debug( + f" OpenAI Batch ({batch_start_log_row}-{batch_end_log_row}) abgeschlossen. {len(batch_results_list)} Ergebnisse erhalten.") if batch_results_list: now_ts_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -3754,38 +5297,49 @@ class DataProcessor: for res_item in batch_results_list: rn = res_item['row_num'] - parent_val = res_item.get('suggested_parent', 'k.A. (Fehler)') - - updates_for_this_batch.append({'range': f'{col_o_letter}{rn}', 'values': [[parent_val]]}) - status_val = "?" if parent_val and parent_val.lower() != "k.a." and not parent_val.startswith("FEHLER") else "" - updates_for_this_batch.append({'range': f'{col_p_letter}{rn}', 'values': [[status_val]]}) - updates_for_this_batch.append({'range': f'{col_q_letter}{rn}', 'values': [[now_ts_str]]}) - + parent_val = res_item.get( + 'suggested_parent', 'k.A. (Fehler)') + + updates_for_this_batch.append( + {'range': f'{col_o_letter}{rn}', 'values': [[parent_val]]}) + status_val = "?" if parent_val and parent_val.lower( + ) != "k.a." and not parent_val.startswith("FEHLER") else "" + updates_for_this_batch.append( + {'range': f'{col_p_letter}{rn}', 'values': [[status_val]]}) + updates_for_this_batch.append( + {'range': f'{col_q_letter}{rn}', 'values': [[now_ts_str]]}) + if res_item.get('justification'): - self.logger.debug(f"Zeile {rn} - Parent Begründung: {res_item.get('justification')[:200]}...") + self.logger.debug( + f"Zeile {rn} - Parent Begründung: {res_item.get('justification')[:200]}...") all_sheet_updates.extend(updates_for_this_batch) - - processed_count += len(current_tasks) # Zähle hier, wenn Tasks tatsächlich an OpenAI gingen + + # Zähle hier, wenn Tasks tatsächlich an OpenAI gingen + processed_count += len(current_tasks) if len(all_sheet_updates) >= update_batch_row_limit * 3: - self.logger.info(f"Sende Batch-Updates für Parent Suggestions ({len(all_sheet_updates)//3} Zeilen)...") + self.logger.info( + f"Sende Batch-Updates für Parent Suggestions ({len(all_sheet_updates)//3} Zeilen)...") self.sheet_handler.batch_update_cells(all_sheet_updates) all_sheet_updates.clear() - + time.sleep(getattr(Config, 'RETRY_DELAY', 5) * 0.5) # Ende der Hilfsfunktion _execute_and_write_openai_batch # Hauptschleife über die Zeilen for i in range(start_sheet_row, end_sheet_row + 1): # Limit-Prüfung erfolgt jetzt innerhalb der _execute_and_write_openai_batch - # oder besser hier vor dem Sammeln von Tasks, um nicht unnötig zu iterieren. + # oder besser hier vor dem Sammeln von Tasks, um nicht unnötig zu + # iterieren. if limit is not None and processed_count >= limit: - self.logger.info(f"Verarbeitungslimit ({limit}) für Parent Suggestions erreicht.") + self.logger.info( + f"Verarbeitungslimit ({limit}) für Parent Suggestions erreicht.") break - + row_idx_list = i - 1 - if row_idx_list >= total_sheet_rows: break + if row_idx_list >= total_sheet_rows: + break row = all_data[row_idx_list] if not any(cell and str(cell).strip() for cell in row): @@ -3793,16 +5347,20 @@ class DataProcessor: continue # Kriterien für Verarbeitung - val_q_timestamp = self._get_cell_value_safe(row, col_q_key).strip() # Timestamp aus Spalte Q - val_p_status = self._get_cell_value_safe(row, col_p_key).strip() # Status aus Spalte P + val_q_timestamp = self._get_cell_value_safe( + row, col_q_key).strip() # Timestamp aus Spalte Q + val_p_status = self._get_cell_value_safe( + row, col_p_key).strip() # Status aus Spalte P needs_processing = False - if not val_q_timestamp: # Spalte Q (Timestamp) ist leer + if not val_q_timestamp: # Spalte Q (Timestamp) ist leer needs_processing = True - elif re_evaluate_question_mark and val_p_status == "?": # Neubewertung für Status "?" auch wenn Timestamp Q gesetzt ist + # Neubewertung für Status "?" auch wenn Timestamp Q gesetzt ist + elif re_evaluate_question_mark and val_p_status == "?": needs_processing = True - self.logger.debug(f"Zeile {i}: Wird trotz vorhandenem Timestamp in Q ('{val_q_timestamp}') verarbeitet, da P='?' und re_evaluate_question_mark=True.") - + self.logger.debug( + f"Zeile {i}: Wird trotz vorhandenem Timestamp in Q ('{val_q_timestamp}') verarbeitet, da P='?' und re_evaluate_question_mark=True.") + if not needs_processing: skipped_count += 1 continue @@ -3823,97 +5381,126 @@ class DataProcessor: if len(tasks_for_current_openai_batch) >= processing_batch_size: _execute_and_write_openai_batch(tasks_for_current_openai_batch) tasks_for_current_openai_batch.clear() - + if tasks_for_current_openai_batch: _execute_and_write_openai_batch(tasks_for_current_openai_batch) - + if all_sheet_updates: - self.logger.info(f"Sende finale Batch-Updates für Parent Suggestions ({len(all_sheet_updates)//3} Zeilen)...") + self.logger.info( + f"Sende finale Batch-Updates für Parent Suggestions ({len(all_sheet_updates)//3} Zeilen)...") self.sheet_handler.batch_update_cells(all_sheet_updates) - self.logger.info(f"Parent Account Suggestion Batch abgeschlossen. {processed_count} Zeilen verarbeitet, {skipped_count} Zeilen übersprungen.") + self.logger.info( + f"Parent Account Suggestion Batch abgeschlossen. {processed_count} Zeilen verarbeitet, {skipped_count} Zeilen übersprungen.") def _predict_technician_bucket(self, row_data): """ Führt eine Vorhersage des Servicetechniker-Buckets für eine einzelne Zeile durch. """ company_name = self._get_cell_value_safe(row_data, 'CRM Name').strip() - self.logger.debug(f"Versuche ML-Schaetzung fuer Zeile ({company_name[:50]}...)") + self.logger.debug( + f"Versuche ML-Schaetzung fuer Zeile ({company_name[:50]}...)") if self.model is None or self.imputer is None or self._expected_features is None: - self.logger.info("Lade ML-Modell, Imputer und Feature-Spalten...") - self._load_ml_model(MODEL_FILE, IMPUTER_FILE) - if self.model is None or self.imputer is None or self._expected_features is None: - self.logger.error("Laden von Modell, Imputer oder Feature-Spalten fehlgeschlagen.") - return "FEHLER Schaetzung (Modell-Laden)" + self.logger.info("Lade ML-Modell, Imputer und Feature-Spalten...") + self._load_ml_model(MODEL_FILE, IMPUTER_FILE) + if self.model is None or self.imputer is None or self._expected_features is None: + self.logger.error( + "Laden von Modell, Imputer oder Feature-Spalten fehlgeschlagen.") + return "FEHLER Schaetzung (Modell-Laden)" try: # === Feature Erstellung (muss exakt zum Training passen!) === - + # 1. Konsolidierte numerische Werte holen - final_umsatz_val_str = self._get_cell_value_safe(row_data, "Finaler Umsatz (Wiki>CRM)") - final_ma_val_str = self._get_cell_value_safe(row_data, "Finaler Mitarbeiter (Wiki>CRM)") - - umsatz_for_pred = get_numeric_filter_value(final_umsatz_val_str, is_umsatz=True) - ma_for_pred = get_numeric_filter_value(final_ma_val_str, is_umsatz=False) - + final_umsatz_val_str = self._get_cell_value_safe( + row_data, "Finaler Umsatz (Wiki>CRM)") + final_ma_val_str = self._get_cell_value_safe( + row_data, "Finaler Mitarbeiter (Wiki>CRM)") + + umsatz_for_pred = get_numeric_filter_value( + final_umsatz_val_str, is_umsatz=True) + ma_for_pred = get_numeric_filter_value( + final_ma_val_str, is_umsatz=False) + umsatz_for_pred = np.nan if umsatz_for_pred == 0 else umsatz_for_pred ma_for_pred = np.nan if ma_for_pred == 0 else ma_for_pred # 2. 'is_part_of_group' Feature erstellen - parent_d_val = self._get_cell_value_safe(row_data, "Parent Account Name").strip().lower() - parent_o_val = self._get_cell_value_safe(row_data, "System Vorschlag Parent Account").strip().lower() - parent_p_val = self._get_cell_value_safe(row_data, "Parent Vorschlag Status").strip().lower() + parent_d_val = self._get_cell_value_safe( + row_data, "Parent Account Name").strip().lower() + parent_o_val = self._get_cell_value_safe( + row_data, "System Vorschlag Parent Account").strip().lower() + parent_p_val = self._get_cell_value_safe( + row_data, "Parent Vorschlag Status").strip().lower() cond1_pred = bool(parent_d_val and parent_d_val != 'k.a.') - cond2_pred = bool(parent_o_val and parent_o_val != 'k.a.' and parent_p_val == 'x') + cond2_pred = bool(parent_o_val and parent_o_val != + 'k.a.' and parent_p_val == 'x') is_group_val = 1 if cond1_pred or cond2_pred else 0 # 3. Zusätzliche Features (Ratio, Log) erstellen # Log-Transformationen - log_umsatz_val = np.log1p(umsatz_for_pred) if pd.notna(umsatz_for_pred) else np.nan - log_ma_val = np.log1p(ma_for_pred) if pd.notna(ma_for_pred) else np.nan - + log_umsatz_val = np.log1p(umsatz_for_pred) if pd.notna( + umsatz_for_pred) else np.nan + log_ma_val = np.log1p(ma_for_pred) if pd.notna( + ma_for_pred) else np.nan + # Umsatz pro MA umsatz_pro_ma_val = np.nan - if pd.notna(umsatz_for_pred) and pd.notna(ma_for_pred) and ma_for_pred > 0: + if pd.notna(umsatz_for_pred) and pd.notna( + ma_for_pred) and ma_for_pred > 0: umsatz_pro_ma_val = umsatz_for_pred / ma_for_pred - + # 4. Branchen-Feature holen - # Wichtig: Hier die gleiche Branchenspalte wie im Training verwenden! - branche_val_str = self._get_cell_value_safe(row_data, "CRM Branche") - - # DataFrame mit einer Zeile und den internen Namen (wie in prepare_data_for_modeling) erstellen + # Wichtig: Hier die gleiche Branchenspalte wie im Training + # verwenden! + branche_val_str = self._get_cell_value_safe( + row_data, "CRM Branche") + + # DataFrame mit einer Zeile und den internen Namen (wie in + # prepare_data_for_modeling) erstellen single_row_dict = { 'Log_Finaler_Umsatz_ML': [log_umsatz_val], 'Log_Finaler_Mitarbeiter_ML': [log_ma_val], 'Umsatz_pro_MA_ML': [umsatz_pro_ma_val], 'is_part_of_group': [is_group_val], - 'branche_crm': [str(branche_val_str).strip() if branche_val_str else 'Unbekannt'] - } + 'branche_crm': [ + str(branche_val_str).strip() if branche_val_str else 'Unbekannt']} df_single_row = pd.DataFrame.from_dict(single_row_dict) - + # One-Hot Encoding - df_encoded = pd.get_dummies(df_single_row, columns=['branche_crm'], prefix='Branche', dummy_na=False) - + df_encoded = pd.get_dummies( + df_single_row, + columns=['branche_crm'], + prefix='Branche', + dummy_na=False) + # Angleichung an die im Training verwendeten Features - # Erstelle einen DataFrame mit einer Zeile und den erwarteten Spalten - data_for_df_processed = {col: [0] for col in self._expected_features} + # Erstelle einen DataFrame mit einer Zeile und den erwarteten + # Spalten + data_for_df_processed = {col: [0] + for col in self._expected_features} for col in self._expected_features: if col in df_encoded.columns: data_for_df_processed[col] = [df_encoded[col].iloc[0]] - - df_processed = pd.DataFrame(data_for_df_processed, columns=self._expected_features) - + + df_processed = pd.DataFrame( + data_for_df_processed, + columns=self._expected_features) + # Imputation und Vorhersage df_imputed_array = self.imputer.transform(df_processed) - - prediction_proba = self.model.predict_proba(df_imputed_array) - predicted_bucket_label = self.model.classes_[np.argmax(prediction_proba[0])] - self.logger.debug(f" -> ML Vorhersage Ergebnis: '{predicted_bucket_label}'") + prediction_proba = self.model.predict_proba(df_imputed_array) + predicted_bucket_label = self.model.classes_[ + np.argmax(prediction_proba[0])] + + self.logger.debug( + f" -> ML Vorhersage Ergebnis: '{predicted_bucket_label}'") return predicted_bucket_label except Exception as e_predict: - self.logger.exception(f"FEHLER bei der ML-Vorhersage für Zeile ({company_name[:50]}...): {e_predict}") + self.logger.exception( + f"FEHLER bei der ML-Vorhersage für Zeile ({company_name[:50]}...): {e_predict}") return f"FEHLER Schaetzung: {str(e_predict)[:100]}..." def _load_ml_model(self, model_path, imputer_path): @@ -3922,14 +5509,18 @@ class DataProcessor: """ self.model, self.imputer, self._expected_features = None, None, None try: - if not os.path.exists(model_path) or not os.path.exists(imputer_path): + if not os.path.exists( + model_path) or not os.path.exists(imputer_path): self.logger.error("Modell- oder Imputer-Datei nicht gefunden.") return - with open(model_path, 'rb') as f: self.model = pickle.load(f) - with open(imputer_path, 'rb') as f: self.imputer = pickle.load(f) + with open(model_path, 'rb') as f: + self.model = pickle.load(f) + with open(imputer_path, 'rb') as f: + self.imputer = pickle.load(f) if os.path.exists(PATTERNS_FILE_JSON): with open(PATTERNS_FILE_JSON, 'r', encoding='utf-8') as f: - self._expected_features = json.load(f).get("feature_columns") + self._expected_features = json.load( + f).get("feature_columns") if self._expected_features: self.logger.info("ML-Modell, Imputer und Features geladen.") else: @@ -3956,29 +5547,36 @@ class DataProcessor: oder None bei Fehlern oder wenn keine gueltigen Trainingsdaten gefunden wurden. """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - self.logger.info("Starte Datenvorbereitung fuer Modellierung (Training)...") # <<< GEÄNDERT + # <<< GEÄNDERT + self.logger.info( + "Starte Datenvorbereitung fuer Modellierung (Training)...") # Nutzt den self.sheet_handler der Klasse (Block 15). # Pruefen Sie, ob der Sheet Handler initialisiert wurde und Daten hat. if not self.sheet_handler or not self.sheet_handler.sheet_values: - self.logger.error("Fehler: Sheet Handler nicht initialisiert oder keine Daten geladen fuer prepare_data_for_modeling.") # <<< GEÄNDERT - # Versuchen Sie die Daten einmalig innerhalb dieser Methode zu laden, falls sie fehlen. - # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). - if not self.sheet_handler.load_data(): - self.logger.critical("Konnte Daten auch nach erneutem Versuch nicht laden. Abbruch der Datenvorbereitung.") # <<< GEÄNDERT - return None # Gebe None zurueck, wenn Laden fehlschlaegt + self.logger.error( + "Fehler: Sheet Handler nicht initialisiert oder keine Daten geladen fuer prepare_data_for_modeling.") # <<< GEÄNDERT + # Versuchen Sie die Daten einmalig innerhalb dieser Methode zu laden, falls sie fehlen. + # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block + # 2). + if not self.sheet_handler.load_data(): + self.logger.critical( + "Konnte Daten auch nach erneutem Versuch nicht laden. Abbruch der Datenvorbereitung.") # <<< GEÄNDERT + return None # Gebe None zurueck, wenn Laden fehlschlaegt - - # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. + # Holen Sie die gesamte Datenliste (inklusive Header) aus dem + # SheetHandler. all_data = self.sheet_handler.get_all_data_with_headers() - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). + # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar + # (Block 14). header_rows = self.sheet_handler._header_rows - # Pruefe auf ausreichende Zeilenzahl (Header + mindestens eine Datenzeile) + # Pruefe auf ausreichende Zeilenzahl (Header + mindestens eine + # Datenzeile) min_required_rows = header_rows + 1 # Wenn nicht genuegend Zeilen da sind if not all_data or len(all_data) < min_required_rows: - self.logger.error(f"Fehler: Nicht genuegend Datenzeilen ({len(all_data)}) im Sheet gefunden fuer Modellierung (mindestens {min_required_rows} benoetigt).") # <<< GEÄNDERT - return None # Gebe None zurueck, wenn nicht genuegend Daten da sind - + self.logger.error( + f"Fehler: Nicht genuegend Datenzeilen ({len(all_data)}) im Sheet gefunden fuer Modellierung (mindestens {min_required_rows} benoetigt).") # <<< GEÄNDERT + return None # Gebe None zurueck, wenn nicht genuegend Daten da sind # --- Header pruefen und DataFrame erstellen --- try: @@ -3987,54 +5585,75 @@ class DataProcessor: # Stellen Sie sicher, dass die Header-Zeile auch die erwartete Mindestlaenge hat, # um die Spaltenindizes aus COLUMN_MAP (Block 1) zu finden. try: - max_col_idx_in_map = max(COLUMN_MAP.values()) # Finde den hoechsten Index in COLUMN_MAP - # Pruefen Sie, ob die Anzahl der geladenen Spalten im Header ausreicht - if len(headers) <= max_col_idx_in_map: - # Logge einen kritischen Fehler, wenn das Mapping auf Spalten zeigt, die nicht im Sheet existieren - self.logger.critical(f"FEHLER: Header-Zeile ({len(headers)} Spalten) ist kuerzer als der hoechste Index in COLUMN_MAP ({max_col_idx_in_map}). COLUMN_MAP passt nicht zum Sheet.") # <<< GEÄNDERT - return None # Beende die Methode - except ValueError: # Tritt auf, wenn COLUMN_MAP leer ist - self.logger.critical("FEHLER: COLUMN_MAP scheint leer zu sein oder enthaelt keine Werte. Kann Max Index nicht ermitteln.") # <<< GEÄNDERT - return None # Beende die Methode + # Finde den hoechsten Index in COLUMN_MAP + max_col_idx_in_map = max(COLUMN_MAP.values()) + # Pruefen Sie, ob die Anzahl der geladenen Spalten im Header + # ausreicht + if len(headers) <= max_col_idx_in_map: + # Logge einen kritischen Fehler, wenn das Mapping auf + # Spalten zeigt, die nicht im Sheet existieren + self.logger.critical( + f"FEHLER: Header-Zeile ({len(headers)} Spalten) ist kuerzer als der hoechste Index in COLUMN_MAP ({max_col_idx_in_map}). COLUMN_MAP passt nicht zum Sheet.") # <<< GEÄNDERT + return None # Beende die Methode + except ValueError: # Tritt auf, wenn COLUMN_MAP leer ist + self.logger.critical( + "FEHLER: COLUMN_MAP scheint leer zu sein oder enthaelt keine Werte. Kann Max Index nicht ermitteln.") # <<< GEÄNDERT + return None # Beende die Methode except Exception as e: - # Fange andere unerwartete Fehler ab - self.logger.critical(f"FEHLER beim Pruefen der Spaltenlaenge der Header-Zeile: {e}") # <<< GEÄNDERT - return None # Beende die Methode + # Fange andere unerwartete Fehler ab + self.logger.critical( + f"FEHLER beim Pruefen der Spaltenlaenge der Header-Zeile: {e}") # <<< GEÄNDERT + return None # Beende die Methode except IndexError: # Wenn das Sheet leer ist oder keine erste Zeile hat - self.logger.critical("FEHLER: Sheet scheint leer zu sein oder hat keine erste Zeile, keine Header gefunden.") # <<< GEÄNDERT - return None # Beende die Methode + self.logger.critical( + "FEHLER: Sheet scheint leer zu sein oder hat keine erste Zeile, keine Header gefunden.") # <<< GEÄNDERT + return None # Beende die Methode except Exception as e: - # Fange andere unerwartete Fehler beim Zugriff auf Header ab - self.logger.critical(f"FEHLER beim Zugriff auf Header: {e}") # <<< GEÄNDERT - # Logge den Traceback - self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT - return None # Beende die Methode - + # Fange andere unerwartete Fehler beim Zugriff auf Header ab + self.logger.critical( + f"FEHLER beim Zugriff auf Header: {e}") # <<< GEÄNDERT + # Logge den Traceback + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT + return None # Beende die Methode # Datenzeilen sind alle Zeilen nach den Header-Zeilen - data_rows = all_data[header_rows:] # Annahme: Die ersten X Zeilen sind Header + # Annahme: Die ersten X Zeilen sind Header + data_rows = all_data[header_rows:] # Erstelle DataFrame aus den Datenzeilen und den Headern df = pd.DataFrame(data_rows, columns=headers) - self.logger.info(f"Initialen DataFrame fuer Modellierung erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.") # <<< GEÄNDERT - + self.logger.info( + f"Initialen DataFrame fuer Modellierung erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.") # <<< GEÄNDERT # DACH-Filter (basierend auf CRM Land - Spalte G) - crm_land_col_header = headers[COLUMN_MAP["CRM Land"]] # Holt den tatsächlichen Spaltennamen - # Erlaubte Werte für DACH-Länder (Groß- und Kleinschreibung wird durch .str.upper() behandelt) - dach_countries = ["DE", "CH", "AT", "DEUTSCHLAND", "ÖSTERREICH", "SCHWEIZ", "OESTERREICH"] # OESTERREICH hinzugefügt - + # Holt den tatsächlichen Spaltennamen + crm_land_col_header = headers[COLUMN_MAP["CRM Land"]] + # Erlaubte Werte für DACH-Länder (Groß- und Kleinschreibung wird durch + # .str.upper() behandelt) + dach_countries = [ + "DE", + "CH", + "AT", + "DEUTSCHLAND", + "ÖSTERREICH", + "SCHWEIZ", + "OESTERREICH"] # OESTERREICH hinzugefügt + # Sicherstellen, dass die Spalte existiert, bevor gefiltert wird if crm_land_col_header in df.columns: - df = df[df[crm_land_col_header].astype(str).str.upper().isin(dach_countries)].copy() # .copy() um Warnung zu vermeiden - self.logger.info(f"Nach DACH-Filter (basierend auf '{crm_land_col_header}'): {len(df)} Zeilen verbleiben.") + df = df[df[crm_land_col_header].astype(str).str.upper().isin( + dach_countries)].copy() # .copy() um Warnung zu vermeiden + self.logger.info( + f"Nach DACH-Filter (basierend auf '{crm_land_col_header}'): {len(df)} Zeilen verbleiben.") if df.empty: - self.logger.error("Keine DACH-Unternehmen im Datensatz nach Filterung.") + self.logger.error( + "Keine DACH-Unternehmen im Datensatz nach Filterung.") return None else: - self.logger.error(f"Spalte '{crm_land_col_header}' für DACH-Filter nicht im DataFrame gefunden.") + self.logger.error( + f"Spalte '{crm_land_col_header}' für DACH-Filter nicht im DataFrame gefunden.") return None # Plausibilitätsfilter (basierend auf Spalten BG und BH) @@ -4045,22 +5664,26 @@ class DataProcessor: if plausi_umsatz_col_header in df.columns and plausi_ma_col_header in df.columns: # Filtere Zeilen, bei denen Plausi-Umsatz oder Plausi-MA einen Fehler anzeigt # Hier gehen wir davon aus, dass Fehler mit "FEHLER_" beginnen. - df = df[~df[plausi_umsatz_col_header].astype(str).str.upper().str.startswith('FEHLER')].copy() - df = df[~df[plausi_ma_col_header].astype(str).str.upper().str.startswith('FEHLER')].copy() - self.logger.info(f"Nach Entfernung von FEHLER-Plausi-Fällen (BG, BH): {len(df)} Zeilen verbleiben.") + df = df[~df[plausi_umsatz_col_header].astype( + str).str.upper().str.startswith('FEHLER')].copy() + df = df[~df[plausi_ma_col_header].astype( + str).str.upper().str.startswith('FEHLER')].copy() + self.logger.info( + f"Nach Entfernung von FEHLER-Plausi-Fällen (BG, BH): {len(df)} Zeilen verbleiben.") if df.empty: self.logger.error("Keine Zeilen nach Plausi-Filterung übrig.") return None - # Hier könnten Sie noch spezifischere Filter für bestimmte WARNUNG-Typen einbauen, falls gewünscht + # Hier könnten Sie noch spezifischere Filter für bestimmte + # WARNUNG-Typen einbauen, falls gewünscht else: - self.logger.error(f"Plausibilitätsspalten '{plausi_umsatz_col_header}' oder '{plausi_ma_col_header}' nicht im DataFrame gefunden.") + self.logger.error( + f"Plausibilitätsspalten '{plausi_umsatz_col_header}' oder '{plausi_ma_col_header}' nicht im DataFrame gefunden.") return None - - # --- Spaltenauswahl und Umbenennung --- # Definiere die notwendigen Spalten anhand ihrer COLUMN_MAP Schluessel (Block 1) - # und weisen ihnen interne, einfachere Namen zu, die im DataFrame verwendet werden. + # und weisen ihnen interne, einfachere Namen zu, die im DataFrame + # verwendet werden. col_keys_mapping = { "name": "CRM Name", # Zur Identifikation, wird spaeter entfernt "branche_ki": "Chat Vorschlag Branche", # Fuer One-Hot Encoding @@ -4068,387 +5691,524 @@ class DataProcessor: "umsatz_wiki": "Wiki Umsatz", # Fuer Konsolidierung "ma_crm": "CRM Anzahl Mitarbeiter", # Fuer Konsolidierung "ma_wiki": "Wiki Mitarbeiter", # Fuer Konsolidierung - "techniker": "CRM Anzahl Techniker", # DIE ZIELVARIABLE (Bekannte Technikerzahl) - "parent_d_raw": "Parent Account Name", # Spalte D <- Dies ist Zeile 9012 + # DIE ZIELVARIABLE (Bekannte Technikerzahl) + "techniker": "CRM Anzahl Techniker", + # Spalte D <- Dies ist Zeile 9012 + "parent_d_raw": "Parent Account Name", "parent_o_raw": "System Vorschlag Parent Account", # Spalte O "parent_p_raw": "Parent Vorschlag Status" # Spalte P } - # Ueberpruefe, ob alle benoetigten Spalten-Schluessel in der COLUMN_MAP (Block 1) vorhanden sind - missing_keys_in_map = [key for key in col_keys_mapping.values() if key not in COLUMN_MAP] + # Ueberpruefe, ob alle benoetigten Spalten-Schluessel in der COLUMN_MAP + # (Block 1) vorhanden sind + missing_keys_in_map = [ + key for key in col_keys_mapping.values() if key not in COLUMN_MAP] if missing_keys_in_map: - self.logger.critical(f"FEHLER: Folgende benoetigte Spalten-Schluessel fehlen in COLUMN_MAP fuer prepare_data_for_modeling: {missing_keys_in_map}.") # <<< GEÄNDERT - return None # Beende die Methode + self.logger.critical( + f"FEHLER: Folgende benoetigte Spalten-Schluessel fehlen in COLUMN_MAP fuer prepare_data_for_modeling: {missing_keys_in_map}.") # <<< GEÄNDERT + return None # Beende die Methode # Erstelle das Mapping von tatsaechlichen Header-Namen zu internen Schluesseln. - # Verwende die Header-Namen aus dem geladenen Sheet und die COLUMN_MAP, um die richtigen Header zu finden. - header_to_internal_key = {} # Dict zum Umbenennen der Spalten - cols_to_select_by_header = [] # Liste der Header-Namen, die aus dem DF ausgewaehlt werden + # Verwende die Header-Namen aus dem geladenen Sheet und die COLUMN_MAP, + # um die richtigen Header zu finden. + header_to_internal_key = {} # Dict zum Umbenennen der Spalten + # Liste der Header-Namen, die aus dem DF ausgewaehlt werden + cols_to_select_by_header = [] try: - # Iteriere ueber das Mapping von internen zu COLUMN_MAP Schluesseln - for internal_key, column_map_key in col_keys_mapping.items(): - # Hole den tatsaechlichen Header-Namen aus dem Sheet - header_name_from_sheet = headers[COLUMN_MAP[column_map_key]] - # Fuege das Mapping hinzu - header_to_internal_key[header_name_from_sheet] = internal_key - # Fuege den Header-Namen zur Liste der auszuwaehlenden Spalten hinzu - cols_to_select_by_header.append(header_name_from_sheet) + # Iteriere ueber das Mapping von internen zu COLUMN_MAP Schluesseln + for internal_key, column_map_key in col_keys_mapping.items(): + # Hole den tatsaechlichen Header-Namen aus dem Sheet + header_name_from_sheet = headers[COLUMN_MAP[column_map_key]] + # Fuege das Mapping hinzu + header_to_internal_key[header_name_from_sheet] = internal_key + # Fuege den Header-Namen zur Liste der auszuwaehlenden Spalten + # hinzu + cols_to_select_by_header.append(header_name_from_sheet) - # Waehle nur die benoetigten Spalten im DataFrame aus - df_subset = df[cols_to_select_by_header].copy() # Kopie erstellen, um SettingWithCopyWarning zu vermeiden - # Benenne die Spalten um zu den internen Namen - df_subset.rename(columns=header_to_internal_key, inplace=True) + # Waehle nur die benoetigten Spalten im DataFrame aus + # Kopie erstellen, um SettingWithCopyWarning zu vermeiden + df_subset = df[cols_to_select_by_header].copy() + # Benenne die Spalten um zu den internen Namen + df_subset.rename(columns=header_to_internal_key, inplace=True) except KeyError as e: - # Dieser Fehler sollte eigentlich durch die obige Pruefung abgefangen werden, - # tritt aber auf, wenn ein erwarteter Header-Name nicht im geladenen DF ist (selten, wenn COLUMN_MAP korrekt ist). - self.logger.critical(f"FEHLER beim Auswaehlen/Umbenennen der Spalten (KeyError: '{e}'). Der Header wurde nicht im DataFrame gefunden.") # <<< GEÄNDERT - self.logger.debug(f"Erwartete Header: {cols_to_select_by_header}. Verfuegbare Header im DF: {list(df.columns)}") # <<< GEÄNDERT - return None # Beende die Methode + # Dieser Fehler sollte eigentlich durch die obige Pruefung abgefangen werden, + # tritt aber auf, wenn ein erwarteter Header-Name nicht im + # geladenen DF ist (selten, wenn COLUMN_MAP korrekt ist). + self.logger.critical( + f"FEHLER beim Auswaehlen/Umbenennen der Spalten (KeyError: '{e}'). Der Header wurde nicht im DataFrame gefunden.") # <<< GEÄNDERT + self.logger.debug( + f"Erwartete Header: {cols_to_select_by_header}. Verfuegbare Header im DF: {list(df.columns)}") # <<< GEÄNDERT + return None # Beende die Methode except IndexError as e: - # Tritt auf, wenn COLUMN_MAP einen Index > Anzahl Spalten im DF hat - self.logger.critical(f"FEHLER beim Auswaehlen/Umbenennen der Spalten (IndexError: '{e}'). COLUMN_MAP zeigt auf Spalten, die nicht im geladenen Sheet existieren.") # <<< GEÄNDERT - self.logger.debug(f"COLUMN_MAP: {COLUMN_MAP}. Sheet hat {len(headers)} Spalten.") # <<< GEÄNDERT - return None # Beende die Methode + # Tritt auf, wenn COLUMN_MAP einen Index > Anzahl Spalten im DF hat + self.logger.critical( + f"FEHLER beim Auswaehlen/Umbenennen der Spalten (IndexError: '{e}'). COLUMN_MAP zeigt auf Spalten, die nicht im geladenen Sheet existieren.") # <<< GEÄNDERT + self.logger.debug( + f"COLUMN_MAP: {COLUMN_MAP}. Sheet hat {len(headers)} Spalten.") # <<< GEÄNDERT + return None # Beende die Methode except Exception as e: - # Fange andere unerwartete Fehler ab - self.logger.critical(f"Unerwarteter FEHLER beim Auswaehlen/Umbenennen der Spalten: {e}") # <<< GEÄNDERT - # Logge den Traceback - self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT - return None # Beende die Methode - - - self.logger.info(f"Benötigte Spalten fuer Modellierung ausgewaehlt und umbenannt: {list(df_subset.columns)}") # <<< GEÄNDERT + # Fange andere unerwartete Fehler ab + self.logger.critical( + f"Unerwarteter FEHLER beim Auswaehlen/Umbenennen der Spalten: {e}") # <<< GEÄNDERT + # Logge den Traceback + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT + return None # Beende die Methode + self.logger.info( + f"Benötigte Spalten fuer Modellierung ausgewaehlt und umbenannt: {list(df_subset.columns)}") # <<< GEÄNDERT self.logger.info("Erstelle Feature 'is_part_of_group'...") - - # Zugreifen auf die Spalten im DataFrame df_subset - # Die Spaltennamen hier müssen den internen Namen entsprechen, - # die in col_keys_mapping definiert wurden (z.B. 'parent_d_raw'). - parent_d_series = df_subset['parent_d_raw'].astype(str).str.strip().str.lower() - parent_o_series = df_subset['parent_o_raw'].astype(str).str.strip().str.lower() - parent_p_series = df_subset['parent_p_raw'].astype(str).str.strip().str.lower() - cond1 = parent_d_series.notna() & (parent_d_series != 'k.a.') & (parent_d_series != '') - cond2_o = parent_o_series.notna() & (parent_o_series != 'k.a.') & (parent_o_series != '') + # Zugreifen auf die Spalten im DataFrame df_subset + # Die Spaltennamen hier müssen den internen Namen entsprechen, + # die in col_keys_mapping definiert wurden (z.B. 'parent_d_raw'). + parent_d_series = df_subset['parent_d_raw'].astype( + str).str.strip().str.lower() + parent_o_series = df_subset['parent_o_raw'].astype( + str).str.strip().str.lower() + parent_p_series = df_subset['parent_p_raw'].astype( + str).str.strip().str.lower() + + cond1 = parent_d_series.notna() & ( + parent_d_series != 'k.a.') & ( + parent_d_series != '') + cond2_o = parent_o_series.notna() & ( + parent_o_series != 'k.a.') & ( + parent_o_series != '') cond2_p = parent_p_series == 'x' cond2 = cond2_o & cond2_p - + # .loc verwenden, um die neue Spalte sicher zuzuweisen und SettingWithCopyWarning zu vermeiden df_subset.loc[:, 'is_part_of_group'] = np.where(cond1 | cond2, 1, 0) - - self.logger.info(f"Feature 'is_part_of_group' erstellt. {df_subset['is_part_of_group'].sum()} Unternehmen als Teil einer Gruppe markiert.") - self.logger.debug(f"Verteilung von 'is_part_of_group':\n{df_subset['is_part_of_group'].value_counts(normalize=True, dropna=False)}") - + self.logger.info( + f"Feature 'is_part_of_group' erstellt. {df_subset['is_part_of_group'].sum()} Unternehmen als Teil einer Gruppe markiert.") + self.logger.debug( + f"Verteilung von 'is_part_of_group':\n{df_subset['is_part_of_group'].value_counts(normalize=True, dropna=False)}") # --- Features konsolidieren (Umsatz, Mitarbeiter) --- - self.logger.debug("Konsolidiere Umsatz und Mitarbeiter für ML-Features...") - cols_to_process_ml = { + self.logger.debug( + "Konsolidiere Umsatz und Mitarbeiter für ML-Features...") + cols_to_process_ml = { 'Umsatz': ('umsatz_wiki', 'umsatz_crm', 'Finaler_Umsatz_ML'), 'Mitarbeiter': ('ma_wiki', 'ma_crm', 'Finaler_Mitarbeiter_ML') } - for base_name, (wiki_col_ml, crm_col_ml, final_col_ml) in cols_to_process_ml.items(): + for base_name, (wiki_col_ml, crm_col_ml, + final_col_ml) in cols_to_process_ml.items(): is_umsatz_flag = (base_name == 'Umsatz') - wiki_series = df_subset[wiki_col_ml].apply(lambda x: get_numeric_filter_value(x, is_umsatz=is_umsatz_flag)) - crm_series = df_subset[crm_col_ml].apply(lambda x: get_numeric_filter_value(x, is_umsatz=is_umsatz_flag)) - + wiki_series = df_subset[wiki_col_ml].apply( + lambda x: get_numeric_filter_value( + x, is_umsatz=is_umsatz_flag)) + crm_series = df_subset[crm_col_ml].apply( + lambda x: get_numeric_filter_value( + x, is_umsatz=is_umsatz_flag)) + # Wähle Wiki-Wert, wenn vorhanden und > 0, sonst CRM-Wert df_subset.loc[:, final_col_ml] = np.where( - (wiki_series.notna()) & (wiki_series > 0), + (wiki_series.notna()) & (wiki_series > 0), wiki_series, crm_series ) - # Ersetze 0 explizit durch NaN, damit es von log1p und Imputer korrekt behandelt wird - df_subset.loc[:, final_col_ml] = df_subset[final_col_ml].replace(0, np.nan) - self.logger.info(f" -> {df_subset[final_col_ml].notna().sum()} gueltige '{final_col_ml}' Werte erstellt (von {len(df_subset)} Zeilen).") + # Ersetze 0 explizit durch NaN, damit es von log1p und Imputer + # korrekt behandelt wird + df_subset.loc[:, final_col_ml] = df_subset[final_col_ml].replace( + 0, np.nan) + self.logger.info( + f" -> {df_subset[final_col_ml].notna().sum()} gueltige '{final_col_ml}' Werte erstellt (von {len(df_subset)} Zeilen).") # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # +++ NEUER BLOCK: Feature Engineering (Ratio & Log-Transformationen) +++++ + # +++ NEUER BLOCK: Feature Engineering (Ratio & Log-Transformationen) + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - self.logger.info("Erstelle zusätzliche Features (Ratio, Log-Transformationen)...") - + self.logger.info( + "Erstelle zusätzliche Features (Ratio, Log-Transformationen)...") + if 'Finaler_Umsatz_ML' in df_subset.columns and 'Finaler_Mitarbeiter_ML' in df_subset.columns: # Umsatz pro Mitarbeiter - ma_for_ratio = df_subset['Finaler_Mitarbeiter_ML'] # Hier sind Nullen schon durch NaN ersetzt - df_subset.loc[:, 'Umsatz_pro_MA_ML'] = df_subset['Finaler_Umsatz_ML'] / ma_for_ratio - df_subset['Umsatz_pro_MA_ML'].replace([np.inf, -np.inf], np.nan, inplace=True) + # Hier sind Nullen schon durch NaN ersetzt + ma_for_ratio = df_subset['Finaler_Mitarbeiter_ML'] + df_subset.loc[:, + 'Umsatz_pro_MA_ML'] = df_subset['Finaler_Umsatz_ML'] / ma_for_ratio + df_subset['Umsatz_pro_MA_ML'].replace( + [np.inf, -np.inf], np.nan, inplace=True) self.logger.debug(f" -> Feature 'Umsatz_pro_MA_ML' erstellt.") - # Log-Transformationen (np.log1p(x) berechnet log(1+x), sicher für NaNs) - df_subset.loc[:, 'Log_Finaler_Umsatz_ML'] = np.log1p(df_subset['Finaler_Umsatz_ML']) - df_subset.loc[:, 'Log_Finaler_Mitarbeiter_ML'] = np.log1p(df_subset['Finaler_Mitarbeiter_ML']) + # Log-Transformationen (np.log1p(x) berechnet log(1+x), sicher für + # NaNs) + df_subset.loc[:, 'Log_Finaler_Umsatz_ML'] = np.log1p( + df_subset['Finaler_Umsatz_ML']) + df_subset.loc[:, 'Log_Finaler_Mitarbeiter_ML'] = np.log1p( + df_subset['Finaler_Mitarbeiter_ML']) self.logger.debug(f" -> Log-transformierte Features erstellt.") else: - self.logger.warning("Konsolidierte Umsatz/Mitarbeiter-Spalten nicht gefunden, Feature Engineering übersprungen.") + self.logger.warning( + "Konsolidierte Umsatz/Mitarbeiter-Spalten nicht gefunden, Feature Engineering übersprungen.") # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # +++ ENDE NEUER BLOCK ++++++++++++++++++++++++++++++++++++++++++++++++++++ + # +++ ENDE NEUER BLOCK ++++++++++++++++++++++++++++++++++++++++++++++++ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # --- Zielvariable vorbereiten (Technikerzahl) --- self.logger.info("Verarbeite Zielvariable 'techniker'...") techniker_col_internal = "techniker" - df_subset.loc[:, 'Anzahl_Servicetechniker_Numeric'] = df_subset[techniker_col_internal].apply(lambda x: get_numeric_filter_value(x, is_umsatz=False)) - + df_subset.loc[:, 'Anzahl_Servicetechniker_Numeric'] = df_subset[techniker_col_internal].apply( + lambda x: get_numeric_filter_value(x, is_umsatz=False)) + initial_rows_before_tech_filter = len(df_subset) df_filtered = df_subset[ - df_subset['Anzahl_Servicetechniker_Numeric'].notna() & + df_subset['Anzahl_Servicetechniker_Numeric'].notna() & (df_subset['Anzahl_Servicetechniker_Numeric'] > 0) ].copy() - - removed_rows_tech_filter = initial_rows_before_tech_filter - len(df_filtered) + + removed_rows_tech_filter = initial_rows_before_tech_filter - \ + len(df_filtered) if removed_rows_tech_filter > 0: - self.logger.info(f"{removed_rows_tech_filter} Zeilen entfernt aufgrund fehlender/ungueltiger Technikerzahl.") - self.logger.info(f"Verbleibende Zeilen fuer Modellierungstraining: {len(df_filtered)}") + self.logger.info( + f"{removed_rows_tech_filter} Zeilen entfernt aufgrund fehlender/ungueltiger Technikerzahl.") + self.logger.info( + f"Verbleibende Zeilen fuer Modellierungstraining: {len(df_filtered)}") if df_filtered.empty: - self.logger.error("FEHLER: Keine Zeilen mit gueltiger Technikerzahl (>0) uebrig fuer Modellierungstraining!") + self.logger.error( + "FEHLER: Keine Zeilen mit gueltiger Technikerzahl (>0) uebrig fuer Modellierungstraining!") return None # --- Techniker-Buckets erstellen (mit reduzierter Klassenanzahl) --- - self.logger.info("Erstelle reduzierte Techniker-Buckets (3 Klassen)...") + self.logger.info( + "Erstelle reduzierte Techniker-Buckets (3 Klassen)...") bins_new = [-1, 49, 249, float('inf')] - labels_new = ['Techniker_Klein (0-49)', 'Techniker_Mittel (50-249)', 'Techniker_Gross (250+)'] - - df_filtered.loc[:, 'Techniker_Bucket'] = pd.cut( - df_filtered['Anzahl_Servicetechniker_Numeric'], bins=bins_new, labels=labels_new, right=True, include_lowest=True - ) - self.logger.info(f"Verteilung der neuen Techniker-Buckets:\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True, dropna=False).sort_index().round(3)}") - + labels_new = [ + 'Techniker_Klein (0-49)', + 'Techniker_Mittel (50-249)', + 'Techniker_Gross (250+)'] + + df_filtered.loc[:, + 'Techniker_Bucket'] = pd.cut(df_filtered['Anzahl_Servicetechniker_Numeric'], + bins=bins_new, + labels=labels_new, + right=True, + include_lowest=True) + self.logger.info( + f"Verteilung der neuen Techniker-Buckets:\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True, dropna=False).sort_index().round(3)}") + # --- Kategoriale Features vorbereiten (Branchen-Gruppen) --- - branche_col_internal = "branche_ki" # Dies ist die Spalte mit den Detail-Branchen - self.logger.info(f"Verarbeite kategoriales Feature '{branche_col_internal}' und mappe es zu 'Branchen_Gruppe'...") + # Dies ist die Spalte mit den Detail-Branchen + branche_col_internal = "branche_ki" + self.logger.info( + f"Verarbeite kategoriales Feature '{branche_col_internal}' und mappe es zu 'Branchen_Gruppe'...") if branche_col_internal not in df_filtered.columns: - self.logger.critical(f"FEHLER: Spalte '{branche_col_internal}' (aus 'Chat Vorschlag Branche') nicht im DataFrame gefunden.") - return None + self.logger.critical( + f"FEHLER: Spalte '{branche_col_internal}' (aus 'Chat Vorschlag Branche') nicht im DataFrame gefunden.") + return None # Normalisiere die Branchennamen aus dem Sheet für das Mapping - normalized_sheet_branches = df_filtered[branche_col_internal].apply(normalize_for_mapping) + normalized_sheet_branches = df_filtered[branche_col_internal].apply( + normalize_for_mapping) # Wende das hartcodierte Mapping aus der Config-Klasse an - df_filtered.loc[:, 'Branchen_Gruppe'] = normalized_sheet_branches.map(Config.BRANCH_GROUP_MAPPING).fillna('Sonstige') - + df_filtered.loc[:, 'Branchen_Gruppe'] = normalized_sheet_branches.map( + Config.BRANCH_GROUP_MAPPING).fillna('Sonstige') + self.logger.info("Mapping zu 'Branchen_Gruppe' durchgeführt.") - self.logger.debug(f"Verteilung der Branchen-Gruppen:\n{df_filtered['Branchen_Gruppe'].value_counts(normalize=True).sort_index().round(3)}") - - # One-Hot Encoding wird jetzt auf der neuen 'Branchen_Gruppe'-Spalte durchgeführt - df_encoded = pd.get_dummies(df_filtered, columns=['Branchen_Gruppe'], prefix='Gruppe', dummy_na=False) - self.logger.info(f"One-Hot Encoding fuer 'Branchen_Gruppe' durchgefuehrt.") - + self.logger.debug( + f"Verteilung der Branchen-Gruppen:\n{df_filtered['Branchen_Gruppe'].value_counts(normalize=True).sort_index().round(3)}") + + # One-Hot Encoding wird jetzt auf der neuen 'Branchen_Gruppe'-Spalte + # durchgeführt + df_encoded = pd.get_dummies( + df_filtered, + columns=['Branchen_Gruppe'], + prefix='Gruppe', + dummy_na=False) + self.logger.info( + f"One-Hot Encoding fuer 'Branchen_Gruppe' durchgefuehrt.") + # --- Finale Auswahl der Features fuer das Modell --- - feature_columns_ml = [col for col in df_encoded.columns if col.startswith('Gruppe_')] + feature_columns_ml = [ + col for col in df_encoded.columns if col.startswith('Gruppe_')] feature_columns_ml.extend([ - 'Log_Finaler_Umsatz_ML', + 'Log_Finaler_Umsatz_ML', 'Log_Finaler_Mitarbeiter_ML', 'Umsatz_pro_MA_ML', 'is_part_of_group' ]) - self.logger.info(f"Finale Feature-Auswahl für das Training: {feature_columns_ml}") + self.logger.info( + f"Finale Feature-Auswahl für das Training: {feature_columns_ml}") target_column_ml = 'Techniker_Bucket' identification_cols_ml = ['name', 'Anzahl_Servicetechniker_Numeric'] - - final_cols_for_df_ml = identification_cols_ml + feature_columns_ml + [target_column_ml] - missing_final_cols_ml = [col for col in final_cols_for_df_ml if col not in df_encoded.columns] + + final_cols_for_df_ml = identification_cols_ml + \ + feature_columns_ml + [target_column_ml] + missing_final_cols_ml = [ + col for col in final_cols_for_df_ml if col not in df_encoded.columns] if missing_final_cols_ml: - self.logger.critical(f"FEHLER: Finale Spalten fuer Modellierung fehlen im DataFrame: {missing_final_cols_ml}") - return None + self.logger.critical( + f"FEHLER: Finale Spalten fuer Modellierung fehlen im DataFrame: {missing_final_cols_ml}") + return None df_model_ready = df_encoded[final_cols_for_df_ml].copy() numeric_features_to_convert = [ - 'Log_Finaler_Umsatz_ML', 'Log_Finaler_Mitarbeiter_ML', 'Umsatz_pro_MA_ML', 'Anzahl_Servicetechniker_Numeric' - ] + 'Log_Finaler_Umsatz_ML', + 'Log_Finaler_Mitarbeiter_ML', + 'Umsatz_pro_MA_ML', + 'Anzahl_Servicetechniker_Numeric'] for col in numeric_features_to_convert: - if col in df_model_ready.columns: - df_model_ready[col] = pd.to_numeric(df_model_ready[col], errors='coerce') + if col in df_model_ready.columns: + df_model_ready[col] = pd.to_numeric( + df_model_ready[col], errors='coerce') df_model_ready = df_model_ready.reset_index(drop=True) - self.logger.info("Datenvorbereitung fuer Modellierung (Training) abgeschlossen.") - self.logger.info(f"Finaler DataFrame fuer Modellierung hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten.") + self.logger.info( + "Datenvorbereitung fuer Modellierung (Training) abgeschlossen.") + self.logger.info( + f"Finaler DataFrame fuer Modellierung hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten.") self.logger.info(f"Anzahl Feature-Spalten: {len(feature_columns_ml)}") numeric_features_for_imputation_ml = [ - 'Log_Finaler_Umsatz_ML', - 'Log_Finaler_Mitarbeiter_ML', + 'Log_Finaler_Umsatz_ML', + 'Log_Finaler_Mitarbeiter_ML', 'Umsatz_pro_MA_ML' ] - existing_numeric_features = [col for col in numeric_features_for_imputation_ml if col in df_model_ready.columns] + existing_numeric_features = [ + col for col in numeric_features_for_imputation_ml if col in df_model_ready.columns] if existing_numeric_features: nan_counts = df_model_ready[existing_numeric_features].isna().sum() - self.logger.info(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}") - rows_with_nan = df_model_ready[existing_numeric_features].isna().any(axis=1).sum() - self.logger.info(f"Anzahl Zeilen mit mindestens einem fehlenden numerischen Feature (vor Imputation): {rows_with_nan}") - + self.logger.info( + f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}") + rows_with_nan = df_model_ready[existing_numeric_features].isna().any( + axis=1).sum() + self.logger.info( + f"Anzahl Zeilen mit mindestens einem fehlenden numerischen Feature (vor Imputation): {rows_with_nan}") + return df_model_ready - def train_technician_model(self, model_out=MODEL_FILE, imputer_out=IMPUTER_FILE, patterns_out=PATTERNS_FILE_JSON): + def train_technician_model( + self, + model_out=MODEL_FILE, + imputer_out=IMPUTER_FILE, + patterns_out=PATTERNS_FILE_JSON): """ Trainiert, evaluiert und speichert das ML-Modell für die Techniker-Schätzung. """ self.logger.info("Starte Training des Servicetechniker-Modells...") - self.logger.info("Starte Training des Servicetechniker Decision Tree Modells...") + self.logger.info( + "Starte Training des Servicetechniker Decision Tree Modells...") # 1. Daten vorbereiten df_model_ready = self.prepare_data_for_modeling() if df_model_ready is None or df_model_ready.empty: - self.logger.error("Datenvorbereitung fuer Modelltraining fehlgeschlagen oder keine Daten. Training abgebrochen.") + self.logger.error( + "Datenvorbereitung fuer Modelltraining fehlgeschlagen oder keine Daten. Training abgebrochen.") return # Feature Spalten und Zielspalte definieren identification_cols = ['name', 'Anzahl_Servicetechniker_Numeric'] target_column = 'Techniker_Bucket' - feature_columns_ml = [col for col in df_model_ready.columns if col not in identification_cols and col != target_column] + feature_columns_ml = [ + col for col in df_model_ready.columns if col not in identification_cols and col != target_column] if not feature_columns_ml: - self.logger.critical("FEHLER: Keine Feature-Spalten nach Datenvorbereitung gefunden. Training nicht moeglich.") - return + self.logger.critical( + "FEHLER: Keine Feature-Spalten nach Datenvorbereitung gefunden. Training nicht moeglich.") + return X = df_model_ready[feature_columns_ml] y = df_model_ready[target_column] - self.logger.info(f"Daten fuer Training vorbereitet. X Shape: {X.shape}, y Shape: {y.shape}") + self.logger.info( + f"Daten fuer Training vorbereitet. X Shape: {X.shape}, y Shape: {y.shape}") # 2. Split in Training und Test Set - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y) - self.logger.info(f"Daten gesplittet. Train Set: {len(X_train)} Zeilen, Test Set: {len(X_test)} Zeilen.") + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.25, random_state=42, stratify=y) + self.logger.info( + f"Daten gesplittet. Train Set: {len(X_train)} Zeilen, Test Set: {len(X_test)} Zeilen.") # 3. Imputation (Fehlende Werte ersetzen) imputer = SimpleImputer(strategy='median') - self.logger.info(f"Fitte Imputer mit Strategie '{imputer.strategy}' auf Trainingsdaten...") - imputer.fit(X_train) + self.logger.info( + f"Fitte Imputer mit Strategie '{imputer.strategy}' auf Trainingsdaten...") + imputer.fit(X_train) # Speichern Sie den Imputer (wird fuer Vorhersagen benoetigt). - self.imputer = imputer # Speichern Sie ihn in der Instanz + self.imputer = imputer # Speichern Sie ihn in der Instanz try: imputer_dir = os.path.dirname(imputer_out) if imputer_dir and not os.path.exists(imputer_dir): os.makedirs(imputer_dir, exist_ok=True) with open(imputer_out, 'wb') as f: pickle.dump(imputer, f) - self.logger.info(f"Imputer erfolgreich gespeichert in '{imputer_out}'.") + self.logger.info( + f"Imputer erfolgreich gespeichert in '{imputer_out}'.") except Exception as e: - self.logger.error(f"FEHLER beim Speichern des Imputers in '{imputer_out}': {e}") + self.logger.error( + f"FEHLER beim Speichern des Imputers in '{imputer_out}': {e}") self.logger.debug(traceback.format_exc()) - # Training sollte hier nicht unbedingt abbrechen, aber ein Hinweis ist wichtig + # Training sollte hier nicht unbedingt abbrechen, aber ein Hinweis + # ist wichtig X_train_imputed = imputer.transform(X_train) X_test_imputed = imputer.transform(X_test) - X_train_imputed = pd.DataFrame(X_train_imputed, columns=feature_columns_ml) # feature_columns_ml verwenden - X_test_imputed = pd.DataFrame(X_test_imputed, columns=feature_columns_ml) # feature_columns_ml verwenden + X_train_imputed = pd.DataFrame( + X_train_imputed, + columns=feature_columns_ml) # feature_columns_ml verwenden + X_test_imputed = pd.DataFrame( + X_test_imputed, + columns=feature_columns_ml) # feature_columns_ml verwenden # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # +++ ANPASSUNG HIER: GridSearchCV mit Pipeline für SMOTE & RandomForest + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # 4. Erstellen einer Pipeline und Definieren des Parameter-Grids - + # Schritt 1: SMOTE für Klassen-Balancierung # Schritt 2: RandomForestClassifier als Modell pipeline = ImbPipeline([ ('smote', SMOTE(random_state=42)), - ('classifier', RandomForestClassifier(random_state=42, n_jobs=-1)) # class_weight nicht nötig bei SMOTE + # class_weight nicht nötig bei SMOTE + ('classifier', RandomForestClassifier(random_state=42, n_jobs=-1)) ]) # Definieren der Hyperparameter, die getestet werden sollen. - # WICHTIG: Die Parameternamen müssen mit dem Namen des Schritts in der Pipeline beginnen (z.B. 'classifier__...') + # WICHTIG: Die Parameternamen müssen mit dem Namen des Schritts in der + # Pipeline beginnen (z.B. 'classifier__...') param_grid = { 'classifier__n_estimators': [200, 300], # Anzahl der Bäume - 'classifier__max_depth': [10, 20, None], # Maximale Tiefe der Bäume (None = unbegrenzt) - 'classifier__min_samples_split': [2, 5], # Mindestanzahl Samples für einen Split - 'classifier__min_samples_leaf': [1, 2] # Mindestanzahl Samples in einem Blatt + # Maximale Tiefe der Bäume (None = unbegrenzt) + 'classifier__max_depth': [10, 20, None], + # Mindestanzahl Samples für einen Split + 'classifier__min_samples_split': [2, 5], + # Mindestanzahl Samples in einem Blatt + 'classifier__min_samples_leaf': [1, 2] } # HINWEIS: Dies sind 2 * 3 * 2 * 2 = 24 Kombinationen. # Mit cv=3 (siehe unten) werden 24 * 3 = 72 Modelle trainiert. Dies kann dauern! - # Für einen schnellen Test können Sie die Anzahl der Optionen reduzieren. + # Für einen schnellen Test können Sie die Anzahl der Optionen + # reduzieren. # Initialisieren von GridSearchCV # cv=3 bedeutet 3-fache Kreuzvalidierung. # scoring='accuracy' bedeutet, dass die beste Kombination anhand der Genauigkeit ausgewählt wird. # verbose=2 gibt detaillierte Log-Ausgaben während der Suche. - grid_search = GridSearchCV(estimator=pipeline, param_grid=param_grid, cv=3, scoring='accuracy', verbose=2, n_jobs=-1) + grid_search = GridSearchCV( + estimator=pipeline, + param_grid=param_grid, + cv=3, + scoring='accuracy', + verbose=2, + n_jobs=-1) self.logger.info("Starte Hyperparameter-Tuning mit GridSearchCV...") start_fit_time = time.time() # Fitte das Grid auf den (noch nicht resampleten) Trainingsdaten. - # Die Pipeline kümmert sich intern darum, dass SMOTE nur auf die Trainings-Folds angewendet wird. + # Die Pipeline kümmert sich intern darum, dass SMOTE nur auf die + # Trainings-Folds angewendet wird. grid_search.fit(X_train_imputed, y_train) end_fit_time = time.time() - self.logger.info(f"GridSearchCV-Suche abgeschlossen. Dauer: {end_fit_time - start_fit_time:.2f} Sekunden.") + self.logger.info( + f"GridSearchCV-Suche abgeschlossen. Dauer: {end_fit_time - start_fit_time:.2f} Sekunden.") # Beste Parameter und bestes Modell ausgeben und speichern - self.logger.info(f"Beste gefundene Parameter: {grid_search.best_params_}") - self.logger.info(f"Beste Cross-Validation Accuracy: {grid_search.best_score_:.4f}") - - # Das beste Modell ist das, das mit den besten Parametern auf den *gesamten* Trainingsdaten trainiert wurde. + self.logger.info( + f"Beste gefundene Parameter: {grid_search.best_params_}") + self.logger.info( + f"Beste Cross-Validation Accuracy: {grid_search.best_score_:.4f}") + + # Das beste Modell ist das, das mit den besten Parametern auf den + # *gesamten* Trainingsdaten trainiert wurde. best_classifier = grid_search.best_estimator_ # Modell speichern - self.model = best_classifier # Zuweisung des besten gefundenen Modells + self.model = best_classifier # Zuweisung des besten gefundenen Modells try: model_dir = os.path.dirname(model_out) if model_dir and not os.path.exists(model_dir): os.makedirs(model_dir, exist_ok=True) with open(model_out, 'wb') as f: - pickle.dump(best_classifier, f) # Das beste Modell speichern - self.logger.info(f"Bestes RandomForest Modell erfolgreich gespeichert in '{model_out}'.") + pickle.dump(best_classifier, f) # Das beste Modell speichern + self.logger.info( + f"Bestes RandomForest Modell erfolgreich gespeichert in '{model_out}'.") except Exception as e: - self.logger.error(f"FEHLER beim Speichern des besten Modells in '{model_out}': {e}") + self.logger.error( + f"FEHLER beim Speichern des besten Modells in '{model_out}': {e}") # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # +++ ENDE ANPASSUNG ++++++++++++++++++++++++++++++++++++++++++++++++++++ + # +++ ENDE ANPASSUNG ++++++++++++++++++++++++++++++++++++++++++++++++++ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # Feature-Liste speichern (bleibt unverändert) - self._expected_features = feature_columns_ml + self._expected_features = feature_columns_ml try: # Wir verwenden die .classes_ vom besten gefundenen Modell - patterns_data = {"feature_columns": self._expected_features, "target_classes": list(best_classifier.classes_)} # << KORRIGIERT + patterns_data = { + "feature_columns": self._expected_features, + "target_classes": list( + best_classifier.classes_)} # << KORRIGIERT patterns_dir = os.path.dirname(patterns_out) if patterns_dir and not os.path.exists(patterns_dir): os.makedirs(patterns_dir, exist_ok=True) with open(patterns_out, 'w', encoding='utf-8') as f: - json.dump(patterns_data, f, indent=4, ensure_ascii=False) - self.logger.info(f"Erwartete Feature-Spalten und Klassen erfolgreich gespeichert in '{patterns_out}'.") + json.dump(patterns_data, f, indent=4, ensure_ascii=False) + self.logger.info( + f"Erwartete Feature-Spalten und Klassen erfolgreich gespeichert in '{patterns_out}'.") except Exception as e: - self.logger.error(f"FEHLER beim Speichern der Feature-Spalten in '{patterns_out}': {e}") + self.logger.error( + f"FEHLER beim Speichern der Feature-Spalten in '{patterns_out}': {e}") # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # +++ ENDE FEATURE-LISTEN SPEICHERUNG +++++++++++++++++++++++++++++++++++ + # +++ ENDE FEATURE-LISTEN SPEICHERUNG +++++++++++++++++++++++++++++++++ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # 5. Evaluation (Optional, aber empfohlen, um die Modellleistung zu bewerten) - self.logger.info("Starte Evaluation des besten Modells auf dem ungesehenen Testset...") - y_pred = best_classifier.predict(X_test_imputed) # << KORRIGIERT + # 5. Evaluation (Optional, aber empfohlen, um die Modellleistung zu + # bewerten) + self.logger.info( + "Starte Evaluation des besten Modells auf dem ungesehenen Testset...") + y_pred = best_classifier.predict(X_test_imputed) # << KORRIGIERT accuracy = accuracy_score(y_test, y_pred) - self.logger.info(f"Finale Modell Genauigkeit auf dem Testset: {accuracy:.4f}") - - class_report_labels = list(best_classifier.classes_) # << KORRIGIERT - class_report = classification_report(y_test, y_pred, zero_division=0, labels=class_report_labels, target_names=[str(c) for c in class_report_labels]) - self.logger.info(f"Klassifikationsbericht auf dem Testset:\n{class_report}") + self.logger.info( + f"Finale Modell Genauigkeit auf dem Testset: {accuracy:.4f}") + + class_report_labels = list(best_classifier.classes_) # << KORRIGIERT + class_report = classification_report( + y_test, + y_pred, + zero_division=0, + labels=class_report_labels, + target_names=[ + str(c) for c in class_report_labels]) + self.logger.info( + f"Klassifikationsbericht auf dem Testset:\n{class_report}") cm = confusion_matrix(y_test, y_pred, labels=class_report_labels) - self.logger.info(f"Konfusionsmatrix auf dem Testset (Zeilen=Wahr, Spalten=Vorhersage):\n{cm}") + self.logger.info( + f"Konfusionsmatrix auf dem Testset (Zeilen=Wahr, Spalten=Vorhersage):\n{cm}") # Block für Feature Importance try: - # Greife auf den Schritt 'classifier' in der Pipeline zu, um das finale Modell zu bekommen + # Greife auf den Schritt 'classifier' in der Pipeline zu, um das + # finale Modell zu bekommen final_rf_model = best_classifier.named_steps['classifier'] self.logger.info("Feature Importance des besten Modells (Top 15):") - importances = final_rf_model.feature_importances_ # << KORRIGIERT + importances = final_rf_model.feature_importances_ # << KORRIGIERT feature_importance_df = pd.DataFrame({ 'Feature': feature_columns_ml, 'Importance': importances }).sort_values(by='Importance', ascending=False) - - self.logger.info(f"\n{feature_importance_df.head(15).to_string(index=False)}") + + self.logger.info( + f"\n{feature_importance_df.head(15).to_string(index=False)}") except Exception as e_feat_imp: - self.logger.warning(f"FEHLER beim Berechnen/Anzeigen der Feature Importance: {e_feat_imp}") + self.logger.warning( + f"FEHLER beim Berechnen/Anzeigen der Feature Importance: {e_feat_imp}") self.logger.info("Modelltraining und -evaluation abgeschlossen.") - def process_website_details(self, start_sheet_row=None, end_sheet_row=None, limit=None): + def process_website_details( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None): """ EXPERIMENTELL: Extrahiert Website-Details für Zeilen mit 'x' in Spalte A. """ @@ -4464,243 +6224,302 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge den Start des Modus auf Warning, da es experimentell ist. - self.logger.warning(f"Starte Modus (EXPERIMENTELL): Website Detail Extraction fuer Zeilen mit 'x' in Spalte A. Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT - self.logger.warning("Hinweis: Dieser Modus nutzt die globale Funktion 'scrape_website_details' (Block 13), deren Implementierung je nach Zielwebsites angepasst werden muss.") # <<< GEÄNDERT - + self.logger.warning( + f"Starte Modus (EXPERIMENTELL): Website Detail Extraction fuer Zeilen mit 'x' in Spalte A. Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT + self.logger.warning( + "Hinweis: Dieser Modus nutzt die globale Funktion 'scrape_website_details' (Block 13), deren Implementierung je nach Zielwebsites angepasst werden muss.") # <<< GEÄNDERT # --- Daten laden --- # Laden Sie Daten neu. Kein automatischer Startindex-Check noetig hier, # da wir explizit nach dem 'x'-Flag suchen. # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer Website Details Extraction.") # <<< GEÄNDERT - return # Beende die Methode, wenn das Laden fehlschlaegt - - - # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. - all_data = self.sheet_handler.get_all_data_with_headers(); - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). - header_rows = self.sheet_handler._header_rows; - total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet + self.logger.error( + "Fehler beim Laden der Daten fuer Website Details Extraction.") # <<< GEÄNDERT + return # Beende die Methode, wenn das Laden fehlschlaegt + # Holen Sie die gesamte Datenliste (inklusive Header) aus dem + # SheetHandler. + all_data = self.sheet_handler.get_all_data_with_headers() + # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar + # (Block 14). + header_rows = self.sheet_handler._header_rows + total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet # Standard Startzeile, wenn nicht manuell gesetzt - if start_sheet_row is None: start_sheet_row = header_rows + 1 # Standardmaessig ab erster Datenzeile (Zeile nach Headern) + if start_sheet_row is None: + # Standardmaessig ab erster Datenzeile (Zeile nach Headern) + start_sheet_row = header_rows + 1 # Berechne Endzeile, wenn nicht manuell gesetzt - if end_sheet_row is None: end_sheet_row = total_sheet_rows # Bis zur letzten Zeile - + if end_sheet_row is None: + end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den Suchbereich fuer das 'x'-Flag - self.logger.info(f"Suchbereich fuer 'x'-Flag: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT + self.logger.info( + f"Suchbereich fuer 'x'-Flag: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT # Pruefe, ob der Bereich gueltig ist if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT - return # Beende die Methode, wenn der Bereich leer ist - + self.logger.info( + "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT + return # Beende die Methode, wenn der Bereich leer ist # --- Indizes und Buchstaben --- - # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind - required_keys = ["ReEval Flag", "CRM Website", "CRM Name"] # A, D, B + # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP + # (Block 1) vorhanden sind + required_keys = ["ReEval Flag", "CRM Website", "CRM Name"] # A, D, B # Erstellen Sie ein Dictionary mit Schluesseln und Indizes col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} - # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden + # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden + # wurden if None in col_indices.values(): - missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_website_details: {missing}. Breche ab.") # <<< GEÄNDERT - return # Beende die Methode bei kritischem Fehler + missing = [k for k, v in col_indices.items() if v is None] + self.logger.critical( + f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_website_details: {missing}. Breche ab.") # <<< GEÄNDERT + return # Beende die Methode bei kritischem Fehler # Ermitteln Sie die Indizes - reeval_col_idx = col_indices["ReEval Flag"] # A - website_col_idx = col_indices["CRM Website"] # D + reeval_col_idx = col_indices["ReEval Flag"] # A + website_col_idx = col_indices["CRM Website"] # D - # Bestimme die ZIELSPALTE fuer die Details (Website Details ODER AR als Fallback) - details_col_idx = COLUMN_MAP.get("Website Details") # Versuche zuerst die dedizierte Spalte (Block 1 Column Map) - details_col_key_for_logging = "Website Details" # Name fuer Logging + # Bestimme die ZIELSPALTE fuer die Details (Website Details ODER AR als + # Fallback) + # Versuche zuerst die dedizierte Spalte (Block 1 Column Map) + details_col_idx = COLUMN_MAP.get("Website Details") + details_col_key_for_logging = "Website Details" # Name fuer Logging # Wenn die dedizierte Spalte nicht gefunden wurde if details_col_idx is None: # Fallback auf 'Website Rohtext' (AR) - details_col_idx = COLUMN_MAP.get("Website Rohtext") # Block 1 Column Map + details_col_idx = COLUMN_MAP.get( + "Website Rohtext") # Block 1 Column Map details_col_key_for_logging = "Website Rohtext" # Pruefen Sie, ob der Fallback-Schluessel gefunden wurde if details_col_idx is None: - self.logger.critical("FEHLER: Weder 'Website Details' noch 'Website Rohtext' Spaltenindex in COLUMN_MAP gefunden.") # <<< GEÄNDERT - return # Beende die Methode bei kritischem Fehler - self.logger.warning(f"Keine Spalte 'Website Details' in COLUMN_MAP, nutze '{details_col_key_for_logging}' ({self.sheet_handler._get_col_letter(details_col_idx+1)}) als Fallback.") # <<< GEÄNDERT + self.logger.critical( + "FEHLER: Weder 'Website Details' noch 'Website Rohtext' Spaltenindex in COLUMN_MAP gefunden.") # <<< GEÄNDERT + return # Beende die Methode bei kritischem Fehler + self.logger.warning( + f"Keine Spalte 'Website Details' in COLUMN_MAP, nutze '{details_col_key_for_logging}' ({self.sheet_handler._get_col_letter(details_col_idx+1)}) als Fallback.") # <<< GEÄNDERT else: - # Logge die Verwendung der dedizierten Spalte - self.logger.info(f"Nutze Spalte '{details_col_key_for_logging}' ({self.sheet_handler._get_col_letter(details_col_idx+1)}) fuer Website Details.") # <<< GEÄNDERT - - - # Ermitteln Sie den Spaltenbuchstaben der Zielspalte (nutzt interne Helfer _get_col_letter Block 14) - details_col_letter = self.sheet_handler._get_col_letter(details_col_idx + 1) + # Logge die Verwendung der dedizierten Spalte + self.logger.info( + f"Nutze Spalte '{details_col_key_for_logging}' ({self.sheet_handler._get_col_letter(details_col_idx+1)}) fuer Website Details.") # <<< GEÄNDERT + # Ermitteln Sie den Spaltenbuchstaben der Zielspalte (nutzt interne + # Helfer _get_col_letter Block 14) + details_col_letter = self.sheet_handler._get_col_letter( + details_col_idx + 1) # --- Verarbeitung --- # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1). update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) + # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) + all_sheet_updates = [] - all_sheet_updates = [] # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) + # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den + # Batch aufgenommen werden (im Rahmen des Limits). + processed_count = 0 + # Zaehlt Zeilen, die uebersprungen wurden (nicht markiert oder fehlende + # URL). + skipped_count = 0 - - processed_count = 0 # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den Batch aufgenommen werden (im Rahmen des Limits). - skipped_count = 0 # Zaehlt Zeilen, die uebersprungen wurden (nicht markiert oder fehlende URL). - - - # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte Sheet-Zeilennummer) + # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte + # Sheet-Zeilennummer) for i in range(start_sheet_row, end_sheet_row + 1): - row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste + row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste # Pruefen Sie, ob das Ende des Sheets erreicht wurde - if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht - - - row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile + if row_index_in_list >= total_sheet_rows: + break # Ende des Sheets erreicht + row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile # Stellen Sie sicher, dass die Zeile nicht leer ist - if not any(cell and isinstance(cell, str) and cell.strip() for cell in row): - #self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") # Zu viel Laerm im Debug - skipped_count += 1 # Zaehlen als uebersprungene Zeile - continue # Springe zur naechsten Zeile - + if not any(cell and isinstance(cell, str) and cell.strip() + for cell in row): + # self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") + # # Zu viel Laerm im Debug + skipped_count += 1 # Zaehlen als uebersprungene Zeile + continue # Springe zur naechsten Zeile # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist --- # Kriterium: Zeile ist mit 'x' in Spalte A (ReEval Flag) markiert. # UND Website URL (D) ist vorhanden und gueltig aussehend. - # Holen Sie den Wert aus Spalte A (ReEval Flag) (nutzt interne Helfer _get_cell_value_safe) - cell_a_value = self._get_cell_value_safe(row, "ReEval Flag").strip().lower() # Block 1 Column Map + # Holen Sie den Wert aus Spalte A (ReEval Flag) (nutzt interne + # Helfer _get_cell_value_safe) + cell_a_value = self._get_cell_value_safe( + row, "ReEval Flag").strip().lower() # Block 1 Column Map # Pruefen Sie, ob die Zelle mit 'x' markiert ist. is_marked_for_processing = cell_a_value == "x" # Wenn die Zeile nicht mit 'x' markiert ist, ueberspringen if not is_marked_for_processing: - skipped_count += 1 # Zaehlen als uebersprungene Zeile - continue # Springe zur naechsten Zeile + skipped_count += 1 # Zaehlen als uebersprungene Zeile + continue # Springe zur naechsten Zeile + # Holen Sie den Wert aus Spalte D (CRM Website) (nutzt interne + # Helfer _get_cell_value_safe) + website_url = self._get_cell_value_safe( + row, "CRM Website").strip() # Block 1 Column Map + # Pruefen Sie, ob die Website URL (D) vorhanden und gueltig + # aussehend ist. + website_url_is_valid_looking = website_url and isinstance( + website_url, + str) and website_url.lower() not in [ + "k.a.", + "kein artikel gefunden", + "fehler bei suche", + "http:"] # Fuege "http:" hinzu basierend auf Log - # Holen Sie den Wert aus Spalte D (CRM Website) (nutzt interne Helfer _get_cell_value_safe) - website_url = self._get_cell_value_safe(row, "CRM Website").strip() # Block 1 Column Map - # Pruefen Sie, ob die Website URL (D) vorhanden und gueltig aussehend ist. - website_url_is_valid_looking = website_url and isinstance(website_url, str) and website_url.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu basierend auf Log - - - # Verarbeitung ist noetig, wenn die Zeile mit 'x' markiert ist UND die Website URL gueltig ist. + # Verarbeitung ist noetig, wenn die Zeile mit 'x' markiert ist UND + # die Website URL gueltig ist. processing_needed_for_row = is_marked_for_processing and website_url_is_valid_looking - # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level - log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) + log_check = ( + i < start_sheet_row + + 5) or ( + i % + 100 == 0) or (processing_needed_for_row) if log_check: - company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Details Check): A='x'? {is_marked_for_processing}, D gueltig? {website_url_is_valid_looking}. Benoetigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT + company_name = self._get_cell_value_safe( + row, "CRM Name").strip() # Block 1 Column Map + self.logger.debug( + f"Zeile {i} ({company_name[:50]}... Website Details Check): A='x'? {is_marked_for_processing}, D gueltig? {website_url_is_valid_looking}. Benoetigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT - - # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist (trotz 'x' fehlte die URL) + # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist (trotz + # 'x' fehlte die URL) if not processing_needed_for_row: - skipped_count += 1 # Zaehlen als uebersprungene Zeile - # Optionale Behandlung: Wenn mit 'x' markiert, aber URL fehlt, was tun? - # Derzeit wird sie uebersprungen. Ggf. Fehler in Spalte notieren? - continue # Springe zur naechsten Zeile - + skipped_count += 1 # Zaehlen als uebersprungene Zeile + # Optionale Behandlung: Wenn mit 'x' markiert, aber URL fehlt, was tun? + # Derzeit wird sie uebersprungen. Ggf. Fehler in Spalte + # notieren? + continue # Springe zur naechsten Zeile # --- Wenn Verarbeitung noetig: Fuehre Details-Extraktion aus --- - processed_count += 1 # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im Rahmen des Limits zaehlen) + # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im + # Rahmen des Limits zaehlen) + processed_count += 1 # Pruefe das Limit fuer verarbeitete Zeilen - if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: - # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_website_details erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT - break # Brich die Schleife ab + if limit is not None and isinstance( + limit, int) and limit > 0 and processed_count > limit: + # Wenn das Limit erreicht ist und es ein positives Limit gibt + self.logger.info( + f"Verarbeitungslimit ({limit}) fuer process_website_details erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT + break # Brich die Schleife ab + # <<< GEÄNDERT (war selflogger) + self.logger.info( + f"Zeile {i}: Extrahiere Website Details von {website_url[:100]}...") - self.logger.info(f"Zeile {i}: Extrahiere Website Details von {website_url[:100]}...") # <<< GEÄNDERT (war selflogger) - - - details = "FEHLER: Funktion 'scrape_website_details' nicht verfuegbar" # Default Fehler, falls die Funktion nicht existiert (Sollte nicht passieren, wenn Block 13 korrekt ist) + # Default Fehler, falls die Funktion nicht existiert (Sollte nicht + # passieren, wenn Block 13 korrekt ist) + details = "FEHLER: Funktion 'scrape_website_details' nicht verfuegbar" try: - # Rufe die globale Funktion scrape_website_details auf (Block 13). - # scrape_website_details ist mit retry_on_failure dekoriert (Block 2). - # Wenn scrape_website_details fehlschlaegt, wirft sie eine Exception oder gibt einen Fehlerwert zurueck. - details = scrape_website_details(website_url) # <<< Ruft globale Funktion (Block 13) + # Rufe die globale Funktion scrape_website_details auf (Block 13). + # scrape_website_details ist mit retry_on_failure dekoriert (Block 2). + # Wenn scrape_website_details fehlschlaegt, wirft sie eine + # Exception oder gibt einen Fehlerwert zurueck. + # <<< Ruft globale Funktion (Block 13) + details = scrape_website_details(website_url) - # Wenn die Funktion einen Fehler geloggt hat und einen Fehlerstring im Ergebnis zurueckgibt, - # wird dies in der 'details' Variable gespeichert. - if isinstance(details, str) and (details.startswith("k.A. (Fehler") or details.startswith("FEHLER:")): - # Fehler wurde bereits in scrape_website_details geloggt. - pass # Details enthaelt bereits den Fehlerstring. - - elif not isinstance(details, str) or not details.strip(): - # Wenn die Funktion keinen String oder einen leeren String zurueckgibt. - details = "k.A. (Extraktion leer oder ungueltig)" # Standard-Fehlerwert + # Wenn die Funktion einen Fehler geloggt hat und einen Fehlerstring im Ergebnis zurueckgibt, + # wird dies in der 'details' Variable gespeichert. + if isinstance(details, str) and (details.startswith( + "k.A. (Fehler") or details.startswith("FEHLER:")): + # Fehler wurde bereits in scrape_website_details geloggt. + pass # Details enthaelt bereits den Fehlerstring. + elif not isinstance(details, str) or not details.strip(): + # Wenn die Funktion keinen String oder einen leeren String + # zurueckgibt. + # Standard-Fehlerwert + details = "k.A. (Extraktion leer oder ungueltig)" except NameError: - # Dieser Fehler sollte nicht auftreten, wenn scrape_website_details in Block 13 ist. - self.logger.critical("FEHLER: Funktion 'scrape_website_details' ist nicht definiert! Kann Details nicht extrahieren.") # <<< GEÄNDERT - # Logge den Traceback. - self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT - details = "FEHLER: Funktion nicht definiert" # Setze spezifischen Fehlerwert + # Dieser Fehler sollte nicht auftreten, wenn + # scrape_website_details in Block 13 ist. + self.logger.critical( + "FEHLER: Funktion 'scrape_website_details' ist nicht definiert! Kann Details nicht extrahieren.") # <<< GEÄNDERT + # Logge den Traceback. + self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT + details = "FEHLER: Funktion nicht definiert" # Setze spezifischen Fehlerwert except Exception as e_detail: - # Fange andere unerwartete Fehler ab, die nicht von scrape_website_details behandelt wurden. - self.logger.exception(f"Unerwarteter Fehler bei scrape_website_details fuer {website_url[:100]}...: {type(e_detail).__name__} - {e_detail}") # <<< GEÄNDERT - details = f"k.A. (Unerwarteter Fehler: {str(e_detail)[:100]}...)" # Signalisiert Fehler (gekuerzt) - + # Fange andere unerwartete Fehler ab, die nicht von + # scrape_website_details behandelt wurden. + self.logger.exception( + f"Unerwarteter Fehler bei scrape_website_details fuer {website_url[:100]}...: {type(e_detail).__name__} - {e_detail}") # <<< GEÄNDERT + # Signalisiert Fehler (gekuerzt) + details = f"k.A. (Unerwarteter Fehler: {str(e_detail)[:100]}...)" # Fuege Update fuer die Details-Spalte hinzu (nutzt interne Helfer _get_col_letter Block 14) # Stellen Sie sicher, dass der Wert ein String ist. - updates_for_row = [] # Lokale Liste fuer Updates dieser Zeile - updates_for_row.append({'range': f'{details_col_letter}{i}', 'values': [[str(details)]]}) # Block 1 Column Map - self.logger.debug(f"Zeile {i}: Details extrahiert und zum Update fuer Spalte {details_col_key_for_logging} ({details_col_letter}{i}) hinzugefuegt.") # <<< GEÄNDERT + updates_for_row = [] # Lokale Liste fuer Updates dieser Zeile + updates_for_row.append({'range': f'{details_col_letter}{i}', 'values': [ + [str(details)]]}) # Block 1 Column Map + self.logger.debug( + f"Zeile {i}: Details extrahiert und zum Update fuer Spalte {details_col_key_for_logging} ({details_col_letter}{i}) hinzugefuegt.") # <<< GEÄNDERT - - # Sammle die Updates fuer diese Zeile in der globalen Liste all_sheet_updates. + # Sammle die Updates fuer diese Zeile in der globalen Liste + # all_sheet_updates. all_sheet_updates.extend(updates_for_row) - # Sende gesammelte Sheet Updates wenn das Update-Batch-Limit erreicht ist. # update_batch_row_limit wird aus Config geholt (Block 1). - # Updates pro Zeile ist 1 in diesem Modus. Anzahl der Zeilen = len(all_sheet_updates). + # Updates pro Zeile ist 1 in diesem Modus. Anzahl der Zeilen = + # len(all_sheet_updates). if len(all_sheet_updates) >= update_batch_row_limit: - self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. - # Wenn es fehlschlaegt, wird es intern geloggt. - success = self.sheet_handler.batch_update_cells(all_sheet_updates) - if success: - self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") # <<< GEÄNDERT - # Der Fehlerfall wird von batch_update_cells geloggt - - # Leere die gesammelten Updates nach dem Senden. - all_sheet_updates = [] + self.logger.debug( + f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT + # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. + # Wenn es fehlschlaegt, wird es intern geloggt. + success = self.sheet_handler.batch_update_cells( + all_sheet_updates) + if success: + self.logger.info( + f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") # <<< GEÄNDERT + # Der Fehlerfall wird von batch_update_cells geloggt + # Leere die gesammelten Updates nach dem Senden. + all_sheet_updates = [] # Kleine Pause nach jeder Extraktion (nutzt Config Block 1). - # Dieser Modus macht API calls (ueber scrape_website_details und dessen Helfer), also Pause einbauen. + # Dieser Modus macht API calls (ueber scrape_website_details und + # dessen Helfer), also Pause einbauen. pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.2 - #self.logger.debug(f"Warte {pause_duration:.2f}s nach Extraktion...") # Zu viel Laerm im Debug + # self.logger.debug(f"Warte {pause_duration:.2f}s nach + # Extraktion...") # Zu viel Laerm im Debug time.sleep(pause_duration) - # --- Finale Sheet Updates senden --- - # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update. + # Sende alle verbleibenden gesammelten Updates in einem letzten + # Batch-Update. if all_sheet_updates: - self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. + self.logger.info( + f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT + # Nutzt die batch_update_cells Methode des Sheet Handlers (Block + # 14) mit Retry. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT + # <<< GEÄNDERT + self.logger.info(f"FINALES Sheet-Update erfolgreich.") # Der Fehlerfall wird von batch_update_cells geloggt - # Logge den Abschluss des Modus - self.logger.info(f"Modus 'website_details' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT - - def process_wiki_updates_from_chatgpt(self, start_sheet_row=None, end_sheet_row=None, limit=None): + self.logger.info( + f"Modus 'website_details' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT + + def process_wiki_updates_from_chatgpt( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None): """ Verarbeitet Wiki-Updates basierend auf ChatGPT-Vorschlägen. """ @@ -4720,67 +6539,83 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Modus - self.logger.info(f"Starte Modus 'wiki_updates_from_chatgpt' (S, U, M, N-V, AN, AO, AX, AP, A). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT - + self.logger.info( + f"Starte Modus 'wiki_updates_from_chatgpt' (S, U, M, N-V, AN, AO, AX, AP, A). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden --- # Laden Sie Daten neu. Kein automatischer Startindex-Check noetig hier, # da wir nach Status S suchen. # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer Wiki Updates.") # <<< GEÄNDERT - return # Beende die Methode, wenn das Laden fehlschlaegt + self.logger.error( + "Fehler beim Laden der Daten fuer Wiki Updates.") # <<< GEÄNDERT + return # Beende die Methode, wenn das Laden fehlschlaegt - - # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. + # Holen Sie die gesamte Datenliste (inklusive Header) aus dem + # SheetHandler. all_data = self.sheet_handler.get_all_data_with_headers() - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). + # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar + # (Block 14). header_rows = self.sheet_handler._header_rows - total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet - + total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet # Standard Startzeile, wenn nicht manuell gesetzt - if start_sheet_row is None: start_sheet_row = header_rows + 1 # Standardmaessig ab erster Datenzeile (Zeile nach Headern) + if start_sheet_row is None: + # Standardmaessig ab erster Datenzeile (Zeile nach Headern) + start_sheet_row = header_rows + 1 # Berechne Endzeile, wenn nicht manuell gesetzt - if end_sheet_row is None: end_sheet_row = total_sheet_rows # Bis zur letzten Zeile - + if end_sheet_row is None: + end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den Suchbereich fuer Status S - self.logger.info(f"Suchbereich fuer Status S: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT + self.logger.info( + f"Suchbereich fuer Status S: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT # Pruefe, ob der Bereich gueltig ist if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT - return # Beende die Methode, wenn der Bereich leer ist - + self.logger.info( + "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT + return # Beende die Methode, wenn der Bereich leer ist # --- Indizes und Buchstaben --- - # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind + # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP + # (Block 1) vorhanden sind required_keys = [ - "Chat Wiki Konsistenzpruefung", "Chat Vorschlag Wiki Artikel", "Wiki URL", # S, U, M (Pruefkriterien / Daten) - "Wikipedia Timestamp", "Wiki Verif. Timestamp", "Timestamp letzte Pruefung", "Version", # AN, AX, AO, AP (Spalten zum Loeschen) - "ReEval Flag", # A (ReEval Flag setzen) - "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", # N-R (Spalten zum Loeschen) - "Chat Begruendung Wiki Inkonsistenz", "Begruendung bei Abweichung", # T, V (Spalten zum Loeschen) - # AY (SerpAPI Wiki Search Timestamp) wird ebenfalls geleert, da abhaengig von M. - "SerpAPI Wiki Search Timestamp" # AY (Spalte zum Leeren) + # S, U, M (Pruefkriterien / Daten) + "Chat Wiki Konsistenzpruefung", "Chat Vorschlag Wiki Artikel", "Wiki URL", + # AN, AX, AO, AP (Spalten zum Loeschen) + "Wikipedia Timestamp", "Wiki Verif. Timestamp", "Timestamp letzte Pruefung", "Version", + "ReEval Flag", # A (ReEval Flag setzen) + # N-R (Spalten zum Loeschen) + "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", + # T, V (Spalten zum Loeschen) + "Chat Begruendung Wiki Inkonsistenz", "Begruendung bei Abweichung", + # AY (SerpAPI Wiki Search Timestamp) wird ebenfalls geleert, da + # abhaengig von M. + "SerpAPI Wiki Search Timestamp" # AY (Spalte zum Leeren) ] # Erstellen Sie ein Dictionary mit Schluesseln und Indizes col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} - # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden + # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden + # wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_wiki_updates_from_chatgpt: {missing}. Breche ab.") # <<< GEÄNDERT - return # Beende die Methode bei kritischem Fehler + self.logger.critical( + f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_wiki_updates_from_chatgpt: {missing}. Breche ab.") # <<< GEÄNDERT + return # Beende die Methode bei kritischem Fehler - - # Ermitteln Sie die Spaltenbuchstaben fuer Updates/Leerung (nutzt interne Helfer _get_col_letter Block 14) - s_letter = self.sheet_handler._get_col_letter(col_indices["Chat Wiki Konsistenzpruefung"] + 1) # Status S - u_letter = self.sheet_handler._get_col_letter(col_indices["Chat Vorschlag Wiki Artikel"] + 1) # Vorschlag U - m_letter = self.sheet_handler._get_col_letter(col_indices["Wiki URL"] + 1) # Wiki URL M - a_letter = self.sheet_handler._get_col_letter(col_indices["ReEval Flag"] + 1) # ReEval Flag A + # Ermitteln Sie die Spaltenbuchstaben fuer Updates/Leerung (nutzt + # interne Helfer _get_col_letter Block 14) + s_letter = self.sheet_handler._get_col_letter( + col_indices["Chat Wiki Konsistenzpruefung"] + 1) # Status S + u_letter = self.sheet_handler._get_col_letter( + col_indices["Chat Vorschlag Wiki Artikel"] + 1) # Vorschlag U + m_letter = self.sheet_handler._get_col_letter( + col_indices["Wiki URL"] + 1) # Wiki URL M + a_letter = self.sheet_handler._get_col_letter( + col_indices["ReEval Flag"] + 1) # ReEval Flag A # Spalten N-V leeren. # N ist Wiki Absatz, V ist Begruendung bei Abweichung. @@ -4789,227 +6624,309 @@ class DataProcessor: # Erstellen Sie den Bereichsnamen (z.B. "N:V") n_letter = self.sheet_handler._get_col_letter(n_idx + 1) v_letter = self.sheet_handler._get_col_letter(v_idx + 1) - nv_range_letter = f'{n_letter}:{v_letter}' # z.B. N:V + nv_range_letter = f'{n_letter}:{v_letter}' # z.B. N:V # Erstellen Sie eine Liste von leeren Strings fuer diesen Bereich - empty_nv_values = [''] * (v_idx - n_idx + 1) # Anzahl der Spalten = V_Index - N_Index + 1 - + # Anzahl der Spalten = V_Index - N_Index + 1 + empty_nv_values = [''] * (v_idx - n_idx + 1) # Timestamps AN, AO, AX, AP, AY leeren. - an_letter = self.sheet_handler._get_col_letter(col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS) - ao_letter = self.sheet_handler._get_col_letter(col_indices["Timestamp letzte Pruefung"] + 1) # AO (Chat Evaluation TS) - ap_letter = self.sheet_handler._get_col_letter(col_indices["Version"] + 1) # AP (Version) - ax_letter = self.sheet_handler._get_col_letter(col_indices["Wiki Verif. Timestamp"] + 1) # AX (Wiki Verif. TS) - ay_letter = self.sheet_handler._get_col_letter(col_indices["SerpAPI Wiki Search Timestamp"] + 1) # AY (SerpAPI Wiki TS) - + an_letter = self.sheet_handler._get_col_letter( + col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS) + ao_letter = self.sheet_handler._get_col_letter( + col_indices["Timestamp letzte Pruefung"] + + 1) # AO (Chat Evaluation TS) + ap_letter = self.sheet_handler._get_col_letter( + col_indices["Version"] + 1) # AP (Version) + ax_letter = self.sheet_handler._get_col_letter( + col_indices["Wiki Verif. Timestamp"] + 1) # AX (Wiki Verif. TS) + ay_letter = self.sheet_handler._get_col_letter( + col_indices["SerpAPI Wiki Search Timestamp"] + + 1) # AY (SerpAPI Wiki TS) # --- Verarbeitung --- # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1). update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) + # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) + all_sheet_updates = [] - all_sheet_updates = [] # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts) - - - processed_rows_count = 0 # Zaehlt Zeilen, die geprueft werden (im Rahmen des Limits zaehlen). - skipped_count = 0 # Zaehlt Zeilen, die uebersprungen werden (Status S im Endzustand etc.). + # Zaehlt Zeilen, die geprueft werden (im Rahmen des Limits zaehlen). + processed_rows_count = 0 + # Zaehlt Zeilen, die uebersprungen werden (Status S im Endzustand + # etc.). + skipped_count = 0 updated_url_count = 0 # Zaehlt Zeilen, wo U -> M kopiert wurde. - cleared_suggestion_count = 0 # Zaehlt Zeilen, wo Vorschlag U geloescht wurde. + # Zaehlt Zeilen, wo Vorschlag U geloescht wurde. + cleared_suggestion_count = 0 - - # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte Sheet-Zeilennummer) + # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte + # Sheet-Zeilennummer) for i in range(start_sheet_row, end_sheet_row + 1): - row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste + row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste # Pruefen Sie, ob das Ende des Sheets erreicht wurde - if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht - - - row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile + if row_index_in_list >= total_sheet_rows: + break # Ende des Sheets erreicht + row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile # Stellen Sie sicher, dass die Zeile nicht leer ist - if not any(cell and isinstance(cell, str) and cell.strip() for cell in row): - #self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") # Zu viel Laerm im Debug - skipped_count += 1 # Zaehlen als uebersprungene Zeile - continue # Springe zur naechsten Zeile - + if not any(cell and isinstance(cell, str) and cell.strip() + for cell in row): + # self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") + # # Zu viel Laerm im Debug + skipped_count += 1 # Zaehlen als uebersprungene Zeile + continue # Springe zur naechsten Zeile # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist --- # Kriterium: Status S ist gesetzt (nicht leer) UND NICHT einer der Endzustaende. - # Endzustaende: "OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID SUGGESTION)" + # Endzustaende: "OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID + # SUGGESTION)" - # Holen Sie den Wert aus Spalte S (Chat Wiki Konsistenzpruefung) (nutzt interne Helfer _get_cell_value_safe) - s_value = self._get_cell_value_safe(row, "Chat Wiki Konsistenzpruefung").strip() # Block 1 Column Map + # Holen Sie den Wert aus Spalte S (Chat Wiki Konsistenzpruefung) + # (nutzt interne Helfer _get_cell_value_safe) + s_value = self._get_cell_value_safe( + row, "Chat Wiki Konsistenzpruefung").strip() # Block 1 Column Map s_value_upper = s_value.upper() # Definieren Sie die Endzustaende (Grossbuchstaben) - s_end_states = ["OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID SUGGESTION)"] + s_end_states = [ + "OK", + "X (UPDATED)", + "X (URL COPIED)", + "X (INVALID SUGGESTION)"] - # Verarbeitung ist noetig, wenn S nicht leer ist UND S NICHT im Endzustand ist. + # Verarbeitung ist noetig, wenn S nicht leer ist UND S NICHT im + # Endzustand ist. processing_needed_for_row = s_value and s_value_upper not in s_end_states - # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level - log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) + log_check = ( + i < start_sheet_row + + 5) or ( + i % + 100 == 0) or (processing_needed_for_row) if log_check: - self.logger.debug(f"Zeile {i} (Wiki Update Check): Status S='{s_value}'. Benoetigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT - + self.logger.debug( + f"Zeile {i} (Wiki Update Check): Status S='{s_value}'. Benoetigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist if not processing_needed_for_row: - skipped_count += 1 # Zaehlen als uebersprungene Zeile - continue # Springe zur naechsten Zeile - + skipped_count += 1 # Zaehlen als uebersprungene Zeile + continue # Springe zur naechsten Zeile # --- Wenn Verarbeitung noetig: Pruefe Vorschlag U und handle --- - processed_rows_count += 1 # Zaehle die Zeile, die geprueft wird (im Rahmen des Limits zaehlen). + # Zaehle die Zeile, die geprueft wird (im Rahmen des Limits + # zaehlen). + processed_rows_count += 1 # Pruefe das Limit fuer verarbeitete Zeilen - if limit is not None and isinstance(limit, int) and limit > 0 and processed_rows_count > limit: - # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_wiki_updates_from_chatgpt erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT - break # Brich die Schleife ab + if limit is not None and isinstance( + limit, int) and limit > 0 and processed_rows_count > limit: + # Wenn das Limit erreicht ist und es ein positives Limit gibt + self.logger.info( + f"Verarbeitungslimit ({limit}) fuer process_wiki_updates_from_chatgpt erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT + break # Brich die Schleife ab + # Holen Sie die Werte aus Spalte U (Chat Vorschlag Wiki Artikel) + # und M (Wiki URL) (nutzt interne Helfer _get_cell_value_safe) + vorschlag_u = self._get_cell_value_safe( + row, "Chat Vorschlag Wiki Artikel").strip() # Block 1 Column Map + url_m = self._get_cell_value_safe( + row, "Wiki URL").strip() # Block 1 Column Map - # Holen Sie die Werte aus Spalte U (Chat Vorschlag Wiki Artikel) und M (Wiki URL) (nutzt interne Helfer _get_cell_value_safe) - vorschlag_u = self._get_cell_value_safe(row, "Chat Vorschlag Wiki Artikel").strip() # Block 1 Column Map - url_m = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map + self.logger.info( + f"Zeile {i}: Pruefe Wiki-Vorschlag U='{vorschlag_u[:100]}...' (aktuell M='{url_m[:100]}...')...") # <<< GEÄNDERT + # Flag, ob U eine gueltige, neue URL ist, die uebernommen werden + # soll. + is_update_candidate = False + new_url = "" # Die URL, die ggf. in M kopiert wird. - self.logger.info(f"Zeile {i}: Pruefe Wiki-Vorschlag U='{vorschlag_u[:100]}...' (aktuell M='{url_m[:100]}...')...") # <<< GEÄNDERT - - is_update_candidate = False # Flag, ob U eine gueltige, neue URL ist, die uebernommen werden soll. - new_url = "" # Die URL, die ggf. in M kopiert wird. - - - # Kriterium 1: Ist Vorschlag U ueberhaupt ein String und sieht nach Wikipedia aus? - condition1_u_is_wiki_url = vorschlag_u and isinstance(vorschlag_u, str) and "wikipedia.org/wiki/" in vorschlag_u.lower() and vorschlag_u.lower().startswith(("http://", "https://")) # Check auf Schema hinzugefuegt - + # Kriterium 1: Ist Vorschlag U ueberhaupt ein String und sieht nach + # Wikipedia aus? + condition1_u_is_wiki_url = vorschlag_u and isinstance( + vorschlag_u, + str) and "wikipedia.org/wiki/" in vorschlag_u.lower() and vorschlag_u.lower().startswith( + ("http://", + "https://")) # Check auf Schema hinzugefuegt # Wenn der Vorschlag U wie eine Wikipedia-URL aussieht if condition1_u_is_wiki_url: - new_url = vorschlag_u # Nehme den Vorschlag als potenzielle neue URL + new_url = vorschlag_u # Nehme den Vorschlag als potenzielle neue URL # Kriterium 2: Unterscheidet sich der Vorschlag U von der aktuellen URL in M? - # Pruefe, ob die neue URL nicht identisch mit der aktuellen M-URL ist. + # Pruefe, ob die neue URL nicht identisch mit der aktuellen + # M-URL ist. condition2_u_differs_m = new_url != url_m - # Wenn sich der Vorschlag U von der aktuellen M-URL unterscheidet + # Wenn sich der Vorschlag U von der aktuellen M-URL + # unterscheidet if condition2_u_differs_m: - self.logger.debug(f" -> Vorschlag U ({new_url[:100]}...) unterscheidet sich von M ({url_m[:100]}). Pruefe Validitaet...") + self.logger.debug( + f" -> Vorschlag U ({new_url[:100]}...) unterscheidet sich von M ({url_m[:100]}). Pruefe Validitaet...") try: - # Nutze die globale Funktion 'is_valid_wikipedia_article_url' (definiert in Block 12) - # Diese Funktion ist bereits mit @retry_on_failure dekoriert. - condition3_u_is_valid = is_valid_wikipedia_article_url(new_url, lang=getattr(Config, 'LANG', 'de')) # lang Argument hinzugefügt für Konsistenz + # Nutze die globale Funktion 'is_valid_wikipedia_article_url' (definiert in Block 12) + # Diese Funktion ist bereits mit @retry_on_failure + # dekoriert. + condition3_u_is_valid = is_valid_wikipedia_article_url(new_url, lang=getattr( + Config, 'LANG', 'de')) # lang Argument hinzugefügt für Konsistenz - if condition3_u_is_valid: - is_update_candidate = True - self.logger.debug(f" -> URL '{new_url[:100]}...' ist ein VALIDER Artikel laut API Check.") # <<< GEÄNDERT - else: - # Wenn die vorgeschlagene URL nicht valide ist - self.logger.debug(f" -> URL '{new_url[:100]}...' ist KEIN valider Artikel laut API Check.") # <<< GEÄNDERT + if condition3_u_is_valid: + is_update_candidate = True + self.logger.debug( + f" -> URL '{new_url[:100]}...' ist ein VALIDER Artikel laut API Check.") # <<< GEÄNDERT + else: + # Wenn die vorgeschlagene URL nicht valide ist + self.logger.debug( + f" -> URL '{new_url[:100]}...' ist KEIN valider Artikel laut API Check.") # <<< GEÄNDERT except Exception as e_validity_check: - # Wenn die Validierungsfunktion eine Exception wirft (nach Retries) - # Der Fehler wird bereits vom retry_on_failure Decorator geloggt. - self.logger.error(f"FEHLER bei Validitaetspruefung von Vorschlag U '{new_url[:100]}...': {e_validity_check}") # <<< GEÄNDERT - # Bei Fehler bleibt is_update_candidate False. - pass # Faert fort - + # Wenn die Validierungsfunktion eine Exception wirft (nach Retries) + # Der Fehler wird bereits vom retry_on_failure + # Decorator geloggt. + self.logger.error( + f"FEHLER bei Validitaetspruefung von Vorschlag U '{new_url[:100]}...': {e_validity_check}") # <<< GEÄNDERT + # Bei Fehler bleibt is_update_candidate False. + pass # Faert fort else: - # Wenn der Vorschlag U identisch mit der aktuellen M-URL ist - self.logger.debug(f" -> Vorschlag U ist identisch mit URL M. Wird nicht uebernommen.") # <<< GEÄNDERT + # Wenn der Vorschlag U identisch mit der aktuellen M-URL + # ist + self.logger.debug( + f" -> Vorschlag U ist identisch mit URL M. Wird nicht uebernommen.") # <<< GEÄNDERT else: - # Wenn der Vorschlag U nicht wie eine Wikipedia-URL aussieht - self.logger.debug(f" -> Vorschlag U ('{vorschlag_u[:100]}...') ist keine Wikipedia URL. Wird nicht uebernommen.") # <<< GEÄNDERT - + # Wenn der Vorschlag U nicht wie eine Wikipedia-URL aussieht + self.logger.debug( + f" -> Vorschlag U ('{vorschlag_u[:100]}...') ist keine Wikipedia URL. Wird nicht uebernommen.") # <<< GEÄNDERT # --- Verarbeitung des Kandidaten ODER Loeschen des ungueltigen Vorschlags --- - updates_for_row = [] # Lokale Liste fuer Updates DIESER Zeile + updates_for_row = [] # Lokale Liste fuer Updates DIESER Zeile if is_update_candidate: - # Fall 1: Gueltiges Update durchfuehren (Vorschlag U wird in M kopiert) - self.logger.info(f"Zeile {i}: Update-Kandidat VALIDIERUNG ERFOLGREICH. Kopiere U->M, setze ReEval-Flag 'x', loesche abhaengige Spalten.") # <<< GEÄNDERT - updated_url_count += 1 # Zaehle die uebernommene URL + # Fall 1: Gueltiges Update durchfuehren (Vorschlag U wird in M + # kopiert) + self.logger.info( + f"Zeile {i}: Update-Kandidat VALIDIERUNG ERFOLGREICH. Kopiere U->M, setze ReEval-Flag 'x', loesche abhaengige Spalten.") # <<< GEÄNDERT + updated_url_count += 1 # Zaehle die uebernommene URL - # Updates sammeln (M, S, U, N-V, AN, AO, AP, AX, AY, A) (nutzt interne Helfer _get_col_letter Block 14) - updates_for_row.append({'range': f'{m_letter}{i}', 'values': [[new_url]]}) # Setze die neue URL in Spalte M (Block 1 Column Map) - updates_for_row.append({'range': f'{s_letter}{i}', 'values': [["X (URL Copied)"]]}) # Setze Status S auf "X (URL Copied)" (Block 1 Column Map) - updates_for_row.append({'range': f'{u_letter}{i}', 'values': [["URL uebernommen"]]}) # Schreibe Info in Spalte U (Block 1 Column Map) - updates_for_row.append({'range': f'{a_letter}{i}', 'values': [["x"]]}) # Setze ReEval Flag (A) auf 'x' (Block 1 Column Map) + # Updates sammeln (M, S, U, N-V, AN, AO, AP, AX, AY, A) (nutzt + # interne Helfer _get_col_letter Block 14) + # Setze die neue URL in Spalte M (Block 1 Column Map) + updates_for_row.append( + {'range': f'{m_letter}{i}', 'values': [[new_url]]}) + # Setze Status S auf "X (URL Copied)" (Block 1 Column Map) + updates_for_row.append( + {'range': f'{s_letter}{i}', 'values': [["X (URL Copied)"]]}) + # Schreibe Info in Spalte U (Block 1 Column Map) + updates_for_row.append( + {'range': f'{u_letter}{i}', 'values': [["URL uebernommen"]]}) + # Setze ReEval Flag (A) auf 'x' (Block 1 Column Map) + updates_for_row.append( + {'range': f'{a_letter}{i}', 'values': [["x"]]}) # Leere Spalten N-V. - # Fuege das Update zum Leeren des Bereichs V-Y hinzu, falls der Bereichsname ermittelt werden konnte. - if nv_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte. - updates_for_row.append({'range': f'{n_letter}{i}:{v_letter}{i}', 'values': [empty_nv_values]}) # Block 1 Column Map, lokale Variable + # Fuege das Update zum Leeren des Bereichs V-Y hinzu, falls der + # Bereichsname ermittelt werden konnte. + if nv_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte. + updates_for_row.append({'range': f'{n_letter}{i}:{v_letter}{i}', 'values': [ + empty_nv_values]}) # Block 1 Column Map, lokale Variable else: - self.logger.warning(f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.") # <<< GEÄNDERT - + self.logger.warning( + f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.") # <<< GEÄNDERT # Leere Timestamps AN, AO, AP, AX, AY. - # Dies setzt die Zeile zurueck, damit andere Schritte sie spaeter bearbeiten. - updates_for_row.append({'range': f'{an_letter}{i}', 'values': [['']]}) # AN (Wiki Extraction TS) Block 1 Column Map - updates_for_row.append({'range': f'{ao_letter}{i}', 'values': [['']]}) # AO (Chat Evaluation TS) Block 1 Column Map - updates_for_row.append({'range': f'{ap_letter}{i}', 'values': [['']]}) # AP (Version) Block 1 Column Map - updates_for_row.append({'range': f'{ax_letter}{i}', 'values': [['']]}) # AX (Wiki Verif. TS) Block 1 Column Map - updates_for_row.append({'range': f'{ay_letter}{i}', 'values': [['']]}) # AY (SerpAPI Wiki TS) Block 1 Column Map - + # Dies setzt die Zeile zurueck, damit andere Schritte sie + # spaeter bearbeiten. + # AN (Wiki Extraction TS) Block 1 Column Map + updates_for_row.append( + {'range': f'{an_letter}{i}', 'values': [['']]}) + # AO (Chat Evaluation TS) Block 1 Column Map + updates_for_row.append( + {'range': f'{ao_letter}{i}', 'values': [['']]}) + # AP (Version) Block 1 Column Map + updates_for_row.append( + {'range': f'{ap_letter}{i}', 'values': [['']]}) + # AX (Wiki Verif. TS) Block 1 Column Map + updates_for_row.append( + {'range': f'{ax_letter}{i}', 'values': [['']]}) + # AY (SerpAPI Wiki TS) Block 1 Column Map + updates_for_row.append( + {'range': f'{ay_letter}{i}', 'values': [['']]}) else: # Fall 2: Ungueltigen Vorschlag loeschen/markieren - # Wenn der Vorschlag U nicht uebernommen wird (weil ungueltig oder identisch mit M). - self.logger.info(f"Zeile {i}: Vorschlag U ('{vorschlag_u[:100]}...') ist ungueltig/identisch. Loesche U und setze Status S auf 'X (Invalid Suggestion)'.") # <<< GEÄNDERT - cleared_suggestion_count += 1 # Zaehle den bereinigten Vorschlag + # Wenn der Vorschlag U nicht uebernommen wird (weil ungueltig + # oder identisch mit M). + self.logger.info( + f"Zeile {i}: Vorschlag U ('{vorschlag_u[:100]}...') ist ungueltig/identisch. Loesche U und setze Status S auf 'X (Invalid Suggestion)'.") # <<< GEÄNDERT + cleared_suggestion_count += 1 # Zaehle den bereinigten Vorschlag - # Updates sammeln (S, U) (nutzt interne Helfer _get_col_letter Block 14) - updates_for_row.append({'range': f'{s_letter}{i}', 'values': [["X (Invalid Suggestion)"]]}) # Setze Status S auf "X (Invalid Suggestion)" (Block 1 Column Map) - updates_for_row.append({'range': f'{u_letter}{i}', 'values': [[""]]}) # Loesche den Vorschlag in Spalte U (Block 1 Column Map) + # Updates sammeln (S, U) (nutzt interne Helfer _get_col_letter + # Block 14) + # Setze Status S auf "X (Invalid Suggestion)" (Block 1 Column + # Map) + updates_for_row.append( + {'range': f'{s_letter}{i}', 'values': [["X (Invalid Suggestion)"]]}) + # Loesche den Vorschlag in Spalte U (Block 1 Column Map) + updates_for_row.append( + {'range': f'{u_letter}{i}', 'values': [[""]]}) # KEIN ReEval-Flag (A) setzen in diesem Fall. - - # Sammle die Updates fuer diese Zeile in der globalen Liste all_sheet_updates. + # Sammle die Updates fuer diese Zeile in der globalen Liste + # all_sheet_updates. all_sheet_updates.extend(updates_for_row) - # Sende gesammelte Sheet Updates wenn das Update-Batch-Limit erreicht ist. # update_batch_row_limit wird aus Config geholt (Block 1). # Die Anzahl der Updates pro Zeile variiert stark (ca. 2 bei ungueltigem Vorschlag, ca. 10+ bei gueltigem). # Pruefen Sie einfach die Laenge der gesammelten Liste. - if len(all_sheet_updates) >= update_batch_row_limit * 5: # Grobe Schaetzung: im Schnitt 5 Updates pro Zeile - self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. - # Wenn es fehlschlaegt, wird es intern geloggt. - success = self.sheet_handler.batch_update_cells(all_sheet_updates) - if success: - self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") # <<< GEÄNDERT - # Der Fehlerfall wird von batch_update_cells geloggt - - # Leere die gesammelten Updates nach dem Senden. - all_sheet_updates = [] + if len(all_sheet_updates) >= update_batch_row_limit * \ + 5: # Grobe Schaetzung: im Schnitt 5 Updates pro Zeile + self.logger.debug( + f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT + # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. + # Wenn es fehlschlaegt, wird es intern geloggt. + success = self.sheet_handler.batch_update_cells( + all_sheet_updates) + if success: + self.logger.info( + f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") # <<< GEÄNDERT + # Der Fehlerfall wird von batch_update_cells geloggt + # Leere die gesammelten Updates nach dem Senden. + all_sheet_updates = [] # Kleine Pause nach jeder geprueften Zeile (nutzt Config Block 1). - # Dieser Modus macht API calls (ueber is_valid_wikipedia_article_url), also Pause einbauen. + # Dieser Modus macht API calls (ueber + # is_valid_wikipedia_article_url), also Pause einbauen. pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.2 - #self.logger.debug(f"Warte {pause_duration:.2f}s nach Pruefung...") # Zu viel Laerm im Debug + # self.logger.debug(f"Warte {pause_duration:.2f}s nach + # Pruefung...") # Zu viel Laerm im Debug time.sleep(pause_duration) - # --- Finale Sheet Updates senden --- - # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update. + # Sende alle verbleibenden gesammelten Updates in einem letzten + # Batch-Update. if all_sheet_updates: - self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT - # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry. + self.logger.info( + f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT + # Nutzt die batch_update_cells Methode des Sheet Handlers (Block + # 14) mit Retry. success = self.sheet_handler.batch_update_cells(all_sheet_updates) if success: - self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT + # <<< GEÄNDERT + self.logger.info(f"FINALES Sheet-Update erfolgreich.") # Der Fehlerfall wird von batch_update_cells geloggt - # Logge den Abschluss des Modus - self.logger.info(f"Modus 'wiki_updates_from_chatgpt' abgeschlossen. {processed_rows_count} Zeilen geprueft, {updated_url_count} URLs kopiert & fuer ReEval markiert, {cleared_suggestion_count} ungueltige Vorschlaege geloescht/markiert, {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT - - def process_wiki_reextract_missing_an(self, start_sheet_row=None, end_sheet_row=None, limit=None): + self.logger.info( + f"Modus 'wiki_updates_from_chatgpt' abgeschlossen. {processed_rows_count} Zeilen geprueft, {updated_url_count} URLs kopiert & fuer ReEval markiert, {cleared_suggestion_count} ungueltige Vorschlaege geloescht/markiert, {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT + + def process_wiki_reextract_missing_an( + self, + start_sheet_row=None, + end_sheet_row=None, + limit=None): """ Startet eine erneute Wiki-Extraktion für Zeilen mit URL aber ohne Timestamp. """ @@ -5026,105 +6943,128 @@ class DataProcessor: """ # Verwenden Sie logger, da das Logging jetzt konfiguriert ist # Logge die Konfiguration des Modus - self.logger.info(f"Starte Modus 'wiki_reextract_missing_an' (M gefuellt & AN leer). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT - + self.logger.info( + f"Starte Modus 'wiki_reextract_missing_an' (M gefuellt & AN leer). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT # --- Daten laden und Startzeile ermitteln --- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt. # Dieser Modus sucht nach leeren AN mit gefuelltem M. Die automatische Startzeile # basierend auf leeren AN ist ein guter Startpunkt. if start_sheet_row is None: - self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AN...") # <<< GEÄNDERT - # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AN (Block 1 Column Map). - # Standardmaessig ab Zeile 7 - start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wikipedia Timestamp", min_sheet_row=7) + self.logger.info( + "Automatische Ermittlung der Startzeile basierend auf leeren AN...") # <<< GEÄNDERT + # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AN (Block 1 Column Map). + # Standardmaessig ab Zeile 7 + start_data_index_no_header = self.sheet_handler.get_start_row_index( + check_column_key="Wikipedia Timestamp", min_sheet_row=7) - # Wenn get_start_row_index -1 zurueckgibt (Fehler) - if start_data_index_no_header == -1: - self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Modus ab.") # <<< GEÄNDERT - return # Beende die Methode + # Wenn get_start_row_index -1 zurueckgibt (Fehler) + if start_data_index_no_header == -1: + self.logger.error( + "FEHLER bei automatischer Ermittlung der Startzeile. Breche Modus ab.") # <<< GEÄNDERT + return # Beende die Methode - # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index - start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut - self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AN Zelle): {start_sheet_row}") # <<< GEÄNDERT + # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten + # Daten-Index + start_sheet_row = start_data_index_no_header + \ + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut + self.logger.info( + f"Automatisch ermittelte Startzeile (erste leere AN Zelle): {start_sheet_row}") # <<< GEÄNDERT else: # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein. - # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2). + # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block + # 2). if not self.sheet_handler.load_data(): - self.logger.error("Fehler beim Laden der Daten fuer wiki_reextract_missing_an.") # <<< GEÄNDERT - return # Beende die Methode, wenn das Laden fehlschlaegt - - - # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler. - all_data = self.sheet_handler.get_all_data_with_headers(); - # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14). - header_rows = self.sheet_handler._header_rows; - total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet + self.logger.error( + "Fehler beim Laden der Daten fuer wiki_reextract_missing_an.") # <<< GEÄNDERT + return # Beende die Methode, wenn das Laden fehlschlaegt + # Holen Sie die gesamte Datenliste (inklusive Header) aus dem + # SheetHandler. + all_data = self.sheet_handler.get_all_data_with_headers() + # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar + # (Block 14). + header_rows = self.sheet_handler._header_rows + total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet # Berechne Endzeile, wenn nicht manuell gesetzt if end_sheet_row is None: - end_sheet_row = total_sheet_rows # Bis zur letzten Zeile - + end_sheet_row = total_sheet_rows # Bis zur letzten Zeile # Logge den verarbeitungsbereich - self.logger.info(f"Suchbereich fuer M gefuellt & AN leer: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT + self.logger.info( + f"Suchbereich fuer M gefuellt & AN leer: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT - # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen) + # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht + # ueber Gesamtzeilen) if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: - self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT - return # Beende die Methode, wenn der Bereich leer ist - + self.logger.info( + "Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT + return # Beende die Methode, wenn der Bereich leer ist # --- Indizes --- - # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind - required_keys = ["Wiki URL", "Wikipedia Timestamp", "CRM Name"] # M, AN, B (Pruefkriterien + Logging) + # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP + # (Block 1) vorhanden sind + required_keys = ["Wiki URL", "Wikipedia Timestamp", + "CRM Name"] # M, AN, B (Pruefkriterien + Logging) # Erstellen Sie ein Dictionary mit Schluesseln und Indizes col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} - # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden + # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden + # wurden if None in col_indices.values(): missing = [k for k, v in col_indices.items() if v is None] - self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer wiki_reextract_missing_an: {missing}. Breche ab.") # <<< GEÄNDERT - return # Beende die Methode bei kritischem Fehler + self.logger.critical( + f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer wiki_reextract_missing_an: {missing}. Breche ab.") # <<< GEÄNDERT + return # Beende die Methode bei kritischem Fehler # Ermitteln Sie die Indizes m_col_idx = col_indices["Wiki URL"] an_col_idx = col_indices["Wikipedia Timestamp"] - # --- Verarbeitung --- - processed_count = 0 # Zaehlt Zeilen, die an _process_single_row uebergeben wurden (im Rahmen des Limits zaehlen). - skipped_count = 0 # Zaehlt Zeilen, die uebersprungen wurden. + # Zaehlt Zeilen, die an _process_single_row uebergeben wurden (im + # Rahmen des Limits zaehlen). + processed_count = 0 + skipped_count = 0 # Zaehlt Zeilen, die uebersprungen wurden. - - # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte Sheet-Zeilennummer) + # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte + # Sheet-Zeilennummer) for i in range(start_sheet_row, end_sheet_row + 1): - row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste + row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste # Pruefen Sie, ob das Ende des Sheets erreicht wurde - if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht - - - row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile + if row_index_in_list >= total_sheet_rows: + break # Ende des Sheets erreicht + row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile # Stellen Sie sicher, dass die Zeile nicht leer ist - if not any(cell and isinstance(cell, str) and cell.strip() for cell in row): - #self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") # Zu viel Laerm im Debug - skipped_count += 1 # Zaehlen als uebersprungene Zeile - continue # Springe zur naechsten Zeile - + if not any(cell and isinstance(cell, str) and cell.strip() + for cell in row): + # self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") + # # Zu viel Laerm im Debug + skipped_count += 1 # Zaehlen als uebersprungene Zeile + continue # Springe zur naechsten Zeile # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist --- # Kriterium: Wiki URL (M) ist vorhanden und gueltig aussehend. # UND Wikipedia Timestamp (AN) ist leer. - # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne Helfer _get_cell_value_safe) - m_value = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map - an_value = self._get_cell_value_safe(row, "Wikipedia Timestamp").strip() # Block 1 Column Map + # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne + # Helfer _get_cell_value_safe) + m_value = self._get_cell_value_safe( + row, "Wiki URL").strip() # Block 1 Column Map + an_value = self._get_cell_value_safe( + row, "Wikipedia Timestamp").strip() # Block 1 Column Map # Pruefen Sie, ob M gefuellt und gueltig aussieht. - is_m_valid_looking = m_value and isinstance(m_value, str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu basierend auf Log + is_m_valid_looking = m_value and isinstance( + m_value, + str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in [ + "k.a.", + "kein artikel gefunden", + "fehler bei suche", + "http:"] # Fuege "http:" hinzu basierend auf Log # Pruefen Sie, ob AN leer ist. is_an_empty = not an_value @@ -5132,58 +7072,72 @@ class DataProcessor: # Verarbeitung ist noetig, wenn M gueltig aussieht UND AN leer ist. processing_needed_for_row = is_m_valid_looking and is_an_empty - # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level - log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row) + log_check = ( + i < start_sheet_row + + 5) or ( + i % + 100 == 0) or (processing_needed_for_row) if log_check: - company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map - self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Re-extract Check): M ('{m_value[:50]}...') gueltig? {is_m_valid_looking}, AN leer? {is_an_empty}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT - + company_name = self._get_cell_value_safe( + row, "CRM Name").strip() # Block 1 Column Map + self.logger.debug( + f"Zeile {i} ({company_name[:50]}... Wiki Re-extract Check): M ('{m_value[:50]}...') gueltig? {is_m_valid_looking}, AN leer? {is_an_empty}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist if not processing_needed_for_row: - skipped_count += 1 # Zaehlen als uebersprungene Zeile - continue # Springe zur naechsten Zeile - + skipped_count += 1 # Zaehlen als uebersprungene Zeile + continue # Springe zur naechsten Zeile # --- Wenn Verarbeitung noetig: Rufe _process_single_row auf --- - processed_count += 1 # Zaehle die Zeile, die an _process_single_row uebergeben wird (im Rahmen des Limits zaehlen) + # Zaehle die Zeile, die an _process_single_row uebergeben wird (im + # Rahmen des Limits zaehlen) + processed_count += 1 # Pruefe das Limit fuer verarbeitete Zeilen - if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: - # Wenn das Limit erreicht ist und es ein positives Limit gibt - self.logger.info(f"Verarbeitungslimit ({limit}) fuer wiki_reextract_missing_an erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT - break # Brich die Schleife ab + if limit is not None and isinstance( + limit, int) and limit > 0 and processed_count > limit: + # Wenn das Limit erreicht ist und es ein positives Limit gibt + self.logger.info( + f"Verarbeitungslimit ({limit}) fuer wiki_reextract_missing_an erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT + break # Brich die Schleife ab - - self.logger.info(f"Zeile {i}: M gefuellt & AN leer. Versuche Wiki-Re-Extraktion ueber _process_single_row...") # <<< GEÄNDERT + self.logger.info( + f"Zeile {i}: M gefuellt & AN leer. Versuche Wiki-Re-Extraktion ueber _process_single_row...") # <<< GEÄNDERT try: # RUFE _process_single_row AUF (Block 19). # Mit steps_to_run={'wiki'} und force_reeval=True, # damit nur der Wiki-Schritt ausgefuehrt wird und Timestamps ignoriert werden. - # Im Re-Extract Modus loeschen wir das 'x'-Flag NICHT automatisch. + # Im Re-Extract Modus loeschen wir das 'x'-Flag NICHT + # automatisch. self._process_single_row( - row_num_in_sheet = i, - row_data = row, # Uebergibt die aktuellen Rohdaten der Zeile - steps_to_run = {'wiki'}, # <<< NUR der Wiki-Schritt soll laufen - force_reeval = True, # <<< Erzwingt die Ausfuehrung des 'wiki' Schritts (ignoriert AN, S). - clear_x_flag = False # <<< 'x'-Flag wird in diesem Modus NICHT geloescht + row_num_in_sheet=i, + row_data=row, # Uebergibt die aktuellen Rohdaten der Zeile + steps_to_run={'wiki'}, + # <<< NUR der Wiki-Schritt soll laufen + force_reeval=True, + # <<< Erzwingt die Ausfuehrung des 'wiki' Schritts (ignoriert AN, S). + clear_x_flag=False # <<< 'x'-Flag wird in diesem Modus NICHT geloescht ) - # _process_single_row (Block 19) loggt intern den Abschluss und fuehrt das Sheet-Update durch. + # _process_single_row (Block 19) loggt intern den Abschluss und + # fuehrt das Sheet-Update durch. except Exception as e_proc: # Wenn _process_single_row einen Fehler wirft (nachdem interne Retries aufgaben), - # fangen wir ihn hier, loggen ihn und fahren mit der naechsten Zeile fort. - self.logger.exception(f"FEHLER bei Verarbeitung von Zeile {i} in wiki_reextract_missing_an: {e_proc}") # <<< GEÄNDERT + # fangen wir ihn hier, loggen ihn und fahren mit der naechsten + # Zeile fort. + self.logger.exception( + f"FEHLER bei Verarbeitung von Zeile {i} in wiki_reextract_missing_an: {e_proc}") # <<< GEÄNDERT # Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte im Sheet schreiben lassen. - # Dieses Update muesste dann separat oder im naechsten Lauf behandelt werden. + # Dieses Update muesste dann separat oder im naechsten Lauf + # behandelt werden. # _process_single_row beinhaltet bereits eine kleine Pause am Ende. # Hier ist keine zusaetzliche Pause noetig, wenn _process_single_row erfolgreich war. # Wenn _process_single_row eine Exception wirft, kann hier eine kurze Pause sinnvoll sein. # time.sleep(0.1) # Optional: Kurze Pause bei Fehler nach Exception - # Logge den Abschluss des Modus - self.logger.info(f"Modus 'wiki_reextract_missing_an' abgeschlossen. {processed_count} Zeilen an _process_single_row uebergeben, {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT + self.logger.info( + f"Modus 'wiki_reextract_missing_an' abgeschlossen. {processed_count} Zeilen an _process_single_row uebergeben, {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT