v1.6.7: Behebt strukturelle/Syntax-Fehler; passt Filter für Wiki-Suche via SerpAPI an

- Inkrementiere Versionsnummer auf v1.6.7.
- Behebe kritischen AttributeError: Korrigiere die Einrückung für mehrere Verarbeitungsmethoden (_process_single_row, process_reevaluation_rows, process_serp_website_lookup_for_empty, process_website_details_for_marked_rows, prepare_data_for_modeling, process_rows_sequentially, process_find_wiki_with_serp), sodass diese korrekt als Methoden innerhalb der Klasse DataProcessor definiert sind.
- Behebe SyntaxError: Löse das Problem mit komplexen f-Strings in _process_single_row und potenziell anderen Stellen, indem die String-Konstruktion von Ausdrücken innerhalb der f-String-Syntax getrennt wird.
- Passe Filterlogik für Modus 'find_wiki_serp' an: Die SerpAPI-Suche nach fehlenden Wiki-URLs (M=k.A./leer) wird nun ausgelöst, wenn (CRM Umsatz (J) > 200 Mio ODER CRM Anzahl Mitarbeiter (K) > 500). Implementiere robuste numerische Extraktion für J und K innerhalb der Filterlogik.
- Stelle sicher, dass SerpAPI Wiki Search Timestamp (AY) immer nach einem Suchversuch im Modus 'find_wiki_serp' gesetzt wird, unabhängig vom Ergebnis.
- Diverse Logging-Anpassungen für Klarheit und Debugging (z.B. im Wiki-Verarbeitungsschritt).
This commit is contained in:
2025-04-23 05:18:30 +00:00
parent 808a015904
commit bf5db76232

View File

