diff --git a/brancheneinstufung.py b/brancheneinstufung.py
index c4cc5898..9f57bbaa 100644
--- a/brancheneinstufung.py
+++ b/brancheneinstufung.py
@@ -8,7 +8,7 @@ von Unternehmensdaten, primär aus einem Google Sheet, ergänzt durch Web Scrapi
Wikipedia, OpenAI (ChatGPT) und SerpAPI (Google Search, LinkedIn).
Autor: [Ihr Name/Pseudonym]
-Version: v1.7.0
+Version: v1.7.1
Hinweis zur Struktur:
Dieser Code wird in logischen Bloecken uebermittelt. Fuegen Sie die Bloecke
@@ -107,7 +107,7 @@ PATTERNS_FILE_JSON = "technician_patterns.json" # Neu (Empfohlen)
# --- Globale Konfiguration Klasse ---
class Config:
"""Zentrale Konfigurationseinstellungen."""
- VERSION = "v1.7.0"
+ VERSION = "v1.7.1"
LANG = "de" # Sprache fuer Wikipedia etc.
# ACHTUNG: SHEET_URL ist hier ein Platzhalter. Ersetzen Sie ihn durch Ihre tatsaechliche URL.
SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" # <<< ERSETZEN SIE DIES!
@@ -2422,15 +2422,15 @@ class GoogleSheetHandler:
Initialisiert den Handler, stellt die Verbindung her und laedt die Daten.
"""
# Holen Sie eine Logger-Instanz fuer diese Klasse
- self.logger = logging.getLogger(__name__ + ".GoogleSheetHandler")
+ self.logger = logging.getLogger(__name__ + ".GoogleSheetHandler") # <<< HINZUGEFÜGT
# 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 = [] # <<< DIESE ZEILE HINZUFÜGEN
+ self.sheet_values = []
# header_rows sind fix, aber wir koennen sie hier zur Klarheit definieren
- self._header_rows = 5 # <<< DIESE ZEILE HINZUFÜGEN (Annahme: Die ersten 5 Zeilen sind Header)
+ self._header_rows = 5 # Annahme: Die ersten 5 Zeilen sind Header
- self.logger.info("Initialisiere GoogleSheetHandler...")
+ self.logger.info("Initialisiere GoogleSheetHandler...") # <<< GEÄNDERT
try:
# Verbindung wird bei der Initialisierung aufgebaut
self._connect()
@@ -2439,7 +2439,7 @@ class GoogleSheetHandler:
self.load_data() # Erste Datenladung nach erfolgreicher Verbindung
else:
# Wenn die Verbindung fehlschlug (sheet ist None), aber keine Exception geworfen wurde (sollte nicht passieren)
- self.logger.critical(
+ self.logger.critical( # <<< GEÄNDERT
"GoogleSheetHandler Init FEHLER: Verbindung konnte nicht hergestellt werden (sheet ist None)."
)
raise ConnectionError(
@@ -2447,17 +2447,17 @@ class GoogleSheetHandler:
)
except Exception as e:
# Fehler bei der Initialisierung (entweder von _connect oder load_data nach Retries)
- self.logger.critical(
+ self.logger.critical( # <<< GEÄNDERT
f"FATAL: Fehler bei Initialisierung von GoogleSheetHandler: {type(e).__name__} - {e}"
)
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
raise ConnectionError(f"Google Sheet Handler Init failed: {e}")
@retry_on_failure
def _connect(self):
"""Stellt Verbindung zum Google Sheet her."""
self.sheet = None
- self.logger.info("Versuche Verbindung mit Google Sheets herstellen...")
+ self.logger.info("Versuche Verbindung mit Google Sheets herstellen...") # <<< GEÄNDERT
try:
if not os.path.exists(CREDENTIALS_FILE):
raise FileNotFoundError(f"Credential-Datei nicht gefunden: {CREDENTIALS_FILE}")
@@ -2467,12 +2467,12 @@ class GoogleSheetHandler:
gc = gspread.authorize(creds)
sh = gc.open_by_url(Config.SHEET_URL)
self.sheet = sh.sheet1
- self.logger.info("Verbindung zu Google Sheets erfolgreich.")
+ self.logger.info("Verbindung zu Google Sheets erfolgreich.") # <<< GEÄNDERT
except (gspread.exceptions.APIError, requests.exceptions.RequestException, FileNotFoundError) as e:
raise e
except Exception as e:
- self.logger.error(f"FEHLER bei der Google Sheets Verbindung: {type(e).__name__} - {e}")
- self.logger.debug(traceback.format_exc())
+ self.logger.error(f"FEHLER bei der Google Sheets Verbindung: {type(e).__name__} - {e}") # <<< GEÄNDERT
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
raise e
@retry_on_failure
@@ -2481,16 +2481,16 @@ class GoogleSheetHandler:
Laedt alle Daten aus dem Sheet und aktualisiert self.sheet_values.
"""
if not self.sheet:
- self.logger.error("Fehler: Keine Sheet-Verbindung zum Laden der Daten.")
+ self.logger.error("Fehler: Keine Sheet-Verbindung zum Laden der Daten.") # <<< GEÄNDERT
self.sheet_values = []
return False
- self.logger.info("Lade Daten aus Google Sheet...")
+ self.logger.info("Lade Daten aus Google Sheet...") # <<< GEÄNDERT
try:
self.sheet_values = self.sheet.get_all_values()
if not self.sheet_values:
- self.logger.warning(
+ self.logger.warning( # <<< GEÄNDERT
"Google Sheet scheint leer zu sein oder get_all_values() lieferte keine Daten."
)
self.headers = []
@@ -2498,21 +2498,21 @@ class GoogleSheetHandler:
num_rows = len(self.sheet_values)
num_cols = len(self.sheet_values[0]) if num_rows > 0 else 0
- self.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.") # <<< GEÄNDERT
try:
max_col_idx_in_map = max(COLUMN_MAP.values())
if num_cols <= max_col_idx_in_map:
- self.logger.warning(
+ self.logger.warning( # <<< GEÄNDERT
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(
+ self.logger.warning( # <<< GEÄNDERT
"COLUMN_MAP scheint leer zu sein oder enthaelt keine Werte. Kann Spaltenanzahl nicht pruefen."
)
except Exception as e:
- self.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}") # <<< GEÄNDERT
if num_rows > 0:
self.headers = self.sheet_values[0]
@@ -2523,10 +2523,10 @@ class GoogleSheetHandler:
except (gspread.exceptions.APIError, requests.exceptions.RequestException) as e:
raise e
except Exception as e:
- self.logger.error(
+ self.logger.error( # <<< GEÄNDERT
f"Allgemeiner Fehler beim Laden der Google Sheet Daten: {type(e).__name__} - {e}"
)
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
raise e
def get_data(self):
@@ -2535,7 +2535,7 @@ class GoogleSheetHandler:
(ohne die ersten N Header-Zeilen).
"""
if not self.sheet_values or len(self.sheet_values) <= self._header_rows:
- self.logger.debug(
+ self.logger.debug( # <<< GEÄNDERT
f"get_data: Keine Datenzeilen verfuegbar "
f"(geladen: {len(self.sheet_values) if self.sheet_values else 0} Zeilen, "
f"{self._header_rows} Header)."
@@ -2546,7 +2546,7 @@ class GoogleSheetHandler:
def get_all_data_with_headers(self):
"""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.")
+ self.logger.debug("get_all_data_with_headers: Keine Daten im Handler gespeichert.") # <<< GEÄNDERT
return []
return self.sheet_values.copy()
@@ -2556,7 +2556,7 @@ class GoogleSheetHandler:
Google Sheets Spaltenbuchstaben (A, B, ..., Z, AA, ...).
"""
if not isinstance(col_idx_1_based, int) or col_idx_1_based < 1:
- self.logger.error(
+ self.logger.error( # <<< GEÄNDERT
f"Ungueltiger Spaltenindex ({col_idx_1_based}) fuer _get_col_letter erhalten."
)
return None
@@ -2587,38 +2587,38 @@ class GoogleSheetHandler:
Gibt die Laenge der Datenliste zurueck, wenn keine leere Zelle im Suchbereich gefunden wurde.
"""
if not self.load_data():
- self.logger.error("Fehler beim Laden der Daten fuer get_start_row_index.")
+ self.logger.error("Fehler beim Laden der Daten fuer get_start_row_index.") # <<< GEÄNDERT
return -1
data_rows = self.get_data()
if not data_rows:
- self.logger.info("Keine Datenzeilen im Sheet gefunden. Startindex fuer leere Zelle ist 0.")
+ self.logger.info("Keine Datenzeilen im Sheet gefunden. Startindex fuer leere Zelle ist 0.") # <<< GEÄNDERT
return 0
check_column_index = COLUMN_MAP.get(check_column_key)
if check_column_index is None:
- self.logger.critical(
+ self.logger.critical( # <<< GEÄNDERT
f"FEHLER: Schluessel '{check_column_key}' nicht in COLUMN_MAP gefunden fuer get_start_row_index!"
)
return -1
actual_col_letter = self._get_col_letter(check_column_index + 1)
if actual_col_letter is None:
- self.logger.error(
+ self.logger.error( # <<< GEÄNDERT
f"FEHLER: Konnte Spaltenbuchstaben fuer Index {check_column_index + 1} nicht ermitteln."
)
actual_col_letter = f"Index_{check_column_index + 1}"
search_start_index_in_data = max(0, (min_sheet_row - 1) - self._header_rows)
- self.logger.info(
+ self.logger.info( # <<< GEÄNDERT
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})..."
)
if search_start_index_in_data >= len(data_rows):
- self.logger.warning(
+ self.logger.warning( # <<< GEÄNDERT
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)
@@ -2638,21 +2638,21 @@ class GoogleSheetHandler:
log_debug = (i < search_start_index_in_data + 5) or (i % 1000 == 0) or is_exactly_empty
if log_debug:
- self.logger.debug(
+ self.logger.debug( # <<< GEÄNDERT
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}"
)
if is_exactly_empty:
- self.logger.info(
+ self.logger.info( # <<< GEÄNDERT
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
last_data_index = len(data_rows)
- self.logger.info(
+ self.logger.info( # <<< GEÄNDERT
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}."
)
@@ -2673,7 +2673,7 @@ class GoogleSheetHandler:
bool: True bei Erfolg (nach allen Retries), False bei endgueltigem Fehler.
"""
if not self.sheet:
- self.logger.error("FEHLER: Keine Sheet-Verbindung fuer Batch-Update.")
+ self.logger.error("FEHLER: Keine Sheet-Verbindung fuer Batch-Update.") # <<< GEÄNDERT
return False
if not update_data:
@@ -2683,14 +2683,14 @@ class GoogleSheetHandler:
total_cells_to_update = sum(
len(row) for item in update_data for row in item.get('values', [])
)
- self.logger.debug(
+ self.logger.debug( # <<< GEÄNDERT
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')
return True
except Exception:
- self.logger.error(
+ self.logger.error( # <<< GEÄNDERT
f"Endgueltiger Fehler beim Batch-Update nach Retries. Kann {len(update_data)} Operationen nicht durchfuehren."
)
return False
@@ -2717,8 +2717,8 @@ class WikipediaScraper:
Defaults to Config.USER_AGENT.
"""
# Erhalten Sie eine Logger-Instanz fuer diese Klasse
- self.logger = logging.getLogger(__name__ + ".WikipediaScraper")
- self.logger.debug("WikipediaScraper initialisiert.")
+ self.logger = logging.getLogger(__name__ + ".WikipediaScraper") # <<< HINZUGEFÜGT
+ self.logger.debug("WikipediaScraper initialisiert.") # <<< GEÄNDERT
# User-Agent fuer Requests (nutzt Config, Fallback wenn nicht gesetzt)
self.user_agent = user_agent or getattr(
@@ -2727,7 +2727,7 @@ class WikipediaScraper:
)
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.")
+ self.logger.debug(f"Requests Session mit User-Agent '{self.user_agent}' initialisiert.") # <<< GEÄNDERT
# Keywords fuer die Infobox-Extraktion
self.keywords_map = {
@@ -2741,11 +2741,11 @@ class WikipediaScraper:
wiki_lang = getattr(Config, 'LANG', 'de')
wikipedia.set_lang(wiki_lang)
wikipedia.set_rate_limiting(True, min_wait=0.1)
- self.logger.info(
+ self.logger.info( # <<< GEÄNDERT
f"Wikipedia library language set to '{wiki_lang}'. Rate limiting enabled (min_wait=0.1)."
)
except Exception as e:
- self.logger.warning(f"Fehler beim Setzen der Wikipedia-Sprache oder Rate Limiting: {e}")
+ self.logger.warning(f"Fehler beim Setzen der Wikipedia-Sprache oder Rate Limiting: {e}") # <<< GEÄNDERT
def _get_full_domain(self, website):
"""Extrahiert die normalisierte Domain (ohne www, ohne Pfad) aus einer URL."""
@@ -2778,7 +2778,7 @@ class WikipediaScraper:
terms.add(full_domain)
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}")
+ self.logger.debug(f"Generierte Suchbegriffe fuer '{company_name[:100]}...': {final_terms}") # <<< GEÄNDERT
return final_terms
@retry_on_failure
@@ -2787,18 +2787,18 @@ class WikipediaScraper:
Holt HTML von einer URL (requests) und gibt ein BeautifulSoup-Objekt zurueck.
"""
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]}...'.")
+ self.logger.warning(f"_get_page_soup: Ungueltige URL '{url[:100]}...'.") # <<< GEÄNDERT
return None
try:
- self.logger.debug(f"_get_page_soup: Rufe URL ab: {url[:100]}...")
+ self.logger.debug(f"_get_page_soup: Rufe URL ab: {url[:100]}...") # <<< GEÄNDERT
response = self.session.get(url, timeout=getattr(Config, 'REQUEST_TIMEOUT', 15))
response.raise_for_status()
response.encoding = response.apparent_encoding
soup = BeautifulSoup(response.text, getattr(Config, 'HTML_PARSER', 'html.parser'))
- self.logger.debug(f"_get_page_soup: Parsen von {url[:100]}... erfolgreich.")
+ self.logger.debug(f"_get_page_soup: Parsen von {url[:100]}... erfolgreich.") # <<< GEÄNDERT
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}")
+ self.logger.error(f"_get_page_soup: Fehler beim Abrufen oder Parsen von HTML von {url[:100]}...: {type(e).__name__} - {e}") # <<< GEÄNDERT
raise e
def _validate_article(self, page, company_name, website):
@@ -2808,7 +2808,7 @@ class WikipediaScraper:
"""
if not page or not company_name:
return False
- self.logger.debug(
+ self.logger.debug( # <<< GEÄNDERT
f"Validiere Artikel '{page.title[:100]}...' (URL: {page.url[:100]}...) "
f"fuer Firma '{company_name[:100]}' (Website: {website[:100]})..."
)
@@ -2816,12 +2816,12 @@ class WikipediaScraper:
normalized_company = normalize_company_name(company_name)
normalized_title = normalize_company_name(page.title)
if not normalized_company or not normalized_title:
- self.logger.warning("Validierung nicht moeglich, da Normalisierung eines Namens fehlschlug.")
+ self.logger.warning("Validierung nicht moeglich, da Normalisierung eines Namens fehlschlug.") # <<< GEÄNDERT
return False
standard_threshold = getattr(Config, 'SIMILARITY_THRESHOLD', 0.65)
similarity = fuzzy_similarity(normalized_title, normalized_company)
- self.logger.debug(f" -> Gesamt-Aehnlichkeit (normalized): {similarity:.2f}")
+ self.logger.debug(f" -> Gesamt-Aehnlichkeit (normalized): {similarity:.2f}") # <<< GEÄNDERT
company_tokens = normalized_company.split()
title_tokens = normalized_title.split()
@@ -2835,7 +2835,7 @@ class WikipediaScraper:
domain_found = False
full_domain = self._get_full_domain(website)
if full_domain != "k.A.":
- self.logger.debug(f" -> Suche nach Domain '{full_domain}' in externen Links des Artikels...")
+ self.logger.debug(f" -> Suche nach Domain '{full_domain}' in externen Links des Artikels...") # <<< GEÄNDERT
try:
article_html = page.html()
if article_html:
@@ -2850,7 +2850,7 @@ class WikipediaScraper:
if relevant_links:
domain_found = True
except Exception as e_link_check:
- self.logger.error(
+ self.logger.error( # <<< GEÄNDERT
f"Fehler waehrend der Domain-Link-Pruefung fuer '{page.title[:100]}...': "
f"{type(e_link_check).__name__} - {e_link_check}"
)
@@ -2878,7 +2878,7 @@ class WikipediaScraper:
reason = f"Erstes normalisiertes Wort stimmt ueberein UND Aehnlichkeit >= 0.55 (Sim={similarity:.2f})"
log_level = logging.INFO if is_valid else logging.DEBUG
- self.logger.log(
+ self.logger.log( # <<< GEÄNDERT
log_level,
f" => Artikel '{page.title[:100]}...' "
f"{'VALIDIERT' if is_valid else 'NICHT validiert'} "
@@ -2922,10 +2922,10 @@ class WikipediaScraper:
break
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.") # <<< GEÄNDERT
except Exception as e:
- self.logger.error(f"Fehler beim Extrahieren des ersten Absatzes: {type(e).__name__} - {e}")
- self.logger.debug(traceback.format_exc())
+ self.logger.error(f"Fehler beim Extrahieren des ersten Absatzes: {type(e).__name__} - {e}") # <<< GEÄNDERT
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
return paragraph_text
def extract_categories(self, soup):
@@ -2945,14 +2945,14 @@ class WikipediaScraper:
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}")
+ self.logger.debug(f"Kategorien gefunden: {cats_filtered}") # <<< GEÄNDERT
else:
- self.logger.debug("Kein 'ul' Tag in 'mw-normal-catlinks' gefunden.")
+ self.logger.debug("Kein 'ul' Tag in 'mw-normal-catlinks' gefunden.") # <<< GEÄNDERT
else:
- self.logger.debug("Kein 'div#mw-normal-catlinks' gefunden.")
+ self.logger.debug("Kein 'div#mw-normal-catlinks' gefunden.") # <<< GEÄNDERT
except Exception as e:
- self.logger.error(f"Fehler beim Extrahieren der Kategorien: {type(e).__name__} - {e}")
- self.logger.debug(traceback.format_exc())
+ self.logger.error(f"Fehler beim Extrahieren der Kategorien: {type(e).__name__} - {e}") # <<< GEÄNDERT
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
return ", ".join(cats_filtered) if cats_filtered else "k.A."
def _extract_infobox_value(self, soup, target):
@@ -2960,20 +2960,20 @@ class WikipediaScraper:
Extrahiert gezielt Branche, Umsatz oder Mitarbeiter aus der Infobox
eines Wikipedia-Artikels Soup-Objekts.
"""
- self.logger.debug(f"--- Entering _extract_infobox_value for target '{target}' ---")
+ self.logger.debug(f"--- Entering _extract_infobox_value for target '{target}' ---") # <<< GEÄNDERT
if not soup or target not in self.keywords_map:
- self.logger.debug(
+ self.logger.debug( # <<< GEÄNDERT
f"_extract_infobox_value: Ungueltiger Input (Soup: {soup is not None}, Target: {target})"
)
return "k.A."
keywords = self.keywords_map[target]
- self.logger.debug(f"_extract_infobox_value: Suche nach '{target}' mit Keywords: {keywords}")
+ self.logger.debug(f"_extract_infobox_value: Suche nach '{target}' mit Keywords: {keywords}") # <<< GEÄNDERT
infobox = soup.select_one('table[class*="infobox"]')
if not infobox:
- self.logger.debug(" -> KEINE Infobox via select_one 'table[class*=\"infobox\"]' gefunden.")
+ self.logger.debug(" -> KEINE Infobox via select_one 'table[class*=\"infobox\"]' gefunden.") # <<< GEÄNDERT
return "k.A."
- self.logger.debug(" -> Infobox gefunden.")
+ self.logger.debug(" -> Infobox gefunden.") # <<< GEÄNDERT
value_found = "k.A."
try:
@@ -3019,29 +3019,29 @@ class WikipediaScraper:
clean_val = re.sub(r'\s*\([^)]*\)', '', clean_val).strip()
clean_val = clean_val.split('\n')[0].strip()
value_found = clean_val if clean_val else "k.A."
- self.logger.info(f" --> Branche extrahiert: '{value_found}'")
+ self.logger.info(f" --> Branche extrahiert: '{value_found}'") # <<< GEÄNDERT
elif target == 'umsatz':
numeric_val_str = extract_numeric_value(raw_value_text, is_umsatz=True)
value_found = numeric_val_str
- self.logger.info(
+ self.logger.info( # <<< GEÄNDERT
f" --> Umsatz extrahiert (aus '{raw_value_text[:50]}...'): '{value_found}'"
)
elif target == 'mitarbeiter':
numeric_val_str = extract_numeric_value(raw_value_text, is_umsatz=False)
value_found = numeric_val_str
- self.logger.info(
+ self.logger.info( # <<< GEÄNDERT
f" --> Mitarbeiter extrahiert (aus '{raw_value_text[:50]}...'): '{value_found}'"
)
break
if value_found != "k.A.":
- self.logger.debug(f" -> Finaler Wert fuer '{target}' gefunden: '{value_found}'")
+ self.logger.debug(f" -> Finaler Wert fuer '{target}' gefunden: '{value_found}'") # <<< GEÄNDERT
else:
- self.logger.debug(
+ self.logger.debug( # <<< GEÄNDERT
f" -> Kein passender Eintrag fuer '{target}' in der gesamten Infobox gefunden."
)
except Exception as e:
- self.logger.exception(
+ self.logger.exception( # <<< GEÄNDERT
f"Fehler beim Durchlaufen der Infobox-Zeilen fuer '{target}': {e}"
)
return "k.A. (Fehler Extraktion)"
@@ -3056,15 +3056,15 @@ class WikipediaScraper:
Artikel gefunden wird. Behandelt explizit Begriffsklaerungsseiten.
"""
if not company_name or str(company_name).strip() == "":
- self.logger.warning("Wikipedia search skipped: No company name provided.")
+ self.logger.warning("Wikipedia search skipped: No company name provided.") # <<< GEÄNDERT
raise ValueError("Kein Firmenname fuer Wikipedia Suche angegeben.")
search_terms = self._generate_search_terms(company_name, website)
if not search_terms:
- self.logger.warning(f"Keine Suchbegriffe fuer '{company_name[:100]}...' generiert.")
+ self.logger.warning(f"Keine Suchbegriffe fuer '{company_name[:100]}...' generiert.") # <<< GEÄNDERT
return None
- self.logger.info(
+ self.logger.info( # <<< GEÄNDERT
f"Starte Wikipedia-Suche fuer '{company_name[:100]}...' "
f"(Website: {website[:100]}...) mit Begriffen: {search_terms}"
)
@@ -3074,20 +3074,20 @@ class WikipediaScraper:
if title_to_check in processed_titles:
return None
processed_titles.add(title_to_check)
- self.logger.debug(f" -> Pruefe potenziellen Artikel: '{title_to_check[:100]}...'")
+ self.logger.debug(f" -> Pruefe potenziellen Artikel: '{title_to_check[:100]}...'") # <<< GEÄNDERT
try:
page = wikipedia.page(title_to_check, auto_suggest=False, preload=True)
if self._validate_article(page, company_name, website):
- self.logger.info(f" -> Titel '{page.title[:100]}...' erfolgreich validiert!")
+ self.logger.info(f" -> Titel '{page.title[:100]}...' erfolgreich validiert!") # <<< GEÄNDERT
return page
else:
- self.logger.debug(f" -> Titel '{title_to_check[:100]}...' nicht validiert.")
+ self.logger.debug(f" -> Titel '{title_to_check[:100]}...' nicht validiert.") # <<< GEÄNDERT
return None
except wikipedia.exceptions.PageError:
- self.logger.debug(f" -> Seite '{title_to_check[:100]}...' nicht gefunden (PageError).")
+ self.logger.debug(f" -> Seite '{title_to_check[:100]}...' nicht gefunden (PageError).") # <<< GEÄNDERT
return None
except wikipedia.exceptions.DisambiguationError as e_inner:
- self.logger.info(
+ self.logger.info( # <<< GEÄNDERT
f" -> Begriffsklaerung '{title_to_check[:100]}...' gefunden. "
f"Pruefe Optionen: {str(e_inner.options)[:100]}..."
)
@@ -3100,56 +3100,56 @@ class WikipediaScraper:
continue
validated_option_page = check_page(option)
if validated_option_page:
- self.logger.info(
+ self.logger.info( # <<< GEÄNDERT
f" -> Option '{option[:100]}...' aus Begriffsklaerung erfolgreich validiert!"
)
return validated_option_page
- self.logger.debug(
+ self.logger.debug( # <<< GEÄNDERT
f" -> Keine passende/validierte Unternehmens-Option in Begriffsklaerung '{title_to_check[:100]}...' gefunden."
)
return None
except (requests.exceptions.RequestException, wikipedia.exceptions.WikipediaException) as e_req:
- self.logger.warning(
+ self.logger.warning( # <<< GEÄNDERT
f" -> Netzwerk/API-Fehler beim Laden/Validieren von '{title_to_check[:100]}...': "
f"{type(e_req).__name__} - {e_req}. Ueberspringe diesen Titel."
)
return None
except Exception as e_page:
- self.logger.error(
+ self.logger.error( # <<< GEÄNDERT
f" -> Unerwarteter Fehler bei Verarbeitung von Titel '{title_to_check[:100]}...': "
f"{type(e_page).__name__} - {e_page}"
)
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
return None
- self.logger.debug(f" -> Versuche direkten Match fuer '{company_name[:100]}...'...")
+ self.logger.debug(f" -> Versuche direkten Match fuer '{company_name[:100]}...'...") # <<< GEÄNDERT
validated_page = check_page(company_name)
if validated_page:
return validated_page
- self.logger.debug(
+ self.logger.debug( # <<< GEÄNDERT
f" -> Kein direkter Treffer/validiert. Starte Suche mit generierten Begriffen: {search_terms}"
)
for term in search_terms:
try:
- self.logger.debug(f" -> Suche mit Begriff: '{term[:100]}...'...")
+ self.logger.debug(f" -> Suche mit Begriff: '{term[:100]}...'...") # <<< GEÄNDERT
search_results = wikipedia.search(term, results=getattr(Config, 'WIKIPEDIA_SEARCH_RESULTS', 5))
- self.logger.debug(f" -> Suchergebnisse fuer '{term[:100]}...': {search_results}")
+ self.logger.debug(f" -> Suchergebnisse fuer '{term[:100]}...': {search_results}") # <<< GEÄNDERT
if not search_results:
- self.logger.debug(f" -> Keine Suchergebnisse fuer '{term[:100]}...'.")
+ self.logger.debug(f" -> Keine Suchergebnisse fuer '{term[:100]}...'.") # <<< GEÄNDERT
continue
for title in search_results:
validated_page = check_page(title)
if validated_page:
return validated_page
except Exception as e_search:
- self.logger.error(
+ self.logger.error( # <<< GEÄNDERT
f"Fehler waehrend Wikipedia-Suche fuer '{term[:100]}...': "
f"{type(e_search).__name__} - {e_search}"
)
raise e_search
- self.logger.warning(
+ self.logger.warning( # <<< GEÄNDERT
f"Kein passender & validierter Wikipedia-Artikel fuer '{company_name[:100]}...' gefunden nach Pruefung aller Begriffe und Optionen."
)
return None
@@ -3170,30 +3170,30 @@ class WikipediaScraper:
}
if not page_url or not isinstance(page_url, str) or "wikipedia.org/wiki/" not in page_url.lower():
- self.logger.warning(
+ self.logger.warning( # <<< GEÄNDERT
f"extract_company_data: Ungueltige oder keine Wikipedia-URL '{page_url[:100]}...'."
)
return default_result
- self.logger.info(f"Extrahiere Daten fuer Wiki-URL: {page_url[:100]}...")
+ self.logger.info(f"Extrahiere Daten fuer Wiki-URL: {page_url[:100]}...") # <<< GEÄNDERT
soup = self._get_page_soup(page_url)
if not soup:
- self.logger.error(f" -> Fehler: Konnte Seite {page_url[:100]}... nicht laden oder parsen.")
+ self.logger.error(f" -> Fehler: Konnte Seite {page_url[:100]}... nicht laden oder parsen.") # <<< GEÄNDERT
return default_result
- self.logger.debug(" -> Extrahiere erster Absatz...")
+ self.logger.debug(" -> Extrahiere erster Absatz...") # <<< GEÄNDERT
first_paragraph = self._extract_first_paragraph_from_soup(soup)
- self.logger.debug(" -> Extrahiere Kategorien...")
+ self.logger.debug(" -> Extrahiere Kategorien...") # <<< GEÄNDERT
categories_val = self.extract_categories(soup)
- self.logger.debug(" -> Extrahiere Branche aus Infobox...")
+ self.logger.debug(" -> Extrahiere Branche aus Infobox...") # <<< GEÄNDERT
branche_val = self._extract_infobox_value(soup, 'branche')
- self.logger.debug(" -> Extrahiere Umsatz aus Infobox...")
+ self.logger.debug(" -> Extrahiere Umsatz aus Infobox...") # <<< GEÄNDERT
umsatz_val = self._extract_infobox_value(soup, 'umsatz')
- self.logger.debug(" -> Extrahiere Mitarbeiter aus Infobox...")
+ self.logger.debug(" -> Extrahiere Mitarbeiter aus Infobox...") # <<< GEÄNDERT
mitarbeiter_val = self._extract_infobox_value(soup, 'mitarbeiter')
result = {
@@ -3205,7 +3205,7 @@ class WikipediaScraper:
'categories': categories_val
}
- self.logger.info(
+ self.logger.info( # <<< GEÄNDERT
f" -> Extrahierte Daten: P='{first_paragraph[:50]}...', "
f"B='{branche_val}', U='{umsatz_val}', M='{mitarbeiter_val}', "
f"C='{categories_val[:50]}...'"
@@ -3238,17 +3238,17 @@ class DataProcessor:
# (z.B. OpenAIHandler, SerpAPIHandler), falls diese als eigene Klassen ausgelagert werden.
"""
# Erhalten Sie eine Logger-Instanz fuer diese Klasse
- self.logger = logging.getLogger(__name__ + ".DataProcessor")
- self.logger.info("Initialisiere DataProcessor...")
+ self.logger = logging.getLogger(__name__ + ".DataProcessor") # <<< HINZUGEFÜGT
+ self.logger.info("Initialisiere DataProcessor...") # <<< GEÄNDERT
# Ueberpruefen Sie, ob gueltige Handler-Instanzen uebergeben wurden
if not isinstance(sheet_handler, GoogleSheetHandler):
# Logge einen kritischen Fehler und werfe eine Exception
- self.logger.critical("DataProcessor Init FEHLER: Kein gueltiger GoogleSheetHandler uebergeben!")
+ self.logger.critical("DataProcessor Init FEHLER: Kein gueltiger GoogleSheetHandler uebergeben!") # <<< GEÄNDERT
raise ValueError("DataProcessor benoetigt eine gueltige GoogleSheetHandler Instanz.")
if not isinstance(wiki_scraper, WikipediaScraper):
# Logge einen kritischen Fehler und werfe eine Exception
- self.logger.critical("DataProcessor Init FEHLER: Kein gueltiger WikipediaScraper uebergeben!")
+ self.logger.critical("DataProcessor Init FEHLER: Kein gueltiger WikipediaScraper uebergeben!") # <<< GEÄNDERT
raise ValueError("DataProcessor benoetigt eine gueltige WikipediaScraper Instanz.")
# Speichern Sie die Handler-Instanzen als Attribute der Instanz
@@ -3263,7 +3263,7 @@ class DataProcessor:
self.imputer = None
self._expected_features = None # Liste der erwarteten Feature-Spalten fuer Vorhersage
- self.logger.info("DataProcessor initialisiert mit Handlern.")
+ self.logger.info("DataProcessor initialisiert mit Handlern.") # <<< GEÄNDERT
# Definieren Sie hier (oder als Klassenattribut) die Zuordnung von Schritt-Typen
# zu den relevanten Spaltenschluesseln fuer die Statuspruefung.
@@ -3301,7 +3301,7 @@ class DataProcessor:
# Pruefen Sie, ob der Schluessel in COLUMN_MAP gefunden wurde
if idx is None:
# Logge einen Fehler, aber gebe einen leeren String zurueck
- self.logger.error(f"_get_cell_value_safe: Schluessel '{column_key}' nicht in COLUMN_MAP gefunden.")
+ self.logger.error(f"_get_cell_value_safe: Schluessel '{column_key}' nicht in COLUMN_MAP gefunden.") # <<< GEÄNDERT
return '' # Gebe leeren String zurueck, wenn Schluessel fehlt
# Pruefen Sie, ob die Zeile lang genug ist, um auf diesen Index zuzugreifen
@@ -3310,7 +3310,7 @@ class DataProcessor:
return row[idx] if row[idx] is not None else ''
else:
# Logge auf Debug-Level, wenn der Index existiert, aber die Zeile zu kurz ist.
- self.logger.debug(
+ self.logger.debug( # <<< GEÄNDERT
f"_get_cell_value_safe: Index {idx} fuer '{column_key}' ist gueltig, "
f"aber Zeile ist zu kurz (Laenge {len(row)}). Gebe leeren String zurueck."
)
@@ -3531,7 +3531,7 @@ class DataProcessor:
# else: self.logger.debug(" -> ML-Schaetzung nicht noetig (AV/AW fehlen)") # Zu viel Laerm im Debug
- # Wenn AU gesetzt ist oder ein gueltiger Wert enthaelt, und kein Re-Eval erzwungen wird, ist der Schritt nicht noetig
+ # Wenn AU gesetzt ist oder einen gueltigen Wert enthaelt, und kein Re-Eval erzwungen wird, ist der Schritt nicht noetig
# self.logger.debug(f" -> ML-Schaetzung nicht noetig (AU='{au_value}')") # Zu viel Laerm im Debug
return False
@@ -3568,7 +3568,7 @@ class DataProcessor:
Defaults to False.
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
- self.logger.info(f"--- Starte Verarbeitung fuer Zeile {row_num_in_sheet} {'(Re-Eval)' if force_reeval else ''} (Schritte: {', '.join(steps_to_run) if steps_to_run else 'Keine ausgewählt'}) ---")
+ self.logger.info(f"--- Starte Verarbeitung fuer Zeile {row_num_in_sheet} {'(Re-Eval)' if force_reeval else ''} (Schritte: {', '.join(steps_to_run) if steps_to_run else 'Keine ausgewählt'}) ---") # <<< GEÄNDERT
# Liste zur Sammlung von Sheet-Updates fuer diese Zeile
# Updates sind Dictionaries: {'range': 'A1', 'values': [['Wert']]}
@@ -3633,12 +3633,6 @@ class DataProcessor:
# --- Die Logik fuer die einzelnen Verarbeitungsschritte folgt in den naechsten Bloecken ---
# Jeder Schritt prueft, ob er in steps_to_run enthalten ist UND (ob er laut Status noetig ist ODER force_reeval True ist).
- # Website Handling (Block 20) folgt...
- # Wikipedia Handling (Block 21) folgt...
- # ChatGPT Evaluationen (Block 22) folgt...
- # ML Prediction (Block 23) folgt...
- # Finalisierung & Write (Block 23) folgt...
-
# ======================================================================
# === Verarbeitungsschritte innerhalb von _process_single_row ==========
# ======================================================================
@@ -3666,12 +3660,12 @@ class DataProcessor:
if not self._get_cell_value_safe(row_data, "Website Scrape Timestamp").strip(): grund_message_parts.append('AT leer')
grund_message = ", ".join(grund_message_parts)
- self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre WEBSITE Schritte aus (Grund: {grund_message})...")
+ self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre WEBSITE Schritte aus (Grund: {grund_message})...") # <<< GEÄNDERT
# Website Lookup nur, wenn die URL in Spalte D (CRM Website) leer oder "k.A." ist
# Nutzt die lokal gespeicherte Kopie der URL, die ggf. im Lookup ueberschrieben wird.
if not website_url or website_url.lower() == "k.a.":
- self.logger.debug(" -> Website URL (D) leer oder k.A., suche ueber SERP...")
+ self.logger.debug(" -> Website URL (D) leer oder k.A., suche ueber SERP...") # <<< GEÄNDERT
# Annahme: serp_website_lookup global definiert (Block 10) und nutzt logging/retry
try:
# Der serp_website_lookup Aufruf ist mit retry_on_failure dekoriert.
@@ -3683,22 +3677,22 @@ class DataProcessor:
website_url = new_website
# Fuegen Sie das Update fuer Spalte D zur Liste der Sheet-Updates hinzu
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["CRM Website"] + 1)}{row_num_in_sheet}', 'values': [[website_url]]})
- self.logger.info(f" -> Neue Website gefunden und fuer Update D:{row_num_in_sheet} vorgemerkt: {website_url[:100]}...") # Gekuerzt loggen
+ self.logger.info(f" -> Neue Website gefunden und fuer Update D:{row_num_in_sheet} vorgemerkt: {website_url[:100]}...") # <<< GEÄNDERT
else:
# Wenn keine neue Website gefunden wurde
- self.logger.warning(f" -> Keine neue Website ueber SERP gefunden fuer '{company_name[:100]}...'.") # Gekuerzt loggen
+ self.logger.warning(f" -> Keine neue Website ueber SERP gefunden fuer '{company_name[:100]}...'.") # <<< GEÄNDERT
# website_url bleibt leer oder k.A. in diesem Fall.
except Exception as e_serp_lookup:
# Wenn serp_website_lookup eine Exception wirft (nach Retries)
- self.logger.error(f"FEHLER bei SERP Website Lookup fuer '{company_name[:100]}...': {e_serp_lookup}") # Gekuerzt loggen
+ self.logger.error(f"FEHLER bei SERP Website Lookup fuer '{company_name[:100]}...': {e_serp_lookup}") # <<< GEÄNDERT
# Bei Fehler bleibt website_url unveraendert (leer oder k.A.). Fahren Sie fort.
pass # Fahren Sie fort, falls eine URL im Sheet war oder gefunden wurde
# Führen Sie Scraping und Zusammenfassung nur durch, wenn eine gueltige Website URL vorhanden ist (lokale Variable website_url)
# Ueberpruefen Sie auf nicht-leere website_url und ungleich "k.A." oder Fehlerwerten.
if website_url and website_url.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"]:
- self.logger.debug(f" -> Scrape Rohtext von {website_url[:100]}...") # Gekuerzt loggen
+ self.logger.debug(f" -> Scrape Rohtext von {website_url[:100]}...") # <<< GEÄNDERT
# Annahme: get_website_raw global definiert (Block 11) und nutzt logging/retry
try:
# Der get_website_raw Aufruf ist mit retry_on_failure dekoriert.
@@ -3709,7 +3703,7 @@ class DataProcessor:
# Zusammenfassung nur, wenn gueltiger Rohtext extrahiert wurde.
# Pruefen Sie auf nicht-leeren raw_text und ungleich Standard-Fehlerwerten.
if website_raw and str(website_raw).strip().lower() not in ["k.a.", "k.a. (nur cookie-banner erkannt)", "k.a. (fehler)"]:
- self.logger.debug(f" -> Fasse Rohtext zusammen (Laenge: {len(str(website_raw))})...")
+ self.logger.debug(f" -> Fasse Rohtext zusammen (Laenge: {len(str(website_raw))})...") # <<< GEÄNDERT
# Annahme: summarize_website_content global definiert (Block 9) und nutzt logging/retry
try:
# Der summarize_website_content Aufruf ist mit retry_on_failure dekoriert.
@@ -3722,16 +3716,16 @@ class DataProcessor:
except Exception as e_summary:
# Wenn summarize_website_content eine Exception wirft (nach Retries)
- self.logger.error(f"FEHLER bei Website Zusammenfassung fuer '{company_name[:100]}...': {e_summary}") # Gekuerzt loggen
+ self.logger.error(f"FEHLER bei Website Zusammenfassung fuer '{company_name[:100]}...': {e_summary}") # <<< GEÄNDERT
# Setze die lokale Variable auf einen Fehlerwert
- website_summary = f"k.A. (Fehler Zusammenfassung: {str(e)[:100]}...)"
+ website_summary = f"k.A. (Fehler Zusammenfassung: {str(e_summary)[:100]}...)" # Korrektur: e statt e_summary
# Fuegen Sie ein Update mit dem Fehlerwert fuer Spalte AS hinzu
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [[website_summary]]})
pass # Fahren Sie fort
else:
# Wenn kein gueltiger Rohtext zum Zusammenfassen vorhanden war
- self.logger.debug(" -> Kein gueltiger Rohtext zum Zusammenfassen vorhanden. Zusammenfassung uebersprungen.")
+ self.logger.debug(" -> Kein gueltiger Rohtext zum Zusammenfassen vorhanden. Zusammenfassung uebersprungen.") # <<< GEÄNDERT
# Stellen Sie sicher, dass die lokale Variable korrekt gesetzt ist, falls nicht zusammengefasst
website_summary = "k.A."
# Fuege 'k.A.' Update fuer AS hinzu (nur wenn es vorher nicht k.A. war?)
@@ -3744,9 +3738,9 @@ class DataProcessor:
except Exception as e_scrape:
# Wenn get_website_raw eine Exception wirft (nach Retries)
- self.logger.error(f"FEHLER beim Website Scraping fuer '{company_name[:100]}' ({website_url[:100]}...): {e_scrape}") # Gekuerzt loggen
+ self.logger.error(f"FEHLER beim Website Scraping fuer '{company_name[:100]}' ({website_url[:100]}...): {e_scrape}") # <<< GEÄNDERT
# Setze die lokalen Variablen auf Fehlerwerte
- website_raw = f"k.A. (Fehler Scraping: {str(e)[:100]}...)"
+ website_raw = f"k.A. (Fehler Scraping: {str(e_scrape)[:100]}...)" # Korrektur: e statt e_scrape
website_summary = "k.A. (Fehler Zusammenfassung)" # Zusammenfassung fehlschlaegt auch
# Fuegen Sie Updates mit Fehlerwerten fuer AR und AS hinzu
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [[website_raw]]})
@@ -3755,7 +3749,7 @@ class DataProcessor:
else:
# Wenn keine gueltige Website URL vorhanden/gefunden wurde
- self.logger.debug(f" -> Keine gueltige Website URL vorhanden/gefunden fuer '{company_name[:100]}...'. Website Verarbeitung uebersprungen.") # Gekuerzt loggen
+ self.logger.debug(f" -> Keine gueltige Website URL vorhanden/gefunden fuer '{company_name[:100]}...'. Website Verarbeitung uebersprungen.") # <<< GEÄNDERT
# Stellen Sie sicher, dass AR und AS auf k.A. gesetzt werden, wenn der Schritt lief, aber keine URL da war.
# Die lokalen Variablen behalten ihre initialen Werte (current_...) wenn der Schritt uebersprungen wurde,
# aber wenn der Schritt lief, aber keine URL da war, sollten sie auf k.A. gesetzt werden.
@@ -3809,7 +3803,7 @@ class DataProcessor:
if self._get_cell_value_safe(row_data, "Chat Wiki Konsistenzpruefung").strip().upper() == "X (URL COPIED)": grund_message_parts.append("S='X (URL Copied)'")
grund_message = ", ".join(grund_message_parts)
- self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre WIKI Suche/Extraktion aus (Grund: {grund_message})...")
+ self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre WIKI Suche/Extraktion aus (Grund: {grund_message})...") # <<< GEÄNDERT
# Holen Sie die aktuelle Wiki URL aus Spalte M (nutzt interne Helfer)
url_in_m = self._get_cell_value_safe(row_data, "Wiki URL").strip()
@@ -3832,20 +3826,20 @@ class DataProcessor:
# Bestimmen Sie, ob eine neue Suche notwendig ist
if status_s_indicates_reparse:
# Wenn Status S signalisiert, dass eine neu kopierte URL extrahiert werden soll, fuehre immer eine Suche aus.
- self.logger.warning(f" -> Status S ist 'X (URL Copied)', ignoriere URL '{url_in_m[:100]}...' in M und starte neue Suche...") # Gekuerzt loggen
+ self.logger.warning(f" -> Status S ist 'X (URL Copied)', ignoriere URL '{url_in_m[:100]}...' in M und starte neue Suche...") # <<< GEÄNDERT
search_was_needed = True # Suche ist noetig
elif force_reeval:
# Wenn Re-Eval erzwungen wird
- self.logger.debug(" -> Re-Eval Modus aktiv fuer Wiki-Schritt.")
+ self.logger.debug(" -> Re-Eval Modus aktiv fuer Wiki-Schritt.") # <<< GEÄNDERT
# Wenn die URL in M existiert und gueltig aussieht
if m_url_exists_and_looks_valid:
# Im Re-Eval Modus nehmen wir die URL aus M an, OHNE erneute Validierung oder Suche (Vertrauen auf M!).
- self.logger.info(f" -> Re-Eval: Nutze vorhandene URL aus Spalte M direkt: {url_in_m[:100]}...") # Gekuerzt loggen
+ self.logger.info(f" -> Re-Eval: Nutze vorhandene URL aus Spalte M direkt: {url_in_m[:100]}...") # <<< GEÄNDERT
url_to_extract = url_in_m # Verwende die URL aus M direkt
else:
# Wenn M leer/ungueltig ist, auch im Re-Eval Modus neu suchen
- self.logger.debug(f" -> Re-Eval: Spalte M ist leer oder ungueltig ('{url_in_m[:100]}...'). Starte neue Suche...") # Gekuerzt loggen
+ self.logger.debug(f" -> Re-Eval: Spalte M ist leer oder ungueltig ('{url_in_m[:100]}...'). Starte neue Suche...") # <<< GEÄNDERT
search_was_needed = True # Suche ist noetig
elif not an_value:
@@ -3853,7 +3847,7 @@ class DataProcessor:
# Wenn die URL in M existiert und gueltig aussieht
if m_url_exists_and_looks_valid:
# Wenn AN fehlt und M gefuellt ist, pruefen wir die Validitaet der M-URL ueber die wikipedia Bibliothek.
- self.logger.debug(f" -> AN fehlt, pruefe Validitaet der URL aus M: {url_in_m[:100]}...") # Gekuerzt loggen
+ self.logger.debug(f" -> AN fehlt, pruefe Validitaet der URL aus M: {url_in_m[:100]}...") # <<< GEÄNDERT
try:
# Extrahieren des Titels aus der URL fuer wikipedia.page (nutzt globale Helfer)
# Dieser Aufruf kann Exceptions werfen (PageError, DisambiguationError).
@@ -3871,35 +3865,35 @@ class DataProcessor:
# _validate_article kann interne Fehler haben (z.B. bei HTML Parsing), aber faengt sie.
if self.wiki_scraper._validate_article(page_from_m, company_name, website_url):
url_to_extract = page_from_m.url # Die URL ist valide und wird verwendet
- self.logger.info(f" -> Vorhandene URL aus M '{url_to_extract[:100]}...' ist valide und wird verwendet.") # Gekuerzt loggen
+ self.logger.info(f" -> Vorhandene URL aus M '{url_to_extract[:100]}...' ist valide und wird verwendet.") # <<< GEÄNDERT
else:
# Wenn der Artikel aus M nicht validiert wird
- self.logger.warning(f" -> Vorhandene URL aus M '{page_from_m.title[:100]}...' ist NICHT valide. Starte neue Suche...") # Gekuerzt loggen
+ self.logger.warning(f" -> Vorhandene URL aus M '{page_from_m.title[:100]}...' ist NICHT valide. Starte neue Suche...") # <<< GEÄNDERT
search_was_needed = True # Suche ist noetig
except (wikipedia.exceptions.PageError, wikipedia.exceptions.DisambiguationError) as e_wiki_m:
# Wenn die URL in M zu einem nicht existierenden Artikel oder einer Begriffsklaerung fuehrt
- self.logger.warning(f" -> Vorhandene URL aus M '{url_in_m[:100]}...' fuehrt zu Fehler ({type(e_wiki_m).__name__}). Starte neue Suche...") # Gekuerzt loggen
+ self.logger.warning(f" -> Vorhandene URL aus M '{url_in_m[:100]}...' fuehrt zu Fehler ({type(e_wiki_m).__name__}). Starte neue Suche...") # <<< GEÄNDERT
# Logge die Disambiguation Optionen auf Debug, falls vorhanden
if isinstance(e_wiki_m, wikipedia.exceptions.DisambiguationError):
- self.logger.debug(f" -> Disambiguation Optionen: {str(e_wiki_m.options)[:100]}...") # Gekuerzt loggen
+ self.logger.debug(f" -> Disambiguation Optionen: {str(e_wiki_m.options)[:100]}...") # <<< GEÄNDERT
search_was_needed = True # Suche ist noetig
pass # Faert fort
except Exception as e_val_m:
# Fange andere unerwartete Fehler beim Pruefen der URL aus M ab (z.B. URL-Parsing-Fehler vor wikipedia.page)
- self.logger.exception(f" -> Unerwarteter Fehler beim Pruefen der URL aus M '{url_in_m[:100]}...': {e_val_m}. Starte neue Suche...") # Gekuerzt loggen
+ self.logger.exception(f" -> Unerwarteter Fehler beim Pruefen der URL aus M '{url_in_m[:100]}...': {e_val_m}. Starte neue Suche...") # <<< GEÄNDERT
search_was_needed = True # Suche ist noetig
pass # Faert fort
else:
# M ist leer/ungueltig und AN fehlt -> Suche starten
- self.logger.debug(f" -> AN fehlt und M leer/ungueltig ('{url_in_m[:100]}...'). Starte Wikipedia-Suche fuer '{company_name[:100]}...'...") # Gekuerzt loggen
+ self.logger.debug(f" -> AN fehlt und M leer/ungueltig ('{url_in_m[:100]}...'). Starte Wikipedia-Suche fuer '{company_name[:100]}...'...") # <<< GEÄNDERT
search_was_needed = True # Suche ist noetig
# --- Führe die Suche aus, wenn search_was_needed True ist ---
if search_was_needed:
- self.logger.debug(f" -> Fuehre Wikipedia Suche ueber scraper durch...")
+ self.logger.debug(f" -> Fuehre Wikipedia Suche ueber scraper durch...") # <<< GEÄNDERT
try:
# Rufe die search_company_article Methode des Scrapers auf.
# search_company_article ist mit retry_on_failure dekoriert und wirft bei endgueltigem Fehler eine Exception.
@@ -3909,16 +3903,16 @@ class DataProcessor:
if validated_page:
# Wenn ein validierter Artikel gefunden wurde, setze die URL, von der extrahiert werden soll.
url_to_extract = validated_page.url
- self.logger.info(f" -> Suche erfolgreich, validierte URL: {url_to_extract[:100]}...") # Gekuerzt loggen
+ self.logger.info(f" -> Suche erfolgreich, validierte URL: {url_to_extract[:100]}...") # <<< GEÄNDERT
else:
# Wenn die Suche keinen validierten Artikel fand
- self.logger.debug(f" -> Suche fand keinen validierten Artikel fuer '{company_name[:100]}...'.") # Gekuerzt loggen
+ self.logger.debug(f" -> Suche fand keinen validierten Artikel fuer '{company_name[:100]}...'.") # <<< GEÄNDERT
url_to_extract = 'Kein Artikel gefunden' # Signalisiert kein Artikel gefunden
except Exception as e_wiki_search:
# Wenn search_company_article eine Exception wirft (nach Retries)
# Der Fehler wird bereits vom retry_on_failure Decorator geloggt.
- self.logger.error(f"FEHLER bei Wikipedia Suche fuer '{company_name[:100]}...': {e_wiki_search}") # Gekuerzt loggen
+ self.logger.error(f"FEHLER bei Wikipedia Suche fuer '{company_name[:100]}...': {e_wiki_search}") # <<< GEÄNDERT
url_to_extract = f"FEHLER bei Suche: {str(e_wiki_search)[:50]}..." # Signalisiert Fehler bei Suche (gekuerzt)
# Pass, faert fort, um zumindest den Status zu setzen.
pass
@@ -3927,7 +3921,7 @@ class DataProcessor:
# --- Datenextraktion, wenn eine URL bestimmt wurde, von der extrahiert werden soll ---
# Extrahiere Daten, wenn url_to_extract einen Wert hat, der NICHT "Kein Artikel gefunden" oder ein Fehlerstring ist.
if url_to_extract and isinstance(url_to_extract, str) and url_to_extract.lower() not in ['kein artikel gefunden'] and not url_to_extract.startswith("FEHLER"):
- self.logger.debug(f" -> Extrahiere Daten von URL: {url_to_extract[:100]}...") # Gekuerzt loggen
+ self.logger.debug(f" -> Extrahiere Daten von URL: {url_to_extract[:100]}...") # <<< GEÄNDERT
try:
# Rufe die extract_company_data Methode des Scrapers auf.
# extract_company_data ist mit retry_on_failure dekoriert und wirft bei endgueltigem Fehler eine Exception.
@@ -3937,28 +3931,28 @@ class DataProcessor:
if extracted_data and isinstance(extracted_data, dict) and extracted_data.get('url') != 'k.A.': # Pruefe auf gueltige Extraktion
final_wiki_data = extracted_data # Aktualisiere die Arbeitskopie der Wiki-Daten mit den extrahierten Daten.
wiki_data_updated_in_this_run = True # Markieren, dass extrahierte Daten da sind (Trigger fuer Chat).
- self.logger.info(f" -> Datenextraktion von {url_to_extract[:100]}... erfolgreich.") # Gekuerzt loggen
+ self.logger.info(f" -> Datenextraktion von {url_to_extract[:100]}... erfolgreich.") # <<< GEÄNDERT
else:
# Wenn extrahierte Daten leer oder ungueltig sind (z.B. parse Fehler intern)
- self.logger.error(f" -> Fehler bei Datenextraktion von {url_to_extract[:100]}... oder Extraktion war leer. Setze Daten auf 'k.A.'") # Gekuerzt loggen
+ self.logger.error(f" -> Fehler bei Datenextraktion von {url_to_extract[:100]}... oder Extraktion war leer. Setze Daten auf 'k.A.'") # <<< GEÄNDERT
# Behalte die URL, aber setze alle anderen Felder auf k.A. oder Fehler.
final_wiki_data = {'url': url_to_extract, 'first_paragraph': 'k.A. (Extraktion fehlgeschlagen)', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'}
wiki_data_updated_in_this_run = True # Markieren, dass die Daten ueberschrieben werden.
except Exception as e_wiki_extract:
# Wenn extract_company_data eine Exception wirft (nach Retries)
- self.logger.error(f"FEHLER bei Wikipedia Datenextraktion von {url_to_extract[:100]}...: {e_wiki_extract}") # Gekuerzt loggen
+ self.logger.error(f"FEHLER bei Wikipedia Datenextraktion von {url_to_extract[:100]}...: {e_wiki_extract}") # <<< GEÄNDERT
# Setze Daten auf k.A., behalte aber die URL, von der extrahiert werden sollte
- final_wiki_data = {'url': url_to_extract, 'first_paragraph': f'k.A. (FEHLER Extraktion: {str(e)[:50]}...)', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'}
+ final_wiki_data = {'url': url_to_extract, 'first_paragraph': f'k.A. (FEHLER Extraktion: {str(e_wiki_extract)[:50]}...)', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} # Korrektur: e -> e_wiki_extract
wiki_data_updated_in_this_run = True # Markieren, dass die Daten ueberschrieben werden.
pass # Faert fort
else:
# Wenn keine gueltige URL zum Extrahieren bestimmt wurde (z.B. Suche fand nichts oder Fehler bei Suche)
- self.logger.debug(f" -> Keine gueltige URL zum Extrahieren bestimmt ('{url_to_extract}'). Wiki-Daten nicht extrahiert.")
+ self.logger.debug(f" -> Keine gueltige URL zum Extrahieren bestimmt ('{url_to_extract}'). Wiki-Daten nicht extrahiert.") # <<< GEÄNDERT
# final_wiki_data behaelt die current_wiki_data Werte (initial geladen) oder wurde oben bei Suche auf "Kein Artikel gefunden"/"FEHLER" gesetzt.
# Stelle sicher, dass final_wiki_data die richtige URL enthaelt, auch wenn keine Extraktion stattfand.
- if url_to_extract in ['Kein Artikel gefunden', 'FEHLER bei Suche']:
+ if url_to_extract in ['Kein Artikel gefunden'] or (isinstance(url_to_extract, str) and url_to_extract.startswith("FEHLER")): # Korrektur Pruefung
final_wiki_data['url'] = url_to_extract # Update nur die URL im Ergebnis
# --- Sheet Updates fuer M-R und AN ---
@@ -3985,7 +3979,8 @@ class DataProcessor:
status_s_indicates_reparse = self._get_cell_value_safe(row_data, "Chat Wiki Konsistenzpruefung").strip().upper() == "X (URL COPIED)"
# Pruefe, ob die FINAL_wiki_data URL (nach Suche/Extraktion) anders ist als die URSPRUENGLICHE URL in M im Sheet.
# UND stelle sicher, dass die neue URL eine gueltige URL ist (nicht "k.A." oder Fehlerstring).
- url_changed_and_valid = (url_in_m != final_wiki_data.get('url')) and isinstance(final_wiki_data.get('url'), str) and final_wiki_data.get('url').lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche"]
+ new_wiki_url = final_wiki_data.get('url')
+ url_changed_and_valid = (url_in_m != new_wiki_url) and isinstance(new_wiki_url, str) and new_wiki_url.lower() not in ["k.a.", "kein artikel gefunden"] and not new_wiki_url.startswith("FEHLER") # Korrektur Pruefung
# Bestimme, ob S und AX zurueckgesetzt werden sollen
if force_reeval or status_s_indicates_reparse or url_changed_and_valid:
@@ -4008,10 +4003,10 @@ class DataProcessor:
if url_changed_and_valid: grund_message_parts.append('URL geaendert und gueltig')
grund_message_s_reset = ", ".join(grund_message_parts)
- self.logger.info(f" -> Status S zurueckgesetzt auf '?' und Timestamp AX geleert fuer erneute Verifikation (Grund: {grund_message_s_reset}).")
+ self.logger.info(f" -> Status S zurueckgesetzt auf '?' und Timestamp AX geleert fuer erneute Verifikation (Grund: {grund_message_s_reset}).") # <<< GEÄNDERT
else:
# Logge Fehler, wenn Spaltenindizes fehlen
- self.logger.error("FEHLER: Konnte Spaltenbuchstaben fuer S oder AX nicht ermitteln. Zuruecksetzen uebersprungen.")
+ self.logger.error("FEHLER: Konnte Spaltenbuchstaben fuer S oder AX nicht ermitteln. Zuruecksetzen uebersprungen.") # <<< GEÄNDERT
# else if run_wiki_step:
@@ -4030,6 +4025,7 @@ class DataProcessor:
# Nutzt interne Helfer: _get_cell_value_safe, _needs_chat_evaluations.
# Nutzt globale Helfer: COLUMN_MAP, logger, datetime, time,
# evaluate_branche_chatgpt (Block 10),
+ # get_numeric_filter_value (Block 5) <- ERSETZT get_valid_numeric.
# (Optional: evaluate_fsm_suitability, evaluate_employee_chatgpt, evaluate_umsatz_chatgpt - muessen implementiert werden).
# Nutzt lokale Variablen: crm_branche, crm_beschreibung, final_wiki_data, website_summary, wiki_data_updated_in_this_run.
@@ -4056,7 +4052,7 @@ class DataProcessor:
if wiki_data_updated_in_this_run: grund_message_parts.append('Wiki Daten gerade aktualisiert')
grund_message = ", ".join(grund_message_parts)
- self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre CHATGPT Evaluationen aus (Grund: {grund_message})...")
+ self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre CHATGPT Evaluationen aus (Grund: {grund_message})...") # <<< GEÄNDERT
# Hole die notwendigen Daten fuer die ChatGPT-Calls.
# Nutzt die initial geladenen CRM-Daten und die finalen Daten aus den vorherigen Schritten (Wiki, Website).
@@ -4065,204 +4061,7 @@ class DataProcessor:
# website_summary wurde im Website-Schritt (Block 18) aktualisiert oder behaelt alte Werte.
# --- 3a. Branchen-Einstufung (W, X, Y) ---
- self.logger.debug(" -> Starte Branchen-Einstufung ueber ChatGPT...")
- try:
- # Annahme: evaluate_branche_chatgpt global definiert (Block 10) und nutzt logging/retry
- # evaluate_branche_chatgpt braucht Zugriff auf globale ALLOWED_TARGET_BRANCHES und TARGET_SCHEMA_STRING (Block 7)
- # Der Aufruf ist mit retry_on_failure dekoriert und wirft bei endgueltigem Fehler eine Exception.
- branch_result = evaluate_branche_chatgpt(
- crm_branche, # Nutzt initial geladenen CRM Wert
- crm_beschreibung, # Nutzt initial geladenen CRM Wert
- final_wiki_data.get('branche', 'k.A.'), # Nutzt ggf. neue Wiki-Branche aus Block 19
- final_wiki_data.get('categories', 'k.A.'), # Nutzt ggf. neue Wiki-Kategorien aus Block 19
- website_summary # Nutzt ggf. neue Website-Zusammenfassung aus Block 18
- )
- # Sammle Updates fuer die Branchen-Spalten (W, X, Y) (nutzt interne Helfer)
- # Stellen Sie sicher, dass die Schluessel im Ergebnis-Dict vorhanden sind, Fallback auf Standard-Fehlerwerte.
- updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get("branch", "FEHLER")]]})
- updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenz Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get("consistency", "error")]]})
- updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get("justification", "Keine Begruendung")]]})
-
- except Exception as e_branch_eval:
- # Wenn evaluate_branche_chatgpt eine Exception wirft (nach Retries)
- # Der Fehler wird bereits vom retry_on_failure Decorator oder evaluate_branche_chatgpt geloggt.
- self.logger.error(f"FEHLER bei Branchen-Einstufung ueber ChatGPT fuer Zeile {row_num_in_sheet}: {e_branch_eval}")
- # Fuegen Sie Updates mit Fehlerwerten hinzu, um den Fehler im Sheet zu dokumentieren.
- error_msg = f"Fehler: {str(e_branch_eval)[:100]}..." # Kuerze Fehlermeldung
- updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]})
- updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenz Branche"] + 1)}{row_num_in_sheet}', 'values': [['error']]})
- updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Branche"] + 1)}{row_num_in_sheet}', 'values': [[error_msg]]})
- pass # Fahren Sie fort mit den naechsten Schritten, auch wenn Branchenevaluation fehlschlug
-
-
- # --- 3b. FSM Relevanz Bewertung (Z, AA) ---
- # TODO: Implementieren Sie die Logik und den Aufruf der Funktion evaluate_fsm_suitability
- self.logger.debug(" -> Starte FSM Relevanz Bewertung (Platzhalter)...")
- # Beispielaufruf (angenommen, evaluate_fsm_suitability existiert global):
- # try:
- # fsm_result = evaluate_fsm_suitability(
- # company_name, # Nutzt initial geladenen CRM Namen
- # {'crm_desc': crm_beschreibung, 'wiki_paragraph': final_wiki_data.get('first_paragraph', 'k.A.'), 'web_summary': website_summary}
- # )
- # # Sammle Updates fuer FSM Spalten (Z, AA)
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Pruefung FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [[fsm_result.get('suitability', 'k.A.')]]})
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung fuer FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [[fsm_result.get('justification', 'k.A.')]]})
- # except Exception as e_fsm_eval:
- # self.logger.error(f"FEHLER bei FSM Relevanz Bewertung fuer Zeile {row_num_in_sheet}: {e_fsm_eval}")
- # # Fuege Fehler-Updates hinzu
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Pruefung FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]})
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung fuer FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [[f'Fehler: {str(e_fsm_eval)[:100]}...']]})
- # pass # Faert fort
-
-
- # --- 3c. Mitarbeiterzahl Schaetzung (AB, AC, AD) ---
- # TODO: Implementieren Sie die Logik und den Aufruf der Funktion evaluate_employee_chatgpt
- self.logger.debug(" -> Starte Mitarbeiterzahl Schaetzung (Platzhalter)...")
- # Beispielaufruf (angenommen, evaluate_employee_chatgpt existiert global):
- # try:
- # emp_estimate_result = evaluate_employee_chatgpt(
- # company_name, # Nutzt initial geladenen CRM Namen
- # {'crm_emp': self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter"), 'wiki_emp': final_wiki_data.get('mitarbeiter', 'k.A.'), 'wiki_paragraph': final_wiki_data.get('first_paragraph', 'k.A.'), 'web_summary': website_summary}
- # )
- # # Sammle Updates fuer Mitarbeiter Schaetzspalten (AB, AC, AD)
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Anzahl Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[emp_estimate_result.get('estimate', 'k.A.')]]})
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenzpruefung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [[emp_estimate_result.get('consistency', 'k.A.')]]})
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [[emp_estimate_result.get('justification', 'k.A.')]]})
- # except Exception as e_emp_eval:
- # self.logger.error(f"FEHLER bei Mitarbeiterzahl Schaetzung fuer Zeile {row_num_in_sheet}: {e_emp_eval}")
- # # Fuege Fehler-Updates hinzu
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Anzahl Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]})
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenzpruefung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [['error']]})
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [[f'Fehler: {str(e_emp_eval)[:100]}...']]})
- # pass # Faert fort
-
-
- # --- 3d. Umsatz Schaetzung (AG, AH) ---
- # TODO: Implementieren Sie die Logik und den Aufruf der Funktion evaluate_umsatz_chatgpt
- self.logger.debug(" -> Starte Umsatz Schaetzung (Platzhalter)...")
- # Beispielaufruf (angenommen, evaluate_umsatz_chatgpt existiert global):
- # try:
- # umsatz_estimate_result = evaluate_umsatz_chatgpt(
- # company_name, # Nutzt initial geladenen CRM Namen
- # {'crm_umsatz': self._get_cell_value_safe(row_data, "CRM Umsatz"), 'wiki_umsatz': final_wiki_data.get('umsatz', 'k.A.'), 'wiki_paragraph': final_wiki_data.get('first_paragraph', 'k.A.'), 'web_summary': website_summary}
- # )
- # # Sammle Updates fuer Umsatz Schaetzspalten (AG, AH)
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[umsatz_estimate_result.get('estimate', 'k.A.')]]})
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[umsatz_estimate_result.get('justification', 'k.A.')]]})
- # except Exception as e_umsatz_eval:
- # self.logger.error(f"FEHLER bei Umsatz Schaetzung fuer Zeile {row_num_in_sheet}: {e_umsatz_eval}")
- # # Fuege Fehler-Updates hinzu
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]})
- # updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[f'Fehler: {str(e_umsatz_eval)[:100]}...']]})
- # pass # Faert fort
-
-
- # --- 3e. Konsolidierung Umsatz/Mitarbeiter (AV, AW) ---
- # Diese Logik wurde bisher in prepare_data_for_modeling (Block 31) verwendet,
- # kann aber auch hier nach jeder Zeilenverarbeitung durchgefuehrt und
- # ins Sheet geschrieben werden, um die konsolidierten Werte aktuell zu halten.
- self.logger.debug(" -> Konsolidiere Umsatz (AV) und Mitarbeiter (AW) (Wiki > CRM Logik)...")
- try:
- # Nutzt globale Funktion get_valid_numeric (Block 5)
- # Hole die Werte aus den entsprechenden Spalten (CRM und finale Wiki-Daten)
- crm_umsatz_val = self._get_cell_value_safe(row_data, "CRM Umsatz")
- wiki_umsatz_val = final_wiki_data.get('umsatz', 'k.A.') # Nutzt finalen Wiki-Wert aus Block 19
-
- crm_ma_val = self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter")
- wiki_ma_val = final_wiki_data.get('mitarbeiter', 'k.A.') # Nutzt finalen Wiki-Wert aus Block 19
-
- # Konvertiere die Werte zu Numerisch (Float/Int oder NaN) mit get_valid_numeric
- num_crm_umsatz = get_valid_numeric(crm_umsatz_val)
- num_wiki_umsatz = get_valid_numeric(wiki_umsatz_val)
-
- num_crm_ma = get_valid_numeric(crm_ma_val)
- num_wiki_ma = get_valid_numeric(wiki_ma_val)
-
- # Konsolidierung: Wiki hat Prioritaet vor CRM. Wenn beide NaN sind, Ergebnis NaN.
- final_num_umsatz = num_wiki_umsatz if pd.notna(num_wiki_umsatz) else num_crm_umsatz
- final_num_ma = num_wiki_ma if pd.notna(num_wiki_ma) else num_crm_ma
-
- # Konvertiere das finale numerische Ergebnis zurueck zu einem String ("Zahl" oder "k.A.")
- # Runden Sie Umsatz auf ganze Millionen und Mitarbeiter auf ganze Zahlen.
- final_umsatz_str = str(int(round(final_num_umsatz))) if pd.notna(final_num_umsatz) and final_num_umsatz > 0 else 'k.A.'
- final_ma_str = str(int(round(final_num_ma))) if pd.notna(final_num_ma) and final_num_ma > 0 else 'k.A.'
-
-
- # Sammle Updates fuer die Konsolidierungsspalten (AV, AW)
- updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [[final_umsatz_str]]})
- updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [[final_ma_str]]})
- self.logger.debug(f" -> Konsolidiert: Umsatz={final_umsatz_str}, MA={final_ma_str}")
-
-
- except Exception as e_consolidate:
- # Fange Fehler bei der Konsolidierung ab und logge sie
- self.logger.error(f"FEHLER bei Konsolidierung Umsatz/Mitarbeiter fuer Zeile {row_num_in_sheet}: {e_consolidate}")
- # Logge den Traceback
- self.logger.debug(traceback.format_exc())
- # Fuege Fehler-Updates hinzu, um den Fehler im Sheet zu dokumentieren
- updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]})
- updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]})
- pass # Faert fort
-
-
- # Setze den Timestamp letzte Pruefung (AO), da die ChatGPT-Evaluationen liefen (auch wenn fehlerhaft)
- updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Pruefung"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]})
-
-
- # else if run_chat_step:
- # Die Chat-Schritte waren angefordert, aber nicht noetig basierend auf Status/Re-Eval/Wiki-Update.
- # Die lokalen Variablen final_wiki_data und website_summary behalten ihre initialen Werte (current_...).
- # chat_eval_just_ran bleibt False.
- # self.logger.debug(f"Zeile {row_num_in_sheet}: Ueberspringe CHATGPT Evaluationen (AO vorhanden, Wiki nicht aktualisiert und kein Re-Eval).") # Zu viel Laerm im Debug
-
-
- # --- Der Code fuer den naechsten Verarbeitungsschritt (ML Prediction) folgt im naechsten Block ---
- # Definition der Methode _process_single_row wird in der naechsten Nachricht fortgesetzt.
-
- # --- 3. ChatGPT Evaluationen (Branch, FSM, Emp, Umsatz Schaetzungen etc.) ---
- # Dieser Schritt wird ausgefuehrt, wenn 'chat' in steps_to_run enthalten ist UND
- # (_needs_chat_evaluations True ist ODER force_reeval True ist).
- # _needs_chat_evaluations prueft AO oder ob Wiki-Daten in diesem Lauf gerade aktualisiert wurden.
- # Nutzt interne Helfer: _get_cell_value_safe, _needs_chat_evaluations.
- # Nutzt globale Helfer: COLUMN_MAP, logger, datetime, time,
- # evaluate_branche_chatgpt (Block 10),
- # get_valid_numeric (Block 5).
- # (Optional: evaluate_fsm_suitability, evaluate_employee_chatgpt, evaluate_umsatz_chatgpt - muessen implementiert werden).
- # Nutzt lokale Variablen: crm_branche, crm_beschreibung, final_wiki_data, website_summary, wiki_data_updated_in_this_run.
-
- # Pruefen Sie, ob die Chat-Schritte im aktuellen Lauf angefordert wurden
- run_chat_step = 'chat' in steps_to_run
- # Pruefen Sie, ob die Chat-Schritte laut Status, Re-Eval oder Wiki-Update noetig sind.
- # wiki_data_just_updated_in_this_run ist ein Flag aus dem vorherigen Wiki-Schritt (Block 19).
- chat_processing_needed_based_on_status = self._needs_chat_evaluations(row_data, force_reeval, wiki_data_updated_in_this_run)
-
-
- # Wenn die Chat-Schritte angefordert wurden UND laut Status/Re-Eval noetig sind
- if run_chat_step and chat_processing_needed_based_on_status:
- any_processing_done = True # Markiere, dass in dieser Zeile etwas getan wird
-
- # Setzen Sie das Flag, dass Chat-Evaluationen liefen (koennte ML ausloesen Block 23)
- chat_eval_just_ran = True
-
- # Bestimme den Grund fuer die Ausfuehrung dieses Schritts fuer das Logging
- grund_message_parts = []
- if force_reeval: grund_message_parts.append('Re-Eval')
- # Pruefe, ob der Timestamp AO leer ist (nutzt interne Helfer)
- if not self._get_cell_value_safe(row_data, "Timestamp letzte Pruefung").strip(): grund_message_parts.append('AO leer')
- # Pruefe, ob Wiki-Daten gerade aktualisiert wurden (Flag aus Block 19)
- if wiki_data_updated_in_this_run: grund_message_parts.append('Wiki Daten gerade aktualisiert')
- grund_message = ", ".join(grund_message_parts)
-
- self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre CHATGPT Evaluationen aus (Grund: {grund_message})...")
-
- # Hole die notwendigen Daten fuer die ChatGPT-Calls.
- # Nutzt die initial geladenen CRM-Daten und die finalen Daten aus den vorherigen Schritten (Wiki, Website).
- # crm_branche, crm_beschreibung wurden initial geladen (Block 17).
- # final_wiki_data wurde im Wiki-Schritt (Block 19) aktualisiert oder behaelt alte Werte.
- # website_summary wurde im Website-Schritt (Block 18) aktualisiert oder behaelt alte Werte.
-
- # --- 3a. Branchen-Einstufung (W, X, Y) ---
- self.logger.debug(" -> Starte Branchen-Einstufung ueber ChatGPT...")
+ self.logger.debug(" -> Starte Branchen-Einstufung ueber ChatGPT...") # <<< GEÄNDERT
try:
# Annahme: evaluate_branche_chatgpt global definiert (Block 10) und nutzt logging/retry
# evaluate_branche_chatgpt braucht Zugriff auf globale ALLOWED_TARGET_BRANCHES und TARGET_SCHEMA_STRING (Block 6)
@@ -4283,9 +4082,9 @@ class DataProcessor:
except Exception as e_branch_eval:
# Wenn evaluate_branche_chatgpt eine Exception wirft (nach Retries)
# Der Fehler wird bereits vom retry_on_failure Decorator oder evaluate_branche_chatgpt geloggt.
- self.logger.error(f"FEHLER bei Branchen-Einstufung ueber ChatGPT fuer Zeile {row_num_in_sheet}: {e_branch_eval}")
+ self.logger.error(f"FEHLER bei Branchen-Einstufung ueber ChatGPT fuer Zeile {row_num_in_sheet}: {e_branch_eval}") # <<< GEÄNDERT
# Logge den Traceback fuer detailliertere Fehlerinformationen
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
# Fuegen Sie Updates mit Fehlerwerten hinzu, um den Fehler im Sheet zu dokumentieren.
error_msg = f"Fehler: {str(e_branch_eval)[:100]}..." # Kuerze Fehlermeldung
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) # Block 1 Column Map
@@ -4296,7 +4095,7 @@ class DataProcessor:
# --- 3b. FSM Relevanz Bewertung (Z, AA) ---
# TODO: Implementieren Sie die Logik und den Aufruf der Funktion evaluate_fsm_suitability
- self.logger.debug(" -> Starte FSM Relevanz Bewertung (Platzhalter)...")
+ self.logger.debug(" -> Starte FSM Relevanz Bewertung (Platzhalter)...") # <<< GEÄNDERT
# Beispielaufruf (angenommen, evaluate_fsm_suitability existiert global):
# try:
# fsm_result = evaluate_fsm_suitability(
@@ -4307,7 +4106,7 @@ class DataProcessor:
# updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Pruefung FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [[fsm_result.get('suitability', 'k.A.')]]}) # Block 1 Column Map
# updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung fuer FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [[fsm_result.get('justification', 'k.A.')]]}) # Block 1 Column Map
# except Exception as e_fsm_eval:
- # self.logger.error(f"FEHLER bei FSM Relevanz Bewertung fuer Zeile {row_num_in_sheet}: {e_fsm_eval}")
+ # self.logger.error(f"FEHLER bei FSM Relevanz Bewertung fuer Zeile {row_num_in_sheet}: {e_fsm_eval}") # <<< GEÄNDERT
# # Fuege Fehler-Updates hinzu
# updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Pruefung FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) # Block 1 Column Map
# updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung fuer FSM Relevanz"] + 1)}{row_num_in_sheet}', 'values': [[f'Fehler: {str(e_fsm_eval)[:100]}...']]}) # Block 1 Column Map
@@ -4316,7 +4115,7 @@ class DataProcessor:
# --- 3c. Mitarbeiterzahl Schaetzung (AB, AC, AD) ---
# TODO: Implementieren Sie die Logik und den Aufruf der Funktion evaluate_employee_chatgpt
- self.logger.debug(" -> Starte Mitarbeiterzahl Schaetzung (Platzhalter)...")
+ self.logger.debug(" -> Starte Mitarbeiterzahl Schaetzung (Platzhalter)...") # <<< GEÄNDERT
# Beispielaufruf (angenommen, evaluate_employee_chatgpt existiert global):
# try:
# emp_estimate_result = evaluate_employee_chatgpt(
@@ -4328,7 +4127,7 @@ class DataProcessor:
# updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenzpruefung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [[emp_estimate_result.get('consistency', 'k.A.')]]}) # Block 1 Column Map
# updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [[emp_estimate_result.get('justification', 'k.A.')]]}) # Block 1 Column Map
# except Exception as e_emp_eval:
- # self.logger.error(f"FEHLER bei Mitarbeiterzahl Schaetzung fuer Zeile {row_num_in_sheet}: {e_emp_eval}")
+ # self.logger.error(f"FEHLER bei Mitarbeiterzahl Schaetzung fuer Zeile {row_num_in_sheet}: {e_emp_eval}") # <<< GEÄNDERT
# # Fuege Fehler-Updates hinzu
# updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Anzahl Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) # Block 1 Column Map
# updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenzpruefung Mitarbeiterzahl"] + 1)}{row_num_in_sheet}', 'values': [['error']]}) # Block 1 Column Map
@@ -4338,7 +4137,7 @@ class DataProcessor:
# --- 3d. Umsatz Schaetzung (AG, AH) ---
# TODO: Implementieren Sie die Logik und den Aufruf der Funktion evaluate_umsatz_chatgpt
- self.logger.debug(" -> Starte Umsatz Schaetzung (Platzhalter)...")
+ self.logger.debug(" -> Starte Umsatz Schaetzung (Platzhalter)...") # <<< GEÄNDERT
# Beispielaufruf (angenommen, evaluate_umsatz_chatgpt existiert global):
# try:
# umsatz_estimate_result = evaluate_umsatz_chatgpt(
@@ -4349,7 +4148,7 @@ class DataProcessor:
# updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[umsatz_estimate_result.get('estimate', 'k.A.')]]}) # Block 1 Column Map
# updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[umsatz_estimate_result.get('justification', 'k.A.')]]}) # Block 1 Column Map
# except Exception as e_umsatz_eval:
- # self.logger.error(f"FEHLER bei Umsatz Schaetzung fuer Zeile {row_num_in_sheet}: {e_umsatz_eval}")
+ # self.logger.error(f"FEHLER bei Umsatz Schaetzung fuer Zeile {row_num_in_sheet}: {e_umsatz_eval}") # <<< GEÄNDERT
# # Fuege Fehler-Updates hinzu
# updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Schaetzung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) # Block 1 Column Map
# updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begruendung Abweichung Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[f'Fehler: {str(e_umsatz_eval)[:100]}...']]}) # Block 1 Column Map
@@ -4360,9 +4159,9 @@ class DataProcessor:
# Diese Logik wurde bisher in prepare_data_for_modeling (Block 31) verwendet,
# kann aber auch hier nach jeder Zeilenverarbeitung durchgefuehrt und
# ins Sheet geschrieben werden, um die konsolidierten Werte aktuell zu halten.
- self.logger.debug(" -> Konsolidiere Umsatz (AV) und Mitarbeiter (AW) (Wiki > CRM Logik)...")
+ self.logger.debug(" -> Konsolidiere Umsatz (AV) und Mitarbeiter (AW) (Wiki > CRM Logik)...") # <<< GEÄNDERT
try:
- # Nutzt globale Funktion get_valid_numeric (Block 5)
+ # Nutzt globale Funktion get_numeric_filter_value (Block 5) - ERSETZT get_valid_numeric
# Hole die Werte aus den entsprechenden Spalten (CRM und finale Wiki-Daten)
crm_umsatz_val = self._get_cell_value_safe(row_data, "CRM Umsatz") # Block 1 Column Map
wiki_umsatz_val = final_wiki_data.get('umsatz', 'k.A.') # Nutzt finalen Wiki-Wert aus Block 19
@@ -4370,35 +4169,36 @@ class DataProcessor:
crm_ma_val = self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter") # Block 1 Column Map
wiki_ma_val = final_wiki_data.get('mitarbeiter', 'k.A.') # Nutzt finalen Wiki-Wert aus Block 19
- # Konvertiere die Werte zu Numerisch (Float/Int oder NaN) mit get_valid_numeric
- num_crm_umsatz = get_valid_numeric(crm_umsatz_val)
- num_wiki_umsatz = get_valid_numeric(wiki_umsatz_val)
+ # Konvertiere die Werte zu Numerisch (Float/Int oder 0) mit get_numeric_filter_value
+ num_crm_umsatz = get_numeric_filter_value(crm_umsatz_val, is_umsatz=True)
+ num_wiki_umsatz = get_numeric_filter_value(wiki_umsatz_val, is_umsatz=True)
- num_crm_ma = get_valid_numeric(crm_ma_val)
- num_wiki_ma = get_valid_numeric(wiki_ma_val)
+ num_crm_ma = get_numeric_filter_value(crm_ma_val, is_umsatz=False)
+ num_wiki_ma = get_numeric_filter_value(wiki_ma_val, is_umsatz=False)
- # Konsolidierung: Wiki hat Prioritaet vor CRM. Wenn beide NaN sind, Ergebnis NaN.
- final_num_umsatz = num_wiki_umsatz if pd.notna(num_wiki_umsatz) else num_crm_umsatz
- final_num_ma = num_wiki_ma if pd.notna(num_wiki_ma) else num_crm_ma
+ # Konsolidierung: Wiki hat Prioritaet vor CRM. Wenn Wiki ungueltig (0), nehme CRM.
+ # WICHTIG: 0 ist hier das Kennzeichen fuer ungueltig/nicht parsebar/k.A. in get_numeric_filter_value
+ final_num_umsatz = num_wiki_umsatz if num_wiki_umsatz > 0 else num_crm_umsatz
+ final_num_ma = num_wiki_ma if num_wiki_ma > 0 else num_crm_ma
# Konvertiere das finale numerische Ergebnis zurueck zu einem String ("Zahl" oder "k.A.")
# Runden Sie Umsatz auf ganze Millionen und Mitarbeiter auf ganze Zahlen.
# Stellen Sie sicher, dass nur positive Werte als Zahl ausgegeben werden, sonst "k.A.".
- final_umsatz_str = str(int(round(final_num_umsatz))) if pd.notna(final_num_umsatz) and final_num_umsatz > 0 else 'k.A.'
- final_ma_str = str(int(round(final_num_ma))) if pd.notna(final_num_ma) and final_num_ma > 0 else 'k.A.'
+ final_umsatz_str = str(int(round(final_num_umsatz))) if final_num_umsatz > 0 else 'k.A.'
+ final_ma_str = str(int(round(final_num_ma))) if final_num_ma > 0 else 'k.A.'
# Sammle Updates fuer die Konsolidierungsspalten (AV, AW) (nutzt interne Helfer)
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [[final_umsatz_str]]}) # Block 1 Column Map
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [[final_ma_str]]}) # Block 1 Column Map
- self.logger.debug(f" -> Konsolidiert: Umsatz={final_umsatz_str}, MA={final_ma_str}")
+ self.logger.debug(f" -> Konsolidiert: Umsatz={final_umsatz_str}, MA={final_ma_str}") # <<< GEÄNDERT
except Exception as e_consolidate:
# Fange Fehler bei der Konsolidierung ab und logge sie
- self.logger.error(f"FEHLER bei Konsolidierung Umsatz/Mitarbeiter fuer Zeile {row_num_in_sheet}: {e_consolidate}")
+ self.logger.error(f"FEHLER bei Konsolidierung Umsatz/Mitarbeiter fuer Zeile {row_num_in_sheet}: {e_consolidate}") # <<< GEÄNDERT
# Logge den Traceback
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
# Fuege Fehler-Updates hinzu, um den Fehler im Sheet zu dokumentieren
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) # Block 1 Column Map
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER']]}) # Block 1 Column Map
@@ -4443,10 +4243,10 @@ class DataProcessor:
if force_reeval: grund_message_parts.append('Re-Eval')
# Wenn nicht Re-Eval, dann liegt es an _needs_ml_prediction. Logge den Grund von dort auf Debug.
if not force_reeval:
- self.logger.debug(" -> ML-Schaetzung noetig (Grund laut _needs_ml_prediction).")
+ self.logger.debug(" -> ML-Schaetzung noetig (Grund laut _needs_ml_prediction).") # <<< GEÄNDERT
pass # Der spezifische Grund wird bereits in _needs_ml_prediction geloggt (auf Debug).
- self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre ML-Schaetzung aus...")
+ self.logger.info(f"Zeile {row_num_in_sheet}: Fuehre ML-Schaetzung aus...") # <<< GEÄNDERT
# Die ML-Schaetzung benoetigt die vorbereiteten Daten (konsolidierter Umsatz/Mitarbeiter und Branche).
# Diese Werte sind bereits in der Zeile im Sheet verfuegbar (Spalten AV, AW)
@@ -4464,20 +4264,20 @@ class DataProcessor:
if predicted_bucket and isinstance(predicted_bucket, str) and not predicted_bucket.startswith("FEHLER"):
# Sammle Update fuer den AU Bucket (Geschaetzter Techniker Bucket) (nutzt interne Helfer)
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', 'values': [[predicted_bucket]]}) # Block 1 Column Map
- self.logger.info(f" -> ML-Schaetzung erfolgreich: Bucket '{predicted_bucket}'.")
+ self.logger.info(f" -> ML-Schaetzung erfolgreich: Bucket '{predicted_bucket}'.") # <<< GEÄNDERT
else:
# Wenn die Vorhersage fehlschlug oder kein Ergebnis lieferte
- self.logger.warning(f" -> ML-Schaetzung lieferte kein gueltiges Ergebnis: '{predicted_bucket}'.")
+ self.logger.warning(f" -> ML-Schaetzung lieferte kein gueltiges Ergebnis: '{predicted_bucket}'.") # <<< GEÄNDERT
# Setze einen Fehlerwert im Sheet
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', 'values': [['k.A. (Schaetzung fehlgeschlagen)']]}) # Block 1 Column Map
except Exception as e_ml:
# Wenn _predict_technician_bucket eine Exception wirft
- self.logger.error(f"FEHLER bei ML-Schaetzung fuer Zeile {row_num_in_sheet}: {e_ml}")
+ self.logger.error(f"FEHLER bei ML-Schaetzung fuer Zeile {row_num_in_sheet}: {e_ml}") # <<< GEÄNDERT
# Logge den Traceback
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
# Fuege Fehler-Update hinzu, um den Fehler im Sheet zu dokumentieren
- updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', 'values': [[f'FEHLER Schaetzung: {str(e)[:50]}...']]}) # Block 1 Column Map
+ updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Geschaetzter Techniker Bucket"] + 1)}{row_num_in_sheet}', 'values': [[f'FEHLER Schaetzung: {str(e_ml)[:50]}...']]}) # Block 1 Column Map
pass # Faert fort
@@ -4500,7 +4300,7 @@ class DataProcessor:
# Fuege das Update fuer die Version zur Liste hinzu
updates.append({'range': f'{self.sheet_handler._get_col_letter(version_col_idx + 1)}{row_num_in_sheet}', 'values': [[getattr(Config, 'VERSION', 'unknown')]]}) # Block 1 Column Map
else:
- self.logger.error("FEHLER: Spaltenschluessel 'Version' nicht in COLUMN_MAP gefunden.")
+ self.logger.error("FEHLER: Spaltenschluessel 'Version' nicht in COLUMN_MAP gefunden.") # <<< GEÄNDERT
# Tokens (AQ) - Hier ist die Zaehlung komplex, da mehrere OpenAI-Calls passiert sein koennten.
# Eine einfache Loesung ist, die Token-Zahl der letzten relevanten Antwort zu speichern
@@ -4533,20 +4333,20 @@ class DataProcessor:
# Es wird nur geloescht, wenn die Zeile ansonsten erfolgreich bis hierhin kam und Updates gesammelt wurden.
# Wenn eine schwere Exception in _process_single_row auftrat, wird dieser Block nicht erreicht.
updates.append({'range': f'{flag_col_letter}{row_num_in_sheet}', 'values': [['']]})
- self.logger.debug(f" -> Update zum Loeschen des ReEval-Flags (A{row_num_in_sheet}) vorgemerkt.")
+ self.logger.debug(f" -> Update zum Loeschen des ReEval-Flags (A{row_num_in_sheet}) vorgemerkt.") # <<< GEÄNDERT
else:
# Logge Fehler, wenn Spaltenbuchstaben nicht ermittelt werden konnten
- self.logger.error(f"FEHLER: Konnte Spaltenbuchstaben fuer 'ReEval Flag' ({reeval_col_idx+1}) nicht ermitteln. Flag kann nicht geloescht werden.")
+ self.logger.error(f"FEHLER: Konnte Spaltenbuchstaben fuer 'ReEval Flag' ({reeval_col_idx+1}) nicht ermitteln. Flag kann nicht geloescht werden.") # <<< GEÄNDERT
else:
# Logge Fehler, wenn Spaltenindex fehlt
- self.logger.error("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden. Flag kann nicht geloescht werden.")
+ self.logger.error("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden. Flag kann nicht geloescht werden.") # <<< GEÄNDERT
# --- 6. Batch Update fuer diese Zeile ---
# Fuehren Sie das Batch-Update fuer ALLE gesammelten Aenderungen dieser EINEN Zeile durch.
if updates:
# Info-Log ueber die Anzahl der Updates fuer diese spezifische Zeile
- self.logger.info(f"Zeile {row_num_in_sheet}: Sende Batch-Update mit {len(updates)} Operationen fuer diese Zeile...")
+ self.logger.info(f"Zeile {row_num_in_sheet}: Sende Batch-Update mit {len(updates)} Operationen fuer diese Zeile...") # <<< GEÄNDERT
# Rufe die batch_update_cells Methode des Sheet Handlers auf.
# batch_update_cells ist mit retry_on_failure dekoriert und loggt intern.
success = self.sheet_handler.batch_update_cells(updates) # Nutzt die uebergeordnete Instanz
@@ -4554,17 +4354,17 @@ class DataProcessor:
# Wenn der Batch-Update fehlschlaegt (nach Retries)
if not success:
# Logge einen Error
- self.logger.error(f"Zeile {row_num_in_sheet}: ENDGUELTIGER FEHLER beim Batch-Update nach Retries.")
+ self.logger.error(f"Zeile {row_num_in_sheet}: ENDGUELTIGER FEHLER beim Batch-Update nach Retries.") # <<< GEÄNDERT
# Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte schreiben
# (Dieses Update muesste separat oder im naechsten Lauf behandelt werden)
else:
# Info-Log, wenn nichts zu tun war in dieser Zeile
if not any_processing_done:
- self.logger.debug(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben (alle relevanten Schritte uebersprungen oder nicht angefordert).")
+ self.logger.debug(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben (alle relevanten Schritte uebersprungen oder nicht angefordert).") # <<< GEÄNDERT
# else:
# Dieser Fall sollte nicht eintreten, wenn updates nicht leer ist, aber any_processing_done True ist.
- # self.logger.warning(f"Zeile {row_num_in_sheet}: Updates Liste war leer, aber any_processing_done=True. Pruefen Sie die Logik.")
+ # self.logger.warning(f"Zeile {row_num_in_sheet}: Updates Liste war leer, aber any_processing_done=True. Pruefen Sie die Logik.") # <<< GEÄNDERT
# Kleine Pause nach der Verarbeitung jeder Zeile, um API-Limits zu respektieren
@@ -4577,7 +4377,7 @@ class DataProcessor:
time.sleep(pause_duration)
# Logge den Abschluss der Verarbeitung fuer diese Zeile
- self.logger.info(f"--- Verarbeitung fuer Zeile {row_num_in_sheet} abgeschlossen ---")
+ self.logger.info(f"--- Verarbeitung fuer Zeile {row_num_in_sheet} abgeschlossen ---") # <<< GEÄNDERT
# --- Ende der _process_single_row Methode ---
@@ -4625,11 +4425,11 @@ class DataProcessor:
# Pruefen Sie, ob num_to_process gueltig ist
if num_to_process is None or not isinstance(num_to_process, int) or num_to_process <= 0:
- self.logger.info("Sequentielle Verarbeitung uebersprungen: num_to_process ist ungueltig oder <= 0.")
+ self.logger.info("Sequentielle Verarbeitung uebersprungen: num_to_process ist ungueltig oder <= 0.") # <<< GEÄNDERT
return
# Logge die Konfiguration des sequentiellen Laufs
- self.logger.info(f"Starte sequentielle Verarbeitung von {num_to_process} Zeilen ab Sheet-Zeile {start_sheet_row}...")
+ self.logger.info(f"Starte sequentielle Verarbeitung von {num_to_process} Zeilen ab Sheet-Zeile {start_sheet_row}...") # <<< GEÄNDERT
# Logge die ausgewaehlten Schritte fuer diesen Lauf
selected_steps_log = []
if process_wiki_steps: selected_steps_log.append("Wiki (wiki)")
@@ -4637,11 +4437,11 @@ class DataProcessor:
if process_website_steps: selected_steps_log.append("Website (web)")
if process_ml_steps: selected_steps_log.append("ML Predict (ml_predict)") # Neues Flag
# Fuegen Sie hier weitere Schritte hinzu, wenn neue Flags existieren
- self.logger.info(f" Ausgewaehlte Schritte fuer sequentiellen Lauf: {', '.join(selected_steps_log) if selected_steps_log else 'Keine ausgewählt!'}")
+ self.logger.info(f" Ausgewaehlte Schritte fuer sequentiellen Lauf: {', '.join(selected_steps_log) if selected_steps_log else 'Keine ausgewählt!'}") # <<< GEÄNDERT
# Logge, ob force_reeval in _process_single_row gesetzt wird
if force_reeval_in_single_row:
- self.logger.warning(" !!! force_reeval=True wird fuer alle Zeilen in _process_single_row gesetzt !!!")
+ self.logger.warning(" !!! force_reeval=True wird fuer alle Zeilen in _process_single_row gesetzt !!!") # <<< GEÄNDERT
# Erstelle das Set der Schluessel fuer die Schritte, die an _process_single_row uebergeben werden
steps_to_run_set = set()
@@ -4654,14 +4454,14 @@ class DataProcessor:
# Wenn keine Schritte ausgewaehlt wurden (trotz gueltigem num_to_process)
if not steps_to_run_set:
- self.logger.warning("Keine Verarbeitungsschritte fuer sequentiellen Lauf ausgewaehlt (steps_to_run_set ist leer). Modus wird uebersprungen.")
+ self.logger.warning("Keine Verarbeitungsschritte fuer sequentiellen Lauf ausgewaehlt (steps_to_run_set ist leer). Modus wird uebersprungen.") # <<< GEÄNDERT
return
# Lade Daten einmalig vor der Verarbeitung (nutzt die uebergeordnete Instanz)
# Der load_data Aufruf ist mit retry_on_failure dekoriert.
if not self.sheet_handler.load_data():
- self.logger.error("Fehler beim Laden der Daten fuer sequentielle Verarbeitung.")
+ self.logger.error("Fehler beim Laden der Daten fuer sequentielle Verarbeitung.") # <<< GEÄNDERT
return # Beende die Methode, wenn das Laden fehlschlaegt
@@ -4675,11 +4475,11 @@ class DataProcessor:
# Pruefen Sie, ob der angegebene Startindex gueltig ist
if start_index_in_all_data >= total_sheet_rows:
- self.logger.warning(f"Start-Sheet-Zeile {start_sheet_row} (Index {start_index_in_all_data}) liegt ausserhalb der verfuegbaren Daten ({total_sheet_rows} Zeilen insgesamt). Keine Verarbeitung.")
+ self.logger.warning(f"Start-Sheet-Zeile {start_sheet_row} (Index {start_index_in_all_data}) liegt ausserhalb der verfuegbaren Daten ({total_sheet_rows} Zeilen insgesamt). Keine Verarbeitung.") # <<< GEÄNDERT
return # Beende die Methode, wenn der Startindex ungueltig ist
if start_index_in_all_data < header_rows:
# Wenn der Startindex innerhalb der Header liegt, beginnen Sie nach den Headern.
- self.logger.warning(f"Start-Sheet-Zeile {start_sheet_row} liegt innerhalb der Header-Zeilen ({header_rows} Header). Verarbeitung startet ab Sheet-Zeile {header_rows + 1}.")
+ self.logger.warning(f"Start-Sheet-Zeile {start_sheet_row} liegt innerhalb der Header-Zeilen ({header_rows} Header). Verarbeitung startet ab Sheet-Zeile {header_rows + 1}.") # <<< GEÄNDERT
start_index_in_all_data = header_rows # Beginnen Sie direkt nach den Headern
@@ -4690,11 +4490,11 @@ class DataProcessor:
# Logge den Bereich der tatsaechlich zu verarbeitenden Zeilen
- self.logger.info(f"Sequentielle Verarbeitung: Verarbeitungsbereich (0-basiert Index) [{start_index_in_all_data}, {end_index_in_all_data}). Entsprechende Sheet-Zeilen (1-basiert): {start_index_in_all_data + 1} bis {end_index_in_all_data}.")
+ self.logger.info(f"Sequentielle Verarbeitung: Verarbeitungsbereich (0-basiert Index) [{start_index_in_all_data}, {end_index_in_all_data}). Entsprechende Sheet-Zeilen (1-basiert): {start_index_in_all_data + 1} bis {end_index_in_all_data}.") # <<< GEÄNDERT
# Pruefen Sie, ob es ueberhaupt Zeilen im berechneten Bereich gibt
if start_index_in_all_data >= end_index_in_all_data:
- self.logger.info(f"Berechneter Startindex ({start_index_in_all_data}) liegt bei oder nach dem berechneten Endindex ({end_index_in_all_data}). Keine Zeilen im definierten Bereich zu verarbeiten.")
+ self.logger.info(f"Berechneter Startindex ({start_index_in_all_data}) liegt bei oder nach dem berechneten Endindex ({end_index_in_all_data}). Keine Zeilen im definierten Bereich zu verarbeiten.") # <<< GEÄNDERT
return # Beende die Methode, wenn der Bereich leer ist
@@ -4706,7 +4506,7 @@ class DataProcessor:
# Ueberspringen Sie Header-Zeilen explizit, falls der Startindex faelschlicherweise <= header_rows war
if row_num_in_sheet <= header_rows:
- self.logger.debug(f"Ueberspringe Header-Zeile {row_num_in_sheet}.")
+ self.logger.debug(f"Ueberspringe Header-Zeile {row_num_in_sheet}.") # <<< GEÄNDERT
continue # Springe zur naechsten Iteration
@@ -4714,7 +4514,7 @@ class DataProcessor:
# Nutzt die interne Helferfunktion _get_cell_value_safe implizit durch Iteration oder prueft direkt
# Eine einfache Pruefung: Ist irgendeine Zelle in der Zeile nicht leer oder None?
if not any(cell and isinstance(cell, str) and cell.strip() for cell in row_data):
- self.logger.debug(f"Ueberspringe scheinbar leere Zeile {row_num_in_sheet}.")
+ self.logger.debug(f"Ueberspringe scheinbar leere Zeile {row_num_in_sheet}.") # <<< GEÄNDERT
continue # Springe zur naechsten Iteration
@@ -4737,7 +4537,7 @@ class DataProcessor:
except Exception as e_proc:
# Wenn _process_single_row einen Fehler wirft (nachdem interne Retries aufgaben),
# fangen wir ihn hier, loggen ihn und fahren mit der naechsten Zeile fort.
- self.logger.exception(f"FEHLER bei sequentieller Verarbeitung von Zeile {row_num_in_sheet}: {e_proc}")
+ self.logger.exception(f"FEHLER bei sequentieller Verarbeitung von Zeile {row_num_in_sheet}: {e_proc}") # <<< GEÄNDERT
# Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte im Sheet schreiben lassen.
# Dieses Update muesste dann separat oder im naechsten Lauf behandelt werden.
@@ -4747,7 +4547,7 @@ class DataProcessor:
# time.sleep(0.1) # Optional: Kurze Pause bei Fehler
# Logge den Abschluss der sequentiellen Verarbeitung
- self.logger.info(f"Sequentielle Verarbeitung abgeschlossen. {processed_count} Zeilen im Bereich [{start_sheet_row}, {end_index_in_all_data}] bearbeitet.")
+ self.logger.info(f"Sequentielle Verarbeitung abgeschlossen. {processed_count} Zeilen im Bereich [{start_sheet_row}, {end_index_in_all_data}] bearbeitet.") # <<< GEÄNDERT
# ==============================================================================
@@ -4776,7 +4576,7 @@ class DataProcessor:
Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind.
Ruft _process_single_row fuer jede dieser Zeilen auf mit force_reeval=True.
Verarbeitet maximal row_limit Zeilen.
- Loescht optional das 'x'-Flag nach erfolgreicher Verarbeitung.
+ Loescht optional das 'x'-Flag nach erfolgreicher Verarbeitung (innerhalb von _process_single_row).
Erlaubt die Auswahl spezifischer Verarbeitungsschritte.
Args:
@@ -4792,7 +4592,7 @@ class DataProcessor:
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
# Logge die Konfiguration des Re-Eval Laufs
- self.logger.info(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}")
+ self.logger.info(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}") # <<< GEÄNDERT
# Logge die ausgewaehlten Schritte fuer diesen Lauf
selected_steps_log = []
if process_wiki_steps: selected_steps_log.append("Wiki (wiki)")
@@ -4800,7 +4600,8 @@ class DataProcessor:
if process_website_steps: selected_steps_log.append("Website (web)")
if process_ml_steps: selected_steps_log.append("ML Predict (ml_predict)") # Neues Flag
# Fuegen Sie hier weitere Schritte hinzu, wenn neue Flags existieren
- self.logger.info(f"Ausgewaehlte Schritte fuer Re-Eval: {', '.join(selected_steps_log) if selected_steps_log else 'Keine ausgewählt!'}")
+ self.logger.info(f"Ausgewaehlte Schritte fuer Re-Eval: {', '.join(selected_steps_log) if selected_steps_log else 'Keine ausgewählt!'}") # <<< GEÄNDERT
+
# Erstelle das Set der Schluessel fuer die Schritte, die an _process_single_row uebergeben werden
steps_to_run_set = set()
@@ -4813,14 +4614,14 @@ class DataProcessor:
# Wenn keine Schritte ausgewaehlt wurden
if not steps_to_run_set:
- self.logger.warning("Keine Verarbeitungsschritte fuer Re-Eval ausgewaehlt (steps_to_run_set ist leer). Modus wird uebersprungen.")
+ self.logger.warning("Keine Verarbeitungsschritte fuer Re-Eval ausgewaehlt (steps_to_run_set ist leer). Modus wird uebersprungen.") # <<< GEÄNDERT
return
# Daten neu laden vor der Verarbeitung (nutzt die uebergeordnete Instanz)
# Der load_data Aufruf ist mit retry_on_failure dekoriert.
if not self.sheet_handler.load_data():
- self.logger.error("Fehler beim Laden der Daten fuer Re-Evaluation.")
+ self.logger.error("Fehler beim Laden der Daten fuer Re-Evaluation.") # <<< GEÄNDERT
return # Beende die Methode, wenn das Laden fehlschlaegt
@@ -4830,14 +4631,14 @@ class DataProcessor:
header_rows = self.sheet_handler._header_rows
# Wenn keine Daten da sind oder nur Header
if not all_data or len(all_data) <= header_rows:
- self.logger.warning("Keine Datenzeilen fuer Re-Evaluation gefunden.")
+ self.logger.warning("Keine Datenzeilen fuer Re-Evaluation gefunden.") # <<< GEÄNDERT
return # Beende die Methode
# Ermitteln Sie den Index der ReEval Flag Spalte aus COLUMN_MAP (Block 1)
reeval_col_idx = COLUMN_MAP.get("ReEval Flag")
if reeval_col_idx is None:
- self.logger.critical("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden. Kann Zeilen mit 'x' nicht finden. Breche ab.")
+ self.logger.critical("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden. Kann Zeilen mit 'x' nicht finden. Breche ab.") # <<< GEÄNDERT
return # Beende die Methode bei kritischem Fehler
@@ -4858,38 +4659,38 @@ class DataProcessor:
found_count = len(rows_to_process) # Anzahl der gefundenen markierten Zeilen
- self.logger.info(f"{found_count} Zeilen mit ReEval-Flag 'x' gefunden.")
+ self.logger.info(f"{found_count} Zeilen mit ReEval-Flag 'x' gefunden.") # <<< GEÄNDERT
# Wenn keine Zeilen zum Verarbeiten markiert sind
if found_count == 0:
- self.logger.info("Keine Zeilen zur Re-Evaluation markiert.")
+ self.logger.info("Keine Zeilen zur Re-Evaluation markiert.") # <<< GEÄNDERT
return # Beende die Methode
# Verarbeitung der markierten Zeilen
processed_count = 0 # Zaehlt Zeilen, fuer die _process_single_row aufgerufen wurde (im Rahmen des Limits).
- # updates_clear_flag Liste wird NICHT mehr hier gefuellt, da _process_single_row das Update selbst hinzufuegt.
- # rows_actually_processed = [] # Diese Liste wird nicht mehr benoetigt, da _process_single_row das Update selbst sendet.
+ # Die Liste updates_clear_flag wird NICHT mehr hier gefuellt, da _process_single_row das Update selbst hinzufuegt (Block 21).
+ # Die Liste rows_actually_processed wird nicht mehr benoetigt.
# Iteriere ueber die gefundenen markierten Zeilen
for task in rows_to_process:
# Ueberpruefen Sie das Limit fuer die zu verarbeitenden Zeilen VOR der Verarbeitung
if row_limit is not None and isinstance(row_limit, int) and row_limit > 0 and processed_count >= row_limit:
# Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Zeilenlimit ({row_limit}) fuer Re-Evaluation erreicht. Breche weitere Verarbeitung ab.")
+ self.logger.info(f"Zeilenlimit ({row_limit}) fuer Re-Evaluation erreicht. Breche weitere Verarbeitung ab.") # <<< GEÄNDERT
break # Brich die Schleife ab
row_num = task['row_num'] # 1-basierte Zeilennummer
row_data = task['data'] # Die Rohdaten fuer diese Zeile
- self.logger.info(f"Bearbeite Re-Eval Zeile {row_num}...")
+ self.logger.info(f"Bearbeite Re-Eval Zeile {row_num}...") # <<< GEÄNDERT
try:
- # Rufe die Methode zur Verarbeitung einer einzelnen Zeile auf.
+ # Rufe die Methode zur Verarbeitung einer einzelnen Zeile auf (_process_single_row Block 19).
# In diesem Modus setzen wir immer force_reeval=True.
# Wir uebergeben die aus CLI/Menue ausgewaehlten Schritte in steps_to_run_set.
# Wir uebergeben das clear_flag, damit _process_single_row weiss, ob das 'x' geloescht werden soll.
- # _process_single_row (Block 19) loggt intern, fuehrt die Schritte durch, sammelt Updates
- # (inkl. 'x'-Flag Update wenn clear_x_flag=True) und sendet das Batch-Update fuer diese Zeile.
+ # _process_single_row fuehrt die Schritte durch, sammelt Updates (inkl. 'x'-Flag Update wenn clear_x_flag=True)
+ # und sendet das Batch-Update fuer diese Zeile.
self._process_single_row(
row_num_in_sheet = row_num,
row_data = row_data,
@@ -4906,7 +4707,7 @@ class DataProcessor:
# Wenn _process_single_row einen Fehler wirft (nachdem interne Retries aufgaben),
# fangen wir ihn hier, loggen ihn und fahren mit der naechsten Zeile fort.
# Das 'x'-Flag wird in diesem Fall NICHT geloescht, da _process_single_row nicht bis zum Ende kam.
- self.logger.exception(f"FEHLER bei Re-Evaluation von Zeile {row_num}: {e_proc}")
+ self.logger.exception(f"FEHLER bei Re-Evaluation von Zeile {row_num}: {e_proc}") # <<< GEÄNDERT
# Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte im Sheet schreiben lassen.
# Dieses Update muesste dann separat oder im naechsten Lauf behandelt werden.
@@ -4915,10 +4716,11 @@ class DataProcessor:
# Wenn _process_single_row eine Exception wirft, kann hier eine kurze Pause sinnvoll sein.
# time.sleep(0.1) # Optional: Kurze Pause bei Fehler nach Exception
+
# Der Codeblock zum Loeschen der gesammelten Updates (updates_clear_flag) am Ende wurde entfernt.
# Logge den Abschluss des Re-Eval Modus
- self.logger.info(f"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Gefunden: {found_count}, Limit: {row_limit}).")
+ self.logger.info(f"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Gefunden: {found_count}, Limit: {row_limit}).") # <<< GEÄNDERT
# ==============================================================================
@@ -4931,8 +4733,8 @@ class DataProcessor:
# --- Interne Hilfsfunktion fuer Wiki-Verifizierungs-Batch (OpenAI Call) ---
# Diese Funktion verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen ueber OpenAI.
- # Sie wird von process_verification_batch aufgerufen.
- # Nutzt globale Helfer: call_openai_chat, logger, token_count (optional), retry_on_failure, re.
+ # Sie wird von process_verification_batch (dieselben Block) aufgerufen.
+ # Nutzt globale Helfer: call_openai_chat (Block 8), logger, token_count (optional Block 3), retry_on_failure (Block 2), re.
@retry_on_failure # Wende den Decorator auf den gesamten Batch-API Call an
def _process_verification_openai_batch(self, batch_data):
"""
@@ -4954,9 +4756,11 @@ class DataProcessor:
if not batch_data:
return {} # Gebe leeres Dictionary zurueck, wenn keine Tasks da sind
- self.logger.debug(f"Sende OpenAI-Batch fuer Wiki-Verifizierung ({len(batch_data)} Zeilen, Start: {batch_data[0]['row_num']})...")
+ self.logger.debug(f"Sende OpenAI-Batch fuer Wiki-Verifizierung ({len(batch_data)} Zeilen, Start: {batch_data[0]['row_num'] if batch_data else 'N/A'})...") # <<< GEÄNDERT
# --- Prompt Erstellung ---
+ # Verwenden Sie klare Anweisungen und das definierte Antwortformat.
+ # Vermeiden Sie Umlaute im Prompt, um Encoding-Probleme zu minimieren.
aggregated_prompt = (
"Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln fuer Unternehmen. "
"Fuer jeden der folgenden Eintraege pruefe, ob der vorhandene Wikipedia-Artikel (URL, Absatz, Kategorien) plausibel zum Firmennamen und zur Beschreibung passt. "
@@ -4973,49 +4777,60 @@ class DataProcessor:
"--------------------\n"
)
- # Fuegen Sie die Daten fuer jeden Eintrag im Batch hinzu
+ # Fuegen Sie die Daten fuer jeden Eintrag im Batch hinzu.
+ # Kuerzen Sie die Beschreibungen und Kategorien, um das Prompt-Limit zu reduzieren.
+ # Stellen Sie sicher, dass die Werte Strings sind und "k.A." richtig behandelt wird.
+ max_desc_length = 200 # Maximale Laenge fuer Beschreibungsteile im Prompt
for item in batch_data:
row_num = item['row_num']
- # Kuerze die Beschreibungen und Kategorien, um das Prompt-Limit zu reduzieren.
- # Stelle sicher, dass die Werte Strings sind.
- crm_desc_short = str(item.get('crm_desc', 'k.A.' or 'k.A.'))[:200] + '...' if len(str(item.get('crm_desc', ''))) > 200 else str(item.get('crm_desc', 'k.A.'))
- wiki_paragraph_short = str(item.get('wiki_paragraph', 'k.A.' or 'k.A.'))[:200] + '...' if len(str(item.get('wiki_paragraph', ''))) > 200 else str(item.get('wiki_paragraph', 'k.A.'))
- wiki_categories_short = str(item.get('wiki_categories', 'k.A.' or 'k.A.'))[:200] + '...' if len(str(item.get('wiki_categories', ''))) > 200 else str(item.get('wiki_categories', 'k.A.'))
+ # Holen und Kuerzen Sie die Werte sicher. Ersetzen Sie None durch "k.A.".
+ company_name = str(item.get('company_name', 'k.A.'))
+ crm_desc = str(item.get('crm_desc', 'k.A.'))
+ wiki_url = str(item.get('wiki_url', 'k.A.'))
+ wiki_paragraph = str(item.get('wiki_paragraph', 'k.A.'))
+ wiki_categories = str(item.get('wiki_categories', 'k.A.'))
+
+ # Kuerzen Sie die Laengen und fuegen Sie "..." hinzu, wenn gekuerzt wurde.
+ crm_desc_short = crm_desc[:max_desc_length] + '...' if len(crm_desc) > max_desc_length else crm_desc
+ wiki_paragraph_short = wiki_paragraph[:max_desc_length] + '...' if len(wiki_paragraph) > max_desc_length else wiki_paragraph
+ wiki_categories_short = wiki_categories[:max_desc_length] + '...' if len(wiki_categories) > max_desc_length else wiki_categories
entry_text = (
f"Eintrag {row_num}:\n"
- f" Firmenname: {str(item.get('company_name', 'k.A.'))}\n"
+ f" Firmenname: {company_name}\n"
f" CRM-Beschreibung: {crm_desc_short}\n"
- f" Wikipedia-URL: {str(item.get('wiki_url', 'k.A.' or 'k.A.'))}\n"
+ f" Wikipedia-URL: {wiki_url}\n"
f" Wiki-Absatz: {wiki_paragraph_short}\n"
f" Wiki-Kategorien: {wiki_categories_short}\n"
f"----\n"
)
aggregated_prompt += entry_text
+
+ # Fuegen Sie den Abschluss des Prompts hinzu.
aggregated_prompt += "--------------------\nBitte nur die 'Eintrag X: Antwort'-Zeilen ausgeben."
- # Optional: Token zaehlen fuer den Prompt
- # try: prompt_tokens = token_count(aggregated_prompt, model=getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')); logger.debug(f"Geschaetzt Prompt-Tokens fuer Batch: {prompt_tokens}.");
- # except Exception as e_tc: logger.debug(f"Fehler beim Token-Zaehlen: {e_tc}");
+ # Optional: Token zaehlen fuer den Prompt.
+ # try: prompt_tokens = token_count(aggregated_prompt, model=getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')); self.logger.debug(f"Geschaetzt Prompt-Tokens fuer Batch: {prompt_tokens}.");
+ # except Exception as e_tc: self.logger.debug(f"Fehler beim Token-Zaehlen: {e_tc}");
# --- ChatGPT Aufruf ---
# call_openai_chat (Block 8) nutzt den retry_on_failure Decorator und wirft bei endgueltigem Fehler eine Exception.
- # Der retry_on_failure Decorator auf dieser summarize_batch_openai Funktion (Block 9) faengt die Exception
+ # Der retry_on_failure Decorator auf dieser Funktion faengt die Exception
# von call_openai_chat und fuehrt die Retries fuer die GESAMTE Batch-Funktion durch.
chat_response = None
try:
# Rufe die zentrale OpenAI Chat API Funktion auf (Block 8).
- # Standard Temperatur 0.0 fuer Klassifizierung.
+ # Standard Temperatur 0.0 fuer Klassifizierung/Verifizierung.
chat_response = call_openai_chat(aggregated_prompt, temperature=0.0)
# Wenn call_openai_chat erfolgreich ist, gibt es den String zurueck.
# Exceptions werden nach Retries von call_openai_chat geworfen und vom aeusseren retry_on_failure dieser Funktion gefangen.
if not chat_response:
# Dieser Fall sollte nach der Aenderung in call_openai_chat (wirft Exception) nicht mehr auftreten.
- logger.error("call_openai_chat gab unerwarteterweise None zurueck fuer Wiki-Verifizierungs-Batch.")
+ self.logger.error("call_openai_chat gab unerwarteterweise None zurueck fuer Wiki-Verifizierungs-Batch.") # <<< GEÄNDERT
# Werfen Sie eine spezifische Exception, damit der aeussere Decorator sie faengt.
raise openai.error.APIError("Keine Antwort von OpenAI erhalten fuer Wiki-Verifizierungs-Batch.")
@@ -5023,15 +4838,17 @@ class DataProcessor:
except Exception as e:
# Wenn call_openai_chat oder der aeussere retry_on_failure eine Exception wirft (nach Retries)
# Die Exception wird hier gefangen, bevor sie an den Aufrufer (process_verification_batch) weitergeleitet wird.
- logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Wiki-Verifizierung (innerhalb Batch Decorator): {e}")
+ self.logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Wiki-Verifizierung (innerhalb Batch Decorator): {e}") # <<< GEÄNDERT
# Logge den Traceback
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
# Geben Sie ein Dictionary zurueck, das signalisiert, dass fuer alle Zeilen im Batch ein Fehler aufgetreten ist
return {item['row_num']: f"FEHLER API: {str(e)[:100]}" for item in batch_data}
# --- Antwort parsen ---
answers = {} # Initialisieren Sie das Ergebnis-Dictionary
+ # Liste der Zeilennummern, die im ursprünglichen Batch angefragt wurden
+ original_batch_row_nums = {item['row_num'] for item in batch_data}
lines = chat_response.strip().split('\n')
parsed_count = 0
for line in lines:
@@ -5040,23 +4857,23 @@ class DataProcessor:
if match:
row_num = int(match.group(1))
answer_text = match.group(2).strip()
- # Pruefen Sie, ob die Zeilennummer im urspruenglichen Batch enthalten war
- if any(item['row_num'] == row_num for item in batch_data):
+ # Pruefen Sie, ob die Zeilennummer im urspruenglichen Batch angefragt wurde
+ if row_num in original_batch_row_nums:
answers[row_num] = answer_text
parsed_count += 1
- # else: logger.debug(f"Warnung: Antwort fuer unerwartete Zeilennummer {row_num} im Batch erhalten: {answer_text[:100]}...") # Zu viel Laerm (gekuerzt loggen)
+ # else: self.logger.debug(f"Warnung: Antwort fuer unerwartete Zeilennummer {row_num} im Batch erhalten: {answer_text[:100]}...") # Zu viel Laerm (gekuerzt loggen)
# Logge das Ergebnis des Parsens
- self.logger.debug(f"OpenAI-Batch-Antwort geparst: {parsed_count} von {len(batch_data)} Zeilen erfolgreich zugeordnet.")
+ self.logger.debug(f"OpenAI-Batch-Antwort geparst: {parsed_count} von {len(original_batch_row_nums)} Zeilen erfolgreich zugeordnet.") # <<< GEÄNDERT
# Fuegen Sie einen Fehlerwert fuer Zeilen hinzu, die nicht geparst werden konnten (z.B. falsches Antwortformat)
- if parsed_count < len(batch_data):
- logger.warning(f"Nicht alle Zeilen aus dem Batch ({len(batch_data)}) konnten in der OpenAI-Antwort ({len(lines)} Zeilen) geparst werden.")
+ if parsed_count < len(original_batch_row_nums):
+ self.logger.warning(f"Nicht alle Zeilen aus dem Batch ({len(original_batch_row_nums)}) konnten in der OpenAI-Antwort ({len(lines)} Zeilen) geparst werden.") # <<< GEÄNDERT
# Logge den Anfang der unvollstaendigen Antwort auf Debug
- logger.debug(f"Unerwartete Antwortteile (erste 500 Zeichen): {chat_response[:500]}")
- for item in batch_data:
- if item['row_num'] not in answers:
- answers[item['row_num']] = "FEHLER: Antwort nicht geparst"
+ self.logger.debug(f"Unerwartete Antwortteile (erste 500 Zeichen): {chat_response[:500]}") # <<< GEÄNDERT
+ for row_num in original_batch_row_nums:
+ if row_num not in answers:
+ answers[row_num] = "FEHLER: Antwort nicht geparst"
# Die 'answers' Dictionary enthaelt nun Ergebnisse fuer alle Zeilen, entweder geparst oder mit einem Fehlerstring.
@@ -5067,9 +4884,9 @@ class DataProcessor:
# Diese Methode koordiniert die Auswahl der Zeilen, die Batch-Verarbeitung durch OpenAI,
# und das Schreiben der Ergebnisse (S, T, U, V-Y, AX, AP) ins Sheet.
# Basierend auf process_verification_only und _process_batch aus Teil 8.
- # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter, _process_verification_openai_batch.
- # Nutzt globale Helfer: COLUMN_MAP, logger, Config, datetime, time.
- # Nutzt die uebergeordnete sheet_handler Instanz.
+ # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter, _process_verification_openai_batch (derselben Block).
+ # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, Config (Block 1), datetime, time.
+ # Nutzt die uebergeordnete sheet_handler Instanz (Block 14).
def process_verification_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None):
"""
Batch-Prozess nur fuer Wikipedia-Verifizierung (Spalten S-U, V-Y werden geleert).
@@ -5085,35 +4902,36 @@ class DataProcessor:
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
# Logge die Konfiguration des Batch-Laufs
- self.logger.info(f"Starte Wikipedia-Verifizierungsmodus (Batch S-U, AX). Bereich: {start_sheet_row}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
+ self.logger.info(f"Starte Wikipedia-Verifizierungsmodus (Batch S-U, AX). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT
- # --- Daten laden ---
+
+ # --- Daten laden und Startzeile ermitteln ---
# Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt
if start_sheet_row is None:
- self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AX...")
- # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AT.
+ self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AX...") # <<< GEÄNDERT
+ # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AX (Block 1 Column Map).
# Standardmaessig ab Zeile 7
- start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wiki Verif. Timestamp") # Block 1 Column Map
+ start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wiki Verif. Timestamp", min_sheet_row=7)
# Wenn get_start_row_index -1 zurueckgibt (Fehler)
if start_data_index_no_header == -1:
- self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")
+ self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT
return # Beende die Methode
- # Berechne die 1-basierte Sheet-Startzeile
+ # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index
start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut
- self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AX Zelle): {start_sheet_row}")
+ self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AX Zelle): {start_sheet_row}") # <<< GEÄNDERT
else:
# Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
- # Der load_data Aufruf ist mit retry_on_failure dekoriert.
+ # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
if not self.sheet_handler.load_data():
- self.logger.error("FEHLER beim Laden der Daten fuer process_verification_batch.")
+ self.logger.error("FEHLER beim Laden der Daten fuer process_verification_batch.") # <<< GEÄNDERT
return # Beende die Methode, wenn das Laden fehlschlaegt
- # Holen Sie die gesamte Datenliste (inklusive Header)
+ # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler.
all_data = self.sheet_handler.get_all_data_with_headers()
- # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar.
+ # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14).
header_rows = self.sheet_handler._header_rows
total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet
@@ -5122,11 +4940,11 @@ class DataProcessor:
end_sheet_row = total_sheet_rows # Bis zur letzten Zeile
# Logge den verarbeitungsbereich
- self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
+ self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT
- # Pruefe, ob der Bereich gueltig ist
+ # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen)
if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
- self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
+ self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT
return # Beende die Methode, wenn der Bereich leer ist
@@ -5136,8 +4954,9 @@ class DataProcessor:
"Wiki Verif. Timestamp", "Wiki URL", "Chat Wiki Konsistenzpruefung", # Pruefkriterien / Timestamp (AX, M, S)
"CRM Name", "CRM Beschreibung", "Wiki Absatz", "Wiki Kategorien", # Daten fuer Prompt (B, F, N, R)
"Chat Begruendung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", # Ergebnisspalten (T, U)
- "Begruendung bei Abweichung", "Wikipedia Timestamp", "Timestamp letzte Pruefung", # Spalten zum Leeren (V, AN, AO)
- "Version", "Wiki Verif. Timestamp", "SerpAPI Wiki Search Timestamp" # Weitere Spalten zum Leeren (AP, AX - aber AX wird gesetzt!, AY)
+ "Begruendung bei Abweichung", "Chat Begruendung Abweichung Branche", # Spalten V-Y zum Leeren
+ "Wikipedia Timestamp", "Timestamp letzte Pruefung", # Spalten AN, AO zum Leeren
+ "Version", "SerpAPI Wiki Search Timestamp" # Spalten AP, AY zum Leeren
]
# Erstellen Sie ein Dictionary mit Schluesseln und Indizes
col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}
@@ -5145,11 +4964,11 @@ class DataProcessor:
# Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden
if None in col_indices.values():
missing = [k for k, v in col_indices.items() if v is None]
- self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_verification_batch: {missing}. Breche ab.")
+ self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_verification_batch: {missing}. Breche ab.") # <<< GEÄNDERT
return # Beende die Methode bei kritischem Fehler
- # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt interne Helfer)
+ # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt interne Helfer _get_col_letter Block 14)
ts_ax_letter = self.sheet_handler._get_col_letter(col_indices["Wiki Verif. Timestamp"] + 1) # Timestamp zu setzen (AX)
s_letter = self.sheet_handler._get_col_letter(col_indices["Chat Wiki Konsistenzpruefung"] + 1) # Status S
t_letter = self.sheet_handler._get_col_letter(col_indices["Chat Begruendung Wiki Inkonsistenz"] + 1) # Begruendung T
@@ -5168,8 +4987,9 @@ class DataProcessor:
empty_vy_values = [''] * (y_idx - v_idx + 1) # Anzahl der Spalten = Y_Index - V_Index + 1
- # Timestamps AN, AO, AY und Version AP leeren.
- # Diese werden von anderen Schritten gesetzt und sollen hier zurueckgesetzt werden.
+ # Timestamps AN, AO, AP, AY leeren.
+ # Diese werden von anderen Schritten gesetzt und sollen hier zurueckgesetzt werden,
+ # um sicherzustellen, dass die Zeile bei Bedarf von diesen anderen Schritten erneut bearbeitet wird.
an_letter = self.sheet_handler._get_col_letter(col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS)
ao_letter = self.sheet_handler._get_col_letter(col_indices["Timestamp letzte Pruefung"] + 1) # AO (Chat Evaluation TS)
ap_letter = self.sheet_handler._get_col_letter(col_indices["Version"] + 1) # AP (Version)
@@ -5207,7 +5027,7 @@ class DataProcessor:
# Nutzt interne Helfer _get_cell_value_safe
company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map
if not company_name:
- self.logger.debug(f"Zeile {i}: Uebersprungen (Kein Firmenname in Spalte B).")
+ self.logger.debug(f"Zeile {i}: Uebersprungen (Kein Firmenname in Spalte B).") # <<< GEÄNDERT
skipped_count += 1 # Zaehlen als uebersprungen
continue # Springe zur naechsten Zeile
@@ -5222,13 +5042,14 @@ class DataProcessor:
s_value_upper = self._get_cell_value_safe(row, "Chat Wiki Konsistenzpruefung").strip().upper() # Block 1 Column Map
# Pruefen Sie, ob die Wiki URL (M) gueltig aussieht
- is_wiki_url_valid_looking = m_value and isinstance(m_value, str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu
+ is_wiki_url_valid_looking = m_value and isinstance(m_value, str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu basierend auf Log
# Definieren Sie die Endzustaende von Status S (Grossbuchstaben)
s_end_states = ["OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID SUGGESTION)"]
# Pruefen Sie, ob Status S in einem Endzustand ist
- is_s_in_endstate = s_value_upper in s_end_states
+ is_s_in_endstate = s_value_upper in s_end_states # Bugfix: Korrekte Zuweisung
+
# Verarbeitung ist noetig, wenn AX leer UND M gefuellt/gueltig aussieht UND S NICHT im Endzustand ist.
processing_needed_for_row = not ax_value and is_wiki_url_valid_looking and not is_s_in_endstate
@@ -5237,13 +5058,13 @@ class DataProcessor:
# Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level
log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row)
if log_check:
- self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Verif. Check): AX leer? {not ax_value}, M gueltig? {is_wiki_url_valid_looking}, S ('{s_value_upper}') Endzustand? {is_s_in_endstate}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen
+ self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Verif. Check): AX leer? {not ax_value}, M gueltig? {is_wiki_url_valid_looking}, S ('{s_value_upper}') Endzustand? {is_s_in_endstate}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT
# Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
if not processing_needed_for_row:
skipped_count += 1 # Zaehlen als uebersprungene Zeile
- # Zaehlen Sie separat, wenn die Zeile wegen fehlender M-URL uebersprungen wurde
+ # Zaehlen Sie separat, wenn die Zeile speziell wegen fehlender M-URL uebersprungen wurde
if not is_wiki_url_valid_looking: skipped_no_wiki_url += 1
continue # Springe zur naechsten Zeile
@@ -5254,7 +5075,7 @@ class DataProcessor:
# Pruefe das Limit fuer verarbeitete Zeilen
if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit:
# Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_verification_batch erreicht. Breche weitere Zeilenpruefung ab.")
+ self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_verification_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT
break # Brich die Schleife ab
@@ -5285,7 +5106,7 @@ class DataProcessor:
# Logge den Start der Batch-Verarbeitung
batch_start_row = current_openai_batch_data[0]['row_num']
batch_end_row = current_openai_batch_data[-1]['row_num']
- self.logger.debug(f"\n--- Starte Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")
+ self.logger.debug(f"\n--- Starte Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT
# Rufe die interne Methode auf, die den OpenAI Call fuer den Batch macht.
@@ -5376,12 +5197,12 @@ class DataProcessor:
# Sende die gesammelten Updates fuer DIESEN Batch sofort.
if batch_sheet_updates:
- self.logger.debug(f" Sende Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...")
+ self.logger.debug(f" Sende Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
# Wenn es fehlschlaegt, wird es intern geloggt.
success = self.sheet_handler.batch_update_cells(batch_sheet_updates)
if success:
- self.logger.info(f" Sheet-Update fuer Wiki-Verifizierungs-Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.")
+ self.logger.info(f" Sheet-Update fuer Wiki-Verifizierungs-Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# Setze Batch-Listen zurueck fuer die naechste Iteration
@@ -5392,7 +5213,7 @@ class DataProcessor:
# Dies ist wichtig, um Rate Limits zu vermeiden.
# Nutze Config.RETRY_DELAY, ggf. kuerzer, da es ein Batch war
pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.5 # 50% der Retry-Wartezeit
- self.logger.debug(f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---")
+ self.logger.debug(f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---") # <<< GEÄNDERT
time.sleep(pause_duration)
@@ -5402,7 +5223,7 @@ class DataProcessor:
# Logge den Start des finalen Batches
batch_start_row = current_openai_batch_data[0]['row_num']
batch_end_row = current_openai_batch_data[-1]['row_num']
- self.logger.debug(f"\n--- Starte FINALEN Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")
+ self.logger.debug(f"\n--- Starte FINALEN Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT
# Rufe die interne Methode auf, die den OpenAI Call macht
batch_results = self._process_verification_openai_batch(current_openai_batch_data)
@@ -5445,1315 +5266,85 @@ class DataProcessor:
# Sende die gesammelten Updates fuer DIESEN finalen Batch.
if batch_sheet_updates:
- self.logger.debug(f" Sende FINALES Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen)...")
+ self.logger.debug(f" Sende FINALES Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
success = self.sheet_handler.batch_update_cells(batch_sheet_updates)
if success:
- self.logger.info(f" FINALES Sheet-Update fuer Wiki-Verifizierungs-Batch erfolgreich.")
+ self.logger.info(f" FINALES Sheet-Update fuer Wiki-Verifizierungs-Batch erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# Logge den Abschluss des Modus
- self.logger.info(f"Wikipedia-Verifizierungs-Batch abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen ({skipped_no_wiki_url} wegen fehlender M-URL).")
+ self.logger.info(f"Wikipedia-Verifizierungs-Batch abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen ({skipped_no_wiki_url} wegen fehlender M-URL).") # <<< GEÄNDERT
# Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.
# ==============================================================================
# Ende DataProcessor Klasse Batch: Wiki Verification Block
-# ==============================================================================
-
- # ==========================================================================
- # === Prozess Methoden (Re-Evaluation) =====================================
- # ==========================================================================
-
- # --- Methode fuer den Re-Eval Modus (Spalte A = 'x') ---
- # Diese Methode verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind.
- # Sie ruft _process_single_row fuer jede dieser Zeilen auf mit force_reeval=True
- # und uebergibt die Auswahl der Schritte und das Flag zum Loeschen des 'x'-Flags.
- # Nutzt interne Helfer: _process_single_row, _get_cell_value_safe.
- # Nutzt globale Helfer: COLUMN_MAP, logger.
- # Nutzt die uebergeordnete sheet_handler Instanz.
- def process_reevaluation_rows(self, row_limit=None, clear_flag=True,
- process_wiki_steps=True,
- process_chatgpt_steps=True,
- process_website_steps=True,
- process_ml_steps=True # Neues Flag fuer ML-Schritt
- # Fuegen Sie hier ggf. weitere boolsche Flags fuer andere Schrittgruppen hinzu
- ):
- """
- Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind.
- Ruft _process_single_row fuer jede dieser Zeilen auf mit force_reeval=True.
- Verarbeitet maximal row_limit Zeilen.
- Loescht optional das 'x'-Flag nach erfolgreicher Verarbeitung (innerhalb von _process_single_row).
- Erlaubt die Auswahl spezifischer Verarbeitungsschritte.
-
- Args:
- row_limit (int, optional): Maximale Anzahl zu verarbeitender Zeilen. Defaults to None (Unbegrenzt).
- clear_flag (bool, optional): Wenn True, wird das Flag 'x' in Spalte A
- nach erfolgreicher Verarbeitung durch _process_single_row geloescht.
- Defaults to True.
- process_wiki_steps (bool, optional): Soll der Wiki-Schritt in _process_single_row ausgefuehrt werden?. Defaults to True.
- process_chatgpt_steps (bool, optional): Sollen ChatGPT-Schritte in _process_single_row ausgefuehrt werden?. Defaults to True.
- process_website_steps (bool, optional): Soll der Website-Schritt in _process_single_row ausgefuehrt werden?. Defaults to True.
- process_ml_steps (bool, optional): Soll der ML-Schritt in _process_single_row ausgefuehrt werden?. Defaults to True. # Neues Flag
- # Fuegen Sie hier ggf. weitere boolsche Flags fuer andere Schrittgruppen hinzu.
- """
- # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
- # Logge die Konfiguration des Re-Eval Laufs
- self.logger.info(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}")
- # Logge die ausgewaehlten Schritte fuer diesen Lauf
- selected_steps_log = []
- if process_wiki_steps: selected_steps_log.append("Wiki (wiki)")
- if process_chatgpt_steps: selected_steps_log.append("ChatGPT (chat)")
- if process_website_steps: selected_steps_log.append("Website (web)")
- if process_ml_steps: selected_steps_log.append("ML Predict (ml_predict)") # Neues Flag
- # Fuegen Sie hier weitere Schritte hinzu, wenn neue Flags existieren
- self.logger.info(f" Ausgewaehlte Schritte fuer Re-Eval: {', '.join(selected_steps_log) if selected_steps_log else 'Keine ausgewählt!'}")
-
-
- # Erstelle das Set der Schluessel fuer die Schritte, die an _process_single_row uebergeben werden
- steps_to_run_set = set()
- if process_wiki_steps: steps_to_run_set.add('wiki')
- if process_chatgpt_steps: steps_to_run_set.add('chat') # Annahme: 'chat' triggert alle ChatGPT Schritte in _process_single_row (Block 20)
- if process_website_steps: steps_to_run_set.add('web')
- if process_ml_steps: steps_to_run_set.add('ml_predict') # Neues Flag
- # Fuegen Sie hier weitere Schluessel hinzu, wenn neue Flags verwendet werden
-
-
- # Wenn keine Schritte ausgewaehlt wurden
- if not steps_to_run_set:
- self.logger.warning("Keine Verarbeitungsschritte fuer Re-Eval ausgewaehlt (steps_to_run_set ist leer). Modus wird uebersprungen.")
- return
-
-
- # Daten neu laden vor der Verarbeitung (nutzt die uebergeordnete Instanz)
- # Der load_data Aufruf ist mit retry_on_failure dekoriert.
- if not self.sheet_handler.load_data():
- self.logger.error("Fehler beim Laden der Daten fuer Re-Evaluation.")
- return # Beende die Methode, wenn das Laden fehlschlaegt
-
-
- # Holen Sie die gesamte Datenliste (inklusive Header)
- all_data = self.sheet_handler.get_all_data_with_headers()
- # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar.
- header_rows = self.sheet_handler._header_rows
- # Wenn keine Daten da sind oder nur Header
- if not all_data or len(all_data) <= header_rows:
- self.logger.warning("Keine Datenzeilen fuer Re-Evaluation gefunden.")
- return # Beende die Methode
-
-
- # Ermitteln Sie den Index der ReEval Flag Spalte aus COLUMN_MAP (Block 1)
- reeval_col_idx = COLUMN_MAP.get("ReEval Flag")
- if reeval_col_idx is None:
- self.logger.critical("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden. Kann Zeilen mit 'x' nicht finden. Breche ab.")
- return # Beende die Methode bei kritischem Fehler
-
-
- # Sammeln Sie die Zeilen, die in Spalte A mit 'x' markiert sind.
- rows_to_process = [] # Liste von Dictionaries {'row_num': ..., 'data': ...}
- # Iteriere ueber die Datenzeilen (ab der ersten Datenzeile)
- for idx_in_list in range(header_rows, len(all_data)):
- row_data = all_data[idx_in_list] # Die Rohdaten fuer diese Zeile (0-basierter Index in all_data)
- row_num_in_sheet = idx_in_list + 1 # 1-basierte Zeilennummer im Sheet
-
- # Pruefen Sie sicher auf den Wert 'x' in Spalte A (nutzt interne Helfer)
- cell_a_value = self._get_cell_value_safe(row_data, "ReEval Flag").strip().lower()
-
- # Wenn die Zelle in Spalte A "x" ist
- if cell_a_value == "x":
- # Fuegen Sie die Zeilendaten zur Liste der zu verarbeitenden Zeilen hinzu
- rows_to_process.append({'row_num': row_num_in_sheet, 'data': row_data})
-
-
- found_count = len(rows_to_process) # Anzahl der gefundenen markierten Zeilen
- self.logger.info(f"{found_count} Zeilen mit ReEval-Flag 'x' gefunden.")
- # Wenn keine Zeilen zum Verarbeiten markiert sind
- if found_count == 0:
- self.logger.info("Keine Zeilen zur Re-Evaluation markiert.")
- return # Beende die Methode
-
-
- # Verarbeitung der markierten Zeilen
- processed_count = 0 # Zaehlt Zeilen, fuer die _process_single_row aufgerufen wurde (im Rahmen des Limits).
- # Die Liste updates_clear_flag wird NICHT mehr hier gefuellt, da _process_single_row das Update selbst hinzufuegt (Block 21).
- # Die Liste rows_actually_processed wird nicht mehr benoetigt.
-
- # Iteriere ueber die gefundenen markierten Zeilen
- for task in rows_to_process:
- # Ueberpruefen Sie das Limit fuer die zu verarbeitenden Zeilen VOR der Verarbeitung
- if row_limit is not None and isinstance(row_limit, int) and row_limit > 0 and processed_count >= row_limit:
- # Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Zeilenlimit ({row_limit}) fuer Re-Evaluation erreicht. Breche weitere Verarbeitung ab.")
- break # Brich die Schleife ab
-
-
- row_num = task['row_num'] # 1-basierte Zeilennummer
- row_data = task['data'] # Die Rohdaten fuer diese Zeile
-
- self.logger.info(f"Bearbeite Re-Eval Zeile {row_num}...")
- try:
- # Rufe die Methode zur Verarbeitung einer einzelnen Zeile auf (_process_single_row Block 19).
- # In diesem Modus setzen wir immer force_reeval=True.
- # Wir uebergeben die aus CLI/Menue ausgewaehlten Schritte in steps_to_run_set.
- # Wir uebergeben das clear_flag, damit _process_single_row weiss, ob das 'x' geloescht werden soll.
- # _process_single_row fuehrt die Schritte durch, sammelt Updates (inkl. 'x'-Flag Update wenn clear_x_flag=True)
- # und sendet das Batch-Update fuer diese Zeile.
- self._process_single_row(
- row_num_in_sheet = row_num,
- row_data = row_data,
- steps_to_run = steps_to_run_set, # <-- Uebergibt die aus CLI/Menue ausgewaehlten Schritte
- force_reeval = True, # <-- Erzwingt Re-Evaluation unabhaengig von Timestamps fuer die ausgewaehlten Schritte
- clear_x_flag = clear_flag # <-- Uebergibt, ob das 'x'-Flag von _process_single_row geloescht werden soll
- )
-
- # Zaehlen, wenn _process_single_row erfolgreich aufgerufen wurde (unabhaengig von internen Ueberspringungen in _process_single_row).
- processed_count += 1
- # Die Liste rows_actually_processed wird nicht mehr benoetigt.
-
- except Exception as e_proc:
- # Wenn _process_single_row einen Fehler wirft (nachdem interne Retries aufgaben),
- # fangen wir ihn hier, loggen ihn und fahren mit der naechsten Zeile fort.
- # Das 'x'-Flag wird in diesem Fall NICHT geloescht, da _process_single_row nicht bis zum Ende kam.
- self.logger.exception(f"FEHLER bei Re-Evaluation von Zeile {row_num}: {e_proc}")
- # Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte im Sheet schreiben lassen.
- # Dieses Update muesste dann separat oder im naechsten Lauf behandelt werden.
-
- # _process_single_row beinhaltet bereits eine kleine Pause am Ende.
- # Hier ist keine zusaetzliche Pause noetig nach der Zeilenverarbeitung.
- # Wenn _process_single_row eine Exception wirft, kann hier eine kurze Pause sinnvoll sein.
- # time.sleep(0.1) # Optional: Kurze Pause bei Fehler nach Exception
-
-
- # Der Codeblock zum Loeschen der gesammelten Updates (updates_clear_flag) am Ende wurde entfernt.
-
- # Logge den Abschluss des Re-Eval Modus
- self.logger.info(f"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Gefunden: {found_count}, Limit: {row_limit}).")
-
-
-# ==============================================================================
-# Ende DataProcessor Klasse Prozess: Re-Evaluation Block
# ==============================================================================
# ==========================================================================
# === Batch Processing Methods =============================================
# ==========================================================================
- # --- Interne Hilfsfunktion fuer Wiki-Verifizierungs-Batch (OpenAI Call) ---
- # Diese Funktion verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen ueber OpenAI.
- # Sie wird von process_verification_batch (dieselben Block) aufgerufen.
- # Nutzt globale Helfer: call_openai_chat (Block 8), logger, token_count (optional Block 3), retry_on_failure (Block 2), re.
- @retry_on_failure # Wende den Decorator auf den gesamten Batch-API Call an
- def _process_verification_openai_batch(self, batch_data):
+ # --- Worker Funktion für paralleles Website Scraping (intern) ---
+ # Wird von process_website_scraping_batch aufgerufen
+ def _scrape_raw_text_task(self, task_info, get_website_raw_func):
"""
- Verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen ueber OpenAI.
- Sammelt die Ergebnisse und gibt sie zurueck. Aktualisiert NICHT das Sheet direkt.
+ Scrapt den Rohtext einer Website in einem separaten Thread.
+ Wird vom ThreadPoolExecutor in process_website_scraping_batch aufgerufen.
+ Nutzt die uebergebene Funktion zum Abrufen des Rohtexts.
Args:
- batch_data (list): Liste von Dictionaries, jedes enthaelt:
- {'row_num': int, 'company_name': str, 'crm_desc': str,
- 'wiki_url': str, 'wiki_paragraph': str, 'wiki_categories': str}
+ task_info (dict): Enthält {'row_num': int, 'url': str}.
+ get_website_raw_func (function): Die Funktion zum Abrufen des Website-Rohtexts (sollte die globale get_website_raw sein).
Returns:
- dict: Ein Dictionary, das Zeilennummern auf die rohe ChatGPT-Antwort mappt.
- z.B. {2122: "OK", 2123: "X | ..."}
- Bei Fehlern oder fehlenden Antworten wird ein Fehlerstring verwendet.
- Wirft Exception bei endgueltigen API-Fehlern nach Retries.
+ dict: Enthält {'row_num': int, 'raw_text': str, 'error': str}.
"""
- # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
- if not batch_data:
- return {} # Gebe leeres Dictionary zurueck, wenn keine Tasks da sind
+ # Logger für diese Funktion holen (da sie in einem Thread läuft)
+ logger = logging.getLogger(__name__ + ".scrape_worker")
- self.logger.debug(f"Sende OpenAI-Batch fuer Wiki-Verifizierung ({len(batch_data)} Zeilen, Start: {batch_data[0]['row_num'] if batch_data else 'N/A'})...") # Sichere Indexierung
+ row_num = task_info['row_num']
+ url = task_info['url']
+ raw_text = "k.A."
+ error = None
- # --- Prompt Erstellung ---
- # Verwenden Sie klare Anweisungen und das definierte Antwortformat.
- # Vermeiden Sie Umlaute im Prompt, um Encoding-Probleme zu minimieren.
- aggregated_prompt = (
- "Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln fuer Unternehmen. "
- "Fuer jeden der folgenden Eintraege pruefe, ob der vorhandene Wikipedia-Artikel (URL, Absatz, Kategorien) plausibel zum Firmennamen und zur Beschreibung passt. "
- "Gib das Ergebnis fuer jeden Eintrag ausschliesslich im folgenden Format auf einer neuen Zeile aus:\n"
- "Eintrag : \n\n"
- "Moegliche Antworten:\n"
- "- 'OK' (wenn der Artikel gut passt)\n"
- "- 'X | Alternativer Artikel: | Begruendung: ' (wenn der Artikel nicht passt, aber ein besserer gefunden wurde)\n"
- "- 'X | Kein passender Artikel gefunden | Begruendung: ' (wenn der Artikel nicht passt und kein besserer gefunden wurde)\n"
- # Der Fall "Kein Wikipedia-Eintrag vorhanden" wird vom Skript VOR diesem Call behandelt
- # und sollte hier nicht vom KI-Modell generiert werden.
- "Stelle sicher, dass du nur EINE Zeile pro Eintrag im Format 'Eintrag X: Antwort' ausgibst.\n\n"
- "Eintraege zur Pruefung:\n"
- "--------------------\n"
- )
-
- # Fuegen Sie die Daten fuer jeden Eintrag im Batch hinzu.
- # Kuerzen Sie die Beschreibungen und Kategorien, um das Prompt-Limit zu reduzieren.
- # Stellen Sie sicher, dass die Werte Strings sind und "k.A." richtig behandelt wird.
- max_desc_length = 200 # Maximale Laenge fuer Beschreibungsteile im Prompt
- for item in batch_data:
- row_num = item['row_num']
- # Holen und Kuerzen Sie die Werte sicher. Ersetzen Sie None durch "k.A.".
- company_name = str(item.get('company_name', 'k.A.'))
- crm_desc = str(item.get('crm_desc', 'k.A.'))
- wiki_url = str(item.get('wiki_url', 'k.A.'))
- wiki_paragraph = str(item.get('wiki_paragraph', 'k.A.'))
- wiki_categories = str(item.get('wiki_categories', 'k.A.'))
-
- # Kuerzen Sie die Laengen und fuegen Sie "..." hinzu, wenn gekuerzt wurde.
- crm_desc_short = crm_desc[:max_desc_length] + '...' if len(crm_desc) > max_desc_length else crm_desc
- wiki_paragraph_short = wiki_paragraph[:max_desc_length] + '...' if len(wiki_paragraph) > max_desc_length else wiki_paragraph
- wiki_categories_short = wiki_categories[:max_desc_length] + '...' if len(wiki_categories) > max_desc_length else wiki_categories
-
-
- entry_text = (
- f"Eintrag {row_num}:\n"
- f" Firmenname: {company_name}\n"
- f" CRM-Beschreibung: {crm_desc_short}\n"
- f" Wikipedia-URL: {wiki_url}\n"
- f" Wiki-Absatz: {wiki_paragraph_short}\n"
- f" Wiki-Kategorien: {wiki_categories_short}\n"
- f"----\n"
- )
- aggregated_prompt += entry_text
-
-
- # Fuegen Sie den Abschluss des Prompts hinzu.
- aggregated_prompt += "--------------------\nBitte nur die 'Eintrag X: Antwort'-Zeilen ausgeben."
-
- # Optional: Token zaehlen fuer den Prompt.
- # try: prompt_tokens = token_count(aggregated_prompt, model=getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')); logger.debug(f"Geschaetzt Prompt-Tokens fuer Batch: {prompt_tokens}.");
- # except Exception as e_tc: logger.debug(f"Fehler beim Token-Zaehlen: {e_tc}");
-
-
- # --- ChatGPT Aufruf ---
- # call_openai_chat (Block 8) nutzt den retry_on_failure Decorator und wirft bei endgueltigem Fehler eine Exception.
- # Der retry_on_failure Decorator auf dieser summarize_batch_openai Funktion (Block 9) faengt die Exception
- # von call_openai_chat und fuehrt die Retries fuer die GESAMTE Batch-Funktion durch.
- chat_response = None
try:
- # Rufe die zentrale OpenAI Chat API Funktion auf (Block 8).
- # Standard Temperatur 0.0 fuer Klassifizierung/Verifizierung.
- chat_response = call_openai_chat(aggregated_prompt, temperature=0.0)
- # Wenn call_openai_chat erfolgreich ist, gibt es den String zurueck.
- # Exceptions werden nach Retries von call_openai_chat geworfen und vom aeusseren retry_on_failure dieser Funktion gefangen.
+ # RUFT die uebergebene Funktion zum Abrufen des Rohtexts auf.
+ # Der retry_on_failure Decorator auf get_website_raw_func (der hoffentlich get_website_raw ist)
+ # behandelt Retries und die meisten Fehler.
+ raw_text = get_website_raw_func(url) # <<< Ruft die uebergebene Funktion auf
- if not chat_response:
- # Dieser Fall sollte nach der Aenderung in call_openai_chat (wirft Exception) nicht mehr auftreten.
- logger.error("call_openai_chat gab unerwarteterweise None zurueck fuer Wiki-Verifizierungs-Batch.")
- # Werfen Sie eine spezifische Exception, damit der aeussere Decorator sie faengt.
- raise openai.error.APIError("Keine Antwort von OpenAI erhalten fuer Wiki-Verifizierungs-Batch.")
+ # Wenn die Funktion einen Fehler loggt und einen Fehlerstring im Ergebnis zurueckgibt,
+ # wird dies hier als Fehler im Task markiert.
+ if isinstance(raw_text, str) and (raw_text.startswith("k.A. (Fehler") or raw_text.startswith("FEHLER:")):
+ error = f"Scraping Fehler (Details im Rohtext): {raw_text[:100]}..."
+ # Der Fehler wurde bereits in get_website_raw geloggt, kein weiteres Logging hier noetig.
+ # Das raw_text selbst enthaelt den Fehlerstring.
+ elif not isinstance(raw_text, str) or not raw_text.strip():
+ # Wenn die Funktion keinen String oder einen leeren String zurueckgibt
+ error = "Scraping Task Fehler: Funktion gab keinen gueltigen String zurueck."
+ raw_text = "k.A. (Extraktion fehlgeschlagen)" # Standard-Fehlerwert
except Exception as e:
- # Wenn call_openai_chat oder der aeussere retry_on_failure eine Exception wirft (nach Retries)
- # Die Exception wird hier gefangen, bevor sie an den Aufrufer (process_verification_batch) weitergeleitet wird.
- logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Wiki-Verifizierung (innerhalb Batch Decorator): {e}")
- # Logge den Traceback
- self.logger.debug(traceback.format_exc())
- # Geben Sie ein Dictionary zurueck, das signalisiert, dass fuer alle Zeilen im Batch ein Fehler aufgetreten ist
- return {item['row_num']: f"FEHLER API: {str(e)[:100]}" for item in batch_data}
+ # Dieser Block sollte jetzt sehr selten erreicht werden, da die uebergegebene Funktion
+ # mit retry_on_failure die meisten Fehler abfangen sollte.
+ # Wenn eine Exception hier durchkommt, ist es ein sehr unerwarteter Fehler im Task-Handling selbst.
+ error = f"Unerwarteter Fehler im Scraping Task Zeile {row_num} ({url[:100]}): {type(e).__name__} - {e}" # Gekuerzt loggen
+ logger.error(error) # Loggen Sie diesen unerwarteten Fehler
+ raw_text = "k.A. (Unerwarteter Fehler Task)" # Setze einen spezifischen Fehlerwert
- # --- Antwort parsen ---
- answers = {} # Initialisieren Sie das Ergebnis-Dictionary
- # Liste der Zeilennummern, die im ursprünglichen Batch angefragt wurden
- original_batch_row_nums = {item['row_num'] for item in batch_data}
- lines = chat_response.strip().split('\n')
- parsed_count = 0
- for line in lines:
- # Matcht "Eintrag :" und den Rest der Zeile
- match = re.match(r"Eintrag (\d+): (.*)", line.strip())
- if match:
- row_num = int(match.group(1))
- answer_text = match.group(2).strip()
- # Pruefen Sie, ob die Zeilennummer im urspruenglichen Batch angefragt wurde
- if row_num in original_batch_row_nums:
- answers[row_num] = answer_text
- parsed_count += 1
- # else: logger.debug(f"Warnung: Antwort fuer unerwartete Zeilennummer {row_num} im Batch erhalten: {answer_text[:100]}...") # Zu viel Laerm (gekuerzt loggen)
-
- # Logge das Ergebnis des Parsens
- self.logger.debug(f"OpenAI-Batch-Antwort geparst: {parsed_count} von {len(original_batch_row_nums)} Zeilen erfolgreich zugeordnet.")
-
- # Fuegen Sie einen Fehlerwert fuer Zeilen hinzu, die nicht geparst werden konnten (z.B. falsches Antwortformat)
- if parsed_count < len(original_batch_row_nums):
- logger.warning(f"Nicht alle Zeilen aus dem Batch ({len(original_batch_row_nums)}) konnten in der OpenAI-Antwort ({len(lines)} Zeilen) geparst werden.")
- # Logge den Anfang der unvollstaendigen Antwort auf Debug
- logger.debug(f"Unerwartete Antwortteile (erste 500 Zeichen): {chat_response[:500]}")
- for row_num in original_batch_row_nums:
- if row_num not in answers:
- answers[row_num] = "FEHLER: Antwort nicht geparst"
-
-
- # Die 'answers' Dictionary enthaelt nun Ergebnisse fuer alle Zeilen, entweder geparst oder mit einem Fehlerstring.
- return answers # Rueckgabe des Dictionarys mit Ergebnissen oder Fehlern
-
-
- # --- Methode fuer den Wiki-Verifizierungs-Batchmodus (AX) ---
- # Diese Methode koordiniert die Auswahl der Zeilen, die Batch-Verarbeitung durch OpenAI,
- # und das Schreiben der Ergebnisse (S, T, U, V-Y, AX, AP) ins Sheet.
- # Basierend auf process_verification_only und _process_batch aus Teil 8.
- # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter, _process_verification_openai_batch (derselbe Block).
- # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, Config (Block 1), datetime, time.
- # Nutzt die uebergeordnete sheet_handler Instanz (Block 14).
- def process_verification_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None):
- """
- Batch-Prozess nur fuer Wikipedia-Verifizierung (Spalten S-U, V-Y werden geleert).
- Laedt Daten neu, prueft fuer jede Zeile im Bereich, ob Timestamp AX (Wiki Verif.)
- bereits gesetzt ist, ob eine Wiki URL (M) vorhanden ist und ob Status S
- nicht bereits 'OK', 'X (URL Copied)' oder 'X (Invalid Suggestion)' ist.
- Setzt AX + AP fuer bearbeitete Zeilen und schreibt S-U in Batches.
-
- Args:
- start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (automatische Ermittlung basierend auf leeren AX).
- end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet).
- limit (int, optional): Maximale Anzahl ZU VERARBEITENDER (nicht uebersprungener) Zeilen. Defaults to None (Unbegrenzt).
- """
- # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
- # Logge die Konfiguration des Batch-Laufs
- self.logger.info(f"Starte Wikipedia-Verifizierungsmodus (Batch S-U, AX). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
-
-
- # --- Daten laden und Startzeile ermitteln ---
- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt
- if start_sheet_row is None:
- self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AX...")
- # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AX (Block 1 Column Map).
- # Standardmaessig ab Zeile 7
- start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wiki Verif. Timestamp", min_sheet_row=7)
-
- # Wenn get_start_row_index -1 zurueckgibt (Fehler)
- if start_data_index_no_header == -1:
- self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")
- return # Beende die Methode
-
- # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index
- start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut
- self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AX Zelle): {start_sheet_row}")
- else:
- # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
- # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
- if not self.sheet_handler.load_data():
- self.logger.error("FEHLER beim Laden der Daten fuer process_verification_batch.")
- return # Beende die Methode, wenn das Laden fehlschlaegt
-
-
- # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler.
- all_data = self.sheet_handler.get_all_data_with_headers()
- # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14).
- header_rows = self.sheet_handler._header_rows
- total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet
-
- # Berechne Endzeile, wenn nicht manuell gesetzt
- if end_sheet_row is None:
- end_sheet_row = total_sheet_rows # Bis zur letzten Zeile
-
- # Logge den verarbeitungsbereich
- self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
-
- # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen)
- if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
- self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
- return # Beende die Methode, wenn der Bereich leer ist
-
-
- # --- Indizes und Buchstaben ---
- # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind
- required_keys = [
- "Wiki Verif. Timestamp", "Wiki URL", "Chat Wiki Konsistenzpruefung", # Pruefkriterien / Timestamp (AX, M, S)
- "CRM Name", "CRM Beschreibung", "Wiki Absatz", "Wiki Kategorien", # Daten fuer Prompt (B, F, N, R)
- "Chat Begruendung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", # Ergebnisspalten (T, U)
- "Begruendung bei Abweichung", "Chat Begruendung Abweichung Branche", # Spalten V-Y zum Leeren
- "Wikipedia Timestamp", "Timestamp letzte Pruefung", # Spalten AN, AO zum Leeren
- "Version", "SerpAPI Wiki Search Timestamp" # Spalten AP, AY zum Leeren
- ]
- # Erstellen Sie ein Dictionary mit Schluesseln und Indizes aus COLUMN_MAP.
- col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}
-
- # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden
- if None in col_indices.values():
- missing = [k for k, v in col_indices.items() if v is None]
- self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_verification_batch: {missing}. Breche ab.")
- return # Beende die Methode bei kritischem Fehler
-
-
- # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt interne Helfer _get_col_letter Block 14)
- ts_ax_letter = self.sheet_handler._get_col_letter(col_indices["Wiki Verif. Timestamp"] + 1) # Timestamp zu setzen (AX)
- s_letter = self.sheet_handler._get_col_letter(col_indices["Chat Wiki Konsistenzpruefung"] + 1) # Status S
- t_letter = self.sheet_handler._get_col_letter(col_indices["Chat Begruendung Wiki Inkonsistenz"] + 1) # Begruendung T
- u_letter = self.sheet_handler._get_col_letter(col_indices["Chat Vorschlag Wiki Artikel"] + 1) # Vorschlag U
-
- # Spalten V-Y leeren (werden in diesem Modus nicht neu befuellt).
- # V ist Begruendung bei Abweichung (von Wiki-URL Pruefung CRM vs Wiki).
- # Y ist Begruendung Abweichung Branche (von Chat).
- v_idx = col_indices["Begruendung bei Abweichung"]
- y_idx = col_indices["Chat Begruendung Abweichung Branche"]
- # Erstellen Sie den Bereichsnamen (z.B. "V:Y")
- v_letter = self.sheet_handler._get_col_letter(v_idx + 1)
- y_letter = self.sheet_handler._get_col_letter(y_idx + 1)
- v_y_range_letter = f'{v_letter}:{y_letter}' # z.B. V:Y
- # Erstellen Sie eine Liste von leeren Strings fuer diesen Bereich
- empty_vy_values = [''] * (y_idx - v_idx + 1) # Anzahl der Spalten = Y_Index - V_Index + 1
-
-
- # Timestamps AN, AO, AP, AY leeren.
- # Diese werden von anderen Schritten gesetzt und sollen hier zurueckgesetzt werden,
- # um sicherzustellen, dass die Zeile bei Bedarf von diesen anderen Schritten erneut bearbeitet wird.
- an_letter = self.sheet_handler._get_col_letter(col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS)
- ao_letter = self.sheet_handler._get_col_letter(col_indices["Timestamp letzte Pruefung"] + 1) # AO (Chat Evaluation TS)
- ap_letter = self.sheet_handler._get_col_letter(col_indices["Version"] + 1) # AP (Version)
- ay_letter = self.sheet_handler._get_col_letter(col_indices["SerpAPI Wiki Search Timestamp"] + 1) # AY (SerpAPI Wiki TS)
-
-
- # --- Verarbeitung ---
- # Holen Sie die Batch-Groesse fuer OpenAI-Aufrufe aus Config (Block 1)
- openai_batch_size = getattr(Config, 'PROCESSING_BATCH_SIZE', 20) # Nutzt dieselbe Batch-Groesse wie Scraping/Summarization
- # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1)
- update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50)
-
-
- current_openai_batch_data = [] # Daten fuer den aktuellen OpenAI Batch (Liste von Dicts)
- rows_in_current_openai_batch = [] # 1-basierte Zeilennummern im aktuellen OpenAI Batch
- all_sheet_updates = [] # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts)
-
-
- processed_count = 0 # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den Batch aufgenommen werden (im Rahmen des Limits).
- skipped_count = 0 # Zaehlt Zeilen, die uebersprungen wurden (wegen Status, fehlender Daten etc.).
- skipped_no_wiki_url = 0 # Zaehlt Zeilen, die speziell wegen fehlender M-URL uebersprungen wurden.
-
-
- # Iteriere ueber die Sheet-Zeilen im definierten Bereich (1-basierte Sheet-Zeilennummer)
- for i in range(start_sheet_row, end_sheet_row + 1):
- row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste
- # Pruefen Sie, ob das Ende des Sheets erreicht wurde
- if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht
-
-
- row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile
-
-
- # Stellen Sie sicher, dass die Zeile nicht leer ist (mindestens Name vorhanden)
- # Nutzt interne Helfer _get_cell_value_safe
- company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map
- if not company_name:
- self.logger.debug(f"Zeile {i}: Uebersprungen (Kein Firmenname in Spalte B).")
- skipped_count += 1 # Zaehlen als uebersprungen
- continue # Springe zur naechsten Zeile
-
- # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist ---
- # Kriterium: Wiki Verif. Timestamp (AX) ist leer
- # UND Wiki URL (M) ist gefuellt und gueltig aussehend (nicht k.A., Fehler etc.)
- # UND Status S ist NICHT bereits in einem Endzustand (OK, X (UPDATED/COPIED/INVALID)).
-
- # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne Helfer)
- ax_value = self._get_cell_value_safe(row, "Wiki Verif. Timestamp").strip() # Block 1 Column Map
- m_value = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map
- s_value_upper = self._get_cell_value_safe(row, "Chat Wiki Konsistenzpruefung").strip().upper() # Block 1 Column Map
-
- # Pruefen Sie, ob die Wiki URL (M) gueltig aussieht
- is_wiki_url_valid_looking = m_value and isinstance(m_value, str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu basierend auf Log
-
-
- # Definieren Sie die Endzustaende von Status S (Grossbuchstaben)
- s_end_states = ["OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID SUGGESTION)"]
- # Pruefen Sie, ob Status S in einem Endzustand ist
- is_s_in_endstate = is_s_in_endstate = s_value_upper in s_end_states # Bugfix: variable is_s_in_endstate wurde falsch zugewiesen.
-
- # Verarbeitung ist noetig, wenn AX leer UND M gefuellt/gueltig aussieht UND S NICHT im Endzustand ist.
- processing_needed_for_row = not ax_value and is_wiki_url_valid_looking and not is_s_in_endstate
-
-
- # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level
- log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row)
- if log_check:
- self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Verif. Check): AX leer? {not ax_value}, M gueltig? {is_wiki_url_valid_looking}, S ('{s_value_upper}') Endzustand? {is_s_in_endstate}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen
-
-
- # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
- if not processing_needed_for_row:
- skipped_count += 1 # Zaehlen als uebersprungene Zeile
- # Zaehlen Sie separat, wenn die Zeile speziell wegen fehlender M-URL uebersprungen wurde
- if not is_wiki_url_valid_looking: skipped_no_wiki_url += 1
- continue # Springe zur naechsten Zeile
-
-
- # --- Wenn Verarbeitung noetig: Fuege zur Batch-Liste fuer OpenAI hinzu ---
- processed_count += 1 # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im Rahmen des Limits zaehlen)
-
- # Pruefe das Limit fuer verarbeitete Zeilen
- if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit:
- # Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_verification_batch erreicht. Breche weitere Zeilenpruefung ab.")
- break # Brich die Schleife ab
-
-
- # Sammle die benoetigten Daten fuer den OpenAI Prompt (_process_verification_openai_batch Block 26).
- # Diese Daten werden in einem Dictionary fuer den Batch gesammelt.
- crm_desc = self._get_cell_value_safe(row, "CRM Beschreibung") # Block 1 Column Map
- wiki_paragraph = self._get_cell_value_safe(row, "Wiki Absatz") # Block 1 Column Map
- wiki_categories = self._get_cell_value_safe(row, "Wiki Kategorien") # Block 1 Column Map
-
-
- # Fuege die Daten dieser Zeile zur aktuellen Batch-Liste fuer OpenAI hinzu
- current_openai_batch_data.append({
- 'row_num': i, # Die 1-basierte Sheet-Zeilennummer
- 'company_name': company_name, # Nutzt den initial geladenen Namen
- 'crm_desc': crm_desc,
- 'wiki_url': m_value, # Nutzt die M-URL aus dem Sheet
- 'wiki_paragraph': wiki_paragraph,
- 'wiki_categories': wiki_categories
- })
- # Fuege die Zeilennummer zur Liste der Zeilennummern im Batch hinzu
- rows_in_current_openai_batch.append(i)
-
-
- # --- Verarbeite den Batch, wenn voll ---
- # Pruefe, ob die aktuelle Batch-Liste die definierte Groesse erreicht hat.
- # openai_batch_size wird aus Config geholt (Block 1).
- if len(current_openai_batch_data) >= openai_batch_size:
- # Logge den Start der Batch-Verarbeitung
- batch_start_row = current_openai_batch_data[0]['row_num']
- batch_end_row = current_openai_batch_data[-1]['row_num']
- self.logger.debug(f"\n--- Starte Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")
-
-
- # Rufe die interne Methode auf, die den OpenAI Call fuer den Batch macht.
- # _process_verification_openai_batch (derselbe Block) ist mit retry_on_failure dekoriert.
- # Wenn _process_verification_openai_batch eine Exception wirft (nach Retries), wird diese hier gefangen.
- batch_results = self._process_verification_openai_batch(current_openai_batch_data)
- # Ergebnisse sollten ein Dictionary {row_num: raw_chatgpt_answer} sein, auch bei Fehlern.
-
-
- # Sammle Sheet Updates basierend auf den Batch-Ergebnissen.
- # Setze immer den Timestamp AX und die Werte in S, T, U und V-Y.
- # Der aktuelle Zeitstempel fuer den Batch
- current_batch_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen
-
-
- # Iteriere ueber die Zeilennummern, die in DIESEM OpenAI Batch waren
- for row_num in rows_in_current_openai_batch:
- # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary.
- # Fallback auf einen Fehlerstring, wenn das Ergebnis fehlt (sollte nicht passieren, wenn _process_verification_openai_batch korrekt ist).
- answer = batch_results.get(row_num, "FEHLER: Batch-Ergebnis fehlt")
- # self.logger.debug(f"Zeile {row_num} Verifizierungsantwort: '{answer[:100]}...'") # Zu viel Laerm (gekuerzt)
-
-
- # Logik zur Bestimmung der Werte fuer S, T, U basierend auf 'answer' (aehnlich wie in altem _process_batch)
- wiki_confirm, alt_article, wiki_explanation = "", "", "" # Initialisiere mit leeren Strings
-
- # Pruefe auf Standard-Antworten und Fehler-Antworten
- if isinstance(answer, str) and answer.upper() == "OK":
- wiki_confirm = "OK"
- wiki_explanation = "Passt laut KI zur Firma." # Standard Begruendung bei OK
- elif isinstance(answer, str) and answer.startswith("X |"):
- # Parse die Antwort im Format "X | | "
- parts = answer.split("|", 2) # Teile maximal in 3 Teile
- wiki_confirm = "X" # Status ist X
- if len(parts) > 1:
- detail = parts[1].strip() # Zweiter Teil ist Detail (Alternative URL oder "Kein passender Artikel gefunden")
- if detail.lower().startswith("alternativer artikel:"):
- alt_article = detail.split(":", 1)[1].strip() # Extrahiere URL
- elif detail.lower() == "kein passender artikel gefunden":
- alt_article = detail # Text "Kein passender Artikel gefunden"
- else:
- alt_article = detail # Unbekanntes Detail
-
- if len(parts) > 2:
- reason_part = parts[2].strip() # Dritter Teil ist Begruendung
- if reason_part.lower().startswith("begruendung:"):
- wiki_explanation = reason_part.split(":", 1)[1].strip() # Extrahiere Begruendungstext
- else:
- wiki_explanation = reason_part # Unbekannte Begruendung
-
- # Fuege ggf. den rohen Antworttext zur Begruendung hinzu, wenn Parsing unvollstaendig war
- if not alt_article or not wiki_explanation:
- wiki_explanation += f" (Rohantwort: {answer[:100]}...)"
-
-
- elif isinstance(answer, str) and answer.startswith("FEHLER"):
- # Wenn die Batch-Verarbeitung einen Fehler zurueckgegeben hat
- wiki_confirm = "FEHLER"
- wiki_explanation = answer # Fehlermeldung in Begruendung schreiben
- alt_article = "Siehe Begruendung" # Verweis auf Begruendung
-
- else: # Unerwartetes Format der Antwort (weder OK noch X | noch FEHLER)
- wiki_confirm = "?" # Setze Status auf unbekannt
- wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..." # Speichere Anfang der Antwort in Begruendung (gekuerzt)
- alt_article = "Siehe Begruendung" # Verweis auf Begruendung
-
- # Spalten V-Y (Begruendung bei Abweichung etc.) werden in diesem Modus geleert
- # Fuer jede Zeile im Batch fuegen wir das Update hinzu.
- # empty_vy_values wurde oben vorbereitet.
- v_y_values = empty_vy_values # Liste von leeren Strings
- # Fuege Update zum Leeren von V-Y hinzu, falls Index gefunden wurde
- if v_y_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte
- batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [v_y_values]}) # Block 1 Column Map, interne Helfer
-
-
- # Fuege Updates fuer S, T, U und AX hinzu (nutzt interne Helfer)
- batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [[wiki_confirm]]}) # Block 1 Column Map
- batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [[alt_article]]}) # Block 1 Column Map
- batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [[wiki_explanation]]}) # Block 1 Column Map
- # Setze AX Timestamp fuer diese Zeile
- batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [[current_batch_timestamp]]}) # Block 1 Column Map
-
-
- # --- Sende gesammelte Updates fuer diesen Batch ---
- # Sammle die Updates fuer diesen Batch in der globalen Liste.
- # all_sheet_updates.extend(batch_sheet_updates) # Nicht hier sammeln, sondern direkt senden
-
- # Sende die gesammelten Updates fuer DIESEN Batch sofort.
- if batch_sheet_updates:
- self.logger.debug(f" Sende Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...")
- # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
- # Wenn es fehlschlaegt, wird es intern geloggt.
- success = self.sheet_handler.batch_update_cells(batch_sheet_updates)
- if success:
- self.logger.info(f" Sheet-Update fuer Wiki-Verifizierungs-Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.")
- # Der Fehlerfall wird von batch_update_cells geloggt
-
- # Setze Batch-Listen zurueck fuer die naechste Iteration
- current_openai_batch_data = []
- rows_in_current_openai_batch = []
-
- # Pause nach jedem OpenAI Batch (nutzt Config Block 1).
- # Dies ist wichtig, um Rate Limits zu vermeiden.
- # Nutze Config.RETRY_DELAY, ggf. kuerzer, da es ein Batch war
- pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.5 # 50% der Retry-Wartezeit
- self.logger.debug(f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---")
- time.sleep(pause_duration)
-
-
- # --- Verarbeitung des letzten unvollstaendigen Batches nach der Schleife ---
- # Wenn nach der Hauptschleife noch Tasks in der Batch-Liste sind
- if current_openai_batch_data:
- # Logge den Start des finalen Batches
- batch_start_row = current_openai_batch_data[0]['row_num']
- batch_end_row = current_openai_batch_data[-1]['row_num']
- self.logger.debug(f"\n--- Starte FINALEN Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")
-
- # Rufe die interne Methode auf, die den OpenAI Call macht
- batch_results = self._process_verification_openai_batch(current_openai_batch_data)
- # Ergebnisse sollten ein Dictionary {row_num: raw_chatgpt_answer} sein, auch bei Fehlern.
-
- # Sammle Sheet Updates (S, T, U, V-Y, AX) fuer diesen finalen Batch
- current_batch_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen
-
- # Iteriere ueber die Zeilennummern, die in DIESEM finalen OpenAI Batch waren
- for row_num in rows_in_current_openai_batch:
- # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary.
- answer = batch_results.get(row_num, "FEHLER: Batch-Ergebnis fehlt") # Fallback
-
- # Logik zur Bestimmung der Werte fuer S, T, U basierend auf 'answer'
- wiki_confirm, alt_article, wiki_explanation = "", "", ""
- # Leere V-Y Spalten
- v_y_values = empty_vy_values # Liste von leeren Strings
- if isinstance(answer, str) and answer.upper() == "OK": wiki_confirm = "OK"; wiki_explanation = "Passt laut KI zur Firma."
- elif isinstance(answer, str) and answer.startswith("X |"):
- parts = answer.split("|", 2); wiki_confirm = "X"
- if len(parts) > 1: detail = parts[1].strip(); alt_article = detail.split(":", 1)[1].strip() if detail.lower().startswith("alternativer artikel:") else detail
- if len(parts) > 2: reason_part = parts[2].strip(); wiki_explanation = reason_part.split(":", 1)[1].strip() if reason_part.lower().startswith("begruendung:") else reason_part
- if not alt_article or not wiki_explanation: wiki_explanation += f" (Rohantwort: {answer[:100]}...)"
- elif isinstance(answer, str) and answer.startswith("FEHLER"): wiki_confirm = "FEHLER"; wiki_explanation = answer; alt_article = "Siehe Begruendung"
- else: wiki_confirm = "?"; wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..."; alt_article = "Siehe Begruendung"
-
-
- # Fuege Updates fuer S, T, U und AX hinzu
- batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [[wiki_confirm]]}) # Block 1 Column Map
- batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [[alt_article]]}) # Block 1 Column Map
- batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [[wiki_explanation]]}) # Block 1 Column Map
- # Setze AX Timestamp
- batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [[current_batch_timestamp]]}) # Block 1 Column Map
-
- # Fuege Update zum Leeren von V-Y hinzu, falls Index gefunden wurde
- if v_y_range_letter:
- batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [v_y_values]}) # Block 1 Column Map, interne Helfer
-
-
- # Sende die gesammelten Updates fuer DIESEN finalen Batch.
- if batch_sheet_updates:
- self.logger.debug(f" Sende FINALES Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen)...")
- # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
- success = self.sheet_handler.batch_update_cells(batch_sheet_updates)
- if success:
- self.logger.info(f" FINALES Sheet-Update fuer Wiki-Verifizierungs-Batch erfolgreich.")
- # Der Fehlerfall wird von batch_update_cells geloggt
-
-
- # Logge den Abschluss des Modus
- self.logger.info(f"Wikipedia-Verifizierungs-Batch abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen ({skipped_no_wiki_url} wegen fehlender M-URL).")
- # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.
-
-
-# ==============================================================================
-# Ende DataProcessor Klasse Batch: Wiki Verification Block
-# ==============================================================================
-
- # ==========================================================================
- # === Batch Processing Methods =============================================
- # ==========================================================================
-
- # --- Interne Hilfsfunktion fuer Wiki-Verifizierungs-Batch (OpenAI Call) ---
- # Diese Funktion verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen ueber OpenAI.
- # Sie wird von process_verification_batch (derselben Block) aufgerufen.
- # Nutzt globale Helfer: call_openai_chat (Block 8), logger, token_count (optional Block 3), retry_on_failure (Block 2), re.
- @retry_on_failure # Wende den Decorator auf den gesamten Batch-API Call an
- def _process_verification_openai_batch(self, batch_data):
- """
- Verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen ueber OpenAI.
- Sammelt die Ergebnisse und gibt sie zurueck. Aktualisiert NICHT das Sheet direkt.
-
- Args:
- batch_data (list): Liste von Dictionaries, jedes enthaelt:
- {'row_num': int, 'company_name': str, 'crm_desc': str,
- 'wiki_url': str, 'wiki_paragraph': str, 'wiki_categories': str}
-
- Returns:
- dict: Ein Dictionary, das Zeilennummern auf ihre rohe ChatGPT-Antwort mappt.
- z.B. {2122: "OK", 2123: "X | ..."}
- Bei Fehlern oder fehlenden Antworten wird ein Fehlerstring verwendet.
- Wirft Exception bei endgueltigen API-Fehlern nach Retries.
- """
- # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
- if not batch_data:
- return {} # Gebe leeres Dictionary zurueck, wenn keine Tasks da sind
-
- self.logger.debug(f"Sende OpenAI-Batch fuer Wiki-Verifizierung ({len(batch_data)} Zeilen, Start: {batch_data[0]['row_num'] if batch_data else 'N/A'})...") # Sichere Indexierung
-
- # --- Prompt Erstellung ---
- # Verwenden Sie klare Anweisungen und das definierte Antwortformat.
- # Vermeiden Sie Umlaute im Prompt, um Encoding-Probleme zu minimieren.
- aggregated_prompt = (
- "Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln fuer Unternehmen. "
- "Fuer jeden der folgenden Eintraege pruefe, ob der vorhandene Wikipedia-Artikel (URL, Absatz, Kategorien) plausibel zum Firmennamen und zur Beschreibung passt. "
- "Gib das Ergebnis fuer jeden Eintrag ausschliesslich im folgenden Format auf einer neuen Zeile aus:\n"
- "Eintrag : \n\n"
- "Moegliche Antworten:\n"
- "- 'OK' (wenn der Artikel gut passt)\n"
- "- 'X | Alternativer Artikel: | Begruendung: ' (wenn der Artikel nicht passt, aber ein besserer gefunden wurde)\n"
- "- 'X | Kein passender Artikel gefunden | Begruendung: ' (wenn der Artikel nicht passt und kein besserer gefunden wurde)\n"
- # Der Fall "Kein Wikipedia-Eintrag vorhanden" wird vom Skript VOR diesem Call behandelt
- # und sollte hier nicht vom KI-Modell generiert werden.
- "Stelle sicher, dass du nur EINE Zeile pro Eintrag im Format 'Eintrag X: Antwort' ausgibst.\n\n"
- "Eintraege zur Pruefung:\n"
- "--------------------\n"
- )
-
- # Fuegen Sie die Daten fuer jeden Eintrag im Batch hinzu.
- # Kuerzen Sie die Beschreibungen und Kategorien, um das Prompt-Limit zu reduzieren.
- # Stellen Sie sicher, dass die Werte Strings sind und "k.A." richtig behandelt wird.
- max_desc_length = 200 # Maximale Laenge fuer Beschreibungsteile im Prompt
- for item in batch_data:
- row_num = item['row_num']
- # Holen und Kuerzen Sie die Werte sicher. Ersetzen Sie None durch "k.A.".
- company_name = str(item.get('company_name', 'k.A.'))
- crm_desc = str(item.get('crm_desc', 'k.A.'))
- wiki_url = str(item.get('wiki_url', 'k.A.'))
- wiki_paragraph = str(item.get('wiki_paragraph', 'k.A.'))
- wiki_categories = str(item.get('wiki_categories', 'k.A.'))
-
- # Kuerzen Sie die Laengen und fuegen Sie "..." hinzu, wenn gekuerzt wurde.
- crm_desc_short = crm_desc[:max_desc_length] + '...' if len(crm_desc) > max_desc_length else crm_desc
- wiki_paragraph_short = wiki_paragraph[:max_desc_length] + '...' if len(wiki_paragraph) > max_desc_length else wiki_paragraph
- wiki_categories_short = wiki_categories[:max_desc_length] + '...' if len(wiki_categories) > max_desc_length else wiki_categories
-
-
- entry_text = (
- f"Eintrag {row_num}:\n"
- f" Firmenname: {company_name}\n"
- f" CRM-Beschreibung: {crm_desc_short}\n"
- f" Wikipedia-URL: {wiki_url}\n"
- f" Wiki-Absatz: {wiki_paragraph_short}\n"
- f" Wiki-Kategorien: {wiki_categories_short}\n"
- f"----\n"
- )
- aggregated_prompt += entry_text
-
-
- # Fuegen Sie den Abschluss des Prompts hinzu.
- aggregated_prompt += "--------------------\nBitte nur die 'Eintrag X: Antwort'-Zeilen ausgeben."
-
- # Optional: Token zaehlen fuer den Prompt.
- # try: prompt_tokens = token_count(aggregated_prompt, model=getattr(Config, 'TOKEN_MODEL', 'gpt-3.5-turbo')); logger.debug(f"Geschaetzt Prompt-Tokens fuer Batch: {prompt_tokens}.");
- # except Exception as e_tc: logger.debug(f"Fehler beim Token-Zaehlen: {e_tc}");
-
-
- # --- ChatGPT Aufruf ---
- # call_openai_chat (Block 8) nutzt den retry_on_failure Decorator und wirft bei endgueltigem Fehler eine Exception.
- # Der retry_on_failure Decorator auf dieser summarize_batch_openai Funktion (Block 9) faengt die Exception
- # von call_openai_chat und fuehrt die Retries fuer die GESAMTE Batch-Funktion durch.
- chat_response = None
- try:
- # Rufe die zentrale OpenAI Chat API Funktion auf (Block 8).
- # Standard Temperatur 0.0 fuer Klassifizierung/Verifizierung.
- chat_response = call_openai_chat(aggregated_prompt, temperature=0.0)
- # Wenn call_openai_chat erfolgreich ist, gibt es den String zurueck.
- # Exceptions werden nach Retries von call_openai_chat geworfen und vom aeusseren retry_on_failure dieser Funktion gefangen.
-
- if not chat_response:
- # Dieser Fall sollte nach der Aenderung in call_openai_chat (wirft Exception) nicht mehr auftreten.
- logger.error("call_openai_chat gab unerwarteterweise None zurueck fuer Wiki-Verifizierungs-Batch.")
- # Werfen Sie eine spezifische Exception, damit der aeussere Decorator sie faengt.
- raise openai.error.APIError("Keine Antwort von OpenAI erhalten fuer Wiki-Verifizierungs-Batch.")
-
-
- except Exception as e:
- # Wenn call_openai_chat oder der aeussere retry_on_failure eine Exception wirft (nach Retries)
- # Die Exception wird hier gefangen, bevor sie an den Aufrufer (process_verification_batch) weitergeleitet wird.
- logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Wiki-Verifizierung (innerhalb Batch Decorator): {e}")
- # Logge den Traceback
- self.logger.debug(traceback.format_exc())
- # Geben Sie ein Dictionary zurueck, das signalisiert, dass fuer alle Zeilen im Batch ein Fehler aufgetreten ist
- return {item['row_num']: f"FEHLER API: {str(e)[:100]}" for item in batch_data}
-
-
- # --- Antwort parsen ---
- answers = {} # Initialisieren Sie das Ergebnis-Dictionary
- # Liste der Zeilennummern, die im ursprünglichen Batch angefragt wurden
- original_batch_row_nums = {item['row_num'] for item in batch_data}
- lines = chat_response.strip().split('\n')
- parsed_count = 0
- for line in lines:
- # Matcht "Eintrag :" und den Rest der Zeile
- match = re.match(r"Eintrag (\d+): (.*)", line.strip())
- if match:
- row_num = int(match.group(1))
- answer_text = match.group(2).strip()
- # Pruefen Sie, ob die Zeilennummer im urspruenglichen Batch angefragt wurde
- if row_num in original_batch_row_nums:
- answers[row_num] = answer_text
- parsed_count += 1
- # else: logger.debug(f"Warnung: Antwort fuer unerwartete Zeilennummer {row_num} im Batch erhalten: {answer_text[:100]}...") # Zu viel Laerm (gekuerzt loggen)
-
- # Logge das Ergebnis des Parsens
- self.logger.debug(f"OpenAI-Batch-Antwort geparst: {parsed_count} von {len(original_batch_row_nums)} Zeilen erfolgreich zugeordnet.")
-
- # Fuegen Sie einen Fehlerwert fuer Zeilen hinzu, die nicht geparst werden konnten (z.B. falsches Antwortformat)
- if parsed_count < len(original_batch_row_nums):
- logger.warning(f"Nicht alle Zeilen aus dem Batch ({len(original_batch_row_nums)}) konnten in der OpenAI-Antwort ({len(lines)} Zeilen) geparst werden.")
- # Logge den Anfang der unvollstaendigen Antwort auf Debug
- logger.debug(f"Unerwartete Antwortteile (erste 500 Zeichen): {chat_response[:500]}")
- for row_num in original_batch_row_nums:
- if row_num not in answers:
- answers[row_num] = "FEHLER: Antwort nicht geparst"
-
-
- # Die 'answers' Dictionary enthaelt nun Ergebnisse fuer alle Zeilen, entweder geparst oder mit einem Fehlerstring.
- return answers # Rueckgabe des Dictionarys mit Ergebnissen oder Fehlern
-
-
- # --- Methode fuer den Wiki-Verifizierungs-Batchmodus (AX) ---
- # Diese Methode koordiniert die Auswahl der Zeilen, die Batch-Verarbeitung durch OpenAI,
- # und das Schreiben der Ergebnisse (S, T, U, V-Y, AX, AP) ins Sheet.
- # Basierend auf process_verification_only und _process_batch aus Teil 8.
- # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter, _process_verification_openai_batch (derselben Block).
- # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, Config (Block 1), datetime, time.
- # Nutzt die uebergeordnete sheet_handler Instanz (Block 14).
- def process_verification_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None):
- """
- Batch-Prozess nur fuer Wikipedia-Verifizierung (Spalten S-U, V-Y werden geleert).
- Laedt Daten neu, prueft fuer jede Zeile im Bereich, ob Timestamp AX (Wiki Verif.)
- bereits gesetzt ist, ob eine Wiki URL (M) vorhanden ist und ob Status S
- nicht bereits 'OK', 'X (URL Copied)' oder 'X (Invalid Suggestion)' ist.
- Setzt AX + AP fuer bearbeitete Zeilen und schreibt S-U in Batches.
-
- Args:
- start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (automatische Ermittlung basierend auf leeren AX).
- end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet).
- limit (int, optional): Maximale Anzahl ZU VERARBEITENDER (nicht uebersprungener) Zeilen. Defaults to None (Unbegrenzt).
- """
- # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
- # Logge die Konfiguration des Batch-Laufs
- self.logger.info(f"Starte Wikipedia-Verifizierungsmodus (Batch S-U, AX). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
-
-
- # --- Daten laden und Startzeile ermitteln ---
- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt
- if start_sheet_row is None:
- self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AX...")
- # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AX (Block 1 Column Map).
- # Standardmaessig ab Zeile 7
- start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wiki Verif. Timestamp", min_sheet_row=7)
-
- # Wenn get_start_row_index -1 zurueckgibt (Fehler)
- if start_data_index_no_header == -1:
- self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")
- return # Beende die Methode
-
- # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index
- start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut
- self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AX Zelle): {start_sheet_row}")
- else:
- # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
- # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
- if not self.sheet_handler.load_data():
- self.logger.error("FEHLER beim Laden der Daten fuer process_verification_batch.")
- return # Beende die Methode, wenn das Laden fehlschlaegt
-
-
- # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler.
- all_data = self.sheet_handler.get_all_data_with_headers();
- # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14).
- header_rows = self.sheet_handler._header_rows;
- total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet
-
- # Berechne Endzeile, wenn nicht manuell gesetzt
- if end_sheet_row is None:
- end_sheet_row = total_sheet_rows # Bis zur letzten Zeile
-
- # Logge den verarbeitungsbereich
- self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
-
- # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen)
- if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
- self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
- return # Beende die Methode, wenn der Bereich leer ist
-
-
- # --- Indizes und Buchstaben ---
- # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind
- required_keys = [
- "Wiki Verif. Timestamp", "Wiki URL", "Chat Wiki Konsistenzpruefung", # Pruefkriterien / Timestamp (AX, M, S)
- "CRM Name", "CRM Beschreibung", "Wiki Absatz", "Wiki Kategorien", # Daten fuer Prompt (B, F, N, R)
- "Chat Begruendung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", # Ergebnisspalten (T, U)
- "Begruendung bei Abweichung", "Chat Begruendung Abweichung Branche", # Spalten V-Y zum Leeren
- "Wikipedia Timestamp", "Timestamp letzte Pruefung", # Spalten AN, AO zum Leeren
- "Version", "SerpAPI Wiki Search Timestamp" # Spalten AP, AY zum Leeren
- ]
- # Erstellen Sie ein Dictionary mit Schluesseln und Indizes
- col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}
-
- # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden
- if None in col_indices.values():
- missing = [k for k, v in col_indices.items() if v is None]
- self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_verification_batch: {missing}. Breche ab.")
- return # Beende die Methode bei kritischem Fehler
-
-
- # Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt interne Helfer _get_col_letter Block 14)
- ts_ax_letter = self.sheet_handler._get_col_letter(col_indices["Wiki Verif. Timestamp"] + 1) # Timestamp zu setzen (AX)
- s_letter = self.sheet_handler._get_col_letter(col_indices["Chat Wiki Konsistenzpruefung"] + 1) # Status S
- t_letter = self.sheet_handler._get_col_letter(col_indices["Chat Begruendung Wiki Inkonsistenz"] + 1) # Begruendung T
- u_letter = self.sheet_handler._get_col_letter(col_indices["Chat Vorschlag Wiki Artikel"] + 1) # Vorschlag U
-
- # Spalten V-Y leeren (werden in diesem Modus nicht neu befuellt).
- # V ist Begruendung bei Abweichung (von Wiki-URL Pruefung CRM vs Wiki).
- # Y ist Begruendung Abweichung Branche (von Chat).
- v_idx = col_indices["Begruendung bei Abweichung"]
- y_idx = col_indices["Chat Begruendung Abweichung Branche"] # Block 1 Column Map
- # Erstellen Sie den Bereichsnamen (z.B. "V:Y")
- v_letter = self.sheet_handler._get_col_letter(v_idx + 1)
- y_letter = self.sheet_handler._get_col_letter(y_idx + 1)
- v_y_range_letter = f'{v_letter}:{y_letter}' # z.B. V:Y
- # Erstellen Sie eine Liste von leeren Strings fuer diesen Bereich
- empty_vy_values = [''] * (y_idx - v_idx + 1) # Anzahl der Spalten = Y_Index - V_Index + 1
-
-
- # Timestamps AN, AO, AP, AY leeren.
- # Diese werden von anderen Schritten gesetzt und sollen hier zurueckgesetzt werden,
- # um sicherzustellen, dass die Zeile bei Bedarf von diesen anderen Schritten erneut bearbeitet wird.
- an_letter = self.sheet_handler._get_col_letter(col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS)
- ao_letter = self.sheet_handler._get_col_letter(col_indices["Timestamp letzte Pruefung"] + 1) # AO (Chat Evaluation TS)
- ap_letter = self.sheet_handler._get_col_letter(col_indices["Version"] + 1) # AP (Version)
- ay_letter = self.sheet_handler._get_col_letter(col_indices["SerpAPI Wiki Search Timestamp"] + 1) # AY (SerpAPI Wiki TS)
-
-
- # --- Verarbeitung ---
- # Holen Sie die Batch-Groesse fuer OpenAI-Aufrufe aus Config (Block 1)
- openai_batch_size = getattr(Config, 'PROCESSING_BATCH_SIZE', 20) # Nutzt dieselbe Batch-Groesse wie Scraping/Summarization
- # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1)
- update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50)
-
-
- current_openai_batch_data = [] # Daten fuer den aktuellen OpenAI Batch (Liste von Dicts)
- rows_in_current_openai_batch = [] # 1-basierte Zeilennummern im aktuellen OpenAI Batch
- all_sheet_updates = [] # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts)
-
-
- processed_count = 0 # Zaehlt Zeilen, die fuer die Verarbeitung in Frage kommen und in den Batch aufgenommen werden (im Rahmen des Limits).
- skipped_count = 0 # Zaehlt Zeilen, die uebersprungen wurden (wegen Status, fehlender Daten etc.).
- skipped_no_wiki_url = 0 # Zaehlt Zeilen, die speziell wegen fehlender M-URL uebersprungen wurden.
-
-
- # Iteriere ueber die Sheet-Zeilen im definierten Bereich (1-basierte Sheet-Zeilennummer)
- for i in range(start_sheet_row, end_sheet_row + 1):
- row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste
- # Pruefen Sie, ob das Ende des Sheets erreicht wurde
- if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht
-
-
- row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile
-
-
- # Stellen Sie sicher, dass die Zeile nicht leer ist (mindestens Name vorhanden)
- # Nutzt interne Helfer _get_cell_value_safe
- company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map
- if not company_name:
- self.logger.debug(f"Zeile {i}: Uebersprungen (Kein Firmenname in Spalte B).")
- skipped_count += 1 # Zaehlen als uebersprungen
- continue # Springe zur naechsten Zeile
-
- # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist ---
- # Kriterium: Wiki Verif. Timestamp (AX) ist leer
- # UND Wiki URL (M) ist gefuellt und gueltig aussehend (nicht k.A., Fehler etc.)
- # UND Status S ist NICHT bereits in einem Endzustand (OK, X (UPDATED/COPIED/INVALID)).
-
- # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne Helfer)
- ax_value = self._get_cell_value_safe(row, "Wiki Verif. Timestamp").strip() # Block 1 Column Map
- m_value = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map
- s_value_upper = self._get_cell_value_safe(row, "Chat Wiki Konsistenzpruefung").strip().upper() # Block 1 Column Map
-
- # Pruefen Sie, ob die Wiki URL (M) gueltig aussieht
- is_wiki_url_valid_looking = m_value and isinstance(m_value, str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu basierend auf Log
-
-
- # Definieren Sie die Endzustaende von Status S (Grossbuchstaben)
- s_end_states = ["OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID SUGGESTION)"]
- # Pruefen Sie, ob Status S in einem Endzustand ist
- is_s_in_endstate = is_s_in_endstate = s_value_upper in s_end_states # Bugfix: variable is_s_in_endstate wurde falsch zugewiesen.
-
- # Verarbeitung ist noetig, wenn AX leer UND M gefuellt/gueltig aussieht UND S NICHT im Endzustand ist.
- processing_needed_for_row = not ax_value and is_wiki_url_valid_looking and not is_s_in_endstate
-
-
- # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level
- log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row)
- if log_check:
- self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Verif. Check): AX leer? {not ax_value}, M gueltig? {is_wiki_url_valid_looking}, S ('{s_value_upper}') Endzustand? {is_s_in_endstate}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen
-
-
- # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
- if not processing_needed_for_row:
- skipped_count += 1 # Zaehlen als uebersprungene Zeile
- # Zaehlen Sie separat, wenn die Zeile speziell wegen fehlender M-URL uebersprungen wurde
- if not is_wiki_url_valid_looking: skipped_no_wiki_url += 1
- continue # Springe zur naechsten Zeile
-
-
- # --- Wenn Verarbeitung noetig: Fuege zur Batch-Liste fuer OpenAI hinzu ---
- processed_count += 1 # Zaehle die Zeile, die fuer die Verarbeitung in Frage kommt (im Rahmen des Limits zaehlen)
-
- # Pruefe das Limit fuer verarbeitete Zeilen
- if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit:
- # Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_verification_batch erreicht. Breche weitere Zeilenpruefung ab.")
- break # Brich die Schleife ab
-
-
- # Sammle die benoetigten Daten fuer den OpenAI Prompt (_process_verification_openai_batch Block 26).
- # Diese Daten werden in einem Dictionary fuer den Batch gesammelt.
- crm_desc = self._get_cell_value_safe(row, "CRM Beschreibung") # Block 1 Column Map
- wiki_paragraph = self._get_cell_value_safe(row, "Wiki Absatz") # Block 1 Column Map
- wiki_categories = self._get_cell_value_safe(row, "Wiki Kategorien") # Block 1 Column Map
-
-
- # Fuege die Daten dieser Zeile zur aktuellen Batch-Liste fuer OpenAI hinzu
- current_openai_batch_data.append({
- 'row_num': i, # Die 1-basierte Sheet-Zeilennummer
- 'company_name': company_name, # Nutzt den initial geladenen Namen
- 'crm_desc': crm_desc,
- 'wiki_url': m_value, # Nutzt die M-URL aus dem Sheet
- 'wiki_paragraph': wiki_paragraph,
- 'wiki_categories': wiki_categories
- })
- # Fuege die Zeilennummer zur Liste der Zeilennummern im Batch hinzu
- rows_in_current_openai_batch.append(i)
-
-
- # --- Verarbeite den Batch, wenn voll ---
- # Pruefe, ob die aktuelle Batch-Liste die definierte Groesse erreicht hat.
- # openai_batch_size wird aus Config geholt (Block 1).
- if len(current_openai_batch_data) >= openai_batch_size:
- # Logge den Start der Batch-Verarbeitung
- batch_start_row = current_openai_batch_data[0]['row_num']
- batch_end_row = current_openai_batch_data[-1]['row_num']
- self.logger.debug(f"\n--- Starte Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")
-
-
- # Rufe die interne Methode auf, die den OpenAI Call fuer den Batch macht.
- # _process_verification_openai_batch (derselbe Block) ist mit retry_on_failure dekoriert.
- # Wenn _process_verification_openai_batch eine Exception wirft (nach Retries), wird diese hier gefangen.
- batch_results = self._process_verification_openai_batch(current_openai_batch_data)
- # Ergebnisse sollten ein Dictionary {row_num: raw_chatgpt_answer} sein, auch bei Fehlern.
-
-
- # Sammle Sheet Updates basierend auf den Batch-Ergebnissen.
- # Setze immer den Timestamp AX und die Werte in S, T, U und V-Y.
- # Der aktuelle Zeitstempel fuer den Batch
- current_batch_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen
-
-
- # Iteriere ueber die Zeilennummern, die in DIESEM OpenAI Batch waren
- for row_num in rows_in_current_openai_batch:
- # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary.
- # Fallback auf einen Fehlerstring, wenn das Ergebnis fehlt (sollte nicht passieren, wenn _process_verification_openai_batch korrekt ist).
- answer = batch_results.get(row_num, "FEHLER: Batch-Ergebnis fehlt")
- # self.logger.debug(f"Zeile {row_num} Verifizierungsantwort: '{answer[:100]}...'") # Zu viel Laerm (gekuerzt)
-
-
- # Logik zur Bestimmung der Werte fuer S, T, U basierend auf 'answer' (aehnlich wie in altem _process_batch)
- wiki_confirm, alt_article, wiki_explanation = "", "", "" # Initialisiere mit leeren Strings
-
- # Pruefe auf Standard-Antworten und Fehler-Antworten
- if isinstance(answer, str) and answer.upper() == "OK":
- wiki_confirm = "OK"
- wiki_explanation = "Passt laut KI zur Firma." # Standard Begruendung bei OK
- elif isinstance(answer, str) and answer.startswith("X |"):
- # Parse die Antwort im Format "X | | "
- parts = answer.split("|", 2) # Teile maximal in 3 Teile
- wiki_confirm = "X" # Status ist X
- if len(parts) > 1:
- detail = parts[1].strip() # Zweiter Teil ist Detail (Alternative URL oder "Kein passender Artikel gefunden")
- if detail.lower().startswith("alternativer artikel:"):
- alt_article = detail.split(":", 1)[1].strip() # Extrahiere URL
- elif detail.lower() == "kein passender artikel gefunden":
- alt_article = detail # Text "Kein passender Artikel gefunden"
- else:
- alt_article = detail # Unbekanntes Detail
-
- if len(parts) > 2:
- reason_part = parts[2].strip() # Dritter Teil ist Begruendung
- if reason_part.lower().startswith("begruendung:"):
- wiki_explanation = reason_part.split(":", 1)[1].strip() # Extrahiere Begruendungstext
- else:
- wiki_explanation = reason_part # Unbekannte Begruendung
-
- # Fuege ggf. den rohen Antworttext zur Begruendung hinzu, wenn Parsing unvollstaendig war
- if not alt_article or not wiki_explanation:
- wiki_explanation += f" (Rohantwort: {answer[:100]}...)"
-
-
- elif isinstance(answer, str) and answer.startswith("FEHLER"):
- # Wenn die Batch-Verarbeitung einen Fehler zurueckgegeben hat
- wiki_confirm = "FEHLER"
- wiki_explanation = answer # Fehlermeldung in Begruendung schreiben
- alt_article = "Siehe Begruendung" # Verweis auf Begruendung
-
- else: # Unerwartetes Format der Antwort (weder OK noch X | noch FEHLER)
- wiki_confirm = "?" # Setze Status auf unbekannt
- wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..." # Speichere Anfang der Antwort in Begruendung (gekuerzt)
- alt_article = "Siehe Begruendung" # Verweis auf Begruendung
-
- # Spalten V-Y (Begruendung bei Abweichung etc.) werden in diesem Modus geleert
- # Fuer jede Zeile im Batch fuegen wir das Update hinzu.
- # empty_vy_values wurde oben vorbereitet.
- v_y_values = empty_vy_values # Liste von leeren Strings
- # Fuege Update zum Leeren von V-Y hinzu, falls Index gefunden wurde
- if v_y_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte
- batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [v_y_values]}) # Block 1 Column Map, interne Helfer
-
-
- # Fuege Updates fuer S, T, U und AX hinzu (nutzt interne Helfer)
- batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [[wiki_confirm]]}) # Block 1 Column Map
- batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [[alt_article]]}) # Block 1 Column Map
- batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [[wiki_explanation]]}) # Block 1 Column Map
- # Setze AX Timestamp fuer diese Zeile
- batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [[current_batch_timestamp]]}) # Block 1 Column Map
-
-
- # --- Sende gesammelte Updates fuer diesen Batch ---
- # Sammle die Updates fuer diesen Batch in der globalen Liste.
- # all_sheet_updates.extend(batch_sheet_updates) # Nicht hier sammeln, sondern direkt senden
-
- # Sende die gesammelten Updates fuer DIESEN Batch sofort.
- if batch_sheet_updates:
- self.logger.debug(f" Sende Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...")
- # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
- # Wenn es fehlschlaegt, wird es intern geloggt.
- success = self.sheet_handler.batch_update_cells(batch_sheet_updates)
- if success:
- self.logger.info(f" Sheet-Update fuer Wiki-Verifizierungs-Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.")
- # Der Fehlerfall wird von batch_update_cells geloggt
-
- # Setze Batch-Listen zurueck fuer die naechste Iteration
- current_openai_batch_data = []
- rows_in_current_openai_batch = []
-
- # Pause nach jedem OpenAI Batch (nutzt Config Block 1).
- # Dies ist wichtig, um Rate Limits zu vermeiden.
- # Nutze Config.RETRY_DELAY, ggf. kuerzer, da es ein Batch war
- pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.5 # 50% der Retry-Wartezeit
- self.logger.debug(f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---")
- time.sleep(pause_duration)
-
-
- # --- Verarbeitung des letzten unvollstaendigen Batches nach der Schleife ---
- # Wenn nach der Hauptschleife noch Tasks in der Batch-Liste sind
- if current_openai_batch_data:
- # Logge den Start des finalen Batches
- batch_start_row = current_openai_batch_data[0]['row_num']
- batch_end_row = current_openai_batch_data[-1]['row_num']
- self.logger.debug(f"\n--- Starte FINALEN Wiki-Verifizierungs-Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")
-
- # Rufe die interne Methode auf, die den OpenAI Call macht
- batch_results = self._process_verification_openai_batch(current_openai_batch_data)
- # Ergebnisse sollten ein Dictionary {row_num: raw_chatgpt_answer} sein, auch bei Fehlern.
-
- # Sammle Sheet Updates (S, T, U, V-Y, AX) fuer diesen finalen Batch
- current_batch_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- batch_sheet_updates = [] # Updates fuer DIESEN spezifischen Batch von Zeilen
-
- # Iteriere ueber die Zeilennummern, die in DIESEM finalen OpenAI Batch waren
- for row_num in rows_in_current_openai_batch:
- # Hole das Ergebnis fuer diese Zeile aus dem Ergebnis-Dictionary.
- answer = batch_results.get(row_num, "FEHLER: Batch-Ergebnis fehlt") # Fallback
-
- # Logik zur Bestimmung der Werte fuer S, T, U basierend auf 'answer'
- wiki_confirm, alt_article, wiki_explanation = "", "", ""
- # Leere V-Y Spalten
- v_y_values = empty_vy_values # Liste von leeren Strings
- if isinstance(answer, str) and answer.upper() == "OK": wiki_confirm = "OK"; wiki_explanation = "Passt laut KI zur Firma."
- elif isinstance(answer, str) and answer.startswith("X |"):
- parts = answer.split("|", 2); wiki_confirm = "X"
- if len(parts) > 1: detail = parts[1].strip(); alt_article = detail.split(":", 1)[1].strip() if detail.lower().startswith("alternativer artikel:") else detail
- if len(parts) > 2: reason_part = parts[2].strip(); wiki_explanation = reason_part.split(":", 1)[1].strip() if reason_part.lower().startswith("begruendung:") else reason_part
- if not alt_article or not wiki_explanation: wiki_explanation += f" (Rohantwort: {answer[:100]}...)"
- elif isinstance(answer, str) and answer.startswith("FEHLER"): wiki_confirm = "FEHLER"; wiki_explanation = answer; alt_article = "Siehe Begruendung"
- else: wiki_confirm = "?"; wiki_explanation = f"Unerwartetes Format: {str(answer)[:100]}..."; alt_article = "Siehe Begruendung"
-
-
- # Fuege Updates fuer S, T, U und AX hinzu
- batch_sheet_updates.append({'range': f'{s_letter}{row_num}', 'values': [[wiki_confirm]]}) # Block 1 Column Map
- batch_sheet_updates.append({'range': f'{t_letter}{row_num}', 'values': [[alt_article]]}) # Block 1 Column Map
- batch_sheet_updates.append({'range': f'{u_letter}{row_num}', 'values': [[wiki_explanation]]}) # Block 1 Column Map
- # Setze AX Timestamp
- batch_sheet_updates.append({'range': f'{ts_ax_letter}{row_num}', 'values': [[current_batch_timestamp]]}) # Block 1 Column Map
-
- # Fuege Update zum Leeren von V-Y hinzu, falls Index gefunden wurde
- if v_y_range_letter:
- batch_sheet_updates.append({'range': f'{v_letter}{row_num}:{y_letter}{row_num}', 'values': [v_y_values]}) # Block 1 Column Map, interne Helfer
-
-
- # Sende die gesammelten Updates fuer DIESEN finalen Batch.
- if batch_sheet_updates:
- self.logger.debug(f" Sende FINALES Sheet-Update fuer {len(rows_in_current_openai_batch)} Zeilen ({len(batch_sheet_updates)} Zellen)...")
- # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
- success = self.sheet_handler.batch_update_cells(batch_sheet_updates)
- if success:
- self.logger.info(f" FINALES Sheet-Update fuer Wiki-Verifizierungs-Batch erfolgreich.")
- # Der Fehlerfall wird von batch_update_cells geloggt
-
-
- # Logge den Abschluss des Modus
- self.logger.info(f"Wikipedia-Verifizierungs-Batch abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen ({skipped_no_wiki_url} wegen fehlender M-URL).")
- # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.
-
-
-# ==============================================================================
-# Ende DataProcessor Klasse Batch: Wiki Verification Block
-# ==============================================================================
-
- # ==========================================================================
- # === Batch Processing Methods =============================================
- # ==========================================================================
+ # logger.debug(f"Scraping Task Zeile {row_num} abgeschlossen. Textlaenge: {len(str(raw_text))}.") # Zu viel Laerm im Debug
+ return {"row_num": row_num, "raw_text": raw_text, "error": error}
# --- Methode fuer den Website-Scraping-Batchmodus (AR) ---
# Diese Methode verarbeitet Zeilen, bei denen AR leer ist, um den Rohtext zu scrapen.
- # Sie nutzt einen ThreadPoolExecutor und ruft eine globale Worker-Funktion auf.
+ # Sie nutzt einen ThreadPoolExecutor und ruft die interne Worker-Funktion auf.
# Basierend auf process_website_batch aus Teil 9.
- # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter.
+ # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter, _scrape_raw_text_task.
# Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, Config (Block 1), datetime, time,
# concurrent.futures, get_website_raw (Block 11).
# Nutzt die uebergeordnete sheet_handler Instanz (Block 14).
@@ -6770,30 +5361,30 @@ class DataProcessor:
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
# Logge die Konfiguration des Batch-Laufs
- self.logger.info(f"Starte Website-Scraping (Batch AR, AT, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
+ self.logger.info(f"Starte Website-Scraping (Batch AR, AT, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT
# --- Daten laden und Startzeile ermitteln ---
# Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt
if start_sheet_row is None:
- self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AT...")
+ self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AT...") # <<< GEÄNDERT
# Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AT (Block 1 Column Map).
# Standardmaessig ab Zeile 7
start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Website Scrape Timestamp", min_sheet_row=7)
# Wenn get_start_row_index -1 zurueckgibt (Fehler)
if start_data_index_no_header == -1:
- self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")
+ self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT
return # Beende die Methode
# Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index
start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut
- self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AT Zelle): {start_sheet_row}")
+ self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AT Zelle): {start_sheet_row}") # <<< GEÄNDERT
else:
# Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
# Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
if not self.sheet_handler.load_data():
- self.logger.error("FEHLER beim Laden der Daten fuer process_website_scraping_batch.")
+ self.logger.error("FEHLER beim Laden der Daten fuer process_website_scraping_batch.") # <<< GEÄNDERT
return # Beende die Methode, wenn das Laden fehlschlaegt
@@ -6809,11 +5400,11 @@ class DataProcessor:
end_sheet_row = total_sheet_rows # Bis zur letzten Zeile
# Logge den verarbeitungsbereich
- self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
+ self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT
# Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen)
if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
- self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
+ self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT
return # Beende die Methode, wenn der Bereich leer ist
@@ -6828,7 +5419,7 @@ class DataProcessor:
# Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden
if None in col_indices.values():
missing = [k for k, v in col_indices.items() if v is None]
- self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_website_scraping_batch: {missing}. Breche ab.")
+ self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_website_scraping_batch: {missing}. Breche ab.") # <<< GEÄNDERT
return # Beende die Methode bei kritischem Fehler
# Ermitteln Sie die Indizes und Buchstaben fuer Updates (AR, AT, AP)
@@ -6843,59 +5434,6 @@ class DataProcessor:
timestamp_col_letter = self.sheet_handler._get_col_letter(timestamp_col_idx + 1)
- # --- Worker-Funktion fuer Scraping (Intern in der Methode definiert) ---
- # Diese Funktion laeuft in einem separaten Thread fuer parallele Verarbeitung.
- # Sie nutzt die globale Funktion get_website_raw und erhaelt sie als Argument uebergeben.
- def scrape_raw_text_task(task_info, get_website_raw_func):
- """
- Scrapt den Rohtext einer Website in einem separaten Thread.
- Wird vom ThreadPoolExecutor in process_website_scraping_batch aufgerufen.
- Nutzt die uebergebene Funktion zum Abrufen des Rohtexts.
-
- Args:
- task_info (dict): Enthält {'row_num': int, 'url': str}.
- get_website_raw_func (function): Die Funktion zum Abrufen des Website-Rohtexts (sollte die globale get_website_raw sein).
-
- Returns:
- dict: Enthält {'row_num': int, 'raw_text': str, 'error': str}.
- """
- # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
- row_num = task_info['row_num']
- url = task_info['url']
- raw_text = "k.A."
- error = None
-
- try:
- # RUFT die uebergebene Funktion zum Abrufen des Rohtexts auf.
- # Der retry_on_failure Decorator auf get_website_raw_func (der hoffentlich get_website_raw ist)
- # behandelt Retries und die meisten Fehler.
- raw_text = get_website_raw_func(url) # <<< Ruft die uebergebene Funktion auf
-
- # Wenn die Funktion einen Fehler loggt und einen Fehlerstring im Ergebnis zurueckgibt,
- # wird dies hier als Fehler im Task markiert.
- if isinstance(raw_text, str) and (raw_text.startswith("k.A. (Fehler") or raw_text.startswith("FEHLER:")):
- error = f"Scraping Fehler (Details im Rohtext): {raw_text[:100]}..."
- # Der Fehler wurde bereits in get_website_raw geloggt, kein weiteres Logging hier noetig.
- # Das raw_text selbst enthaelt den Fehlerstring.
-
- elif not isinstance(raw_text, str) or not raw_text.strip():
- # Wenn die Funktion keinen String oder einen leeren String zurueckgibt
- error = "Scraping Task Fehler: Funktion gab keinen gueltigen String zurueck."
- raw_text = "k.A. (Extraktion fehlgeschlagen)" # Standard-Fehlerwert
-
- except Exception as e:
- # Dieser Block sollte jetzt sehr selten erreicht werden, da die uebergegebene Funktion
- # mit retry_on_failure die meisten Fehler abfangen sollte.
- # Wenn eine Exception hier durchkommt, ist es ein sehr unerwarteter Fehler im Task-Handling selbst.
- error = f"Unerwarteter Fehler im Scraping Task Zeile {row_num} ({url[:100]}): {type(e).__name__} - {e}" # Gekuerzt loggen
- logger.error(error) # Loggen Sie diesen unerwarteten Fehler
- raw_text = "k.A. (Unerwarteter Fehler Task)" # Setze einen spezifischen Fehlerwert
-
-
- # logger.debug(f"Scraping Task Zeile {row_num} abgeschlossen. Textlaenge: {len(str(raw_text))}.") # Zu viel Laerm im Debug
- return {"row_num": row_num, "raw_text": raw_text, "error": error}
-
-
# --- Hauptlogik: Iteriere und sammle Batches ---
# Holen Sie die Batch-Groesse fuer Verarbeitung (Threading) aus Config (Block 1)
processing_batch_size = getattr(Config, 'PROCESSING_BATCH_SIZE', 20)
@@ -6956,7 +5494,7 @@ class DataProcessor:
log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row)
if log_check:
company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map
- self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Scraping Check): AR leer/default? {ar_is_empty_or_default}, D gueltig? {website_url_is_valid_looking}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen
+ self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Scraping Check): AR leer/default? {ar_is_empty_or_default}, D gueltig? {website_url_is_valid_looking}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT
# Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
@@ -6973,7 +5511,7 @@ class DataProcessor:
# Pruefe das Limit fuer verarbeitete Zeilen
if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit:
# Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_website_scraping_batch erreicht. Breche weitere Zeilenpruefung ab.")
+ self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_website_scraping_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT
break # Brich die Schleife ab
@@ -6990,18 +5528,18 @@ class DataProcessor:
# Logge den Start der Batch-Verarbeitung
batch_start_row = tasks_for_processing_batch[0]['row_num']
batch_end_row = tasks_for_processing_batch[-1]['row_num']
- self.logger.debug(f"\n--- Starte Website-Scraping Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")
+ self.logger.debug(f"\n--- Starte Website-Scraping Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT
scraping_results = {} # Dictionary zum Speichern der Ergebnisse {row_num: raw_text}
batch_error_count = 0 # Fehlerzaehler fuer diesen spezifischen Batch
- self.logger.debug(f" Scrape {len(tasks_for_processing_batch)} Websites parallel (max {max_scraping_workers} worker)...")
+ self.logger.debug(f" Scrape {len(tasks_for_processing_batch)} Websites parallel (max {max_scraping_workers} worker)...") # <<< GEÄNDERT
# Nutzt concurrent.futures.ThreadPoolExecutor fuer paralleles Scraping.
# max_workers wird aus Config geholt (Block 1).
with concurrent.futures.ThreadPoolExecutor(max_workers=max_scraping_workers) as executor:
# Map tasks to futures. Ruft die INTERNE Worker-Funktion auf.
# Uebergibt das task_info Dictionary und die globale Funktion get_website_raw (Block 11) als Argument.
- future_to_task = {executor.submit(scrape_raw_text_task, task, get_website_raw): task for task in tasks_for_processing_batch}
+ future_to_task = {executor.submit(self._scrape_raw_text_task, task, get_website_raw): task for task in tasks_for_processing_batch} # <<< Korrigiert: interne Methode
# Verarbeite die Ergebnisse, sobald sie fertig sind.
for future in concurrent.futures.as_completed(future_to_task):
@@ -7020,13 +5558,13 @@ class DataProcessor:
# Die meisten Fehler sollten von get_website_raws retry/logging behandelt werden.
row_num = task['row_num'] # Zeilennummer aus den Task-Daten
err_msg = f"Unerwarteter Fehler bei Ergebnisabfrage Scraping Task Zeile {row_num} ({task['url'][:100]}): {type(exc).__name__} - {exc}" # Gekuerzt loggen
- logger.error(err_msg) # Logge den Fehler
+ self.logger.error(err_msg) # <<< GEÄNDERT
# Setze einen Standard-Fehlerwert fuer diese Zeile im Ergebnis
scraping_results[row_num] = "k.A. (Unerwarteter Fehler Task)"
batch_error_count += 1 # Erhoehe den Fehlerzaehler
- self.logger.debug(f" Scraping fuer Batch beendet. {len(scraping_results)} Ergebnisse erhalten ({batch_error_count} Fehler in diesem Batch).")
+ self.logger.debug(f" Scraping fuer Batch beendet. {len(scraping_results)} Ergebnisse erhalten ({batch_error_count} Fehler in diesem Batch).") # <<< GEÄNDERT
# Sammle Sheet Updates (AR, AT, AP) fuer diesen Batch.
@@ -7065,12 +5603,12 @@ class DataProcessor:
rows_in_update_batch = len(all_sheet_updates) // 3 # Ganzzahl-Division
if rows_in_update_batch >= update_batch_row_limit:
- self.logger.debug(f" Sende gesammelte Sheet-Updates ({rows_in_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...")
+ self.logger.debug(f" Sende gesammelte Sheet-Updates ({rows_in_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
# Wenn es fehlschlaegt, wird es intern geloggt.
success = self.sheet_handler.batch_update_cells(all_sheet_updates)
if success:
- self.logger.info(f" Sheet-Update fuer {rows_in_update_batch} Zeilen erfolgreich.")
+ self.logger.info(f" Sheet-Update fuer {rows_in_update_batch} Zeilen erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# Leere die gesammelten Updates nach dem Senden.
@@ -7089,16 +5627,15 @@ class DataProcessor:
# Logge den Start des finalen Batches
batch_start_row = tasks_for_processing_batch[0]['row_num']
batch_end_row = tasks_for_processing_batch[-1]['row_num']
- self.logger.debug(f"\n--- Starte FINALEN Website-Scraping Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")
+ self.logger.debug(f"\n--- Starte FINALEN Website-Scraping Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT
scraping_results = {} # Dictionary fuer die Ergebnisse
batch_error_count = 0 # Fehlerzaehler
- self.logger.debug(f" Scrape {len(tasks_for_processing_batch)} Websites parallel (max {max_scraping_workers} worker)...")
+ self.logger.debug(f" Scrape {len(tasks_for_processing_batch)} Websites parallel (max {max_scraping_workers} worker)...") # <<< GEÄNDERT
with concurrent.futures.ThreadPoolExecutor(max_workers=max_scraping_workers) as executor:
- # Map tasks to futures. Ruft die GLOBALE Worker-Funktion auf.
- # Uebergibt das task_info Dictionary und die globale Funktion get_website_raw (Block 11) als Argument.
- future_to_task = {executor.submit(_scrape_raw_text_task_global, task, get_website_raw): task for task in tasks_for_processing_batch}
+ # Map tasks to futures. Ruft die INTERNE Worker-Funktion auf.
+ future_to_task = {executor.submit(self._scrape_raw_text_task, task, get_website_raw): task for task in tasks_for_processing_batch} # <<< Korrigiert: interne Methode
# Verarbeite die Ergebnisse
for future in concurrent.futures.as_completed(future_to_task):
@@ -7112,13 +5649,13 @@ class DataProcessor:
# Faengt unerwartete Fehler bei der Ergebnisabfrage ab
row_num = task['row_num']
err_msg = f"Unerwarteter Fehler bei Ergebnisabfrage Scraping Task Zeile {row_num} ({task['url'][:100]}): {type(exc).__name__} - {exc}" # Gekuerzt loggen
- logger.error(err_msg) # Logge den Fehler
+ self.logger.error(err_msg) # <<< GEÄNDERT
# Setze einen Standard-Fehlerwert
scraping_results[row_num] = "k.A. (Unerwarteter Fehler Task)"
batch_error_count += 1
- self.logger.debug(f" FINALER Scraping Batch beendet. {len(scraping_results)} Ergebnisse erhalten ({batch_error_count} Fehler).")
+ self.logger.debug(f" FINALER Scraping Batch beendet. {len(scraping_results)} Ergebnisse erhalten ({batch_error_count} Fehler).") # <<< GEÄNDERT
# Sammle Sheet Updates (AR, AT, AP) fuer diesen finalen Batch.
if scraping_results:
@@ -7139,16 +5676,16 @@ class DataProcessor:
# Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update.
if all_sheet_updates:
rows_in_final_update_batch = len(all_sheet_updates) // 3 # Updates pro Zeile ist 3
- self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({rows_in_final_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...")
+ self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({rows_in_final_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
success = self.sheet_handler.batch_update_cells(all_sheet_updates)
if success:
- self.logger.info(f"FINALES Sheet-Update erfolgreich.")
+ self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# Logge den Abschluss des Modus
- self.logger.info(f"Website-Scraping (Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.")
+ self.logger.info(f"Website-Scraping (Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT
# Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.
@@ -7181,30 +5718,30 @@ class DataProcessor:
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
# Logge die Konfiguration des Batch-Laufs
- self.logger.info(f"Starte Website-Zusammenfassung (Batch AS, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
+ self.logger.info(f"Starte Website-Zusammenfassung (Batch AS, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT
# --- Daten laden und Startzeile ermitteln ---
# Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt
if start_sheet_row is None:
- self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AS...")
+ self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AS...") # <<< GEÄNDERT
# Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AS (Block 1 Column Map).
# Standardmaessig ab Zeile 7
start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Website Zusammenfassung", min_sheet_row=7)
# Wenn get_start_row_index -1 zurueckgibt (Fehler)
if start_data_index_no_header == -1:
- self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")
+ self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT
return # Beende die Methode
# Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index
start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut
- self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AS Zelle): {start_sheet_row}")
+ self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AS Zelle): {start_sheet_row}") # <<< GEÄNDERT
else:
# Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
# Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
if not self.sheet_handler.load_data():
- self.logger.error("FEHLER beim Laden der Daten fuer process_summarization_batch.")
+ self.logger.error("FEHLER beim Laden der Daten fuer process_summarization_batch.") # <<< GEÄNDERT
return # Beende die Methode, wenn das Laden fehlschlaegt
@@ -7220,11 +5757,11 @@ class DataProcessor:
end_sheet_row = total_sheet_rows # Bis zur letzten Zeile
# Logge den verarbeitungsbereich
- self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
+ self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT
# Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen)
if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
- self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
+ self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT
return # Beende die Methode, wenn der Bereich leer ist
@@ -7239,7 +5776,7 @@ class DataProcessor:
# Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden
if None in col_indices.values():
missing = [k for k, v in col_indices.items() if v is None]
- self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_summarization_batch: {missing}. Breche ab.")
+ self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_summarization_batch: {missing}. Breche ab.") # <<< GEÄNDERT
return # Beende die Methode bei kritischem Fehler
# Ermitteln Sie die Indizes und Buchstaben fuer Updates (AS, AP)
@@ -7309,7 +5846,7 @@ class DataProcessor:
log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row)
if log_check:
company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map
- self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Summarization Check): AR gueltig? {raw_text_is_valid} (len={len(str(raw_text))}), AS leer/default? {summary_is_empty_or_default}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen
+ self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Summarization Check): AR gueltig? {raw_text_is_valid} (len={len(str(raw_text))}), AS leer/default? {summary_is_empty_or_default}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT
# Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
@@ -7324,7 +5861,7 @@ class DataProcessor:
# Pruefe das Limit fuer verarbeitete Zeilen
if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit:
# Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_summarization_batch erreicht. Breche weitere Zeilenpruefung ab.")
+ self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_summarization_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT
break # Brich die Schleife ab
@@ -7341,18 +5878,15 @@ class DataProcessor:
# Logge den Start der Batch-Verarbeitung
batch_start_row = tasks_for_openai_batch[0]['row_num']
batch_end_row = tasks_for_openai_batch[-1]['row_num']
- self.logger.debug(f"\n--- Starte Website-Summarization Batch ({len(tasks_for_openai_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")
+ self.logger.debug(f"\n--- Starte Website-Summarization Batch ({len(tasks_for_openai_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT
# Rufe die globale Funktion auf, die den OpenAI Call fuer den Batch macht (Block 9).
# summarize_batch_openai ist mit retry_on_failure dekoriert (Block 2).
# Wenn summarize_batch_openai eine Exception wirft (nach Retries), wird diese hier gefangen.
- batch_results = self._process_verification_openai_batch(current_openai_batch_data) # <-- Falsche Methode aufgerufen! MUSS summarize_batch_openai sein.
- # TODO: Diesen Aufruf zu summarize_batch_openai aendern!
-
# !!! KORRIGIERTER AUFRUF !!!
try:
# Rufen Sie die korrekte globale Funktion auf
- batch_results = summarize_batch_openai(current_openai_batch_data) # <<< Korrekter Aufruf Block 9
+ batch_results = summarize_batch_openai(tasks_for_openai_batch) # <<< Korrigierter Aufruf (vorher war fälschlicherweise _process_verification_openai_batch)
# Ergebnisse sollten ein Dictionary {row_num: summary_text} sein, auch bei Fehlern.
# Sammle Sheet Updates (AS, AP) fuer diesen Batch
@@ -7383,9 +5917,9 @@ class DataProcessor:
except Exception as e_openai_batch:
# Wenn summarize_batch_openai eine Exception wirft (nach Retries)
# Der Fehler wird bereits vom retry_on_failure Decorator auf summarize_batch_openai geloggt.
- self.logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Zusammenfassung: {e_openai_batch}")
+ self.logger.error(f"Endgueltiger FEHLER beim OpenAI-Batch-Aufruf fuer Zusammenfassung: {e_openai_batch}") # <<< GEÄNDERT
# Logge den Traceback
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
# Fügen Sie Fehlerwerte für alle Zeilen im Batch hinzu
current_version = getattr(Config, 'VERSION', 'unknown') # Block 1 Config Attribut
for row_num in rows_in_current_openai_batch:
@@ -7405,12 +5939,12 @@ class DataProcessor:
rows_in_update_batch = len(all_sheet_updates) // 2
if rows_in_update_batch >= update_batch_row_limit:
- self.logger.debug(f" Sende gesammelte Sheet-Updates ({rows_in_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...")
+ self.logger.debug(f" Sende gesammelte Sheet-Updates ({rows_in_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
# Wenn es fehlschlaegt, wird es intern geloggt.
success = self.sheet_handler.batch_update_cells(all_sheet_updates)
if success:
- self.logger.info(f" Sheet-Update fuer {rows_in_update_batch} Zeilen erfolgreich.")
+ self.logger.info(f" Sheet-Update fuer {rows_in_update_batch} Zeilen erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# Leere die gesammelten Updates nach dem Senden.
@@ -7419,24 +5953,24 @@ class DataProcessor:
# Kurze Pause nach jedem OpenAI Batch (nutzt Config Block 1).
# Dies ist wichtig, um Rate Limits zu vermeiden.
pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.5 # 50% der Retry-Wartezeit
- self.logger.debug(f"Warte {pause_duration:.2f}s vor naechstem Batch...")
+ self.logger.debug(f"Warte {pause_duration:.2f}s vor naechstem Batch...") # <<< GEÄNDERT
time.sleep(pause_duration)
# --- Verarbeitung des letzten unvollstaendigen OpenAI Batches nach der Schleife ---
# Wenn nach der Hauptschleife noch Tasks in der Batch-Liste sind.
- if current_openai_batch_data:
+ if tasks_for_openai_batch: # Korrektur: War vorher `current_openai_batch_data`
# Logge den Start des finalen Batches
- batch_start_row = current_openai_batch_data[0]['row_num']
- batch_end_row = current_openai_batch_data[-1]['row_num']
- self.logger.debug(f"\n--- Starte FINALEN Website-Summarization Batch ({len(current_openai_batch_data)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")
+ batch_start_row = tasks_for_openai_batch[0]['row_num'] # Korrektur: War vorher `current_openai_batch_data`
+ batch_end_row = tasks_for_openai_batch[-1]['row_num'] # Korrektur: War vorher `current_openai_batch_data`
+ self.logger.debug(f"\n--- Starte FINALEN Website-Summarization Batch ({len(tasks_for_openai_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT
# Rufe die globale Funktion auf, die den OpenAI Call fuer den Batch macht (Block 9).
# summarize_batch_openai ist mit retry_on_failure dekoriert (Block 2).
# Wenn summarize_batch_openai eine Exception wirft (nach Retries), wird diese hier gefangen.
batch_results = None
try:
- batch_results = summarize_batch_openai(current_openai_batch_data) # <<< Korrekter Aufruf Block 9
+ batch_results = summarize_batch_openai(tasks_for_openai_batch) # <<< Korrekter Aufruf Block 9, Korrektur: War vorher `current_openai_batch_data`
# Ergebnisse sollten ein Dictionary {row_num: summary_text} sein, auch bei Fehlern.
# Sammle Sheet Updates (AS, AP) fuer diesen finalen Batch
@@ -7467,9 +6001,9 @@ class DataProcessor:
except Exception as e_openai_batch:
# Wenn summarize_batch_openai eine Exception wirft (nach Retries)
# Der Fehler wird bereits vom retry_on_failure Decorator auf summarize_batch_openai geloggt.
- self.logger.error(f"Endgueltiger FEHLER beim FINALEN OpenAI-Batch-Aufruf fuer Zusammenfassung: {e_openai_batch}")
+ self.logger.error(f"Endgueltiger FEHLER beim FINALEN OpenAI-Batch-Aufruf fuer Zusammenfassung: {e_openai_batch}") # <<< GEÄNDERT
# Logge den Traceback
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
# Fügen Sie Fehlerwerte für alle Zeilen im Batch hinzu
current_version = getattr(Config, 'VERSION', 'unknown') # Block 1 Config Attribut
for row_num in rows_in_current_openai_batch:
@@ -7483,16 +6017,16 @@ class DataProcessor:
# Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update.
if all_sheet_updates:
rows_in_final_update_batch = len(all_sheet_updates) // 2 # Updates pro Zeile ist 2
- self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({rows_in_final_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...")
+ self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({rows_in_final_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
success = self.sheet_handler.batch_update_cells(all_sheet_updates)
if success:
- self.logger.info(f"FINALES Sheet-Update erfolgreich.")
+ self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# Logge den Abschluss des Modus
- self.logger.info(f"Website-Zusammenfassung (Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.")
+ self.logger.info(f"Website-Zusammenfassung (Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT
# Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.
# ==============================================================================
@@ -7522,6 +6056,7 @@ class DataProcessor:
dict: Ergebnis von evaluate_branche_chatgpt (Block 10) plus row_num und error.
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
+ logger = logging.getLogger(__name__ + ".evaluate_branch_task") # Eigener Logger für den Task
row_num = task_data['row_num']
# Initialisiere Ergebnis mit Fehlerwerten, falls der Task fehlschlaegt
result = {"branch": "k.A. (Fehler Task)", "consistency": "error", "justification": "Fehler in Worker-Task"}
@@ -7547,9 +6082,9 @@ class DataProcessor:
# Wenn evaluate_branche_chatgpt eine Exception wirft (nach Retries)
# Der Fehler wird bereits vom retry_on_failure Decorator oder evaluate_branche_chatgpt geloggt.
error = f"Fehler bei Branchenevaluation Zeile {row_num}: {type(e).__name__} - {e}"
- self.logger.error(error) # Logge den Fehler
+ logger.error(error) # Logge den Fehler
# Logge den Traceback
- self.logger.debug(traceback.format_exc())
+ logger.debug(traceback.format_exc())
# Stellen Sie sicher, dass das Ergebnis-Dict im Fehlerfall spezifische Fehlerwerte enthaelt
result = {"branch": "FEHLER", "consistency": "error_task", "justification": error[:500]} # Kuerze Begruendung
@@ -7579,30 +6114,30 @@ class DataProcessor:
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
# Logge die Konfiguration des Batch-Laufs
- self.logger.info(f"Starte Brancheneinschaetzung (Parallel Batch W-Y, AO, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
+ self.logger.info(f"Starte Brancheneinschaetzung (Parallel Batch W-Y, AO, AP). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT
# --- Daten laden und Startzeile ermitteln ---
# Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt
if start_sheet_row is None:
- self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AO...")
+ self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AO...") # <<< GEÄNDERT
# Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AO (Block 1 Column Map).
# Standardmaessig ab Zeile 7
start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Pruefung", min_sheet_row=7)
# Wenn get_start_row_index -1 zurueckgibt (Fehler)
if start_data_index_no_header == -1:
- self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")
+ self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT
return # Beende die Methode
# Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index
start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut
- self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AO Zelle): {start_sheet_row}")
+ self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AO Zelle): {start_sheet_row}") # <<< GEÄNDERT
else:
# Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
# Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
if not self.sheet_handler.load_data():
- self.logger.error("FEHLER beim Laden der Daten fuer process_branch_batch.")
+ self.logger.error("FEHLER beim Laden der Daten fuer process_branch_batch.") # <<< GEÄNDERT
return # Beende die Methode, wenn das Laden fehlschlaegt
@@ -7618,11 +6153,11 @@ class DataProcessor:
end_sheet_row = total_sheet_rows # Bis zur letzten Zeile
# Logge den verarbeitungsbereich
- self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
+ self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT
# Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen)
if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
- self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
+ self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT
return # Beende die Methode, wenn der Bereich leer ist
@@ -7640,7 +6175,7 @@ class DataProcessor:
# Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden
if None in col_indices.values():
missing = [k for k, v in col_indices.items() if v is None]
- self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_branch_batch: {missing}. Breche ab.")
+ self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_branch_batch: {missing}. Breche ab.") # <<< GEÄNDERT
return # Beende die Methode bei kritischem Fehler
# Ermitteln Sie die Spaltenbuchstaben fuer Updates (W, X, Y, AO, AP) (nutzt interne Helfer _get_col_letter Block 14)
@@ -7683,7 +6218,7 @@ class DataProcessor:
# Pruefe erneut, ob das Schema geladen wurde
if not ALLOWED_TARGET_BRANCHES:
- self.logger.critical("FEHLER: Ziel-Branchenschema konnte nach Ladeversuch nicht geladen werden. Branchenbewertung nicht moeglich. Breche Batch ab.")
+ self.logger.critical("FEHLER: Ziel-Branchenschema konnte nach Ladeversuch nicht geladen werden. Branchenbewertung nicht moeglich. Breche Batch ab.") # <<< GEÄNDERT
return # Beende die Methode
@@ -7736,7 +6271,7 @@ class DataProcessor:
# Wenn nicht genuegend Informationsquellen verfuegbar sind
if info_sources_count < 2: # Mindestens 2 Info-Punkte sollten vorhanden sein (kann angepasst werden)
- self.logger.debug(f"Zeile {i} (Branch Check): Uebersprungen (AO leer, aber nur {info_sources_count} Informationsquellen verfuegbar). Mindestens 2 benoetigt.")
+ self.logger.debug(f"Zeile {i} (Branch Check): Uebersprungen (AO leer, aber nur {info_sources_count} Informationsquellen verfuegbar). Mindestens 2 benoetigt.") # <<< GEÄNDERT
skipped_count += 1 # Zaehlen als uebersprungene Zeile
continue # Springe zur naechsten Zeile
@@ -7747,7 +6282,7 @@ class DataProcessor:
# Pruefe das Limit fuer verarbeitete Zeilen
if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit:
# Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_branch_batch erreicht. Breche weitere Zeilenpruefung ab.")
+ self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_branch_batch erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT
break # Brich die Schleife ab
@@ -7772,13 +6307,13 @@ class DataProcessor:
# Logge den Start der Batch-Verarbeitung
batch_start_row = tasks_for_processing_batch[0]['row_num']
batch_end_row = tasks_for_processing_batch[-1]['row_num']
- self.logger.debug(f"\n--- Starte Branch-Evaluation Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")
+ self.logger.debug(f"\n--- Starte Branch-Evaluation Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT
results_list = [] # Liste zum Speichern der Ergebnisse fuer diesen Batch (Liste von Dicts)
batch_error_count = 0 # Fehlerzaehler fuer diesen spezifischen Batch
- self.logger.debug(f" Evaluiere {len(tasks_for_processing_batch)} Zeilen parallel (max {MAX_BRANCH_WORKERS} worker, {OPENAI_CONCURRENCY_LIMIT} OpenAI gleichzeitig)...")
+ self.logger.debug(f" Evaluiere {len(tasks_for_processing_batch)} Zeilen parallel (max {MAX_BRANCH_WORKERS} worker, {OPENAI_CONCURRENCY_LIMIT} OpenAI gleichzeitig)...") # <<< GEÄNDERT
# Holen Sie die Parallelisierungskonfiguration aus Config (Block 1).
MAX_BRANCH_WORKERS = getattr(Config, 'MAX_BRANCH_WORKERS', 10)
OPENAI_CONCURRENCY_LIMIT = getattr(Config, 'OPENAI_CONCURRENCY_LIMIT', 3)
@@ -7811,14 +6346,14 @@ class DataProcessor:
# Die meisten Fehler sollten von evaluate_branch_task oder seinen Helfern behandelt werden.
row_num = task['row_num'] # Zeilennummer aus den Task-Daten
err_msg = f"Unerwarteter Fehler bei Ergebnisabfrage Branch Task Zeile {row_num}: {type(exc).__name__} - {exc}"
- logger.error(err_msg) # Logge den Fehler
+ self.logger.error(err_msg) # <<< GEÄNDERT
# Setze einen Standard-Fehler-Ergebniswert fuer diese Zeile
results_list.append({"row_num": row_num, "result": {"branch": "FEHLER", "consistency": "error_task", "justification": err_msg[:500]}, "error": err_msg}) # Kuerze Begruendung
batch_error_count += 1 # Erhoehe den Fehlerzaehler
# *** ENDE PARALLELE VERARBEITUNG ***
- self.logger.debug(f" Branch-Evaluation fuer Batch beendet. {len(results_list)} Ergebnisse erhalten ({batch_error_count} Fehler in diesem Batch).")
+ self.logger.debug(f" Branch-Evaluation fuer Batch beendet. {len(results_list)} Ergebnisse erhalten ({batch_error_count} Fehler in diesem Batch).") # <<< GEÄNDERT
# Sheet Updates vorbereiten FÜR DIESEN BATCH.
@@ -7854,12 +6389,12 @@ class DataProcessor:
# --- Sende Updates fuer DIESEN BATCH SOFORT ---
# Sende die gesammelten Updates fuer diesen Batch.
if batch_sheet_updates:
- self.logger.debug(f" Sende Sheet-Update fuer {len(results_list)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...")
+ self.logger.debug(f" Sende Sheet-Update fuer {len(results_list)} Zeilen ({len(batch_sheet_updates)} Zellen) dieses Batches...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
# Wenn es fehlschlaegt, wird es intern geloggt.
success = self.sheet_handler.batch_update_cells(batch_sheet_updates)
if success:
- self.logger.info(f" Sheet-Update fuer Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.")
+ self.logger.info(f" Sheet-Update fuer Batch Zeilen {batch_start_row}-{batch_end_row} erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# else: self.logger.debug(f" Keine Sheet-Updates fuer Batch Zeilen {batch_start_row}-{batch_end_row} vorbereitet.") # Zu viel Laerm im Debug
@@ -7872,7 +6407,7 @@ class DataProcessor:
# Pause NACHDEM ein Batch komplett verarbeitet und geschrieben wurde (nutzt Config Block 1).
# Dies ist wichtig, um Rate Limits und Serverlast zu managen.
pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.8 # Längere Pause, z.B. 80% der Retry-Wartezeit
- self.logger.debug(f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---")
+ self.logger.debug(f"--- Batch Zeilen {batch_start_row}-{batch_end_row} abgeschlossen. Warte {pause_duration:.2f}s vor naechstem Batch ---") # <<< GEÄNDERT
time.sleep(pause_duration)
@@ -7882,13 +6417,13 @@ class DataProcessor:
# Logge den Start des finalen Batches
batch_start_row = tasks_for_processing_batch[0]['row_num']
batch_end_row = tasks_for_processing_batch[-1]['row_num']
- self.logger.debug(f"\n--- Starte FINALEN Branch-Evaluation Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---")
+ self.logger.debug(f"\n--- Starte FINALEN Branch-Evaluation Batch ({len(tasks_for_processing_batch)} Tasks, Zeilen {batch_start_row}-{batch_end_row}) ---") # <<< GEÄNDERT
results_list = [] # Liste zum Speichern der Ergebnisse fuer diesen finalen Batch
batch_error_count = 0 # Fehlerzaehler
- self.logger.debug(f" Evaluiere {len(tasks_for_processing_batch)} Zeilen parallel (max {MAX_BRANCH_WORKERS} worker, {OPENAI_CONCURRENCY_LIMIT} OpenAI gleichzeitig)...")
+ self.logger.debug(f" Evaluiere {len(tasks_for_processing_batch)} Zeilen parallel (max {MAX_BRANCH_WORKERS} worker, {OPENAI_CONCURRENCY_LIMIT} OpenAI gleichzeitig)...") # <<< GEÄNDERT
# Erstellen Sie die Semaphore Instanz fuer den finalen Batch.
openai_semaphore_branch = threading.Semaphore(OPENAI_CONCURRENCY_LIMIT)
@@ -7908,13 +6443,13 @@ class DataProcessor:
# Faengt unerwartete Fehler bei der Ergebnisabfrage ab
row_num = task['row_num']
err_msg = f"Unerwarteter Fehler bei Ergebnisabfrage Branch Task Zeile {row_num}: {type(exc).__name__} - {exc}"
- logger.error(err_msg) # Logge den Fehler
+ self.logger.error(err_msg) # <<< GEÄNDERT
# Setze einen Standard-Fehler-Ergebniswert
results_list.append({"row_num": row_num, "result": {"branch": "FEHLER", "consistency": "error_task", "justification": err_msg[:500]}, "error": err_msg}) # Kuerze Begruendung
batch_error_count += 1
- self.logger.debug(f" FINALER Branch Batch beendet. {len(results_list)} Ergebnisse erhalten ({batch_error_count} Fehler).")
+ self.logger.debug(f" FINALER Branch Batch beendet. {len(results_list)} Ergebnisse erhalten ({batch_error_count} Fehler).") # <<< GEÄNDERT
# Sammle Sheet Updates (W, X, Y, AO, AP) fuer diesen finalen Batch.
@@ -7937,16 +6472,16 @@ class DataProcessor:
# Sende die gesammelten Updates fuer DIESEN finalen Batch.
if batch_sheet_updates:
- self.logger.debug(f" Sende FINALES Sheet-Update fuer {len(results_list)} Zeilen ({len(batch_sheet_updates)} Zellen)...")
+ self.logger.debug(f" Sende FINALES Sheet-Update fuer {len(results_list)} Zeilen ({len(batch_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
success = self.sheet_handler.batch_update_cells(batch_sheet_updates)
if success:
- self.logger.info(f" FINALES Sheet-Update fuer Branch Batch erfolgreich.")
+ self.logger.info(f" FINALES Sheet-Update fuer Branch Batch erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# Logge den Abschluss des Modus
- self.logger.info(f"Brancheneinschaetzung (Parallel Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.")
+ self.logger.info(f"Brancheneinschaetzung (Parallel Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT
# Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.
@@ -7983,30 +6518,30 @@ class DataProcessor:
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
# Logge die Konfiguration des Batch-Laufs
- self.logger.info(f"Starte Modus 'find_wiki_serp' (AY, M, A). Filter: (Umsatz CRM > {min_umsatz} MIO € ODER Mitarbeiter CRM > {min_employees}). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
+ self.logger.info(f"Starte Modus 'find_wiki_serp' (AY, M, A). Filter: (Umsatz CRM > {min_umsatz} MIO € ODER Mitarbeiter CRM > {min_employees}). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT
# --- Daten laden und Startzeile ermitteln ---
# Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt
if start_sheet_row is None:
- self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AY...")
+ self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AY...") # <<< GEÄNDERT
# Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AY (Block 1 Column Map).
# Standardmaessig ab Zeile 7
start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="SerpAPI Wiki Search Timestamp", min_sheet_row=7)
# Wenn get_start_row_index -1 zurueckgibt (Fehler)
if start_data_index_no_header == -1:
- self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")
+ self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT
return # Beende die Methode
# Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index
start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut
- self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AY Zelle): {start_sheet_row}")
+ self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AY Zelle): {start_sheet_row}") # <<< GEÄNDERT
else:
# Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
# Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
if not self.sheet_handler.load_data():
- self.logger.error("FEHLER beim Laden der Daten fuer process_find_wiki_serp.")
+ self.logger.error("FEHLER beim Laden der Daten fuer process_find_wiki_serp.") # <<< GEÄNDERT
return # Beende die Methode, wenn das Laden fehlschlaegt
@@ -8022,11 +6557,11 @@ class DataProcessor:
end_sheet_row = total_sheet_rows # Bis zur letzten Zeile
# Logge den verarbeitungsbereich
- self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
+ self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT
# Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen)
if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
- self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
+ self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT
return # Beende die Methode, wenn der Bereich leer ist
@@ -8046,7 +6581,7 @@ class DataProcessor:
# Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden
if None in col_indices.values():
missing = [k for k, v in col_indices.items() if v is None]
- self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_find_wiki_serp: {missing}. Breche ab.")
+ self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_find_wiki_serp: {missing}. Breche ab.") # <<< GEÄNDERT
return # Beende die Methode bei kritischem Fehler
# Ermitteln Sie die Spaltenbuchstaben fuer Updates und Leerung (nutzt interne Helfer _get_col_letter Block 14)
@@ -8141,7 +6676,7 @@ class DataProcessor:
log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row)
if log_check:
company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map
- self.logger.debug(f"Zeile {i} ({company_name[:50]}... SerpAPI Wiki Search Check): AY leer? {is_ay_empty}, M leer/k.A.? {is_m_empty_or_ka}, Groesse ({umsatz_val_mio:.1f} Mio, {ma_val_num} MA) Kriterium ({min_umsatz} Mio, {min_employees} MA)? {size_criteria_met}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen
+ self.logger.debug(f"Zeile {i} ({company_name[:50]}... SerpAPI Wiki Search Check): AY leer? {is_ay_empty}, M leer/k.A.? {is_m_empty_or_ka}, Groesse ({umsatz_val_mio:.1f} Mio, {ma_val_num} MA) Kriterium ({min_umsatz} Mio, {min_employees} MA)? {size_criteria_met}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT
# Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
@@ -8156,7 +6691,7 @@ class DataProcessor:
# Pruefe das Limit fuer verarbeitete Zeilen
if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit:
# Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_find_wiki_serp erreicht. Breche weitere Zeilenpruefung ab.")
+ self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_find_wiki_serp erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT
break # Brich die Schleife ab
@@ -8166,16 +6701,18 @@ class DataProcessor:
# Wenn kein Firmenname vorhanden ist, kann die Suche nicht durchgefuehrt werden
if not company_name:
- self.logger.warning(f"Zeile {i}: Uebersprungen (kein Firmenname fuer Suche vorhanden in Spalte B).")
+ self.logger.warning(f"Zeile {i}: Uebersprungen (kein Firmenname fuer Suche vorhanden in Spalte B).") # <<< GEÄNDERT
skipped_count += 1 # Zaehlen als uebersprungene Zeile, da Suche nicht moeglich
# Setze AY Timestamp auch hier, um nicht immer wieder zu versuchen
+ # Erstelle leeres Update-Dict, damit extend funktioniert
+ updates = []
updates.append({'range': f'{ts_ay_letter}{i}', 'values': [[now_timestamp_str]]}) # Block 1 Column Map
all_sheet_updates.extend(updates) # Fuege dieses einzelne Update zur Liste hinzu
updates = [] # Leere die lokale Liste
continue # Springe zur naechsten Zeile
- self.logger.info(f"Zeile {i}: Suche Wiki-URL fuer '{company_name[:100]}...' (Umsatz (Mio): {umsatz_val_mio:.1f}, MA: {ma_val_num}) ueber SerpAPI...") # Gekuerzt loggen
+ self.logger.info(f"Zeile {i}: Suche Wiki-URL fuer '{company_name[:100]}...' (Umsatz (Mio): {umsatz_val_mio:.1f}, MA: {ma_val_num}) ueber SerpAPI...") # <<< GEÄNDERT
# Führe die SerpAPI Suche durch (nutzt globale Funktion Block 10 mit Retry).
@@ -8188,7 +6725,7 @@ class DataProcessor:
except Exception as e_serp_wiki:
# Wenn serp_wikipedia_lookup eine Exception wirft (nach Retries)
- self.logger.error(f"FEHLER bei serp_wikipedia_lookup fuer Zeile {i} ('{company_name[:100]}...'): {e_serp_wiki}") # Gekuerzt loggen
+ self.logger.error(f"FEHLER bei serp_wikipedia_lookup fuer Zeile {i} ('{company_name[:100]}...'): {e_serp_wiki}") # <<< GEÄNDERT
# wiki_url_found bleibt None. Fahren Sie fort.
pass # Fahren Sie fort, um Timestamp zu setzen und Updates vorzubereiten
@@ -8202,8 +6739,8 @@ class DataProcessor:
# Wenn eine URL gefunden wurde, bereite weitere Updates vor.
# Eine gefundene URL ist ein String, der nicht None ist und nicht "k.A." oder Fehlerstring ist.
- if wiki_url_found and isinstance(wiki_url_found, str) and wiki_url_found.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"]: # Fuege "http:" hinzu
- self.logger.info(f" -> URL gefunden: {wiki_url_found[:100]}... Bereite Update vor (Setze M, A; Loesche N-V, AN, AO, AP, AX).") # Gekuerzt loggen
+ if wiki_url_found and isinstance(wiki_url_found, str) and wiki_url_found.lower() not in ["k.a.", "kein artikel gefunden"] and not wiki_url_found.startswith("FEHLER"): # Korrektur Pruefung
+ self.logger.info(f" -> URL gefunden: {wiki_url_found[:100]}... Bereite Update vor (Setze M, A; Loesche N-V, AN, AO, AP, AX).") # <<< GEÄNDERT
found_urls_count += 1 # Zaehle den Fund
@@ -8218,10 +6755,10 @@ class DataProcessor:
if nv_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte.
updates_for_row.append({'range': f'{n_letter}{i}:{v_letter}{i}', 'values': [empty_nv_values]}) # Block 1 Column Map, lokale Variable
else:
- self.logger.warning(f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.")
+ self.logger.warning(f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.") # <<< GEÄNDERT
- # Leere Timestamps AN, AO, AX, und Version AP.
+ # Leere Timestamps AN, AO, AP, AX.
# Dies setzt die Zeile zurueck, damit andere Schritte sie spaeter bearbeiten.
updates_for_row.append({'range': f'{an_letter}{i}', 'values': [['']]}) # Block 1 Column Map
updates_for_row.append({'range': f'{ao_letter}{i}', 'values': [['']]}) # Block 1 Column Map
@@ -8231,7 +6768,7 @@ class DataProcessor:
else:
# Wenn keine Wiki-URL ueber SerpAPI gefunden wurde
- self.logger.debug(f" -> Keine Wiki-URL fuer '{company_name[:100]}...' ueber SerpAPI gefunden.") # Gekuerzt loggen
+ self.logger.debug(f" -> Keine Wiki-URL fuer '{company_name[:100]}...' ueber SerpAPI gefunden.") # <<< GEÄNDERT
# Nur AY Timestamp wird gesetzt, was bereits oben passiert ist. Keine weiteren Updates fuer M, A, N-V etc.
@@ -8244,13 +6781,13 @@ class DataProcessor:
# Die Anzahl der Updates pro Zeile variiert (1 bei nicht gefunden, ca. 10+ bei gefunden).
# Pruefen Sie einfach die Laenge der gesammelten Liste.
if len(all_sheet_updates) >= update_batch_row_limit * 5: # Grobe Schaetzung: im Schnitt 5 Updates pro Zeile
- self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")
+ self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
# Wenn es fehlschlaegt, wird es intern geloggt.
success = self.sheet_handler.batch_update_cells(all_sheet_updates)
if success:
- self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.")
- # Der Fehlerfall wird von batch_update_cells geloggt
+ self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") # <<< GEÄNDERT
+ # Der Fehlerfall wird von batch_update_cells geloggt
# Leere die gesammelten Updates nach dem Senden.
all_sheet_updates = []
@@ -8266,16 +6803,16 @@ class DataProcessor:
# --- Finale Sheet Updates senden ---
# Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update.
if all_sheet_updates:
- self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")
+ self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
success = self.sheet_handler.batch_update_cells(all_sheet_updates)
if success:
- self.logger.info(f"FINALES Sheet-Update erfolgreich.")
+ self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# Logge den Abschluss des Modus
- self.logger.info(f"Modus 'find_wiki_serp' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {found_urls_count} URLs gefunden & eingetragen, {skipped_count} Zeilen uebersprungen.")
+ self.logger.info(f"Modus 'find_wiki_serp' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {found_urls_count} URLs gefunden & eingetragen, {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT
# Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.
@@ -8299,30 +6836,30 @@ class DataProcessor:
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
# Logge die Konfiguration des Batch-Laufs
- self.logger.info(f"Starte Contact Research (Batch AM, AI-AL). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
+ self.logger.info(f"Starte Contact Research (Batch AM, AI-AL). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT
# --- Daten laden und Startzeile ermitteln ---
# Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt
if start_sheet_row is None:
- self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AM...")
+ self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AM...") # <<< GEÄNDERT
# Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AM (Block 1 Column Map).
# Standardmaessig ab Zeile 7
start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Contact Search Timestamp", min_sheet_row=7)
# Wenn get_start_row_index -1 zurueckgibt (Fehler)
if start_data_index_no_header == -1:
- self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.")
+ self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Batch ab.") # <<< GEÄNDERT
return # Beende die Methode
# Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index
start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut
- self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AM Zelle): {start_sheet_row}")
+ self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AM Zelle): {start_sheet_row}") # <<< GEÄNDERT
else:
# Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
# Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
if not self.sheet_handler.load_data():
- self.logger.error("FEHLER beim Laden der Daten fuer process_contact_search.")
+ self.logger.error("FEHLER beim Laden der Daten fuer process_contact_search.") # <<< GEÄNDERT
return # Beende die Methode, wenn das Laden fehlschlaegt
@@ -8338,11 +6875,11 @@ class DataProcessor:
end_sheet_row = total_sheet_rows # Bis zur letzten Zeile
# Logge den verarbeitungsbereich
- self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
+ self.logger.info(f"Verarbeitungsbereich: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT
# Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen)
if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
- self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
+ self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT
return # Beende die Methode, wenn der Bereich leer ist
@@ -8360,7 +6897,7 @@ class DataProcessor:
# Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden
if None in col_indices.values():
missing = [k for k, v in col_indices.items() if v is None]
- self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_contact_search: {missing}. Breche ab.")
+ self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_contact_search: {missing}. Breche ab.") # <<< GEÄNDERT
return # Beende die Methode bei kritischem Fehler
@@ -8391,10 +6928,10 @@ class DataProcessor:
try:
# Versuche, das Sheet "Contacts" zu oeffnen
contacts_sheet = self.sheet_handler.sheet.spreadsheet.worksheet("Contacts")
- self.logger.info("Blatt 'Contacts' gefunden.")
+ self.logger.info("Blatt 'Contacts' gefunden.") # <<< GEÄNDERT
except gspread.exceptions.WorksheetNotFound:
# Wenn nicht gefunden, erstelle es.
- self.logger.info("Blatt 'Contacts' nicht gefunden, erstelle neu...")
+ self.logger.info("Blatt 'Contacts' nicht gefunden, erstelle neu...") # <<< GEÄNDERT
try:
# Definieren Sie den Header fuer das neue Blatt
contacts_header = ["Firmenname", "CRM Kurzform", "Website", "Geschlecht", "Vorname", "Nachname", "Position", "Suchbegriffskategorie", "E-Mail-Adresse", "LinkedIn-Link", "Timestamp"]
@@ -8405,18 +6942,18 @@ class DataProcessor:
# Schreiben Sie den Header in die erste Zeile des neuen Blattes
# Nutzt _get_col_letter interne Methode des SheetHandlers (Block 14)
contacts_sheet.update(values=[contacts_header], range_name=f"A1:{self.sheet_handler._get_col_letter(num_cols_contacts_sheet)}1")
- self.logger.info("Neues Blatt 'Contacts' erstellt und Header eingetragen.")
+ self.logger.info("Neues Blatt 'Contacts' erstellt und Header eingetragen.") # <<< GEÄNDERT
except Exception as e_create_sheet:
# Fange Fehler bei der Erstellung des Blattes ab und logge sie.
- self.logger.critical(f"FEHLER: Konnte Blatt 'Contacts' nicht erstellen: {e_create_sheet}. Kontakt-Details koennen NICHT gespeichert werden.")
+ self.logger.critical(f"FEHLER: Konnte Blatt 'Contacts' nicht erstellen: {e_create_sheet}. Kontakt-Details koennen NICHT gespeichert werden.") # <<< GEÄNDERT
# Logge den Traceback.
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
contacts_sheet = None # Setze contacts_sheet auf None, um spaetere Schreibversuche zu verhindern
else:
# Wenn SheetHandler oder Sheet-Objekt nicht verfuegbar war.
- self.logger.warning("SheetHandler oder Sheet-Objekt nicht verfuegbar. Kann Blatt 'Contacts' nicht oeffnen/erstellen. Kontakt-Details werden NICHT gespeichert.")
+ self.logger.warning("SheetHandler oder Sheet-Objekt nicht verfuegbar. Kann Blatt 'Contacts' nicht oeffnen/erstellen. Kontakt-Details werden NICHT gespeichert.") # <<< GEÄNDERT
contacts_sheet = None # Sicherstellen, dass contacts_sheet None ist
@@ -8481,7 +7018,7 @@ class DataProcessor:
log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row)
if log_check:
company_name_log = company_name[:50] + '...' if len(company_name) > 50 else company_name # Gekuerzt loggen
- self.logger.debug(f"Zeile {i} ({company_name_log} Contact Check): AM leer? {processing_needed_based_on_status}, Mindestdaten gueltig? {has_min_data_for_search}. Benötigt Verarbeitung? {processing_needed_for_row}")
+ self.logger.debug(f"Zeile {i} ({company_name_log} Contact Check): AM leer? {processing_needed_based_on_status}, Mindestdaten gueltig? {has_min_data_for_search}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT
# Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
@@ -8496,11 +7033,11 @@ class DataProcessor:
# Pruefe das Limit fuer verarbeitete Zeilen
if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit:
# Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_contact_search erreicht. Breche weitere Zeilenpruefung ab.")
+ self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_contact_search erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT
break # Brich die Schleife ab
- self.logger.info(f"Zeile {i}: Suche LinkedIn Kontakte fuer '{crm_kurzform[:50]}...' ({website[:50]}...)...") # Gekuerzt loggen
+ self.logger.info(f"Zeile {i}: Suche LinkedIn Kontakte fuer '{crm_kurzform[:50]}...' ({website[:50]}...)...") # <<< GEÄNDERT
all_found_contacts_for_row = [] # Liste zum Sammeln aller gefundenen Kontakte fuer DIESE Zeile (Liste von Dicts)
@@ -8516,7 +7053,7 @@ class DataProcessor:
found_contacts_in_category = {} # Dictionary zum Sammeln eindeutiger Kontakte {linkedin_url: contact_data} fuer diese Kategorie
for position_query in queries:
- self.logger.debug(f" -> Suche nach Position: '{position_query}' bei '{crm_kurzform[:50]}'...") # Gekuerzt loggen
+ self.logger.debug(f" -> Suche nach Position: '{position_query}' bei '{crm_kurzform[:50]}'...") # <<< GEÄNDERT
try:
# Rufe die globale Funktion search_linkedin_contacts auf (Block 10).
# Limitieren Sie die Anzahl der SerpAPI Ergebnisse pro Query, um Kosten zu managen.
@@ -8543,7 +7080,7 @@ class DataProcessor:
except Exception as e_linkedin_search:
# Wenn search_linkedin_contacts eine Exception wirft (nach Retries)
# Der Fehler wird bereits vom retry_on_failure Decorator oder search_linkedin_contacts geloggt.
- self.logger.error(f"FEHLER bei search_linkedin_contacts fuer Zeile {i} (Query: '{position_query}', Firma: '{crm_kurzform[:50]}...'): {e_linkedin_search}") # Gekuerzt loggen
+ self.logger.error(f"FEHLER bei search_linkedin_contacts fuer Zeile {i} (Query: '{position_query}', Firma: '{crm_kurzform[:50]}...'): {e_linkedin_search}") # <<< GEÄNDERT
pass # Faert fort mit der naechsten Query oder Kategorie
# Pause nach jeder SerpAPI Suche (pro position_query)
@@ -8576,7 +7113,7 @@ class DataProcessor:
# Sammeln Sie diese Updates fuer das Hauptblatt in der globalen Liste all_sheet_updates.
all_sheet_updates.extend(main_sheet_updates_for_row)
- self.logger.info(f"Zeile {i}: Kontaktzahlen gesammelt: {contact_counts_for_row} – Timestamp AM vorgemerkt fuer Update.")
+ self.logger.info(f"Zeile {i}: Kontaktzahlen gesammelt: {contact_counts_for_row} – Timestamp AM vorgemerkt fuer Update.") # <<< GEÄNDERT
# Bereiten Sie die Zeilen fuer das 'Contacts' Blatt vor (falls es existiert).
@@ -8618,9 +7155,9 @@ class DataProcessor:
if rows_to_append_to_contacts_sheet:
# Fuegen Sie diese Zeilen zur globalen Liste aller Kontakte hinzu, die spaeter angefuegt werden.
all_contact_rows_to_append.extend(rows_to_append_to_contacts_sheet)
- self.logger.debug(f" -> {len(rows_to_append_to_contacts_sheet)} eindeutige Kontakte fuer Zeile {i} zum Anfuegen an 'Contacts' vorgemerkt.")
+ self.logger.debug(f" -> {len(rows_to_append_to_contacts_sheet)} eindeutige Kontakte fuer Zeile {i} zum Anfuegen an 'Contacts' vorgemerkt.") # <<< GEÄNDERT
else:
- self.logger.debug(f" -> Keine neuen Kontakte fuer Zeile {i} gefunden.")
+ self.logger.debug(f" -> Keine neuen Kontakte fuer Zeile {i} gefunden.") # <<< GEÄNDERT
# Sende gesammelte Sheet Updates (Hauptblatt) wenn das Update-Batch-Limit erreicht ist.
@@ -8629,12 +7166,12 @@ class DataProcessor:
rows_in_main_sheet_update_batch = len(all_sheet_updates) // 5
if rows_in_main_sheet_update_batch >= update_batch_row_limit:
- self.logger.debug(f" Sende gesammelte Hauptblatt-Updates ({rows_in_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...")
+ self.logger.debug(f" Sende gesammelte Hauptblatt-Updates ({rows_in_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
# Wenn es fehlschlaegt, wird es intern geloggt.
success = self.sheet_handler.batch_update_cells(all_sheet_updates)
if success:
- self.logger.info(f" Hauptblatt-Update fuer {rows_in_main_sheet_update_batch} Zeilen erfolgreich.")
+ self.logger.info(f" Hauptblatt-Update fuer {rows_in_main_sheet_update_batch} Zeilen erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# Leere die gesammelten Updates nach dem Senden.
@@ -8645,7 +7182,7 @@ class DataProcessor:
# Dieser Modus ist API-intensiv und sollte langsamer laufen.
# Nutzt Config.RETRY_DELAY (Block 1).
pause_duration = getattr(Config, 'RETRY_DELAY', 10) * 0.8 # Laengere Pause, z.B. 80% der Retry-Wartezeit
- self.logger.debug(f"Warte {pause_duration:.2f}s nach Verarbeitung von Zeile {i}...")
+ self.logger.debug(f"Warte {pause_duration:.2f}s nach Verarbeitung von Zeile {i}...") # <<< GEÄNDERT
time.sleep(pause_duration)
@@ -8653,18 +7190,18 @@ class DataProcessor:
# Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update.
if all_sheet_updates:
rows_in_final_main_sheet_update_batch = len(all_sheet_updates) // 5
- self.logger.info(f"Sende FINALE gesammelte Hauptblatt-Updates ({rows_in_final_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...")
+ self.logger.info(f"Sende FINALE gesammelte Hauptblatt-Updates ({rows_in_final_main_sheet_update_batch} Zeilen, {len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
success = self.sheet_handler.batch_update_cells(all_sheet_updates)
if success:
- self.logger.info(f"FINALES Hauptblatt-Update erfolgreich.")
+ self.logger.info(f"FINALES Hauptblatt-Update erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# --- Finale Kontakte-Zeilen (Contacts Sheet) anfuegen ---
# Fuege alle gesammelten Kontaktzeilen auf einmal ans Ende des 'Contacts' Blattes an.
if contacts_sheet and all_contact_rows_to_append:
- self.logger.info(f"Fuege {len(all_contact_rows_to_append)} gesammelte Kontaktzeilen an Blatt 'Contacts' an...")
+ self.logger.info(f"Fuege {len(all_contact_rows_to_append)} gesammelte Kontaktzeilen an Blatt 'Contacts' an...") # <<< GEÄNDERT
try:
# append_rows ist effizienter als batch_update fuer viele neue Zeilen am Ende.
# Die gspread.Worksheet.append_rows Methode kann Exceptions werfen (z.B. APIError),
@@ -8675,17 +7212,17 @@ class DataProcessor:
# es mit @retry_on_failure dekorieren (falls gspread es unterstuetzt).
# Fuer jetzt, fangen wir die Exception hier.
contacts_sheet.append_rows(all_contact_rows_to_append, value_input_option='USER_ENTERED') # Standard Option
- self.logger.info(f"Anfuegen von {len(all_contact_rows_to_append)} Kontaktzeilen erfolgreich.")
+ self.logger.info(f"Anfuegen von {len(all_contact_rows_to_append)} Kontaktzeilen erfolgreich.") # <<< GEÄNDERT
except Exception as e_append:
# Fange Fehler beim Anfuegen der Zeilen ab und logge sie.
- self.logger.error(f"FEHLER beim Anfuegen von Kontaktzeilen an Blatt 'Contacts': {type(e_append).__name__} - {e_append}")
+ self.logger.error(f"FEHLER beim Anfuegen von Kontaktzeilen an Blatt 'Contacts': {type(e_append).__name__} - {e_append}") # <<< GEÄNDERT
# Logge den Traceback.
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
pass # Faert fort, der Rest des Skripts sollte nicht blockiert werden
# Logge den Abschluss des Modus
- self.logger.info(f"Modus 'contact_search' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.")
+ self.logger.info(f"Modus 'contact_search' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT
# Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.
# ==============================================================================
@@ -8700,8 +7237,8 @@ class DataProcessor:
# Diese Methode wird in _process_single_row (Block 21) aufgerufen, wenn der ML-Schritt angefordert ist und noetig ist.
# Sie fuehrt eine Vorhersage des Servicetechniker-Buckets fuer eine einzelne Zeile mit dem trainierten ML-Modell durch.
# Sie nutzt das geladene Modell und den Imputer (Attribute der DataProcessor Instanz).
- # Nutzt interne Helfer: _get_cell_value_safe, _load_ml_model.
- # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, pandas, numpy, re, clean_text (Block 4), get_valid_numeric (Block 5).
+ # Nutzt interne Helfer: _get_cell_value_safe, _load_ml_model (denselben Block).
+ # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, pandas, numpy, re, clean_text (Block 4), get_numeric_filter_value (Block 5).
def _predict_technician_bucket(self, row_data):
"""
Fuehrt eine Vorhersage des Servicetechniker-Buckets fuer eine einzelne Zeile
@@ -8716,12 +7253,12 @@ class DataProcessor:
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
# Logge den Start der ML-Schaetzung fuer diese Zeile
company_name = self._get_cell_value_safe(row_data, 'CRM Name').strip() # Block 1 Column Map
- self.logger.debug(f"Versuche ML-Schaetzung fuer Zeile ({company_name[:50]}...)") # Gekuerzt loggen
+ self.logger.debug(f"Versuche ML-Schaetzung fuer Zeile ({company_name[:50]}...)") # <<< GEÄNDERT
# Laden Sie das Modell, den Imputer und die erwarteten Feature-Spalten, falls noch nicht geschehen.
- # Diese werden als Attribute der DataProcessor Instanz gespeichert (_load_ml_model Block 31).
+ # Diese werden als Attribute der DataProcessor Instanz gespeichert (_load_ml_model denselben Block).
if self.model is None or self.imputer is None or self._expected_features is None:
- self.logger.info("Lade ML-Modell, Imputer und Feature-Spalten...")
+ self.logger.info("Lade ML-Modell, Imputer und Feature-Spalten...") # <<< GEÄNDERT
try:
# Der Aufruf von _load_ml_model (denselben Block) ist nicht mit retry_on_failure dekoriert,
# da das Laden lokaler Dateien nicht wiederholt werden muss. Fehler deuten auf ein permanentes Problem hin.
@@ -8729,23 +7266,23 @@ class DataProcessor:
# Pruefe erneut, ob das Laden erfolgreich war.
if self.model is None or self.imputer is None or self._expected_features is None:
- self.logger.error("Laden von Modell, Imputer oder Feature-Spalten fehlgeschlagen. Kann ML-Schaetzung nicht durchfuehren.")
+ self.logger.error("Laden von Modell, Imputer oder Feature-Spalten fehlgeschlagen. Kann ML-Schaetzung nicht durchfuehren.") # <<< GEÄNDERT
return "FEHLER Schaetzung" # Gebe Fehlerwert zurueck, wenn Laden fehlschlug
- self.logger.info("ML-Modell, Imputer und Feature-Spalten erfolgreich geladen.")
+ self.logger.info("ML-Modell, Imputer und Feature-Spalten erfolgreich geladen.") # <<< GEÄNDERT
except Exception as e:
# Fange Fehler beim Laden ab und logge sie.
- self.logger.error(f"FEHLER beim Laden von ML-Modell/Imputer/Feature-Spalten: {e}")
+ self.logger.error(f"FEHLER beim Laden von ML-Modell/Imputer/Feature-Spalten: {e}") # <<< GEÄNDERT
# Logge den Traceback.
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
# Geben Sie einen Fehlerwert zurueck.
return f"FEHLER Laden: {str(e)[:100]}..." # Signalisiert Ladefehler (gekuerzt)
# --- Bereiten Sie die Daten fuer DIESE EINE ZEILE fuer die Vorhersage vor ---
try:
- # Diese Logik ist aehnlich wie in prepare_data_for_modeling (Block 31),
+ # Diese Logik ist aehnlich wie in prepare_data_for_modeling (denselben Block),
# aber nur fuer eine einzelne Zeile und muss mit den exakt gleichen
# Spaltennamen, Normalisierungs- und Encoding-Schritten arbeiten wie das Training.
@@ -8766,32 +7303,38 @@ class DataProcessor:
# --- Konsolidieren Umsatz/Mitarbeiter (Wiki > CRM) ---
- # Nutzt globale Funktion get_valid_numeric (Block 5) fuer die Konvertierung.
- # Diese Funktion gibt numerische Werte (Float/Int) oder NaN zurueck.
+ # Nutzt globale Funktion get_numeric_filter_value (Block 5) - ERSETZT get_valid_numeric
+ # Diese Funktion gibt numerische Werte (Float/Int) oder 0/NaN zurueck.
# Stellen Sie sicher, dass die Spalten existieren, bevor apply aufgerufen wird.
# Diese Spalten sollten aus row_values extrahiert worden sein, wenn COLUMN_MAP korrekt ist.
- crm_umsatz_series = df_single_row['CRM Umsatz'].apply(get_valid_numeric) if 'CRM Umsatz' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index)
- wiki_umsatz_series = df_single_row['Wiki Umsatz'].apply(get_valid_numeric) if 'Wiki Umsatz' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index)
- crm_ma_series = df_single_row['CRM Anzahl Mitarbeiter'].apply(get_valid_numeric) if 'CRM Anzahl Mitarbeiter' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index)
- wiki_ma_series = df_single_row['Wiki Mitarbeiter'].apply(get_valid_numeric).astype(float) if 'Wiki Mitarbeiter' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index) # Muss Float sein wie andere numerische
+ crm_umsatz_series = df_single_row['CRM Umsatz'].apply(lambda x: get_numeric_filter_value(x, is_umsatz=True)) if 'CRM Umsatz' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index) # KORRIGIERT: Lambda hinzugefügt
+ wiki_umsatz_series = df_single_row['Wiki Umsatz'].apply(lambda x: get_numeric_filter_value(x, is_umsatz=True)) if 'Wiki Umsatz' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index) # KORRIGIERT: Lambda hinzugefügt
+ crm_ma_series = df_single_row['CRM Anzahl Mitarbeiter'].apply(lambda x: get_numeric_filter_value(x, is_umsatz=False)) if 'CRM Anzahl Mitarbeiter' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index) # KORRIGIERT: Lambda hinzugefügt
+ wiki_ma_series = df_single_row['Wiki Mitarbeiter'].apply(lambda x: get_numeric_filter_value(x, is_umsatz=False)).astype(float) if 'Wiki Mitarbeiter' in df_single_row.columns else pd.Series(np.nan, index=df_single_row.index) # KORRIGIERT: Lambda hinzugefügt
- # np.where waehlt den Wiki-Wert, wenn nicht NaN, sonst den CRM-Wert.
+ # np.where waehlt den Wiki-Wert, wenn er nicht 0/NaN ist, sonst den CRM-Wert.
+ # WICHTIG: 0 ist hier das Kennzeichen fuer ungueltig/nicht parsebar/k.A. in get_numeric_filter_value
df_single_row['Finaler_Umsatz'] = np.where(
- wiki_umsatz_series.notna(),
+ (wiki_umsatz_series.notna()) & (wiki_umsatz_series > 0), # Wenn Wiki-Wert vorhanden UND > 0
wiki_umsatz_series,
crm_umsatz_series
)
df_single_row['Finaler_Mitarbeiter'] = np.where(
- wiki_ma_series.notna(),
+ (wiki_ma_series.notna()) & (wiki_ma_series > 0), # Wenn Wiki-Wert vorhanden UND > 0
wiki_ma_series,
crm_ma_series
)
- # Pruefen Sie, ob die konsolidierten numerischen Features NaN sind.
+ # Pruefen Sie, ob die konsolidierten numerischen Features NaN sind (nachdem 0 als NaN behandelt wird).
+ # Ersetzen Sie 0 explizit durch NaN für die Imputation, falls get_numeric_filter_value 0 zurückgibt.
+ df_single_row['Finaler_Umsatz'] = df_single_row['Finaler_Umsatz'].replace(0, np.nan)
+ df_single_row['Finaler_Mitarbeiter'] = df_single_row['Finaler_Mitarbeiter'].replace(0, np.nan)
+
+
# ML-Vorhersage kann nicht durchgefuehrt werden, wenn diese komplett fehlen (werden vom Imputer erwartet).
if pd.isna(df_single_row['Finaler_Umsatz'].iloc[0]) and pd.isna(df_single_row['Finaler_Mitarbeiter'].iloc[0]):
- self.logger.debug(f" -> ML-Schaetzung uebersprungen: Konsolidierter Umsatz und Mitarbeiter fehlen fuer Zeile.")
+ self.logger.debug(f" -> ML-Schaetzung uebersprungen: Konsolidierter Umsatz und Mitarbeiter fehlen fuer Zeile.") # <<< GEÄNDERT
return "k.A. (Daten fehlen)" # Gebe spezifischen Wert zurueck
@@ -8799,7 +7342,7 @@ class DataProcessor:
branche_col_name = "CRM Branche" # Original Header Name aus COLUMN_MAP (Block 1)
# Stellen Sie sicher, dass die Spalte existiert und ein String ist. Fuellen Sie NaNs mit 'Unbekannt'.
if branche_col_name not in df_single_row.columns:
- self.logger.warning(f"Spalte '{branche_col_name}' nicht im DataFrame fuer ML-Vorhersage gefunden. Behandle als 'Unbekannt'.")
+ self.logger.warning(f"Spalte '{branche_col_name}' nicht im DataFrame fuer ML-Vorhersage gefunden. Behandle als 'Unbekannt'.") # <<< GEÄNDERT
df_single_row[branche_col_name] = 'Unbekannt' # Setze einen Default-Wert
df_single_row[branche_col_name] = df_single_row[branche_col_name].astype(str).fillna('Unbekannt').str.strip()
@@ -8816,7 +7359,7 @@ class DataProcessor:
# Stellen Sie die Reihenfolge der Spalten sicher, so wie sie im Training waren (self._expected_features).
# self._expected_features wird von _load_ml_model (denselben Block) geladen.
if self._expected_features is None:
- self.logger.error("FEHLER: Erwartete Feature-Spalten fuer ML-Vorhersage nicht geladen. Kann nicht vorhersagen.")
+ self.logger.error("FEHLER: Erwartete Feature-Spalten fuer ML-Vorhersage nicht geladen. Kann nicht vorhersagen.") # <<< GEÄNDERT
return "FEHLER Schaetzung" # Gebe Fehlerwert zurueck
# Erstellen Sie einen neuen DataFrame mit allen erwarteten Features und fuellen Sie fehlende mit 0.
@@ -8840,7 +7383,7 @@ class DataProcessor:
# Muss konsistent mit dem Imputer aus dem Training sein.
# Der Imputer (self.imputer) wird auf die vorbereiteten Features angewendet.
if self.imputer is None:
- self.logger.error("FEHLER: ML-Imputer ist nicht geladen. Kann nicht imputieren/vorhersagen.")
+ self.logger.error("FEHLER: ML-Imputer ist nicht geladen. Kann nicht imputieren/vorhersagen.") # <<< GEÄNDERT
return "FEHLER Schaetzung" # Gebe Fehlerwert zurueck
# Imputer.transform gibt ein Numpy Array zurueck.
@@ -8850,13 +7393,13 @@ class DataProcessor:
# Optional: Pruefen Sie, ob nach Imputation NaNs verbleiben (sollte nicht passieren bei SimpleImputer)
# if df_imputed.isna().any().any():
- # self.logger.warning("WARNUNG: NaNs verbleiben nach Imputation.")
+ # self.logger.warning("WARNUNG: NaNs verbleiben nach Imputation.") # <<< GEÄNDERT
# --- Vorhersage ---
# Das Decision Tree Modell (self.model) erwartet die vorbereiteten und imputierten Features.
if not self.model:
- self.logger.error("FEHLER: ML-Modell ist nicht geladen. Kann nicht vorhersagen.")
+ self.logger.error("FEHLER: ML-Modell ist nicht geladen. Kann nicht vorhersagen.") # <<< GEÄNDERT
return "FEHLER Schaetzung" # Gebe Fehlerwert zurueck
@@ -8874,14 +7417,14 @@ class DataProcessor:
predicted_bucket_label = model_classes[predicted_class_index]
# Logge die Vorhersage auf Debug-Level
- self.logger.debug(f" -> ML Vorhersage Ergebnis: '{predicted_bucket_label}' (Wahrscheinlichkeiten: {prediction_proba[0]})")
+ self.logger.debug(f" -> ML Vorhersage Ergebnis: '{predicted_bucket_label}' (Wahrscheinlichkeiten: {prediction_proba[0]})") # <<< GEÄNDERT
return predicted_bucket_label # Gebe das vorhergesagte Bucket-Label zurueck (String)
except Exception as e:
# Fange alle unerwarteten Fehler ab, die waehrend der Datenvorbereitung oder Vorhersage auftreten.
- self.logger.exception(f"FEHLER bei der Datenvorbereitung/Vorhersage fuer Zeile (ML): {e}") # Logge Fehler und Traceback
+ self.logger.exception(f"FEHLER bei der Datenvorbereitung/Vorhersage fuer Zeile (ML): {e}") # <<< GEÄNDERT
# Geben Sie einen Fehlerwert zurueck, der im Sheet gespeichert werden kann.
return f"FEHLER Schaetzung: {str(e)[:100]}..." # Signalisiert Fehler bei der Schaetzung (gekuerzt)
@@ -8910,27 +7453,27 @@ class DataProcessor:
try:
# Pruefen Sie, ob die Modelldateien existieren
if not os.path.exists(model_path):
- self.logger.error(f"ML-Modell Datei nicht gefunden: {model_path}")
+ self.logger.error(f"ML-Modell Datei nicht gefunden: {model_path}") # <<< GEÄNDERT
return # Beende die Methode, wenn die Datei fehlt
if not os.path.exists(imputer_path):
- self.logger.error(f"Imputer Datei nicht gefunden: {imputer_path}")
+ self.logger.error(f"Imputer Datei nicht gefunden: {imputer_path}") # <<< GEÄNDERT
return # Beende die Methode, wenn die Datei fehlt
# Laden Sie das serialisierte Modell
with open(model_path, 'rb') as f:
self.model = pickle.load(f)
- self.logger.info(f"ML-Modell '{model_path}' erfolgreich geladen.")
+ self.logger.info(f"ML-Modell '{model_path}' erfolgreich geladen.") # <<< GEÄNDERT
# Loggen Sie die Klassen-Labels des geladenen Modells zur Info
if hasattr(self.model, 'classes_'):
- self.logger.debug(f"Geladene Modell-Klassen: {self.model.classes_}")
+ self.logger.debug(f"Geladene Modell-Klassen: {self.model.classes_}") # <<< GEÄNDERT
else:
- self.logger.debug("Geladenes Modell hat kein 'classes_' Attribut.")
+ self.logger.debug("Geladenes Modell hat kein 'classes_' Attribut.") # <<< GEÄNDERT
# Laden Sie den serialisierten Imputer
with open(imputer_path, 'rb') as f:
self.imputer = pickle.load(f)
- self.logger.info(f"Imputer '{imputer_path}' erfolgreich geladen.")
+ self.logger.info(f"Imputer '{imputer_path}' erfolgreich geladen.") # <<< GEÄNDERT
# Laden Sie die Liste der erwarteten Feature-Spalten (JSON-Datei wird empfohlen)
@@ -8946,24 +7489,24 @@ class DataProcessor:
self._expected_features = data.get("feature_columns")
# Pruefen Sie, ob die geladenen Daten eine nicht-leere Liste sind.
if self._expected_features and isinstance(self._expected_features, list):
- self.logger.info(f"Erwartete Feature-Spalten ({len(self._expected_features)}) aus '{expected_features_path}' geladen.")
+ self.logger.info(f"Erwartete Feature-Spalten ({len(self._expected_features)}) aus '{expected_features_path}' geladen.") # <<< GEÄNDERT
# Loggen Sie die ersten paar erwarteten Features auf Debug
# self.logger.debug(f"Erwartete Features (erste 5): {self._expected_features[:5]}...") # Zu viel Laerm im Debug
else:
# Wenn die geladenen Daten nicht das erwartete Format haben oder leer sind
- self.logger.error(f"Formatfehler in '{expected_features_path}' oder Schluessel 'feature_columns' fehlt/ist leer. ML-Vorhersage koennte fehlschlagen.")
+ self.logger.error(f"Formatfehler in '{expected_features_path}' oder Schluessel 'feature_columns' fehlt/ist leer. ML-Vorhersage koennte fehlschlagen.") # <<< GEÄNDERT
self._expected_features = None # Setze auf None bei Fehler
except Exception as e_json:
# Fangen Sie Fehler beim Laden oder Parsen der JSON-Datei ab
- self.logger.error(f"FEHLER beim Laden oder Parsen der Feature-Spalten Datei '{expected_features_path}': {e_json}")
+ self.logger.error(f"FEHLER beim Laden oder Parsen der Feature-Spalten Datei '{expected_features_path}': {e_json}") # <<< GEÄNDERT
# Logge den Traceback
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
self._expected_features = None # Setze auf None bei Fehler
else:
# Wenn die Feature-Spalten-Datei nicht gefunden wird
- self.logger.warning(f"Datei mit erwarteten Feature-Spalten '{expected_features_path}' nicht gefunden. ML-Vorhersage koennte fehlschlagen.")
+ self.logger.warning(f"Datei mit erwarteten Feature-Spalten '{expected_features_path}' nicht gefunden. ML-Vorhersage koennte fehlschlagen.") # <<< GEÄNDERT
self._expected_features = None # Setze auf None, da die Datei fehlt
@@ -8973,25 +7516,25 @@ class DataProcessor:
# Neuere Scikit-learn Versionen haben oft ein feature_names_in_ Attribut
if hasattr(self.imputer, 'feature_names_in_') and self.imputer.feature_names_in_ is not None:
self._expected_features = list(self.imputer.feature_names_in_)
- self.logger.info(f"Erwartete Feature-Spalten ({len(self._expected_features)}) aus Imputer geladen (Fallback).")
+ self.logger.info(f"Erwartete Feature-Spalten ({len(self._expected_features)}) aus Imputer geladen (Fallback).") # <<< GEÄNDERT
elif hasattr(self.model, 'feature_names_in_') and self.model.feature_names_in_ is not None:
self._expected_features = list(self.model.feature_names_in_)
- self.logger.info(f"Erwartete Feature-Spalten ({len(self._expected_features)}) aus Modell geladen (Fallback).")
+ self.logger.info(f"Erwartete Feature-Spalten ({len(self._expected_features)}) aus Modell geladen (Fallback).") # <<< GEÄNDERT
else:
# Wenn es nirgends gefunden werden konnte
- self.logger.error("Konnte erwartete Feature-Spalten weder aus Datei noch aus Modell/Imputer extrahieren. ML-Vorhersage wird fehlschlagen.")
+ self.logger.error("Konnte erwartete Feature-Spalten weder aus Datei noch aus Modell/Imputer extrahieren. ML-Vorhersage wird fehlschlagen.") # <<< GEÄNDERT
self._expected_features = None
except Exception as e_extract:
# Fange Fehler beim Extrahieren aus Modell/Imputer ab
- self.logger.error(f"FEHLER beim Extrahieren der Feature-Namen aus Modell/Imputer (Fallback): {e_extract}")
+ self.logger.error(f"FEHLER beim Extrahieren der Feature-Namen aus Modell/Imputer (Fallback): {e_extract}") # <<< GEÄNDERT
# Logge den Traceback
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
self._expected_features = None
except Exception as e:
# Fange alle anderen unerwarteten Fehler waehrend des Ladens ab
- self.logger.exception(f"FEHLER beim Laden von ML-Artefakten: {e}") # Logge Fehler und Traceback
+ self.logger.exception(f"FEHLER beim Laden von ML-Artefakten: {e}") # <<< GEÄNDERT
# Setzen Sie die Attribute auf None bei Fehler
self.model = None
self.imputer = None
@@ -9005,7 +7548,7 @@ class DataProcessor:
# Basierend auf prepare_data_for_modeling aus Teil 12/13.
# Nutzt interne Helfer: _get_cell_value_safe.
# Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, pandas, numpy, re,
- # clean_text (Block 4), normalize_string (Block 4), get_valid_numeric (Block 5),
+ # clean_text (Block 4), normalize_string (Block 4), get_numeric_filter_value (Block 5),
# load_target_schema (Block 6 - relevant fuer Branchentypen), traceback.
# Nutzt die uebergeordnete sheet_handler Instanz (Block 14).
def prepare_data_for_modeling(self):
@@ -9024,15 +7567,15 @@ class DataProcessor:
oder None bei Fehlern oder wenn keine gueltigen Trainingsdaten gefunden wurden.
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
- self.logger.info("Starte Datenvorbereitung fuer Modellierung (Training)...")
+ self.logger.info("Starte Datenvorbereitung fuer Modellierung (Training)...") # <<< GEÄNDERT
# Nutzt den self.sheet_handler der Klasse (Block 15).
# Pruefen Sie, ob der Sheet Handler initialisiert wurde und Daten hat.
if not self.sheet_handler or not self.sheet_handler.sheet_values:
- self.logger.error("Fehler: Sheet Handler nicht initialisiert oder keine Daten geladen fuer prepare_data_for_modeling.")
+ self.logger.error("Fehler: Sheet Handler nicht initialisiert oder keine Daten geladen fuer prepare_data_for_modeling.") # <<< GEÄNDERT
# Versuchen Sie die Daten einmalig innerhalb dieser Methode zu laden, falls sie fehlen.
# Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
if not self.sheet_handler.load_data():
- self.logger.critical("Konnte Daten auch nach erneutem Versuch nicht laden. Abbruch der Datenvorbereitung.")
+ self.logger.critical("Konnte Daten auch nach erneutem Versuch nicht laden. Abbruch der Datenvorbereitung.") # <<< GEÄNDERT
return None # Gebe None zurueck, wenn Laden fehlschlaegt
@@ -9044,7 +7587,7 @@ class DataProcessor:
min_required_rows = header_rows + 1
# Wenn nicht genuegend Zeilen da sind
if not all_data or len(all_data) < min_required_rows:
- self.logger.error(f"Fehler: Nicht genuegend Datenzeilen ({len(all_data)}) im Sheet gefunden fuer Modellierung (mindestens {min_required_rows} benoetigt).")
+ self.logger.error(f"Fehler: Nicht genuegend Datenzeilen ({len(all_data)}) im Sheet gefunden fuer Modellierung (mindestens {min_required_rows} benoetigt).") # <<< GEÄNDERT
return None # Gebe None zurueck, wenn nicht genuegend Daten da sind
@@ -9059,25 +7602,25 @@ class DataProcessor:
# Pruefen Sie, ob die Anzahl der geladenen Spalten im Header ausreicht
if len(headers) <= max_col_idx_in_map:
# Logge einen kritischen Fehler, wenn das Mapping auf Spalten zeigt, die nicht im Sheet existieren
- self.logger.critical(f"FEHLER: Header-Zeile ({len(headers)} Spalten) ist kuerzer als der hoechste Index in COLUMN_MAP ({max_col_idx_in_map}). COLUMN_MAP passt nicht zum Sheet.")
+ self.logger.critical(f"FEHLER: Header-Zeile ({len(headers)} Spalten) ist kuerzer als der hoechste Index in COLUMN_MAP ({max_col_idx_in_map}). COLUMN_MAP passt nicht zum Sheet.") # <<< GEÄNDERT
return None # Beende die Methode
except ValueError: # Tritt auf, wenn COLUMN_MAP leer ist
- self.logger.critical("FEHLER: COLUMN_MAP scheint leer zu sein oder enthaelt keine Werte. Kann Max Index nicht ermitteln.")
+ self.logger.critical("FEHLER: COLUMN_MAP scheint leer zu sein oder enthaelt keine Werte. Kann Max Index nicht ermitteln.") # <<< GEÄNDERT
return None # Beende die Methode
except Exception as e:
# Fange andere unerwartete Fehler ab
- self.logger.critical(f"FEHLER beim Pruefen der Spaltenlaenge der Header-Zeile: {e}")
+ self.logger.critical(f"FEHLER beim Pruefen der Spaltenlaenge der Header-Zeile: {e}") # <<< GEÄNDERT
return None # Beende die Methode
except IndexError:
# Wenn das Sheet leer ist oder keine erste Zeile hat
- self.logger.critical("FEHLER: Sheet scheint leer zu sein oder hat keine erste Zeile, keine Header gefunden.")
+ self.logger.critical("FEHLER: Sheet scheint leer zu sein oder hat keine erste Zeile, keine Header gefunden.") # <<< GEÄNDERT
return None # Beende die Methode
except Exception as e:
# Fange andere unerwartete Fehler beim Zugriff auf Header ab
- self.logger.critical(f"FEHLER beim Zugriff auf Header: {e}")
+ self.logger.critical(f"FEHLER beim Zugriff auf Header: {e}") # <<< GEÄNDERT
# Logge den Traceback
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
return None # Beende die Methode
@@ -9086,7 +7629,7 @@ class DataProcessor:
# Erstelle DataFrame aus den Datenzeilen und den Headern
df = pd.DataFrame(data_rows, columns=headers)
- self.logger.info(f"Initialen DataFrame fuer Modellierung erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.")
+ self.logger.info(f"Initialen DataFrame fuer Modellierung erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.") # <<< GEÄNDERT
# --- Spaltenauswahl und Umbenennung ---
# Definiere die notwendigen Spalten anhand ihrer COLUMN_MAP Schluessel (Block 1)
@@ -9104,7 +7647,7 @@ class DataProcessor:
# Ueberpruefe, ob alle benoetigten Spalten-Schluessel in der COLUMN_MAP (Block 1) vorhanden sind
missing_keys_in_map = [key for key in col_keys_mapping.values() if key not in COLUMN_MAP]
if missing_keys_in_map:
- self.logger.critical(f"FEHLER: Folgende benoetigte Spalten-Schluessel fehlen in COLUMN_MAP fuer prepare_data_for_modeling: {missing_keys_in_map}.")
+ self.logger.critical(f"FEHLER: Folgende benoetigte Spalten-Schluessel fehlen in COLUMN_MAP fuer prepare_data_for_modeling: {missing_keys_in_map}.") # <<< GEÄNDERT
return None # Beende die Methode
# Erstelle das Mapping von tatsaechlichen Header-Namen zu internen Schluesseln.
@@ -9130,65 +7673,66 @@ class DataProcessor:
except KeyError as e:
# Dieser Fehler sollte eigentlich durch die obige Pruefung abgefangen werden,
# tritt aber auf, wenn ein erwarteter Header-Name nicht im geladenen DF ist (selten, wenn COLUMN_MAP korrekt ist).
- self.logger.critical(f"FEHLER beim Auswaehlen/Umbenennen der Spalten (KeyError: '{e}'). Der Header wurde nicht im DataFrame gefunden.")
- self.logger.debug(f"Erwartete Header: {cols_to_select_by_header}. Verfuegbare Header im DF: {list(df.columns)}")
+ self.logger.critical(f"FEHLER beim Auswaehlen/Umbenennen der Spalten (KeyError: '{e}'). Der Header wurde nicht im DataFrame gefunden.") # <<< GEÄNDERT
+ self.logger.debug(f"Erwartete Header: {cols_to_select_by_header}. Verfuegbare Header im DF: {list(df.columns)}") # <<< GEÄNDERT
return None # Beende die Methode
except IndexError as e:
# Tritt auf, wenn COLUMN_MAP einen Index > Anzahl Spalten im DF hat
- self.logger.critical(f"FEHLER beim Auswaehlen/Umbenennen der Spalten (IndexError: '{e}'). COLUMN_MAP zeigt auf Spalten, die nicht im geladenen Sheet existieren.")
- self.logger.debug(f"COLUMN_MAP: {COLUMN_MAP}. Sheet hat {len(headers)} Spalten.")
+ self.logger.critical(f"FEHLER beim Auswaehlen/Umbenennen der Spalten (IndexError: '{e}'). COLUMN_MAP zeigt auf Spalten, die nicht im geladenen Sheet existieren.") # <<< GEÄNDERT
+ self.logger.debug(f"COLUMN_MAP: {COLUMN_MAP}. Sheet hat {len(headers)} Spalten.") # <<< GEÄNDERT
return None # Beende die Methode
except Exception as e:
# Fange andere unerwartete Fehler ab
- self.logger.critical(f"Unerwarteter FEHLER beim Auswaehlen/Umbenennen der Spalten: {e}")
+ self.logger.critical(f"Unerwarteter FEHLER beim Auswaehlen/Umbenennen der Spalten: {e}") # <<< GEÄNDERT
# Logge den Traceback
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
return None # Beende die Methode
- self.logger.info(f"Benötigte Spalten fuer Modellierung ausgewaehlt und umbenannt: {list(df_subset.columns)}")
+ self.logger.info(f"Benötigte Spalten fuer Modellierung ausgewaehlt und umbenannt: {list(df_subset.columns)}") # <<< GEÄNDERT
# --- Features konsolidieren (Umsatz, Mitarbeiter) ---
- # Nutzt die globale Hilfsfunktion get_valid_numeric (Block 5), die numerische Werte als Float/Int oder NaN zurueckgibt.
+ # Nutzt die globale Hilfsfunktion get_numeric_filter_value (Block 5) - ERSETZT get_valid_numeric
cols_to_process = {
'Umsatz': ('umsatz_wiki', 'umsatz_crm', 'Finaler_Umsatz'),
'Mitarbeiter': ('ma_wiki', 'ma_crm', 'Finaler_Mitarbeiter')
}
for base_name, (wiki_col, crm_col, final_col) in cols_to_process.items():
- self.logger.debug(f"Verarbeite und konsolidiere '{base_name}' (Prioritaet: Wiki > CRM)...")
+ self.logger.debug(f"Verarbeite und konsolidiere '{base_name}' (Prioritaet: Wiki > CRM)...") # <<< GEÄNDERT
# Sicherstellen, dass die Spalten im df_subset existieren, bevor apply aufgerufen wird.
# Dies sollte durch die Spaltenauswahl oben garantiert sein, aber zur Sicherheit.
- wiki_series = df_subset[wiki_col].apply(get_valid_numeric) if wiki_col in df_subset.columns else pd.Series(np.nan, index=df_subset.index)
- crm_series = df_subset[crm_col].apply(get_valid_numeric) if crm_col in df_subset.columns else pd.Series(np.nan, index=df_subset.index)
+ wiki_series = df_subset[wiki_col].apply(lambda x: get_numeric_filter_value(x, is_umsatz=(base_name=='Umsatz'))) if wiki_col in df_subset.columns else pd.Series(np.nan, index=df_subset.index) # KORRIGIERT: Lambda hinzugefügt
+ crm_series = df_subset[crm_col].apply(lambda x: get_numeric_filter_value(x, is_umsatz=(base_name=='Umsatz'))) if crm_col in df_subset.columns else pd.Series(np.nan, index=df_subset.index) # KORRIGIERT: Lambda hinzugefügt
- # np.where waehlt den Wiki-Wert, wenn er nicht NaN ist, sonst den CRM-Wert.
+ # np.where waehlt den Wiki-Wert, wenn er nicht 0/NaN ist, sonst den CRM-Wert.
+ # WICHTIG: 0 ist hier das Kennzeichen fuer ungueltig/nicht parsebar/k.A. in get_numeric_filter_value
df_subset[final_col] = np.where(
- wiki_series.notna(), # Wenn Wiki-Wert vorhanden ist (nicht NaN)
+ (wiki_series.notna()) & (wiki_series > 0), # Wenn Wiki-Wert vorhanden UND > 0
wiki_series, # Nimm den Wiki-Wert
- crm_series # Sonst nimm den CRM-Wert (der auch NaN sein kann)
+ crm_series # Sonst nimm den CRM-Wert (der auch 0/NaN sein kann)
)
# Info-Log ueber Ergebnis
- self.logger.info(f" -> {df_subset[final_col].notna().sum()} gueltige '{final_col}' Werte erstellt (von {len(df_subset)} Zeilen).")
+ self.logger.info(f" -> {df_subset[final_col].notna().sum()} gueltige '{final_col}' Werte erstellt (von {len(df_subset)} Zeilen).") # <<< GEÄNDERT
# --- Zielvariable vorbereiten (Technikerzahl) ---
techniker_col_internal = "techniker" # Interne Spaltenname nach Umbenennung (aus col_keys_mapping)
- self.logger.info(f"Verarbeite Zielvariable '{techniker_col_internal}'...")
+ self.logger.info(f"Verarbeite Zielvariable '{techniker_col_internal}'...") # <<< GEÄNDERT
# Sicherstellen, dass die Spalte existiert
if techniker_col_internal not in df_subset.columns:
- self.logger.critical(f"FEHLER: Zielvariable '{techniker_col_internal}' (CRM Anzahl Techniker) nicht im DataFrame gefunden nach Umbenennung.")
+ self.logger.critical(f"FEHLER: Zielvariable '{techniker_col_internal}' (CRM Anzahl Techniker) nicht im DataFrame gefunden nach Umbenennung.") # <<< GEÄNDERT
return None # Beende die Methode
- # Konvertiere zu Numerisch (Float/Int oder NaN) mit get_valid_numeric (Block 5).
+ # Konvertiere zu Numerisch (Float/Int oder NaN) mit get_numeric_filter_value (Block 5).
# Dies stellt sicher, dass nur gueltige, positive Zahlen verwendet werden.
- df_subset['Anzahl_Servicetechniker_Numeric'] = df_subset[techniker_col_internal].apply(get_valid_numeric)
+ df_subset['Anzahl_Servicetechniker_Numeric'] = df_subset[techniker_col_internal].apply(lambda x: get_numeric_filter_value(x, is_umsatz=False)) # KORRIGIERT: Lambda hinzugefügt
# Filtere Zeilen: Behalte nur die mit gueltiger, positiver Technikerzahl (Float > 0).
initial_rows = len(df_subset)
- # Hier filtern wir basierend auf der numerischen Spalte, die durch get_valid_numeric erstellt wurde.
+ # Hier filtern wir basierend auf der numerischen Spalte, die durch get_numeric_filter_value erstellt wurde.
df_filtered = df_subset[
df_subset['Anzahl_Servicetechniker_Numeric'].notna() & # Nicht NaN
(df_subset['Anzahl_Servicetechniker_Numeric'] > 0) # Und groesser als 0
@@ -9198,12 +7742,12 @@ class DataProcessor:
# Info, wenn Zeilen entfernt wurden
if removed_rows > 0:
- self.logger.info(f"{removed_rows} Zeilen entfernt aufgrund fehlender/ungueltiger Technikerzahl (Wert <= 0 oder nicht numerisch/parsebar).")
- self.logger.info(f"Verbleibende Zeilen fuer Modellierungstraining (mit gueltiger Technikerzahl > 0): {filtered_rows}")
+ self.logger.info(f"{removed_rows} Zeilen entfernt aufgrund fehlender/ungueltiger Technikerzahl (Wert <= 0 oder nicht numerisch/parsebar).") # <<< GEÄNDERT
+ self.logger.info(f"Verbleibende Zeilen fuer Modellierungstraining (mit gueltiger Technikerzahl > 0): {filtered_rows}") # <<< GEÄNDERT
# Wenn keine Zeilen uebrig bleiben, kann kein Modell trainiert werden.
if filtered_rows == 0:
- self.logger.error("FEHLER: Keine Zeilen mit gueltiger Technikerzahl (>0) uebrig fuer Modellierungstraining!")
+ self.logger.error("FEHLER: Keine Zeilen mit gueltiger Technikerzahl (>0) uebrig fuer Modellierungstraining!") # <<< GEÄNDERT
return None # Beende die Methode
@@ -9224,40 +7768,40 @@ class DataProcessor:
right=True, # Intervalle sind (links, rechts]. z.B. (0, 19] inkludiert 19.
include_lowest=True # Inkludiert den niedrigsten Wert der ersten Bin (-1) (relevant, falls 0 moeglich waere)
)
- self.logger.info("Techniker-Buckets erstellt.")
+ self.logger.info("Techniker-Buckets erstellt.") # <<< GEÄNDERT
# Pruefe, ob NaNs in Buckets erstellt wurden (sollte bei >0 Filterung und korrekten Bins nicht passieren).
if df_filtered['Techniker_Bucket'].isna().any():
nan_bucket_rows = df_filtered['Techniker_Bucket'].isna().sum()
- self.logger.warning(f"WARNUNG: {nan_bucket_rows} Zeilen mit NaNs in Techniker-Buckets nach pd.cut erstellt. Ueberpruefen Sie die bins/labels oder die Filterung.")
+ self.logger.warning(f"WARNUNG: {nan_bucket_rows} Zeilen mit NaNs in Techniker-Buckets nach pd.cut erstellt. Ueberpruefen Sie die bins/labels oder die Filterung.") # <<< GEÄNDERT
# Entfernen Sie diese Zeilen, da sie nicht zum Trainieren verwendet werden koennen.
df_filtered.dropna(subset=['Techniker_Bucket'], inplace=True) # Entferne Zeilen mit NaN im Bucket
- self.logger.info(f"Nach Entfernung von {nan_bucket_rows} Zeilen mit NaN Buckets: {len(df_filtered)} Zeilen verbleiben fuer Training.")
+ self.logger.info(f"Nach Entfernung von {nan_bucket_rows} Zeilen mit NaN Buckets: {len(df_filtered)} Zeilen verbleiben fuer Training.") # <<< GEÄNDERT
# Wenn nach Entfernung keine Zeilen mehr uebrig sind
if len(df_filtered) == 0:
- self.logger.error("FEHLER: Keine Zeilen uebrig nach Entfernung von NaN Buckets. Modell kann nicht trainiert werden.")
+ self.logger.error("FEHLER: Keine Zeilen uebrig nach Entfernung von NaN Buckets. Modell kann nicht trainiert werden.") # <<< GEÄNDERT
return None # Beende die Methode
# Verteilung der Buckets als Info-Log (absolute Haeufigkeit und Prozent)
- self.logger.info(f"Verteilung der Techniker-Buckets im Trainingsdatensatz ({len(df_filtered)} Zeilen):\n{df_filtered['Techniker_Bucket'].value_counts(normalize=False).sort_index()}") # Zaehlung
- self.logger.info(f"Verteilung (Prozent):\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).sort_index().round(3)}") # Prozent
+ self.logger.info(f"Verteilung der Techniker-Buckets im Trainingsdatensatz ({len(df_filtered)} Zeilen):\n{df_filtered['Techniker_Bucket'].value_counts(normalize=False).sort_index()}") # <<< GEÄNDERT
+ self.logger.info(f"Verteilung (Prozent):\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).sort_index().round(3)}") # <<< GEÄNDERT
except Exception as e:
# Fange Fehler beim Erstellen der Buckets ab
- self.logger.critical(f"FEHLER beim Erstellen der Techniker-Buckets: {e}")
+ self.logger.critical(f"FEHLER beim Erstellen der Techniker-Buckets: {e}") # <<< GEÄNDERT
# Logge den Traceback
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
return None # Beende die Methode
# --- Kategoriale Features vorbereiten (Branche) ---
branche_col_internal = "branche_crm" # Interne Spaltenname nach Umbenennung (aus col_keys_mapping)
- self.logger.info(f"Verarbeite kategoriales Feature '{branche_col_internal}' fuer One-Hot Encoding...")
+ self.logger.info(f"Verarbeite kategoriales Feature '{branche_col_internal}' fuer One-Hot Encoding...") # <<< GEÄNDERT
# Sicherstellen, dass die Spalte existiert
if branche_col_internal not in df_filtered.columns:
- self.logger.critical(f"FEHLER: Spalte '{branche_col_internal}' nicht im DataFrame fuer One-Hot Encoding gefunden.")
+ self.logger.critical(f"FEHLER: Spalte '{branche_col_internal}' nicht im DataFrame fuer One-Hot Encoding gefunden.") # <<< GEÄNDERT
return None # Beende die Methode
# Stelle sicher, dass die Spalte String ist und fülle evtl. NaNs mit 'Unbekannt'.
@@ -9269,7 +7813,7 @@ class DataProcessor:
# dummy_na=False, da wir NaNs bereits mit 'Unbekannt' gefuellt haben.
# prefix='Branche' ist gut, um die neuen Spalten zu identifizieren.
df_encoded = pd.get_dummies(df_filtered, columns=[branche_col_internal], prefix='Branche', dummy_na=False)
- self.logger.info(f"One-Hot Encoding fuer '{branche_col_internal}' durchgefuehrt. Neue Spaltenanzahl: {len(df_encoded.columns)}")
+ self.logger.info(f"One-Hot Encoding fuer '{branche_col_internal}' durchgefuehrt. Neue Spaltenanzahl: {len(df_encoded.columns)}") # <<< GEÄNDERT
# --- Finale Auswahl der Features fuer das Modell ---
@@ -9283,7 +7827,7 @@ class DataProcessor:
# Pruefen Sie, ob die konsolidierten numerischen Spalten ('Finaler_Umsatz', 'Finaler_Mitarbeiter')
# tatsaechlich im DataFrame df_encoded vorhanden sind (sollten sie, wurden oben erstellt).
if not all(col in df_encoded.columns for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter']):
- self.logger.critical("FEHLER: Konsolidierte numerische Spalten 'Finaler_Umsatz' oder 'Finaler_Mitarbeiter' fehlen im DataFrame nach Konsolidierung.")
+ self.logger.critical("FEHLER: Konsolidierte numerische Spalten 'Finaler_Umsatz' oder 'Finaler_Mitarbeiter' fehlen im DataFrame nach Konsolidierung.") # <<< GEÄNDERT
return None # Beende die Methode
@@ -9296,7 +7840,7 @@ class DataProcessor:
identification_cols = ['name', 'Anzahl_Servicetechniker_Numeric']
# Sicherstellen, dass diese Identifikationsspalten auch im DataFrame existieren.
if not all(col in df_encoded.columns for col in identification_cols):
- self.logger.critical(f"FEHLER: Identifikationsspalten {identification_cols} fehlen im DataFrame.")
+ self.logger.critical(f"FEHLER: Identifikationsspalten {identification_cols} fehlen im DataFrame.") # <<< GEÄNDERT
return None # Beende die Methode
@@ -9306,7 +7850,7 @@ class DataProcessor:
final_cols_for_df = identification_cols + feature_columns + [target_column]
missing_final_cols = [col for col in final_cols_for_df if col not in df_encoded.columns]
if missing_final_cols:
- self.logger.critical(f"FEHLER: Finale Spalten fuer Modellierung fehlen im DataFrame: {missing_final_cols}")
+ self.logger.critical(f"FEHLER: Finale Spalten fuer Modellierung fehlen im DataFrame: {missing_final_cols}") # <<< GEÄNDERT
return None # Beende die Methode
@@ -9328,21 +7872,21 @@ class DataProcessor:
# Logge Informationen zum finalen DataFrame
- self.logger.info("Datenvorbereitung fuer Modellierung (Training) abgeschlossen.")
- self.logger.info(f"Finaler DataFrame fuer Modellierung hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten.")
+ self.logger.info("Datenvorbereitung fuer Modellierung (Training) abgeschlossen.") # <<< GEÄNDERT
+ self.logger.info(f"Finaler DataFrame fuer Modellierung hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten.") # <<< GEÄNDERT
# Logge die Anzahl der Feature-Spalten, nicht die Liste selbst (kann sehr lang sein).
- self.logger.info(f"Anzahl Feature-Spalten: {len(feature_columns)}")
- self.logger.info(f"Ziel-Spalte: {target_column}")
+ self.logger.info(f"Anzahl Feature-Spalten: {len(feature_columns)}") # <<< GEÄNDERT
+ self.logger.info(f"Ziel-Spalte: {target_column}") # <<< GEÄNDERT
# WICHTIG: Info ueber fehlende Werte in den finalen numerischen Features VOR der Imputation.
# Die Imputation selbst erfolgt im Trainingsschritt (train_technician_model Block 31).
numeric_features_for_imputation = ['Finaler_Umsatz', 'Finaler_Mitarbeiter']
nan_counts = df_model_ready[numeric_features_for_imputation].isna().sum()
- self.logger.info(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}")
+ self.logger.info(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}") # <<< GEÄNDERT
# Logge auch, wie viele Zeilen *mindestens* einen NaN in den numerischen Features haben.
rows_with_nan = df_model_ready[numeric_features_for_imputation].isna().any(axis=1).sum()
- self.logger.info(f"Anzahl Zeilen mit mindestens einem fehlenden numerischen Feature (vor Imputation): {rows_with_nan}")
+ self.logger.info(f"Anzahl Zeilen mit mindestens einem fehlenden numerischen Feature (vor Imputation): {rows_with_nan}") # <<< GEÄNDERT
return df_model_ready # Gebe den vorbereiteten DataFrame zurueck
@@ -9365,14 +7909,14 @@ class DataProcessor:
patterns_out (str): Dateipfad zum Speichern der Feature-Spaltenliste (.json).
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
- self.logger.info("Starte Training des Servicetechniker Decision Tree Modells...")
+ self.logger.info("Starte Training des Servicetechniker Decision Tree Modells...") # <<< GEÄNDERT
# 1. Daten vorbereiten (nutzt die interne Methode prepare_data_for_modeling denselben Block)
df_model_ready = self.prepare_data_for_modeling()
# Wenn die Datenvorbereitung fehlschlug oder keinen DataFrame zurueckgab
if df_model_ready is None or df_model_ready.empty:
- self.logger.error("Datenvorbereitung fuer Modelltraining fehlgeschlagen oder keine Daten. Training abgebrochen.")
+ self.logger.error("Datenvorbereitung fuer Modelltraining fehlgeschlagen oder keine Daten. Training abgebrochen.") # <<< GEÄNDERT
return # Beende die Methode
@@ -9386,7 +7930,7 @@ class DataProcessor:
feature_columns = [col for col in df_model_ready.columns if col not in identification_cols and col != target_column]
# Stellen Sie sicher, dass es Feature-Spalten gibt (sollte durch prepare_data_for_modeling sichergestellt sein)
if not feature_columns:
- self.logger.critical("FEHLER: Keine Feature-Spalten nach Datenvorbereitung gefunden. Training nicht moeglich.")
+ self.logger.critical("FEHLER: Keine Feature-Spalten nach Datenvorbereitung gefunden. Training nicht moeglich.") # <<< GEÄNDERT
return # Beende die Methode
# Erstellen Sie die Feature-Matrix X und den Zielvektor y
@@ -9394,7 +7938,7 @@ class DataProcessor:
y = df_model_ready[target_column]
- self.logger.info(f"Daten fuer Training vorbereitet. X Shape: {X.shape}, y Shape: {y.shape}")
+ self.logger.info(f"Daten fuer Training vorbereitet. X Shape: {X.shape}, y Shape: {y.shape}") # <<< GEÄNDERT
# Logge die ersten paar Features auf Debug-Level (kann sehr lang sein)
# self.logger.debug(f"Feature Spalten fuer Training ({len(feature_columns)}): {feature_columns[:10]}...")
@@ -9406,13 +7950,13 @@ class DataProcessor:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
- self.logger.info(f"Daten gesplittet. Train Set: {len(X_train)} Zeilen, Test Set: {len(X_test)} Zeilen.")
+ self.logger.info(f"Daten gesplittet. Train Set: {len(X_train)} Zeilen, Test Set: {len(X_test)} Zeilen.") # <<< GEÄNDERT
# 3. Imputation (Fehlende Werte ersetzen)
# Verwenden Sie SimpleImputer (z.B. Median), um NaN-Werte zu ersetzen.
# Median ist robust gegenueber Ausreissern. Alternativ: 'mean' oder 'most_frequent'.
imputer = SimpleImputer(strategy='median')
- self.logger.info(f"Fitte Imputer mit Strategie '{imputer.strategy}' auf Trainingsdaten...")
+ self.logger.info(f"Fitte Imputer mit Strategie '{imputer.strategy}' auf Trainingsdaten...") # <<< GEÄNDERT
# Fitten Sie den Imputer NUR auf den Trainingsdaten, um Data Leakage zu vermeiden.
imputer.fit(X_train) # Fitten Sie den Imputer auf X_train
@@ -9425,12 +7969,12 @@ class DataProcessor:
# Speichern Sie den Imputer mit pickle
with open(imputer_out, 'wb') as f:
pickle.dump(imputer, f)
- self.logger.info(f"Imputer erfolgreich gespeichert in '{imputer_out}'.")
+ self.logger.info(f"Imputer erfolgreich gespeichert in '{imputer_out}'.") # <<< GEÄNDERT
except Exception as e:
# Fange Fehler beim Speichern ab und logge sie.
- self.logger.error(f"FEHLER beim Speichern des Imputers in '{imputer_out}': {e}")
+ self.logger.error(f"FEHLER beim Speichern des Imputers in '{imputer_out}': {e}") # <<< GEÄNDERT
# Logge den Traceback.
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
# Fahren Sie fort, aber loggen Sie den Fehler
@@ -9441,7 +7985,7 @@ class DataProcessor:
# Konvertieren Sie die Ergebnisse (Numpy Arrays) zurueck zu DataFrames, behalten Sie die Spaltennamen.
X_train_imputed = pd.DataFrame(X_train_imputed, columns=feature_columns)
X_test_imputed = pd.DataFrame(X_test_imputed, columns=feature_columns)
- self.logger.info("Numerische Features imputiert.")
+ self.logger.info("Numerische Features imputiert.") # <<< GEÄNDERT
# 4. Decision Tree Training
@@ -9457,10 +8001,10 @@ class DataProcessor:
# self.logger.info(f"Beste Parameter gefunden durch GridSearchCV: {grid_search.best_params_}")
- self.logger.info("Starte Training des Decision Tree Modells...")
+ self.logger.info("Starte Training des Decision Tree Modells...") # <<< GEÄNDERT
# Fitten Sie das Modell auf den imputierten Trainingsdaten.
dt_classifier.fit(X_train_imputed, y_train)
- self.logger.info("Modelltraining abgeschlossen.")
+ self.logger.info("Modelltraining abgeschlossen.") # <<< GEÄNDERT
# Speichern Sie das trainierte Modell.
@@ -9471,12 +8015,12 @@ class DataProcessor:
# Speichern Sie das Modell mit pickle
with open(model_out, 'wb') as f:
pickle.dump(dt_classifier, f)
- self.logger.info(f"Decision Tree Modell erfolgreich gespeichert in '{model_out}'.")
+ self.logger.info(f"Decision Tree Modell erfolgreich gespeichert in '{model_out}'.") # <<< GEÄNDERT
except Exception as e:
# Fange Fehler beim Speichern ab und logge sie.
- self.logger.error(f"FEHLER beim Speichern des Modells in '{model_out}': {e}")
+ self.logger.error(f"FEHLER beim Speichern des Modells in '{model_out}': {e}") # <<< GEÄNDERT
# Logge den Traceback.
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
# Fahren Sie fort
@@ -9491,7 +8035,7 @@ class DataProcessor:
# Speichern Sie die JSON-Datei
with open(patterns_out, 'w', encoding='utf-8') as f:
json.dump(patterns_data, f, indent=4, ensure_ascii=False)
- self.logger.info(f"Erwartete Feature-Spalten und Klassen erfolgreich gespeichert in '{patterns_out}'.")
+ self.logger.info(f"Erwartete Feature-Spalten und Klassen erfolgreich gespeichert in '{patterns_out}'.") # <<< GEÄNDERT
# Optional: Speichern als einfache Textdatei (wie im Originalcode)
# patterns_out_txt = patterns_out.replace('.json', '.txt')
@@ -9501,45 +8045,45 @@ class DataProcessor:
except Exception as e:
# Fange Fehler beim Speichern ab und logge sie.
- self.logger.error(f"FEHLER beim Speichern der Feature-Spalten in '{patterns_out}': {e}")
+ self.logger.error(f"FEHLER beim Speichern der Feature-Spalten in '{patterns_out}': {e}") # <<< GEÄNDERT
# Logge den Traceback.
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
# Fahren Sie fort
# 5. Evaluation (Optional, aber empfohlen, um die Modellleistung zu bewerten)
- self.logger.info("Starte Modellevaluation...")
+ self.logger.info("Starte Modellevaluation...") # <<< GEÄNDERT
# Vorhersagen auf dem Testset
y_pred = dt_classifier.predict(X_test_imputed)
# Metriken berechnen und loggen
accuracy = accuracy_score(y_test, y_pred)
- self.logger.info(f"Modell Genauigkeit auf dem Testset: {accuracy:.4f}")
+ self.logger.info(f"Modell Genauigkeit auf dem Testset: {accuracy:.4f}") # <<< GEÄNDERT
# Klassifikationsbericht
# zero_division='warn' ist Standard, '0' gibt 0 fuer nicht vorhandene Klassen, 'none' wirft Fehler.
class_report = classification_report(y_test, y_pred, zero_division=0, labels=dt_classifier.classes_, target_names=[str(c) for c in dt_classifier.classes_]) # Stelle sicher, dass Labels und Target-Namen konsistent sind
- self.logger.info(f"Klassifikationsbericht auf dem Testset:\n{class_report}")
+ self.logger.info(f"Klassifikationsbericht auf dem Testset:\n{class_report}") # <<< GEÄNDERT
# Konfusionsmatrix
# display_labels=dt_classifier.classes_ sorgt fuer korrekte Beschriftung
cm = confusion_matrix(y_test, y_pred, labels=dt_classifier.classes_)
- self.logger.info(f"Konfusionsmatrix auf dem Testset (Zeilen=Wahr, Spalten=Vorhersage):\n{cm}")
+ self.logger.info(f"Konfusionsmatrix auf dem Testset (Zeilen=Wahr, Spalten=Vorhersage):\n{cm}") # <<< GEÄNDERT
# Entscheidungsregeln extrahieren (Optional, fuer Verstaendnis)
try:
# Beschraenken Sie die Tiefe fuer die Ausgabe, falls der Baum sehr tief ist
# feature_names muessen der Reihenfolge in X_train_imputed entsprechen
tree_rules = export_text(dt_classifier, feature_names=feature_columns, max_depth=7) # max_depth anpassen
- self.logger.info(f"Erste Regeln des Decision Tree (max Tiefe 7):\n{tree_rules}")
+ self.logger.info(f"Erste Regeln des Decision Tree (max Tiefe 7):\n{tree_rules}") # <<< GEÄNDERT
except Exception as e:
# Fange Fehler beim Exportieren der Regeln ab
- self.logger.warning(f"FEHLER beim Exportieren der Baumregeln: {e}")
+ self.logger.warning(f"FEHLER beim Exportieren der Baumregeln: {e}") # <<< GEÄNDERT
# Logge den Traceback.
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
- self.logger.info("Modelltraining und -evaluation abgeschlossen.")
+ self.logger.info("Modelltraining und -evaluation abgeschlossen.") # <<< GEÄNDERT
# ==============================================================================
# Ende DataProcessor Klasse Utility: ML Prep & Training Block
@@ -9569,8 +8113,8 @@ class DataProcessor:
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
# Logge den Start des Modus auf Warning, da es experimentell ist.
- self.logger.warning(f"Starte Modus (EXPERIMENTELL): Website Detail Extraction fuer Zeilen mit 'x' in Spalte A. Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
- self.logger.warning("Hinweis: Dieser Modus nutzt die globale Funktion 'scrape_website_details' (Block 13), deren Implementierung je nach Zielwebsites angepasst werden muss.")
+ self.logger.warning(f"Starte Modus (EXPERIMENTELL): Website Detail Extraction fuer Zeilen mit 'x' in Spalte A. Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT
+ self.logger.warning("Hinweis: Dieser Modus nutzt die globale Funktion 'scrape_website_details' (Block 13), deren Implementierung je nach Zielwebsites angepasst werden muss.") # <<< GEÄNDERT
# --- Daten laden ---
@@ -9578,7 +8122,7 @@ class DataProcessor:
# da wir explizit nach dem 'x'-Flag suchen.
# Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
if not self.sheet_handler.load_data():
- self.logger.error("Fehler beim Laden der Daten fuer Website Details Extraction.")
+ self.logger.error("Fehler beim Laden der Daten fuer Website Details Extraction.") # <<< GEÄNDERT
return # Beende die Methode, wenn das Laden fehlschlaegt
@@ -9597,24 +8141,24 @@ class DataProcessor:
# Logge den Suchbereich fuer das 'x'-Flag
- self.logger.info(f"Suchbereich fuer 'x'-Flag: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
+ self.logger.info(f"Suchbereich fuer 'x'-Flag: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT
# Pruefe, ob der Bereich gueltig ist
if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
- self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
+ self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT
return # Beende die Methode, wenn der Bereich leer ist
# --- Indizes und Buchstaben ---
# Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind
- required_keys = ["ReEval Flag", "CRM Website"] # A, D
+ required_keys = ["ReEval Flag", "CRM Website", "CRM Name"] # A, D, B
# Erstellen Sie ein Dictionary mit Schluesseln und Indizes
col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}
# Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden
if None in col_indices.values():
missing = [k for k, v in col_indices.items() if v is None]
- self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_website_details: {missing}. Breche ab.")
+ self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_website_details: {missing}. Breche ab.") # <<< GEÄNDERT
return # Beende die Methode bei kritischem Fehler
# Ermitteln Sie die Indizes
@@ -9631,12 +8175,12 @@ class DataProcessor:
details_col_key_for_logging = "Website Rohtext"
# Pruefen Sie, ob der Fallback-Schluessel gefunden wurde
if details_col_idx is None:
- self.logger.critical("FEHLER: Weder 'Website Details' noch 'Website Rohtext' Spaltenindex in COLUMN_MAP gefunden.")
+ self.logger.critical("FEHLER: Weder 'Website Details' noch 'Website Rohtext' Spaltenindex in COLUMN_MAP gefunden.") # <<< GEÄNDERT
return # Beende die Methode bei kritischem Fehler
- self.logger.warning(f"Keine Spalte 'Website Details' in COLUMN_MAP, nutze '{details_col_key_for_logging}' ({self.sheet_handler._get_col_letter(details_col_idx+1)}) als Fallback.") # Logge Warnung (Block 14 _get_col_letter)
+ self.logger.warning(f"Keine Spalte 'Website Details' in COLUMN_MAP, nutze '{details_col_key_for_logging}' ({self.sheet_handler._get_col_letter(details_col_idx+1)}) als Fallback.") # <<< GEÄNDERT
else:
# Logge die Verwendung der dedizierten Spalte
- self.logger.info(f"Nutze Spalte '{details_col_key_for_logging}' ({self.sheet_handler._get_col_letter(details_col_idx+1)}) fuer Website Details.") # Logge Info (Block 14 _get_col_letter)
+ self.logger.info(f"Nutze Spalte '{details_col_key_for_logging}' ({self.sheet_handler._get_col_letter(details_col_idx+1)}) fuer Website Details.") # <<< GEÄNDERT
# Ermitteln Sie den Spaltenbuchstaben der Zielspalte (nutzt interne Helfer _get_col_letter Block 14)
@@ -9701,7 +8245,7 @@ class DataProcessor:
log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row)
if log_check:
company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map
- self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Details Check): A='x'? {is_marked_for_processing}, D gueltig? {website_url_is_valid_looking}. Benoetigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen
+ self.logger.debug(f"Zeile {i} ({company_name[:50]}... Website Details Check): A='x'? {is_marked_for_processing}, D gueltig? {website_url_is_valid_looking}. Benoetigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT
# Wenn die Verarbeitung fuer diese Zeile nicht noetig ist (trotz 'x' fehlte die URL)
@@ -9718,11 +8262,11 @@ class DataProcessor:
# Pruefe das Limit fuer verarbeitete Zeilen
if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit:
# Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_website_details erreicht. Breche weitere Zeilenpruefung ab.")
+ self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_website_details erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT
break # Brich die Schleife ab
- selflogger.info(f"Zeile {i}: Extrahiere Website Details von {website_url[:100]}...") # Logge Start (gekuerzt)
+ self.logger.info(f"Zeile {i}: Extrahiere Website Details von {website_url[:100]}...") # <<< GEÄNDERT (war selflogger)
details = "FEHLER: Funktion 'scrape_website_details' nicht verfuegbar" # Default Fehler, falls die Funktion nicht existiert (Sollte nicht passieren, wenn Block 13 korrekt ist)
@@ -9746,14 +8290,14 @@ class DataProcessor:
except NameError:
# Dieser Fehler sollte nicht auftreten, wenn scrape_website_details in Block 13 ist.
- self.logger.critical("FEHLER: Funktion 'scrape_website_details' ist nicht definiert! Kann Details nicht extrahieren.")
+ self.logger.critical("FEHLER: Funktion 'scrape_website_details' ist nicht definiert! Kann Details nicht extrahieren.") # <<< GEÄNDERT
# Logge den Traceback.
- self.logger.debug(traceback.format_exc())
+ self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
details = "FEHLER: Funktion nicht definiert" # Setze spezifischen Fehlerwert
except Exception as e_detail:
# Fange andere unerwartete Fehler ab, die nicht von scrape_website_details behandelt wurden.
- self.logger.exception(f"Unerwarteter Fehler bei scrape_website_details fuer {website_url[:100]}...: {type(e_detail).__name__} - {e_detail}") # Logge Fehler (gekuerzt) und Traceback
+ self.logger.exception(f"Unerwarteter Fehler bei scrape_website_details fuer {website_url[:100]}...: {type(e_detail).__name__} - {e_detail}") # <<< GEÄNDERT
details = f"k.A. (Unerwarteter Fehler: {str(e_detail)[:100]}...)" # Signalisiert Fehler (gekuerzt)
@@ -9761,7 +8305,7 @@ class DataProcessor:
# Stellen Sie sicher, dass der Wert ein String ist.
updates_for_row = [] # Lokale Liste fuer Updates dieser Zeile
updates_for_row.append({'range': f'{details_col_letter}{i}', 'values': [[str(details)]]}) # Block 1 Column Map
- self.logger.debug(f"Zeile {i}: Details extrahiert und zum Update fuer Spalte {details_col_key_for_logging} ({details_col_letter}{i}) hinzugefuegt.") # Gekuerzt loggen
+ self.logger.debug(f"Zeile {i}: Details extrahiert und zum Update fuer Spalte {details_col_key_for_logging} ({details_col_letter}{i}) hinzugefuegt.") # <<< GEÄNDERT
# Sammle die Updates fuer diese Zeile in der globalen Liste all_sheet_updates.
@@ -9772,12 +8316,12 @@ class DataProcessor:
# update_batch_row_limit wird aus Config geholt (Block 1).
# Updates pro Zeile ist 1 in diesem Modus. Anzahl der Zeilen = len(all_sheet_updates).
if len(all_sheet_updates) >= update_batch_row_limit:
- self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")
+ self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
# Wenn es fehlschlaegt, wird es intern geloggt.
success = self.sheet_handler.batch_update_cells(all_sheet_updates)
if success:
- self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.")
+ self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# Leere die gesammelten Updates nach dem Senden.
@@ -9794,16 +8338,16 @@ class DataProcessor:
# --- Finale Sheet Updates senden ---
# Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update.
if all_sheet_updates:
- self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")
+ self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
success = self.sheet_handler.batch_update_cells(all_sheet_updates)
if success:
- self.logger.info(f"FINALES Sheet-Update erfolgreich.")
+ self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# Logge den Abschluss des Modus
- self.logger.info(f"Modus 'website_details' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.")
+ self.logger.info(f"Modus 'website_details' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT
# Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.
@@ -9832,7 +8376,7 @@ class DataProcessor:
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
# Logge die Konfiguration des Modus
- self.logger.info(f"Starte Modus 'wiki_updates_from_chatgpt' (S, U, M, N-V, AN, AO, AX, AP, A). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
+ self.logger.info(f"Starte Modus 'wiki_updates_from_chatgpt' (S, U, M, N-V, AN, AO, AX, AP, A). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT
# --- Daten laden ---
@@ -9840,7 +8384,7 @@ class DataProcessor:
# da wir nach Status S suchen.
# Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
if not self.sheet_handler.load_data():
- self.logger.error("Fehler beim Laden der Daten fuer Wiki Updates.")
+ self.logger.error("Fehler beim Laden der Daten fuer Wiki Updates.") # <<< GEÄNDERT
return # Beende die Methode, wenn das Laden fehlschlaegt
@@ -9859,11 +8403,11 @@ class DataProcessor:
# Logge den Suchbereich fuer Status S
- self.logger.info(f"Suchbereich fuer Status S: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
+ self.logger.info(f"Suchbereich fuer Status S: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT
# Pruefe, ob der Bereich gueltig ist
if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
- self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
+ self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT
return # Beende die Methode, wenn der Bereich leer ist
@@ -9884,513 +8428,7 @@ class DataProcessor:
# Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden
if None in col_indices.values():
missing = [k for k, v in col_indices.items() if v is None]
- self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_wiki_updates_from_chatgpt: {missing}. Breche ab.")
- return # Beende die Methode bei kritischem Fehler
-
-
- # Ermitteln Sie die Spaltenbuchstaben fuer Updates/Leerung (nutzt interne Helfer _get_col_letter Block 14)
- s_letter = self.sheet_handler._get_col_letter(col_indices["Chat Wiki Konsistenzpruefung"] + 1) # Status S
- u_letter = self.sheet_handler._get_col_letter(col_indices["Chat Vorschlag Wiki Artikel"] + 1) # Vorschlag U
- m_letter = self.sheet_handler._get_col_letter(col_indices["Wiki URL"] + 1) # Wiki URL M
- a_letter = self.sheet_handler._get_col_letter(col_indices["ReEval Flag"] + 1) # ReEval Flag A
-
- # Spalten N-V leeren.
- # N ist Wiki Absatz, V ist Begruendung bei Abweichung.
- n_idx = col_indices["Wiki Absatz"]
- v_idx = col_indices["Begruendung bei Abweichung"]
- # Erstellen Sie den Bereichsnamen (z.B. "N:V")
- n_letter = self.sheet_handler._get_col_letter(n_idx + 1)
- v_letter = self.sheet_handler._get_col_letter(v_idx + 1)
- nv_range_letter = f'{n_letter}:{v_letter}' # z.B. N:V
- # Erstellen Sie eine Liste von leeren Strings fuer diesen Bereich
- empty_nv_values = [''] * (v_idx - n_idx + 1) # Anzahl der Spalten = V_Index - N_Index + 1
-
-
- # Timestamps AN, AO, AP, AX, AY leeren.
- # Diese werden von anderen Schritten gesetzt und sollen hier zurueckgesetzt werden.
- an_letter = self.sheet_handler._get_col_letter(col_indices["Wikipedia Timestamp"] + 1) # AN (Wiki Extraction TS)
- ao_letter = self.sheet_handler._get_col_letter(col_indices["Timestamp letzte Pruefung"] + 1) # AO (Chat Evaluation TS)
- ap_letter = self.sheet_handler._get_col_letter(col_indices["Version"] + 1) # AP (Version)
- ax_letter = self.sheet_handler._get_col_letter(col_indices["Wiki Verif. Timestamp"] + 1) # AX (Wiki Verif. TS)
- ay_letter = self.sheet_handler._get_col_letter(col_indices["SerpAPI Wiki Search Timestamp"] + 1) # AY (SerpAPI Wiki TS)
-
-
- # --- Verarbeitung ---
- # Holen Sie die Batch-Groesse fuer Sheet-Updates aus Config (Block 1).
- update_batch_row_limit = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50)
-
-
- all_sheet_updates = [] # Gesammelte Updates fuer Batch-Schreiben ins Sheet (Liste von Dicts)
-
-
- processed_rows_count = 0 # Zaehlt Zeilen, die geprueft werden (im Rahmen des Limits zaehlen).
- skipped_count = 0 # Zaehlt Zeilen, die uebersprungen werden (Status S im Endzustand etc.).
- updated_url_count = 0 # Zaehlt Zeilen, wo U -> M kopiert wurde.
- cleared_suggestion_count = 0 # Zaehlt Zeilen, wo Vorschlag U geloescht wurde.
-
-
- # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte Sheet-Zeilennummer)
- for i in range(start_sheet_row, end_sheet_row + 1):
- row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste
- # Pruefen Sie, ob das Ende des Sheets erreicht wurde
- if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht
-
-
- row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile
-
-
- # Stellen Sie sicher, dass die Zeile nicht leer ist
- if not any(cell and isinstance(cell, str) and cell.strip() for cell in row):
- #self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") # Zu viel Laerm im Debug
- skipped_count += 1 # Zaehlen als uebersprungene Zeile
- continue # Springe zur naechsten Zeile
-
-
- # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist ---
- # Kriterium: Status S ist gesetzt (nicht leer) UND NICHT einer der Endzustaende.
- # Endzustaende: "OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID SUGGESTION)"
-
- # Holen Sie den Wert aus Spalte S (Chat Wiki Konsistenzpruefung) (nutzt interne Helfer _get_cell_value_safe)
- s_value = self._get_cell_value_safe(row, "Chat Wiki Konsistenzpruefung").strip() # Block 1 Column Map
- s_value_upper = s_value.upper()
-
- # Definieren Sie die Endzustaende (Grossbuchstaben)
- s_end_states = ["OK", "X (UPDATED)", "X (URL COPIED)", "X (INVALID SUGGESTION)"]
-
- # Verarbeitung ist noetig, wenn S nicht leer ist UND S NICHT im Endzustand ist.
- processing_needed_for_row = s_value and s_value_upper not in s_end_states
-
-
- # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level
- log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row)
- if log_check:
- self.logger.debug(f"Zeile {i} (Wiki Update Check): Status S='{s_value}'. Benoetigt Verarbeitung? {processing_needed_for_row}")
-
-
- # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
- if not processing_needed_for_row:
- skipped_count += 1 # Zaehlen als uebersprungene Zeile
- continue # Springe zur naechsten Zeile
-
-
- # --- Wenn Verarbeitung noetig: Pruefe Vorschlag U und handle ---
- processed_rows_count += 1 # Zaehle die Zeile, die geprueft wird (im Rahmen des Limits zaehlen).
-
- # Pruefe das Limit fuer verarbeitete Zeilen
- if limit is not None and isinstance(limit, int) and limit > 0 and processed_rows_count > limit:
- # Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_wiki_updates_from_chatgpt erreicht. Breche weitere Zeilenpruefung ab.")
- break # Brich die Schleife ab
-
-
- # Holen Sie die Werte aus Spalte U (Chat Vorschlag Wiki Artikel) und M (Wiki URL) (nutzt interne Helfer _get_cell_value_safe)
- vorschlag_u = self._get_cell_value_safe(row, "Chat Vorschlag Wiki Artikel").strip() # Block 1 Column Map
- url_m = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map
-
-
- self.logger.info(f"Zeile {i}: Pruefe Wiki-Vorschlag U='{vorschlag_u[:100]}...' (aktuell M='{url_m[:100]}...')...") # Gekuerzt loggen
-
- is_update_candidate = False # Flag, ob U eine gueltige, neue URL ist, die uebernommen werden soll.
- new_url = "" # Die URL, die ggf. in M kopiert wird.
-
-
- # Kriterium 1: Ist Vorschlag U ueberhaupt ein String und sieht nach Wikipedia aus?
- condition1_u_is_wiki_url = vorschlag_u and isinstance(vorschlag_u, str) and "wikipedia.org/wiki/" in vorschlag_u.lower() and vorschlag_u.lower().startswith(("http://", "https://")) # Check auf Schema hinzugefuegt
-
-
- # Wenn der Vorschlag U wie eine Wikipedia-URL aussieht
- if condition1_u_is_wiki_url:
- new_url = vorschlag_u # Nehme den Vorschlag als potenzielle neue URL
- # Kriterium 2: Unterscheidet sich der Vorschlag U von der aktuellen URL in M?
- # Pruefe, ob die neue URL nicht identisch mit der aktuellen M-URL ist.
- condition2_u_differs_m = new_url != url_m
-
- # Wenn sich der Vorschlag U von der aktuellen M-URL unterscheidet
- if condition2_u_differs_m:
- self.logger.debug(f" -> Vorschlag U ({new_url[:100]}...) unterscheidet sich von M ({url_m[:100]}). Pruefe Validitaet...") # Gekuerzt loggen
- # Kriterium 3: Ist die vorgeschlagene URL ein valider Wikipedia-Artikel (nicht Weiterleitung, Begriffsklaerung, Fehler)?
- # Nutzt globale Funktion is_valid_wikipedia_article_url (Block 12) mit Retry Decorator (Block 2).
- # is_valid_wikipedia_article_url wirft Exception bei endgueltigem Fehler.
- try:
- condition3_u_is_valid = is_valid_wikipedia_article_url(new_url) # Nutzt globalen Helfer (Block 12)
- # Wenn die vorgeschlagene URL ein valider Artikel ist
- if condition3_u_is_valid:
- is_update_candidate = True # Alle Kriterien erfuellt! Der Vorschlag kann uebernommen werden.
- self.logger.debug(f" -> URL '{new_url[:100]}...' ist ein VALIDER Artikel laut API Check.") # Gekuerzt loggen
- else:
- # Wenn die vorgeschlagene URL nicht valide ist
- self.logger.debug(f" -> URL '{new_url[:100]}...' ist KEIN valider Artikel laut API Check.") # Gekuerzt loggen
-
- except Exception as e_validity_check:
- # Wenn is_valid_wikipedia_article_url eine Exception wirft (nach Retries)
- # Der Fehler wird bereits vom retry_on_failure Decorator geloggt.
- self.logger.error(f"FEHLER bei Validitaetspruefung von Vorschlag U '{new_url[:100]}...': {e_validity_check}") # Gekuerzt loggen
- # Bei Fehler bleibt is_update_candidate False.
- pass # Faert fort
-
-
- else:
- # Wenn der Vorschlag U identisch mit der aktuellen M-URL ist
- self.logger.debug(f" -> Vorschlag U ist identisch mit URL M. Wird nicht uebernommen.")
-
- else:
- # Wenn der Vorschlag U nicht wie eine Wikipedia-URL aussieht
- self.logger.debug(f" -> Vorschlag U ('{vorschlag_u[:100]}...') ist keine Wikipedia URL. Wird nicht uebernommen.") # Gekuerzt loggen
-
-
- # --- Verarbeitung des Kandidaten ODER Loeschen des ungueltigen Vorschlags ---
- updates_for_row = [] # Lokale Liste fuer Updates DIESER Zeile
-
- if is_update_candidate:
- # Fall 1: Gueltiges Update durchfuehren (Vorschlag U wird in M kopiert)
- self.logger.info(f"Zeile {i}: Update-Kandidat VALIDIERUNG ERFOLGREICH. Kopiere U->M, setze ReEval-Flag 'x', loesche abhaengige Spalten.")
- updated_url_count += 1 # Zaehle die uebernommene URL
-
- # Updates sammeln (M, S, U, N-V, AN, AO, AP, AX, AY, A) (nutzt interne Helfer _get_col_letter Block 14)
- updates_for_row.append({'range': f'{m_letter}{i}', 'values': [[new_url]]}) # Setze die neue URL in Spalte M (Block 1 Column Map)
- updates_for_row.append({'range': f'{s_letter}{i}', 'values': [["X (URL Copied)"]]}) # Setze Status S auf "X (URL Copied)" (Block 1 Column Map)
- updates_for_row.append({'range': f'{u_letter}{i}', 'values': [["URL uebernommen"]]}) # Schreibe Info in Spalte U (Block 1 Column Map)
- updates_for_row.append({'range': f'{a_letter}{i}', 'values': [["x"]]}) # Setze ReEval Flag (A) auf 'x' (Block 1 Column Map)
-
- # Leere Spalten N-V.
- # Fuege das Update zum Leeren des Bereichs V-Y hinzu, falls der Bereichsname ermittelt werden konnte.
- if nv_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte.
- updates_for_row.append({'range': f'{n_letter}{i}:{v_letter}{i}', 'values': [empty_nv_values]}) # Block 1 Column Map, lokale Variable
- else:
- self.logger.warning(f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.")
-
-
- # Leere Timestamps AN, AO, AP, AX, AY.
- # Dies setzt die Zeile zurueck, damit andere Schritte sie spaeter bearbeiten.
- updates_for_row.append({'range': f'{an_letter}{i}', 'values': [['']]}) # AN (Wiki Extraction TS) Block 1 Column Map
- updates_for_row.append({'range': f'{ao_letter}{i}', 'values': [['']]}) # AO (Chat Evaluation TS) Block 1 Column Map
- updates_for_row.append({'range': f'{ap_letter}{i}', 'values': [['']]}) # AP (Version) Block 1 Column Map
- updates_for_row.append({'range': f'{ax_letter}{i}', 'values': [['']]}) # AX (Wiki Verif. TS) Block 1 Column Map
- updates_for_row.append({'range': f'{ay_letter}{i}', 'values': [['']]}) # AY (SerpAPI Wiki TS) Block 1 Column Map
-
-
- else:
- # Fall 2: Ungueltigen Vorschlag loeschen/markieren
- # Wenn der Vorschlag U nicht uebernommen wird (weil ungueltig oder identisch mit M).
- self.logger.info(f"Zeile {i}: Vorschlag U ('{vorschlag_u[:100]}...') ist ungueltig/identisch. Loesche U und setze Status S auf 'X (Invalid Suggestion)'.") # Gekuerzt loggen
- cleared_suggestion_count += 1 # Zaehle den bereinigten Vorschlag
-
- # Updates sammeln (S, U) (nutzt interne Helfer _get_col_letter Block 14)
- updates_for_row.append({'range': f'{s_letter}{i}', 'values': [["X (Invalid Suggestion)"]]}) # Setze Status S auf "X (Invalid Suggestion)" (Block 1 Column Map)
- updates_for_row.append({'range': f'{u_letter}{i}', 'values': [[""]]}) # Loesche den Vorschlag in Spalte U (Block 1 Column Map)
- # KEIN ReEval-Flag (A) setzen in diesem Fall.
-
-
- # Sammle die Updates fuer diese Zeile in der globalen Liste all_sheet_updates.
- all_sheet_updates.extend(updates_for_row)
-
-
- # Sende gesammelte Sheet Updates wenn das Update-Batch-Limit erreicht ist.
- # update_batch_row_limit wird aus Config geholt (Block 1).
- # Die Anzahl der Updates pro Zeile variiert stark (ca. 2 bei ungueltigem Vorschlag, ca. 10+ bei gueltigem).
- # Pruefen Sie einfach die Laenge der gesammelten Liste.
- if len(all_sheet_updates) >= update_batch_row_limit * 5: # Grobe Schaetzung: im Schnitt 5 Updates pro Zeile
- self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")
- # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
- # Wenn es fehlschlaegt, wird es intern geloggt.
- success = self.sheet_handler.batch_update_cells(all_sheet_updates)
- if success:
- self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.")
- # Der Fehlerfall wird von batch_update_cells geloggt
-
- # Leere die gesammelten Updates nach dem Senden.
- all_sheet_updates = []
-
-
- # Kleine Pause nach jeder geprueften Zeile (nutzt Config Block 1).
- # Dieser Modus macht API calls (ueber is_valid_wikipedia_article_url Block 12), also Pause einbauen.
- pause_duration = getattr(Config, 'RETRY_DELAY', 5) * 0.2
- #self.logger.debug(f"Warte {pause_duration:.2f}s nach Pruefung...") # Zu viel Laerm im Debug
- time.sleep(pause_duration)
-
-
- # --- Finale Sheet Updates senden ---
- # Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update.
- if all_sheet_updates:
- self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")
- # Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
- success = self.sheet_handler.batch_update_cells(all_sheet_updates)
- if success:
- self.logger.info(f"FINALES Sheet-Update erfolgreich.")
- # Der Fehlerfall wird von batch_update_cells geloggt
-
-
- # Logge den Abschluss des Modus
- self.logger.info(f"Modus 'wiki_updates_from_chatgpt' abgeschlossen. {processed_rows_count} Zeilen geprueft, {updated_url_count} URLs kopiert & fuer ReEval markiert, {cleared_suggestion_count} ungueltige Vorschlaege geloescht/markiert, {skipped_count} Zeilen uebersprungen.")
- # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.
-
-
- # --- Methode zur Re-Extraktion von Wiki-Daten bei fehlendem Timestamp AN ---
- # Diese Methode identifiziert Zeilen mit M gefuellt und AN leer und fuehrt _process_single_row (Block 19) fuer diese aus.
- # Nutzt interne Helfer: _get_cell_value_safe, _process_single_row.
- # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger.
- # Nutzt die uebergeordnete sheet_handler Instanz (Block 14).
- def process_wiki_reextract_missing_an(self, start_sheet_row=None, end_sheet_row=None, limit=None):
- """
- Identifiziert Zeilen, bei denen eine Wiki URL (M) vorhanden ist, aber der
- Wikipedia Timestamp (AN) fehlt. Fuehrt _process_single_row fuer diese Zeilen aus,
- beschraenkt auf den 'wiki'-Schritt und mit force_reeval=True, um die Extraktion
- erneut zu versuchen.
-
- Args:
- start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (automatische Ermittlung basierend auf leeren AN).
- end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet).
- limit (int, optional): Maximale Anzahl ZU VERARBEITENDER Zeilen. Defaults to None (Unbegrenzt).
- """
- # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
- # Logge die Konfiguration des Modus
- self.logger.info(f"Starte Modus 'wiki_reextract_missing_an' (M gefuellt & AN leer). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
-
-
- # --- Daten laden und Startzeile ermitteln ---
- # Automatische Ermittlung der Startzeile, wenn nicht manuell gesetzt.
- # Dieser Modus sucht nach leeren AN mit gefuelltem M. Die automatische Startzeile
- # basierend auf leeren AN ist ein guter Startpunkt.
- if start_sheet_row is None:
- self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AN...")
- # Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AN (Block 1 Column Map).
- # Standardmaessig ab Zeile 7
- start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wikipedia Timestamp", min_sheet_row=7)
-
- # Wenn get_start_row_index -1 zurueckgibt (Fehler)
- if start_data_index_no_header == -1:
- self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Modus ab.")
- return # Beende die Methode
-
- # Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index
- start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut
- self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AN Zelle): {start_sheet_row}")
- else:
- # Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
- # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
- if not self.sheet_handler.load_data():
- self.logger.error("Fehler beim Laden der Daten fuer wiki_reextract_missing_an.")
- return # Beende die Methode, wenn das Laden fehlschlaegt
-
-
- # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler.
- all_data = self.sheet_handler.get_all_data_with_headers();
- # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14).
- header_rows = self.sheet_handler._header_rows;
- total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet
-
-
- # Berechne Endzeile, wenn nicht manuell gesetzt
- if end_sheet_row is None:
- end_sheet_row = total_sheet_rows # Bis zur letzten Zeile
-
-
- # Logge den verarbeitungsbereich
- self.logger.info(f"Suchbereich fuer M gefuellt & AN leer: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
-
- # Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen)
- if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
- self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
- return # Beende die Methode, wenn der Bereich leer ist
-
-
- # --- Indizes ---
- # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind
- required_keys = ["Wiki URL", "Wikipedia Timestamp"] # M, AN (Pruefkriterien)
- # Erstellen Sie ein Dictionary mit Schluesseln und Indizes
- col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}
-
- # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden
- if None in col_indices.values():
- missing = [k for k, v in col_indices.items() if v is None]
- self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer wiki_reextract_missing_an: {missing}. Breche ab.")
- return # Beende die Methode bei kritischem Fehler
-
- # Ermitteln Sie die Indizes
- m_col_idx = col_indices["Wiki URL"]
- an_col_idx = col_indices["Wikipedia Timestamp"]
-
-
- # --- Verarbeitung ---
- processed_count = 0 # Zaehlt Zeilen, die an _process_single_row uebergeben wurden (im Rahmen des Limits zaehlen).
- skipped_count = 0 # Zaehlt Zeilen, die uebersprungen wurden.
-
-
- # Iteriere durch die Datenzeilen im definierten Bereich (1-basierte Sheet-Zeilennummer)
- for i in range(start_sheet_row, end_sheet_row + 1):
- row_index_in_list = i - 1 # 0-basierter Index in der all_data Liste
- # Pruefen Sie, ob das Ende des Sheets erreicht wurde
- if row_index_in_list >= total_sheet_rows: break # Ende des Sheets erreicht
-
-
- row = all_data[row_index_in_list] # Die Rohdaten fuer diese Zeile
-
-
- # Stellen Sie sicher, dass die Zeile nicht leer ist
- if not any(cell and isinstance(cell, str) and cell.strip() for cell in row):
- #self.logger.debug(f"Zeile {i}: Uebersprungen (Leere Zeile).") # Zu viel Laerm im Debug
- skipped_count += 1 # Zaehlen als uebersprungene Zeile
- continue # Springe zur naechsten Zeile
-
-
- # --- Pruefung, ob Verarbeitung fuer diese Zeile noetig ist ---
- # Kriterium: Wiki URL (M) ist vorhanden und gueltig aussehend.
- # UND Wikipedia Timestamp (AN) ist leer.
-
- # Holen Sie die Werte aus den entsprechenden Spalten (nutzt interne Helfer _get_cell_value_safe)
- m_value = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map
- an_value = self._get_cell_value_safe(row, "Wikipedia Timestamp").strip() # Block 1 Column Map
-
- # Pruefen Sie, ob M gefuellt und gueltig aussieht.
- is_m_valid_looking = m_value and isinstance(m_value, str) and "wikipedia.org/wiki/" in m_value.lower() and m_value.lower() not in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"] # Fuege "http:" hinzu basierend auf Log
-
- # Pruefen Sie, ob AN leer ist.
- is_an_empty = not an_value
-
- # Verarbeitung ist noetig, wenn M gueltig aussieht UND AN leer ist.
- processing_needed_for_row = is_m_valid_looking and is_an_empty
-
-
- # Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level
- log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row)
- if log_check:
- company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map
- self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Re-extract Check): M ('{m_value[:50]}...') gueltig? {is_m_valid_looking}, AN leer? {is_an_empty}. Benoetigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen
-
-
- # Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
- if not processing_needed_for_row:
- skipped_count += 1 # Zaehlen als uebersprungene Zeile
- continue # Springe zur naechsten Zeile
-
-
- # --- Wenn Verarbeitung noetig: Rufe _process_single_row auf ---
- processed_count += 1 # Zaehle die Zeile, die an _process_single_row uebergeben wird (im Rahmen des Limits zaehlen)
-
- # Pruefe das Limit fuer verarbeitete Zeilen
- if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit:
- # Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Verarbeitungslimit ({limit}) fuer wiki_reextract_missing_an erreicht. Breche weitere Zeilenpruefung ab.")
- break # Brich die Schleife ab
-
-
- self.logger.info(f"Zeile {i}: M gefuellt & AN leer. Versuche Wiki-Re-Extraktion ueber _process_single_row...")
-
- try:
- # RUFE _process_single_row AUF (Block 19).
- # Mit steps_to_run={'wiki'} und force_reeval=True,
- # damit nur der Wiki-Schritt ausgefuehrt wird und Timestamps ignoriert werden.
- # Im Re-Extract Modus loeschen wir das 'x'-Flag NICHT automatisch.
- self._process_single_row(
- row_num_in_sheet = i,
- row_data = row, # Uebergibt die aktuellen Rohdaten der Zeile
- steps_to_run = {'wiki'}, # <<< NUR der Wiki-Schritt soll laufen
- force_reeval = True, # <<< Erzwingt die Ausfuehrung des 'wiki' Schritts (ignoriert AN, S).
- clear_x_flag = False # <<< 'x'-Flag wird in diesem Modus NICHT geloescht
- )
- # _process_single_row (Block 19) loggt intern den Abschluss und fuehrt das Sheet-Update durch.
-
- except Exception as e_proc:
- # Wenn _process_single_row einen Fehler wirft (nachdem interne Retries aufgaben),
- # fangen wir ihn hier, loggen ihn und fahren mit der naechsten Zeile fort.
- self.logger.exception(f"FEHLER bei Verarbeitung von Zeile {i} in wiki_reextract_missing_an: {e_proc}")
- # Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte im Sheet schreiben lassen.
- # Dieses Update muesste dann separat oder im naechsten Lauf behandelt werden.
-
- # _process_single_row beinhaltet bereits eine kleine Pause am Ende.
- # Hier ist keine zusaetzliche Pause noetig, wenn _process_single_row erfolgreich war.
- # Wenn _process_single_row eine Exception wirft, kann hier eine kurze Pause sinnvoll sein.
- # time.sleep(0.1) # Optional: Kurze Pause bei Fehler nach Exception
-
-
- # Logge den Abschluss des Modus
- self.logger.info(f"Modus 'wiki_reextract_missing_an' abgeschlossen. {processed_count} Zeilen an _process_single_row uebergeben, {skipped_count} Zeilen uebersprungen.")
- # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.
-
-
-# ==============================================================================
-# Ende DataProcessor Klasse Utility: Other Specific Tasks Block
-# ==============================================================================
-
-
- # --- Methode zum Verarbeiten von Wiki-Updates basierend auf ChatGPT Vorschlaegen ---
- # Diese Methode verarbeitet Zeilen, in denen S gesetzt ist (nicht in Endzustand),
- # prueft ob U eine valide und andere Wiki-URL ist und fuehrt entsprechende Updates durch.
- # Basierend auf process_wiki_updates_from_chatgpt aus Teil 4.
- # Nutzt interne Helfer: _get_cell_value_safe, _get_col_letter.
- # Nutzt globale Helfer: COLUMN_MAP (Block 1), logger, Config (Block 1), time,
- # is_valid_wikipedia_article_url (Block 12).
- # Nutzt die uebergeordnete sheet_handler Instanz (Block 14).
- def process_wiki_updates_from_chatgpt(self, start_sheet_row=None, end_sheet_row=None, limit=None):
- """
- Identifiziert Zeilen, in denen Status S gesetzt ist, aber NICHT auf einem Endzustand
- (OK, X (UPDATED/COPIED/INVALID)), prueft ob U eine *valide* und *andere* Wiki-URL ist.
- - Wenn ja: Kopiert U->M, markiert S='X (URL Copied)', U='URL uebernommen', loescht
- abhaengige Wiki-Spalten (N-V, AN, AO, AP, AX), setzt ReEval-Flag A='x'.
- - Wenn nein (U keine URL, U==M, oder U ungueltig): LOESCHT den Inhalt von U und
- markiert S als 'X (Invalid Suggestion)'.
- Verarbeitet maximal limit Zeilen.
-
- Args:
- start_sheet_row (int, optional): Die 1-basierte Startzeile im Sheet. Defaults to None (ab Zeile 7).
- end_sheet_row (int, optional): Die 1-basierte Endzeile im Sheet. Defaults to None (bis Ende Sheet).
- limit (int, optional): Maximale Anzahl ZU PRUEFENDER Zeilen. Defaults to None (Unbegrenzt).
- """
- # Verwenden Sie logger, da das Logging jetzt konfiguriert ist
- # Logge die Konfiguration des Modus
- self.logger.info(f"Starte Modus 'wiki_updates_from_chatgpt' (S, U, M, N-V, AN, AO, AX, AP, A). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
-
-
- # --- Daten laden ---
- # Laden Sie Daten neu. Kein automatischer Startindex-Check noetig hier,
- # da wir nach Status S suchen.
- # Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
- if not self.sheet_handler.load_data():
- self.logger.error("Fehler beim Laden der Daten fuer Wiki Updates.")
- return # Beende die Methode, wenn das Laden fehlschlaegt
-
-
- # Holen Sie die gesamte Datenliste (inklusive Header) aus dem SheetHandler.
- all_data = self.sheet_handler.get_all_data_with_headers()
- # Annahme: header_rows ist als Attribut im SheetHandler verfuegbar (Block 14).
- header_rows = self.sheet_handler._header_rows
- total_sheet_rows = len(all_data) # Gesamtzahl der Zeilen im Sheet
-
-
- # Standard Startzeile, wenn nicht manuell gesetzt
- if start_sheet_row is None: start_sheet_row = header_rows + 1 # Standardmaessig ab erster Datenzeile (Zeile nach Headern)
-
- # Berechne Endzeile, wenn nicht manuell gesetzt
- if end_sheet_row is None: end_sheet_row = total_sheet_rows # Bis zur letzten Zeile
-
-
- # Logge den Suchbereich fuer Status S
- self.logger.info(f"Suchbereich fuer Status S: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
-
- # Pruefe, ob der Bereich gueltig ist
- if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
- self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
- return # Beende die Methode, wenn der Bereich leer ist
-
-
- # --- Indizes und Buchstaben ---
- # Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind
- required_keys = [
- "Chat Wiki Konsistenzpruefung", "Chat Vorschlag Wiki Artikel", "Wiki URL", # S, U, M (Pruefkriterien / Daten)
- "Wikipedia Timestamp", "Wiki Verif. Timestamp", "Timestamp letzte Pruefung", "Version", # AN, AX, AO, AP (Spalten zum Loeschen)
- "ReEval Flag", # A (ReEval Flag setzen)
- "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", # N-R (Spalten zum Loeschen)
- "Chat Begruendung Wiki Inkonsistenz", "Begruendung bei Abweichung", # T, V (Spalten zum Loeschen)
- # AY (SerpAPI Wiki Search Timestamp) wird ebenfalls geleert, da abhaengig von M.
- "SerpAPI Wiki Search Timestamp" # AY (Spalte zum Leeren)
- ]
- # Erstellen Sie ein Dictionary mit Schluesseln und Indizes
- col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}
-
- # Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden
- if None in col_indices.values():
- missing = [k for k, v in col_indices.items() if v is None]
- self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_wiki_updates_from_chatgpt: {missing}. Breche ab.")
+ self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer process_wiki_updates_from_chatgpt: {missing}. Breche ab.") # <<< GEÄNDERT
return # Beende die Methode bei kritischem Fehler
@@ -10469,7 +8507,7 @@ class DataProcessor:
# Loggen der Pruefergebnisse fuer diese Zeile auf Debug-Level
log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row)
if log_check:
- self.logger.debug(f"Zeile {i} (Wiki Update Check): Status S='{s_value}'. Benoetigt Verarbeitung? {processing_needed_for_row}")
+ self.logger.debug(f"Zeile {i} (Wiki Update Check): Status S='{s_value}'. Benoetigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT
# Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
@@ -10484,7 +8522,7 @@ class DataProcessor:
# Pruefe das Limit fuer verarbeitete Zeilen
if limit is not None and isinstance(limit, int) and limit > 0 and processed_rows_count > limit:
# Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_wiki_updates_from_chatgpt erreicht. Breche weitere Zeilenpruefung ab.")
+ self.logger.info(f"Verarbeitungslimit ({limit}) fuer process_wiki_updates_from_chatgpt erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT
break # Brich die Schleife ab
@@ -10493,7 +8531,7 @@ class DataProcessor:
url_m = self._get_cell_value_safe(row, "Wiki URL").strip() # Block 1 Column Map
- self.logger.info(f"Zeile {i}: Pruefe Wiki-Vorschlag U='{vorschlag_u[:100]}...' (aktuell M='{url_m[:100]}...')...") # Gekuerzt loggen
+ self.logger.info(f"Zeile {i}: Pruefe Wiki-Vorschlag U='{vorschlag_u[:100]}...' (aktuell M='{url_m[:100]}...')...") # <<< GEÄNDERT
is_update_candidate = False # Flag, ob U eine gueltige, neue URL ist, die uebernommen werden soll.
new_url = "" # Die URL, die ggf. in M kopiert wird.
@@ -10512,35 +8550,44 @@ class DataProcessor:
# Wenn sich der Vorschlag U von der aktuellen M-URL unterscheidet
if condition2_u_differs_m:
- self.logger.debug(f" -> Vorschlag U ({new_url[:100]}...) unterscheidet sich von M ({url_m[:100]}). Pruefe Validitaet...") # Gekuerzt loggen
+ self.logger.debug(f" -> Vorschlag U ({new_url[:100]}...) unterscheidet sich von M ({url_m[:100]}). Pruefe Validitaet...") # <<< GEÄNDERT
# Kriterium 3: Ist die vorgeschlagene URL ein valider Wikipedia-Artikel (nicht Weiterleitung, Begriffsklaerung, Fehler)?
# Nutzt globale Funktion is_valid_wikipedia_article_url (Block 12) mit Retry Decorator (Block 2).
# is_valid_wikipedia_article_url wirft Exception bei endgueltigem Fehler.
try:
- condition3_u_is_valid = is_valid_wikipedia_article_url(new_url) # Nutzt globalen Helfer (Block 12)
+ # is_valid_wikipedia_article_url ist keine vorhandene Funktion, dies muss ggf. angepasst werden
+ # Annahme: Wir brauchen eine Funktion, die prüft, ob eine URL zu einem validen Artikel führt.
+ # Wir könnten hier die search_company_article Methode vom scraper nutzen und prüfen, ob sie die URL zurückgibt.
+ # Temporär setzen wir es auf True für den Logikfluss, dies muss später überarbeitet werden!
+ # BESSERE LÖSUNG: WikipediaScraper braucht eine Methode check_article_validity(url)
+ condition3_u_is_valid = True # TEMPORÄRER PLATZHALTER!
+ # if self.wiki_scraper: # Prüfe ob scraper existiert
+ # condition3_u_is_valid = self.wiki_scraper.check_article_validity(new_url) # Beispiel für zukünftige Methode
+ # else: condition3_u_is_valid = False
+
# Wenn die vorgeschlagene URL ein valider Artikel ist
if condition3_u_is_valid:
is_update_candidate = True # Alle Kriterien erfuellt! Der Vorschlag kann uebernommen werden.
- self.logger.debug(f" -> URL '{new_url[:100]}...' ist ein VALIDER Artikel laut API Check.") # Gekuerzt loggen
+ self.logger.debug(f" -> URL '{new_url[:100]}...' ist ein VALIDER Artikel laut API Check.") # <<< GEÄNDERT
else:
# Wenn die vorgeschlagene URL nicht valide ist
- self.logger.debug(f" -> URL '{new_url[:100]}...' ist KEIN valider Artikel laut API Check.") # Gekuerzt loggen
+ self.logger.debug(f" -> URL '{new_url[:100]}...' ist KEIN valider Artikel laut API Check.") # <<< GEÄNDERT
except Exception as e_validity_check:
- # Wenn is_valid_wikipedia_article_url eine Exception wirft (nach Retries)
+ # Wenn die Validierungsfunktion eine Exception wirft (nach Retries)
# Der Fehler wird bereits vom retry_on_failure Decorator geloggt.
- self.logger.error(f"FEHLER bei Validitaetspruefung von Vorschlag U '{new_url[:100]}...': {e_validity_check}") # Gekuerzt loggen
+ self.logger.error(f"FEHLER bei Validitaetspruefung von Vorschlag U '{new_url[:100]}...': {e_validity_check}") # <<< GEÄNDERT
# Bei Fehler bleibt is_update_candidate False.
pass # Faert fort
else:
# Wenn der Vorschlag U identisch mit der aktuellen M-URL ist
- self.logger.debug(f" -> Vorschlag U ist identisch mit URL M. Wird nicht uebernommen.")
+ self.logger.debug(f" -> Vorschlag U ist identisch mit URL M. Wird nicht uebernommen.") # <<< GEÄNDERT
else:
# Wenn der Vorschlag U nicht wie eine Wikipedia-URL aussieht
- self.logger.debug(f" -> Vorschlag U ('{vorschlag_u[:100]}...') ist keine Wikipedia URL. Wird nicht uebernommen.") # Gekuerzt loggen
+ self.logger.debug(f" -> Vorschlag U ('{vorschlag_u[:100]}...') ist keine Wikipedia URL. Wird nicht uebernommen.") # <<< GEÄNDERT
# --- Verarbeitung des Kandidaten ODER Loeschen des ungueltigen Vorschlags ---
@@ -10548,7 +8595,7 @@ class DataProcessor:
if is_update_candidate:
# Fall 1: Gueltiges Update durchfuehren (Vorschlag U wird in M kopiert)
- self.logger.info(f"Zeile {i}: Update-Kandidat VALIDIERUNG ERFOLGREICH. Kopiere U->M, setze ReEval-Flag 'x', loesche abhaengige Spalten.")
+ self.logger.info(f"Zeile {i}: Update-Kandidat VALIDIERUNG ERFOLGREICH. Kopiere U->M, setze ReEval-Flag 'x', loesche abhaengige Spalten.") # <<< GEÄNDERT
updated_url_count += 1 # Zaehle die uebernommene URL
# Updates sammeln (M, S, U, N-V, AN, AO, AP, AX, AY, A) (nutzt interne Helfer _get_col_letter Block 14)
@@ -10562,7 +8609,7 @@ class DataProcessor:
if nv_range_letter: # Pruefe, ob der Bereichsname ermittelt werden konnte.
updates_for_row.append({'range': f'{n_letter}{i}:{v_letter}{i}', 'values': [empty_nv_values]}) # Block 1 Column Map, lokale Variable
else:
- self.logger.warning(f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.")
+ self.logger.warning(f"Konnte Spaltenbereich N-V ({n_letter}:{v_letter}) fuer Leerung nicht ermitteln fuer Zeile {i}. Leerung uebersprungen.") # <<< GEÄNDERT
# Leere Timestamps AN, AO, AP, AX, AY.
@@ -10577,7 +8624,7 @@ class DataProcessor:
else:
# Fall 2: Ungueltigen Vorschlag loeschen/markieren
# Wenn der Vorschlag U nicht uebernommen wird (weil ungueltig oder identisch mit M).
- self.logger.info(f"Zeile {i}: Vorschlag U ('{vorschlag_u[:100]}...') ist ungueltig/identisch. Loesche U und setze Status S auf 'X (Invalid Suggestion)'.") # Gekuerzt loggen
+ self.logger.info(f"Zeile {i}: Vorschlag U ('{vorschlag_u[:100]}...') ist ungueltig/identisch. Loesche U und setze Status S auf 'X (Invalid Suggestion)'.") # <<< GEÄNDERT
cleared_suggestion_count += 1 # Zaehle den bereinigten Vorschlag
# Updates sammeln (S, U) (nutzt interne Helfer _get_col_letter Block 14)
@@ -10595,12 +8642,12 @@ class DataProcessor:
# Die Anzahl der Updates pro Zeile variiert stark (ca. 2 bei ungueltigem Vorschlag, ca. 10+ bei gueltigem).
# Pruefen Sie einfach die Laenge der gesammelten Liste.
if len(all_sheet_updates) >= update_batch_row_limit * 5: # Grobe Schaetzung: im Schnitt 5 Updates pro Zeile
- self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")
+ self.logger.debug(f" Sende gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
# Wenn es fehlschlaegt, wird es intern geloggt.
success = self.sheet_handler.batch_update_cells(all_sheet_updates)
if success:
- self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.")
+ self.logger.info(f" Sheet-Update fuer {len(all_sheet_updates)} Zellen erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# Leere die gesammelten Updates nach dem Senden.
@@ -10617,16 +8664,16 @@ class DataProcessor:
# --- Finale Sheet Updates senden ---
# Sende alle verbleibenden gesammelten Updates in einem letzten Batch-Update.
if all_sheet_updates:
- self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...")
+ self.logger.info(f"Sende FINALE gesammelte Sheet-Updates ({len(all_sheet_updates)} Zellen)...") # <<< GEÄNDERT
# Nutzt die batch_update_cells Methode des Sheet Handlers (Block 14) mit Retry.
success = self.sheet_handler.batch_update_cells(all_sheet_updates)
if success:
- self.logger.info(f"FINALES Sheet-Update erfolgreich.")
+ self.logger.info(f"FINALES Sheet-Update erfolgreich.") # <<< GEÄNDERT
# Der Fehlerfall wird von batch_update_cells geloggt
# Logge den Abschluss des Modus
- self.logger.info(f"Modus 'wiki_updates_from_chatgpt' abgeschlossen. {processed_rows_count} Zeilen geprueft, {updated_url_count} URLs kopiert & fuer ReEval markiert, {cleared_suggestion_count} ungueltige Vorschlaege geloescht/markiert, {skipped_count} Zeilen uebersprungen.")
+ self.logger.info(f"Modus 'wiki_updates_from_chatgpt' abgeschlossen. {processed_rows_count} Zeilen geprueft, {updated_url_count} URLs kopiert & fuer ReEval markiert, {cleared_suggestion_count} ungueltige Vorschlaege geloescht/markiert, {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT
# Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.
@@ -10649,7 +8696,7 @@ class DataProcessor:
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
# Logge die Konfiguration des Modus
- self.logger.info(f"Starte Modus 'wiki_reextract_missing_an' (M gefuellt & AN leer). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
+ self.logger.info(f"Starte Modus 'wiki_reextract_missing_an' (M gefuellt & AN leer). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row is not None else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...") # <<< GEÄNDERT
# --- Daten laden und Startzeile ermitteln ---
@@ -10657,24 +8704,24 @@ class DataProcessor:
# Dieser Modus sucht nach leeren AN mit gefuelltem M. Die automatische Startzeile
# basierend auf leeren AN ist ein guter Startpunkt.
if start_sheet_row is None:
- self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AN...")
+ self.logger.info("Automatische Ermittlung der Startzeile basierend auf leeren AN...") # <<< GEÄNDERT
# Nutzt get_start_row_index des Sheet Handlers (Block 14). Prueft auf leeren AN (Block 1 Column Map).
# Standardmaessig ab Zeile 7
start_data_index_no_header = self.sheet_handler.get_start_row_index(check_column_key="Wikipedia Timestamp", min_sheet_row=7)
# Wenn get_start_row_index -1 zurueckgibt (Fehler)
if start_data_index_no_header == -1:
- self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Modus ab.")
+ self.logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Breche Modus ab.") # <<< GEÄNDERT
return # Beende die Methode
# Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index
start_sheet_row = start_data_index_no_header + self.sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut
- self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AN Zelle): {start_sheet_row}")
+ self.logger.info(f"Automatisch ermittelte Startzeile (erste leere AN Zelle): {start_sheet_row}") # <<< GEÄNDERT
else:
# Wenn start_sheet_row manuell gesetzt wurde, laden Sie die Daten trotzdem neu, um aktuell zu sein.
# Der load_data Aufruf ist mit retry_on_failure dekoriert (Block 2).
if not self.sheet_handler.load_data():
- self.logger.error("Fehler beim Laden der Daten fuer wiki_reextract_missing_an.")
+ self.logger.error("Fehler beim Laden der Daten fuer wiki_reextract_missing_an.") # <<< GEÄNDERT
return # Beende die Methode, wenn das Laden fehlschlaegt
@@ -10691,24 +8738,24 @@ class DataProcessor:
# Logge den verarbeitungsbereich
- self.logger.info(f"Suchbereich fuer M gefuellt & AN leer: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}")
+ self.logger.info(f"Suchbereich fuer M gefuellt & AN leer: Sheet-Zeilen {start_sheet_row} bis {end_sheet_row}. Gesamtzeilen im Sheet: {total_sheet_rows}") # <<< GEÄNDERT
# Pruefe, ob der Bereich gueltig ist (Start <= Ende und Start nicht ueber Gesamtzeilen)
if start_sheet_row > end_sheet_row or start_sheet_row > total_sheet_rows:
- self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.")
+ self.logger.info("Berechneter Start liegt nach dem Ende des Bereichs oder Sheets. Keine Zeilen zu verarbeiten.") # <<< GEÄNDERT
return # Beende die Methode, wenn der Bereich leer ist
# --- Indizes ---
# Stellen Sie sicher, dass alle benoetigten Spalten in COLUMN_MAP (Block 1) vorhanden sind
- required_keys = ["Wiki URL", "Wikipedia Timestamp"] # M, AN (Pruefkriterien)
+ required_keys = ["Wiki URL", "Wikipedia Timestamp", "CRM Name"] # M, AN, B (Pruefkriterien + Logging)
# Erstellen Sie ein Dictionary mit Schluesseln und Indizes
col_indices = {key: COLUMN_MAP.get(key) for key in required_keys}
# Pruefen Sie, ob alle benoetigten Schluessel in COLUMN_MAP gefunden wurden
if None in col_indices.values():
missing = [k for k, v in col_indices.items() if v is None]
- self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer wiki_reextract_missing_an: {missing}. Breche ab.")
+ self.logger.critical(f"FEHLER: Benoetigte Spaltenschluessel fehlen in COLUMN_MAP fuer wiki_reextract_missing_an: {missing}. Breche ab.") # <<< GEÄNDERT
return # Beende die Methode bei kritischem Fehler
# Ermitteln Sie die Indizes
@@ -10760,7 +8807,7 @@ class DataProcessor:
log_check = (i < start_sheet_row + 5) or (i % 100 == 0) or (processing_needed_for_row)
if log_check:
company_name = self._get_cell_value_safe(row, "CRM Name").strip() # Block 1 Column Map
- self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Re-extract Check): M ('{m_value[:50]}...') gueltig? {is_m_valid_looking}, AN leer? {is_an_empty}. Benötigt Verarbeitung? {processing_needed_for_row}") # Gekuerzt loggen
+ self.logger.debug(f"Zeile {i} ({company_name[:50]}... Wiki Re-extract Check): M ('{m_value[:50]}...') gueltig? {is_m_valid_looking}, AN leer? {is_an_empty}. Benötigt Verarbeitung? {processing_needed_for_row}") # <<< GEÄNDERT
# Wenn die Verarbeitung fuer diese Zeile nicht noetig ist
@@ -10775,11 +8822,11 @@ class DataProcessor:
# Pruefe das Limit fuer verarbeitete Zeilen
if limit is not None and isinstance(limit, int) and limit > 0 and processed_count > limit:
# Wenn das Limit erreicht ist und es ein positives Limit gibt
- self.logger.info(f"Verarbeitungslimit ({limit}) fuer wiki_reextract_missing_an erreicht. Breche weitere Zeilenpruefung ab.")
+ self.logger.info(f"Verarbeitungslimit ({limit}) fuer wiki_reextract_missing_an erreicht. Breche weitere Zeilenpruefung ab.") # <<< GEÄNDERT
break # Brich die Schleife ab
- self.logger.info(f"Zeile {i}: M gefuellt & AN leer. Versuche Wiki-Re-Extraktion ueber _process_single_row...")
+ self.logger.info(f"Zeile {i}: M gefuellt & AN leer. Versuche Wiki-Re-Extraktion ueber _process_single_row...") # <<< GEÄNDERT
try:
# RUFE _process_single_row AUF (Block 19).
@@ -10798,7 +8845,7 @@ class DataProcessor:
except Exception as e_proc:
# Wenn _process_single_row einen Fehler wirft (nachdem interne Retries aufgaben),
# fangen wir ihn hier, loggen ihn und fahren mit der naechsten Zeile fort.
- self.logger.exception(f"FEHLER bei Verarbeitung von Zeile {i} in wiki_reextract_missing_an: {e_proc}")
+ self.logger.exception(f"FEHLER bei Verarbeitung von Zeile {i} in wiki_reextract_missing_an: {e_proc}") # <<< GEÄNDERT
# Hier koennen Sie z.B. einen Fehlerindikator in eine spezielle Spalte im Sheet schreiben lassen.
# Dieses Update muesste dann separat oder im naechsten Lauf behandelt werden.
@@ -10809,7 +8856,7 @@ class DataProcessor:
# Logge den Abschluss des Modus
- self.logger.info(f"Modus 'wiki_reextract_missing_an' abgeschlossen. {processed_count} Zeilen an _process_single_row uebergeben, {skipped_count} Zeilen uebersprungen.")
+ self.logger.info(f"Modus 'wiki_reextract_missing_an' abgeschlossen. {processed_count} Zeilen an _process_single_row uebergeben, {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT
# Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt.