v1.7.2: behebung OpenAI Scope, URL-Neusuche

feat: Verbesserte Fehlerbehandlung Website-Scraping & URL-Neusuche (v1.7.2)

Diese Version behebt kritische Fehler im Zusammenhang mit dem OpenAI-Modul und verbessert die Robustheit des Website-Scrapings erheblich.

**Fehlerbehebungen:**
- **OpenAI `NameError`:** Behoben durch expliziten globalen Import von `openai` und Anpassung der Exception-Behandlung im `retry_on_failure`-Decorator. OpenAI-Calls (Zusammenfassung, Branchenbewertung) funktionieren nun korrekt.
- **Wikipedia `TypeError`:** Behoben durch Deaktivieren der internen Ratenbegrenzung der `wikipedia`-Bibliothek, da diese bei der Initialisierung der Ratenbegrenzungsvariablen einen Fehler verursachte.
- **Doppelte Definitionen:** Redundante Codeblöcke entfernt.
- **Klassen-Logger:** Korrekte Initialisierung und Verwendung von `self.logger` in den Klassen `GoogleSheetHandler`, `WikipediaScraper` und `DataProcessor` implementiert, um `NameError` für `logger` zu beheben.
- **Funktionsaufrufe:** Korrektur kleinerer Fehler in Funktionsaufrufen (`summarize_batch_openai`, `_scrape_raw_text_task`, `get_numeric_filter_value`).
- **Tippfehler:** `selflogger` zu `self.logger` korrigiert.

**Neue Features & Verbesserungen:**
- **Verbesserte Fehlerbehandlung `get_website_raw`:**
    - Standardmäßige Deaktivierung der SSL-Zertifikatsprüfung (`verify=False`) für pragmatischeres Scraping.
    - Implementierung von spezifischeren Fehlermeldungen (z.B. "k.A. (Timeout)", "k.A. (SSL Fehler)", "k.A. (HTTP Error 403)") für eine bessere Fehleranalyse direkt im Sheet.
    - Einführung eines Markers `URL_CHECK_NEEDED` für URLs, die beim Scraping auf fundamentale Probleme (ConnectionError, 404) hinweisen.
- **User-Agent Rotation:** Eingeführt in `get_website_raw`, um die Wahrscheinlichkeit von 403-Fehlern durch Bot-Erkennung zu reduzieren.
- **Neuer Modus `check_urls`:**
    - Implementiert in `DataProcessor.process_url_check`.
    - Sucht nach Zeilen mit dem `URL_CHECK_MARKER` oder generischen "k.A. (Fehler...)"-Einträgen in der Rohtext-Spalte (AR), bei denen der AY-Timestamp (SerpAPI Wiki Search Timestamp) noch nicht gesetzt ist.
    - Führt für diese Zeilen `serp_website_lookup` aus, um eine neue URL zu finden.
    - Bei Fund einer *neuen und anderen* URL: Aktualisiert Spalte D, leert AR, setzt ReEval-Flag (A) und löscht abhängige Timestamps (AT, AO, AN, AX, AP) zur erneuten Verarbeitung.
    - Bei identischer oder keiner neuen URL: Aktualisiert AR mit entsprechender Info.
    - Setzt immer den AY-Timestamp, um den Prüfversuch zu dokumentieren.
- **Funktion `is_valid_wikipedia_article_url`:** Globale Hilfsfunktion implementiert, um die Gültigkeit von Wikipedia-URLs zu prüfen (existierender Artikel, keine Begriffsklärung). Wird von `process_wiki_updates_from_chatgpt` verwendet.

**Bekannte offene Punkte:**
- ML-Modell und Imputer-Dateien müssen noch erstellt werden (`technician_decision_tree_model.pkl`, `median_imputer.pkl`). Aktuelle Fehler diesbezüglich sind erwartet.
- Implementierung der Platzhalter-Funktionen für FSM, Mitarbeiter- und Umsatzschätzung via OpenAI steht noch aus.
This commit is contained in:
2025-05-09 06:52:15 +00:00
parent 09d6fc2697
commit 80e1b60825

View File

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