@@ -1,22 +1,14 @@
#!/usr/bin/env python3
"""
v1.6.6: Füge SerpAPI-Suche für fehlende Wiki-URLs großer Firmen hinzu
v1.6.7: Behebt strukturelle/Syntax-Fehler; passt Filter für Wiki-Suche via SerpAPI an
Git-Änderungsbeschreibung:
- Füge neuen Betriebsmodus `--mode find_wiki_serp` hinzu.
- Implementiere neue Funktion `serp_wikipedia_lookup`, die SerpAPI nutzt, um gezielt nach Wikipedia-Artikeln für einen Firmennamen zu suchen.
- Implementiere neue Funktion `process_find_wiki_with_serp`:
- Lädt aktuelle Sheet-Daten.
- Filtert Zeilen, bei denen Spalte M (Wiki URL) leer/'k.A.' ist UND Spalte K (CRM Mitarbeiter) einen Schwellenwert (Standard: 500) überschreitet.
- Ruft `serp_wikipedia_lookup` für gefilterte Zeilen auf.
- Bei erfolgreicher URL-Findung:
- Schreibt die gefundene URL in Spalte M.
- Setzt Flag 'x' in Spalte A (ReEval Flag).
- Löscht Timestamps in Spalten AN (Wikipedia Timestamp) und AO (Timestamp letzte Prüfung).
- Führt gebündelte Sheet-Updates am Ende durch.
- Integriere den neuen Modus `find_wiki_serp` in die Argumentenverarbeitung und Ausführungslogik der `main`-Funktion.
- Füge notwendige Imports hinzu und stelle sicher, dass die neuen Funktionen Logging verwenden.
- Aktualisiere Versionsnummer in `Config.VERSION` auf v1.6.6.
- Inkrementiere Versionsnummer auf v1.6.7.
- Behebe kritischen AttributeError: Korrigiere die Einrückung für mehrere Verarbeitungsmethoden (_process_single_row, process_reevaluation_rows, process_serp_website_lookup_for_empty, process_website_details_for_marked_rows, prepare_data_for_modeling, process_rows_sequentially, process_find_wiki_with_serp), sodass diese korrekt als Methoden innerhalb der Klasse DataProcessor definiert sind.
- Behebe SyntaxError: Löse das Problem mit komplexen f-Strings in _process_single_row und potenziell anderen Stellen, indem die String-Konstruktion von Ausdrücken innerhalb der f-String-Syntax getrennt wird.
- Passe Filterlogik für Modus 'find_wiki_serp' an: Die SerpAPI-Suche nach fehlenden Wiki-URLs (M=k.A./leer) wird nun ausgelöst, wenn (CRM Umsatz (J) > 200 Mio ODER CRM Anzahl Mitarbeiter (K) > 500). Implementiere robuste numerische Extraktion für J und K innerhalb der Filterlogik.
- Stelle sicher, dass SerpAPI Wiki Search Timestamp (AY) immer nach einem Suchversuch im Modus 'find_wiki_serp' gesetzt wird, unabhängig vom Ergebnis.
- Diverse Logging-Anpassungen für Klarheit und Debugging (z.B. im Wiki-Verarbeitungsschritt).
"""
import os
@@ -73,7 +65,7 @@ PATTERNS_FILE_JSON = "technician_patterns.json" # Optional
# ==================== KONFIGURATION ====================
class Config:
# ... (Alle deine bisherigen Config-Einstellungen) ...
VERSION = "v1.6.6" # Versionsnummer erhöhen
VERSION = "v1.6.7" # Versionsnummer erhöhen
LANG = "de"
SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo"
MAX_RETRIES = 3
@@ -368,9 +360,17 @@ def serp_wikipedia_lookup(company_name, website=None, min_score=0.4):
return None # Bei unerwarteten Fehlern None zurückgeben
# Kann als eigenständige Funktion oder Methode in DataProcessor implementiert werden
def process_find_wiki_with_serp(sheet_handler, row_limit=None, min_employees=500):
# Annahme: COLUMN_MAP ist global definiert und enthält mindestens:
# "CRM Name" (B), "CRM Anzahl Mitarbeiter" (K), "CRM Umsatz" (J),
# "Wiki URL" (M), "ReEval Flag" (A), "Wiki Absatz" (N), ..., "Wiki Verif. Timestamp" (AX), "SerpAPI Wiki Search Timestamp" (AY)
# Annahme: serp_wikipedia_lookup und simple_normalize_url sind definiert und nutzen logging/retry
# Annahme: clean_text und normalize_company_name sind definiert
# Annahme: get_valid_numeric (oder ähnliche Logik zur numerischen Extraktion) ist verfügbar/implementiert.
def process_find_wiki_with_serp(sheet_handler, row_limit=None, min_employees=500, min_umsatz=200):
"""
Sucht fehlende Wikipedia-URLs (Spalte M = k.A.) für Unternehmen mit > min_employees
Sucht fehlende Wikipedia-URLs (Spalte M = k.A.) für Unternehmen mit
(Umsatz CRM > min_umsatz ODER Mitarbeiter CRM > min_employees)
über SerpAPI und trägt gefundene URLs in Spalte M ein. Setzt ReEval-Flag (A)
und löscht abhängige Wiki-Spalten (N-V, AN, AO, AP, AX).
Merkt sich in Spalte AY, wann die Suche durchgeführt wurde.
@@ -378,149 +378,218 @@ def process_find_wiki_with_serp(sheet_handler, row_limit=None, min_employees=500
Args:
sheet_handler (GoogleSheetHandler): Initialisierte Instanz.
row_limit (int, optional): Maximale Anzahl zu prüfender Zeilen. Defaults to None.
min_employees (int, optional): Mindestanzahl Mitarbeiter (Spalte K) als Filter. Defaults to 500.
min_employees (int, optional): Mindestanzahl Mitarbeiter (Spalte K) als Teilfilter. Defaults to 500.
min_umsatz (int, optional): Mindestumsatz (Spalte J) als Teilfilter. Defaults to 200.
"""
logging.info(f"Starte Modus 'find_wiki_serp': Suche fehlende Wiki-URLs für Firmen > {min_employees} MA...")
logging.info(f"Starte Modus 'find_wiki_serp': Suche fehlende Wiki-URLs für Firmen mit (Umsatz CRM > {min_umsatz} ODER Mitarbeiter CRM > {min_employees})...")
if not sheet_handler.load_data(): return
all_data = sheet_handler.get_all_data_with_headers()
if not all_data or len(all_data) <= 5:
# Annahme: 5 Header-Zeilen
header_rows = 5
if not all_data or len(all_data) <= header_rows:
logging.warning("Keine oder zu wenige Daten im Sheet für 'find_wiki_serp' gefunden.")
return
header_rows = 5
data_rows = all_data[header_rows:]
data_rows = all_data[header_rows:] # Daten ab Zeile 6
# Benötigte Spaltenindizes holen (inkl. aller zu löschenden Spalten)
try:
col_indices = {
"A": COLUMN_MAP["ReEval Flag"],
"K": COLUMN_MAP["CRM Anzahl Mitarbeiter"],
"M": COLUMN_MAP["Wiki URL"],
"B": COLUMN_MAP["CRM Name"],
"N": COLUMN_MAP["Wiki Absatz"], # NEU zum Löschen
"O": COLUMN_MAP["Wiki Branche"], # NEU zum Löschen
"P": COLUMN_MAP["Wiki Umsatz"], # NEU zum Löschen
"Q": COLUMN_MAP["Wiki Mitarbeiter"], # NEU zum Löschen
"R": COLUMN_MAP["Wiki Kategorien"], # NEU zum Löschen
"S": COLUMN_MAP["Chat Wiki Konsistenzprüfung"], # NEU zum Löschen
"T": COLUMN_MAP["Chat Begründung Wiki Inkonsistenz"], # NEU zum Löschen
"U": COLUMN_MAP["Chat Vorschlag Wiki Artikel"], # NEU zum Löschen
"V": COLUMN_MAP["Begründung bei Abweichung"], # NEU zum Löschen
"AN": COLUMN_MAP["Wikipedia Timestamp"],
"AO": COLUMN_MAP["Timestamp letzte Prüfung"],
"AP": COLUMN_MAP["Version"], # NEU zum Löschen
"AX": COLUMN_MAP["Wiki Verif. Timestamp"], # NEU zum Löschen
"AY": COLUMN_MAP["SerpAPI Wiki Search Timestamp"]
}
col_letters = {key: sheet_handler._get_col_letter(idx + 1) for key, idx in col_indices.items()}
except KeyError as e:
logging.critical(f"FEHLER: Benötigter Spaltenschlüssel '{e}' nicht in COLUMN_MAP gefunden! Modus abgebrochen.")
return
except Exception as e:
logging.critical(f"FEHLER beim Holen der Spaltenbuchstaben: {e}")
return
# Verwenden Sie hier das COLUMN_MAP robust
col_indices = {}
required_keys = [
"ReEval Flag", "CRM Anzahl Mitarbeiter", "CRM Umsatz", "Wiki URL", "CRM Name",
"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", "Wikipedia Timestamp", "Timestamp letzte Prüfung",
"Version", "Wiki Verif. Timestamp", "SerpAPI Wiki Search Timestamp"
]
all_keys_found = True
for key in required_keys:
idx = COLUMN_MAP.get(key)
col_indices[key] = idx
if idx is None:
logging.critical(f"FEHLER: Benötigter Spaltenschlüssel '{key}' nicht in COLUMN_MAP gefunden! Modus abgebrochen.")
all_keys_found = False
if not all_keys_found:
return # Abbruch, da Spalten fehlen
# Hilfsfunktion zur Konvertierung Spaltenindex -> Buchstabe
col_letters = {key: sheet_handler._get_col_letter(idx + 1) for key, idx in col_indices.items()}
all_sheet_updates = []
processed_rows = 0
found_urls = 0
skipped_timestamp_ay = 0
skipped_employee_count = 0
processed_rows_count = 0 # Zählt Zeilen, für die SerpAPI versucht wurde
found_urls_count = 0 # Zählt Zeilen, wo eine URL gefunden wurde
skipped_timestamp_ay_count = 0
skipped_size_count = 0
skipped_m_filled_count = 0
now_timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
for idx, row in enumerate(data_rows):
row_num_in_sheet = idx + header_rows + 1
# --- Hilfsfunktion zur sicheren numerischen Extraktion (adaptiert von prepare_data_for_modeling) ---
def safe_numeric_extract(value_str):
if value_str is None or pd.isna(value_str) or str(value_str).strip() == '': return 0 # Return 0, nicht NaN, für die Vergleichslogik
try:
# Annahme: clean_text existiert
processed_value = clean_text(str(value_str))
if processed_value == "k.A.": return 0
if row_limit is not None and processed_rows >= row_limit:
processed_value = re.sub(r'(?i)^\s*(ca\.?|circa|rund|etwa|über|unter|mehr als|weniger als|bis zu)\s+', '', processed_value)
processed_value = re.sub(r'[€$£¥]', '', processed_value).strip()
processed_value = re.split(r'\s*(-||bis)\s*', processed_value, 1)[0].strip()
# Entferne Punkte UND Apostrophe als Tausendertrenner, ersetze Komma durch Punkt
processed_value = processed_value.replace('.', '').replace("'", "").replace(',', '.')
match = re.search(r'([\d.]+)', processed_value)
if not match: return 0 # Keine numerischen Zeichen gefunden
num_str = match.group(1)
if not num_str or num_str == '.': return 0
num = float(num_str)
# Einheiten-Multiplikatoren (Mrd, Mio, Tsd) - Wichtig für Umsatz
multiplier = 1.0
original_lower = str(value_str).lower() # Nutze den Originalstring für Einheiten
if "mrd" in original_lower or "milliarden" in original_lower or "billion" in original_lower: multiplier = 1000000000.0
elif "mio" in original_lower or "millionen" in original_lower or "mill." in original_lower: multiplier = 1000000.0
elif "tsd" in original_lower or "tausend" in original_lower: multiplier = 1000.0
num = num * multiplier
# Für den Vergleich (Umsatz in Mio, Mitarbeiter int)
# Umsatz soll > 200 MIO sein, also num direkt mit 200,000,000 vergleichen
# Mitarbeiter > 500, num direkt mit 500 vergleichen
# Geben Sie den Rohwert nach Multiplikator zurück
return num if num > 0 else 0 # Nur positive Werte zählen
except Exception as e:
logging.debug(f"Fehler in safe_numeric_extract für Wert '{str(value_str)[:50]}...': {e}")
return 0
# --- Ende Hilfsfunktion ---
# Iteriere durch die Datenzeilen
for idx, row in enumerate(data_rows):
row_num_in_sheet = idx + header_rows + 1 # 1-basierte Sheet-Zeilennummer
# Limit-Prüfung
if row_limit is not None and processed_rows_count >= row_limit:
logging.info(f"Zeilenlimit ({row_limit}) für durchgeführte Suchen erreicht.")
break
# Prüfe AY Timestamp
ts_ay_val = row[col_indices["AY"]] if len(row) > col_indices["AY"] else ""
# Sicherstellen, dass die Zeile lang genug für alle benötigten Spalten ist
max_needed_idx = max(col_indices.values())
if len(row) <= max_needed_idx:
logging.debug(f"Zeile {row_num_in_sheet}: Übersprungen (Zeile zu kurz für benötigte Spalten, erwartet > {max_needed_idx}, hat {len(row)}).")
continue
# Prüfe AY Timestamp: Überspringe, wenn SerpAPI Suche für diese Zeile schon versucht wurde
ts_ay_val = row[col_indices["SerpAPI Wiki Search Timestamp"]]
if ts_ay_val and ts_ay_val.strip():
skipped_timestamp_ay += 1
skipped_timestamp_ay_count += 1
continue
try:
# Mitarbeiterzahl prüfen
ma_val_str = row[col_indices["K"]] if len(row) > col_indices["K"] else "0"
try:
ma_val_str_cleaned = re.sub(r"[^\d]", "", ma_val_str)
ma_val = int(ma_val_str_cleaned) if ma_val_str_cleaned else 0
except ValueError: ma_val = 0
if ma_val <= min_employees:
skipped_employee_count += 1
continue
# Wiki URL (M) prüfen
m_value = row[col_indices["M"]] if len(row) > col_indices["M"] else ""
if m_value and m_value.strip().lower() != "k.a.":
skipped_m_filled_count += 1
continue
# Kandidat gefunden
company_name = row[col_indices["B"]] if len(row) > col_indices["B"] else ""
if not company_name:
logging.warning(f"Zeile {row_num_in_sheet}: Übersprungen, kein Firmenname für Suche vorhanden.")
continue
# SerpAPI Suche
logging.info(f"Zeile {row_num_in_sheet}: Suche Wiki-URL für '{company_name}' (MA: {ma_val})...")
wiki_url_found = serp_wikipedia_lookup(company_name)
processed_rows += 1
time.sleep(1.5)
# Updates vorbereiten
# Timestamp AY IMMER setzen
row_updates = [{'range': f'{col_letters["AY"]}{row_num_in_sheet}', 'values': [[now_timestamp_str]]}]
if wiki_url_found:
logging.info(f" -> URL gefunden: {wiki_url_found}. Bereite Update vor (Setze M, A; Lösche N-V, AN, AO, AP, AX).")
found_urls += 1
# Zusätzliche Updates für gefundene URL
row_updates.extend([
{'range': f'{col_letters["M"]}{row_num_in_sheet}', 'values': [[wiki_url_found]]}, # URL setzen
{'range': f'{col_letters["A"]}{row_num_in_sheet}', 'values': [['x']]}, # ReEval Flag
# --- Spalten leeren ---
{'range': f'{col_letters["N"]}{row_num_in_sheet}', 'values': [['']]},
{'range': f'{col_letters["O"]}{row_num_in_sheet}', 'values': [['']]},
{'range': f'{col_letters["P"]}{row_num_in_sheet}', 'values': [['']]},
{'range': f'{col_letters["Q"]}{row_num_in_sheet}', 'values': [['']]},
{'range': f'{col_letters["R"]}{row_num_in_sheet}', 'values': [['']]},
{'range': f'{col_letters["S"]}{row_num_in_sheet}', 'values': [['']]},
{'range': f'{col_letters["T"]}{row_num_in_sheet}', 'values': [['']]},
{'range': f'{col_letters["U"]}{row_num_in_sheet}', 'values': [['']]},
{'range': f'{col_letters["V"]}{row_num_in_sheet}', 'values': [['']]},
{'range': f'{col_letters["AN"]}{row_num_in_sheet}', 'values': [['']]},
{'range': f'{col_letters["AO"]}{row_num_in_sheet}', 'values': [['']]},
{'range': f'{col_letters["AP"]}{row_num_in_sheet}', 'values': [['']]},
{'range': f'{col_letters["AX"]}{row_num_in_sheet}', 'values': [['']]}
])
else:
logging.info(f" -> Keine Wiki-URL für '{company_name}' via SerpAPI gefunden.")
# Nur AY Timestamp wird geschrieben
all_sheet_updates.extend(row_updates)
except Exception as e:
logging.exception(f"Unerwarteter Fehler bei Verarbeitung von Zeile {row_num_in_sheet}: {e}")
# Prüfe Wiki URL (M): Überspringe, wenn bereits gefüllt (nicht k.A. oder leer)
m_value = row[col_indices["Wiki URL"]]
if m_value and str(m_value).strip().lower() != "k.a.": # Jetzt auch 'leer' abfangen? Nein, '' ist schon abgedeckt
skipped_m_filled_count += 1
continue
# --- Prüfe Unternehmensgröße (J Umsatz ODER K Mitarbeiter) ---
umsatz_val_str = row[col_indices["CRM Umsatz"]]
ma_val_str = row[col_indices["CRM Anzahl Mitarbeiter"]]
umsatz_val_num = safe_numeric_extract(umsatz_val_str)
ma_val_num = safe_numeric_extract(ma_val_str)
# Filterlogik: Umsatz > min_umsatz ODER Mitarbeiter > min_employees
# Wenn *nicht* (Umsatz > min_umsatz ODER Mitarbeiter > min_employees), dann überspringe
if not (umsatz_val_num > min_umsatz * 1000000 or ma_val_num > min_employees):
logging.debug(f"Zeile {row_num_in_sheet}: Übersprungen (Größe nicht ausreichend. Umsatz: {umsatz_val_num:.0f}, MA: {ma_val_num}).")
skipped_size_count += 1
continue
# --- Ende Größenprüfung ---
# Kandidat gefunden: M leer/k.A., AY leer, und Größe passt
company_name = row[col_indices["CRM Name"]] if len(row) > col_indices["CRM Name"] else ""
if not company_name or str(company_name).strip() == "":
logging.warning(f"Zeile {row_num_in_sheet}: Übersprungen, kein Firmenname für Suche vorhanden.")
# Setze AY Timestamp, damit wir nicht immer wieder versuchen
ay_col_letter = col_letters["SerpAPI Wiki Search Timestamp"]
all_sheet_updates.append({'range': f'{ay_col_letter}{row_num_in_sheet}', 'values': [[now_timestamp_str]]})
continue
# SerpAPI Suche
logging.info(f"Zeile {row_num_in_sheet}: Suche Wiki-URL für '{company_name}' (Umsatz: {umsatz_val_num:.0f}, MA: {ma_val_num})...")
processed_rows_count += 1 # Zähle VOR dem Call, dass ein Versuch gestartet wird
# Annahme: serp_wikipedia_lookup existiert und nutzt logging/retry
# website_url wird nicht direkt im SerpAPI Lookup verwendet, kann aber als Kontext hilfreich sein
website_url = row[COLUMN_MAP.get("CRM Website", -1)] if COLUMN_MAP.get("CRM Website", -1) != -1 and len(row) > COLUMN_MAP.get("CRM Website", -1) else None
wiki_url_found = serp_wikipedia_lookup(company_name, website=website_url)
# Updates vorbereiten
# Timestamp AY IMMER setzen, nachdem der Versuch gemacht wurde
ay_col_letter = col_letters["SerpAPI Wiki Search Timestamp"]
all_sheet_updates.append({'range': f'{ay_col_letter}{row_num_in_sheet}', 'values': [[now_timestamp_str]]})
if wiki_url_found and wiki_url_found.strip() and wiki_url_found != "k.A.":
logging.info(f" -> URL gefunden: {wiki_url_found}. Bereite Update vor (Setze M, A; Lösche N-V, AN, AO, AP, AX).")
found_urls_count += 1
# Zusätzliche Updates für gefundene URL
m_l = col_letters["Wiki URL"]
a_l = col_letters["ReEval Flag"]
# Spalten N-V leeren
n_l = col_letters["Wiki Absatz"]
v_l = col_letters["Begründung bei Abweichung"]
# Timestamps AN, AO, AX, Version AP leeren
an_l = col_letters["Wikipedia Timestamp"]
ao_l = col_letters["Timestamp letzte Prüfung"]
ap_l = col_letters["Version"]
ax_l = col_letters["Wiki Verif. Timestamp"]
all_sheet_updates.extend([
{'range': f'{m_l}{row_num_in_sheet}', 'values': [[wiki_url_found]]}, # URL setzen in M
{'range': f'{a_l}{row_num_in_sheet}', 'values': [['x']]}, # ReEval Flag setzen in A
# --- Spalten leeren, damit sie neu befüllt werden ---
# Range N:V leeren
{'range': f'{n_l}{row_num_in_sheet}:{v_l}{row_num_in_sheet}', 'values': [[''] * (col_indices["Begründung bei Abweichung"] - col_indices["Wiki Absatz"] + 1)]},
{'range': f'{an_l}{row_num_in_sheet}', 'values': [['']]}, # AN leeren
{'range': f'{ao_l}{row_num_in_sheet}', 'values': [['']]}, # AO leeren
{'range': f'{ap_l}{row_num_in_sheet}', 'values': [['']]}, # AP leeren
{'range': f'{ax_l}{row_num_in_sheet}', 'values': [['']]} # AX leeren
])
else:
logging.info(f" -> Keine Wiki-URL für '{company_name}' via SerpAPI gefunden.")
# Nur AY Timestamp wird gesetzt, was bereits oben passiert ist.
# Kleiner Sleep nach jeder SerpAPI-Suche, auch wenn es ein Retry gibt
# Der Decorator kümmert sich um Retries mit Backoff, dies ist nur eine globale Rate-Limit-Vorsorge.
time.sleep(getattr(Config, 'RETRY_DELAY', 5) * 0.3)
# --- Batch Update am Ende ---
if all_sheet_updates:
logging.info(f"Sende Batch-Update für {processed_rows} geprüfte Zeilen ({found_urls} URLs gefunden, {len(all_sheet_updates)} Zellen)...")
logging.info(f"Sende Batch-Update für {processed_rows_count} geprüfte Zeilen ({found_urls_count} URLs gefunden, {len(all_sheet_updates)} Zellen)...")
# Annahme: sheet_handler.batch_update_cells existiert und nutzt logging/retry
success = sheet_handler.batch_update_cells(all_sheet_updates)
if success:
logging.info(f"Sheet-Update für 'find_wiki_serp' erfolgreich.")
# Der else-Fall wird von batch_update_cells geloggt
else:
logging.info("Keine Zeilen gefunden, für die eine SerpAPI Wiki-Suche durchgeführt werden musste/konnte.")
logging.info(f"Modus 'find_wiki_serp' abgeschlossen.")
logging.info(f" Durchgeführte Suchen in diesem Lauf: {processed_rows}")
logging.info(f" Gefundene & eingetragene URLs: {found_urls}")
logging.info(f" Übersprungen (AY bereits gesetzt): {skipped_timestamp_ay}")
logging.info(f" Übersprungen (MA <= {min_employees}): {skipped_employee_count}")
logging.info(f" Durchgeführte Suchen in diesem Lauf: {processed_rows_count}")
logging.info(f" Gefundene & eingetragene URLs: {found_urls_count}")
logging.info(f" Übersprungen (AY bereits gesetzt): {skipped_timestamp_ay_count}")
logging.info(f" Übersprungen (Größe nicht ausreichend): {skipped_size_count}")
logging.info(f" Übersprungen (M bereits gefüllt): {skipped_m_filled_count}")
def prepare_data_for_modeling(sheet_handler):