This commit is contained in:
2025-04-16 12:13:09 +00:00
parent 6c49d4ccc3
commit 362a3fc8d5

View File

@@ -953,38 +953,48 @@ class GoogleSheetHandler:
self.sheet_values = [] self.sheet_values = []
self.headers = [] # Um Header-Zeilen zu speichern self.headers = [] # Um Header-Zeilen zu speichern
self._connect() self._connect()
# _load_data wird jetzt nach erfolgreicher Verbindung aufgerufen
if self.sheet:
self._load_data() self._load_data()
@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
debug_print("Verbinde mit Google Sheets...") debug_print("Verbinde mit Google Sheets...")
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)
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 self.sheet = sh.sheet1 # Zugriff auf das erste Blatt (Sheet1)
debug_print("Verbindung zu Google Sheets erfolgreich.") debug_print("Verbindung zu Google Sheets erfolgreich.")
except Exception as e:
debug_print(f"FEHLER bei der Google Sheets Verbindung: {e}")
# Hier könnte man den Fehler weitergeben oder None lassen
raise # Fehler weitergeben, damit retry greift oder main abbricht
@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."""
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.headers = []
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) >= 5: if len(self.sheet_values) >= 1: # Mindestens Header sollte da sein
self.headers = self.sheet_values[:5] # Zeilen 1-5 als Header speichern self.headers = self.sheet_values[:5] # Annahme: Zeilen 1-5 sind Header
else: else:
self.headers = self.sheet_values[:] # Alle Zeilen als Header, falls weniger als 5 self.headers = []
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.")
# Hier könnte die COLUMN_MAP dynamisch erstellt werden, falls gewünscht
def get_data(self): def get_data(self):
"""Gibt die geladenen Daten zurück (ohne Header).""" """Gibt die geladenen Daten zurück (ohne Header)."""
# Annahme: Die ersten 5 Zeilen sind Header header_rows = 5 # Annahme: Zeile 1-5 sind Header
header_rows = 5
if len(self.sheet_values) <= header_rows: if len(self.sheet_values) <= header_rows:
return [] return []
return self.sheet_values[header_rows:] return self.sheet_values[header_rows:]
@@ -993,131 +1003,86 @@ class GoogleSheetHandler:
"""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_start_row_index(self, check_column_index=COLUMN_MAP["Website Scrape Timestamp"], min_sheet_row=7): def get_start_row_index(self, check_column_index=COLUMN_MAP["Website Scrape Timestamp"], 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. angegebenen Spalte fehlt.
Args:
check_column_index (int): Der 0-basierte Index der zu prüfenden Spalte.
min_sheet_row (int): Die 1-basierte Zeilennummer im Sheet, ab der gesucht werden soll.
Returns:
int: Der 0-basierte Index in der Datenliste (ohne Header),
oder der Index nach der letzten Zeile, wenn alle gefüllt sind.
""" """
header_rows = 5 # Annahme: Zeilen 1-5 sind Header header_rows = 5
data_rows = self.get_data() # Holt Daten ohne Header data_rows = self.get_data()
if not data_rows: if not data_rows:
debug_print("Keine Datenzeilen vorhanden.") debug_print("Keine Datenzeilen vorhanden für get_start_row_index.")
return 0 return 0
# Berechne den 0-basierten Startindex für die *Datenliste*,
# der der min_sheet_row entspricht.
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)
# Stelle sicher, dass der Startindex nicht außerhalb der Liste liegt
if search_start_index_in_data >= len(data_rows):
col_letter = self._get_col_letter(check_column_index + 1)
debug_print(f"Start-Suchindex ({search_start_index_in_data}) liegt nach der letzten Datenzeile ({len(data_rows)-1}). Suche ab letzter Zeile für Spalte {col_letter}.")
search_start_index_in_data = len(data_rows) -1 # Beginne bei der letzten Zeile falls Start zu weit
if search_start_index_in_data < 0: return 0 # Falls gar keine Datenzeilen
for i, row in enumerate(data_rows[search_start_index_in_data:], start=search_start_index_in_data): for i, row in enumerate(data_rows[search_start_index_in_data:], start=search_start_index_in_data):
# Sicherheitscheck für Zeilenlänge
if len(row) <= check_column_index or not row[check_column_index].strip(): if len(row) <= check_column_index or not row[check_column_index].strip():
actual_sheet_row = i + header_rows + 1 # 1-basierte Zeilennummer im Sheet actual_sheet_row = i + header_rows + 1
# Finde den Spaltenbuchstaben für die Log-Ausgabe
col_letter = self._get_col_letter(check_column_index + 1) col_letter = self._get_col_letter(check_column_index + 1)
debug_print(f"Erste Zeile ab Zeile {min_sheet_row} ohne Zeitstempel in Spalte {col_letter} (Index {check_column_index}) gefunden: Zeile {actual_sheet_row} (Daten-Index {i})") debug_print(f"Erste Zeile ab Zeile {min_sheet_row} ohne Zeitstempel in Spalte {col_letter} (Index {check_column_index}) gefunden: Zeile {actual_sheet_row} (Daten-Index {i})")
return i # Gibt den 0-basierten Index *innerhalb der Datenliste* zurück return i
# Wenn alle Zeilen ab min_sheet_row einen Zeitstempel haben
last_index = len(data_rows) last_index = len(data_rows)
col_letter = self._get_col_letter(check_column_index + 1) col_letter = self._get_col_letter(check_column_index + 1)
debug_print(f"Alle Zeilen ab Zeile {min_sheet_row} haben einen Zeitstempel in Spalte {col_letter}. Nächster Daten-Index wäre {last_index}.") debug_print(f"Alle Zeilen ab Zeile {min_sheet_row} haben einen Zeitstempel in Spalte {col_letter}. Nächster Daten-Index wäre {last_index}.")
return last_index # Gibt den Index nach der letzten Datenzeile zurück return last_index
# Hilfsfunktion zur Umwandlung von Spaltenindex in Buchstaben (für Logs)
def _get_col_letter(self, col_idx): def _get_col_letter(self, col_idx):
""" Konvertiert 1-basierten Spaltenindex in Buchstaben (A, B, ..., Z, AA, ...). """ """ Konvertiert 1-basierten Spaltenindex in Buchstaben. """
string = "" string = ""
while col_idx > 0: while col_idx > 0:
col_idx, remainder = divmod(col_idx - 1, 26) col_idx, remainder = divmod(col_idx - 1, 26)
string = chr(65 + remainder) + string string = chr(65 + remainder) + string
return string return string
# Anpassung in run_dispatcher: Verwende die neue Methode mit Spalte AT # --- NEU HINZUGEFÜGTE METHODE ---
def run_dispatcher(mode, sheet_handler, row_limit=None):
"""Wählt den passenden Batch-Prozess basierend auf dem Modus."""
debug_print(f"Starte Dispatcher im Modus '{mode}' mit row_limit={row_limit}.")
# Finde Startzeile basierend auf Timestamp in Spalte AT (Index 45)
# Verwende die neue Methode des Handlers
start_data_index = sheet_handler.get_start_row_index(check_column_index=COLUMN_MAP["Website Scrape Timestamp"], min_sheet_row=7)
header_rows = 5
start_row_index_in_sheet = start_data_index + header_rows + 1
all_data = sheet_handler.get_all_data_with_headers() # Hole alle Daten
total_sheet_rows = len(all_data)
if start_row_index_in_sheet > total_sheet_rows:
debug_print(f"Startzeile ({start_row_index_in_sheet}) liegt hinter der letzten Sheet-Zeile ({total_sheet_rows}). Dispatcher beendet.")
return
# Bestimme Endzeile
if row_limit is not None and row_limit > 0:
# Berechne Endzeile basierend auf Startzeile und Limit
end_row_index_in_sheet = min(start_row_index_in_sheet + row_limit - 1, total_sheet_rows)
else:
end_row_index_in_sheet = total_sheet_rows # Bis zum Ende des Sheets
debug_print(f"Dispatcher: Verarbeitung startet ab Zeile {start_row_index_in_sheet}, bis Zeile {end_row_index_in_sheet}.")
if start_row_index_in_sheet > end_row_index_in_sheet:
debug_print("Startzeile liegt nach Endzeile. Keine Verarbeitung.")
return
# --- Modusausführung (bleibt gleich, ABER die aufgerufenen Funktionen müssen den Timestamp prüfen!) ---
if mode == "wiki":
# process_verification_only muss AN Timestamp prüfen
process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet)
elif mode == "website":
# process_website_batch muss AT Timestamp prüfen
process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet)
elif mode == "branch":
# process_branch_batch muss AO Timestamp prüfen
process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet)
elif mode == "combined":
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}' im Dispatcher.")
@retry_on_failure @retry_on_failure
def batch_update_cells(self, update_data): def batch_update_cells(self, update_data):
""" """
Führt ein Batch-Update im Google Sheet durch. Führt ein Batch-Update im Google Sheet durch. Beinhaltet Fehlerbehandlung.
Args: Args:
update_data (list): Eine Liste von Dictionaries, jedes mit 'range' und 'values'. update_data (list): Eine Liste von Dictionaries, jedes mit 'range' und 'values'.
z.B. [{'range': 'A1', 'values': [['Wert']]}, ...] z.B. [{'range': 'A1', 'values': [['Wert']]}, ...]
Returns:
bool: True bei Erfolg, False bei Fehler nach Retries.
""" """
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 return False # Kein Erfolg, da keine Verbindung
if not update_data: if not update_data:
debug_print("Keine Daten für Batch-Update vorhanden.") # debug_print("Keine Daten für Batch-Update vorhanden.") # Weniger Lärm im Log
return True # Kein Fehler, aber nichts zu tun return True # Kein Fehler, aber nichts zu tun
try: try:
self.sheet.batch_update(update_data) # Der eigentliche API-Aufruf
debug_print(f"Batch-Update erfolgreich ({len(update_data)} Zellen/Bereiche aktualisiert).") 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:
debug_print(f"Google API Fehler beim Batch-Update: {e}") # Spezifische Fehlerbehandlung für gspread/Google API Fehler
# Hier könnte spezifische Fehlerbehandlung erfolgen (z.B. RateLimit) debug_print(f"Google API Fehler beim Batch-Update: {e.response.status_code} - {e.response.text[:500]}")
raise # Fehler weitergeben, damit retry greifen kann # Der @retry_on_failure Decorator sollte diesen Fehler fangen und Wiederholungen versuchen
raise e # Fehler weitergeben, damit der Decorator ihn fängt
except Exception as e: except Exception as e:
debug_print(f"Allgemeiner Fehler beim Batch-Update: {e}") # Allgemeine Fehlerbehandlung
raise # Fehler weitergeben debug_print(f"Allgemeiner Fehler beim Batch-Update: {type(e).__name__} - {e}")
raise e # Fehler weitergeben
# ==================== WIKIPEDIA SCRAPER ==================== # ==================== WIKIPEDIA SCRAPER ====================