diff --git a/brancheneinstufung.py b/brancheneinstufung.py index e4a8bb39..0eae4c5d 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -2421,210 +2421,148 @@ class GoogleSheetHandler: """ Initialisiert den Handler, stellt die Verbindung her und laedt die Daten. """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist + # Holen Sie eine Logger-Instanz fuer diese Klasse + self.logger = logging.getLogger(__name__ + ".GoogleSheetHandler") # Initialisieren Sie die Attribute self.sheet = None - # Daten werden hier als Instanzvariable gespeichert, um nicht bei jedem Zugriff neu laden zu muessen - self.sheet_values = [] - # header_rows sind fix, aber wir koennen sie hier zur Klarheit definieren - self._header_rows = 5 # Annahme: Die ersten 5 Zeilen sind Header - - logger.info("Initialisiere GoogleSheetHandler...") + # ... + self.logger.info("Initialisiere GoogleSheetHandler...") try: # Verbindung wird bei der Initialisierung aufgebaut - # Der _connect Aufruf ist mit retry_on_failure dekoriert. - # Wenn _connect eine Exception wirft (auch nach Retries), wird diese hier gefangen. self._connect() # Daten werden ebenfalls bei der Initialisierung geladen, nur wenn die Verbindung erfolgreich war if self.sheet: - # Der load_data Aufruf ist mit retry_on_failure dekoriert. - # Wenn load_data eine Exception wirft (auch nach Retries), wird diese hier gefangen. - self.load_data() # Erste Datenladung nach erfolgreicher Verbindung + self.load_data() # Erste Datenladung nach erfolgreicher Verbindung else: - # Wenn die Verbindung fehlschlug (sheet ist None), aber keine Exception geworfen wurde (sollte nicht passieren) - logger.critical("GoogleSheetHandler Init FEHLER: Verbindung konnte nicht hergestellt werden (sheet ist None).") - # Werfen Sie eine spezifische Exception - raise ConnectionError("Google Sheet Handler Init failed: Verbindung konnte nicht hergestellt werden.") - + # Wenn die Verbindung fehlschlug (sheet ist None), aber keine Exception geworfen wurde (sollte nicht passieren) + self.logger.critical( + "GoogleSheetHandler Init FEHLER: Verbindung konnte nicht hergestellt werden (sheet ist None)." + ) + raise ConnectionError( + "Google Sheet Handler Init failed: Verbindung konnte nicht hergestellt werden." + ) except Exception as e: # Fehler bei der Initialisierung (entweder von _connect oder load_data nach Retries) - # werden hier gefangen und erneut geworfen, damit die main-Funktion (Block 34) entsprechend reagieren kann. - logger.critical(f"FATAL: Fehler bei Initialisierung von GoogleSheetHandler: {type(e).__name__} - {e}") - # Loggen Sie den Traceback fuer detailliertere Fehlerinformationen - logger.debug(traceback.format_exc()) - # Werfen Sie eine aussagekraeftige Exception - raise ConnectionError(f"Google Sheet Handler Init failed: {e}") # Signalisiert Verbindungsproblem + self.logger.critical( + f"FATAL: Fehler bei Initialisierung von GoogleSheetHandler: {type(e).__name__} - {e}" + ) + self.logger.debug(traceback.format_exc()) + raise ConnectionError(f"Google Sheet Handler Init failed: {e}") - - @retry_on_failure # Wende den Decorator an, da es externe Calls macht + @retry_on_failure def _connect(self): """Stellt Verbindung zum Google Sheet her.""" - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - self.sheet = None # Setze sheet vor dem Versuch auf None - logger.info("Versuche Verbindung mit Google Sheets herstellen...") + self.sheet = None + self.logger.info("Versuche Verbindung mit Google Sheets herstellen...") try: - # Stellen Sie sicher, dass CREDENTIALS_FILE korrekt ist if not os.path.exists(CREDENTIALS_FILE): - # Werfen Sie FileNotFoundError, dies wird vom retry_on_failure als permanent behandelt. - raise FileNotFoundError(f"Credential-Datei nicht gefunden: {CREDENTIALS_FILE}") + raise FileNotFoundError(f"Credential-Datei nicht gefunden: {CREDENTIALS_FILE}") - # Definieren Sie den Scope fuer den Zugriff auf Google Sheets scope = ["https://www.googleapis.com/auth/spreadsheets"] - # Laden Sie die Credentials aus der JSON-Datei creds = ServiceAccountCredentials.from_json_keyfile_name(CREDENTIALS_FILE, scope) - # Autorisiere die Verbindung mit den Credentials gc = gspread.authorize(creds) - # Oeffne das Sheet ueber die URL aus Config - # Dieser Aufruf kann gspread.exceptions.SpreadsheetNotFound werfen, was vom Decorator behandelt wird. - # Er kann auch andere gspread.exceptions.APIError oder requests.exceptions.RequestException werfen. - sh = gc.open_by_url(Config.SHEET_URL) # Nutzt die URL aus Config (Block 1) - - # Greife auf das erste Blatt zu (Index 0, ueblicherweise "Tabelle1") - # Dieser Aufruf kann gspread.exceptions.WorksheetNotFound werfen, wenn das erste Blatt fehlt. - # Wenn das erste Blatt fehlt, ist das ein Konfigurationsproblem und sollte eine Exception werfen. - self.sheet = sh.sheet1 # Greift auf das erste Blatt zu (Index 0) - - logger.info("Verbindung zu Google Sheets erfolgreich.") - # Die Methode gibt implizit None zurueck, wenn keine Exception geworfen wird. - - - # Spezifische Fehlerbehandlung fuer gspread/requests Fehler, die vom Decorator behandelt werden. - # Wenn hier eine Exception durchkommt, hat der Decorator (nach Retries oder weil permanent) aufgegeben. + sh = gc.open_by_url(Config.SHEET_URL) + self.sheet = sh.sheet1 + self.logger.info("Verbindung zu Google Sheets erfolgreich.") except (gspread.exceptions.APIError, requests.exceptions.RequestException, FileNotFoundError) as e: - # Der Decorator hat diesen Fehler bereits geloggt und (falls nicht permanent) Retries versucht. - # Werfen Sie den Fehler erneut, damit der Aufrufer (die __init__ Methode) ihn fangen kann. - raise e - + raise e except Exception as e: - # Fangen Sie andere unerwartete Verbindungsfehler ab (z.B. Probleme mit der Bibliothek selbst) - logger.error(f"FEHLER bei der Google Sheets Verbindung: {type(e).__name__} - {e}") - # Loggen Sie den Traceback fuer detaillierte Fehlerinformationen - logger.debug(traceback.format_exc()) - # Werfen Sie die Exception erneut, damit der Aufrufer sie behandeln kann - raise e + self.logger.error(f"FEHLER bei der Google Sheets Verbindung: {type(e).__name__} - {e}") + self.logger.debug(traceback.format_exc()) + raise e - - @retry_on_failure # Wende den Decorator an, da es externe Calls macht + @retry_on_failure def load_data(self): """ Laedt alle Daten aus dem Sheet und aktualisiert self.sheet_values. """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - # Pruefen Sie, ob eine Sheet-Verbindung vorhanden ist if not self.sheet: - logger.error("Fehler: Keine Sheet-Verbindung zum Laden der Daten.") - self.sheet_values = [] # Stelle sicher, dass die Datenliste leer ist - return False # Signalisiert Fehler beim Laden + self.logger.error("Fehler: Keine Sheet-Verbindung zum Laden der Daten.") + self.sheet_values = [] + return False - logger.info("Lade Daten aus Google Sheet...") + self.logger.info("Lade Daten aus Google Sheet...") try: - # Lade alle Werte aus dem Sheet. Dieser Aufruf kann Exceptions werfen. - # get_all_values() laedt standardmaessig das erste Blatt ("Tabelle1"). - # Wenn das Blatt umbenannt wurde oder fehlt, kann dies einen Fehler werfen. self.sheet_values = self.sheet.get_all_values() - # Wenn get_all_values eine leere Liste zurueckgibt (z.B. Sheet ist leer) if not self.sheet_values: - logger.warning("Google Sheet scheint leer zu sein oder get_all_values() lieferte keine Daten.") - # Wenn die erste Zeile nicht geladen werden kann (z.B. leeres Sheet), headers ist leer - self.headers = [] # Setzen Sie die Header-Liste auf leer - return True # Ladevorgang war technisch erfolgreich (keine Exception), aber keine Daten + self.logger.warning( + "Google Sheet scheint leer zu sein oder get_all_values() lieferte keine Daten." + ) + self.headers = [] + return True - - # Logge die Anzahl der Zeilen und Spalten, die geladen wurden num_rows = len(self.sheet_values) num_cols = len(self.sheet_values[0]) if num_rows > 0 else 0 - logger.info(f"Daten neu geladen: {num_rows} Zeilen, {num_cols} Spalten.") + self.logger.info(f"Daten neu geladen: {num_rows} Zeilen, {num_cols} Spalten.") - # Optional: Ueberpruefen Sie, ob die Anzahl der Spalten mindestens dem hoechsten Index in COLUMN_MAP entspricht try: - # Finden Sie den hoechsten Index in COLUMN_MAP (Block 1) - max_col_idx_in_map = max(COLUMN_MAP.values()) - # Ueberpruefen Sie, ob die Anzahl der geladenen Spalten ausreicht - if num_cols <= max_col_idx_in_map: # Verwenden Sie <= weil Indizes 0-basiert sind - # Logge eine Warnung, wenn das Mapping auf Spalten zeigt, die nicht geladen wurden - logger.warning(f"Geladenes Sheet hat {num_cols} Spalten, erwartet werden aber mindestens {max_col_idx_in_map + 1} basierend auf COLUMN_MAP. Das COLUMN_MAP passt moeglicherweise nicht zum Sheet!") - except ValueError: # Tritt auf, wenn COLUMN_MAP leer ist - logger.warning("COLUMN_MAP scheint leer zu sein oder enthaelt keine Werte. Kann Spaltenanzahl nicht pruefen.") + max_col_idx_in_map = max(COLUMN_MAP.values()) + if num_cols <= max_col_idx_in_map: + self.logger.warning( + f"Geladenes Sheet hat {num_cols} Spalten, erwartet werden aber mindestens " + f"{max_col_idx_in_map + 1} basierend auf COLUMN_MAP." + ) + except ValueError: + self.logger.warning( + "COLUMN_MAP scheint leer zu sein oder enthaelt keine Werte. Kann Spaltenanzahl nicht pruefen." + ) except Exception as e: - # Fangen Sie andere unerwartete Fehler bei der Pruefung ab - logger.error(f"Fehler bei der Pruefung der Spaltenanzahl gegen COLUMN_MAP: {e}") + self.logger.error(f"Fehler bei der Pruefung der Spaltenanzahl gegen COLUMN_MAP: {e}") - - # Speichere die erste Zeile als Header-Namen (optional, kann fuer spaetere Zuordnung nuetzlich sein) - # Dies sollte erst nach Pruefung auf leere sheet_values geschehen if num_rows > 0: - self.headers = self.sheet_values[0] + self.headers = self.sheet_values[0] else: - self.headers = [] + self.headers = [] - - return True # Signalisiert erfolgreiches Laden (auch wenn keine Daten da sind) - - # Spezifische Fehlerbehandlung fuer gspread/requests Fehler, die vom Decorator behandelt werden. - # Wenn hier eine Exception durchkommt, hat der Decorator (nach Retries) aufgegeben. + return True except (gspread.exceptions.APIError, requests.exceptions.RequestException) as e: - # Der Decorator hat diesen Fehler bereits geloggt und Retries versucht. - # Werfen Sie den Fehler erneut, damit der Aufrufer (die __init__ Methode) ihn fangen kann. - raise e - + raise e except Exception as e: - # Fangen Sie andere unerwartete Ladefehler ab - logger.error(f"Allgemeiner Fehler beim Laden der Google Sheet Daten: {type(e).__name__} - {e}") - # Loggen Sie den Traceback - logger.debug(traceback.format_exc()) - # Werfen Sie die Exception erneut, damit der Aufrufer sie behandeln kann - raise e - + self.logger.error( + f"Allgemeiner Fehler beim Laden der Google Sheet Daten: {type(e).__name__} - {e}" + ) + self.logger.debug(traceback.format_exc()) + raise e def get_data(self): """ Gibt die aktuell im Handler gespeicherten Datenzeilen zurueck (ohne die ersten N Header-Zeilen). """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - # Pruefen Sie, ob Daten geladen wurden und ob genuegend Zeilen fuer Header vorhanden sind if not self.sheet_values or len(self.sheet_values) <= self._header_rows: - # Logge nur auf Debug, da dies oft passiert, wenn das Sheet leer ist oder nur Header enthaelt - logger.debug(f"get_data: Keine Datenzeilen verfuegbar (geladen: {len(self.sheet_values) if self.sheet_values else 0} Zeilen, {self._header_rows} Header).") - return [] # Gebe eine leere Liste zurueck, wenn keine Datenzeilen vorhanden sind - - # Gibt eine Slice der Liste zurueck (Kopie, um unbeabsichtigte Aenderungen am Original zu vermeiden) + self.logger.debug( + f"get_data: Keine Datenzeilen verfuegbar " + f"(geladen: {len(self.sheet_values) if self.sheet_values else 0} Zeilen, " + f"{self._header_rows} Header)." + ) + return [] return self.sheet_values[self._header_rows:].copy() - def get_all_data_with_headers(self): - """Gibt alle aktuell im Handler gespeicherten Daten inklusive Header zurueck.""" - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - # Pruefen Sie, ob Daten geladen wurden - if not self.sheet_values: - logger.debug("get_all_data_with_headers: Keine Daten im Handler gespeichert.") - return [] # Gebe eine leere Liste zurueck, wenn keine Daten geladen wurden - - # Geben Sie eine Kopie der gesamten Datenliste zurueck - return self.sheet_values.copy() - + """Gibt alle aktuell im Handler gespeicherten Daten inklusive Header zurueck.""" + if not self.sheet_values: + self.logger.debug("get_all_data_with_headers: Keine Daten im Handler gespeichert.") + return [] + return self.sheet_values.copy() def _get_col_letter(self, col_idx_1_based): """ Konvertiert einen 1-basierten Spaltenindex in den entsprechenden Google Sheets Spaltenbuchstaben (A, B, ..., Z, AA, ...). """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - # Pruefen Sie, ob der Index ein gueltiger Integer und positiv ist if not isinstance(col_idx_1_based, int) or col_idx_1_based < 1: - # Logge den Fehler auf Error-Level - logger.error(f"Ungueltiger Spaltenindex ({col_idx_1_based}) fuer _get_col_letter erhalten.") - return None # Gebe None zurueck bei ungueltigem Index + self.logger.error( + f"Ungueltiger Spaltenindex ({col_idx_1_based}) fuer _get_col_letter erhalten." + ) + return None - # Implementierung zur Konvertierung von Zahl zu Buchstaben string = "" n = col_idx_1_based while n > 0: n, remainder = divmod(n - 1, 26) string = chr(65 + remainder) + string - return string # Gebe den berechneten Spaltenbuchstaben zurueck - + return string def get_start_row_index(self, check_column_key, min_sheet_row=7): """ @@ -2639,90 +2577,84 @@ class GoogleSheetHandler: Standardmaessig ab Zeile 7 (erste Zeile nach 5 Headern und einer leeren). Returns: - int: Der 0-basierte Index in der Datenliste (ohne Header) der ersten Zeile + int: Der 0-basierten Index in der Datenliste (ohne Header) der ersten Zeile mit einem EXAKT leeren Wert in der Zielspalte innerhalb des Suchbereichs. Gibt -1 zurueck bei schwerwiegenden Fehlern (z.B. Schluessel fehlt in COLUMN_MAP). Gibt die Laenge der Datenliste zurueck, wenn keine leere Zelle im Suchbereich gefunden wurde. """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - # Daten neu laden, um sicherzustellen, dass sie aktuell sind if not self.load_data(): - logger.error("Fehler beim Laden der Daten fuer get_start_row_index.") - return -1 # Signalisiert Fehler beim Laden + self.logger.error("Fehler beim Laden der Daten fuer get_start_row_index.") + return -1 - data_rows = self.get_data() # Datenzeilen ohne Header (Kopie) - # Wenn keine Datenzeilen vorhanden sind + data_rows = self.get_data() if not data_rows: - logger.info("Keine Datenzeilen im Sheet gefunden. Startindex fuer leere Zelle ist 0.") - return 0 # Die erste Datenzeile (Index 0) waere der Start + self.logger.info("Keine Datenzeilen im Sheet gefunden. Startindex fuer leere Zelle ist 0.") + return 0 - - # Ermitteln Sie den Index der zu pruefenden Spalte aus COLUMN_MAP (Block 1) check_column_index = COLUMN_MAP.get(check_column_key) if check_column_index is None: - logger.critical(f"FEHLER: Schluessel '{check_column_key}' nicht in COLUMN_MAP gefunden fuer get_start_row_index!") - return -1 # Signalisiert Fehler (Schluessel fehlt) + self.logger.critical( + f"FEHLER: Schluessel '{check_column_key}' nicht in COLUMN_MAP gefunden fuer get_start_row_index!" + ) + return -1 - # Ermitteln Sie den Spaltenbuchstaben fuer Logging actual_col_letter = self._get_col_letter(check_column_index + 1) - # Wenn Spaltenbuchstabe nicht ermittelt werden konnte (ungueltiger Index), loggen wir es. if actual_col_letter is None: - logger.error(f"FEHLER: Konnte Spaltenbuchstaben fuer Index {check_column_index + 1} nicht ermitteln.") - # Gehen Sie weiter, aber die Logs werden weniger informativ sein. - actual_col_letter = f"Index_{check_column_index + 1}" + self.logger.error( + f"FEHLER: Konnte Spaltenbuchstaben fuer Index {check_column_index + 1} nicht ermitteln." + ) + actual_col_letter = f"Index_{check_column_index + 1}" - - # Berechne den Startindex in der 0-basierten 'data_rows' Liste - # min_sheet_row (1-basiert) -> 0-basierten Index in all_data -> 0-basierten Index in data_rows - # 1-basierte Sheet-Zeilennummer minus 1 ergibt den 0-basierten Index in der Gesamtliste (all_data) - # Den Index der Header-Zeilen subtrahieren ergibt den 0-basierten Index in der Datenliste (data_rows) search_start_index_in_data = max(0, (min_sheet_row - 1) - self._header_rows) + self.logger.info( + f"get_start_row_index: Suche ab Daten-Index {search_start_index_in_data} " + f"(Sheet-Zeile {search_start_index_in_data + self._header_rows + 1}) " + f"nach EXAKT LEEREM Wert (=='') in Spalte '{check_column_key}' ({actual_col_letter})..." + ) - logger.info(f"get_start_row_index: Suche ab Daten-Index {search_start_index_in_data} (Sheet-Zeile {search_start_index_in_data + self._header_rows + 1}) nach EXAKT LEEREM Wert (=='') in Spalte '{check_column_key}' ({actual_col_letter})...") - - # Pruefen Sie, ob der Start-Suchindex ausserhalb der Datenliste liegt if search_start_index_in_data >= len(data_rows): - # Wenn der Startindex groesser oder gleich der Laenge der Datenliste ist, - # bedeutet dies, dass der gesamte Suchbereich ausserhalb der vorhandenen Daten liegt. - logger.warning(f"Start-Suchindex in Daten ({search_start_index_in_data}) liegt hinter der letzten Datenzeile ({len(data_rows)}). Keine leere Zelle gefunden im Suchbereich.") - # Rueckgabe der Laenge der Datenliste signalisiert, dass keine leere Zelle im Suchbereich gefunden wurde. + self.logger.warning( + f"Start-Suchindex in Daten ({search_start_index_in_data}) liegt hinter der letzten Datenzeile ({len(data_rows)}). Keine leere Zelle gefunden im Suchbereich." + ) return len(data_rows) - # Iteriere ueber die Datenzeilen ab dem berechneten Startindex for i in range(search_start_index_in_data, len(data_rows)): - row = data_rows[i] # Die aktuelle Zeile in der Datenliste (0-basiert) - current_sheet_row = i + self._header_rows + 1 # 1-basierte Sheet-Zeilennummer fuer Logging + row = data_rows[i] + current_sheet_row = i + self._header_rows + 1 - cell_value = ""; is_exactly_empty = True - # Ueberpruefe, ob die Zeile lang genug ist, um auf die Spalte zuzugreifen + cell_value = "" + is_exactly_empty = True if len(row) > check_column_index: - cell_value = row[check_column_index] # Hole den Wert in der Zielspalte - # Pruefen Sie, ob der Wert EXAKT ein leerer String ist - if cell_value != "": is_exactly_empty = False + cell_value = row[check_column_index] + if cell_value != "": + is_exactly_empty = False else: - # Wenn die Zeile nicht lang genug ist, gilt die Zelle in der Zielspalte als leer - is_exactly_empty = True + is_exactly_empty = True - # Logge die ersten paar Zeilen, jede 1000. Zeile oder wenn eine leere Zelle gefunden wird log_debug = (i < search_start_index_in_data + 5) or (i % 1000 == 0) or is_exactly_empty if log_debug: - # Logge den Wert und den Status der Pruefung - logger.debug(f" -> Pruefe Daten-Index {i} (Sheet {current_sheet_row}): Wert in {actual_col_letter}='{str(cell_value).strip()}' (Roh='{cell_value}' Typ: {type(cell_value)}). Ist exakt leer ('')? {is_exactly_empty}") + self.logger.debug( + f" -> Pruefe Daten-Index {i} (Sheet {current_sheet_row}): " + f"Wert in {actual_col_letter}='{str(cell_value).strip()}' " + f"(Roh='{cell_value}' Typ: {type(cell_value)}). Leer? {is_exactly_empty}" + ) - # Wenn eine exakt leere Zelle gefunden wurde if is_exactly_empty: - logger.info(f"Erste Zeile ab Sheet-Zeile {min_sheet_row} mit EXAKT LEEREM Wert in Spalte {actual_col_letter} gefunden: Zeile {current_sheet_row} (Daten-Index {i})") - return i # Gebe den 0-basierten Index in der Datenliste zurueck (dies ist der Startindex fuer die Verarbeitung) + self.logger.info( + f"Erste Zeile ab Sheet-Zeile {min_sheet_row} mit EXAKT LEEREM Wert in Spalte " + f"{actual_col_letter} gefunden: Zeile {current_sheet_row} (Daten-Index {i})" + ) + return i - # Wenn die Schleife durchlaeuft, ohne eine exakt leere Zelle im Suchbereich zu finden - last_data_index = len(data_rows) # Der Index nach der letzten Datenzeile - logger.info(f"Alle Zeilen ab Daten-Index {search_start_index_in_data} im Suchbereich haben einen nicht-leeren Wert in Spalte {actual_col_letter}. Naechster Daten-Index waere {last_data_index}.") - # Rueckgabe der Laenge der Datenliste signalisiert, dass keine leere Zelle gefunden wurde. + last_data_index = len(data_rows) + self.logger.info( + f"Alle Zeilen ab Daten-Index {search_start_index_in_data} im Suchbereich haben einen " + f"nicht-leeren Wert in Spalte {actual_col_letter}. Naechster Daten-Index waere {last_data_index}." + ) return last_data_index - - @retry_on_failure # Wende den Decorator an, da es externe Calls macht + @retry_on_failure def batch_update_cells(self, update_data): """ Fuehrt ein Batch-Update im Google Sheet durch. Beinhaltet robustere @@ -2736,45 +2668,28 @@ class GoogleSheetHandler: Returns: bool: True bei Erfolg (nach allen Retries), False bei endgueltigem Fehler. """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - # Pruefen Sie, ob eine Sheet-Verbindung vorhanden ist if not self.sheet: - logger.error("FEHLER: Keine Sheet-Verbindung fuer Batch-Update.") - return False # Signalisiert Fehler + self.logger.error("FEHLER: Keine Sheet-Verbindung fuer Batch-Update.") + return False - # Wenn keine Update-Daten vorhanden sind if not update_data: - # logger.debug("Keine Daten fuer Batch-Update vorhanden.") # Zu viel Laerm im Debug - return True # Nichts zu tun ist technisch ein Erfolg - - - # Die retry_on_failure Logik kuemmert sich um die Wiederholung und das Werfen - # der Exception im Fehlerfall. Wir muessen hier nur den Aufruf machen und - # das Ergebnis (oder die Exception) weitergeben. + return True try: - # Schaetze die Anzahl der zu aktualisierenden Zellen fuer Logging - total_cells_to_update = sum(len(row) for item in update_data for row in item.get('values', [])) - logger.debug(f" -> Versuche sheet.batch_update mit {len(update_data)} Anfragen ({total_cells_to_update} Zellen)...") - - # Fuehren Sie das Batch-Update durch. gspread.batch_update wirft bei Fehlern Exceptions, - # die vom @retry_on_failure Decorator gefangen werden. - # value_input_option='USER_ENTERED' interpretiert die Eingaben wie ein Nutzer. + total_cells_to_update = sum( + len(row) for item in update_data for row in item.get('values', []) + ) + self.logger.debug( + f" -> Versuche sheet.batch_update mit {len(update_data)} Anfragen " + f"({total_cells_to_update} Zellen)..." + ) self.sheet.batch_update(update_data, value_input_option='USER_ENTERED') - - # Wenn keine Exception aufgetreten ist, war der Aufruf (ggf. nach Retries) erfolgreich. - # logger.debug(f" -> sheet.batch_update erfolgreich abgeschlossen.") # Zu viel Laerm im Debug - return True # Signalisiert Erfolg - - # Exceptions werden vom retry_on_failure gefangen und (im Fehlerfall) neu geworfen. - # Wenn eine Exception hier durchkommt, hat retry_on_failure aufgegeben. - except Exception as e: - # Der endgueltige Fehler wurde bereits vom Decorator geloggt. - # Wir fangen ihn hier nur, um False zurueckzugeben, wie in der Signatur versprochen. - # Das re-raising im Decorator sorgt dafuer, dass wir hier landen, wenn der Decorator aufgibt. - logger.error(f"Endgueltiger Fehler beim Batch-Update nach Retries. Kann {len(update_data)} Operationen nicht durchfuehren.") - # Der Traceback wurde bereits vom Decorator (im except Exception Fall) geloggt. - return False # Signalisiert endgueltigen Fehler + return True + except Exception: + self.logger.error( + f"Endgueltiger Fehler beim Batch-Update nach Retries. Kann {len(update_data)} Operationen nicht durchfuehren." + ) + return False # --- WIKIPEDIA SCRAPER CLASS --- @@ -2802,741 +2717,483 @@ class WikipediaScraper: self.logger.debug("WikipediaScraper initialisiert.") # User-Agent fuer Requests (nutzt Config, Fallback wenn nicht gesetzt) - self.user_agent = user_agent or getattr(Config, 'USER_AGENT', 'Mozilla/5.0 (compatible; UnternehmenSkript/1.0; +http://www.example.com/bot)') # Beispiel URL anpassen - self.session = requests.Session() # Nutzt eine Requests Session fuer bessere Performance bei mehreren Anfragen - self.session.headers.update({'User-Agent': self.user_agent}) # Setzt den User-Agent Header + self.user_agent = user_agent or getattr( + Config, 'USER_AGENT', + 'Mozilla/5.0 (compatible; UnternehmenSkript/1.0; +http://www.example.com/bot)' + ) + self.session = requests.Session() + self.session.headers.update({'User-Agent': self.user_agent}) self.logger.debug(f"Requests Session mit User-Agent '{self.user_agent}' initialisiert.") # Keywords fuer die Infobox-Extraktion self.keywords_map = { - 'branche': ['branche', 'wirtschaftszweig', 'industry', 'taetigkeit', 'sektor', 'produkte', 'leistungen'], # Umlaute vermeiden - 'umsatz': ['umsatz', 'erloes', 'revenue', 'jahresumsatz', 'konzernumsatz', 'ergebnis'], # Umlaute vermeiden - 'mitarbeiter': ['mitarbeiter', 'mitarbeiterzahl', 'beschaeftigte', 'employees', 'number of employees', 'personal', 'belegschaft'] # Umlaute vermeiden + 'branche': ['branche', 'wirtschaftszweig', 'industry', 'taetigkeit', 'sektor', 'produkte', 'leistungen'], + 'umsatz': ['umsatz', 'erloes', 'revenue', 'jahresumsatz', 'konzernumsatz', 'ergebnis'], + 'mitarbeiter': ['mitarbeiter', 'mitarbeiterzahl', 'beschaeftigte', 'employees', 'number of employees', 'personal', 'belegschaft'] } # Konfiguriere die wikipedia-Bibliothek try: - wiki_lang = getattr(Config, 'LANG', 'de') # Sprache aus Config holen - wikipedia.set_lang(wiki_lang) # Setzt die Sprache fuer die wikipedia Bibliothek - # Aktivieren Sie Rate Limiting, um die Wikipedia-API nicht zu ueberlasten - wikipedia.set_rate_limiting(True, min_wait=0.1) # Minimum 0.1 Sekunden warten zwischen API calls - self.logger.info(f"Wikipedia library language set to '{wiki_lang}'. Rate limiting enabled (min_wait=0.1).") + wiki_lang = getattr(Config, 'LANG', 'de') + wikipedia.set_lang(wiki_lang) + wikipedia.set_rate_limiting(True, min_wait=0.1) + self.logger.info( + f"Wikipedia library language set to '{wiki_lang}'. Rate limiting enabled (min_wait=0.1)." + ) except Exception as e: - # Logge Fehler bei der Konfiguration der wikipedia Bibliothek self.logger.warning(f"Fehler beim Setzen der Wikipedia-Sprache oder Rate Limiting: {e}") - - # --- Interne Helfermethoden --- - # Diese Methoden sind nur fuer die interne Nutzung der WikipediaScraper Klasse gedacht. - def _get_full_domain(self, website): """Extrahiert die normalisierte Domain (ohne www, ohne Pfad) aus einer URL.""" - # Diese Funktion nutzt die globale simple_normalize_url, was besser ist als eine duplizierte Implementierung. - return simple_normalize_url(website) # Nutzt die globale Funktion (Block 4) - + return simple_normalize_url(website) def _generate_search_terms(self, company_name, website): """ Generiert eine Liste von Suchbegriffen fuer die Wikipedia-Suche, inklusive normalisiertem Namen, Kurzformen und Domain. """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - if not company_name: return [] # Gebe leere Liste zurueck, wenn kein Firmenname da ist - terms = set() # Nutzt ein Set, um Duplikate automatisch zu behandeln + if not company_name: + return [] + terms = set() - # Fuegen Sie den originalen Namen hinzu original_name_cleaned = str(company_name).strip() if original_name_cleaned: terms.add(original_name_cleaned) - # Fuegen Sie den normalisierten Namen und Teile davon hinzu (nutzt globale Funktion) - normalized_name = normalize_company_name(company_name) # Nutzt die globale Funktion (Block 4) + normalized_name = normalize_company_name(company_name) if normalized_name: - terms.add(normalized_name) - name_parts = normalized_name.split() - if len(name_parts) > 0: terms.add(name_parts[0]) # Erstes Wort - if len(name_parts) > 1: terms.add(" ".join(name_parts[:2])) # Erste zwei Woerter + terms.add(normalized_name) + name_parts = normalized_name.split() + if len(name_parts) > 0: + terms.add(name_parts[0]) + if len(name_parts) > 1: + terms.add(" ".join(name_parts[:2])) - # Fuegen Sie die Domain hinzu (nutzt interne Methode, die simple_normalize_url nutzt) full_domain = self._get_full_domain(website) - # Wenn die Domain gueltig ist (nicht "k.A.") if full_domain != "k.A.": - terms.add(full_domain) + terms.add(full_domain) - - # Entferne leere Strings aus dem Set und konvertiere zu einer Liste. - # Limitiere die Anzahl der Begriffe auf die konfigurierte Anzahl der Suchergebnisse. - final_terms = [term for term in list(terms) if term][:getattr(Config, 'WIKIPEDIA_SEARCH_RESULTS', 5)] # Limitiere auf Anzahl der Suchergebnisse (Config, Block 1) - # Logge die generierten Suchbegriffe auf Debug-Level - self.logger.debug(f"Generierte Suchbegriffe fuer '{company_name[:100]}...': {final_terms}") # Gekuerzt loggen + final_terms = [term for term in list(terms) if term][:getattr(Config, 'WIKIPEDIA_SEARCH_RESULTS', 5)] + self.logger.debug(f"Generierte Suchbegriffe fuer '{company_name[:100]}...': {final_terms}") return final_terms - - @retry_on_failure # Wende den Decorator an, da es externe Calls macht (Requests) + @retry_on_failure def _get_page_soup(self, url): """ Holt HTML von einer URL (requests) und gibt ein BeautifulSoup-Objekt zurueck. - Dies wird fuer die manuelle Extraktion von Infoboxen und anderen Details benoetigt, - die nicht direkt ueber die wikipedia Bibliothek verfuegbar sind. """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - # Pruefen Sie auf ungueltige oder leere URLs if not url or not isinstance(url, str) or not url.lower().startswith(("http://", "https://")): - self.logger.warning(f"_get_page_soup: Ungueltige URL '{url[:100]}...'.") # Gekuerzt loggen - return None # Gebe None zurueck bei ungueltigen Eingaben - - + self.logger.warning(f"_get_page_soup: Ungueltige URL '{url[:100]}...'.") + return None try: - # Logge den Versuch, die URL abzurufen - self.logger.debug(f"_get_page_soup: Rufe URL ab: {url[:100]}...") # Gekuerzt loggen - # Führen Sie die GET-Anfrage mit der Instanz Session durch (behaelt Cookies etc.). - # Timeout sollte aus Config kommen. - # Die raise_for_status() und die RequestsExceptions werden vom retry_on_failure Decorator behandelt. + self.logger.debug(f"_get_page_soup: Rufe URL ab: {url[:100]}...") response = self.session.get(url, timeout=getattr(Config, 'REQUEST_TIMEOUT', 15)) - response.raise_for_status() # Wirft HTTPError fuer 4xx/5xx Antworten. - - # Versuchen Sie, das Encoding aus dem Header oder dem Content zu erraten + response.raise_for_status() response.encoding = response.apparent_encoding - - # Parsen Sie den HTML-Inhalt mit BeautifulSoup. - # Nutzt den konfigurierten Parser aus Config oder einen Fallback. soup = BeautifulSoup(response.text, getattr(Config, 'HTML_PARSER', 'html.parser')) + self.logger.debug(f"_get_page_soup: Parsen von {url[:100]}... erfolgreich.") + return soup + except Exception as e: + self.logger.error(f"_get_page_soup: Fehler beim Abrufen oder Parsen von HTML von {url[:100]}...: {type(e).__name__} - {e}") + raise e - # Logge den Erfolg des Parsens - self.logger.debug(f"_get_page_soup: Parsen von {url[:100]}... erfolgreich.") # Gekuerzt loggen - return soup # Gebe das Soup-Objekt zurueck - - # Exceptions (wie RequestsErrors) werden vom retry_on_failure Decorator behandelt. - # Wenn eine Exception hier durchkommt, hat der Decorator aufgegeben. - except Exception as e: # Fangen Sie alle verbleibenden Exceptions ab - # Logge den Fehler auf Error-Level - self.logger.error(f"_get_page_soup: Fehler beim Abrufen oder Parsen von HTML von {url[:100]}...: {type(e).__name__} - {e}") # Gekuerzt loggen - # Die Exception wurde bereits vom retry_on_failure Decorator als finaler Fehler geloggt. - # Werfen Sie die Exception erneut, damit der Aufrufer dies behandeln kann (z.B. extract_company_data). - raise e # Leite die Exception weiter - - - # --- Überarbeitete Validierungsmethode --- - # Validiert, ob ein Wikipedia-Artikel zum Unternehmen passt. - # Nutzt interne Methoden und globale Helfer. def _validate_article(self, page, company_name, website): """ Validiert, ob ein Wikipedia-Artikel (represented by the Page object) - zum Unternehmen passt. Prueft Titelaehnlichkeit (gewichtet Anfangsworte), - Domain-Match in Links und passt Schwellenwerte dynamisch an. - - Args: - page (wikipedia.WikipediaPage): Das geladene Wikipedia Page Objekt. - company_name (str): Der Name des Unternehmens (CRM Name). - website (str): Die Website des Unternehmens (CRM Website). - - Returns: - bool: True, wenn der Artikel validiert wurde, sonst False. + zum Unternehmen passt. """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - if not page or not company_name: return False # Grundlegende Pruefung - # page.title ist der Titel des Wikipedia-Artikels - self.logger.debug(f"Validiere Artikel '{page.title[:100]}...' (URL: {page.url[:100]}...) fuer Firma '{company_name[:100]}' (Website: {website[:100]})...") # Gekuerzt loggen + if not page or not company_name: + return False + self.logger.debug( + f"Validiere Artikel '{page.title[:100]}...' (URL: {page.url[:100]}...) " + f"fuer Firma '{company_name[:100]}' (Website: {website[:100]})..." + ) - - # Normalisiere Namen (nutzt globale Funktion Block 4) normalized_company = normalize_company_name(company_name) normalized_title = normalize_company_name(page.title) - - # Wenn Normalisierung fehlschlaegt oder zu leeren Strings fuehrt if not normalized_company or not normalized_title: - self.logger.warning("Validierung nicht moeglich, da Normalisierung eines Namens fehlschlug.") - return False + self.logger.warning("Validierung nicht moeglich, da Normalisierung eines Namens fehlschlug.") + return False - # Basisschwelle fuer Aehnlichkeit aus Config (Block 1) standard_threshold = getattr(Config, 'SIMILARITY_THRESHOLD', 0.65) - - # 1. Titelaehnlichkeit (Gesamt) - # Berechne Aehnlichkeit zwischen den normalisierten Namen (nutzt globale Helfer Block 4) similarity = fuzzy_similarity(normalized_title, normalized_company) - self.logger.debug(f" -> Gesamt-Aehnlichkeit (normalized): {similarity:.2f} ('{normalized_title[:50]}...' vs '{normalized_company[:50]}...')") # Gekuerzt loggen + self.logger.debug(f" -> Gesamt-Aehnlichkeit (normalized): {similarity:.2f}") - # 2. Aehnlichkeit der ersten Worte (Normalisiert) company_tokens = normalized_company.split() title_tokens = normalized_title.split() first_word_match = False first_two_words_match = False + if company_tokens and title_tokens and company_tokens[0] == title_tokens[0]: + first_word_match = True + if len(company_tokens) > 1 and len(title_tokens) > 1 and company_tokens[1] == title_tokens[1]: + first_two_words_match = True - if len(company_tokens) > 0 and len(title_tokens) > 0: - # Pruefe das erste Wort (case-sensitive nach Normalisierung) - if company_tokens[0] == title_tokens[0]: - first_word_match = True - # self.logger.debug(" -> Erstes normalisiertes Wort stimmt ueberein.") # Zu viel Laerm im Debug - # Wenn das erste Wort uebereinstimmt, pruefe das zweite Wort - if len(company_tokens) > 1 and len(title_tokens) > 1: - # Pruefe das zweite Wort (case-sensitive nach Normalisierung) - if company_tokens[1] == title_tokens[1]: - first_two_words_match = True - # self.logger.debug(" -> Erste zwei normalisierte Worte stimmen ueberein.") # Zu viel Laerm im Debug - - # 3. Link-Pruefung (Domain-Match in externen Links des Artikels) domain_found = False - # Extrahieren Sie die normalisierte Domain aus der Website-URL (nutzt interne Methode, die simple_normalize_url nutzt) full_domain = self._get_full_domain(website) - # Nur weitermachen, wenn eine gueltige Domain extrahiert wurde if full_domain != "k.A.": - self.logger.debug(f" -> Suche nach Domain '{full_domain}' in externen Links des Artikels...") - try: - # Direkte Abfrage des HTML ueber page.html() kann schneller sein als erneuter Requests Call - # page.html() kann Exceptions werfen. - article_html = page.html() - if article_html: - # Parsen Sie den HTML-Inhalt des Artikels - soup = BeautifulSoup(article_html, getattr(Config, 'HTML_PARSER', 'html.parser')) - # Suche nach externen Links (href, die mit http starten) - external_links = soup.select('a[href^="http"]') # Suche nach allen A-Tags mit href, der mit "http" beginnt + self.logger.debug(f" -> Suche nach Domain '{full_domain}' in externen Links des Artikels...") + try: + article_html = page.html() + if article_html: + soup = BeautifulSoup(article_html, getattr(Config, 'HTML_PARSER', 'html.parser')) + external_links = soup.select('a[href^="http"]') + relevant_links = [] + for link in external_links: + href = link.get('href', '') + if href and isinstance(href, str) and full_domain in simple_normalize_url(href): + if not any(ex in href.lower() for ex in ['wikipedia.org', 'wikimedia.org', 'wikidata.org', 'archive.org', 'webcitation.org']): + relevant_links.append(link) + if relevant_links: + domain_found = True + except Exception as e_link_check: + self.logger.error( + f"Fehler waehrend der Domain-Link-Pruefung fuer '{page.title[:100]}...': " + f"{type(e_link_check).__name__} - {e_link_check}" + ) - # Filtere Links: Muss die gesuchte Domain enthalten UND darf kein Wikipedia-eigener Link sein. - relevant_links = [] - for link in external_links: - href = link.get('href', '') # Hole das href-Attribut - # Pruefe, ob href gueltig ist und die Domain im normalisierten Link enthalten ist - if href and isinstance(href, str) and full_domain in simple_normalize_url(href): - # Pruefe, ob der Link NICHT auf eine Wikipedia-eigene Seite verweist - if not any(exclude in href.lower() for exclude in ['wikipedia.org', 'wikimedia.org', 'wikidata.org', 'archive.org', 'webcitation.org']): - relevant_links.append(link) # Fuege den relevanten Link zur Liste hinzu - - # Wenn relevante Links gefunden wurden - if relevant_links: - # Optional: Pruefe, ob der Link in der Infobox ist oder typischen Text hat (Komplexitaet vs Nutzen) - # Fuer eine einfachere Implementierung reicht es, wenn der Link existiert. - domain_found = True - # logger.debug(f" -> Domain '{full_domain}' in {len(relevant_links)} externen Links gefunden.") # Zu viel Laerm im Debug - else: - # logger.debug(f" -> Domain '{full_domain}' nicht in externen Links gefunden.") # Zu viel Laerm im Debug - pass # domain_found bleibt False - - else: - self.logger.warning(" -> Konnte HTML fuer Link-Pruefung nicht abrufen (page.html() leer).") - - except Exception as e_link_check: - # Logge Fehler waehrend der Domain-Link-Pruefung, aber fahre fort - self.logger.error(f"Fehler waehrend der Domain-Link-Pruefung fuer '{page.title[:100]}...': {type(e_link_check).__name__} - {e_link_check}") # Gekuerzt loggen - # Fehler beim Link-Check sollte die Validierung nicht blockieren, nur beeinflussen. - pass # domain_found bleibt False oder sein aktueller Wert. - - else: - self.logger.debug(" -> Keine Website-Domain fuer Link-Pruefung vorhanden oder ungueltig ('k.A.').") - - - # 4. Dynamische Schwellenwert-Entscheidung (Bewertung des Artikels) - is_valid = False # Initialisierung des Validierungs-Flags - reason = "Keine Validierungsregel traf zu" # Default Grund fuer das Ergebnis - - # Pruefe die Validierungsbedingungen in absteigender Reihenfolge ihrer Stärke / Relevanz - # Die erste Bedingung, die True ist, bestimmt das Ergebnis. + # Dynamische Schwellenwert-Entscheidung + is_valid = False + reason = "Keine Validierungsregel traf zu" if similarity >= standard_threshold: is_valid = True reason = f"Gesamt-Aehnlichkeit ({similarity:.2f}) >= Standard-Schwelle ({standard_threshold:.2f})" - elif domain_found and first_two_words_match: # Staerkste Kombination von Indikatoren - is_valid = True - reason = f"Domain gefunden UND erste 2 normalisierte Worte stimmen ueberein (Sim={similarity:.2f})" - elif domain_found and first_word_match and similarity >= 0.40: # Domain + Erstes Wort + Moderate Aehnlichkeit - is_valid = True - reason = f"Domain gefunden UND erstes normalisiertes Wort stimmt ueberein UND Aehnlichkeit >= 0.40 (Sim={similarity:.2f})" - elif first_two_words_match and similarity >= 0.45: # Erste zwei Worte + Moderate Aehnlichkeit (auch ohne Domain) - is_valid = True - reason = f"Erste zwei normalisierte Worte stimmen ueberein UND Aehnlichkeit >= 0.45 (Sim={similarity:.2f})" - elif domain_found and similarity >= 0.50: # Nur Domain + Etwas hoehere Aehnlichkeit - is_valid = True - reason = f"Domain gefunden UND Aehnlichkeit >= 0.50 (Sim={similarity:.2f})" - elif first_word_match and similarity >= 0.55: # Nur Erstes Wort + Etwas hoehere Aehnlichkeit - is_valid = True - reason = f"Erstes normalisiertes Wort stimmt ueberein UND Aehnlichkeit >= 0.55 (Sim={similarity:.2f})" - # Weitere optionale, weniger strenge Schwellen koennten hier hinzugefuegt werden, wenn gewuenscht. - # z.B. elif domain_found and similarity >= 0.30: + elif domain_found and first_two_words_match: + is_valid = True + reason = f"Domain gefunden UND erste 2 normalisierte Worte stimmen ueberein (Sim={similarity:.2f})" + elif domain_found and first_word_match and similarity >= 0.40: + is_valid = True + reason = f"Domain gefunden UND erstes normalisiertes Wort stimmt ueberein UND Aehnlichkeit >= 0.40 (Sim={similarity:.2f})" + elif first_two_words_match and similarity >= 0.45: + is_valid = True + reason = f"Erste zwei normalisierte Worte stimmen ueberein UND Aehnlichkeit >= 0.45 (Sim={similarity:.2f})" + elif domain_found and similarity >= 0.50: + is_valid = True + reason = f"Domain gefunden UND Aehnlichkeit >= 0.50 (Sim={similarity:.2f})" + elif first_word_match and similarity >= 0.55: + is_valid = True + reason = f"Erstes normalisiertes Wort stimmt ueberein UND Aehnlichkeit >= 0.55 (Sim={similarity:.2f})" - - # Logge das Ergebnis der Validierung (INFO wenn validiert, DEBUG sonst) log_level = logging.INFO if is_valid else logging.DEBUG - self.logger.log(log_level, f" => Artikel '{page.title[:100]}...' {'VALIDIERT' if is_valid else 'NICHT validiert'} (Grund: {reason}. Details: Sim={similarity:.2f}, Domain? {domain_found}, 1stWord? {first_word_match}, 2ndWord? {first_two_words_match})") # Gekuerzt loggen - - return is_valid # Gebe das Ergebnis der Validierung zurueck - - - # --- Extraktionsmethoden --- - # Methoden zum Extrahieren spezifischer Daten aus einem Wikipedia Soup-Objekt. + self.logger.log( + log_level, + f" => Artikel '{page.title[:100]}...' " + f"{'VALIDIERT' if is_valid else 'NICHT validiert'} " + f"(Grund: {reason}. Details: Sim={similarity:.2f}, Domain? {domain_found}, " + f"1stWord? {first_word_match}, 2ndWord? {first_two_words_match})" + ) + return is_valid def _extract_first_paragraph_from_soup(self, soup): """ Extrahiert den ersten aussagekraeftigen Absatz aus dem Soup-Objekt eines Wikipedia-Artikels. Entfernt Referenzen und unerwuenschte Elemente. """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - if not soup: return "k.A." # Gebe "k.A." zurueck, wenn kein Soup-Objekt da ist - paragraph_text = "k.A." # Initialisiere mit "k.A." + if not soup: + return "k.A." + paragraph_text = "k.A." try: - # Finden Sie den Hauptinhalt-Div (mw-parser-output ist Standard auf moderner Wikipedia) content_div = soup.find('div', class_='mw-parser-output') - # Wenn der Haupt-Div gefunden wurde, suchen Sie darin, sonst im gesamten Soup search_area = content_div if content_div else soup - # Suchen Sie die ersten p-Tags direkt unter diesem Div (recursive=False) oder im gesamten Suchbereich. - # Oft ist der erste relevante Absatz ein direktes Kind des Haupt-Divs. paragraphs = search_area.find_all('p', recursive=False) - # Fallback, falls keine direkten p-Tags gefunden werden, suche rekursiv im gesamten Suchbereich. - if not paragraphs: paragraphs = search_area.find_all('p') + if not paragraphs: + paragraphs = search_area.find_all('p') - - # Gehe durch die gefundenen Absaetze, um den ersten geeigneten zu finden - # logger.debug(f"Suche ersten Absatz in {len(paragraphs)} gefundenen
-Tags...") # Zu viel Laerm im Debug for p in paragraphs: - # Entferne Stoerende Elemente wie Referenzen ([1]), versteckte Spans oder Koordinaten innerhalb des p-Tags, BEVOR der Text extrahiert wird. - for sup in p.find_all('sup', class_='reference'): sup.decompose() # Referenz-Tags - for span in p.find_all('span', style=lambda value: value and 'display:none' in value): span.decompose() # Versteckte Spans (z.B. fuer Sortierschluessel) - for span in p.find_all('span', id='coordinates'): span.decompose() # Koordinaten-Span + for sup in p.find_all('sup', class_='reference'): + sup.decompose() + for span in p.find_all('span', style=lambda v: v and 'display:none' in v): + span.decompose() + for span in p.find_all('span', id='coordinates'): + span.decompose() - - # Extrahiere und bereinige den Text des Absatzes (nutzt globale Funktion clean_text Block 4) text = clean_text(p.get_text(separator=' ', strip=True)) + if text != "k.A." and len(text) > 50: + if not re.match( + r'^(Datei:|Abbildung:|Siehe auch:|Einzelnachweise|Siehe auch|Literatur)', + text, + re.IGNORECASE + ): + paragraph_text = text[:1500] + break - # Pruefe, ob der Text nach der Bereinigung gueltig und lang genug ist. - # Ein sehr kurzer Text ist oft nur eine Bildunterschrift oder aehnliches. - if text != "k.A." and len(text) > 50: # Mindestlaenge 50 Zeichen (kann angepasst werden) - # Pruefe auf gaengige unerwuenschte Anfange (z.B. nach Infoboxen oder Listen) - # Vermeide Absaetze, die mit "Datei:", "Abbildung:", "Siehe auch:", "Einzelnachweise" etc. beginnen. - if not re.match(r'^(Datei:|Abbildung:|Siehe auch:|Einzelnachweise|Siehe auch|Literatur|Einzelnachweise)', text, re.IGNORECASE): - # Wenn der Absatz relevant aussieht, nimm ihn und begrenze die Laenge. - paragraph_text = text[:1500] # Begrenze die Laenge des extrahierten Absatzes (z.B. auf 1500 Zeichen) - # logger.debug(f" -> Ersten gueltigen Absatz gefunden: {paragraph_text[:100]}...") # Logge den Anfang des Absatzes (gekuerzt) - break # Hoere beim ersten guten Absatz auf - - - # Wenn die Schleife durchlaeuft und kein passender Absatz gefunden wurde if paragraph_text == "k.A.": - self.logger.debug("Kein passender erster Absatz gefunden nach Pruefung der
-Tags.") - + self.logger.debug("Kein passender erster Absatz gefunden nach Pruefung der
-Tags.") except Exception as e: - # Fange unerwartete Fehler waehrend der Extraktion ab self.logger.error(f"Fehler beim Extrahieren des ersten Absatzes: {type(e).__name__} - {e}") - # Logge den Traceback self.logger.debug(traceback.format_exc()) - # paragraph_text bleibt "k.A." oder sein Initialwert - - return paragraph_text # Gebe den extrahierten Text oder "k.A." zurueck - + return paragraph_text def extract_categories(self, soup): """ Extrahiert Wikipedia-Kategorien aus dem Soup-Objekt. """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - if not soup: return "k.A." # Gebe "k.A." zurueck, wenn kein Soup-Objekt da ist - cats_filtered = [] # Liste zum Sammeln der bereinigten Kategorien + if not soup: + return "k.A." + cats_filtered = [] try: - # Kategorien sind normalerweise in einem div mit id="mw-normal-catlinks" cat_div = soup.find('div', id="mw-normal-catlinks") - # Wenn der Kategorien-Div gefunden wurde if cat_div: - # Die Kategorien sind innerhalb eines ul-Tags unter diesem div ul = cat_div.find('ul') - # Wenn das ul-Tag gefunden wurde if ul: - # Jede Kategorie ist ein li-Element innerhalb des ul. - # Extrahieren Sie den Text und bereinigen Sie ihn (clean_text nutzt globale Funktion Block 4). cats = [clean_text(li.get_text()) for li in ul.find_all('li')] - # Filtere leere oder unerwuenschte Eintraege (wie "Kategorien:") aus der Liste - cats_filtered = [c for c in cats if c and isinstance(c, str) and c.strip() and "kategorien:" not in c.lower()] - # Logge die gefundenen Kategorien auf Debug-Level + cats_filtered = [ + c for c in cats + if c and isinstance(c, str) and c.strip() and "kategorien:" not in c.lower() + ] self.logger.debug(f"Kategorien gefunden: {cats_filtered}") else: - # Wenn kein ul-Tag im Kategorien-Div gefunden wurde - self.logger.debug("Kein 'ul' Tag in 'mw-normal-catlinks' gefunden.") + self.logger.debug("Kein 'ul' Tag in 'mw-normal-catlinks' gefunden.") else: - # Wenn der Kategorien-Div nicht gefunden wurde - self.logger.debug("Kein 'div#mw-normal-catlinks' gefunden.") - + self.logger.debug("Kein 'div#mw-normal-catlinks' gefunden.") except Exception as e: - # Fange unerwartete Fehler waehrend der Extraktion ab - self.logger.error(f"Fehler beim Extrahieren der Kategorien: {type(e).__name__} - {e}") - # Logge den Traceback - self.logger.debug(traceback.format_exc()) - - # Verbinde die bereinigten Kategorien mit Komma und Leerzeichen, oder gebe "k.A." zurueck, wenn keine gefunden wurden. + self.logger.error(f"Fehler beim Extrahieren der Kategorien: {type(e).__name__} - {e}") + self.logger.debug(traceback.format_exc()) return ", ".join(cats_filtered) if cats_filtered else "k.A." - def _extract_infobox_value(self, soup, target): """ Extrahiert gezielt Branche, Umsatz oder Mitarbeiter aus der Infobox eines Wikipedia-Artikels Soup-Objekts. - Beruecksichtigt Header in