soup.find(class_='content') # Oder
# Füge ggf. weitere spezifische Selektoren hinzu, die du oft siehst ) if content_area: debug_print(f"Gezielten Inhaltsbereich gefunden ({content_area.name}#{content_area.get('id')} oder .{content_area.get('class')}) für {url}") else: # --- Fallback: Body nehmen, ABER Banner versuchen zu entfernen --- debug_print(f"Kein spezifischer Inhaltsbereich gefunden für {url}. Nutze Body und versuche Banner zu entfernen.") content_area = soup.find('body') if content_area: # Versuche, häufige Cookie-Banner Strukturen zu entfernen banner_selectors = [ '[id*="cookie"]', # IDs die "cookie" enthalten '[class*="cookie"]', # Klassen die "cookie" enthalten '[id*="consent"]', # IDs die "consent" enthalten '[class*="consent"]', # Klassen die "consent" enthalten '[id*="banner"]', # IDs die "banner" enthalten (vorsichtig!) '[class*="banner"]', # Klassen die "banner" enthalten (vorsichtig!) '[role="dialog"]' # Oft für Popups/Banner genutzt (vorsichtig!) ] banners_removed_count = 0 for selector in banner_selectors: try: potential_banners = content_area.select(selector) for banner in potential_banners: # Zusätzliche Prüfung: Enthält das Element typischen Banner-Text? banner_text = banner.get_text(" ", strip=True).lower() keywords = ["cookie", "zustimm", "ablehnen", "einverstanden", "datenschutz", "privacy", "akzeptier"] if any(keyword in banner_text for keyword in keywords): debug_print(f"Entferne potenzielles Banner ({selector}) mit Text: {banner_text[:100]}...") banner.decompose() # Entferne das Element aus dem Baum banners_removed_count += 1 except Exception as e_select: debug_print(f"Fehler beim Versuch Banner mit Selektor '{selector}' zu entfernen: {e_select}") if banners_removed_count > 0: debug_print(f"{banners_removed_count} potenzielle Banner-Elemente entfernt.") # --- Text extrahieren aus gefundenem Bereich (oder Body) --- if content_area: for script_or_style in content_area(["script", "style"]): # Skripte/Styles entfernen script_or_style.decompose() text = content_area.get_text(separator=' ', strip=True) text = re.sub(r'\s+', ' ', text) # Normalisiere Whitespace # --- Zusätzliche Prüfung: Ist der extrahierte Text *nur* Banner-Text? --- banner_keywords_strict = ["cookie", "zustimmen", "ablehnen", "einverstanden", "datenschutz", "privacy", "akzeptier", "einstellung", "partner", "analyse", "marketing"] text_lower = text.lower() keyword_hits = sum(1 for keyword in banner_keywords_strict if keyword in text_lower) # Heuristik: Wenn der Text kurz ist UND viele Banner-Keywords enthält -> Verwerfen if len(text) < 500 and keyword_hits >= 3: debug_print(f"WARNUNG: Extrahierter Text für {url} scheint nur Cookie-Banner zu sein (Länge {len(text)}, {keyword_hits} Keywords). Verwerfe Text.") return "k.A. (Nur Cookie-Banner erkannt)" result = text[:max_length] debug_print(f"Website {url} erfolgreich gescrapt. Extrahierter Text (Länge {len(result)}): {result[:100]}...") return result else: debug_print(f"Kein oder spezifischer Inhaltsbereich gefunden in {url}") return "k.A." except requests.exceptions.SSLError as e: debug_print(f"SSL-Fehler beim Abrufen der Website {url}: {e}. Versuche ohne Zertifikatsprüfung...") if verify_cert: return get_website_raw(url, max_length, verify_cert=False) else: return "k.A." except requests.exceptions.RequestException as e: debug_print(f"Netzwerk-/HTTP-Fehler beim Abrufen der Website {url}: {e}") return "k.A." except Exception as e: debug_print(f"Allgemeiner Fehler beim Scraping von {url}: {e}") return "k.A." @retry_on_failure def scrape_website_details(url): """Extrahiert Title, Description, H1-H3 von einer Website.""" if not url or not isinstance(url, str) or url.strip().lower() == 'k.a.': return "k.A." if not url.lower().startswith("http"): url = "https://" + url headers = {"User-Agent": "Mozilla/5.0"} try: response = requests.get(url, timeout=10, headers=headers, verify=False) # Oft nötig bei vielen Seiten response.raise_for_status() response.encoding = response.apparent_encoding soup = BeautifulSoup(response.text, Config.HTML_PARSER) # Title title_tag = soup.find("title") title = clean_text(title_tag.get_text()) if title_tag else "k.A." # Description meta_tag = soup.find("meta", attrs={"name": lambda x: x and x.lower() == "description"}) description = clean_text(meta_tag["content"]) if meta_tag and meta_tag.get("content") else "k.A." # Headers H1-H3 headers_data = {} for tag in ["h1", "h2", "h3"]: elements = soup.find_all(tag) header_texts = [clean_text(el.get_text()) for el in elements] header_texts = [h for h in header_texts if h != "k.A." and len(h) > 2] # Filtern headers_data[tag] = ", ".join(header_texts[:5]) if header_texts else "k.A." # Max 5 pro Typ combined = ( f"Title: {title} | Description: {description} | " f"H1: {headers_data['h1']} | H2: {headers_data['h2']} | H3: {headers_data['h3']}" ) # Kürze ggf. das Gesamtergebnis return combined[:1500] # Limit Gesamtstring except requests.exceptions.RequestException as e: debug_print(f"Netzwerk-/HTTP-Fehler beim Detail-Scraping von {url}: {e}") return "k.A." except Exception as e: debug_print(f"Allgemeiner Fehler beim Detail-Scraping von {url}: {e}") return "k.A." # ==================== OPENAI / CHATGPT FUNCTIONS ==================== @retry_on_failure def call_openai_chat(prompt, temperature=0.3, model=None): """Zentrale Funktion für OpenAI Chat API Aufrufe.""" if not Config.API_KEYS.get('openai'): debug_print("Fehler: OpenAI API Key nicht konfiguriert.") return None if not prompt: debug_print("Fehler: Leerer Prompt für OpenAI.") return None current_model = model if model else Config.TOKEN_MODEL try: # Token zählen vor dem Senden (optional, aber gut für Debugging) # prompt_tokens = token_count(prompt) # debug_print(f"Sende Prompt an OpenAI ({current_model}, {prompt_tokens} Tokens)...") response = openai.ChatCompletion.create( model=current_model, messages=[{"role": "user", "content": prompt}], temperature=temperature ) result = response.choices[0].message.content.strip() # Token zählen für die Antwort # completion_tokens = token_count(result) # total_tokens = response.usage.total_tokens # debug_print(f"OpenAI Antwort erhalten ({completion_tokens} Completion Tokens, {total_tokens} Gesamt).") return result except openai.error.InvalidRequestError as e: debug_print(f"OpenAI Invalid Request Error: {e}") # Hier könnte man prüfen, ob es am Token Limit liegt if "maximum context length" in str(e): debug_print("Fehler scheint Token Limit zu sein. Prompt evtl. zu lang.") # TODO: Strategie für zu lange Prompts (kürzen, splitten?) return None except openai.error.OpenAIError as e: # Fängt RateLimitError, APIError etc. ab debug_print(f"OpenAI API Fehler: {e}") raise # Fehler weitergeben, damit retry_on_failure greifen kann except Exception as e: debug_print(f"Allgemeiner Fehler bei OpenAI-Aufruf: {e}") raise # Fehler weitergeben def summarize_website_content(raw_text): """Erstellt Zusammenfassung von Website-Rohtext via OpenAI.""" if not raw_text or raw_text == "k.A." or raw_text.strip() == "": return "k.A." # Kürze den Rohtext, falls er sehr lang ist, um Token zu sparen/Limits zu vermeiden max_raw_length = 3000 # Zeichenlimit für den Input der Zusammenfassung if len(raw_text) > max_raw_length: debug_print(f"Kürze Rohtext für Zusammenfassung von {len(raw_text)} auf {max_raw_length} Zeichen.") raw_text = raw_text[:max_raw_length] prompt = ( "Du bist ein KI-Assistent, der Webinhalte analysiert.\n" "Fasse den folgenden Text einer Unternehmenswebsite prägnant zusammen. " "Konzentriere dich auf:\n" "- Haupttätigkeitsfeld des Unternehmens\n" "- Wichtigste Produkte und/oder Dienstleistungen\n" "- Zielgruppe (falls erkennbar)\n\n" f"Website-Text:\n```\n{raw_text}\n```\n\n" "Zusammenfassung (max. 100 Wörter):" ) summary = call_openai_chat(prompt, temperature=0.2) return summary if summary else "k.A." def evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kategorien, website_summary): """ Ordnet das Unternehmen basierend auf den angegebenen Informationen exakt einer Branche aus dem Ziel-Branchenschema (nur Kurzformen) zu. Validiert den ChatGPT-Vorschlag strikt gegen die erlaubten Kurzformen und führt einen Fallback auf die (extrahierte) CRM-Kurzform durch, falls der Vorschlag ungültig ist. Args: crm_branche (str): Branche laut CRM (kann noch Präfix enthalten). beschreibung (str): Unternehmensbeschreibung (CRM). wiki_branche (str): Branche aus Wikipedia (falls vorhanden). wiki_kategorien (str): Wikipedia-Kategorien. website_summary (str): Zusammenfassung des Website-Inhalts. Returns: dict: Enthält "branch" (die finale, gültige Kurzform oder Fehler), "consistency" ('ok', 'X', 'fallback_crm_valid', 'fallback_invalid') und "justification" (Begründung von ChatGPT oder Fallback-Info). """ # Globale Variablen für Schema und erlaubte Branches verwenden global ALLOWED_TARGET_BRANCHES, TARGET_SCHEMA_STRING # Grundlegende Prüfung: Ist das Schema überhaupt geladen? if not ALLOWED_TARGET_BRANCHES: debug_print("FEHLER in evaluate_branche_chatgpt: Ziel-Branchenschema (ALLOWED_TARGET_BRANCHES) ist leer. Abbruch.") # Gib den CRM-Wert zurück, aber markiere als Fehler return {"branch": crm_branche, "consistency": "error_schema_missing", "justification": "Fehler: Ziel-Schema nicht geladen"} # Erstelle ein Set/Dict der erlaubten Branches in Kleinbuchstaben für effizientes Nachschlagen # Speichert die Originalschreibweise als Wert. allowed_branches_lookup = {b.lower(): b for b in ALLOWED_TARGET_BRANCHES} # --- Prompt für ChatGPT erstellen --- # Beginne mit den Regeln und der Liste der gültigen Kurzformen prompt_parts = [TARGET_SCHEMA_STRING] # TARGET_SCHEMA_STRING sollte bereits die klare Anweisung enthalten prompt_parts.append("\nOrdne das Unternehmen anhand folgender Angaben exakt einer Branche des Ziel-Branchenschemas (Kurzformen) zu:") # Füge nur vorhandene Informationen hinzu und kürze sie ggf. if crm_branche and crm_branche != "k.A.": prompt_parts.append(f"- CRM-Branche (Referenz): {crm_branche}") if beschreibung and beschreibung != "k.A.": prompt_parts.append(f"- Beschreibung: {beschreibung[:500]}") # Kürzen if wiki_branche and wiki_branche != "k.A.": prompt_parts.append(f"- Wikipedia-Branche: {wiki_branche}") if wiki_kategorien and wiki_kategorien != "k.A.": prompt_parts.append(f"- Wikipedia-Kategorien: {wiki_kategorien[:500]}") # Kürzen if website_summary and website_summary != "k.A.": prompt_parts.append(f"- Website-Zusammenfassung: {website_summary[:500]}") # Kürzen # Fallback, wenn gar keine spezifischen Infos da sind if len(prompt_parts) <= 2: debug_print("Warnung in evaluate_branche_chatgpt: Zu wenige Informationen für Branchenevaluierung.") return {"branch": crm_branche, "consistency": "error_no_info", "justification": "Fehler: Zu wenige Informationen für eine Einschätzung"} # Füge die strengen Anweisungen für das Antwortformat hinzu prompt_parts.append("\nWICHTIG: Antworte NUR mit dem exakten Kurznamen einer Branche aus der obigen Liste. Verwende KEINE Präfixe wie 'Hersteller / Produzenten >' oder 'Service provider (Dienstleister) >'.") prompt_parts.append("\nAntworte ausschließlich im folgenden Format (keine Einleitung, kein Schlusssatz):") prompt_parts.append("Branche: ") prompt_parts.append("Übereinstimmung: ") prompt_parts.append("Begründung: ") prompt = "\n".join(prompt_parts) # --- ChatGPT aufrufen --- chat_response = call_openai_chat(prompt, temperature=0.0) # Niedrige Temperatur für konsistente Zuordnung if not chat_response: debug_print("Fehler in evaluate_branche_chatgpt: Keine Antwort von OpenAI erhalten.") return {"branch": crm_branche, "consistency": "error_api_no_response", "justification": "Fehler: Keine Antwort von API"} # --- Antwort parsen --- lines = chat_response.strip().split("\n") result = {"branch": None, "consistency": None, "justification": ""} # Initialisiere mit None suggested_branch = "" for line in lines: line_lower = line.lower() if line_lower.startswith("branche:"): suggested_branch = line.split(":", 1)[1].strip() # Entferne mögliche Anführungszeichen suggested_branch = suggested_branch.strip('"\'') elif line_lower.startswith("übereinstimmung:"): # Wir überschreiben die Konsistenz später basierend auf unserer Logik pass elif line_lower.startswith("begründung:"): result["justification"] = line.split(":", 1)[1].strip() if not suggested_branch: debug_print(f"Fehler in evaluate_branche_chatgpt: Konnte 'Branche:' nicht aus Antwort parsen: {chat_response}") # Optional: Versuche Begründung als Branche zu nehmen? Eher nicht. return {"branch": crm_branche, "consistency": "error_parsing", "justification": f"Fehler: Parsing der API Antwort fehlgeschlagen. Antwort: {chat_response}"} # --- Validierung des ChatGPT-Vorschlags --- final_branch = None suggested_branch_lower = suggested_branch.lower() if suggested_branch_lower in allowed_branches_lookup: final_branch = allowed_branches_lookup[suggested_branch_lower] # Nimm korrekte Schreibweise debug_print(f"ChatGPT-Branchenvorschlag '{suggested_branch}' ist gültig ('{final_branch}').") # Konsistenz wird später gesetzt result["consistency"] = "pending_comparison" # Temporärer Status else: # --- Fallback-Logik --- debug_print(f"ChatGPT-Branchenvorschlag '{suggested_branch}' ist NICHT im Ziel-Schema ({len(ALLOWED_TARGET_BRANCHES)} Einträge) enthalten. Starte Fallback...") # Versuche Kurzform aus CRM-Branche zu extrahieren crm_short_branch = "k.A." if crm_branche and ">" in crm_branche: crm_short_branch = crm_branche.split(">", 1)[1].strip() elif crm_branche and crm_branche != "k.A.": # Wenn CRM schon Kurzform sein könnte crm_short_branch = crm_branche.strip() # Prüfe, ob die extrahierte CRM-Kurzform gültig ist if crm_short_branch != "k.A." and crm_short_branch.lower() in allowed_branches_lookup: final_branch = allowed_branches_lookup[crm_short_branch.lower()] # Nimm korrekte Schreibweise result["consistency"] = "fallback_crm_valid" # Setze Fallback-Status # Kombiniere ChatGPT Begründung (falls vorhanden) mit Fallback-Info fallback_reason = f"Fallback: Ungültiger ChatGPT-Vorschlag ('{suggested_branch}'). Gültige CRM-Kurzform '{final_branch}' verwendet." result["justification"] = f"{fallback_reason} (ChatGPT Begründung war: {result.get('justification', 'Keine')})" debug_print(f"Fallback auf gültige CRM-Kurzform erfolgreich: '{final_branch}'") else: # Wenn auch CRM-Kurzform ungültig oder nicht extrahierbar final_branch = suggested_branch # Behalte ungültigen Vorschlag result["consistency"] = "fallback_invalid" # Setze Fehler-Fallback-Status error_reason = f"Fehler: Ungültiger ChatGPT-Vorschlag ('{suggested_branch}') und keine gültige CRM-Kurzform ('{crm_short_branch}') als Fallback verfügbar." result["justification"] = f"{error_reason} (ChatGPT Begründung war: {result.get('justification', 'Keine')})" debug_print(f"Fallback fehlgeschlagen. Ungültiger Vorschlag: '{final_branch}', Ungültige CRM-Kurzform: '{crm_short_branch}'") # Alternativ: Gib einen speziellen Fehlerwert zurück # final_branch = "FEHLER - UNGÜLTIGE ZUWEISUNG" # Setze den finalen Branch im Ergebnis-Dictionary result["branch"] = final_branch if final_branch else "FEHLER" # --- Konsistenzprüfung (Finale Bewertung) --- # Extrahiere CRM-Kurzform für den Vergleich (erneut oder Variable von oben) crm_short_to_compare = "k.A." if crm_branche and ">" in crm_branche: crm_short_to_compare = crm_branche.split(">", 1)[1].strip() elif crm_branche and crm_branche != "k.A.": crm_short_to_compare = crm_branche.strip() # Vergleiche finalen Branch (falls nicht FEHLER) mit CRM-Kurzform (case-insensitive) if result["branch"] != "FEHLER" and result["branch"].lower() == crm_short_to_compare.lower(): # Wenn sie übereinstimmen UND *kein* Fallback stattgefunden hat, ist es 'ok'. if result["consistency"] == "pending_comparison": result["consistency"] = "ok" # Wenn Fallback auf gültige CRM stattfand (Status 'fallback_crm_valid'), bleibt dieser Status. elif result["consistency"] == "pending_comparison": # Wenn sie nicht übereinstimmen und kein Fallback stattfand, ist es 'X'. result["consistency"] = "X" # Wenn der Status bereits 'fallback_crm_valid' oder 'fallback_invalid' ist, bleibt er unverändert. elif result["consistency"] is None: # Sollte nicht passieren, aber zur Sicherheit result["consistency"] = "error_unknown_state" # Entferne den temporären Status, falls er noch da ist if result["consistency"] == "pending_comparison": result["consistency"] = "error_comparison_failed" # Debug-Ausgabe des finalen Ergebnisses vor Rückgabe debug_print(f"Finale Branch-Evaluation: {result}") return result # TODO: Weitere ChatGPT-Funktionen (evaluate_fsm_suitability, etc.) analog überarbeiten: # - Prompts verbessern (klarere Anweisungen, Kontext nur bei Bedarf) # - call_openai_chat verwenden # - Parsing der Antworten robuster machen def process_wiki_verification(crm_data, wiki_data_str): # Platzhalter - Implementierung anpassen oder entfernen, falls durch _process_batch abgedeckt debug_print(f"TODO: process_wiki_verification aufrufen/implementieren für {crm_data}") return "k.A. (Not Implemented)" def evaluate_fsm_suitability(company_name, company_data): # Platzhalter - Implementierung anpassen debug_print(f"TODO: evaluate_fsm_suitability aufrufen/implementieren für {company_name}") return {"suitability": "k.A.", "justification": "Not Implemented"} def evaluate_servicetechnicians_estimate(company_name, company_data): # Platzhalter - Implementierung anpassen debug_print(f"TODO: evaluate_servicetechnicians_estimate aufrufen/implementieren für {company_name}") return "k.A. (Not Implemented)" def map_internal_technicians(value): # Platzhalter - Implementierung anpassen debug_print(f"TODO: map_internal_technicians aufrufen/implementieren für {value}") return "k.A. (Not Implemented)" def evaluate_servicetechnicians_explanation(company_name, st_estimate, company_data): # Platzhalter - Implementierung anpassen debug_print(f"TODO: evaluate_servicetechnicians_explanation aufrufen/implementieren für {company_name}") return "k.A. (Not Implemented)" def process_employee_estimation(company_name, wiki_paragraph, crm_employee): # Platzhalter - Implementierung anpassen debug_print(f"TODO: process_employee_estimation aufrufen/implementieren für {company_name}") return "k.A. (Not Implemented)" def process_employee_consistency(crm_employee, wiki_employee, emp_estimate): # Platzhalter - Implementierung anpassen debug_print(f"TODO: process_employee_consistency aufrufen/implementieren für {crm_employee} vs {wiki_employee} vs {emp_estimate}") return "k.A. (Not Implemented)" def evaluate_umsatz_chatgpt(company_name, wiki_umsatz): # Platzhalter - Implementierung anpassen debug_print(f"TODO: evaluate_umsatz_chatgpt aufrufen/implementieren für {company_name}") return "k.A. (Not Implemented)" # ==================== BATCH PROCESSING FUNCTIONS ==================== def _process_batch(sheet, batches, row_numbers): """ Hilfsfunktion für process_verification_only: Verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen. Aktualisiert Spalten S-Y sowie Zeitstempel (AO) und Version (AP). """ if not batches: return aggregated_prompt = ( "Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln für Unternehmen. " "Für jeden der folgenden Einträge prüfe, ob der vorhandene Wikipedia-Artikel (URL, Absatz, Kategorien) plausibel zum Firmennamen und zur Beschreibung passt. " "Gib das Ergebnis für jeden Eintrag ausschließlich im folgenden Format auf einer neuen Zeile aus:\n" "Eintrag : \n\n" "Mögliche Antworten:\n" "- 'OK' (wenn der Artikel gut passt)\n" "- 'X | Alternativer Artikel: | Begründung: ' (wenn der Artikel nicht passt, aber ein besserer gefunden wurde)\n" "- 'X | Kein passender Artikel gefunden | Begründung: ' (wenn der Artikel nicht passt und kein besserer gefunden wurde)\n" "- 'Kein Wikipedia-Eintrag vorhanden.' (wenn initial keine URL angegeben wurde und keine Suche erfolgreich war - dieser Fall sollte selten sein, da die Suche vorher stattfindet)\n\n" "Einträge:\n" "----------\n" ) aggregated_prompt += "".join(batches) # Join ohne zusätzliches \n aggregated_prompt += "----------\nBitte nur die 'Eintrag X: Antwort'-Zeilen ausgeben." debug_print(f"Verarbeite Verifizierungs-Batch für Zeilen {row_numbers[0]} bis {row_numbers[-1]}.") # Token Count für den Prompt prompt_tokens = token_count(aggregated_prompt) debug_print(f"Token-Zahl für Verifizierungs-Batch: {prompt_tokens}") # Optional: Prüfung auf Token-Limit vor dem Senden # if prompt_tokens > 3800: # Beispiel-Limit für gpt-3.5-turbo (4096 gesamt) # debug_print(f"WARNUNG: Prompt für Batch {row_numbers[0]}-{row_numbers[-1]} überschreitet möglicherweise Token-Limit ({prompt_tokens}). Überspringe Batch.") # # Hier könnte man den Batch aufteilen # return chat_response = call_openai_chat(aggregated_prompt, temperature=0.0) if not chat_response: debug_print(f"Fehler: Keine Antwort von OpenAI für Verifizierungs-Batch {row_numbers[0]}-{row_numbers[-1]}.") # Optional: Markiere Zeilen als fehlerhaft return # Parse die aggregierte Antwort answers = {} lines = chat_response.strip().split('\n') for line in lines: match = re.match(r"Eintrag (\d+): (.*)", line.strip()) if match: row_num = int(match.group(1)) answer_text = match.group(2).strip() if row_num in row_numbers: answers[row_num] = answer_text else: debug_print(f"Warnung: Antwort für unerwartete Zeilennummer {row_num} im Batch erhalten: {answer_text}") # Bereite Batch-Update für Google Sheet vor updates = [] current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") current_version = Config.VERSION for row_num in row_numbers: answer = answers.get(row_num, "k.A. (Keine Antwort im Batch)") # Fallback debug_print(f"Zeile {row_num} Verifizierungsantwort: '{answer}'") wiki_confirm = "" # Spalte S alt_article = "" # Spalte T wiki_explanation = "" # Spalte U # Spalten V-Y bleiben vorerst leer oder werden hier gesetzt v_val, w_val, x_val, y_val = "", "", "", "" # Beispiel if answer.upper() == "OK": wiki_confirm = "OK" elif answer.upper() == "KEIN WIKIPEDIA-EINTRAG VORHANDEN.": wiki_confirm = "X" # Markieren, da eigentlich einer da sein sollte alt_article = "Kein Wikipedia-Eintrag vorhanden." wiki_explanation = "Ursprünglich keine URL oder Suche erfolglos." elif answer.startswith("X |"): parts = answer.split("|", 2) # Splitte maximal 2 mal wiki_confirm = "X" if len(parts) > 1: detail = parts[1].strip() if detail.startswith("Alternativer Artikel:"): alt_article = detail.split(":", 1)[1].strip() elif detail == "Kein passender Artikel gefunden": alt_article = "Kein passender Artikel gefunden" else: # Fallback, falls Format unerwartet alt_article = detail if len(parts) > 2: reason_part = parts[2].strip() if reason_part.startswith("Begründung:"): wiki_explanation = reason_part.split(":", 1)[1].strip() else: # Fallback wiki_explanation = reason_part else: # Unerwartetes Format wiki_confirm = "?" wiki_explanation = f"Unerwartetes Format: {answer}" # Füge Updates für diese Zeile zur Liste hinzu updates.append({'range': f'S{row_num}', 'values': [[wiki_confirm]]}) updates.append({'range': f'T{row_num}', 'values': [[alt_article]]}) updates.append({'range': f'U{row_num}', 'values': [[wiki_explanation]]}) # Setze V-Y zurück/leer updates.append({'range': f'V{row_num}:Y{row_num}', 'values': [[v_val, w_val, x_val, y_val]]}) # Zeitstempel und Version updates.append({'range': f'AO{row_num}', 'values': [[current_timestamp]]}) updates.append({'range': f'AP{row_num}', 'values': [[current_version]]}) # Führe das Batch-Update für alle Zeilen dieses Batches durch if updates: GoogleSheetHandler().batch_update_cells(updates) # Nutze die zentrale Update-Funktion debug_print(f"Verifizierungs-Batch {row_numbers[0]}-{row_numbers[-1]} erfolgreich in Google Sheet aktualisiert.") else: debug_print(f"Keine Updates für Verifizierungs-Batch {row_numbers[0]}-{row_numbers[-1]} generiert.") # Kurze Pause nach jedem Batch-API-Call time.sleep(Config.RETRY_DELAY) def process_verification_only(sheet_handler, row_limit=None): """Batch-Prozess nur für Wikipedia-Verifizierung (Modus 51, jetzt 'wiki' im Dispatcher).""" debug_print("Starte Wikipedia-Verifizierungsmodus (Batch)...") all_data = sheet_handler.get_all_data_with_headers() # Hole alle Daten inkl. Header header_rows = 5 # Zeilen 1-5 sind Header # Finde Startzeile (erste Zeile ab Zeile 7 ohne Zeitstempel in AO) start_row_index_in_sheet = -1 # 1-basierter Index im Sheet for i in range(header_rows + 1, len(all_data) + 1): # Starte Prüfung ab Zeile 6 (Index 5) if i < 7: continue # Überspringe Zeilen vor 7 row_index_in_list = i - 1 # 0-basierter Index in all_data row = all_data[row_index_in_list] # Prüfe Zeitstempel in Spalte AO (Index 40) if len(row) <= COLUMN_MAP["Timestamp letzte Prüfung"] or not row[COLUMN_MAP["Timestamp letzte Prüfung"]].strip(): start_row_index_in_sheet = i break if start_row_index_in_sheet == -1: debug_print("Keine Zeile ohne Zeitstempel in Spalte AO (ab Zeile 7) gefunden. Verifizierung übersprungen.") return debug_print(f"Verarbeitung startet ab Zeile {start_row_index_in_sheet} (erste Zeile ab 7 ohne Zeitstempel in AO).") # Bestimme Endzeile basierend auf row_limit if row_limit is not None and row_limit > 0: end_row_index_in_sheet = min(start_row_index_in_sheet + row_limit - 1, len(all_data)) else: end_row_index_in_sheet = len(all_data) # Bis zum Ende des Sheets if start_row_index_in_sheet > end_row_index_in_sheet: debug_print("Startzeile liegt nach der Endzeile. Keine Verarbeitung.") return debug_print(f"Verarbeite Zeilen von {start_row_index_in_sheet} bis {end_row_index_in_sheet}.") batch_size = Config.BATCH_SIZE current_batch = [] current_row_numbers = [] for i in range(start_row_index_in_sheet, end_row_index_in_sheet + 1): row_index_in_list = i - 1 row = all_data[row_index_in_list] # Erstelle Text für den Prompt (verwende Spaltennamen/Indizes) # Annahme: COLUMN_MAP ist verfügbar und korrekt company_name = row[COLUMN_MAP["CRM Name"]] if len(row) > COLUMN_MAP["CRM Name"] else '' crm_desc = row[COLUMN_MAP["CRM Beschreibung"]] if len(row) > COLUMN_MAP["CRM Beschreibung"] else '' wiki_url = row[COLUMN_MAP["Wiki URL"]] if len(row) > COLUMN_MAP["Wiki URL"] and row[COLUMN_MAP["Wiki URL"]].strip() not in ['', 'k.A.'] else 'k.A.' wiki_paragraph = row[COLUMN_MAP["Wiki Absatz"]] if len(row) > COLUMN_MAP["Wiki Absatz"] else 'k.A.' wiki_categories = row[COLUMN_MAP["Wiki Kategorien"]] if len(row) > COLUMN_MAP["Wiki Kategorien"] else 'k.A.' entry_text = ( f"Eintrag {i}:\n" f" Firmenname: {company_name}\n" f" CRM-Beschreibung: {crm_desc[:200]}...\n" # Gekürzt f" Wikipedia-URL: {wiki_url}\n" f" Wiki-Absatz: {wiki_paragraph[:200]}...\n" # Gekürzt f" Wiki-Kategorien: {wiki_categories[:200]}...\n" # Gekürzt f"----\n" ) current_batch.append(entry_text) current_row_numbers.append(i) # Wenn Batch voll oder letzte Zeile erreicht if len(current_batch) == batch_size or i == end_row_index_in_sheet: _process_batch(sheet_handler.sheet, current_batch, current_row_numbers) # Reset für nächsten Batch current_batch = [] current_row_numbers = [] debug_print("Wikipedia-Verifizierungs-Batch abgeschlossen.") def process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet): """Batch-Prozess für Website-Scraping (Rohtext & Zusammenfassung).""" debug_print(f"Starte Website-Scraping (Batch) für Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}...") all_data = sheet_handler.get_all_data_with_headers() sheet = sheet_handler.sheet # Direkter Zugriff auf das Sheet-Objekt for i in range(start_row_index_in_sheet, end_row_index_in_sheet + 1): row_index_in_list = i - 1 row = all_data[row_index_in_list] # TODO: Hier prüfen, ob Verarbeitung übersprungen werden soll (z.B. Zeitstempel schon vorhanden?) # if len(row) > COLUMN_MAP["Timestamp letzte Prüfung"] and row[COLUMN_MAP["Timestamp letzte Prüfung"]].strip(): # debug_print(f"Zeile {i}: Überspringe Website-Scraping (Zeitstempel vorhanden).") # continue website_url = row[COLUMN_MAP["CRM Website"]] if len(row) > COLUMN_MAP["CRM Website"] else "" if not website_url or website_url.strip().lower() == "k.a.": debug_print(f"Zeile {i}: Kein gültiger Website-Eintrag, überspringe Website-Scraping.") # Optional: Zeitstempel trotzdem setzen? # sheet.update_cell(i, COLUMN_MAP["Timestamp letzte Prüfung"] + 1, datetime.now().strftime("%Y-%m-%d %H:%M:%S")) # sheet.update_cell(i, COLUMN_MAP["Version"] + 1, Config.VERSION) continue debug_print(f"Zeile {i}: Verarbeite Website {website_url}...") raw_text = get_website_raw(website_url) summary = summarize_website_content(raw_text) updates = [] current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") current_version = Config.VERSION updates.append({'range': f'AR{i}', 'values': [[raw_text]]}) # Spalte AR updates.append({'range': f'AS{i}', 'values': [[summary]]}) # Spalte AS updates.append({'range': f'AO{i}', 'values': [[current_timestamp]]}) # Spalte AO updates.append({'range': f'AP{i}', 'values': [[current_version]]}) # Spalte AP # Führe Batch-Update für diese eine Zeile durch if updates: sheet_handler.batch_update_cells(updates) debug_print(f"Zeile {i}: Website-Daten aktualisiert | Zeitstempel: {current_timestamp}, Version: {current_version}") # Pause zwischen den Zeilen/Websites time.sleep(Config.RETRY_DELAY) debug_print("Website-Scraping (Batch) abgeschlossen.") def process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet): """Batch-Prozess für Brancheneinschätzung.""" debug_print(f"Starte Brancheneinschätzung (Batch) für Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}...") all_data = sheet_handler.get_all_data_with_headers() sheet = sheet_handler.sheet # Stelle sicher, dass das Branchenschema geladen ist if not ALLOWED_TARGET_BRANCHES: load_target_schema() # Versuch es zu laden if not ALLOWED_TARGET_BRANCHES: debug_print("FEHLER: Ziel-Branchenschema konnte nicht geladen werden. Breche Branch-Batch ab.") return for i in range(start_row_index_in_sheet, end_row_index_in_sheet + 1): row_index_in_list = i - 1 row = all_data[row_index_in_list] # TODO: Zeitstempelprüfung zum Überspringen? # if len(row) > COLUMN_MAP["Timestamp letzte Prüfung"] and row[COLUMN_MAP["Timestamp letzte Prüfung"]].strip(): # debug_print(f"Zeile {i}: Überspringe Branchen-Einschätzung (Zeitstempel vorhanden).") # continue # Hole benötigte Daten aus der Zeile (verwende COLUMN_MAP) crm_branche = row[COLUMN_MAP["CRM Branche"]] if len(row) > COLUMN_MAP["CRM Branche"] else "" beschreibung = row[COLUMN_MAP["CRM Beschreibung"]] if len(row) > COLUMN_MAP["CRM Beschreibung"] else "" wiki_branche = row[COLUMN_MAP["Wiki Branche"]] if len(row) > COLUMN_MAP["Wiki Branche"] else "" wiki_kategorien = row[COLUMN_MAP["Wiki Kategorien"]] if len(row) > COLUMN_MAP["Wiki Kategorien"] else "" # Nimm Website Zusammenfassung aus Spalte AS (Index 44) website_summary = row[COLUMN_MAP["Website Zusammenfassung"]] if len(row) > COLUMN_MAP["Website Zusammenfassung"] else "" debug_print(f"Zeile {i}: Starte Brancheneinschätzung...") result = evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kategorien, website_summary) updates = [] current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") current_version = Config.VERSION updates.append({'range': f'W{i}', 'values': [[result.get("branch", "Fehler")]]}) # Spalte W updates.append({'range': f'X{i}', 'values': [[result.get("consistency", "Fehler")]]}) # Spalte X updates.append({'range': f'Y{i}', 'values': [[result.get("justification", "Fehler")]]}) # Spalte Y updates.append({'range': f'AO{i}', 'values': [[current_timestamp]]}) # Spalte AO updates.append({'range': f'AP{i}', 'values': [[current_version]]}) # Spalte AP # Führe Batch-Update für diese eine Zeile durch if updates: sheet_handler.batch_update_cells(updates) debug_print(f"Zeile {i}: Branch-Einschätzung aktualisiert: {result} | Zeitstempel: {current_timestamp}, Version: {current_version}") # Pause zwischen den API-Aufrufen time.sleep(Config.RETRY_DELAY) debug_print("Brancheneinschätzung (Batch) abgeschlossen.") def run_dispatcher(mode, sheet_handler, row_limit=None): """Wählt den passenden Batch-Prozess basierend auf dem Modus.""" debug_print(f"Starte Dispatcher im Modus '{mode}' mit row_limit={row_limit}.") # Finde Startzeile (erste Zeile ab 7 ohne Zeitstempel in AO) data = sheet_handler.get_all_data_with_headers() header_rows = 5 start_row_index_in_sheet = -1 for i in range(header_rows + 1, len(data) + 1): if i < 7: continue row_index_in_list = i - 1 row = data[row_index_in_list] if len(row) <= COLUMN_MAP["Timestamp letzte Prüfung"] or not row[COLUMN_MAP["Timestamp letzte Prüfung"]].strip(): start_row_index_in_sheet = i break if start_row_index_in_sheet == -1: debug_print("Keine Zeile ohne Zeitstempel in Spalte AO (ab Zeile 7) gefunden. Dispatcher beendet.") return # Bestimme Endzeile if row_limit is not None and row_limit > 0: end_row_index_in_sheet = min(start_row_index_in_sheet + row_limit - 1, len(data)) else: end_row_index_in_sheet = len(data) debug_print(f"Dispatcher: Verarbeitung startet ab Zeile {start_row_index_in_sheet}, bis Zeile {end_row_index_in_sheet}.") if start_row_index_in_sheet > end_row_index_in_sheet: debug_print("Startzeile liegt nach Endzeile. Keine Verarbeitung.") return # Modus auswählen if mode == "wiki": process_verification_only(sheet_handler, row_limit) # Nutzt jetzt row_limit intern anders elif mode == "website": process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) elif mode == "branch": process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) elif mode == "combined": debug_print("--- Start Combined Mode: Wiki ---") process_verification_only(sheet_handler, row_limit) debug_print("--- Start Combined Mode: Website ---") # Website und Branch brauchen evtl. aktualisierte Daten nach Wiki -> neu laden? Oder mit alten Daten arbeiten? # Annahme: Arbeite erstmal mit den Daten wie sie sind. Start/End Row bleiben gleich. process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) debug_print("--- Start Combined Mode: Branch ---") process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) debug_print("--- Combined Mode abgeschlossen ---") else: debug_print(f"Ungültiger Modus '{mode}' im Dispatcher.") # ==================== SERP API / LINKEDIN FUNCTIONS ==================== @retry_on_failure def serp_website_lookup(company_name): """Ermittelt Website via SERP API (Google Suche).""" serp_key = Config.API_KEYS.get('serpapi') if not serp_key: debug_print("Fehler: SerpAPI Key nicht verfügbar für Website Lookup.") return "k.A." if not company_name: return "k.A." # Blacklist unerwünschter Domains blacklist = ["bloomberg.com", "northdata.de", "finanzen.net", "handelsblatt.com", "wikipedia.org", "linkedin.com"] query = f'{company_name} offizielle Website' # Präzisere Query params = { "engine": "google", "q": query, "api_key": serp_key, "hl": "de", "gl": "de" # Geolocation auf Deutschland setzen } api_url = "https://serpapi.com/search" try: response = requests.get(api_url, params=params, timeout=10) response.raise_for_status() data = response.json() # 1. Knowledge Graph prüfen (oft die offizielle Seite) if "knowledge_graph" in data and "website" in data["knowledge_graph"]: kg_url = data["knowledge_graph"]["website"] if kg_url and not any(bad_domain in kg_url for bad_domain in blacklist): normalized_url = simple_normalize_url(kg_url) if normalized_url != "k.A.": debug_print(f"SERP Lookup: Website '{normalized_url}' aus Knowledge Graph für '{company_name}' gefunden.") return normalized_url # 2. Organische Ergebnisse prüfen if "organic_results" in data: for result in data["organic_results"]: url = result.get("link", "") # Prüfe Blacklist und ob es eine "echte" Website ist (nicht nur Suche etc.) if url and not any(bad_domain in url for bad_domain in blacklist) and url.startswith("http"): normalized_url = simple_normalize_url(url) if normalized_url != "k.A.": # Zusätzliche Plausibilitätsprüfung: Enthält die Domain Teile des Firmennamens? domain_part = normalized_url.replace('www.', '').split('.')[0] if domain_part in normalize_company_name(company_name): debug_print(f"SERP Lookup: Website '{normalized_url}' aus Organic Results für '{company_name}' gefunden.") return normalized_url else: debug_print(f"SERP Lookup: URL '{normalized_url}' übersprungen (Domain passt nicht zu '{company_name}').") debug_print(f"SERP Lookup: Keine passende Website für '{company_name}' gefunden.") return "k.A." except requests.exceptions.RequestException as e: debug_print(f"Fehler beim SERP API Website Lookup für '{company_name}': {e}") return "k.A." except Exception as e: debug_print(f"Allgemeiner Fehler beim SERP API Website Lookup für '{company_name}': {e}") return "k.A." @retry_on_failure def search_linkedin_contacts(company_name, website, position_query, crm_kurzform, num_results=10): """Sucht LinkedIn Kontakte via SERP API.""" serp_key = Config.API_KEYS.get('serpapi') if not serp_key: debug_print("Fehler: SerpAPI Key nicht verfügbar für LinkedIn Suche.") return [] if not all([company_name, position_query, crm_kurzform]): return [] # Query anpassen für bessere Ergebnisse query = f'site:linkedin.com/in "{position_query}" "{crm_kurzform}"' # Suche nach Kurzform im Titel # query = f'site:linkedin.com/in "{position_query}" "{company_name}"' # Original Query params = { "engine": "google", "q": query, "api_key": serp_key, "hl": "de", "gl": "de", "num": num_results # Google's num Parameter (max 100, aber oft weniger geliefert) } api_url = "https://serpapi.com/search" try: response = requests.get(api_url, params=params, timeout=15) # Längerer Timeout response.raise_for_status() data = response.json() contacts = [] if "organic_results" in data: for result in data["organic_results"]: title = result.get("title", "") linkedin_url = result.get("link", "") # Filter: Muss LinkedIn URL sein und Kurzform muss im Titel vorkommen if not linkedin_url or "linkedin.com/in/" not in linkedin_url: continue if crm_kurzform.lower() not in title.lower(): debug_print(f"LinkedIn Treffer übersprungen: Kurzform '{crm_kurzform}' nicht in Titel '{title}'") continue # Extrahiere Name und Position aus Titel name_part = "" pos_part = position_query # Fallback auf Suchbegriff # Versuche gängige Trennzeichen separators = ["–", "-", "|", " at ", " bei "] title_cleaned = title.replace("...", "").strip() # Bereinige Titel found_sep = False for sep in separators: if sep in title_cleaned: parts = title_cleaned.split(sep, 1) name_part = parts[0].strip() # Versuche, LinkedIn/Profil etc. aus Namen zu entfernen name_part = name_part.replace(" | LinkedIn", "").replace(" - LinkedIn", "").replace(" - Profil", "").strip() # Positionsteil kann komplex sein, nehme alles nach dem Trenner potential_pos = parts[1].strip() # Entferne Firmenteil, wenn er dem Kurznamen ähnelt if crm_kurzform.lower() in potential_pos.lower(): potential_pos = potential_pos.replace(crm_kurzform, "", 1).strip() # Nur erste Instanz ersetzen # Entferne generische Endungen potential_pos = potential_pos.split(" | LinkedIn")[0].split(" - LinkedIn")[0].strip() pos_part = potential_pos if potential_pos else position_query found_sep = True break if not found_sep: # Kein Trennzeichen gefunden name_part = title_cleaned.split(" | LinkedIn")[0].split(" - LinkedIn")[0].strip() # Prüfe, ob der Suchbegriff im verbleibenden Namensteil ist if position_query.lower() in name_part.lower(): name_part = name_part.replace(position_query, "", 1).strip() # Versuche Position zu entfernen # Teile Namen in Vor- und Nachname firstname = "" lastname = "" name_parts = name_part.split() if len(name_parts) > 1: firstname = name_parts[0] lastname = " ".join(name_parts[1:]) elif len(name_parts) == 1: firstname = name_parts[0] # Nur Vorname gefunden? if not firstname: # Wenn Name nicht extrahiert werden konnte, überspringe debug_print(f"Kontakt übersprungen: Name konnte nicht extrahiert werden aus Titel '{title}'") continue contact_data = { "Firmenname": company_name, # Originalname für Kontext "CRM Kurzform": crm_kurzform, "Website": website, "Vorname": firstname, "Nachname": lastname, "Position": pos_part, "LinkedInURL": linkedin_url } contacts.append(contact_data) debug_print(f"Gefundener LinkedIn Kontakt: {firstname} {lastname} - {pos_part}") debug_print(f"LinkedIn Suche für '{position_query}' bei '{crm_kurzform}' ergab {len(contacts)} Kontakte.") return contacts except requests.exceptions.RequestException as e: debug_print(f"Fehler bei der SERP API LinkedIn Suche: {e}") return [] except Exception as e: debug_print(f"Allgemeiner Fehler bei der SERP API LinkedIn Suche: {e}") return [] # Funktion count_linkedin_contacts wurde entfernt, da search_linkedin_contacts jetzt die Liste liefert # und len() darauf angewendet werden kann. def process_contact_research(sheet_handler): """Sucht LinkedIn Kontakte und trägt sie in 'Contacts' Sheet ein.""" debug_print("Starte Contact Research (LinkedIn)...") main_sheet = sheet_handler.sheet all_data = sheet_handler.get_all_data_with_headers() header_rows = 5 # Finde Startzeile basierend auf Timestamp in Spalte AM (Index 38) timestamp_col_index = COLUMN_MAP["Contact Search Timestamp"] start_row_index_in_sheet = -1 for i in range(header_rows + 1, len(all_data) + 1): if i < 7: continue # Normalerweise ab Zeile 7 row_index_in_list = i - 1 row = all_data[row_index_in_list] if len(row) <= timestamp_col_index or not row[timestamp_col_index].strip(): start_row_index_in_sheet = i break if start_row_index_in_sheet == -1: debug_print("Keine Zeile ohne Contact Search Timestamp (Spalte AM, ab Zeile 7) gefunden. Überspringe.") return debug_print(f"Contact Research startet ab Zeile {start_row_index_in_sheet}.") # Kontakte-Blatt öffnen oder erstellen try: contacts_sheet = sheet_handler.sheet.spreadsheet.worksheet("Contacts") debug_print("Blatt 'Contacts' gefunden.") except gspread.exceptions.WorksheetNotFound: debug_print("Blatt 'Contacts' nicht gefunden, erstelle neu...") contacts_sheet = sheet_handler.sheet.spreadsheet.add_worksheet(title="Contacts", rows="1000", cols="12") header = ["Firmenname", "CRM Kurzform", "Website", "Geschlecht", "Vorname", "Nachname", "Position", "Suchbegriffskategorie", "E-Mail-Adresse", "LinkedIn-Link", "Timestamp"] contacts_sheet.update(values=[header], range_name="A1:K1") # Optional: Alignment Demo hier nicht mehr aufrufen # alignment_demo(contacts_sheet) # NICHT MEHR NÖTIG/FALSCH debug_print("Neues Blatt 'Contacts' erstellt und Header eingetragen.") # Positionen, nach denen gesucht wird positions_to_search = ["Serviceleiter", "Leiter Kundendienst", "IT-Leiter", "Leiter IT", "Geschäftsführer", "Vorstand", "Disponent", "Einsatzleiter"] # Gehe Zeilen im Hauptblatt durch for i in range(start_row_index_in_sheet, len(all_data) + 1): row_index_in_list = i - 1 row = all_data[row_index_in_list] company_name = row[COLUMN_MAP["CRM Name"]] if len(row) > COLUMN_MAP["CRM Name"] else "" crm_kurzform = row[COLUMN_MAP["CRM Kurzform"]] if len(row) > COLUMN_MAP["CRM Kurzform"] else "" website = row[COLUMN_MAP["CRM Website"]] if len(row) > COLUMN_MAP["CRM Website"] else "" if not all([company_name, crm_kurzform, website]): debug_print(f"Zeile {i}: Übersprungen (fehlende CRM Daten: Name, Kurzform oder Website).") continue debug_print(f"Zeile {i}: Suche Kontakte für '{crm_kurzform}'...") all_found_contacts = [] contact_counts = {pos: 0 for pos in ["Serviceleiter", "IT-Leiter", "Geschäftsführer", "Disponent"]} # Für die Zählung im Hauptblatt for position in positions_to_search: # Suche max. 5 Kontakte pro Position, um API Calls/Kosten zu begrenzen found_contacts = search_linkedin_contacts(company_name, website, position, crm_kurzform, num_results=5) # Zählung für das Hauptblatt (vereinfachte Kategorien) if "serviceleiter" in position.lower() or "kundendienst" in position.lower() or "einsatzleiter" in position.lower(): contact_counts["Serviceleiter"] += len(found_contacts) elif "it-leiter" in position.lower() or "leiter it" in position.lower(): contact_counts["IT-Leiter"] += len(found_contacts) elif "geschäftsführer" in position.lower() or "vorstand" in position.lower(): contact_counts["Geschäftsführer"] += len(found_contacts) elif "disponent" in position.lower(): contact_counts["Disponent"] += len(found_contacts) # Füge gefundene Kontakte zur Liste hinzu (mit Suchkategorie) for contact in found_contacts: contact["Suchbegriffskategorie"] = position all_found_contacts.append(contact) time.sleep(1.5) # Kleine Pause zwischen SerpAPI-Aufrufen # Verarbeite gefundene Kontakte und schreibe ins Contacts-Sheet rows_to_append = [] timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") unique_contacts = {c['LinkedInURL']: c for c in all_found_contacts}.values() # Deduplizieren basierend auf URL for contact in unique_contacts: firstname = contact.get("Vorname", "") lastname = contact.get("Nachname", "") gender_value = get_gender(firstname) email = get_email_address(firstname, lastname, website) contact_row = [ contact.get("Firmenname", ""), contact.get("CRM Kurzform", ""), contact.get("Website", ""), gender_value, firstname, lastname, contact.get("Position", ""), contact.get("Suchbegriffskategorie", ""), email, contact.get("LinkedInURL", ""), timestamp ] rows_to_append.append(contact_row) if rows_to_append: try: # Verwende append_rows für Effizienz contacts_sheet.append_rows(rows_to_append, value_input_option='USER_ENTERED') debug_print(f"Zeile {i}: {len(rows_to_append)} neue Kontakte zum 'Contacts'-Blatt hinzugefügt.") except Exception as e: debug_print(f"Zeile {i}: Fehler beim Hinzufügen von Kontakten zum Sheet: {e}") # Evtl. einzeln versuchen bei Fehler? # Aktualisiere Trefferzahlen und Timestamp im Hauptblatt (Batch Update) main_sheet_updates = [] main_sheet_updates.append({'range': f'AI{i}', 'values': [[str(contact_counts["Serviceleiter"])]]}) main_sheet_updates.append({'range': f'AJ{i}', 'values': [[str(contact_counts["IT-Leiter"])]]}) main_sheet_updates.append({'range': f'AK{i}', 'values': [[str(contact_counts["Geschäftsführer"])]]}) main_sheet_updates.append({'range': f'AL{i}', 'values': [[str(contact_counts["Disponent"])]]}) main_sheet_updates.append({'range': f'AM{i}', 'values': [[timestamp]]}) # Contact Search Timestamp sheet_handler.batch_update_cells(main_sheet_updates) debug_print(f"Zeile {i}: Kontaktzahlen im Hauptblatt aktualisiert: {contact_counts} – Timestamp in AM gesetzt.") # Pause nach Verarbeitung einer Firma time.sleep(Config.RETRY_DELAY) debug_print("Contact Research abgeschlossen.") # ==================== ALIGNMENT DEMO (Hauptblatt) ==================== # ==================== ALIGNMENT DEMO (Hauptblatt) ==================== # Diese Funktion ist bereits im Code vorhanden (Zeile ~1230 in der vorherigen Version) # Sie bleibt unverändert: def alignment_demo(sheet): """Schreibt die Header-Struktur (Zeilen 1-5) ins angegebene Sheet.""" # Definition der Header wie im Original-Code new_headers = [ ["ReEval Flag", "CRM Name", "CRM Kurzform", "CRM Website", "CRM Ort", "CRM Beschreibung", "CRM Branche", "CRM Beschreibung Branche extern", "CRM Anzahl Techniker", "CRM Umsatz", "CRM Anzahl Mitarbeiter", "CRM Vorschlag Wiki URL", "Wiki URL", "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", "Chat Wiki Konsistenzprüfung", "Chat Begründung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", "Begründung bei Abweichung", "Chat Vorschlag Branche", "Chat Konsistenz Branche", "Chat Begründung Abweichung Branche", "Chat Prüfung FSM Relevanz", "Chat Begründung für FSM Relevanz", "Chat Schätzung Anzahl Mitarbeiter", "Chat Konsistenzprüfung Mitarbeiterzahl", "Chat Begründung Abweichung Mitarbeiterzahl", "Chat Einschätzung Anzahl Servicetechniker", "Chat Begründung Abweichung Anzahl Servicetechniker", "Chat Schätzung Umsatz", "Chat Begründung Abweichung Umsatz", "Linked Serviceleiter gefunden", "Linked It-Leiter gefunden", "Linked Management gefunden", "Linked Disponent gefunden", "Contact Search Timestamp", "Wikipedia Timestamp", "Timestamp letzte Prüfung", "Version", "Tokens", "Website Rohtext", "Website Zusammenfassung"], ["CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "System", "System", "System", "System", "System", "Web Scraper", "Chat GPT API"], ["Prozess", "Firmenname", "Firmenname", "Website", "Ort", "Beschreibung (Text)", "Branche", "Branche", "Anzahl Servicetechniker", "Umsatz", "Anzahl Mitarbeiter", "Wikipedia Artikel URL", "Wikipedia Artikel", "Beschreibung (Text)", "Branche", "Umsatz", "Anzahl Mitarbeiter", "Kategorien (Text)", "Verifizierung", "Begründung bei Abweichung", "Wikipedia Artikel", "Wikipedia Artikel", "Branche", "Branche", "Branche", "FSM Relevanz", "FSM Relevanz", "Anzahl Mitarbeiter", "Anzahl Mitarbeiter", "Anzahl Mitarbeiter", "Anzahl Servicetechniker", "Anzahl Servicetechniker", "Umsatz", "Umsatz", "Kontakte zur Firma", "Kontakte zur Firma", "Kontakte zur Firma", "Kontakte zur Firma", "Timestamp", "Timestamp", "Timestamp", "Version des Skripts die verwendet wurde", "ChatGPT Tokens", "Website-Content", "Website Zusammenfassung"], ["Systemspalte, irrelevant für den Prompt. Wird zur manuellen Neuprüfung genutzt.", "Enthält den Firmennamen; Normalisierung erfolgt bei der Suche.", "Manuell gepflegte Kurzform, meist die ersten 2 Worte.", "Website des Unternehmens.", "Ort des Unternehmens.", "Kurze Beschreibung des Unternehmens.", "Aktuelle Branchenzuweisung gemäß Ziel-Branchenschema.", "Externe Branchenbeschreibung (z.B. von Dealfront).", "Recherchierte Anzahl Servicetechniker.", "Umsatz in Mio. € (CRM).", "Anzahl Mitarbeiter (CRM).", "Vorgeschlagene Wikipedia URL (Ausgangspunkt).", "Wikipedia URL (Ergebnis der Suche).", "Erster Absatz des Wikipedia-Artikels.", "Wikipedia-Branche – für den Branchenabgleich.", "Wikipedia-Umsatz – zur Validierung.", "Wikipedia-Mitarbeiterzahl – zur Validierung.", "Liste der Wikipedia-Kategorien.", "\"OK\" oder \"X\" – Ergebnis der Wikipedia-Validierung.", "Begründung bei Inkonsistenz (Wiki).", "Chat-Vorschlag Wiki Artikel: Falls kein passender Artikel gefunden, alternativ vorschlagen.", "Nicht genutzt, evtl. für zukünftige Funktionen.", "Branchenvorschlag via ChatGPT (alternativer Vorschlag).", "Vergleich: Übereinstimmung CRM vs. ChatGPT-Branche (OK/X).", "Begründung bei abweichender Branchenzuordnung.", "FSM-Relevanz: Bewertung, ob das Unternehmen für FSM geeignet ist (OK/X).", "Begründung zur FSM-Bewertung.", "Schätzung Anzahl Mitarbeiter via ChatGPT (nur falls Wiki-Daten fehlen).", "Vergleich CRM vs. Wiki vs. ChatGPT Mitarbeiterzahl (OK/X).", "Begründung bei Mitarbeiterabweichung (Prozentdifferenz).", "Schätzung Servicetechniker via ChatGPT (in Kategorien, z.B. <50, >100, >200, >500).", "Begründung bei Abweichung der Technikerzahl.", "Schätzung Umsatz via ChatGPT.", "Begründung bei Umsatzabweichung.", "Anzahl Kontakte (Serviceleiter) gefunden.", "Anzahl Kontakte (IT-Leiter) gefunden.", "Anzahl Kontakte (Management) gefunden.", "Anzahl Kontakte (Disponent) gefunden.", "Timestamp der Kontaktsuche.", "Timestamp der Wikipedia-Suche.", "Timestamp der ChatGPT-Bewertung.", "Ausgabe der Skriptversion, die das Ergebnis erzeugt hat.", "Token-Zählung (separat pro Modul).", "Roh extrahierter Text der Firmenwebsite (maximal 1000 Zeichen).", "Zusammenfassung des Webseiteninhalts, fokussiert auf Tätigkeitsfeld, Produkte & Leistungen."], ["Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Wird durch Wikipedia Scraper bereitgestellt", "Wird zunächst nicht verwendet, kann aber zum Vergleich mit der CRM-Beschreibung genutzt werden.", "Wird u.a. zur finalen Ermittlung der Branche im Ziel-Branchenschema genutzt und mit der CRM-Branche bzw. CRM-Beschreibung Branche Extern verglichen. Stimmen alle drei Einstufungen grob überein, bestärkt dies die ursprüngliche Einstufung. Laufen diese Branchen weit auseinander, soll – sofern der Wikipedia-Artikel verifiziert ist – die Branche von Wikipedia als zuverlässigste Quelle bewertet werden, danach folgen CRM-Beschreibung Branche Extern und CRM-Branche an dritter Stelle.", "Wird u.a. mit CRM-Umsatz zur Validierung des Unternehmens verglichen bzw. zur Bewertung der Größe / Einschätzung der Technikerzahl bzw. FSM-Relevanz genutzt.", "Wird u.a. mit CRM-Anzahl Mitarbeiter zur Validierung des Unternehmens verglichen bzw. zur Bewertung der Größe / Einschätzung der Technikerzahl bzw. FSM-Relevanz genutzt.", "Wenn Website-Daten fehlen, wird in diesem Feld keine zusätzliche Information einbezogen; ansonsten als zusätzlicher Kontext.", "\"Es soll durch ChatGPT geprüft werden, ob anhand der vorliegenden Daten bestätigt werden kann, dass der Wikipedia-Eintrag das Unternehmen sicher beschreibt. Dabei können alle Daten (Website, Umsatz, Mitarbeiterzahl etc.) berücksichtigt werden. Eine gewisse Toleranz (±30%) ist erlaubt. Insbesondere bei Konzernstrukturen muss großzügig bewertet werden. Abweichungen sollen in der Spalte 'Chat Begründung Wiki Inkonsistenz' begründet werden.\"", "\"Liegt eine Inkonsistenz zwischen dem gefundenen Wikipedia-Artikel und dem Unternehmen vor, so soll dies kurz begründet werden. Wurde der Artikel als unpassend identifiziert, soll ChatGPT einen alternativen Wikipedia-Artikel vorschlagen und diesen in 'Chat Vorschlag Wiki Artikel' ausgeben.\"", "\"Sollte durch die Wikipedia-Suche kein Artikel gefunden werden oder als unpassend bewertet werden, soll ChatGPT eigenständig nach einem passenden Artikel recherchieren. Der gefundene Artikel muss vom als unpassend bewerteten Artikel abweichen. Wird kein passender Artikel gefunden, soll 'kein Artikel verfügbar' ausgegeben werden.\"", "XXX derzeit nicht verwendet, wird vermutlich gelöscht xxx", "\"ChatGPT soll anhand der vorliegenden Informationen prüfen, welcher Branche des Ziel-Branchenschemas das Unternehmen am ehesten zugeordnet werden kann. Das Ziel-Branchenschema darf nicht verändert werden, sondern die Vorschläge müssen exakt diesem Schema entsprechen.\"", "Die in Spalte CRM festgelegte Branche soll mit der von ChatGPT ermittelten Branche in 'Chat Vorschlag Branche' verglichen werden.", "Weicht die von ChatGPT ermittelte Branche von der in CRM vorliegenden ab, so soll ChatGPT die Abweichung kurz begründen.", "ChatGPT soll anhand der vorliegenden Daten prüfen, ob das Unternehmen für den Einsatz einer Field Service Management Lösung geeignet ist.", "Die in 'Chat Begründung für FSM Relevanz' angegebene Begründung soll zur Bewertung der FSM-Eignung herangezogen werden.", "Nur wenn kein Wikipedia-Eintrag vorhanden ist, soll ChatGPT basierend auf öffentlich verfügbaren Informationen die Mitarbeiterzahl schätzen. Falls keine Schätzung möglich ist, wird 'keine Schätzung möglich' ausgegeben.", "Entspricht die durch ChatGPT ermittelte Mitarbeiterzahl ungefähr den in CRM und Wikipedia ermittelten Werten (±30%), wird 'OK' ausgegeben, andernfalls 'X' und eine Begründung in 'Chat Begründung Abweichung Mitarbeiterzahl'.", "Weicht die von ChatGPT geschätzte Mitarbeiterzahl signifikant von den CRM- oder Wikipedia-Werten ab, soll dies kurz begründet werden.", "ChatGPT soll auf Basis öffentlich zugänglicher Informationen eine Schätzung der Anzahl Servicetechniker abgeben (Kategorisierung: 0, <50, >100, >200, >500). Bei Abweichungen der Recherche-Werte soll 'X' ausgegeben werden, ansonsten 'OK'.", "Weicht die von ChatGPT geschätzte Technikerzahl von den CRM-Werten ab, soll dies begründet werden.", "Nur wenn kein Wikipedia-Eintrag vorhanden ist, soll ChatGPT den Umsatz anhand der Unternehmenswebsite oder anderer Daten schätzen. Bei fehlender Schätzung soll 'keine Schätzung möglich' ausgegeben werden.", "ChatGPT soll signifikante Umsatzabweichungen zwischen den Schätzungen von Chat, Wikipedia und CRM begründen. Stimmen die Werte (±30%) überein, wird 'OK' ausgegeben.", "Über SerpAPI wird zusammen mit der in 'CRM Kurzform' enthaltenen Information nach 'Serviceleiter' gesucht.", "Über SerpAPI wird zusammen mit 'CRM Kurzform' nach 'Leiter IT' gesucht.", "Über SerpAPI wird zusammen mit 'CRM Kurzform' nach 'Geschäftsführer' gesucht.", "Über SerpAPI wird zusammen mit 'CRM Kurzform' erneut nach 'Serviceleiter' gesucht.", "Wenn die Kontaktsuche gestartet wird, wird der erste Eintrag ohne Zeitstempel gesucht; Zeilen mit vorhandenem Zeitstempel werden übersprungen.", "Wenn die Wikipedia-Suche gestartet wird, wird der erste Eintrag ohne Zeitstempel gesucht; Zeilen mit vorhandenem Zeitstempel werden übersprungen.", "Wenn die ChatGPT-Bewertung gestartet wird, wird der erste Eintrag ohne Zeitstempel gesucht; Zeilen mit vorhandenem Zeitstempel werden übersprungen.", "Wird durch das System befüllt", "Wird durch tiktoken berechnet"] ] # Bestimme den Bereich basierend auf der Anzahl der Spalten in der ersten Header-Zeile num_cols = len(new_headers[0]) # Konvertiere Spaltenanzahl in Buchstaben (A=1, B=2, ..., Z=26, AA=27, ...) def colnum_string(n): string = "" while n > 0: n, remainder = divmod(n - 1, 26) string = chr(65 + remainder) + string return string end_col_letter = colnum_string(num_cols) header_range = f"A1:{end_col_letter}{len(new_headers)}" try: sheet.update(values=new_headers, range_name=header_range) print(f"Alignment-Demo abgeschlossen: Header in Bereich {header_range} geschrieben.") # Geändert zu print für Sichtbarkeit debug_print(f"Alignment-Demo: Header in Bereich {header_range} geschrieben.") except Exception as e: print(f"FEHLER beim Schreiben der Alignment-Demo Header: {e}") # Geändert zu print debug_print(f"FEHLER beim Schreiben der Alignment-Demo Header: {e}") # ==================== DATA PROCESSOR ==================== class DataProcessor: # Diese Klasse enthält jetzt hauptsächlich die Logik für die Verarbeitung einzelner Zeilen # und spezifische Modi, die nicht als Batch laufen. def __init__(self, sheet_handler): self.sheet_handler = sheet_handler self.wiki_scraper = WikipediaScraper() # Eigene Instanz des Scrapers # @retry_on_failure # Vorsicht mit Retry auf dieser Ebene, kann lange dauern def _process_single_row(self, row_num_in_sheet, row_data, process_wiki=True, process_chatgpt=True, process_website=True): """Verarbeitet die Daten für eine einzelne Zeile.""" debug_print(f"--- Starte Verarbeitung für Zeile {row_num_in_sheet} ---") # Verwende COLUMN_MAP für sicherere Zugriffe # Beispiel: company_name = row_data[COLUMN_MAP["CRM Name"]] if len(row_data) > COLUMN_MAP["CRM Name"] else "" # Der Einfachheit halber bleiben wir vorerst bei Indizes, aber mit Bewusstsein für die Karte company_name = row_data[1] if len(row_data) > 1 else "" website_url = row_data[3] if len(row_data) > 3 else "" crm_kurzform = row_data[2] if len(row_data) > 2 else company_name # Fallback für Kurzform # --- 1. Website Handling (Lookup, Scrape, Summarize) --- original_website = website_url website_raw = "k.A." website_summary = "k.A." website_details = "k.A." # Website Lookup, wenn leer if process_website and (not website_url or website_url.strip().lower() == "k.a."): debug_print(f"Zeile {row_num_in_sheet}: CRM Website fehlt, starte SERP Lookup für '{company_name}'...") new_website = serp_website_lookup(company_name) if new_website != "k.A.": website_url = new_website debug_print(f"Zeile {row_num_in_sheet}: SERP Lookup erfolgreich: {website_url}") # Schreibe neue Website direkt zurück (optional, oder sammle für Batch Update) # self.sheet_handler.sheet.update_cell(row_num_in_sheet, COLUMN_MAP["CRM Website"] + 1, website_url) else: debug_print(f"Zeile {row_num_in_sheet}: SERP Lookup erfolglos.") # Website Scraping (Rohtext, Zusammenfassung, Details), wenn URL vorhanden if process_website and website_url and website_url.strip().lower() != "k.a.": debug_print(f"Zeile {row_num_in_sheet}: Starte Website Scraping für {website_url}...") website_raw = get_website_raw(website_url) website_summary = summarize_website_content(website_raw) # Benötigt OpenAI Key # Website Details (optional, kann viele Tokens kosten) # website_details = scrape_website_details(website_url) # debug_print(f"Zeile {row_num_in_sheet}: Website Details: {website_details[:100]}...") elif process_website: debug_print(f"Zeile {row_num_in_sheet}: Überspringe Website Scraping (keine gültige URL).") # --- 2. Wikipedia Handling --- wiki_data = { # Standardwerte 'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.' } wiki_timestamp_needed = len(row_data) <= COLUMN_MAP["Wikipedia Timestamp"] or not row_data[COLUMN_MAP["Wikipedia Timestamp"]].strip() if process_wiki and wiki_timestamp_needed: debug_print(f"Zeile {row_num_in_sheet}: Starte Wikipedia Verarbeitung...") # Prüfe, ob CRM einen Vorschlag hat crm_wiki_url = row_data[COLUMN_MAP["CRM Vorschlag Wiki URL"]] if len(row_data) > COLUMN_MAP["CRM Vorschlag Wiki URL"] and row_data[COLUMN_MAP["CRM Vorschlag Wiki URL"]].strip() not in ["", "k.A."] else None article_page = None if crm_wiki_url: debug_print(f"Zeile {row_num_in_sheet}: Prüfe CRM Wiki Vorschlag: {crm_wiki_url}") page = self.wiki_scraper._fetch_page_content(crm_wiki_url.split('/')[-1]) if page and self.wiki_scraper._validate_article(page, company_name, website_url): article_page = page else: debug_print(f"Zeile {row_num_in_sheet}: CRM Wiki Vorschlag nicht validiert. Starte Suche...") article_page = self.wiki_scraper.search_company_article(company_name, website_url) else: debug_print(f"Zeile {row_num_in_sheet}: Kein CRM Wiki Vorschlag. Starte Suche...") article_page = self.wiki_scraper.search_company_article(company_name, website_url) if article_page: debug_print(f"Zeile {row_num_in_sheet}: Extrahiere Daten aus Artikel: {article_page.url}") wiki_data = self.wiki_scraper.extract_company_data(article_page.url) else: debug_print(f"Zeile {row_num_in_sheet}: Kein passender Wikipedia Artikel gefunden.") wiki_data['url'] = 'Kein Artikel gefunden' # Spezifische Kennung elif process_wiki: debug_print(f"Zeile {row_num_in_sheet}: Überspringe Wikipedia Verarbeitung (Timestamp AN vorhanden).") # Lade vorhandene Wiki-Daten aus der Zeile, um sie für ChatGPT verfügbar zu machen wiki_data['url'] = row_data[COLUMN_MAP["Wiki URL"]] if len(row_data) > COLUMN_MAP["Wiki URL"] else 'k.A.' wiki_data['first_paragraph'] = row_data[COLUMN_MAP["Wiki Absatz"]] if len(row_data) > COLUMN_MAP["Wiki Absatz"] else 'k.A.' wiki_data['branche'] = row_data[COLUMN_MAP["Wiki Branche"]] if len(row_data) > COLUMN_MAP["Wiki Branche"] else 'k.A.' wiki_data['umsatz'] = row_data[COLUMN_MAP["Wiki Umsatz"]] if len(row_data) > COLUMN_MAP["Wiki Umsatz"] else 'k.A.' wiki_data['mitarbeiter'] = row_data[COLUMN_MAP["Wiki Mitarbeiter"]] if len(row_data) > COLUMN_MAP["Wiki Mitarbeiter"] else 'k.A.' wiki_data['categories'] = row_data[COLUMN_MAP["Wiki Kategorien"]] if len(row_data) > COLUMN_MAP["Wiki Kategorien"] else 'k.A.' # --- 3. ChatGPT Evaluationen --- chat_results = {} # Sammle Ergebnisse der einzelnen ChatGPT Calls chat_timestamp_needed = len(row_data) <= COLUMN_MAP["Timestamp letzte Prüfung"] or not row_data[COLUMN_MAP["Timestamp letzte Prüfung"]].strip() if process_chatgpt and chat_timestamp_needed: debug_print(f"Zeile {row_num_in_sheet}: Starte ChatGPT Evaluationen...") # 3.1 Branchenevaluierung (Wichtigste zuerst?) crm_branche = row_data[COLUMN_MAP["CRM Branche"]] if len(row_data) > COLUMN_MAP["CRM Branche"] else "" crm_beschreibung = row_data[COLUMN_MAP["CRM Beschreibung"]] if len(row_data) > COLUMN_MAP["CRM Beschreibung"] else "" chat_results['branche'] = evaluate_branche_chatgpt(crm_branche, crm_beschreibung, wiki_data['branche'], wiki_data['categories'], website_summary) # 3.2 Weitere Evaluationen (Beispiele, ggf. anpassen/implementieren) # chat_results['wiki_verification'] = process_wiki_verification(row_data, wiki_data) # Siehe Batch-Mode # chat_results['fsm'] = evaluate_fsm_suitability(company_name, wiki_data) # chat_results['st_estimate'] = evaluate_servicetechnicians_estimate(company_name, wiki_data) # crm_techniker = row_data[COLUMN_MAP["CRM Anzahl Techniker"]] if len(row_data) > COLUMN_MAP["CRM Anzahl Techniker"] else "k.A." # internal_category = map_internal_technicians(crm_techniker) # chat_results['st_explanation'] = evaluate_servicetechnicians_explanation(company_name, chat_results.get('st_estimate'), wiki_data) if internal_category != chat_results.get('st_estimate') else "ok" # crm_mitarbeiter = row_data[COLUMN_MAP["CRM Anzahl Mitarbeiter"]] if len(row_data) > COLUMN_MAP["CRM Anzahl Mitarbeiter"] else "k.A." # chat_results['emp_estimate'] = process_employee_estimation(company_name, wiki_data['first_paragraph'], crm_mitarbeiter) # chat_results['emp_consistency'] = process_employee_consistency(crm_mitarbeiter, wiki_data['mitarbeiter'], chat_results.get('emp_estimate')) # crm_umsatz = row_data[COLUMN_MAP["CRM Umsatz"]] if len(row_data) > COLUMN_MAP["CRM Umsatz"] else "k.A." # chat_results['umsatz_estimate'] = evaluate_umsatz_chatgpt(company_name, wiki_data['umsatz']) # TODO: Vergleich Umsatz / Begründung Abweichung # 3.x Token Zählung (Beispielhaft, genauer pro Call) # total_tokens = token_count(str(wiki_data)) + token_count(str(chat_results)) # Grobe Schätzung # chat_results['tokens'] = total_tokens elif process_chatgpt: debug_print(f"Zeile {row_num_in_sheet}: Überspringe ChatGPT Evaluationen (Timestamp AO vorhanden).") # --- 4. Daten für Batch Update sammeln --- updates = [] current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 4.1 Website-Daten if process_website: if website_url != original_website: # Nur wenn URL sich geändert hat (durch Lookup) updates.append({'range': f'D{row_num_in_sheet}', 'values': [[website_url]]}) updates.append({'range': f'AR{row_num_in_sheet}', 'values': [[website_raw]]}) updates.append({'range': f'AS{row_num_in_sheet}', 'values': [[website_summary]]}) # Optional: Details schreiben # updates.append({'range': f'??{row_num_in_sheet}', 'values': [[website_details]]}) # Braucht neue Spalte # 4.2 Wikipedia-Daten (nur wenn neu verarbeitet) if process_wiki and wiki_timestamp_needed: updates.append({'range': f'M{row_num_in_sheet}', 'values': [[wiki_data.get('url', 'k.A.')]]}) updates.append({'range': f'N{row_num_in_sheet}', 'values': [[wiki_data.get('first_paragraph', 'k.A.')]]}) updates.append({'range': f'O{row_num_in_sheet}', 'values': [[wiki_data.get('branche', 'k.A.')]]}) updates.append({'range': f'P{row_num_in_sheet}', 'values': [[wiki_data.get('umsatz', 'k.A.')]]}) updates.append({'range': f'Q{row_num_in_sheet}', 'values': [[wiki_data.get('mitarbeiter', 'k.A.')]]}) updates.append({'range': f'R{row_num_in_sheet}', 'values': [[wiki_data.get('categories', 'k.A.')]]}) updates.append({'range': f'AN{row_num_in_sheet}', 'values': [[current_timestamp]]}) # Wiki Timestamp # 4.3 ChatGPT-Daten (nur wenn neu verarbeitet) if process_chatgpt and chat_timestamp_needed: # Branche updates.append({'range': f'W{row_num_in_sheet}', 'values': [[chat_results.get('branche', {}).get('branch', 'k.A.')]]}) updates.append({'range': f'X{row_num_in_sheet}', 'values': [[chat_results.get('branche', {}).get('consistency', 'k.A.')]]}) updates.append({'range': f'Y{row_num_in_sheet}', 'values': [[chat_results.get('branche', {}).get('justification', 'k.A.')]]}) # Weitere ChatGPT Ergebnisse hier einfügen... # updates.append({'range': f'Z{row_num_in_sheet}', 'values': [[chat_results.get('fsm', {}).get('suitability', 'k.A.')]]}) # updates.append({'range': f'AA{row_num_in_sheet}', 'values': [[chat_results.get('fsm', {}).get('justification', 'k.A.')]]}) # ... etc. ... # Tokens # updates.append({'range': f'AQ{row_num_in_sheet}', 'values': [[str(chat_results.get('tokens', 0))]]}) # Timestamp für ChatGPT/Letzte Prüfung updates.append({'range': f'AO{row_num_in_sheet}', 'values': [[current_timestamp]]}) # 4.4 Immer Update: Version updates.append({'range': f'AP{row_num_in_sheet}', 'values': [[Config.VERSION]]}) # --- 5. Batch Update durchführen --- if updates: self.sheet_handler.batch_update_cells(updates) debug_print(f"Zeile {row_num_in_sheet}: Batch-Update erfolgreich.") else: debug_print(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben.") debug_print(f"--- Verarbeitung für Zeile {row_num_in_sheet} abgeschlossen ---") # Kurze Pause nach jeder Zeile time.sleep(max(0.5, Config.RETRY_DELAY / 2)) # Kürzere Pause bei Einzelverarbeitung def process_rows_sequentially(self, start_row_index, num_rows_to_process, process_wiki=True, process_chatgpt=True, process_website=True): """ Verarbeitet Zeilen sequentiell ab einem Startindex. """ data_rows = self.sheet_handler.get_data() # Daten ohne Header header_rows = 5 end_row_index = min(start_row_index + num_rows_to_process, len(data_rows)) if start_row_index >= len(data_rows): debug_print("Startindex liegt hinter der letzten Datenzeile. Keine Verarbeitung.") return debug_print(f"Verarbeite {end_row_index - start_row_index} Zeilen sequentiell (Index {start_row_index} bis {end_row_index - 1})...") for i in range(start_row_index, end_row_index): row_data = data_rows[i] row_num_in_sheet = i + header_rows + 1 # 1-basierter Sheet-Index # Überspringe Zeilen vor Zeile 7 generell? Nein, start_row_index sollte das regeln. # if row_num_in_sheet < 7: continue self._process_single_row(row_num_in_sheet, row_data, process_wiki, process_chatgpt, process_website) def process_reevaluation_rows(self): """ Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind. """ debug_print("Starte Re-Evaluierungsmodus (Spalte A = 'x')...") data_rows = self.sheet_handler.get_data() header_rows = 5 rows_processed = 0 for i, row in enumerate(data_rows): row_num_in_sheet = i + header_rows + 1 # Prüfe Flag in Spalte A (Index 0) if len(row) > COLUMN_MAP["ReEval Flag"] and row[COLUMN_MAP["ReEval Flag"]].strip().lower() == "x": debug_print(f"Re-Evaluiere Zeile {row_num_in_sheet}...") # Führe volle Verarbeitung für diese Zeile durch self._process_single_row(row_num_in_sheet, row, process_wiki=True, process_chatgpt=True, process_website=True) rows_processed += 1 # Optional: Flag nach Verarbeitung löschen? # self.sheet_handler.sheet.update_cell(row_num_in_sheet, COLUMN_MAP["ReEval Flag"] + 1, "") debug_print(f"Re-Evaluierung abgeschlossen. {rows_processed} Zeilen verarbeitet.") def process_website_details_for_marked_rows(self): """ Neuer Modus 23: Extrahiert Website-Details für markierte Zeilen. """ debug_print("Starte Modus 23: Website Detail Extraction für Zeilen mit 'x' in Spalte A.") data_rows = self.sheet_handler.get_data() header_rows = 5 rows_processed = 0 for i, row in enumerate(data_rows): row_num_in_sheet = i + header_rows + 1 # Prüfe Flag in Spalte A (Index 0) if len(row) > COLUMN_MAP["ReEval Flag"] and row[COLUMN_MAP["ReEval Flag"]].strip().lower() == "x": website_url = row[COLUMN_MAP["CRM Website"]] if len(row) > COLUMN_MAP["CRM Website"] else "" if not website_url or website_url.strip().lower() == "k.a.": debug_print(f"Zeile {row_num_in_sheet}: Keine gültige Website in Spalte D vorhanden, überspringe.") continue debug_print(f"Zeile {row_num_in_sheet}: Extrahiere Website Details von {website_url}...") details = scrape_website_details(website_url) # Speichere das Detail-Ergebnis in Spalte AR (Index 43) update_data = [{'range': f'AR{row_num_in_sheet}', 'values': [[details]]}] self.sheet_handler.batch_update_cells(update_data) debug_print(f"Zeile {row_num_in_sheet}: Website Detail Extraction abgeschlossen, Ergebnis in Spalte AR geschrieben.") rows_processed += 1 time.sleep(Config.RETRY_DELAY) debug_print(f"Modus 23 abgeschlossen. {rows_processed} Zeilen verarbeitet.") def process_serp_website_lookup_for_empty(self): """ Neuer Modus 22: Füllt fehlende Websites via SERP API. """ debug_print("Starte Modus 22: SERP API Website Lookup für leere Zellen in Spalte D.") data_rows = self.sheet_handler.get_data() header_rows = 5 rows_processed = 0 for i, row in enumerate(data_rows): row_num_in_sheet = i + header_rows + 1 current_website = row[COLUMN_MAP["CRM Website"]] if len(row) > COLUMN_MAP["CRM Website"] else "" if not current_website or current_website.strip().lower() == "k.a.": company_name = row[COLUMN_MAP["CRM Name"]] if len(row) > COLUMN_MAP["CRM Name"] else "" if not company_name: debug_print(f"Zeile {row_num_in_sheet}: Übersprungen (kein Firmenname für Lookup).") continue debug_print(f"Zeile {row_num_in_sheet}: Suche Website für '{company_name}'...") new_website = serp_website_lookup(company_name) if new_website != "k.A.": update_data = [{'range': f'D{row_num_in_sheet}', 'values': [[new_website]]}] self.sheet_handler.batch_update_cells(update_data) debug_print(f"Zeile {row_num_in_sheet}: Neue Website '{new_website}' gefunden und in Spalte D eingetragen.") rows_processed += 1 else: debug_print(f"Zeile {row_num_in_sheet}: Keine Website gefunden.") time.sleep(Config.RETRY_DELAY) # Pause nach jedem Lookup debug_print(f"Modus 22 abgeschlossen. {rows_processed} Websites ergänzt.") def prepare_data_for_modeling(self): # Wird zu einer Methode """ Lädt Daten aus dem Google Sheet über den sheet_handler, bereitet sie für das Decision Tree Modell vor: - Wählt relevante Spalten aus. - Konsolidiert Umsatz/Mitarbeiter (Wiki > CRM Priorität). - Filtert nach gültiger Technikerzahl (> 0). - Erstellt die Zielvariable (Techniker-Bucket). - Bereitet Features auf (One-Hot Encoding für Branche). - Behält NaNs in numerischen Features für spätere Imputation. Returns: pandas.DataFrame: Vorbereiteter DataFrame für Training/Test-Split, oder None bei Fehlern. """ debug_print("Starte Datenvorbereitung für Modellierung...") try: # --- 1. Daten laden & Spalten auswählen --- # Zugriff auf sheet_values über self.sheet_handler if not self.sheet_handler or not self.sheet_handler.sheet_values: debug_print("Fehler: Sheet Handler nicht initialisiert oder keine Daten geladen.") return None all_data = self.sheet_handler.sheet_values # Verwende die bereits geladenen Daten if len(all_data) <= 5: debug_print("Fehler: Nicht genügend Datenzeilen im Sheet gefunden.") return None # Annahme: Die ersten Header (Zeile 1) enthalten die Spaltennamen headers = all_data[0] data_rows = all_data[5:] # Daten ohne die ersten 5 Header-Zeilen df = pd.DataFrame(data_rows, columns=headers) debug_print(f"DataFrame erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.") # Wähle benötigte Spalten aus ( passe die Schlüssel an deine COLUMN_MAP an!) # Verwende die global definierte COLUMN_MAP required_cols_keys_in_map = [ "CRM Name", "CRM Branche", "CRM Umsatz", "Wiki Umsatz", "CRM Anzahl Mitarbeiter", "Wiki Mitarbeiter", "CRM Anzahl Techniker" # <-- ANPASSEN, falls die bekannte Zahl woanders steht! ] # Finde die tatsächlichen Spaltennamen cols_to_select = [] missing_keys = [] # Wir gehen davon aus, dass die *erste* Zeile (Index 0) die tatsächlichen Headernamen sind actual_headers = {name: idx for idx, name in enumerate(all_data[0])} # Überprüfe, ob alle benötigten Spalten via COLUMN_MAP gefunden werden # und hole die echten Spaltennamen # Annahme: COLUMN_MAP mappt Beschreibung (Zeile 4?) zu Index (0-basiert) col_indices = {} try: tech_col_key = "CRM Anzahl Techniker" # Schlüssel in COLUMN_MAP anpassen! col_indices = { "name": all_data[0][COLUMN_MAP["CRM Name"]], "branche": all_data[0][COLUMN_MAP["CRM Branche"]], "umsatz_crm": all_data[0][COLUMN_MAP["CRM Umsatz"]], "umsatz_wiki": all_data[0][COLUMN_MAP["Wiki Umsatz"]], "ma_crm": all_data[0][COLUMN_MAP["CRM Anzahl Mitarbeiter"]], "ma_wiki": all_data[0][COLUMN_MAP["Wiki Mitarbeiter"]], "techniker": all_data[0][COLUMN_MAP[tech_col_key]] } cols_to_select = list(col_indices.values()) except KeyError as e: debug_print(f"FEHLER: Konnte Mapping für Schlüssel '{e}' in COLUMN_MAP nicht finden oder Spalte nicht im Header.") return None except IndexError as e: debug_print(f"FEHLER: Spaltenindex aus COLUMN_MAP ist außerhalb der Grenzen der Header-Zeile: {e}") return None df_subset = df[cols_to_select].copy() # Spalten umbenennen für einfachere Handhabung intern rename_map = {v: k for k, v in col_indices.items()} df_subset.rename(columns=rename_map, inplace=True) debug_print(f"Benötigte Spalten ausgewählt und umbenannt: {list(df_subset.columns)}") # --- 2. Features konsolidieren (Umsatz, Mitarbeiter) --- # Hilfsfunktion bleibt dieselbe def get_valid_numeric(value_str): if value_str is None or pd.isna(value_str) or value_str == '': return np.nan # Leere Strings auch als NaN try: val = float(str(value_str).replace(',', '.')) # Komma als Dezimaltrenner erlauben return val if val > 0 else np.nan except (ValueError, TypeError): cleaned_str = re.sub(r'[^\d.]', '', str(value_str)) # Nur Ziffern und Punkt behalten if not cleaned_str: return np.nan try: val = float(cleaned_str) return val if val > 0 else np.nan except ValueError: return np.nan # Konvertiere Quellen-Spalten und wende Priorisierung an cols_to_process = { 'Umsatz': ('umsatz_wiki', 'umsatz_crm', 'Finaler_Umsatz'), 'Mitarbeiter': ('ma_wiki', 'ma_crm', 'Finaler_Mitarbeiter') } for base_name, (wiki_col, crm_col, final_col) in cols_to_process.items(): debug_print(f"Verarbeite '{base_name}' (Wiki: {wiki_col}, CRM: {crm_col})...") # Stelle sicher, dass Spalten existieren bevor apply angewendet wird if wiki_col not in df_subset.columns: df_subset[wiki_col] = np.nan if crm_col not in df_subset.columns: df_subset[crm_col] = np.nan wiki_numeric = df_subset[wiki_col].apply(get_valid_numeric) crm_numeric = df_subset[crm_col].apply(get_valid_numeric) df_subset[final_col] = np.where( wiki_numeric.notna(), wiki_numeric, # Wiki hat Prio 1 (wenn nicht NaN) np.where(crm_numeric.notna(), crm_numeric, np.nan) # Sonst CRM (wenn nicht NaN) ) debug_print(f" -> {df_subset[final_col].notna().sum()} gültige '{final_col}' Werte erstellt.") # --- 3. Zielvariable vorbereiten (Technikerzahl) --- techniker_col = "techniker" # Umbenannter Spaltenname debug_print(f"Verarbeite Zielvariable '{techniker_col}'...") df_subset['Anzahl_Servicetechniker_Numeric'] = pd.to_numeric(df_subset[techniker_col], errors='coerce') initial_rows = len(df_subset) df_filtered = df_subset[ df_subset['Anzahl_Servicetechniker_Numeric'].notna() & (df_subset['Anzahl_Servicetechniker_Numeric'] > 0) ].copy() filtered_rows = len(df_filtered) debug_print(f"{initial_rows - filtered_rows} Zeilen entfernt aufgrund fehlender/ungültiger Technikerzahl.") debug_print(f"Verbleibende Zeilen für Modellierung: {filtered_rows}") if filtered_rows < 50: # Mindestanzahl für sinnvolles Training debug_print(f"WARNUNG: Nur {filtered_rows} Zeilen mit gültiger Technikerzahl. Modelltraining möglicherweise nicht sinnvoll.") # return None # Optional hier abbrechen if filtered_rows == 0: return None # --- 4. Techniker-Buckets erstellen --- bins = [-1, 0, 19, 49, 99, 249, 499, float('inf')] labels = ['Bucket_1_(0)', 'Bucket_2_(<20)', 'Bucket_3_(<50)', 'Bucket_4_(<100)', 'Bucket_5_(<250)', 'Bucket_6_(<500)', 'Bucket_7_(>499)'] df_filtered['Techniker_Bucket'] = pd.cut( df_filtered['Anzahl_Servicetechniker_Numeric'], bins=bins, labels=labels, right=True ) debug_print("Techniker-Buckets erstellt.") debug_print(f"Verteilung der Buckets:\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).round(3)}") # Relative Häufigkeit # --- 5. Kategoriale Features vorbereiten (Branche) --- branche_col = "branche" # Umbenannter Spaltenname debug_print(f"Verarbeite kategoriales Feature '{branche_col}'...") df_filtered[branche_col] = df_filtered[branche_col].astype(str).fillna('Unbekannt').str.strip() # Leerzeichen entfernen # One-Hot Encoding df_encoded = pd.get_dummies(df_filtered, columns=[branche_col], prefix='Branche', dummy_na=False) debug_print(f"One-Hot Encoding für Branche durchgeführt. Neue Spaltenanzahl: {len(df_encoded.columns)}") # --- 6. Finale Auswahl der Features für das Modell --- feature_columns = [col for col in df_encoded.columns if col.startswith('Branche_')] feature_columns.extend(['Finaler_Umsatz', 'Finaler_Mitarbeiter']) target_column = 'Techniker_Bucket' original_data_cols = ['name', 'Anzahl_Servicetechniker_Numeric'] # Behalte Original-Technikerzahl und Name für Referenz df_model_ready = df_encoded[original_data_cols + feature_columns + [target_column]].copy() for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter']: df_model_ready[col] = pd.to_numeric(df_model_ready[col], errors='coerce') df_model_ready = df_model_ready.reset_index(drop=True) debug_print("Datenvorbereitung abgeschlossen.") debug_print(f"Finaler DataFrame für Modellierung hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten.") # debug_print(f"Feature-Spalten: {feature_columns}") # Kann sehr lang sein debug_print(f"Ziel-Spalte: {target_column}") nan_counts = df_model_ready[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].isna().sum() debug_print(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}") return df_model_ready except Exception as e: debug_print(f"FEHLER während der Datenvorbereitung: {e}") import traceback debug_print(traceback.format_exc()) return None # ==================== MAIN FUNCTION ==================== def main(): global LOG_FILE # LOG_FILE wird global benötigt # --- Initialisierung --- # Argument Parser parser = argparse.ArgumentParser(description="Firmen-Datenanreicherungs-Skript") # HIER NEU: 'train_technician_model' als Option hinzugefügt parser.add_argument("--mode", type=str, help="Betriebsmodus (z.B. combined, ..., full_run, alignment, train_technician_model)") parser.add_argument("--limit", type=int, help="Maximale Anzahl zu verarbeitender Zeilen (für Batch/sequentielle Modi)", default=None) # Optional: Argumente speziell für das Training hinzufügen? z.B. --output_model_file # parser.add_argument("--model_out", type=str, default="technician_model.pkl", help="Dateipfad zum Speichern des trainierten Modells") # parser.add_argument("--patterns_out", type=str, default="technician_patterns.json", help="Dateipfad zum Speichern der extrahierten Muster") args = parser.parse_args() # Lade API Keys Config.load_api_keys() # Betriebsmodus ermitteln # HIER NEU: 'train_technician_model' hinzugefügt valid_modes = ["combined", "wiki", "website", "branch", "reeval", "website_lookup", "website_details", "contacts", "full_run", "alignment", "train_technician_model"] mode = None # Priorisiere Kommandozeilenargumente if args.mode and args.mode.lower() in valid_modes: mode = args.mode.lower() print(f"Betriebsmodus (aus Kommandozeile): {mode}") else: # Nur wenn KEIN Modus über die Kommandozeile kam, FRAGE interaktiv print("Bitte wählen Sie den Betriebsmodus:") print(" combined: Wiki-Verifizierung, Website-Scraping & Branch-Einschätzung (Batch, ab erster leerer Zeile)") print(" wiki: Nur Wikipedia-Verifizierung (Batch, ab erster leerer Zeile)") print(" website: Nur Website-Scraping & Zusammenfassung (Batch, ab erster leerer Zeile)") print(" branch: Nur Branchen-Einschätzung (Batch, ab erster leerer Zeile)") print(" reeval: Verarbeitet alle Zeilen mit 'x' in Spalte A (volle Verarbeitung)") print(" website_lookup: Sucht fehlende Websites (Spalte D) via SERP API") print(" website_details:Extrahiert Title/Desc/H-Tags für Zeilen mit 'x' in Spalte A") print(" contacts: Sucht LinkedIn Kontakte via SERP API und schreibt in 'Contacts' Blatt") print(" full_run: Verarbeitet alle Zeilen sequentiell ab der ersten ohne Zeitstempel (AO)") print(" alignment: Schreibt die Definitions-Header (Zeilen 1-5) ins Hauptblatt (Überschreibt A1:AS5!)") print(" train_technician_model: Bereitet Daten vor, trainiert & evaluiert Decision Tree zur Technikerschätzung") # NEUE Beschreibung try: mode_input = input(f"Geben Sie den Modus ein ({', '.join(valid_modes)}): ").strip().lower() if mode_input in valid_modes: mode = mode_input else: print("Ungültige Eingabe. Standardmodus 'combined' wird verwendet.") mode = "combined" except OSError as e: if e.errno == 9: print("Fehler: Interaktive Modus-Abfrage nicht möglich (läuft im Hintergrund?). Standardmodus 'combined' wird verwendet.") mode = "combined" else: print(f"Unerwarteter OS-Fehler bei Modus-Abfrage: {e}") print("Standardmodus 'combined' wird verwendet.") mode = "combined" except EOFError: print("Fehler: Interaktive Modus-Abfrage nicht möglich (EOF). Standardmodus 'combined' wird verwendet.") mode = "combined" # Zeilenlimit ermitteln (Logik bleibt unverändert, fragt nur wenn nötig) # Hinweis: Das Limit wird im 'train_technician_model' Modus aktuell nicht direkt verwendet, # da alle verfügbaren Daten mit Technikerzahl genutzt werden sollten. row_limit = None if args.limit is not None: if args.limit >= 0: row_limit = args.limit print(f"Zeilenlimit (aus Kommandozeile): {row_limit}") else: print("Warnung: Negatives Zeilenlimit ignoriert. Kein Limit gesetzt.") row_limit = None elif mode in ["combined", "wiki", "website", "branch", "full_run"]: # Limit nur für diese Modi interaktiv abfragen try: limit_input = input("Wie viele Zeilen sollen maximal bearbeitet werden? (Enter für alle) ") if limit_input.strip(): limit_val = int(limit_input) if limit_val >= 0: row_limit = limit_val print(f"Zeilenlimit: {row_limit}") else: print("Warnung: Negatives Zeilenlimit ignoriert. Kein Limit gesetzt.") row_limit = None else: row_limit = None print("Kein Zeilenlimit gesetzt.") except ValueError: print("Ungültige Eingabe für Zeilenlimit. Kein Limit gesetzt.") row_limit = None except OSError as e: if e.errno == 9: print("Warnung: Interaktive Abfrage des Limits nicht möglich (läuft im Hintergrund?). Kein Limit gesetzt.") row_limit = None else: print(f"Unerwarteter OS-Fehler bei Limit-Abfrage: {e}") print("Kein Limit gesetzt.") row_limit = None except EOFError: print("Warnung: Interaktive Abfrage des Limits nicht möglich (EOF). Kein Limit gesetzt.") row_limit = None # Logfile initialisieren LOG_FILE = create_log_filename(mode) debug_print(f"===== Skript gestartet =====") debug_print(f"Version: {Config.VERSION}") debug_print(f"Betriebsmodus: {mode}") limit_log_text = str(row_limit) if row_limit is not None else 'N/A für diesen Modus' if mode in ["combined", "wiki", "website", "branch", "full_run"]: limit_log_text = str(row_limit) if row_limit is not None else 'Unbegrenzt' if row_limit == 0: limit_log_text = '0 (Keine Verarbeitung geplant)' debug_print(f"Zeilenlimit: {limit_log_text}") debug_print(f"Logdatei: {LOG_FILE}") # --- Vorbereitung --- # Lade Branchenschema (wird für fast alle Modi benötigt) load_target_schema() # Initialisiere Google Sheet Handler try: sheet_handler = GoogleSheetHandler() except Exception as e: debug_print(f"FATAL: Konnte Google Sheet Handler nicht initialisieren: {e}") print(f"FEHLER: Verbindung zu Google Sheets fehlgeschlagen. Siehe Logdatei: {LOG_FILE}") return # Abbruch # Initialisiere DataProcessor (wird für einige Modi gebraucht) data_processor = DataProcessor(sheet_handler) # --- Modusausführung --- start_time = time.time() debug_print(f"Starte Verarbeitung um {datetime.now().strftime('%H:%M:%S')}...") try: if mode in ["wiki", "website", "branch", "combined"]: if row_limit == 0: debug_print("Zeilenlimit ist 0. Überspringe Dispatcher-Aufruf.") else: run_dispatcher(mode, sheet_handler, row_limit) elif mode == "reeval": data_processor.process_reevaluation_rows() elif mode == "website_lookup": data_processor.process_serp_website_lookup_for_empty() elif mode == "website_details": data_processor.process_website_details_for_marked_rows() elif mode == "contacts": process_contact_research(sheet_handler) elif mode == "full_run": if row_limit == 0: debug_print("Zeilenlimit ist 0. Überspringe sequenzielle Verarbeitung.") else: start_index = sheet_handler.get_start_row_index() if start_index < len(sheet_handler.get_data()): num_available = len(sheet_handler.get_data()) - start_index if row_limit is not None and row_limit >= 0: num_to_process = min(row_limit, num_available) else: num_to_process = num_available if num_to_process > 0: data_processor.process_rows_sequentially(start_index, num_to_process, process_wiki=True, process_chatgpt=True, process_website=True) else: debug_print("Keine Zeilen für 'full_run' zu verarbeiten (Limit 0 oder Startindex am Ende).") else: debug_print(f"Startindex {start_index} liegt hinter der letzten Datenzeile. Keine Verarbeitung für 'full_run'.") elif mode == "alignment": print("\nACHTUNG: Dieser Modus überschreibt die Zellen A1:AS5 im Haupt-Sheet!") print("Diese Zellen enthalten die Spaltendefinitionen (Alignment Demo).") try: confirm = input("Möchten Sie wirklich fortfahren? (j/N): ").strip().lower() if confirm == 'j': debug_print("Bestätigung erhalten. Starte Alignment Demo...") alignment_demo(sheet_handler.sheet) debug_print("Alignment Demo Aufruf beendet.") else: print("Vorgang abgebrochen.") debug_print("Alignment Demo vom Benutzer abgebrochen.") except OSError as e: if e.errno == 9: print("Fehler: Interaktive Bestätigung nicht möglich (läuft im Hintergrund?). Vorgang abgebrochen.") debug_print("Alignment Demo abgebrochen (keine interaktive Bestätigung möglich).") else: print(f"Unerwarteter OS-Fehler bei Bestätigung: {e}. Vorgang abgebrochen.") debug_print(f"Alignment Demo abgebrochen (OS-Fehler: {e}).") except EOFError: print("Fehler: Interaktive Bestätigung nicht möglich (EOF). Vorgang abgebrochen.") debug_print("Alignment Demo abgebrochen (EOF).") # HIER NEU: Block für den Modelltrainings-Modus elif mode == "train_technician_model": debug_print("Starte Modus: train_technician_model") # 1. Daten vorbereiten prepared_df = prepare_data_for_modeling(sheet_handler) if prepared_df is not None and not prepared_df.empty: # 2. Train/Test Split debug_print("Aufteilen der Daten in Trainings- und Testsets...") try: X = prepared_df.drop(columns=['Techniker_Bucket']) y = prepared_df['Techniker_Bucket'] # Stratify=y ist wichtig, um die Verteilung der Buckets in Train/Test ähnlich zu halten X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y ) debug_print(f"Trainingsdaten: {X_train.shape}, Testdaten: {X_test.shape}") except Exception as e: debug_print(f"Fehler beim Train/Test Split: {e}") X_train, X_test, y_train, y_test = None, None, None, None # Zurücksetzen if X_train is not None: # 3. Imputation fehlender Werte (Umsatz/Mitarbeiter) debug_print("Imputation fehlender Werte (Median)...") numeric_features = ['Finaler_Umsatz', 'Finaler_Mitarbeiter'] try: imputer = SimpleImputer(strategy='median') # WICHTIG: Imputer NUR auf Trainingsdaten fitten! imputer.fit(X_train[numeric_features]) # Transformiere Trainings- UND Testdaten X_train_imputed_np = imputer.transform(X_train[numeric_features]) X_test_imputed_np = imputer.transform(X_test[numeric_features]) # Konvertiere zurück zu DataFrames und setze Spaltennamen und Index zurück X_train[numeric_features] = X_train_imputed_np X_test[numeric_features] = X_test_imputed_np # Speichere den Imputer für spätere Verwendung (z.B. bei neuen Daten) imputer_filename = "median_imputer.pkl" with open(imputer_filename, 'wb') as f_imputer: pickle.dump(imputer, f_imputer) debug_print(f"Median-Imputer trainiert und gespeichert als '{imputer_filename}'.") imputation_successful = True except Exception as e: debug_print(f"Fehler bei der Imputation: {e}") imputation_successful = False if imputation_successful: # 4. Modelltraining & Hyperparameter-Tuning (Beispielhaft) debug_print("Starte Decision Tree Training mit GridSearchCV...") # Definiere den Parameter-Grid für die Suche param_grid = { 'criterion': ['gini', 'entropy'], 'max_depth': [5, 8, 10, 12, None], # None = unbegrenzt (vorsicht) 'min_samples_split': [10, 20, 40], 'min_samples_leaf': [5, 10, 20], 'ccp_alpha': [0.0, 0.001, 0.005, 0.01] # Für Pruning } # Erstelle Decision Tree Classifier dtree = DecisionTreeClassifier(random_state=42) # Erstelle GridSearchCV Objekt (cv=5 für 5-fache Kreuzvalidierung) # scoring='accuracy' oder 'f1_weighted' etc. grid_search = GridSearchCV(estimator=dtree, param_grid=param_grid, cv=5, scoring='accuracy', n_jobs=-1, verbose=1) # n_jobs=-1 nutzt alle CPU Kerne try: grid_search.fit(X_train, y_train) # Bestes Modell und Parameter ausgeben best_params = grid_search.best_params_ best_score = grid_search.best_score_ best_estimator = grid_search.best_estimator_ debug_print(f"GridSearchCV abgeschlossen.") debug_print(f"Beste Parameter gefunden: {best_params}") debug_print(f"Bester Kreuzvalidierungs-Score (Accuracy): {best_score:.4f}") # Speichere das beste Modell model_filename = "technician_decision_tree_model.pkl" with open(model_filename, 'wb') as f_model: pickle.dump(best_estimator, f_model) debug_print(f"Bestes Modell gespeichert als '{model_filename}'.") # 5. Evaluation auf dem Test-Set debug_print("Evaluiere bestes Modell auf dem Test-Set...") y_pred = best_estimator.predict(X_test) test_accuracy = accuracy_score(y_test, y_pred) report = classification_report(y_test, y_pred, zero_division=0) conf_matrix = confusion_matrix(y_test, y_pred) debug_print(f"\n--- Evaluationsergebnisse (Test-Set) ---") debug_print(f"Genauigkeit: {test_accuracy:.4f}") debug_print(f"Klassifikationsbericht:\n{report}") debug_print(f"Konfusionsmatrix:\n{conf_matrix}") print(f"\nModell-Evaluation abgeschlossen. Genauigkeit auf Test-Set: {test_accuracy:.4f}") # Auch für User sichtbar print(f"Detaillierter Bericht im Logfile: {LOG_FILE}") # 6. Muster extrahieren debug_print("\nExtrahiere Regeln aus dem besten Baum (Textformat)...") try: feature_names = list(X_train.columns) # Namen der Features # Stelle sicher, dass die Label-Namen (Buckets) verfügbar sind class_names = best_estimator.classes_ # Die Bucket-Labels rules_text = export_text(best_estimator, feature_names=feature_names, show_weights=True) # show_weights zeigt Verteilung in Blättern debug_print(f"--- Baumregeln (Text) ---:\n{rules_text}") # Speichere Regeln als Textdatei patterns_filename_txt = "technician_patterns.txt" with open(patterns_filename_txt, 'w', encoding='utf-8') as f_rules: f_rules.write(rules_text) debug_print(f"Regeln als Text gespeichert in '{patterns_filename_txt}'.") # TODO (Optional): Regeln als JSON extrahieren (komplexer) # Hier müsste man den Baum traversieren (tree_ Attribut) except Exception as e_export: debug_print(f"Fehler beim Extrahieren/Speichern der Baumregeln: {e_export}") except Exception as e_train: debug_print(f"FEHLER während des Modelltrainings/-tunings: {e_train}") import traceback debug_print(traceback.format_exc()) else: debug_print("Datenvorbereitung fehlgeschlagen oder keine Daten vorhanden. Modus 'train_technician_model' abgebrochen.") else: debug_print(f"Unbekannter Modus '{mode}' - keine Aktion ausgeführt.") except Exception as e: debug_print(f"FATAL: Unerwarteter Fehler auf oberster Ebene während der Modusausführung: {e}") import traceback debug_print(traceback.format_exc()) # --- Abschluss --- end_time = time.time() duration = end_time - start_time debug_print(f"Verarbeitung abgeschlossen um {datetime.now().strftime('%H:%M:%S')}.") debug_print(f"Gesamtdauer: {duration:.2f} Sekunden.") debug_print(f"===== Skript beendet =====") if LOG_FILE: try: with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ===== Skript wirklich beendet =====\n") except: pass print(f"Verarbeitung abgeschlossen. Logfile: {LOG_FILE}") # Führt die main-Funktion aus, wenn das Skript direkt gestartet wird if __name__ == '__main__': main()