This commit is contained in:
2025-04-16 13:50:32 +00:00
parent 12199fb22b
commit 808c292e6d

View File

@@ -955,10 +955,16 @@ class GoogleSheetHandler:
def __init__(self):
self.sheet = None
self.sheet_values = []
self.headers = [] # Um Header-Zeilen zu speichern (Annahme: Zeile 1 sind die Namen)
self._connect()
if self.sheet:
self._load_data()
self.headers = []
try:
self._connect()
if self.sheet:
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
@@ -966,6 +972,7 @@ class GoogleSheetHandler:
"""Stellt Verbindung zum Google Sheet her."""
self.sheet = None
debug_print("Verbinde mit Google Sheets...")
# Fehlerbehandlung innerhalb ist gut, aber raise am Ende, damit retry greift
try:
scope = ["https://www.googleapis.com/auth/spreadsheets"]
creds = ServiceAccountCredentials.from_json_keyfile_name(CREDENTIALS_FILE, scope)
@@ -975,49 +982,76 @@ class GoogleSheetHandler:
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
raise e
except Exception as 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
def _load_data(self):
"""Lädt alle Daten aus dem Sheet."""
def load_data(self):
"""Lädt alle Daten aus dem Sheet und aktualisiert self.sheet_values."""
if not self.sheet:
debug_print("Fehler: Keine Sheet-Verbindung zum Laden der Daten.")
self.sheet_values = []
self.headers = []
return
return False # Signalisiert Fehler
debug_print("Lade Daten aus Google Sheet...")
self.sheet_values = self.sheet.get_all_values()
if len(self.sheet_values) >= 1:
# 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:
self.headers = []
debug_print("Warnung: Google Sheet scheint leer zu sein.")
debug_print(f"Daten geladen: {len(self.sheet_values)} Zeilen insgesamt.")
try:
# Hol die rohen Daten
raw_values = self.sheet.get_all_values()
# Prüfe, ob überhaupt Daten zurückkamen
if not raw_values:
debug_print("Warnung: Google Sheet scheint leer zu sein oder keine Daten zurückgegeben.")
self.sheet_values = []
self.headers = []
return True # Kein Fehler beim Laden, aber keine Daten
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):
"""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
if len(self.sheet_values) <= header_rows:
debug_print("Warnung in get_data: Weniger Zeilen als Header-Zeilen vorhanden.")
if not self.sheet_values or len(self.sheet_values) <= header_rows:
# 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 []
# Gibt eine Slice der Liste zurück, die die Datenzeilen enthält
return self.sheet_values[header_rows:]
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
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 = ""
n = col_idx_1_based
if n < 1: return None # Ungültiger Index
while n > 0:
n, remainder = divmod(n - 1, 26)
string = chr(65 + remainder) + string
@@ -1026,8 +1060,9 @@ class GoogleSheetHandler:
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),
ab einer Mindestzeilennummer im Sheet, in der der Timestamp in der
Spalte, die durch den *Schlüssel* in COLUMN_MAP definiert ist, fehlt.
ab einer Mindestzeilennummer im Sheet, in der der Wert in der
Spalte (definiert durch check_column_key in COLUMN_MAP) fehlt oder leer ist.
Lädt die Daten vor der Prüfung neu.
Args:
check_column_key (str): Der Schlüssel in COLUMN_MAP für die zu prüfende Spalte.
@@ -1035,25 +1070,28 @@ class GoogleSheetHandler:
Returns:
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.
"""
# 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
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:
debug_print("Keine Datenzeilen vorhanden für get_start_row_index.")
return 0
debug_print("Keine Datenzeilen vorhanden für get_start_row_index nach Neuladen.")
return 0 # Index 0 signalisiert Start am Anfang (oder keine Daten)
# 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
return -1 # Fehlerindikator
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
search_start_index_in_data = max(0, min_sheet_row - header_rows - 1)
@@ -1074,11 +1112,14 @@ class GoogleSheetHandler:
is_empty = True
if len(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
# else: is_empty bleibt True, da Spalte nicht existiert
# DEBUG Log für jede 1000ste Zeile oder wenn ein relevanter Übergang erwartet wird
if i == search_start_index_in_data or i % 1000 == 0 or current_sheet_row in [2121, 2122, 8926, 8927, 8928]:
# Debug Log für jede 1000ste Zeile oder relevante Übergänge
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}")
if is_empty:
@@ -1108,6 +1149,7 @@ class GoogleSheetHandler:
debug_print("FEHLER: Keine Sheet-Verbindung für Batch-Update.")
return False
if not update_data:
# debug_print("Keine Daten für Batch-Update vorhanden.") # Weniger Lärm
return True
try:
@@ -1115,11 +1157,13 @@ class GoogleSheetHandler:
return True
except gspread.exceptions.APIError as e:
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:
debug_print(f"Allgemeiner Fehler beim Batch-Update: {type(e).__name__} - {e}")
raise e
# --- Ende GoogleSheetHandler Klasse ---
# ==================== 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.
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}.")
header_rows = 5 # Definiere die Anzahl der Header-Zeilen
header_rows = 5 # Feste Annahme für Header
# --- 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)
min_start_row = 7 # Mindestens ab Zeile 7 suchen
if mode == "website":
start_col_key = "Website Scrape Timestamp" # Spalte AT
elif mode == "wiki":
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
min_start_row = 7
if mode == "website": start_col_key = "Website Scrape Timestamp" # AT
elif mode == "wiki": start_col_key = "Wikipedia Timestamp" # AN
elif mode == "branch": start_col_key = "Timestamp letzte Prüfung" # AO
elif mode == "combined": start_col_key = "Timestamp letzte Prüfung" # AO (Combined startet, wo der letzte Schritt fehlt)
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)
# 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:
debug_print(f"FEHLER: Konnte Startzeile nicht ermitteln (Spaltenschlüssel '{start_col_key}' in COLUMN_MAP prüfen?). Dispatcher beendet.")
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
start_row_index_in_sheet = start_data_index + header_rows + 1
# Hole Gesamtzahl der Zeilen für die Endberechnung
all_data = sheet_handler.get_all_data_with_headers()
total_sheet_rows = len(all_data)
# Hole Gesamtzahl der Zeilen (nach potentiellem Neuladen in get_start_row_index)
total_sheet_rows = len(sheet_handler.sheet_values)
# Prüfe, ob der Startpunkt überhaupt im Sheet liegt
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
# Prüfe, ob der Startpunkt überhaupt im Sheet liegt oder ob das Sheet leer ist
if start_data_index >= len(sheet_handler.get_data()): # Prüfe gegen Daten ohne Header
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.")
return
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.")
return
# --- Endzeilen-Ermittlung ---
if row_limit is not None and row_limit > 0:
# Berechne Endzeile basierend auf Startzeile und Limit,
# aber nicht über die letzte Zeile hinaus.
# Berechne Endzeile basierend auf Startzeile und Limit
end_row_index_in_sheet = min(start_row_index_in_sheet + row_limit - 1, total_sheet_rows)
elif row_limit == 0:
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}.")
# 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:
debug_print("Berechnete Startzeile liegt nach der Endzeile. Keine Verarbeitung.")
return
# --- 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
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 ---")
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 ---")
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 ---")
else:
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:
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
debug_print(traceback.format_exc())
# --- Ende run_dispatcher Funktion ---