This commit is contained in:
2025-04-16 13:50:32 +00:00
parent 3478a68e2f
commit 3339376f3a

View File

@@ -955,10 +955,16 @@ 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 (Annahme: Zeile 1 sind die Namen) self.headers = []
try:
self._connect() self._connect()
if self.sheet: if self.sheet:
self._load_data() self.load_data() # Erste Datenladung bei Initialisierung
except Exception as e:
# Fehler bei Initialisierung bereits loggen und None zurückgeben?
debug_print(f"FATAL: Fehler bei Initialisierung von GoogleSheetHandler: {e}")
# Hier sollte das Hauptprogramm den Fehler erkennen und abbrechen.
# Man könnte auch eine Exception werfen: raise ConnectionError(...)
# retry_on_failure Decorator sollte hier angewendet werden # retry_on_failure Decorator sollte hier angewendet werden
@retry_on_failure @retry_on_failure
@@ -966,6 +972,7 @@ class GoogleSheetHandler:
"""Stellt Verbindung zum Google Sheet her.""" """Stellt Verbindung zum Google Sheet her."""
self.sheet = None self.sheet = None
debug_print("Verbinde mit Google Sheets...") debug_print("Verbinde mit Google Sheets...")
# Fehlerbehandlung innerhalb ist gut, aber raise am Ende, damit retry greift
try: try:
scope = ["https://www.googleapis.com/auth/spreadsheets"] scope = ["https://www.googleapis.com/auth/spreadsheets"]
creds = ServiceAccountCredentials.from_json_keyfile_name(CREDENTIALS_FILE, scope) creds = ServiceAccountCredentials.from_json_keyfile_name(CREDENTIALS_FILE, scope)
@@ -975,49 +982,76 @@ class GoogleSheetHandler:
debug_print("Verbindung zu Google Sheets erfolgreich.") debug_print("Verbindung zu Google Sheets erfolgreich.")
except gspread.exceptions.APIError as e: except gspread.exceptions.APIError as e:
debug_print(f"FEHLER bei Google API Verbindung: Status {e.response.status_code} - {e.response.text[:200]}") debug_print(f"FEHLER bei Google API Verbindung: Status {e.response.status_code} - {e.response.text[:200]}")
raise # Damit retry greift raise e
except Exception as e: except Exception as e:
debug_print(f"FEHLER bei der Google Sheets Verbindung: {type(e).__name__} - {e}") debug_print(f"FEHLER bei der Google Sheets Verbindung: {type(e).__name__} - {e}")
raise # Damit retry greift raise e
# retry_on_failure Decorator sollte hier angewendet werden # 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 und aktualisiert self.sheet_values."""
if not self.sheet: if not self.sheet:
debug_print("Fehler: Keine Sheet-Verbindung zum Laden der Daten.") debug_print("Fehler: Keine Sheet-Verbindung zum Laden der Daten.")
self.sheet_values = [] self.sheet_values = []
self.headers = [] self.headers = []
return return False # Signalisiert Fehler
debug_print("Lade Daten aus Google Sheet...") debug_print("Lade Daten aus Google Sheet...")
self.sheet_values = self.sheet.get_all_values() try:
if len(self.sheet_values) >= 1: # Hol die rohen Daten
# Speichere die *echten* Header-Namen aus der ersten Zeile raw_values = self.sheet.get_all_values()
self.headers = self.sheet_values[0]
# Die Alignment-Demo Header (Zeilen 1-5) werden hier nicht separat gespeichert, # Prüfe, ob überhaupt Daten zurückkamen
# sheet_values enthält alles. if not raw_values:
else: debug_print("Warnung: Google Sheet scheint leer zu sein oder keine Daten zurückgegeben.")
self.sheet_values = []
self.headers = [] self.headers = []
debug_print("Warnung: Google Sheet scheint leer zu sein.") return True # Kein Fehler beim Laden, aber keine Daten
debug_print(f"Daten geladen: {len(self.sheet_values)} Zeilen insgesamt.")
self.sheet_values = raw_values # Speichere die kompletten Daten
# Setze Header basierend auf der ersten Zeile
if len(self.sheet_values) >= 1:
self.headers = self.sheet_values[0]
else:
self.headers = [] # Sollte nicht passieren, wenn raw_values nicht leer war
debug_print(f"Daten neu geladen: {len(self.sheet_values)} Zeilen insgesamt.")
return True # Signalisiert Erfolg
except gspread.exceptions.APIError as e:
debug_print(f"Google API Fehler beim Laden der Sheet Daten: Status {e.response.status_code} - {e.response.text[:200]}")
# self.sheet_values = [] # Im Fehlerfall alte Daten behalten oder leeren? Besser behalten.
# self.headers = []
raise e # Damit retry greift
except Exception as e:
debug_print(f"Allgemeiner Fehler beim Laden der Google Sheet Daten: {e}")
# self.sheet_values = []
# self.headers = []
raise e # Damit retry greift
# return False # Wird nur bei Exception erreicht, die nicht weitergeworfen wird
def get_data(self): def get_data(self):
"""Gibt die geladenen Daten zurück (ohne die ersten 5 Header-Zeilen).""" """Gibt die aktuell im Handler gespeicherten Daten zurück (ohne die ersten 5 Header-Zeilen)."""
header_rows = 5 # Definiert die Anzahl der zu überspringenden Header-Zeilen header_rows = 5 # Definiert die Anzahl der zu überspringenden Header-Zeilen
if len(self.sheet_values) <= header_rows: if not self.sheet_values or len(self.sheet_values) <= header_rows:
debug_print("Warnung in get_data: Weniger Zeilen als Header-Zeilen vorhanden.") # Logge nur, wenn sheet_values existiert aber zu kurz ist
if self.sheet_values:
debug_print(f"Warnung in get_data: Nur {len(self.sheet_values)} Zeilen vorhanden, weniger als {header_rows} Header-Zeilen erwartet.")
return [] return []
# Gibt eine Slice der Liste zurück, die die Datenzeilen enthält # 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 aktuell im Handler gespeicherten Daten inklusive Header zurück."""
if not self.sheet_values:
debug_print("Warnung in get_all_data_with_headers: Keine Daten im Handler gespeichert.")
return self.sheet_values return self.sheet_values
def _get_col_letter(self, col_idx_1_based): def _get_col_letter(self, col_idx_1_based):
""" Konvertiert 1-basierten Spaltenindex in Buchstaben. """ """ Konvertiert 1-basierten Spaltenindex in Buchstaben (A, B, ..., Z, AA, ...). """
string = "" string = ""
n = col_idx_1_based n = col_idx_1_based
if n < 1: return None # Ungültiger Index
while n > 0: while n > 0:
n, remainder = divmod(n - 1, 26) n, remainder = divmod(n - 1, 26)
string = chr(65 + remainder) + string string = chr(65 + remainder) + string
@@ -1026,8 +1060,9 @@ class GoogleSheetHandler:
def get_start_row_index(self, check_column_key, min_sheet_row=7): 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 Wert in der
Spalte, die durch den *Schlüssel* in COLUMN_MAP definiert ist, fehlt. Spalte (definiert durch check_column_key in COLUMN_MAP) fehlt oder leer ist.
Lädt die Daten vor der Prüfung neu.
Args: Args:
check_column_key (str): Der Schlüssel in COLUMN_MAP für die zu prüfende Spalte. check_column_key (str): Der Schlüssel in COLUMN_MAP für die zu prüfende Spalte.
@@ -1035,25 +1070,28 @@ class GoogleSheetHandler:
Returns: Returns:
int: Der 0-basierte Index in der Datenliste (ohne Header), int: Der 0-basierte Index in der Datenliste (ohne Header),
oder -1 bei Fehler (z.B. Schlüssel nicht gefunden),
oder der Index nach der letzten Zeile, wenn alle gefüllt sind. oder der Index nach der letzten Zeile, wenn alle gefüllt sind.
""" """
# Lade Daten *vor* der Prüfung neu, um Aktualität sicherzustellen
if not self.load_data():
debug_print("FEHLER beim Laden der Daten in get_start_row_index. Breche ab.")
return -1 # Fehlerindikator
header_rows = 5 header_rows = 5
data_rows = self.get_data() # Holt Daten OHNE die 5 Header data_rows = self.get_data() # Greift auf die neu geladenen Daten zu
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 nach Neuladen.")
return 0 return 0 # Index 0 signalisiert Start am Anfang (oder keine Daten)
# Hole den Spaltenindex aus COLUMN_MAP # Hole den Spaltenindex aus COLUMN_MAP
check_column_index = COLUMN_MAP.get(check_column_key) check_column_index = COLUMN_MAP.get(check_column_key)
if check_column_index is None: if check_column_index is None:
debug_print(f"FEHLER: Schlüssel '{check_column_key}' nicht in COLUMN_MAP gefunden!") 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. return -1 # Fehlerindikator
# 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 actual_col_letter = self._get_col_letter(check_column_index + 1)
# Berechne den 0-basierten Startindex für die *Datenliste* data_rows # 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)
@@ -1074,11 +1112,14 @@ class GoogleSheetHandler:
is_empty = True is_empty = True
if len(row) > check_column_index: if len(row) > check_column_index:
cell_value = row[check_column_index] cell_value = row[check_column_index]
if cell_value and str(cell_value).strip(): # Prüft auf nicht leer und nicht nur Whitespace # Prüft explizit auf None und leeren String nach strip()
if cell_value is not None and str(cell_value).strip():
is_empty = False is_empty = False
# else: is_empty bleibt True, da Spalte nicht existiert
# DEBUG Log für jede 1000ste Zeile oder wenn ein relevanter Übergang erwartet wird # Debug Log für jede 1000ste Zeile oder relevante Übergänge
if i == search_start_index_in_data or i % 1000 == 0 or current_sheet_row in [2121, 2122, 8926, 8927, 8928]: log_debug = (i == search_start_index_in_data or i % 1000 == 0 or current_sheet_row in [2121, 2122, 8926, 8927, 8928])
if log_debug:
debug_print(f" -> Prüfe Daten-Index {i} (Sheet Zeile {current_sheet_row}): Wert in Spalte {actual_col_letter}='{cell_value}' -> Leer? {is_empty}") debug_print(f" -> Prüfe Daten-Index {i} (Sheet Zeile {current_sheet_row}): Wert in Spalte {actual_col_letter}='{cell_value}' -> Leer? {is_empty}")
if is_empty: if is_empty:
@@ -1108,6 +1149,7 @@ class GoogleSheetHandler:
debug_print("FEHLER: Keine Sheet-Verbindung für Batch-Update.") debug_print("FEHLER: Keine Sheet-Verbindung für Batch-Update.")
return False return False
if not update_data: if not update_data:
# debug_print("Keine Daten für Batch-Update vorhanden.") # Weniger Lärm
return True return True
try: try:
@@ -1115,11 +1157,13 @@ class GoogleSheetHandler:
return True return True
except gspread.exceptions.APIError as e: except gspread.exceptions.APIError as e:
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: Status {e.response.status_code} - {e.response.text[:500]}")
raise e # Fehler weitergeben, damit der Decorator ihn fängt raise e
except Exception as e: except Exception as e:
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 raise e
# --- Ende GoogleSheetHandler Klasse ---
# ==================== WIKIPEDIA SCRAPER ==================== # ==================== WIKIPEDIA SCRAPER ====================
@@ -2464,31 +2508,24 @@ 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. Ermittelt die Startzeile dynamisch basierend auf dem Timestamp in der relevanten Spalte.
Die aufgerufenen Prozessfunktionen laden ihre Daten selbst oder verwenden den Handler.
""" """
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 header_rows = 5 # Feste Annahme für Header
# --- Startzeilen-Ermittlung basierend auf Modus --- # --- Startzeilen-Ermittlung basierend auf Modus ---
# Definiere, welche Spalte für welchen Modus den Startpunkt bestimmt
# Standardmäßig AO für Gesamtprüfung, AT für Website, AN für Wiki, AO für Branch
start_col_key = "Timestamp letzte Prüfung" # Standard (Spalte AO) start_col_key = "Timestamp letzte Prüfung" # Standard (Spalte AO)
min_start_row = 7 # Mindestens ab Zeile 7 suchen min_start_row = 7
if mode == "website": start_col_key = "Website Scrape Timestamp" # AT
if mode == "website": elif mode == "wiki": start_col_key = "Wikipedia Timestamp" # AN
start_col_key = "Website Scrape Timestamp" # Spalte AT elif mode == "branch": start_col_key = "Timestamp letzte Prüfung" # AO
elif mode == "wiki": elif mode == "combined": start_col_key = "Timestamp letzte Prüfung" # AO (Combined startet, wo der letzte Schritt fehlt)
start_col_key = "Wikipedia Timestamp" # Spalte AN
elif mode == "branch":
start_col_key = "Timestamp letzte Prüfung" # Spalte AO
elif mode == "combined":
# HIER KORRIGIERT: Combined startet basierend auf AO (letzter Schritt)
start_col_key = "Timestamp letzte Prüfung" # Spalte AO
# Füge ggf. andere Modi hinzu
debug_print(f"Dispatcher: Ermittle Startzeile basierend auf Spalte '{start_col_key}'...") debug_print(f"Dispatcher: Ermittle Startzeile basierend auf Spalte '{start_col_key}'...")
# get_start_row_index wird aufgerufen, lädt die Daten neu
start_data_index = sheet_handler.get_start_row_index(check_column_key=start_col_key, min_sheet_row=min_start_row) 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) # Fehlerprüfung für get_start_row_index
if start_data_index == -1: 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.") debug_print(f"FEHLER: Konnte Startzeile nicht ermitteln (Spaltenschlüssel '{start_col_key}' in COLUMN_MAP prüfen?). Dispatcher beendet.")
return return
@@ -2496,25 +2533,21 @@ def run_dispatcher(mode, sheet_handler, row_limit=None):
# Umrechnung des 0-basierten Daten-Index in die 1-basierte Sheet-Zeilennummer # Umrechnung des 0-basierten Daten-Index in die 1-basierte Sheet-Zeilennummer
start_row_index_in_sheet = start_data_index + header_rows + 1 start_row_index_in_sheet = start_data_index + header_rows + 1
# Hole Gesamtzahl der Zeilen für die Endberechnung # Hole Gesamtzahl der Zeilen (nach potentiellem Neuladen in get_start_row_index)
all_data = sheet_handler.get_all_data_with_headers() total_sheet_rows = len(sheet_handler.sheet_values)
total_sheet_rows = len(all_data)
# Prüfe, ob der Startpunkt überhaupt im Sheet liegt # Prüfe, ob der Startpunkt überhaupt im Sheet liegt oder ob das Sheet leer ist
if start_row_index_in_sheet > total_sheet_rows and total_sheet_rows > header_rows: if start_data_index >= len(sheet_handler.get_data()): # Prüfe gegen Daten ohne Header
# Wenn der Startpunkt HINTER der letzten Zeile liegt, gibt es nichts zu tun debug_print(f"Ermittelter Start-Daten-Index ({start_data_index}) liegt nach der letzten Datenzeile ({len(sheet_handler.get_data())-1}). Keine neuen Zeilen zu verarbeiten. Dispatcher beendet.")
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 return
elif start_row_index_in_sheet > total_sheet_rows: elif start_row_index_in_sheet > total_sheet_rows:
# Wenn das Sheet nur Header hat oder leer ist # Dieser Fall sollte durch die Prüfung oben abgedeckt sein, aber zur Sicherheit
debug_print(f"Sheet hat keine Datenzeilen oder Startzeile ({start_row_index_in_sheet}) ist ungültig. Dispatcher beendet.") debug_print(f"Sheet hat keine Datenzeilen oder Startzeile ({start_row_index_in_sheet}) ist ungültig. Dispatcher beendet.")
return return
# --- Endzeilen-Ermittlung --- # --- Endzeilen-Ermittlung ---
if row_limit is not None and row_limit > 0: if row_limit is not None and row_limit > 0:
# Berechne Endzeile basierend auf Startzeile und Limit, # Berechne Endzeile basierend auf Startzeile und Limit
# aber nicht über die letzte Zeile hinaus.
end_row_index_in_sheet = min(start_row_index_in_sheet + row_limit - 1, total_sheet_rows) end_row_index_in_sheet = min(start_row_index_in_sheet + row_limit - 1, total_sheet_rows)
elif row_limit == 0: elif row_limit == 0:
debug_print("Zeilenlimit ist 0. Keine Verarbeitung.") debug_print("Zeilenlimit ist 0. Keine Verarbeitung.")
@@ -2524,32 +2557,27 @@ def run_dispatcher(mode, sheet_handler, row_limit=None):
debug_print(f"Dispatcher: Verarbeitung geplant für Sheet-Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}.") debug_print(f"Dispatcher: Verarbeitung geplant für Sheet-Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}.")
# Zusätzliche Prüfung: Liegt Start nach Ende? (Sollte nicht passieren, aber sicher ist sicher)
if start_row_index_in_sheet > end_row_index_in_sheet: if start_row_index_in_sheet > end_row_index_in_sheet:
debug_print("Berechnete Startzeile liegt nach der Endzeile. Keine Verarbeitung.") debug_print("Berechnete Startzeile liegt nach der Endzeile. Keine Verarbeitung.")
return return
# --- Modusauswahl und Aufruf der Verarbeitungsfunktionen --- # --- 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: try:
if mode == "wiki": 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) process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet)
elif mode == "website": 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) process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet)
elif mode == "branch": 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) process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet)
elif mode == "combined": elif mode == "combined":
# Führt die Teile nacheinander aus, jeder Teil prüft seinen eigenen Timestamp
debug_print("--- Start Combined Mode: Wiki ---") debug_print("--- Start Combined Mode: Wiki ---")
process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AN process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Lädt Daten, prüft AN
time.sleep(1) # Kurze Pause
debug_print("--- Start Combined Mode: Website ---") debug_print("--- Start Combined Mode: Website ---")
process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AT process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Lädt Daten, prüft AT
time.sleep(1) # Kurze Pause
debug_print("--- Start Combined Mode: Branch ---") debug_print("--- Start Combined Mode: Branch ---")
process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AO process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Lädt Daten, prüft AO
debug_print("--- Combined Mode abgeschlossen ---") debug_print("--- Combined Mode abgeschlossen ---")
else: else:
debug_print(f"Ungültiger Modus '{mode}' wurde im Dispatcher übergeben.") debug_print(f"Ungültiger Modus '{mode}' wurde im Dispatcher übergeben.")
@@ -2557,7 +2585,7 @@ def run_dispatcher(mode, sheet_handler, row_limit=None):
except Exception as e: except Exception as e:
debug_print(f"FEHLER im Dispatcher während der Ausführung von Modus '{mode}': {e}") debug_print(f"FEHLER im Dispatcher während der Ausführung von Modus '{mode}': {e}")
import traceback import traceback
debug_print(traceback.format_exc()) # Gib den Traceback aus für detaillierte Fehlersuche debug_print(traceback.format_exc())
# --- Ende run_dispatcher Funktion --- # --- Ende run_dispatcher Funktion ---