bugfix
This commit is contained in:
@@ -3861,8 +3861,9 @@ def alignment_demo(sheet):
|
||||
class DataProcessor:
|
||||
"""
|
||||
Verarbeitet Daten aus dem Google Sheet, führt verschiedene Anreicherungs-
|
||||
und Analyseprozesse durch, inklusive Timestamp-basierter Überspringung.
|
||||
Enthält jetzt auch die Datenvorbereitung für das ML-Modell.
|
||||
und Analyseprozesse durch, inklusive Timestamp-basierter Überspringung
|
||||
und erzwungener Neuverarbeitung im Re-Eval-Modus.
|
||||
Enthält auch die Datenvorbereitung für das ML-Modell.
|
||||
"""
|
||||
def __init__(self, sheet_handler):
|
||||
"""
|
||||
@@ -3872,236 +3873,270 @@ class DataProcessor:
|
||||
sheet_handler (GoogleSheetHandler): Eine initialisierte Instanz des GoogleSheetHandlers.
|
||||
"""
|
||||
self.sheet_handler = sheet_handler
|
||||
self.wiki_scraper = WikipediaScraper() # Eigene Instanz des Scrapers
|
||||
# Erstelle eine Instanz des Scrapers für diesen Prozessor
|
||||
# Annahme: WikipediaScraper ist importiert
|
||||
self.wiki_scraper = WikipediaScraper()
|
||||
logging.info("DataProcessor initialisiert.")
|
||||
|
||||
# @retry_on_failure # Vorsicht mit Retry auf dieser Ebene für die ganze Zeile
|
||||
# Die zentrale Methode zur Verarbeitung einer einzelnen Zeile
|
||||
# @retry_on_failure # Retry auf der gesamten Zeile ist riskant
|
||||
# @retry_on_failure # Retry auf der gesamten Zeile ist riskant
|
||||
def _process_single_row(self, row_num_in_sheet, row_data,
|
||||
process_wiki=True, process_chatgpt=True, process_website=True,
|
||||
force_reeval=False): # <-- Parameter ist entscheidend
|
||||
"""
|
||||
Verarbeitet die Daten für eine einzelne Zeile.
|
||||
Priorisiert Wiki-Artikelsuche/-Validierung VOR Extraktion.
|
||||
Prüft Timestamps, es sei denn force_reeval=True.
|
||||
"""
|
||||
logging.info(f"--- Starte Verarbeitung für Zeile {row_num_in_sheet} {'(Re-Eval)' if force_reeval else ''} ---")
|
||||
updates = []
|
||||
now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
any_processing_done = False
|
||||
wiki_data_updated_in_this_run = False
|
||||
def _process_single_row(self, row_num_in_sheet, row_data,
|
||||
process_wiki=True, process_chatgpt=True, process_website=True,
|
||||
force_reeval=False): # <-- Neuer Parameter
|
||||
"""
|
||||
Verarbeitet die Daten für eine einzelne Zeile.
|
||||
Priorisiert Wiki-Artikelsuche/-Validierung VOR Extraktion.
|
||||
Prüft Timestamps, es sei denn force_reeval=True.
|
||||
"""
|
||||
logging.info(f"--- Starte Verarbeitung für Zeile {row_num_in_sheet} {'(Re-Eval)' if force_reeval else ''} ---")
|
||||
updates = []
|
||||
now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
any_processing_done = False
|
||||
wiki_data_updated_in_this_run = False
|
||||
|
||||
# Hilfsfunktion für sicheren Zellenzugriff
|
||||
def get_cell_value(key):
|
||||
idx = COLUMN_MAP.get(key)
|
||||
if idx is not None and len(row_data) > idx: return row_data[idx]
|
||||
return ""
|
||||
# Hilfsfunktion für sicheren Zellenzugriff
|
||||
def get_cell_value(key):
|
||||
# Annahme: COLUMN_MAP ist global verfügbar
|
||||
idx = COLUMN_MAP.get(key)
|
||||
if idx is not None and len(row_data) > idx:
|
||||
return row_data[idx]
|
||||
return ""
|
||||
|
||||
# Lese initiale Werte
|
||||
company_name = get_cell_value("CRM Name")
|
||||
website_url = get_cell_value("CRM Website"); original_website = website_url
|
||||
crm_branche = get_cell_value("CRM Branche"); crm_beschreibung = get_cell_value("CRM Beschreibung")
|
||||
konsistenz_s = get_cell_value("Chat Wiki Konsistenzprüfung")
|
||||
website_raw = get_cell_value("Website Rohtext") or "k.A."
|
||||
website_summary = get_cell_value("Website Zusammenfassung") or "k.A."
|
||||
# Lese initiale Werte
|
||||
company_name = get_cell_value("CRM Name")
|
||||
website_url = get_cell_value("CRM Website"); original_website = website_url
|
||||
crm_branche = get_cell_value("CRM Branche"); crm_beschreibung = get_cell_value("CRM Beschreibung")
|
||||
konsistenz_s = get_cell_value("Chat Wiki Konsistenzprüfung")
|
||||
website_raw = get_cell_value("Website Rohtext") or "k.A."
|
||||
website_summary = get_cell_value("Website Zusammenfassung") or "k.A."
|
||||
|
||||
final_wiki_data = {
|
||||
'url': get_cell_value("Wiki URL") or 'k.A.', 'first_paragraph': get_cell_value("Wiki Absatz") or 'k.A.',
|
||||
'branche': get_cell_value("Wiki Branche") or 'k.A.', 'umsatz': get_cell_value("Wiki Umsatz") or 'k.A.',
|
||||
'mitarbeiter': get_cell_value("Wiki Mitarbeiter") or 'k.A.', 'categories': get_cell_value("Wiki Kategorien") or 'k.A.'
|
||||
}
|
||||
final_page_object = None
|
||||
final_wiki_data = {
|
||||
'url': get_cell_value("Wiki URL") or 'k.A.',
|
||||
'first_paragraph': get_cell_value("Wiki Absatz") or 'k.A.',
|
||||
'branche': get_cell_value("Wiki Branche") or 'k.A.',
|
||||
'umsatz': get_cell_value("Wiki Umsatz") or 'k.A.',
|
||||
'mitarbeiter': get_cell_value("Wiki Mitarbeiter") or 'k.A.',
|
||||
'categories': get_cell_value("Wiki Kategorien") or 'k.A.'
|
||||
}
|
||||
final_page_object = None
|
||||
|
||||
# --- 1. Website Handling (Prüft AT oder force_reeval) ---
|
||||
website_ts_missing = not get_cell_value("Website Scrape Timestamp").strip()
|
||||
website_processing_needed = process_website and (force_reeval or website_ts_missing)
|
||||
# --- 1. Website Handling (Prüft AT oder force_reeval) ---
|
||||
website_ts_missing = not get_cell_value("Website Scrape Timestamp").strip()
|
||||
website_processing_needed = process_website and (force_reeval or website_ts_missing)
|
||||
|
||||
if website_processing_needed:
|
||||
any_processing_done = True
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Starte Website Verarbeitung (Grund: {'Re-Eval' if force_reeval else 'AT fehlt'})...")
|
||||
if not website_url or website_url.strip().lower() == "k.a.":
|
||||
logging.debug(" -> Suche Website via SERP...")
|
||||
new_website = serp_website_lookup(company_name)
|
||||
if new_website != "k.A.":
|
||||
website_url = new_website
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["CRM Website"] + 1)}{row_num_in_sheet}', 'values': [[website_url]]})
|
||||
if website_url and website_url.strip().lower() != "k.a.":
|
||||
logging.debug(f" -> Scrape Rohtext von {website_url}...")
|
||||
new_website_raw = get_website_raw(website_url)
|
||||
logging.debug(f" -> Fasse Rohtext zusammen (Länge: {len(new_website_raw)})...")
|
||||
new_website_summary = summarize_website_content(new_website_raw)
|
||||
website_raw = new_website_raw
|
||||
website_summary = new_website_summary
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [[website_raw]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [[website_summary]]})
|
||||
if website_processing_needed:
|
||||
any_processing_done = True
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Starte Website Verarbeitung (Grund: {'Re-Eval' if force_reeval else 'AT fehlt'})...")
|
||||
if not website_url or website_url.strip().lower() == "k.a.":
|
||||
logging.debug(" -> Suche Website via SERP...")
|
||||
# Annahme: serp_website_lookup existiert und nutzt logging
|
||||
new_website = serp_website_lookup(company_name)
|
||||
if new_website != "k.A.":
|
||||
website_url = new_website
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["CRM Website"] + 1)}{row_num_in_sheet}', 'values': [[website_url]]})
|
||||
if website_url and website_url.strip().lower() != "k.a.":
|
||||
logging.debug(f" -> Scrape Rohtext von {website_url}...")
|
||||
# Annahme: get_website_raw existiert und nutzt logging
|
||||
new_website_raw = get_website_raw(website_url)
|
||||
logging.debug(f" -> Fasse Rohtext zusammen (Länge: {len(str(new_website_raw))})...") # str() für Sicherheit
|
||||
# Annahme: summarize_website_content existiert und nutzt logging
|
||||
new_website_summary = summarize_website_content(new_website_raw)
|
||||
website_raw = new_website_raw
|
||||
website_summary = new_website_summary
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [[website_raw]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [[website_summary]]})
|
||||
else:
|
||||
logging.warning(f" -> Keine gültige Website gefunden/vorhanden für {company_name}.")
|
||||
website_raw, website_summary = "k.A.", "k.A."
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [['k.A.']]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [['k.A.']]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Scrape Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]})
|
||||
elif process_website:
|
||||
logging.debug(f"Zeile {row_num_in_sheet}: Überspringe Website (AT vorhanden und kein Re-Eval).")
|
||||
|
||||
# --- 2. Wikipedia Artikel Findung/Validierung (Prüft AN, S='X(Copied)' oder force_reeval) ---
|
||||
wiki_ts_an_missing = not get_cell_value("Wikipedia Timestamp").strip()
|
||||
status_s_indicates_reparse = konsistenz_s.strip().upper() == "X (URL COPIED)"
|
||||
wiki_processing_needed = process_wiki and (force_reeval or wiki_ts_an_missing or status_s_indicates_reparse)
|
||||
url_to_potentially_parse = get_cell_value("Wiki URL").strip()
|
||||
|
||||
if wiki_processing_needed:
|
||||
any_processing_done = True
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Starte Wikipedia Artikel Findung/Validierung (Grund: {'Re-Eval' if force_reeval else f'AN fehlt? {wiki_ts_an_missing}, S=X(Copied)? {status_s_indicates_reparse}'})...")
|
||||
validated_page = None
|
||||
# Prüfe zuerst, ob die URL in M direkt valide ist
|
||||
if url_to_potentially_parse and url_to_potentially_parse.lower() not in ["k.a.", "kein artikel gefunden"] and url_to_potentially_parse.lower().startswith("http"):
|
||||
logging.debug(f" -> Prüfe Validität der vorhandenen URL aus Spalte M: {url_to_potentially_parse}")
|
||||
try:
|
||||
# Verwende die wiki_scraper Instanz der Klasse
|
||||
page_from_m = wikipedia.page(url_to_potentially_parse.split('/wiki/')[-1].replace('_', ' '), auto_suggest=False, preload=True)
|
||||
if self.wiki_scraper._validate_article(page_from_m, company_name, website_url): # self. hinzufügen
|
||||
validated_page = page_from_m
|
||||
logging.info(f" -> Vorhandene URL aus M '{validated_page.url}' ist valide.")
|
||||
else:
|
||||
logging.debug(f" -> Vorhandene URL aus M '{page_from_m.title}' ist NICHT valide.")
|
||||
except wikipedia.exceptions.PageError:
|
||||
logging.warning(f" -> Seite für vorhandene URL aus M '{url_to_potentially_parse}' nicht gefunden (PageError).")
|
||||
except wikipedia.exceptions.DisambiguationError as e_disamb_m:
|
||||
logging.info(f" -> Vorhandene URL aus M '{url_to_potentially_parse}' ist eine Begriffsklärung. Starte Suche...")
|
||||
# Verwende die wiki_scraper Instanz der Klasse
|
||||
validated_page = self.wiki_scraper.search_company_article(company_name, website_url) # self. hinzufügen
|
||||
except Exception as e_val_m:
|
||||
logging.error(f" -> Fehler beim Prüfen der URL aus M '{url_to_potentially_parse}': {e_val_m}")
|
||||
|
||||
# Wenn URL aus M nicht valide war oder keine vorhanden war, starte die Suche
|
||||
if not validated_page:
|
||||
logging.info(f" -> Keine valide URL in M gefunden oder Prüfung fehlgeschlagen. Starte Wikipedia-Suche für '{company_name}'...")
|
||||
# Verwende die wiki_scraper Instanz der Klasse
|
||||
validated_page = self.wiki_scraper.search_company_article(company_name, website_url) # self. hinzufügen
|
||||
|
||||
# Datenextraktion NACH erfolgreicher Findung/Validierung
|
||||
if validated_page:
|
||||
logging.info(f" -> Valider Artikel gefunden/bestätigt: {validated_page.url}. Extrahiere Daten...")
|
||||
final_page_object = validated_page
|
||||
# Verwende die wiki_scraper Instanz der Klasse
|
||||
extracted_data = self.wiki_scraper.extract_company_data(validated_page.url) # self. hinzufügen
|
||||
final_wiki_data = extracted_data
|
||||
wiki_data_updated_in_this_run = True
|
||||
logging.info(f" -> Datenextraktion für '{validated_page.title}' abgeschlossen.")
|
||||
else:
|
||||
logging.warning(f" -> Konnte keinen validen Wikipedia Artikel für '{company_name}' finden/bestätigen.")
|
||||
final_wiki_data = {'url': 'Kein Artikel gefunden', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'}
|
||||
wiki_data_updated_in_this_run = True
|
||||
|
||||
# Füge Updates für M-R und AN hinzu
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki URL"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('url', 'k.A.')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Absatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('first_paragraph', 'k.A.')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Branche"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('branche', 'k.A.')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('umsatz', 'k.A.')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('mitarbeiter', 'k.A.')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Kategorien"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('categories', 'k.A.')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wikipedia Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]})
|
||||
|
||||
# Setze S zurück, wenn nötig
|
||||
if status_s_indicates_reparse or (url_to_potentially_parse != final_wiki_data.get('url')) or force_reeval:
|
||||
s_idx = COLUMN_MAP.get("Chat Wiki Konsistenzprüfung")
|
||||
if s_idx is not None:
|
||||
s_let = self.sheet_handler._get_col_letter(s_idx + 1)
|
||||
updates.append({'range': f'{s_let}{row_num_in_sheet}', 'values': [["?"]]})
|
||||
logging.info(f" -> Status S zurückgesetzt auf '?' für erneute Verifikation.")
|
||||
|
||||
elif process_wiki:
|
||||
logging.debug(f"Zeile {row_num_in_sheet}: Überspringe Wikipedia Verarbeitung (AN vorhanden, kein S=X(Copied) und kein Re-Eval).")
|
||||
|
||||
# --- 3. ChatGPT Evaluationen (Branch etc.) ---
|
||||
chat_ts_ao_missing = not get_cell_value("Timestamp letzte Prüfung").strip()
|
||||
run_chat_eval = process_chatgpt and (force_reeval or chat_ts_ao_missing or wiki_data_updated_in_this_run)
|
||||
|
||||
if run_chat_eval:
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Starte ChatGPT Evaluationen (Grund: {'Re-Eval' if force_reeval else f'AO fehlt? {chat_ts_ao_missing}, Wiki gerade aktualisiert? {wiki_data_updated_in_this_run}'})...")
|
||||
any_processing_done = True
|
||||
|
||||
# Annahme: evaluate_branche_chatgpt existiert und nutzt logging
|
||||
branch_result = evaluate_branche_chatgpt(
|
||||
crm_branche, crm_beschreibung,
|
||||
final_wiki_data.get('branche', 'k.A.'),
|
||||
final_wiki_data.get('categories', 'k.A.'),
|
||||
website_summary
|
||||
)
|
||||
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', 'Fehler')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begründung Abweichung Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('justification', 'Fehler')]]})
|
||||
|
||||
# --- Hier Platz für weitere ChatGPT-Calls ---
|
||||
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Prüfung"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]})
|
||||
|
||||
elif process_chatgpt:
|
||||
logging.debug(f"Zeile {row_num_in_sheet}: Überspringe ChatGPT Evaluationen (AO vorhanden, Wiki nicht gerade aktualisiert und kein Re-Eval).")
|
||||
|
||||
# --- 4. Abschließende Updates ---
|
||||
if any_processing_done:
|
||||
# Annahme: Config ist verfügbar
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Version"] + 1)}{row_num_in_sheet}', 'values': [[Config.VERSION]]})
|
||||
|
||||
# --- 5. Batch Update für diese Zeile ---
|
||||
if updates:
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Sende Batch-Update mit {len(updates)} Operationen...")
|
||||
success = self.sheet_handler.batch_update_cells(updates) # Annahme: nutzt logging
|
||||
if not success: logging.error(f"Zeile {row_num_in_sheet}: FEHLER beim Batch-Update.")
|
||||
else:
|
||||
logging.warning(f" -> Keine gültige Website gefunden/vorhanden für {company_name}.")
|
||||
website_raw, website_summary = "k.A.", "k.A."
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [['k.A.']]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [['k.A.']]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Scrape Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]})
|
||||
elif process_website:
|
||||
logging.debug(f"Zeile {row_num_in_sheet}: Überspringe Website (AT vorhanden und kein Re-Eval).")
|
||||
if not any_processing_done:
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben (alle Schritte übersprungen).")
|
||||
|
||||
# --- 2. Wikipedia Artikel Findung/Validierung (Prüft AN, S='X(Copied)' oder force_reeval) ---
|
||||
wiki_ts_an_missing = not get_cell_value("Wikipedia Timestamp").strip()
|
||||
status_s_indicates_reparse = konsistenz_s.strip().upper() == "X (URL COPIED)"
|
||||
wiki_processing_needed = process_wiki and (force_reeval or wiki_ts_an_missing or status_s_indicates_reparse)
|
||||
url_to_potentially_parse = get_cell_value("Wiki URL").strip()
|
||||
logging.info(f"--- Verarbeitung für Zeile {row_num_in_sheet} abgeschlossen ---")
|
||||
# Annahme: Config ist verfügbar
|
||||
time.sleep(max(0.1, getattr(Config, 'RETRY_DELAY', 5) / 20))
|
||||
|
||||
if wiki_processing_needed:
|
||||
any_processing_done = True
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Starte Wikipedia Artikel Findung/Validierung (Grund: {'Re-Eval' if force_reeval else f'AN fehlt? {wiki_ts_an_missing}, S=X(Copied)? {status_s_indicates_reparse}'})...")
|
||||
validated_page = None
|
||||
if url_to_potentially_parse and url_to_potentially_parse.lower() not in ["k.a.", "kein artikel gefunden"] and url_to_potentially_parse.lower().startswith("http"):
|
||||
logging.debug(f" -> Prüfe Validität der vorhandenen URL aus Spalte M: {url_to_potentially_parse}")
|
||||
try:
|
||||
page_from_m = wikipedia.page(url_to_potentially_parse.split('/wiki/')[-1].replace('_', ' '), auto_suggest=False, preload=True)
|
||||
if self.wiki_scraper._validate_article(page_from_m, company_name, website_url):
|
||||
validated_page = page_from_m
|
||||
logging.info(f" -> Vorhandene URL aus M '{validated_page.url}' ist valide.")
|
||||
else:
|
||||
logging.debug(f" -> Vorhandene URL aus M '{page_from_m.title}' ist NICHT valide.")
|
||||
except wikipedia.exceptions.PageError:
|
||||
logging.warning(f" -> Seite für vorhandene URL aus M '{url_to_potentially_parse}' nicht gefunden (PageError).")
|
||||
except wikipedia.exceptions.DisambiguationError as e_disamb_m:
|
||||
logging.info(f" -> Vorhandene URL aus M '{url_to_potentially_parse}' ist eine Begriffsklärung. Starte Suche...")
|
||||
validated_page = self.wiki_scraper.search_company_article(company_name, website_url)
|
||||
except Exception as e_val_m:
|
||||
logging.error(f" -> Fehler beim Prüfen der URL aus M '{url_to_potentially_parse}': {e_val_m}")
|
||||
|
||||
if not validated_page:
|
||||
logging.info(f" -> Keine valide URL in M gefunden oder Prüfung fehlgeschlagen. Starte Wikipedia-Suche für '{company_name}'...")
|
||||
validated_page = self.wiki_scraper.search_company_article(company_name, website_url)
|
||||
|
||||
if validated_page:
|
||||
logging.info(f" -> Valider Artikel gefunden/bestätigt: {validated_page.url}. Extrahiere Daten...")
|
||||
final_page_object = validated_page
|
||||
extracted_data = self.wiki_scraper.extract_company_data(validated_page.url)
|
||||
final_wiki_data = extracted_data
|
||||
wiki_data_updated_in_this_run = True
|
||||
logging.info(f" -> Datenextraktion für '{validated_page.title}' abgeschlossen.")
|
||||
else:
|
||||
logging.warning(f" -> Konnte keinen validen Wikipedia Artikel für '{company_name}' finden/bestätigen.")
|
||||
final_wiki_data = {'url': 'Kein Artikel gefunden', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'}
|
||||
wiki_data_updated_in_this_run = True
|
||||
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki URL"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('url', 'k.A.')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Absatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('first_paragraph', 'k.A.')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Branche"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('branche', 'k.A.')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('umsatz', 'k.A.')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('mitarbeiter', 'k.A.')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Kategorien"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('categories', 'k.A.')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wikipedia Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]})
|
||||
|
||||
if status_s_indicates_reparse or (url_to_potentially_parse != final_wiki_data.get('url')) or force_reeval:
|
||||
s_idx = COLUMN_MAP.get("Chat Wiki Konsistenzprüfung")
|
||||
if s_idx is not None:
|
||||
s_let = self.sheet_handler._get_col_letter(s_idx + 1)
|
||||
updates.append({'range': f'{s_let}{row_num_in_sheet}', 'values': [["?"]]})
|
||||
logging.info(f" -> Status S zurückgesetzt auf '?' für erneute Verifikation.")
|
||||
|
||||
elif process_wiki:
|
||||
logging.debug(f"Zeile {row_num_in_sheet}: Überspringe Wikipedia Verarbeitung (AN vorhanden, kein S=X(Copied) und kein Re-Eval).")
|
||||
|
||||
# --- 3. ChatGPT Evaluationen (Branch etc.) ---
|
||||
chat_ts_ao_missing = not get_cell_value("Timestamp letzte Prüfung").strip()
|
||||
run_chat_eval = process_chatgpt and (force_reeval or chat_ts_ao_missing or wiki_data_updated_in_this_run)
|
||||
|
||||
if run_chat_eval:
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Starte ChatGPT Evaluationen (Grund: {'Re-Eval' if force_reeval else f'AO fehlt? {chat_ts_ao_missing}, Wiki gerade aktualisiert? {wiki_data_updated_in_this_run}'})...")
|
||||
any_processing_done = True
|
||||
|
||||
branch_result = evaluate_branche_chatgpt(
|
||||
crm_branche, crm_beschreibung,
|
||||
final_wiki_data.get('branche', 'k.A.'),
|
||||
final_wiki_data.get('categories', 'k.A.'),
|
||||
website_summary
|
||||
)
|
||||
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', 'Fehler')]]})
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begründung Abweichung Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('justification', 'Fehler')]]})
|
||||
|
||||
# --- Hier Platz für weitere ChatGPT-Calls ---
|
||||
# z.B. FSM Relevanz, Mitarbeiter-Schätzung etc.
|
||||
# Denke daran, 'final_wiki_data' und 'website_summary' / 'website_raw' zu verwenden.
|
||||
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Prüfung"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]})
|
||||
|
||||
elif process_chatgpt:
|
||||
logging.debug(f"Zeile {row_num_in_sheet}: Überspringe ChatGPT Evaluationen (AO vorhanden, Wiki nicht gerade aktualisiert und kein Re-Eval).")
|
||||
|
||||
# --- 4. Abschließende Updates ---
|
||||
if any_processing_done:
|
||||
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Version"] + 1)}{row_num_in_sheet}', 'values': [[Config.VERSION]]})
|
||||
|
||||
# --- 5. Batch Update für diese Zeile ---
|
||||
if updates:
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Sende Batch-Update mit {len(updates)} Operationen...")
|
||||
success = self.sheet_handler.batch_update_cells(updates)
|
||||
if not success: logging.error(f"Zeile {row_num_in_sheet}: FEHLER beim Batch-Update.")
|
||||
else:
|
||||
if not any_processing_done:
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben (alle Schritte übersprungen).")
|
||||
|
||||
logging.info(f"--- Verarbeitung für Zeile {row_num_in_sheet} abgeschlossen ---")
|
||||
time.sleep(max(0.1, getattr(Config, 'RETRY_DELAY', 5) / 20))
|
||||
|
||||
|
||||
def process_rows_sequentially(self, start_row_index, num_rows_to_process, process_wiki=True, process_chatgpt=True, process_website=True):
|
||||
# Methode zur sequenziellen Verarbeitung (ruft _process_single_row ohne force_reeval)
|
||||
def process_rows_sequentially(self, start_data_index, num_rows_to_process,
|
||||
process_wiki=True, process_chatgpt=True, process_website=True):
|
||||
""" Verarbeitet Zeilen sequentiell ab einem Startindex. """
|
||||
data_rows = self.sheet_handler.get_data() # Daten ohne Header
|
||||
header_rows = 5
|
||||
|
||||
if start_row_index >= len(data_rows):
|
||||
debug_print("Startindex liegt hinter der letzten Datenzeile. Keine Verarbeitung.")
|
||||
# Annahme: sheet_handler wurde im __init__ gesetzt
|
||||
if not self.sheet_handler or not self.sheet_handler.sheet_values:
|
||||
logging.error("Sheet Handler nicht verfügbar oder keine Daten geladen in process_rows_sequentially.")
|
||||
return
|
||||
|
||||
# Berechne den Endindex sicher
|
||||
end_row_index = min(start_row_index + num_rows_to_process, len(data_rows))
|
||||
actual_rows_to_process = end_row_index - start_row_index
|
||||
data_rows = self.sheet_handler.get_data() # Daten ohne Header
|
||||
header_rows = 5 # Annahme
|
||||
|
||||
if start_data_index >= len(data_rows):
|
||||
logging.warning(f"Startindex {start_data_index} liegt hinter der letzten Datenzeile ({len(data_rows)}). Keine Verarbeitung.")
|
||||
return
|
||||
|
||||
end_row_index = min(start_data_index + num_rows_to_process, len(data_rows))
|
||||
actual_rows_to_process = end_row_index - start_data_index
|
||||
|
||||
if actual_rows_to_process <= 0:
|
||||
debug_print("Keine Zeilen zur sequenziellen Verarbeitung übrig.")
|
||||
logging.info("Keine Zeilen zur sequenziellen Verarbeitung übrig.")
|
||||
return
|
||||
|
||||
debug_print(f"Verarbeite {actual_rows_to_process} Zeilen sequenziell (Daten-Index {start_row_index} bis {end_row_index - 1})...")
|
||||
logging.info(f"Verarbeite {actual_rows_to_process} Zeilen sequenziell (Daten-Index {start_data_index} bis {end_row_index - 1})...")
|
||||
|
||||
for i in range(start_row_index, end_row_index):
|
||||
if i >= len(data_rows): # Zusätzliche Sicherheitsprüfung
|
||||
debug_print(f"WARNUNG: Index {i} überschreitet Datenlänge ({len(data_rows)}). Breche Schleife ab.")
|
||||
for i in range(start_data_index, end_row_index):
|
||||
if i >= len(data_rows):
|
||||
logging.warning(f"WARNUNG: Index {i} überschreitet Datenlänge ({len(data_rows)}). Breche Schleife ab.")
|
||||
break
|
||||
row_data = data_rows[i]
|
||||
row_num_in_sheet = i + header_rows + 1 # 1-basierter Sheet-Index
|
||||
row_num_in_sheet = i + header_rows + 1
|
||||
|
||||
# Rufe die detaillierte Verarbeitungsmethode auf
|
||||
self._process_single_row(row_num_in_sheet, row_data, process_wiki, process_chatgpt, process_website)
|
||||
# Rufe die zentrale Verarbeitungsmethode auf, OHNE force_reeval
|
||||
try:
|
||||
self._process_single_row(row_num_in_sheet, row_data,
|
||||
process_wiki, process_chatgpt, process_website,
|
||||
force_reeval=False) # HIER ist der Unterschied zu reeval
|
||||
except Exception as e_seq:
|
||||
# Fange Fehler bei der Verarbeitung einzelner Zeilen ab, um den Lauf nicht zu stoppen
|
||||
logging.exception(f"Fehler bei der sequenziellen Verarbeitung von Zeile {row_num_in_sheet}: {e_seq}")
|
||||
# Optional: Markiere die Zeile im Sheet als fehlerhaft?
|
||||
|
||||
logging.info(f"Sequenzielle Verarbeitung von {actual_rows_to_process} Zeilen abgeschlossen.")
|
||||
|
||||
|
||||
# Methode für den Re-Eval Modus (ruft _process_single_row MIT force_reeval)
|
||||
def process_reevaluation_rows(self, row_limit=None, clear_flag=True):
|
||||
"""
|
||||
Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind.
|
||||
Ruft _process_single_row für jede dieser Zeilen auf.
|
||||
Ruft _process_single_row für jede dieser Zeilen auf mit force_reeval=True.
|
||||
Verarbeitet maximal row_limit Zeilen.
|
||||
Löscht optional das 'x'-Flag nach erfolgreicher Verarbeitung.
|
||||
"""
|
||||
logging.info(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}")
|
||||
|
||||
# Lade Daten frisch
|
||||
if not self.sheet_handler.load_data(): return
|
||||
all_data = self.sheet_handler.get_all_data_with_headers()
|
||||
if not all_data or len(all_data) <= 5:
|
||||
logging.warning("Keine Daten für Re-Evaluation gefunden.")
|
||||
return
|
||||
logging.warning("Keine Daten für Re-Evaluation gefunden.")
|
||||
return
|
||||
header_rows = 5
|
||||
data_rows = all_data[header_rows:]
|
||||
|
||||
# Annahme: COLUMN_MAP ist global verfügbar
|
||||
reeval_col_idx = COLUMN_MAP.get("ReEval Flag")
|
||||
if reeval_col_idx is None:
|
||||
logging.error("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden.")
|
||||
return
|
||||
logging.error("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden.")
|
||||
return
|
||||
|
||||
# Finde zuerst alle Kandidaten (Zeilennummer im Sheet und Rohdaten)
|
||||
rows_to_process = []
|
||||
for idx, row in enumerate(data_rows):
|
||||
if len(row) > reeval_col_idx and row[reeval_col_idx].strip().lower() == "x":
|
||||
@@ -4112,107 +4147,67 @@ def _process_single_row(self, row_num_in_sheet, row_data,
|
||||
logging.info(f"{found_count} Zeilen mit ReEval-Flag 'x' gefunden.")
|
||||
|
||||
if found_count == 0:
|
||||
logging.info("Keine Zeilen zur Re-Evaluation markiert.")
|
||||
return
|
||||
logging.info("Keine Zeilen zur Re-Evaluation markiert.")
|
||||
return
|
||||
|
||||
processed_count = 0
|
||||
updates_clear_flag = []
|
||||
rows_actually_processed = [] # Liste der Zeilennummern, die verarbeitet wurden
|
||||
rows_actually_processed = []
|
||||
|
||||
# Verarbeite die gefundenen Kandidaten bis zum Limit
|
||||
# WICHTIG: Iteriere nur über die Kandidatenliste, nicht die gesamten Daten
|
||||
for task in rows_to_process:
|
||||
# --- KORRIGIERTE LIMIT-PRÜFUNG ---
|
||||
# Prüfe das Limit *bevor* die Verarbeitung beginnt
|
||||
if row_limit is not None and processed_count >= row_limit:
|
||||
logging.info(f"Zeilenlimit ({row_limit}) für Re-Evaluation erreicht. Breche weitere Verarbeitung ab.")
|
||||
break # Verlasse die Schleife
|
||||
break
|
||||
|
||||
row_num = task['row_num']
|
||||
row_data = task['data']
|
||||
logging.info(f"--- Re-Evaluiere Zeile {row_num} ---")
|
||||
try:
|
||||
# Führe volle Verarbeitung für diese Zeile durch
|
||||
# _process_single_row prüft intern Timestamps AN, AT, AO etc.
|
||||
# Rufe _process_single_row mit force_reeval=True auf
|
||||
self._process_single_row(row_num, row_data,
|
||||
process_wiki=True, process_chatgpt=True, process_website=True,
|
||||
force_reeval=True)
|
||||
process_wiki=True, process_chatgpt=True, process_website=True,
|
||||
force_reeval=True) # WICHTIG!
|
||||
processed_count += 1
|
||||
rows_actually_processed.append(row_num) # Füge zur Liste der verarbeiteten hinzu
|
||||
rows_actually_processed.append(row_num)
|
||||
|
||||
# Optional: Flag nach *erfolgreicher* Verarbeitung löschen (Sammeln)
|
||||
if clear_flag:
|
||||
flag_col_letter = self.sheet_handler._get_col_letter(reeval_col_idx + 1)
|
||||
if flag_col_letter: # Nur wenn Buchstabe gültig
|
||||
updates_clear_flag.append({'range': f'{flag_col_letter}{row_num}', 'values': [['']]})
|
||||
flag_col_letter = self.sheet_handler._get_col_letter(reeval_col_idx + 1)
|
||||
if flag_col_letter:
|
||||
updates_clear_flag.append({'range': f'{flag_col_letter}{row_num}', 'values': [['']]})
|
||||
|
||||
except Exception as e_proc:
|
||||
# Logge Fehler, aber mache mit der nächsten Zeile weiter
|
||||
logging.exception(f"FEHLER bei Re-Evaluation von Zeile {row_num}: {e_proc}")
|
||||
# Flag hier nicht löschen, damit es beim nächsten Mal versucht wird
|
||||
|
||||
# Lösche Flags am Ende gebündelt für die *erfolgreich* verarbeiteten Zeilen
|
||||
if clear_flag and updates_clear_flag:
|
||||
# Logge, welche Flags gelöscht werden
|
||||
logging.info(f"Lösche ReEval-Flags für {len(updates_clear_flag)} erfolgreich verarbeitete Zeilen ({rows_actually_processed})...")
|
||||
success = self.sheet_handler.batch_update_cells(updates_clear_flag)
|
||||
if not success:
|
||||
logging.error("FEHLER beim Löschen der ReEval-Flags.")
|
||||
logging.info(f"Lösche ReEval-Flags für {len(updates_clear_flag)} erfolgreich verarbeitete Zeilen ({rows_actually_processed})...")
|
||||
success = self.sheet_handler.batch_update_cells(updates_clear_flag)
|
||||
if not success:
|
||||
logging.error("FEHLER beim Löschen der ReEval-Flags.")
|
||||
|
||||
logging.info(f"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Limit war: {row_limit}, Gefunden: {found_count}).")
|
||||
|
||||
|
||||
def process_website_details_for_marked_rows(self):
|
||||
""" Neuer Modus 23: Extrahiert Website-Details für markierte Zeilen. """
|
||||
debug_print("Starte Modus 23: Website Detail Extraction für Zeilen mit 'x' in Spalte A.")
|
||||
data_rows = self.sheet_handler.get_data()
|
||||
header_rows = 5
|
||||
rows_processed = 0
|
||||
reeval_col_idx = COLUMN_MAP.get("ReEval Flag")
|
||||
website_col_idx = COLUMN_MAP.get("CRM Website")
|
||||
details_col = f"AR" # Spalte AR für Details? War vorher Rohtext. Ggf. neue Spalte?
|
||||
|
||||
if reeval_col_idx is None or website_col_idx is None:
|
||||
debug_print("FEHLER: Benötigte Spalten für Modus 23 nicht in COLUMN_MAP gefunden.")
|
||||
return
|
||||
|
||||
for i, row in enumerate(data_rows):
|
||||
row_num_in_sheet = i + header_rows + 1
|
||||
if len(row) > reeval_col_idx and row[reeval_col_idx].strip().lower() == "x":
|
||||
website_url = row[website_col_idx] if len(row) > website_col_idx else ""
|
||||
if not website_url or website_url.strip().lower() == "k.a.":
|
||||
debug_print(f"Zeile {row_num_in_sheet}: Keine gültige Website in Spalte D vorhanden, überspringe.")
|
||||
continue
|
||||
|
||||
debug_print(f"Zeile {row_num_in_sheet}: Extrahiere Website Details von {website_url}...")
|
||||
details = scrape_website_details(website_url) # Annahme: Diese Funktion existiert
|
||||
|
||||
# Speichere das Detail-Ergebnis in Spalte AR (Index 43)
|
||||
update_data = [{'range': f'{details_col}{row_num_in_sheet}', 'values': [[details]]}]
|
||||
# Optional: Timestamp setzen? In AT?
|
||||
# update_data.append({'range': f'AT{row_num_in_sheet}', 'values': [[datetime.now().strftime("%Y-%m-%d %H:%M:%S")]]})
|
||||
|
||||
self.sheet_handler.batch_update_cells(update_data)
|
||||
debug_print(f"Zeile {row_num_in_sheet}: Website Detail Extraction abgeschlossen, Ergebnis in Spalte {details_col} geschrieben.")
|
||||
rows_processed += 1
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
|
||||
debug_print(f"Modus 23 abgeschlossen. {rows_processed} Zeilen verarbeitet.")
|
||||
|
||||
|
||||
# Methode für SERP API Website Lookup
|
||||
def process_serp_website_lookup_for_empty(self):
|
||||
""" Neuer Modus 22: Füllt fehlende Websites via SERP API. """
|
||||
debug_print("Starte Modus 22: SERP API Website Lookup für leere Zellen in Spalte D.")
|
||||
logging.info("Starte Modus: SERP API Website Lookup für leere Zellen in Spalte D.")
|
||||
# Annahme: sheet_handler wurde initialisiert
|
||||
if not self.sheet_handler.load_data(): return
|
||||
data_rows = self.sheet_handler.get_data()
|
||||
header_rows = 5
|
||||
rows_processed = 0
|
||||
website_col_idx = COLUMN_MAP.get("CRM Website")
|
||||
name_col_idx = COLUMN_MAP.get("CRM Name")
|
||||
|
||||
if website_col_idx is None or name_col_idx is None:
|
||||
debug_print("FEHLER: Benötigte Spalten für Modus 22 nicht in COLUMN_MAP gefunden.")
|
||||
try:
|
||||
website_col_idx = COLUMN_MAP["CRM Website"]
|
||||
name_col_idx = COLUMN_MAP["CRM Name"]
|
||||
website_col_letter = self.sheet_handler._get_col_letter(website_col_idx + 1)
|
||||
except KeyError as e:
|
||||
logging.critical(f"FEHLER: Benötigte Spalte '{e}' für Modus nicht in COLUMN_MAP.")
|
||||
return
|
||||
except Exception as e:
|
||||
logging.critical(f"FEHLER beim Holen der Spaltenbuchstaben: {e}")
|
||||
return
|
||||
|
||||
updates = []
|
||||
for i, row in enumerate(data_rows):
|
||||
row_num_in_sheet = i + header_rows + 1
|
||||
current_website = row[website_col_idx] if len(row) > website_col_idx else ""
|
||||
@@ -4220,158 +4215,220 @@ def _process_single_row(self, row_num_in_sheet, row_data,
|
||||
if not current_website or current_website.strip().lower() == "k.a.":
|
||||
company_name = row[name_col_idx] if len(row) > name_col_idx else ""
|
||||
if not company_name:
|
||||
debug_print(f"Zeile {row_num_in_sheet}: Übersprungen (kein Firmenname für Lookup).")
|
||||
logging.warning(f"Zeile {row_num_in_sheet}: Übersprungen (kein Firmenname für Lookup).")
|
||||
continue
|
||||
|
||||
debug_print(f"Zeile {row_num_in_sheet}: Suche Website für '{company_name}'...")
|
||||
new_website = serp_website_lookup(company_name) # Annahme: Diese Funktion existiert
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Suche Website für '{company_name}'...")
|
||||
# Annahme: serp_website_lookup existiert und nutzt logging
|
||||
new_website = serp_website_lookup(company_name)
|
||||
time.sleep(getattr(Config, 'RETRY_DELAY', 5) * 0.3) # Kurze Pause
|
||||
|
||||
if new_website != "k.A.":
|
||||
update_data = [{'range': f'D{row_num_in_sheet}', 'values': [[new_website]]}]
|
||||
# Optional: Timestamp setzen? Wo? AT?
|
||||
self.sheet_handler.batch_update_cells(update_data)
|
||||
debug_print(f"Zeile {row_num_in_sheet}: Neue Website '{new_website}' gefunden und in Spalte D eingetragen.")
|
||||
updates.append({'range': f'{website_col_letter}{row_num_in_sheet}', 'values': [[new_website]]})
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Neue Website '{new_website}' gefunden und zum Update hinzugefügt.")
|
||||
rows_processed += 1
|
||||
else:
|
||||
debug_print(f"Zeile {row_num_in_sheet}: Keine Website gefunden.")
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Keine Website gefunden.")
|
||||
# Optional: Limit für diesen Modus?
|
||||
# if row_limit is not None and rows_processed >= row_limit: break
|
||||
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
if updates:
|
||||
logging.info(f"Sende Batch-Update für {rows_processed} gefundene Websites...")
|
||||
self.sheet_handler.batch_update_cells(updates)
|
||||
else:
|
||||
logging.info("Keine fehlenden Websites gefunden zum Aktualisieren.")
|
||||
|
||||
debug_print(f"Modus 22 abgeschlossen. {rows_processed} Websites ergänzt.")
|
||||
logging.info(f"Modus 'website_lookup' abgeschlossen. {rows_processed} Websites ergänzt.")
|
||||
|
||||
# --- NEU: Datenvorbereitung als Methode der Klasse ---
|
||||
# Methode für experimentelle Website Details
|
||||
def process_website_details_for_marked_rows(self):
|
||||
""" Neuer Modus 23 (EXPERIMENTELL): Extrahiert Website-Details für markierte Zeilen. """
|
||||
logging.info("Starte Modus (EXPERIMENTELL): Website Detail Extraction für Zeilen mit 'x' in Spalte A.")
|
||||
if not self.sheet_handler.load_data(): return
|
||||
data_rows = self.sheet_handler.get_data()
|
||||
header_rows = 5
|
||||
rows_processed = 0
|
||||
|
||||
try:
|
||||
reeval_col_idx = COLUMN_MAP["ReEval Flag"]
|
||||
website_col_idx = COLUMN_MAP["CRM Website"]
|
||||
details_col_idx = COLUMN_MAP["Website Rohtext"] # Nutze AR für Details? Besser neue Spalte!
|
||||
details_col_letter = self.sheet_handler._get_col_letter(details_col_idx + 1)
|
||||
# at_col_letter = self.sheet_handler._get_col_letter(COLUMN_MAP["Website Scrape Timestamp"] + 1) # Für Timestamp
|
||||
except KeyError as e:
|
||||
logging.critical(f"FEHLER: Benötigte Spalte '{e}' für Modus nicht in COLUMN_MAP.")
|
||||
return
|
||||
except Exception as e:
|
||||
logging.critical(f"FEHLER beim Holen der Spaltenbuchstaben: {e}")
|
||||
return
|
||||
|
||||
updates = []
|
||||
for i, row in enumerate(data_rows):
|
||||
row_num_in_sheet = i + header_rows + 1
|
||||
if len(row) > reeval_col_idx and row[reeval_col_idx].strip().lower() == "x":
|
||||
website_url = row[website_col_idx] if len(row) > website_col_idx else ""
|
||||
if not website_url or website_url.strip().lower() == "k.a.":
|
||||
logging.warning(f"Zeile {row_num_in_sheet}: Keine gültige Website in Spalte D vorhanden, überspringe.")
|
||||
continue
|
||||
|
||||
logging.info(f"Zeile {row_num_in_sheet}: Extrahiere Website Details von {website_url}...")
|
||||
# Annahme: Funktion scrape_website_details existiert
|
||||
try:
|
||||
details = scrape_website_details(website_url)
|
||||
except NameError:
|
||||
logging.error("Funktion 'scrape_website_details' ist nicht definiert!")
|
||||
details = "FEHLER: Funktion nicht definiert"
|
||||
except Exception as e_detail:
|
||||
logging.exception(f"Fehler bei scrape_website_details für {website_url}: {e_detail}")
|
||||
details = f"FEHLER: {e_detail}"
|
||||
|
||||
updates.append({'range': f'{details_col_letter}{row_num_in_sheet}', 'values': [[str(details)]]}) # In AR schreiben
|
||||
# Optional: Timestamp in AT setzen
|
||||
# updates.append({'range': f'{at_col_letter}{row_num_in_sheet}', 'values': [[datetime.now().strftime("%Y-%m-%d %H:%M:%S")]]})
|
||||
|
||||
rows_processed += 1
|
||||
time.sleep(getattr(Config, 'RETRY_DELAY', 5) * 0.2) # Kleine Pause
|
||||
|
||||
if updates:
|
||||
logging.info(f"Sende Batch-Update für {rows_processed} Detail-Extraktionen...")
|
||||
self.sheet_handler.batch_update_cells(updates)
|
||||
else:
|
||||
logging.info("Keine Zeilen mit 'x' gefunden für Detail-Extraktion.")
|
||||
|
||||
logging.info(f"Modus 'website_details' abgeschlossen. {rows_processed} Zeilen verarbeitet.")
|
||||
|
||||
|
||||
# Methode zur Datenvorbereitung für ML
|
||||
def prepare_data_for_modeling(self):
|
||||
"""
|
||||
Lädt Daten aus dem Google Sheet über den sheet_handler,
|
||||
bereitet sie für das Decision Tree Modell vor. (Implementierung siehe vorherige Antwort)
|
||||
bereitet sie für das Decision Tree Modell vor.
|
||||
"""
|
||||
debug_print("Starte Datenvorbereitung für Modellierung...")
|
||||
logging.info("Starte Datenvorbereitung für Modellierung...")
|
||||
# Annahme: sheet_handler ist initialisiert und hat Daten geladen
|
||||
if not self.sheet_handler or not self.sheet_handler.sheet_values:
|
||||
logging.error("Fehler: Sheet Handler nicht initialisiert oder keine Daten geladen für prepare_data.")
|
||||
if not self.sheet_handler.load_data(): # Versuch nachzuladen
|
||||
logging.critical("Konnte Daten auch nach erneutem Versuch nicht laden.")
|
||||
return None
|
||||
|
||||
try:
|
||||
# --- 1. Daten laden & Spalten auswählen ---
|
||||
if not self.sheet_handler or not self.sheet_handler.sheet_values:
|
||||
debug_print("Fehler: Sheet Handler nicht initialisiert oder keine Daten geladen.")
|
||||
return None
|
||||
|
||||
all_data = self.sheet_handler.sheet_values
|
||||
if len(all_data) <= 5:
|
||||
debug_print("Fehler: Nicht genügend Datenzeilen im Sheet gefunden.")
|
||||
return None
|
||||
|
||||
headers = all_data[0]
|
||||
data_rows = all_data[5:]
|
||||
|
||||
df = pd.DataFrame(data_rows, columns=headers)
|
||||
debug_print(f"DataFrame erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.")
|
||||
|
||||
# Finde die tatsächlichen Spaltennamen anhand der COLUMN_MAP
|
||||
col_indices = {}
|
||||
tech_col_key = "CRM Anzahl Techniker" # <- ANPASSEN, FALLS NÖTIG
|
||||
try:
|
||||
col_indices = {
|
||||
"name": all_data[0][COLUMN_MAP["CRM Name"]],
|
||||
"branche": all_data[0][COLUMN_MAP["CRM Branche"]],
|
||||
"umsatz_crm": all_data[0][COLUMN_MAP["CRM Umsatz"]],
|
||||
"umsatz_wiki": all_data[0][COLUMN_MAP["Wiki Umsatz"]],
|
||||
"ma_crm": all_data[0][COLUMN_MAP["CRM Anzahl Mitarbeiter"]],
|
||||
"ma_wiki": all_data[0][COLUMN_MAP["Wiki Mitarbeiter"]],
|
||||
"techniker": all_data[0][COLUMN_MAP[tech_col_key]]
|
||||
}
|
||||
cols_to_select = list(col_indices.values())
|
||||
except KeyError as e:
|
||||
debug_print(f"FEHLER: Konnte Mapping für Schlüssel '{e}' in COLUMN_MAP nicht finden oder Spalte nicht im Header.")
|
||||
return None
|
||||
except IndexError as e:
|
||||
debug_print(f"FEHLER: Spaltenindex aus COLUMN_MAP ist außerhalb der Grenzen der Header-Zeile: {e}")
|
||||
return None
|
||||
|
||||
df_subset = df[cols_to_select].copy()
|
||||
rename_map = {v: k for k, v in col_indices.items()}
|
||||
df_subset.rename(columns=rename_map, inplace=True)
|
||||
debug_print(f"Benötigte Spalten ausgewählt und umbenannt: {list(df_subset.columns)}")
|
||||
|
||||
# --- 2. Features konsolidieren ---
|
||||
def get_valid_numeric(value_str):
|
||||
# (Implementierung wie in vorheriger Antwort)
|
||||
if value_str is None or pd.isna(value_str) or value_str == '': return np.nan
|
||||
try:
|
||||
val = float(str(value_str).replace(',', '.'))
|
||||
return val if val > 0 else np.nan
|
||||
except (ValueError, TypeError):
|
||||
cleaned_str = re.sub(r'[^\d.]', '', str(value_str))
|
||||
if not cleaned_str: return np.nan
|
||||
try:
|
||||
val = float(cleaned_str)
|
||||
return val if val > 0 else np.nan
|
||||
except ValueError: return np.nan
|
||||
|
||||
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():
|
||||
debug_print(f"Verarbeite '{base_name}'...")
|
||||
if wiki_col not in df_subset.columns: df_subset[wiki_col] = np.nan
|
||||
if crm_col not in df_subset.columns: df_subset[crm_col] = np.nan
|
||||
wiki_numeric = df_subset[wiki_col].apply(get_valid_numeric)
|
||||
crm_numeric = df_subset[crm_col].apply(get_valid_numeric)
|
||||
df_subset[final_col] = np.where(
|
||||
wiki_numeric.notna(), wiki_numeric,
|
||||
np.where(crm_numeric.notna(), crm_numeric, np.nan)
|
||||
)
|
||||
debug_print(f" -> {df_subset[final_col].notna().sum()} gültige '{final_col}' Werte erstellt.")
|
||||
|
||||
# --- 3. Zielvariable vorbereiten ---
|
||||
techniker_col = "techniker"
|
||||
debug_print(f"Verarbeite Zielvariable '{techniker_col}'...")
|
||||
df_subset['Anzahl_Servicetechniker_Numeric'] = pd.to_numeric(df_subset[techniker_col], errors='coerce')
|
||||
initial_rows = len(df_subset)
|
||||
df_filtered = df_subset[
|
||||
df_subset['Anzahl_Servicetechniker_Numeric'].notna() &
|
||||
(df_subset['Anzahl_Servicetechniker_Numeric'] > 0)
|
||||
].copy()
|
||||
filtered_rows = len(df_filtered)
|
||||
debug_print(f"{initial_rows - filtered_rows} Zeilen entfernt (fehlende/ungültige Technikerzahl).")
|
||||
debug_print(f"Verbleibende Zeilen für Modellierung: {filtered_rows}")
|
||||
if filtered_rows == 0: return None
|
||||
|
||||
# --- 4. Techniker-Buckets erstellen ---
|
||||
bins = [-1, 0, 19, 49, 99, 249, 499, float('inf')]
|
||||
labels = ['Bucket_1_(0)', 'Bucket_2_(<20)', 'Bucket_3_(<50)', 'Bucket_4_(<100)', 'Bucket_5_(<250)', 'Bucket_6_(<500)', 'Bucket_7_(>499)']
|
||||
df_filtered['Techniker_Bucket'] = pd.cut(
|
||||
df_filtered['Anzahl_Servicetechniker_Numeric'],
|
||||
bins=bins, labels=labels, right=True
|
||||
)
|
||||
debug_print("Techniker-Buckets erstellt.")
|
||||
debug_print(f"Verteilung der Buckets:\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).round(3)}")
|
||||
|
||||
# --- 5. Kategoriale Features vorbereiten (Branche) ---
|
||||
branche_col = "branche"
|
||||
debug_print(f"Verarbeite kategoriales Feature '{branche_col}'...")
|
||||
df_filtered[branche_col] = df_filtered[branche_col].astype(str).fillna('Unbekannt').str.strip()
|
||||
df_encoded = pd.get_dummies(df_filtered, columns=[branche_col], prefix='Branche', dummy_na=False)
|
||||
debug_print(f"One-Hot Encoding für Branche durchgeführt.")
|
||||
|
||||
# --- 6. Finale Auswahl ---
|
||||
feature_columns = [col for col in df_encoded.columns if col.startswith('Branche_')]
|
||||
feature_columns.extend(['Finaler_Umsatz', 'Finaler_Mitarbeiter'])
|
||||
target_column = 'Techniker_Bucket'
|
||||
original_data_cols = ['name', 'Anzahl_Servicetechniker_Numeric']
|
||||
df_model_ready = df_encoded[original_data_cols + feature_columns + [target_column]].copy()
|
||||
for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter']:
|
||||
df_model_ready[col] = pd.to_numeric(df_model_ready[col], errors='coerce')
|
||||
df_model_ready = df_model_ready.reset_index(drop=True)
|
||||
debug_print("Datenvorbereitung abgeschlossen.")
|
||||
nan_counts = df_model_ready[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].isna().sum()
|
||||
debug_print(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}")
|
||||
|
||||
return df_model_ready
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"FEHLER während der Datenvorbereitung: {e}")
|
||||
import traceback
|
||||
debug_print(traceback.format_exc())
|
||||
all_data = self.sheet_handler.sheet_values # Verwende die Daten aus dem Handler
|
||||
if len(all_data) <= 5:
|
||||
logging.error("Fehler: Nicht genügend Datenzeilen im Sheet gefunden für Modellierung.")
|
||||
return None
|
||||
|
||||
try: # Fange Fehler beim Zugriff auf Header ab
|
||||
headers = all_data[0]
|
||||
except IndexError:
|
||||
logging.critical("FEHLER: Sheet scheint leer zu sein, keine Header gefunden.")
|
||||
return None
|
||||
data_rows = all_data[5:]
|
||||
|
||||
df = pd.DataFrame(data_rows, columns=headers)
|
||||
logging.info(f"DataFrame für Modellierung erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.")
|
||||
|
||||
# Notwendige Schlüssel aus COLUMN_MAP holen
|
||||
try:
|
||||
col_keys = {
|
||||
"name": "CRM Name", "branche": "CRM Branche", "umsatz_crm": "CRM Umsatz",
|
||||
"umsatz_wiki": "Wiki Umsatz", "ma_crm": "CRM Anzahl Mitarbeiter",
|
||||
"ma_wiki": "Wiki Mitarbeiter", "techniker": "CRM Anzahl Techniker" # ANPASSEN WENN NÖTIG
|
||||
}
|
||||
col_indices = {key: COLUMN_MAP[val] for key, val in col_keys.items()}
|
||||
# Erstelle Liste der Spaltennamen basierend auf den Headern im Sheet
|
||||
cols_to_select = [headers[idx] for idx in col_indices.values()]
|
||||
# Mapping von echtem Header zu internem Namen
|
||||
rename_map = {headers[idx]: key for key, idx in col_indices.items()}
|
||||
except KeyError as e:
|
||||
logging.critical(f"FEHLER: Konnte Mapping für Schlüssel '{e}' in COLUMN_MAP nicht finden.")
|
||||
return None
|
||||
except IndexError as e:
|
||||
logging.critical(f"FEHLER: Spaltenindex aus COLUMN_MAP ({e}) außerhalb der Grenzen der Header-Zeile ({len(headers)} Spalten). Prüfe COLUMN_MAP!")
|
||||
return None
|
||||
|
||||
try: # Fange Fehler beim Auswählen der Spalten ab
|
||||
df_subset = df[cols_to_select].copy()
|
||||
df_subset.rename(columns=rename_map, inplace=True)
|
||||
except KeyError as e:
|
||||
logging.critical(f"FEHLER beim Auswählen/Umbenennen der Spalten: {e}. Verfügbare Spalten: {list(df.columns)}")
|
||||
return None
|
||||
|
||||
logging.info(f"Benötigte Spalten für Modellierung ausgewählt und umbenannt: {list(df_subset.columns)}")
|
||||
|
||||
# --- Konsolidierung (wie in vorheriger Antwort, mit Logging) ---
|
||||
def get_valid_numeric(value_str):
|
||||
if value_str is None or pd.isna(value_str) or str(value_str).strip() == '': return np.nan
|
||||
original_value = value_str
|
||||
try:
|
||||
cleaned_str = str(value_str).replace('.', '').replace("'", "").replace(',', '.') # Auch Apostroph entfernen
|
||||
val = float(cleaned_str)
|
||||
return val if val > 0 else np.nan
|
||||
except (ValueError, TypeError):
|
||||
logging.debug(f"Konntze Wert '{original_value}' nicht direkt in Float umwandeln.")
|
||||
cleaned_str = re.sub(r'[^\d.]', '', str(value_str))
|
||||
if not cleaned_str: return np.nan
|
||||
try:
|
||||
val = float(cleaned_str)
|
||||
return val if val > 0 else np.nan
|
||||
except ValueError:
|
||||
logging.debug(f"Konntze auch bereinigten String '{cleaned_str}' aus '{original_value}' nicht umwandeln.")
|
||||
return np.nan
|
||||
|
||||
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():
|
||||
logging.info(f"Verarbeite und konsolidiere '{base_name}' (Priorität: Wiki > CRM)...")
|
||||
if wiki_col not in df_subset.columns: df_subset[wiki_col] = np.nan
|
||||
if crm_col not in df_subset.columns: df_subset[crm_col] = np.nan
|
||||
wiki_numeric = df_subset[wiki_col].apply(get_valid_numeric)
|
||||
crm_numeric = df_subset[crm_col].apply(get_valid_numeric)
|
||||
df_subset[final_col] = np.where(wiki_numeric.notna(), wiki_numeric, crm_numeric) # Vereinfacht: Wiki > CRM, sonst NaN
|
||||
logging.info(f" -> {df_subset[final_col].notna().sum()} gültige '{final_col}' Werte erstellt.")
|
||||
|
||||
# --- Zielvariable vorbereiten (wie in vorheriger Antwort, mit Logging) ---
|
||||
techniker_col = "techniker"
|
||||
logging.info(f"Verarbeite Zielvariable '{techniker_col}'...")
|
||||
df_subset['Anzahl_Servicetechniker_Numeric'] = pd.to_numeric(df_subset[techniker_col], errors='coerce')
|
||||
initial_rows = len(df_subset)
|
||||
df_filtered = df_subset[df_subset['Anzahl_Servicetechniker_Numeric'].notna() & (df_subset['Anzahl_Servicetechniker_Numeric'] > 0)].copy()
|
||||
filtered_rows = len(df_filtered)
|
||||
removed_rows = initial_rows - filtered_rows
|
||||
if removed_rows > 0: logging.info(f"{removed_rows} Zeilen entfernt (fehlende/ungültige Technikerzahl).")
|
||||
logging.info(f"Verbleibende Zeilen für Modellierung: {filtered_rows}")
|
||||
if filtered_rows == 0: logging.error("FEHLER: Keine Zeilen mit gültiger Technikerzahl übrig!"); return None
|
||||
|
||||
# --- Techniker-Buckets erstellen (wie gehabt) ---
|
||||
bins = [-1, 0, 19, 49, 99, 249, 499, float('inf')]
|
||||
labels = ['Bucket_1_(0)', 'Bucket_2_(<20)', 'Bucket_3_(<50)', 'Bucket_4_(<100)', 'Bucket_5_(<250)', 'Bucket_6_(<500)', 'Bucket_7_(>499)']
|
||||
df_filtered['Techniker_Bucket'] = pd.cut(df_filtered['Anzahl_Servicetechniker_Numeric'], bins=bins, labels=labels, right=True)
|
||||
logging.info("Techniker-Buckets erstellt.")
|
||||
logging.debug(f"Verteilung der Buckets:\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).round(3)}")
|
||||
|
||||
# --- Kategoriale Features (Branche) (wie gehabt) ---
|
||||
branche_col = "branche"
|
||||
logging.info(f"Verarbeite kategoriales Feature '{branche_col}' für One-Hot Encoding...")
|
||||
df_filtered[branche_col] = df_filtered[branche_col].astype(str).fillna('Unbekannt').str.strip()
|
||||
df_encoded = pd.get_dummies(df_filtered, columns=[branche_col], prefix='Branche', dummy_na=False)
|
||||
logging.info(f"One-Hot Encoding für '{branche_col}' durchgeführt.")
|
||||
|
||||
# --- Finale Auswahl (wie gehabt) ---
|
||||
feature_columns = [col for col in df_encoded.columns if col.startswith('Branche_')]
|
||||
feature_columns.extend(['Finaler_Umsatz', 'Finaler_Mitarbeiter'])
|
||||
target_column = 'Techniker_Bucket'
|
||||
original_data_cols = ['name', 'Anzahl_Servicetechniker_Numeric'] # 'name' statt 'CRM Name' intern
|
||||
df_model_ready = df_encoded[original_data_cols + feature_columns + [target_column]].copy()
|
||||
for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter']:
|
||||
df_model_ready[col] = pd.to_numeric(df_model_ready[col], errors='coerce')
|
||||
df_model_ready = df_model_ready.reset_index(drop=True)
|
||||
|
||||
logging.info("Datenvorbereitung für Modellierung abgeschlossen.")
|
||||
nan_counts = df_model_ready[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].isna().sum()
|
||||
logging.info(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}")
|
||||
|
||||
return df_model_ready
|
||||
|
||||
|
||||
# ==================== MAIN FUNCTION ====================
|
||||
# ==================== MAIN FUNCTION ====================
|
||||
|
||||
Reference in New Issue
Block a user