diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 3aa1da92..4e110d74 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -8,7 +8,7 @@ von Unternehmensdaten, primär aus einem Google Sheet, ergänzt durch Web Scrapi Wikipedia, OpenAI (ChatGPT) und SerpAPI (Google Search, LinkedIn). Autor: [Ihr Name/Pseudonym] -Version: v1.7.1 +Version: v1.7.2 Hinweis zur Struktur: Dieser Code wird in logischen Bloecken uebermittelt. Fuegen Sie die Bloecke @@ -107,7 +107,7 @@ PATTERNS_FILE_JSON = "technician_patterns.json" # Neu (Empfohlen) # --- Globale Konfiguration Klasse --- class Config: """Zentrale Konfigurationseinstellungen.""" - VERSION = "v1.7.1" + VERSION = "v1.7.2" LANG = "de" # Sprache fuer Wikipedia etc. # ACHTUNG: SHEET_URL ist hier ein Platzhalter. Ersetzen Sie ihn durch Ihre tatsaechliche URL. SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" # <<< ERSETZEN SIE DIES! @@ -192,6 +192,9 @@ BRANCH_MAPPING = {} TARGET_SCHEMA_STRING = "Ziel-Branchenschema nicht verfuegbar." ALLOWED_TARGET_BRANCHES = [] +# Marker für URLs, die erneut per SERP gesucht werden sollen +URL_CHECK_MARKER = "URL_CHECK_NEEDED" # <<< NEU HINZUFÜGEN + # Liste gängiger User-Agents für Rotation USER_AGENTS = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', @@ -2092,12 +2095,13 @@ def search_linkedin_contacts(company_name, website, position_query, crm_kurzform # --- Globale Funktion zum Scrapen des Website Rohtextes --- # Basierend auf get_website_raw aus Teil 7. Global platziert. -# Nutzt globale Helfer: simple_normalize_url, clean_text, re, requests, BeautifulSoup, Config, getattr, logger, retry_on_failure. +# Nutzt globale Helfer: simple_normalize_url, clean_text, re, requests, BeautifulSoup, Config, getattr, logger, retry_on_failure, USER_AGENTS, URL_CHECK_MARKER. @retry_on_failure # Wende den Decorator auf diese Funktion an -def get_website_raw(url, max_length=20000, verify_cert=True): # verify_cert Default bleibt True +def get_website_raw(url, max_length=20000, verify_cert=False): # verify_cert Default ist jetzt False """ Holt Textinhalt von einer Website, versucht Cookie-Banner zu umgehen. - Implementiert SSL-Fallback und gibt spezifischere Fehlerwerte zurueck. + Ignoriert standardmäßig SSL-Zertifikatfehler und gibt spezifischere Fehlerwerte + oder einen Marker fuer erneute URL-Suche zurueck. """ logger = logging.getLogger(__name__) if not url or not isinstance(url, str) or url.strip().lower() in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"]: @@ -2110,72 +2114,65 @@ def get_website_raw(url, max_length=20000, verify_cert=True): # verify_cert Defa headers = { "User-Agent": random.choice(USER_AGENTS) # Wählt zufälligen User-Agent aus der Liste } - # --- ANPASSUNG START: SSL Fallback & Spezifische Fehler --- response = None error_reason = "Unbekannter Fehler" # Default + return_marker = False # Flag für URL_CHECK_MARKER - for ssl_verify_attempt in [True, False]: # Erst mit True, dann mit False versuchen - if not ssl_verify_attempt and not verify_cert: # Wenn verify_cert schon False war, nicht nochmal versuchen - break - try: - current_verify_setting = verify_cert if ssl_verify_attempt else False - if not ssl_verify_attempt: - logger.warning(f"SSL-Fehler bei verify=True. Versuche erneut mit verify=False fuer {url[:100]}...") + try: + logger.debug(f"Versuche Website abzurufen: {url[:100]}... (verify={verify_cert})") + response = requests.get( + url, + timeout=getattr(Config, 'REQUEST_TIMEOUT', 20), # Timeout aus Config (Standard 20s) + headers=headers, + verify=verify_cert, # Nutzt den Default oder den übergebenen Wert + allow_redirects=True, + stream=False + ) + response.raise_for_status() # Wirft HTTPError fuer 4xx/5xx + error_reason = None # Kein Fehler, wenn erfolgreich - response = requests.get( - url, - timeout=getattr(Config, 'REQUEST_TIMEOUT', 30), # Timeout aus Config (erhöht auf 30s als Default) - headers=headers, - verify=current_verify_setting, # Aktuelle Einstellung verwenden - allow_redirects=True, # Redirects folgen - stream=False # Stream deaktivieren, da wir gesamten Inhalt brauchen - ) - response.raise_for_status() # Wirft HTTPError fuer 4xx/5xx - error_reason = None # Kein Fehler, wenn bis hierhin erfolgreich - break # Erfolgreicher Request, Schleife verlassen + except requests.exceptions.SSLError as e_ssl: + error_reason = f"SSL Fehler: {str(e_ssl)[:100]}..." + logger.warning(f"SSL Fehler (verify={verify_cert}) fuer {url[:100]}...: {e_ssl}") + # Wenn verify=True war, hätte man hier nochmal mit False versuchen können, + # aber da der Default jetzt False ist, ist ein erneuter Versuch meist redundant. - except requests.exceptions.SSLError as e_ssl: - error_reason = f"SSL Fehler: {str(e_ssl)[:100]}..." - logger.warning(f"SSL Fehler bei verify={current_verify_setting} fuer {url[:100]}...: {e_ssl}") - if ssl_verify_attempt: # Wenn es der erste Versuch (verify=True) war - verify_cert = False # Setze Flag für nächsten Versuch auf False - continue # Mache nächsten Versuch mit verify=False - else: # Wenn auch verify=False fehlschlägt - logger.error(f"Endgueltiger SSL Fehler auch bei verify=False fuer {url[:100]}...") - break # Beende Versuche nach SSL Fehler mit verify=False + except requests.exceptions.Timeout as e_timeout: + error_reason = f"Timeout ({getattr(Config, 'REQUEST_TIMEOUT', 20)}s)" + logger.warning(f"{error_reason} fuer {url[:100]}...") - except requests.exceptions.Timeout as e_timeout: - error_reason = f"Timeout ({getattr(Config, 'REQUEST_TIMEOUT', 30)}s)" - logger.warning(f"{error_reason} fuer {url[:100]}...") - break # Timeout ist endgültig für diesen Call (Decorator macht Retries) + except requests.exceptions.ConnectionError as e_conn: + error_reason = f"Connection Error: {str(e_conn)[:100]}..." + logger.warning(f"{error_reason} fuer {url[:100]}...") + # Prüfe, ob es ein 'Name or service not known' oder 'Connection refused' Fehler ist etc. + if "[Errno -2] Name or service not known" in str(e_conn) or \ + "[Errno -3] Temporary failure in name resolution" in str(e_conn) or \ + "[Errno 111] Connection refused" in str(e_conn) or \ + "[Errno 113] No route to host" in str(e_conn) or \ + "Failed to establish a new connection" in str(e_conn): + return_marker = True # Starker Hinweis auf falsche/unerreichbare URL - except requests.exceptions.ConnectionError as e_conn: - error_reason = f"Connection Error: {str(e_conn)[:100]}..." - logger.warning(f"{error_reason} fuer {url[:100]}...") - break # Connection Error ist endgültig für diesen Call + except requests.exceptions.HTTPError as e_http: + status_code = e_http.response.status_code + error_reason = f"HTTP Error {status_code} ({e_http.response.reason})" + logger.warning(f"{error_reason} fuer {url[:100]}...") + if status_code == 404: # Bei 404 Fehler auch Marker setzen + return_marker = True - except requests.exceptions.HTTPError as e_http: - status_code = e_http.response.status_code - error_reason = f"HTTP Error {status_code} ({e_http.response.reason})" - logger.warning(f"{error_reason} fuer {url[:100]}...") - # Non-retryable HTTP errors werden bereits im Decorator behandelt - break # HTTP Error ist endgültig für diesen Call + except Exception as e_gen: + error_reason = f"Allg. Fehler: {type(e_gen).__name__} - {str(e_gen)[:100]}..." + logger.error(f"Allgemeiner Fehler beim Abrufen von {url[:100]}...: {e_gen}") + logger.debug(traceback.format_exc()) - except Exception as e_gen: - error_reason = f"Allg. Fehler: {type(e_gen).__name__} - {str(e_gen)[:100]}..." - logger.error(f"Allgemeiner Fehler beim Abrufen von {url[:100]}...: {e_gen}") - logger.debug(traceback.format_exc()) - break # Allgemeiner Fehler ist endgültig für diesen Call - # --- ANPASSUNG ENDE --- - - # Wenn nach allen Versuchen keine gueltige Response erhalten wurde - if response is None or error_reason: - # Gebe spezifischen Fehlerwert zurueck + if return_marker: + logger.warning(f"Markiere URL {url[:100]}... zur erneuten Prüfung (Grund: {error_reason}).") + return URL_CHECK_MARKER # <<< Speziellen Marker zurückgeben + elif response is None or error_reason: return f"k.A. ({error_reason})" - # --- Ab hier: Verarbeitung der erfolgreichen Response --- + # --- Ab hier: Verarbeitung der erfolgreichen Response (Code bleibt gleich wie vorher) --- try: response.encoding = response.apparent_encoding soup = BeautifulSoup(response.text, getattr(Config, 'HTML_PARSER', 'html.parser')) @@ -2228,8 +2225,8 @@ def get_website_raw(url, max_length=20000, verify_cert=True): # verify_cert Defa logger.warning(f"WARNUNG: Extrahierter Text fuer {url[:100]}... scheint nur Cookie-Banner zu sein (Laenge {len(text)}, {keyword_hits} Keywords). Verwerfe Text.") return "k.A. (Nur Cookie-Banner erkannt)" - if len(text.split()) < 10 or len(text) < 50: - pass + if len(text.split()) < 10 or len(text) < 50: # Ursprünglich 50, aber das ist sehr restriktiv für kurze Seiten + pass # Nur kurze Texte sind nicht unbedingt ein Fehler result = text[:max_length] logger.debug(f"Website {url[:100]}... erfolgreich gescrapt. Extrahierter Text (Laenge {len(result)}).") @@ -2243,7 +2240,6 @@ def get_website_raw(url, max_length=20000, verify_cert=True): # verify_cert Defa logger.debug(traceback.format_exc()) return f"k.A. (Fehler Parsing: {str(e_parse)[:50]}...)" - # ============================================================================== # Ende Website Raw Scraping Funktion Block # ============================================================================== @@ -2342,6 +2338,75 @@ def scrape_website_details(url): return f"k.A. (Fehler: {str(e)[:100]}...)" # Signalisiert Fehler (gekuerzt) +def is_valid_wikipedia_article_url(url_to_check, lang=None): + """ + Prueft, ob eine gegebene URL zu einem gueltigen, existierenden Wikipedia-Artikel + fuehrt (keine Begriffsklaerung, kein Fehler). + Nutzt die wikipedia-Bibliothek. + + Args: + url_to_check (str): Die zu pruefende Wikipedia-URL. + lang (str, optional): Die Sprache der Wikipedia (z.B. 'de', 'en'). + Wenn None, wird die aktuell in der wikipedia-Bibliothek + gesetzte Sprache verwendet. + + Returns: + bool: True, wenn die URL zu einem gueltigen Artikel fuehrt, sonst False. + """ + logger = logging.getLogger(__name__) + if not url_to_check or not isinstance(url_to_check, str) or "wikipedia.org/wiki/" not in url_to_check.lower(): + logger.debug(f"is_valid_wikipedia_article_url: Ungueltige URL-Struktur: {url_to_check[:100]}...") + return False + + original_lang = None + if lang: + try: + original_lang = wikipedia.get_lang() # Speichere aktuelle Sprache + wikipedia.set_lang(lang) + logger.debug(f"Temporaer Wikipedia-Sprache auf '{lang}' gesetzt für Validierung.") + except Exception as e_lang: + logger.warning(f"Konnte Wikipedia-Sprache nicht auf '{lang}' setzen für Validierung: {e_lang}") + # Fahre mit der global eingestellten Sprache fort + + + is_valid = False + try: + # Extrahiere den Titel aus der URL + title_part = url_to_check.split('/wiki/', 1)[1].split('#')[0] + title = unquote(title_part).replace('_', ' ') + + logger.debug(f"Validiere Wikipedia-Artikel: '{title[:100]}...' (URL: {url_to_check[:100]}...)") + # Versuche, die Seite zu laden. auto_suggest=False, um keine alternativen Vorschlaege zu bekommen. + # preload=True laedt den Inhalt direkt, um Fehler frueh zu erkennen. + page = wikipedia.page(title, auto_suggest=False, preload=True) + # Wenn keine Exception geworfen wurde, existiert die Seite. + # Wir nehmen an, dass es ein gueltiger Artikel ist, wenn keine DisambiguationError auftritt. + # Eine genauere Pruefung, ob es wirklich ein *Unternehmens*-Artikel ist, + # wuerde die Logik von WikipediaScraper._validate_article erfordern. + is_valid = True + logger.debug(f" -> Artikel '{title[:100]}...' scheint valide zu sein (Seite geladen).") + + except wikipedia.exceptions.PageError: + logger.debug(f" -> Seite '{title[:100]}...' nicht gefunden (PageError).") + is_valid = False + except wikipedia.exceptions.DisambiguationError as e_disamb: + logger.debug(f" -> Seite '{title[:100]}...' ist eine Begriffsklaerungsseite. Optionen: {str(e_disamb.options)[:100]}...") + is_valid = False # Begriffsklaerungen sind keine direkten Artikel + except Exception as e: + logger.error(f" -> Unerwarteter Fehler bei Validierung von '{title[:100]}...': {type(e).__name__} - {e}") + logger.debug(traceback.format_exc()) + is_valid = False + finally: + if original_lang: # Setze Sprache zurueck, falls sie geaendert wurde + try: + wikipedia.set_lang(original_lang) + logger.debug(f"Wikipedia-Sprache zurueck auf '{original_lang}' gesetzt.") + except Exception as e_lang_reset: + logger.warning(f"Konnte Wikipedia-Sprache nicht zurueck auf '{original_lang}' setzen: {e_lang_reset}") + + return is_valid + + # ============================================================================== # Ende Website Details Scraping Funktion Block # ============================================================================== @@ -7253,6 +7318,173 @@ class DataProcessor: # Ende DataProcessor Klasse Batch: SerpAPI Suchen & Contacts Block # ============================================================================== + + # ========================================================================== + # === Utility Methods (URL Check & Update) ================================= + # ========================================================================== + + def process_url_check(self, start_sheet_row=None, end_sheet_row=None, limit=None): + """ + Sucht nach Zeilen, die in Spalte AR mit URL_CHECK_MARKER oder bekannten "k.A. (Fehler...)" + Mustern markiert sind UND bei denen der AY-Timestamp (SerpAPI Wiki Search Timestamp) leer ist + (außer bei URL_CHECK_MARKER, der immer eine Suche auslöst). + Versucht, eine neue URL ueber SerpAPI zu finden. + Wenn erfolgreich und URL ist NEU: Aktualisiert D, loescht AR, setzt ReEval-Flag (A) und loescht Timestamps. + Wenn URL identisch oder keine neue URL gefunden: AR wird entsprechend aktualisiert. + Setzt immer den AY-Timestamp (als Timestamp der URL-Prüfung). + + Args: + start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (ab Zeile 7). + end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet). + limit (int, optional): Maximale Anzahl ZU VERARBEITENDER Zeilen. Defaults to None (Unbegrenzt). + """ + self.logger.info(f"Starte Modus 'check_urls'. Sucht nach '{URL_CHECK_MARKER}' oder 'k.A. (Fehler...)' in AR. Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") + + if not self.sheet_handler.load_data(): + self.logger.error("Fehler beim Laden der Daten fuer URL Check.") + return + + all_data = self.sheet_handler.get_all_data_with_headers() + header_rows = self.sheet_handler._header_rows + total_sheet_rows = len(all_data) + + if start_sheet_row is None: start_sheet_row = header_rows + 1 + if end_sheet_row is None: end_sheet_row = total_sheet_rows + + self.logger.info(f"Suchbereich fuer URL Checks: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}.") + if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows: + self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") + return + + required_keys = [ + "Website Rohtext", "CRM Name", "CRM Website", "ReEval Flag", + "Website Scrape Timestamp", "Timestamp letzte Pruefung", + "Wikipedia Timestamp", "Wiki Verif. Timestamp", "SerpAPI Wiki Search Timestamp", + "Version" + ] + col_indices = {key: COLUMN_MAP.get(key) for key in required_keys} + if None in col_indices.values(): + missing = [k for k, v in col_indices.items() if v is None] + self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_url_check: {missing}. Breche ab.") + return + + ar_letter = self.sheet_handler._get_col_letter(col_indices["Website Rohtext"] + 1) + d_letter = self.sheet_handler._get_col_letter(col_indices["CRM Website"] + 1) + a_letter = self.sheet_handler._get_col_letter(col_indices["ReEval Flag"] + 1) + at_letter = self.sheet_handler._get_col_letter(col_indices["Website Scrape Timestamp"] + 1) + ao_letter = self.sheet_handler._get_col_letter(col_indices["Timestamp letzte Pruefung"] + 1) + an_letter = self.sheet_handler._get_col_letter(col_indices["Wikipedia Timestamp"] + 1) + ax_letter = self.sheet_handler._get_col_letter(col_indices["Wiki Verif. Timestamp"] + 1) + ay_letter = self.sheet_handler._get_col_letter(col_indices["SerpAPI Wiki Search Timestamp"] + 1) # Timestamp dieser Funktion + ap_letter = self.sheet_handler._get_col_letter(col_indices["Version"] + 1) + + ka_error_patterns = [ + "k.A.", "k.A. (Extraktion leer)", "k.A. (Nur Cookie-Banner erkannt)", + "k.A. (Kein Body gefunden)", "k.A. (Fehler Parsing:", "k.A. (Unerwarteter Fehler Task)", + "k.A. (Fehler Scraping:", "k.A. (Timeout", "k.A. (SSL Fehler", + "k.A. (Connection Error", "k.A. (HTTP Error", URL_CHECK_MARKER + ] + + all_sheet_updates = [] + processed_count = 0 + skipped_count = 0 + found_new_url_count = 0 + now_timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + for i in range(start_sheet_row, end_sheet_row + 1): + row_index_in_list = i - 1 + if row_index_in_list >= total_sheet_rows: break + + row = all_data[row_index_in_list] + if not any(cell and isinstance(cell, str) and cell.strip() for cell in row): + skipped_count += 1 + continue + + ar_value = self._get_cell_value_safe(row, "Website Rohtext").strip() + ay_timestamp_value = self._get_cell_value_safe(row, "SerpAPI Wiki Search Timestamp").strip() # Verwende den spezifischen Timestamp für diese Funktion + + processing_needed_for_row = False + is_marker_case = ar_value == URL_CHECK_MARKER + is_ka_error_case = any(pattern in ar_value for pattern in ka_error_patterns if pattern != URL_CHECK_MARKER) + + if is_marker_case: # URL_CHECK_MARKER löst immer eine Suche aus + processing_needed_for_row = True + elif is_ka_error_case and not ay_timestamp_value: # Alte k.A.-Fehler nur, wenn AY-Timestamp noch nicht gesetzt wurde + processing_needed_for_row = True + + if not processing_needed_for_row: + skipped_count += 1 + continue + + processed_count += 1 + if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit: + self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_url_check erreicht.") + break + + company_name = self._get_cell_value_safe(row, "CRM Name").strip() + old_crm_website_url = self._get_cell_value_safe(row, "CRM Website").strip() + normalized_old_crm_url = simple_normalize_url(old_crm_website_url) + + if not company_name: + self.logger.warning(f"Zeile {i}: Uebersprungen (kein Firmenname fuer Suche vorhanden).") + skipped_count += 1 + updates_for_row_skip = [{'range': f'{ay_letter}{i}', 'values': [[now_timestamp_str]]}] # Timestamp trotzdem setzen + all_sheet_updates.extend(updates_for_row_skip) + continue + + self.logger.info(f"Zeile {i}: AR='{ar_value[:50]}...'. Suche neue URL für '{company_name[:50]}...' (Aktuell D: '{old_crm_website_url[:50]}...')...") + + updates_for_row = [] + new_url_found_str = "k.A." + + try: + new_url_found_str = serp_website_lookup(company_name) + normalized_new_url = simple_normalize_url(new_url_found_str) + + if new_url_found_str != "k.A." and normalized_new_url != "k.A.": + if normalized_new_url != normalized_old_crm_url: + self.logger.info(f" -> Neue, andere URL gefunden: {new_url_found_str}. Alte war: '{old_crm_website_url}'. Bereite Update vor.") + found_new_url_count += 1 + updates_for_row.append({'range': f'{d_letter}{i}', 'values': [[new_url_found_str]]}) + updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [['']]}) + updates_for_row.append({'range': f'{a_letter}{i}', 'values': [['x']]}) + updates_for_row.append({'range': f'{at_letter}{i}', 'values': [['']]}) + updates_for_row.append({'range': f'{ao_letter}{i}', 'values': [['']]}) + updates_for_row.append({'range': f'{an_letter}{i}', 'values': [['']]}) + updates_for_row.append({'range': f'{ax_letter}{i}', 'values': [['']]}) + updates_for_row.append({'range': f'{ay_letter}{i}', 'values': [['']]}) # Wird unten explizit neu gesetzt + updates_for_row.append({'range': f'{ap_letter}{i}', 'values': [['']]}) + else: + self.logger.info(f" -> SerpAPI fand URL '{new_url_found_str}', aber diese ist identisch mit der bereits vorhandenen URL in Spalte D. Keine Änderung in D.") + updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [["k.A. (URL via SerpAPI identisch mit alter URL)"]]}) + else: + self.logger.warning(f" -> Keine neue gueltige URL via SerpAPI für '{company_name[:50]}...' gefunden. Setze AR auf 'k.A. (Keine URL bei Neusuche)'.") + updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [["k.A. (Keine URL bei Neusuche)"]]}) + + except Exception as e_serp_lookup: + self.logger.error(f"FEHLER bei SERP Website Lookup für Zeile {i} ('{company_name[:50]}...'): {e_serp_lookup}") + updates_for_row.append({'range': f'{ar_letter}{i}', 'values': [[f"k.A. (Fehler URL Suche)"]]}) + pass + + updates_for_row.append({'range': f'{ay_letter}{i}', 'values': [[now_timestamp_str]]}) # AY Timestamp immer setzen + all_sheet_updates.extend(updates_for_row) + + if len(all_sheet_updates) >= update_batch_row_limit * 3 : # Angepasst, da Anzahl Updates variiert + self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Operationen)...") + success = self.sheet_handler.batch_update_cells(all_sheet_updates) + if success: self.logger.info(f" Sheet-Update für {len(all_sheet_updates)} Operationen erfolgreich.") + all_sheet_updates = [] + + serp_delay = getattr(Config, 'SERPAPI_DELAY', 1.5) + time.sleep(serp_delay) + + if all_sheet_updates: + self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Operationen)...") + success = self.sheet_handler.batch_update_cells(all_sheet_updates) + if success: self.logger.info(f"FINALES Sheet-Update erfolgreich.") + + self.logger.info(f"Modus 'check_urls' abgeschlossen. {processed_count} Zeilen mit Marker/Fehler verarbeitet, {found_new_url_count} neue URLs gefunden, {skipped_count} Zeilen uebersprungen.") + # ========================================================================== # === Utility Methods (ML Data Prep & Training) ============================ # ========================================================================== @@ -8969,6 +9201,7 @@ def main(): "Einzelne Dienstprogramme / Suchen": [ "find_wiki_serp", # Nutzt process_find_wiki_serp (Block 30) "website_lookup", # Nutzt process_serp_website_lookup (Block 30) + "check_urls", # <<< NEUER MODUS HIER EINFÜGEN "contacts", # Nutzt process_contact_search (Block 30) "update_wiki_suggestions", # Nutzt process_wiki_updates_from_chatgpt (Block 32) "wiki_reextract_missing_an", # Nutzt process_wiki_reextract_missing_an (Block 32) @@ -9395,6 +9628,13 @@ def main(): limit=limit_arg # Kann manuell gesetzt werden ) + elif selected_mode == "check_urls": + data_processor.process_url_check( + start_sheet_row=start_row_arg, + end_sheet_row=end_row_arg, + limit=limit_arg + ) + elif selected_mode == "contacts": # Nutzt process_contact_search (Block 30) # contacts sucht leere AM. Nutzt limit. Start/Endzeile koennen manuell gesetzt werden. data_processor.process_contact_search(