This commit is contained in:
2025-04-16 12:43:30 +00:00
parent eaf388bf40
commit 5b5507dc32

View File

@@ -947,20 +947,23 @@ def token_count(text):
return len(text.split()) return len(text.split())
# ==================== GOOGLE SHEET HANDLER ==================== # ==================== GOOGLE SHEET HANDLER ====================
# Annahmen:
# - Globale Variablen/Konstanten: retry_on_failure, Config, CREDENTIALS_FILE, Config.SHEET_URL, debug_print, COLUMN_MAP
# - COLUMN_MAP enthält den Schlüssel "Website Scrape Timestamp" mit dem korrekten Index (45)
class GoogleSheetHandler: class GoogleSheetHandler:
def __init__(self): def __init__(self):
self.sheet = None self.sheet = None
self.sheet_values = [] self.sheet_values = []
self.headers = [] # Um Header-Zeilen zu speichern self.headers = [] # Um Header-Zeilen zu speichern (Annahme: Zeile 1 sind die Namen)
self._connect() self._connect()
# _load_data wird jetzt nach erfolgreicher Verbindung aufgerufen
if self.sheet: if self.sheet:
self._load_data() self._load_data()
# retry_on_failure Decorator sollte hier angewendet werden
@retry_on_failure @retry_on_failure
def _connect(self): def _connect(self):
"""Stellt Verbindung zum Google Sheet her.""" """Stellt Verbindung zum Google Sheet her."""
# Setze self.sheet initial auf None
self.sheet = None self.sheet = None
debug_print("Verbinde mit Google Sheets...") debug_print("Verbinde mit Google Sheets...")
try: try:
@@ -968,13 +971,16 @@ class GoogleSheetHandler:
creds = ServiceAccountCredentials.from_json_keyfile_name(CREDENTIALS_FILE, scope) creds = ServiceAccountCredentials.from_json_keyfile_name(CREDENTIALS_FILE, scope)
gc = gspread.authorize(creds) gc = gspread.authorize(creds)
sh = gc.open_by_url(Config.SHEET_URL) sh = gc.open_by_url(Config.SHEET_URL)
self.sheet = sh.sheet1 # Zugriff auf das erste Blatt (Sheet1) self.sheet = sh.sheet1
debug_print("Verbindung zu Google Sheets erfolgreich.") debug_print("Verbindung zu Google Sheets erfolgreich.")
except gspread.exceptions.APIError as e:
debug_print(f"FEHLER bei Google API Verbindung: Status {e.response.status_code} - {e.response.text[:200]}")
raise # Damit retry greift
except Exception as e: except Exception as e:
debug_print(f"FEHLER bei der Google Sheets Verbindung: {e}") debug_print(f"FEHLER bei der Google Sheets Verbindung: {type(e).__name__} - {e}")
# Hier könnte man den Fehler weitergeben oder None lassen raise # Damit retry greift
raise # Fehler weitergeben, damit retry greift oder main abbricht
# retry_on_failure Decorator sollte hier angewendet werden
@retry_on_failure @retry_on_failure
def _load_data(self): def _load_data(self):
"""Lädt alle Daten aus dem Sheet.""" """Lädt alle Daten aus dem Sheet."""
@@ -985,33 +991,46 @@ class GoogleSheetHandler:
return return
debug_print("Lade Daten aus Google Sheet...") debug_print("Lade Daten aus Google Sheet...")
self.sheet_values = self.sheet.get_all_values() self.sheet_values = self.sheet.get_all_values()
if len(self.sheet_values) >= 1: # Mindestens Header sollte da sein if len(self.sheet_values) >= 1:
self.headers = self.sheet_values[:5] # Annahme: Zeilen 1-5 sind Header # Speichere die *echten* Header-Namen aus der ersten Zeile
self.headers = self.sheet_values[0]
# Die Alignment-Demo Header (Zeilen 1-5) werden hier nicht separat gespeichert,
# sheet_values enthält alles.
else: else:
self.headers = [] self.headers = []
debug_print("Warnung: Google Sheet scheint leer zu sein.") debug_print("Warnung: Google Sheet scheint leer zu sein.")
debug_print(f"Daten geladen: {len(self.sheet_values)} Zeilen insgesamt.") debug_print(f"Daten geladen: {len(self.sheet_values)} Zeilen insgesamt.")
def get_data(self): def get_data(self):
"""Gibt die geladenen Daten zurück (ohne Header).""" """Gibt die geladenen Daten zurück (ohne die ersten 5 Header-Zeilen)."""
header_rows = 5 # Annahme: Zeile 1-5 sind Header header_rows = 5 # Definiert die Anzahl der zu überspringenden Header-Zeilen
if len(self.sheet_values) <= header_rows: if len(self.sheet_values) <= header_rows:
debug_print("Warnung in get_data: Weniger Zeilen als Header-Zeilen vorhanden.")
return [] return []
# Gibt eine Slice der Liste zurück, die die Datenzeilen enthält
return self.sheet_values[header_rows:] return self.sheet_values[header_rows:]
def get_all_data_with_headers(self): def get_all_data_with_headers(self):
"""Gibt alle Daten inklusive Header zurück.""" """Gibt alle Daten inklusive Header zurück."""
return self.sheet_values return self.sheet_values
# Angepasste Methode, um den Startindex basierend auf einer spezifischen Spalte zu finden def _get_col_letter(self, col_idx_1_based):
def get_start_row_index(self, check_column_index, min_sheet_row=7): # Entferne Standardwert hier """ Konvertiert 1-basierten Spaltenindex in Buchstaben. """
string = ""
n = col_idx_1_based
while n > 0:
n, remainder = divmod(n - 1, 26)
string = chr(65 + remainder) + string
return string
def get_start_row_index(self, check_column_key, min_sheet_row=7):
""" """
Findet den Index der ersten Zeile (0-basiert für Daten nach Header), Findet den Index der ersten Zeile (0-basiert für Daten nach Header),
ab einer Mindestzeilennummer im Sheet, in der der Timestamp in der ab einer Mindestzeilennummer im Sheet, in der der Timestamp in der
angegebenen Spalte fehlt. Spalte, die durch den *Schlüssel* in COLUMN_MAP definiert ist, fehlt.
Args: Args:
check_column_index (int): Der 0-basierte Index der zu prüfenden Spalte. check_column_key (str): Der Schlüssel in COLUMN_MAP für die zu prüfende Spalte.
min_sheet_row (int): Die 1-basierte Zeilennummer im Sheet, ab der gesucht werden soll. min_sheet_row (int): Die 1-basierte Zeilennummer im Sheet, ab der gesucht werden soll.
Returns: Returns:
@@ -1019,55 +1038,60 @@ class GoogleSheetHandler:
oder der Index nach der letzten Zeile, wenn alle gefüllt sind. oder der Index nach der letzten Zeile, wenn alle gefüllt sind.
""" """
header_rows = 5 header_rows = 5
data_rows = self.get_data() data_rows = self.get_data() # Holt Daten OHNE die 5 Header
if not data_rows: if not data_rows:
debug_print("Keine Datenzeilen vorhanden für get_start_row_index.") debug_print("Keine Datenzeilen vorhanden für get_start_row_index.")
return 0 return 0
# Hole den Spaltenindex aus COLUMN_MAP
check_column_index = COLUMN_MAP.get(check_column_key)
if check_column_index is None:
debug_print(f"FEHLER: Schlüssel '{check_column_key}' nicht in COLUMN_MAP gefunden!")
# Fallback oder Fehler werfen? Vorerst auf eine bekannte Spalte (AO) zurückfallen? Schlecht.
# Besser: None zurückgeben oder Fehler werfen, damit aufrufende Funktion es merkt.
# Hier: Gib -1 zurück als Fehlerindikator
return -1
actual_col_letter = self._get_col_letter(check_column_index + 1) # +1 für 1-basierte Konvertierung
# Berechne den 0-basierten Startindex für die *Datenliste* data_rows
search_start_index_in_data = max(0, min_sheet_row - header_rows - 1) search_start_index_in_data = max(0, min_sheet_row - header_rows - 1)
# DEBUG: Logge, welche Spalte tatsächlich geprüft wird debug_print(f"get_start_row_index: Suche ab Daten-Index {search_start_index_in_data} (Sheet-Zeile {search_start_index_in_data + header_rows + 1}) nach leerem Wert in Spalte '{check_column_key}' ({actual_col_letter}, Index {check_column_index}).")
actual_col_letter = self._get_col_letter(check_column_index + 1)
debug_print(f"get_start_row_index: Suche ab Daten-Index {search_start_index_in_data} nach leerem Timestamp in Spalte {actual_col_letter} (Index {check_column_index}).")
if search_start_index_in_data >= len(data_rows): if search_start_index_in_data >= len(data_rows):
debug_print(f"Start-Suchindex ({search_start_index_in_data}) liegt nach oder auf letzter Datenzeile ({len(data_rows)-1}).") debug_print(f"Start-Suchindex ({search_start_index_in_data}) liegt nach oder auf letzter Datenzeile ({len(data_rows)-1}). Alle vorherigen Zeilen scheinen gefüllt.")
# Prüfe die *letzte* Zeile, falls der Index genau darauf zeigt return len(data_rows) # Index nach der letzten Zeile
if search_start_index_in_data == len(data_rows) -1:
last_row = data_rows[search_start_index_in_data]
if len(last_row) <= check_column_index or not last_row[check_column_index].strip():
debug_print(f"Letzte Zeile (Daten-Index {search_start_index_in_data}) hat keinen Timestamp in Spalte {actual_col_letter}. Starte hier.")
return search_start_index_in_data
# Ansonsten sind alle vorherigen gefüllt
debug_print(f"Alle Zeilen ab Zeile {min_sheet_row} scheinen einen Zeitstempel in Spalte {actual_col_letter} zu haben. Nächster Daten-Index wäre {len(data_rows)}.")
return len(data_rows)
# Durchlaufe die Datenzeilen ab dem berechneten Startindex
for i in range(search_start_index_in_data, len(data_rows)):
row = data_rows[i]
current_sheet_row = i + header_rows + 1
for i, row in enumerate(data_rows[search_start_index_in_data:], start=search_start_index_in_data): # Prüfe den Wert in der Zielspalte
# DEBUG: Logge den geprüften Wert für die ersten paar Iterationen cell_value = None
# if i < search_start_index_in_data + 5: is_empty = True
# checked_value = row[check_column_index].strip() if len(row) > check_column_index else "INDEX_FEHLER" if len(row) > check_column_index:
# debug_print(f" -> Prüfe Zeile {i + header_rows + 1}: Wert in Spalte {actual_col_letter} = '{checked_value}'") cell_value = row[check_column_index]
if cell_value and str(cell_value).strip(): # Prüft auf nicht leer und nicht nur Whitespace
is_empty = False
if len(row) <= check_column_index or not row[check_column_index].strip(): # DEBUG Log für jede 1000ste Zeile oder wenn ein relevanter Übergang erwartet wird
actual_sheet_row = i + header_rows + 1 if i == search_start_index_in_data or i % 1000 == 0 or current_sheet_row in [2121, 2122, 8926, 8927, 8928]:
debug_print(f"Erste Zeile ab Zeile {min_sheet_row} ohne Zeitstempel in Spalte {actual_col_letter} (Index {check_column_index}) gefunden: Zeile {actual_sheet_row} (Daten-Index {i})") debug_print(f" -> Prüfe Daten-Index {i} (Sheet Zeile {current_sheet_row}): Wert in Spalte {actual_col_letter}='{cell_value}' -> Leer? {is_empty}")
return i
if is_empty:
debug_print(f"Erste Zeile ab Zeile {min_sheet_row} ohne Wert in Spalte {actual_col_letter} gefunden: Zeile {current_sheet_row} (Daten-Index {i})")
return i # Gibt den 0-basierten Index *innerhalb der Datenliste* zurück
# Wenn die Schleife durchläuft, sind alle Zeilen ab dem Start gefüllt
last_index = len(data_rows) last_index = len(data_rows)
debug_print(f"Alle Zeilen ab Zeile {min_sheet_row} haben einen Zeitstempel in Spalte {actual_col_letter}. Nächster Daten-Index wäre {last_index}.") debug_print(f"Alle Zeilen ab Daten-Index {search_start_index_in_data} (Sheet Zeile {search_start_index_in_data + header_rows + 1}) haben einen Wert in Spalte {actual_col_letter}. Nächster Daten-Index wäre {last_index}.")
return last_index return last_index
def _get_col_letter(self, col_idx):
""" Konvertiert 1-basierten Spaltenindex in Buchstaben. """
string = ""
while col_idx > 0:
col_idx, remainder = divmod(col_idx - 1, 26)
string = chr(65 + remainder) + string
return string
# --- NEU HINZUGEFÜGTE METHODE --- # --- NEU HINZUGEFÜGTE METHODE ---
# retry_on_failure Decorator sollte hier angewendet werden
@retry_on_failure @retry_on_failure
def batch_update_cells(self, update_data): def batch_update_cells(self, update_data):
""" """
@@ -1082,25 +1106,19 @@ class GoogleSheetHandler:
""" """
if not self.sheet: if not self.sheet:
debug_print("FEHLER: Keine Sheet-Verbindung für Batch-Update.") debug_print("FEHLER: Keine Sheet-Verbindung für Batch-Update.")
return False # Kein Erfolg, da keine Verbindung return False
if not update_data: if not update_data:
# debug_print("Keine Daten für Batch-Update vorhanden.") # Weniger Lärm im Log return True
return True # Kein Fehler, aber nichts zu tun
try: try:
# Der eigentliche API-Aufruf
self.sheet.batch_update(update_data, value_input_option='USER_ENTERED') self.sheet.batch_update(update_data, value_input_option='USER_ENTERED')
# debug_print(f"Batch-Update erfolgreich ({len(update_data)} Zellen/Bereiche aktualisiert).") # Optional weniger Lärm
return True return True
except gspread.exceptions.APIError as e: except gspread.exceptions.APIError as e:
# Spezifische Fehlerbehandlung für gspread/Google API Fehler debug_print(f"Google API Fehler beim Batch-Update: Status {e.response.status_code} - {e.response.text[:500]}")
debug_print(f"Google API Fehler beim Batch-Update: {e.response.status_code} - {e.response.text[:500]}")
# Der @retry_on_failure Decorator sollte diesen Fehler fangen und Wiederholungen versuchen
raise e # Fehler weitergeben, damit der Decorator ihn fängt raise e # Fehler weitergeben, damit der Decorator ihn fängt
except Exception as e: except Exception as e:
# Allgemeine Fehlerbehandlung
debug_print(f"Allgemeiner Fehler beim Batch-Update: {type(e).__name__} - {e}") debug_print(f"Allgemeiner Fehler beim Batch-Update: {type(e).__name__} - {e}")
raise e # Fehler weitergeben raise e
@@ -2356,57 +2374,114 @@ def process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_
debug_print("Brancheneinschätzung (Batch) abgeschlossen.") debug_print("Brancheneinschätzung (Batch) abgeschlossen.")
# Annahmen:
# - Funktionen debug_print, process_verification_only, process_website_batch, process_branch_batch sind definiert.
# - sheet_handler ist eine initialisierte Instanz von GoogleSheetHandler (mit der korrekten get_start_row_index Methode).
# - Globale Konstante header_rows (oder besser, hol sie vom sheet_handler?)
def run_dispatcher(mode, sheet_handler, row_limit=None): def run_dispatcher(mode, sheet_handler, row_limit=None):
"""Wählt den passenden Batch-Prozess basierend auf dem Modus.""" """
Wählt den passenden Batch-Prozess basierend auf dem Modus.
Ermittelt die Startzeile dynamisch basierend auf dem Timestamp in der relevanten Spalte.
"""
debug_print(f"Starte Dispatcher im Modus '{mode}' mit row_limit={row_limit}.") debug_print(f"Starte Dispatcher im Modus '{mode}' mit row_limit={row_limit}.")
header_rows = 5 # Definiere die Anzahl der Header-Zeilen
# Finde Startzeile (erste Zeile ab 7 ohne Zeitstempel in AO)
data = sheet_handler.get_all_data_with_headers() # --- Startzeilen-Ermittlung basierend auf Modus ---
header_rows = 5 # Definiere, welche Spalte für welchen Modus den Startpunkt bestimmt
start_row_index_in_sheet = -1 # Standardmäßig AO für Gesamtprüfung, AT für Website, AN für Wiki, AO für Branch
for i in range(header_rows + 1, len(data) + 1): start_col_key = "Timestamp letzte Prüfung" # Standard (Spalte AO)
if i < 7: continue min_start_row = 7 # Mindestens ab Zeile 7 suchen
row_index_in_list = i - 1
row = data[row_index_in_list] if mode == "website":
if len(row) <= COLUMN_MAP["Timestamp letzte Prüfung"] or not row[COLUMN_MAP["Timestamp letzte Prüfung"]].strip(): start_col_key = "Website Scrape Timestamp" # Spalte AT
start_row_index_in_sheet = i elif mode == "wiki":
break start_col_key = "Wikipedia Timestamp" # Spalte AN
elif mode == "branch":
if start_row_index_in_sheet == -1: start_col_key = "Timestamp letzte Prüfung" # Spalte AO
debug_print("Keine Zeile ohne Zeitstempel in Spalte AO (ab Zeile 7) gefunden. Dispatcher beendet.") elif mode == "combined":
# Für combined nehmen wir den frühesten relevanten Timestamp,
# meist der letzte Gesamt-Timestamp (AO), da die Teilprozesse
# ihre eigenen Timestamps prüfen sollten. Oder AT als generellen Start?
# Nehmen wir AT als Startpunkt für den Bereich, den wir betrachten.
start_col_key = "Website Scrape Timestamp" # Spalte AT
# Füge ggf. andere Modi hinzu
debug_print(f"Dispatcher: Ermittle Startzeile basierend auf Spalte '{start_col_key}'...")
start_data_index = sheet_handler.get_start_row_index(check_column_key=start_col_key, min_sheet_row=min_start_row)
# Fehlerprüfung: Wenn get_start_row_index -1 zurückgibt (Schlüssel nicht gefunden)
if start_data_index == -1:
debug_print(f"FEHLER: Konnte Startzeile nicht ermitteln (Spaltenschlüssel '{start_col_key}' in COLUMN_MAP prüfen?). Dispatcher beendet.")
return return
# Bestimme Endzeile # Umrechnung des 0-basierten Daten-Index in die 1-basierte Sheet-Zeilennummer
if row_limit is not None and row_limit > 0: start_row_index_in_sheet = start_data_index + header_rows + 1
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}.") # Hole Gesamtzahl der Zeilen für die Endberechnung
all_data = sheet_handler.get_all_data_with_headers()
total_sheet_rows = len(all_data)
if start_row_index_in_sheet > end_row_index_in_sheet: # Prüfe, ob der Startpunkt überhaupt im Sheet liegt
debug_print("Startzeile liegt nach Endzeile. Keine Verarbeitung.") if start_row_index_in_sheet > total_sheet_rows and total_sheet_rows > header_rows:
# Wenn der Startpunkt HINTER der letzten Zeile liegt, gibt es nichts zu tun
debug_print(f"Startzeile ({start_row_index_in_sheet}) liegt hinter der letzten Sheet-Zeile ({total_sheet_rows}). Keine neuen Zeilen zu verarbeiten. Dispatcher beendet.")
return
elif start_row_index_in_sheet > total_sheet_rows:
# Wenn das Sheet nur Header hat oder leer ist
debug_print(f"Sheet hat keine Datenzeilen oder Startzeile ({start_row_index_in_sheet}) ist ungültig. Dispatcher beendet.")
return return
# Modus auswählen
if mode == "wiki": # --- Endzeilen-Ermittlung ---
process_verification_only(sheet_handler, row_limit) # Nutzt jetzt row_limit intern anders if row_limit is not None and row_limit > 0:
elif mode == "website": # Berechne Endzeile basierend auf Startzeile und Limit,
process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # aber nicht über die letzte Zeile hinaus.
elif mode == "branch": end_row_index_in_sheet = min(start_row_index_in_sheet + row_limit - 1, total_sheet_rows)
process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) elif row_limit == 0:
elif mode == "combined": debug_print("Zeilenlimit ist 0. Keine Verarbeitung.")
debug_print("--- Start Combined Mode: Wiki ---") return
process_verification_only(sheet_handler, row_limit) else: # Kein Limit oder negatives Limit -> bis zum Ende des Sheets
debug_print("--- Start Combined Mode: Website ---") end_row_index_in_sheet = total_sheet_rows
# 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. debug_print(f"Dispatcher: Verarbeitung geplant für Sheet-Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}.")
process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet)
debug_print("--- Start Combined Mode: Branch ---") # Zusätzliche Prüfung: Liegt Start nach Ende? (Sollte nicht passieren, aber sicher ist sicher)
process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) if start_row_index_in_sheet > end_row_index_in_sheet:
debug_print("--- Combined Mode abgeschlossen ---") debug_print("Berechnete Startzeile liegt nach der Endzeile. Keine Verarbeitung.")
else: return
debug_print(f"Ungültiger Modus '{mode}' im Dispatcher.")
# --- Modusauswahl und Aufruf der Verarbeitungsfunktionen ---
# Die aufgerufenen Funktionen müssen jetzt selbst prüfen, ob die jeweilige Zeile
# wegen eines bereits vorhandenen Timestamps übersprungen werden soll.
try:
if mode == "wiki":
# process_verification_only prüft Timestamp AN intern
process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet)
elif mode == "website":
# process_website_batch prüft Timestamp AT intern
process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet)
elif mode == "branch":
# process_branch_batch prüft Timestamp AO intern
process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet)
elif mode == "combined":
# Führt die Teile nacheinander aus, jeder Teil prüft seinen eigenen Timestamp
debug_print("--- Start Combined Mode: Wiki ---")
process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AN
debug_print("--- Start Combined Mode: Website ---")
process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AT
debug_print("--- Start Combined Mode: Branch ---")
process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AO
debug_print("--- Combined Mode abgeschlossen ---")
else:
debug_print(f"Ungültiger Modus '{mode}' wurde im Dispatcher übergeben.")
except Exception as e:
debug_print(f"FEHLER im Dispatcher während der Ausführung von Modus '{mode}': {e}")
import traceback
debug_print(traceback.format_exc()) # Gib den Traceback aus für detaillierte Fehlersuche
# --- Ende run_dispatcher Funktion ---
# ==================== SERP API / LINKEDIN FUNCTIONS ==================== # ==================== SERP API / LINKEDIN FUNCTIONS ====================