bugfix
This commit is contained in:
@@ -252,7 +252,7 @@ decorator_logger = logging.getLogger(__name__ + ".Retry")
|
||||
|
||||
|
||||
# --- Retry Decorator ---
|
||||
# KORRIGIERTE Version (Behandelt SpreadsheetNotFound und 404 HTTPError explizit)
|
||||
# KORRIGIERTE Version (Behandelt SpreadsheetNotFound und 404/400 HTTPError explizit)
|
||||
def retry_on_failure(func):
|
||||
"""
|
||||
Decorator, der eine Funktion bei bestimmten Fehlern mehrmals wiederholt.
|
||||
@@ -292,8 +292,7 @@ def retry_on_failure(func):
|
||||
|
||||
return func(*args, **kwargs) # Call the original function
|
||||
|
||||
# Spezifische Exceptions, die ein Retry rechtfertigen
|
||||
# Fangen Sie nicht-wiederholbare Fehler separat
|
||||
# Spezifische Exceptions, die ein Retry nicht rechtfertigen (permanente Fehler)
|
||||
except (gspread.exceptions.SpreadsheetNotFound, openai.error.AuthenticationError, ValueError) as e:
|
||||
# Diese Fehler deuten auf ein permanentes Problem hin (falsche URL, falscher Key, falsche Eingabe)
|
||||
decorator_logger.critical(f"❌ ENDGÜLTIGER FEHLER bei '{effective_func_name}': Permanentes Problem erkannt. {type(e).__name__} - {str(e)[:150]}...")
|
||||
@@ -302,9 +301,12 @@ def retry_on_failure(func):
|
||||
|
||||
# Fangen Sie Requests HTTP Errors (wie 404)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response is not None:
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
status_code = e.response.status_code
|
||||
if status_code in [404, 400]: # Fügen Sie hier weitere Status-Codes hinzu, die NICHT wiederholt werden sollen
|
||||
# Definieren Sie hier eine Liste von Status-Codes, die NICHT wiederholt werden sollen
|
||||
non_retryable_status_codes = [404, 400, 401, 403] # Not Found, Bad Request, Unauthorized, Forbidden
|
||||
|
||||
if status_code in non_retryable_status_codes:
|
||||
decorator_logger.critical(f"❌ ENDGÜLTIGER FEHLER bei '{effective_func_name}': HTTP Fehler {status_code} erhalten ({e.response.reason}). Nicht wiederholbar. {str(e)[:150]}...")
|
||||
decorator_logger.exception("Details:") # Log traceback
|
||||
raise e # Leiten Sie diese nicht-wiederholbare Exception sofort weiter
|
||||
@@ -338,8 +340,6 @@ def retry_on_failure(func):
|
||||
time.sleep(wait_time) # Warte vor dem nächsten Versuch
|
||||
else: # Letzter Versuch fehlgeschlagen
|
||||
decorator_logger.error(f"❌ ENDGÜLTIGER FEHLER bei '{effective_func_name}' nach {max_retries_config} Versuchen.")
|
||||
# Log traceback für erwartete Fehler beim endgültigen Scheitern
|
||||
# decorator_logger.exception("Details zum endgültigen Fehler:") # Kann zu viel Lärm machen
|
||||
raise e # Leite die ursprüngliche Exception weiter
|
||||
|
||||
except Exception as e:
|
||||
@@ -351,8 +351,6 @@ def retry_on_failure(func):
|
||||
|
||||
# Dieser Teil sollte theoretisch nicht erreicht werden, wenn max_retries_config > 0
|
||||
# und eine Exception immer zu einer raise e Anweisung führt.
|
||||
# Wenn die Schleife endet, ohne zurückzukehren oder eine Exception zu werfen,
|
||||
# deutet dies auf einen Logikfehler im Decorator hin.
|
||||
raise RuntimeError(f"Retry decorator logic error: Loop completed unexpectedly for {effective_func_name}. This should not happen.")
|
||||
|
||||
|
||||
@@ -815,6 +813,7 @@ def load_target_schema(csv_filepath=BRANCH_MAPPING_FILE):
|
||||
TARGET_SCHEMA_STRING = "Ziel-Branchenschema nicht verfügbar (Keine gültigen Branchen in Datei gefunden)."
|
||||
logger.warning("Keine gültigen Zielbranchen im Schema gefunden. Branchenbewertung ist nicht möglich.")
|
||||
|
||||
|
||||
# map_external_branch ist in dieser Struktur nicht mehr notwendig,
|
||||
# da die Branchenevaluation über ChatGPT (evaluate_branche_chatgpt)
|
||||
# direkt gegen ALLOWED_TARGET_BRANCHES validiert.
|
||||
@@ -1490,9 +1489,10 @@ def search_linkedin_contacts(company_name, website, position_query, crm_kurzform
|
||||
|
||||
|
||||
# --- Experimentelle Website Details Scraping Funktion ---
|
||||
# Diese Funktion wurde in DataProcessor.process_website_details aufgerufen
|
||||
# Diese Funktion wurde in DataProcessor.process_website_details aufgerufen.
|
||||
# Sie ist hier global platziert, da sie nicht spezifisch von DataProcessor state abhängt,
|
||||
# sondern nur von globalen Helfern und Requests.
|
||||
# Ihre Implementierung hängt stark von der Struktur der Zielwebsites ab.
|
||||
# HIER ist ein Platzhalter für diese Funktion. Sie MUSS implementiert werden.
|
||||
def scrape_website_details(url):
|
||||
"""
|
||||
EXPERIMENTELL: Scrapt eine Website und extrahiert spezifische Details.
|
||||
@@ -1504,14 +1504,10 @@ def scrape_website_details(url):
|
||||
Returns:
|
||||
str: Extrahierte Details als String oder Fehler/k.A.
|
||||
"""
|
||||
logger.warning(f"Platzhalter Funktion 'scrape_website_details' für URL {url} aufgerufen.")
|
||||
logger.warning(f"Ausführe 'scrape_website_details' für URL {url}.")
|
||||
# Beispiel: Einfaches Abrufen des <title> Tags
|
||||
try:
|
||||
# Nutzt get_page_soup aus der WikipediaScraper Klasse, aber das ist eine Methode.
|
||||
# Wir brauchen hier eine separate Funktion oder eine Instanz.
|
||||
# Oder wir verschieben diese Logik in die DataProcessor Klasse, die den Scraper hat.
|
||||
# Am besten: Lassen Sie diese Funktion global, aber nutzen Sie requests direkt oder eine Hilfsfunktion.
|
||||
# Nutzen wir requests direkt mit retry_on_failure
|
||||
# Hilfsfunktion zum Abrufen des Soup-Objekts mit Retry
|
||||
@retry_on_failure
|
||||
def get_soup_for_details(target_url):
|
||||
response = requests.get(target_url, timeout=getattr(Config, 'REQUEST_TIMEOUT', 15))
|
||||
@@ -1527,6 +1523,7 @@ def scrape_website_details(url):
|
||||
h1 = soup.find('h1')
|
||||
|
||||
details_list = []
|
||||
# clean_text nutzt globale Funktion
|
||||
if title: details_list.append(f"Title: {clean_text(title.get_text())}")
|
||||
if meta_desc and meta_desc.get('content'): details_list.append(f"Description: {clean_text(meta_desc['content'])}")
|
||||
if h1: details_list.append(f"H1: {clean_text(h1.get_text())}")
|
||||
@@ -1537,13 +1534,144 @@ def scrape_website_details(url):
|
||||
return "k.A. (Keine Standard-Details gefunden)"
|
||||
|
||||
else:
|
||||
return "k.A. (Scraping fehlgeschlagen)" # Fehler wurde bereits geloggt
|
||||
# Fehler wurde bereits in get_soup_for_details oder retry geloggt
|
||||
return "k.A. (Scraping fehlgeschlagen)"
|
||||
|
||||
except Exception as e: # retry_on_failure wirft am Ende Exception
|
||||
# Dieser Fehler wird bereits vom retry_on_failure geloggt
|
||||
logger.error(f"FEHLER in scrape_website_details für {url}: {e}")
|
||||
return f"FEHLER: {str(e)[:100]}" # Rückgabe der Fehlermeldung
|
||||
|
||||
|
||||
# --- Globale Funktion zum Scrapen des Website Rohtextes ---
|
||||
# Übernommen aus get_website_raw in Teil 7. Global platziert.
|
||||
@retry_on_failure
|
||||
def get_website_raw(url, max_length=20000, verify_cert=True): # Längeres Default Limit, SSL-Zertifikat standardmäßig prüfen
|
||||
"""
|
||||
Holt Textinhalt von einer Website, versucht Cookie-Banner zu umgehen.
|
||||
Gibt den Rohtext zurück oder einen Fehlerwert ("k.A.", "k.A. (Fehler)", etc.).
|
||||
"""
|
||||
if not url or not isinstance(url, str) or url.strip().lower() in ["k.a.", "kein artikel gefunden", "fehler bei suche"]:
|
||||
logger.debug(f"get_website_raw skipped: Ungültige oder leere URL '{url}'.")
|
||||
return "k.A."
|
||||
|
||||
# Falls kein Schema vorhanden ist, hinzufügen (HTTPS bevorzugen)
|
||||
if not url.lower().startswith(("http://", "https://")):
|
||||
#logger.debug(f"Kein Schema in URL '{url}', füge https:// hinzu.") # Zu viel Lärm
|
||||
url = "https://" + url
|
||||
|
||||
# Verwenden Sie eine Requests Session oder requests direkt.
|
||||
# Eine Session in DataProcessor könnte besser sein, aber globale Funktion nutzt requests direkt.
|
||||
headers = {
|
||||
"User-Agent": getattr(Config, 'USER_AGENT', 'Mozilla/5.0 (compatible; UnternehmenSkript/1.0; +http://www.example.com/bot)') # Nutzt Config oder Fallback
|
||||
}
|
||||
|
||||
try:
|
||||
# Der Requests Call wird vom retry_on_failure Decorator behandelt.
|
||||
# Timeout sollte aus Config kommen.
|
||||
response = requests.get(url, timeout=getattr(Config, 'REQUEST_TIMEOUT', 15), headers=headers, verify=verify_cert)
|
||||
response.raise_for_status() # Wirft HTTPError für 4xx/5xx Antworten. Wird vom Decorator gefangen.
|
||||
|
||||
# Versuche, das Encoding aus dem Header oder dem Content zu erraten
|
||||
response.encoding = response.apparent_encoding
|
||||
|
||||
soup = BeautifulSoup(response.text, getattr(Config, 'HTML_PARSER', 'html.parser')) # Nutzt Config oder Fallback
|
||||
|
||||
# --- Versuch 1: Hauptinhalt-Tags finden ---
|
||||
# Verwenden Sie eine Liste von Selektoren
|
||||
content_selectors = [
|
||||
'main', 'article', '#content', '#main-content', '.main-content', '.content',
|
||||
'div[role="main"]', 'div.page-content', 'div.container' # Weitere gängige Selektoren
|
||||
]
|
||||
content_area = None
|
||||
for selector in content_selectors:
|
||||
content_area = soup.select_one(selector)
|
||||
if content_area:
|
||||
#logger.debug(f"Gezielten Inhaltsbereich gefunden mit Selektor '{selector}' für {url}.") # Zu viel Lärm
|
||||
break # Ersten gefundenen Bereich nehmen
|
||||
|
||||
if not content_area:
|
||||
# --- Fallback: Body nehmen, ABER Banner versuchen zu entfernen ---
|
||||
#logger.debug(f"Kein spezifischer Inhaltsbereich gefunden für {url}. Nutze Body und versuche Banner zu entfernen.") # Zu viel Lärm
|
||||
content_area = soup.find('body')
|
||||
|
||||
if content_area:
|
||||
# Versuche, häufige Cookie-Banner Strukturen zu entfernen
|
||||
# Diese Selektoren sollten angepasst werden, wenn spezifische Banner Probleme machen
|
||||
banner_selectors = [
|
||||
'[id*="cookie"]', '[class*="cookie"]', '[id*="consent"]', '[class*="consent"]',
|
||||
'.cookie-banner', '.consent-banner', '.modal', '#modal', '.popup', '#popup',
|
||||
'[role="dialog"]', '[aria-modal="true"]'
|
||||
]
|
||||
banners_removed_count = 0
|
||||
# Gehe rückwärts durch die gefundenen Elemente, um Decompose sicher zu machen
|
||||
for selector in banner_selectors:
|
||||
try:
|
||||
# select findet alle passenden Elemente
|
||||
potential_banners = content_area.select(selector)
|
||||
for banner in potential_banners:
|
||||
# Zusätzliche Prüfung: Enthält das Element typischen Banner-Text?
|
||||
# Vermeiden Sie das Entfernen von echtem Inhalt, der zufällig das Wort "cookie" enthält.
|
||||
banner_text = banner.get_text(" ", strip=True).lower()
|
||||
keywords = ["cookie", "zustimm", "ablehnen", "einverstanden", "datenschutz", "privacy", "akzeptier", "einstellung", "partner", "analyse", "marketing"]
|
||||
# Prüfe, ob ein Keyword im Text ODER im class/id Namen vorkommt
|
||||
if any(keyword in banner_text for keyword in keywords) or any(keyword in (banner.get('id', '') + banner.get('class', '')).lower() for keyword in keywords):
|
||||
#logger.debug(f"Entferne potenzielles Banner ({selector}) mit Text: {banner_text[:100]}...") # Zu viel Lärm
|
||||
banner.decompose() # Entferne das Element aus dem Baum
|
||||
banners_removed_count += 1
|
||||
except Exception as e_select:
|
||||
# Logge Fehler bei der Banner-Entfernung, aber fahre fort
|
||||
logger.debug(f"Fehler beim Versuch Banner mit Selektor '{selector}' zu entfernen: {e_select}")
|
||||
if banners_removed_count > 0:
|
||||
logger.debug(f"{banners_removed_count} potenzielle Banner-Elemente für {url} entfernt.")
|
||||
|
||||
# --- Text extrahieren aus gefundenem Bereich (oder Body) ---
|
||||
if content_area:
|
||||
# Entferne Skripte und Styles, bevor der Text extrahiert wird
|
||||
for script_or_style in content_area(["script", "style"]):
|
||||
script_or_style.decompose()
|
||||
|
||||
# Extrahiere Text mit Leerzeichen als Trenner
|
||||
text = content_area.get_text(separator=' ', strip=True)
|
||||
text = re.sub(r'\s+', ' ', text).strip() # Normalisiere und trimme Whitespace
|
||||
|
||||
# --- Zusätzliche Prüfung: Ist der extrahierte Text *nur* Banner-Text? ---
|
||||
# Diese Heuristik ist eine Fallback-Maßnahme, wenn die Decompose-Logik nicht perfekt war.
|
||||
banner_keywords_strict = ["cookie", "zustimmen", "ablehnen", "einverstanden", "datenschutz", "privacy", "akzeptier", "einstellung", "partner", "analyse", "marketing"]
|
||||
text_lower = text.lower()
|
||||
keyword_hits = sum(1 for keyword in banner_keywords_strict if keyword in text_lower)
|
||||
|
||||
# Heuristik: Wenn der Text kurz ist UND viele Banner-Keywords enthält -> Verwerfen
|
||||
# Passen Sie die Schwellenwerte an
|
||||
if len(text) < 500 and keyword_hits >= 3: # Wenn Text kürzer als 500 Zeichen und >= 3 Keywords
|
||||
logger.warning(f"WARNUNG: Extrahierter Text für {url} scheint nur Cookie-Banner zu sein (Länge {len(text)}, {keyword_hits} Keywords). Verwerfe Text.")
|
||||
return "k.A. (Nur Cookie-Banner erkannt)"
|
||||
|
||||
# Wenn der Text nach Bereinigung immer noch sehr kurz ist (z.B. nur ein paar Worte)
|
||||
if len(text.split()) < 10 or len(text) < 50:
|
||||
#logger.debug(f"Extrahierter Text für {url} ist sehr kurz ({len(text.split())} Worte, {len(text)} Zeichen).") # Zu viel Lärm
|
||||
# Kann immer noch valide sein, aber ist oft kein relevanter Inhalt.
|
||||
# Geben wir ihn trotzdem zurück, gekürzt.
|
||||
|
||||
pass # Behalte den Text, keine weitere Filterung
|
||||
|
||||
# Begrenzen Sie die Länge des zurückgegebenen Rohtextes
|
||||
result = text[:max_length]
|
||||
logger.debug(f"Website {url} erfolgreich gescrapt. Extrahierter Text (Länge {len(result)}).")
|
||||
# logger.debug(f"Extrahierter Text Anfang: {result[:100]}...") # Zu viel Lärm
|
||||
return result if result else "k.A. (Extraktion leer)" # Rückgabe des gekürzten Textes
|
||||
|
||||
else:
|
||||
logger.warning(f"Kein <body> oder spezifischer Inhaltsbereich gefunden in {url}.")
|
||||
return "k.A. (Kein Body gefunden)"
|
||||
|
||||
# Exceptions (wie RequestsErrors) werden vom retry_on_failure Decorator behandelt.
|
||||
# Wenn eine Exception hier durchkommt, hat der Decorator aufgegeben.
|
||||
except Exception as e: # Fangen Sie alle verbleibenden Exceptions, die nicht vom Decorator behandelt wurden
|
||||
logger.error(f"Allgemeiner Fehler beim Scraping von {url}: {type(e).__name__} - {e}")
|
||||
# Die Exception wurde bereits vom Decorator geloggt
|
||||
return f"k.A. (Fehler: {str(e)[:100]}...)" # Signalisiert Fehler
|
||||
|
||||
# TODO: Weitere globale Helferfunktionen (z.B. für FSM, Emp, Umsatz Schätzung Prompts und Parsing) müssen hier implementiert werden,
|
||||
# falls sie nicht in den DataProcessor integriert wurden. Platzhalter wurden in DataProcessor._process_single_row hinzugefügt.
|
||||
|
||||
@@ -3659,7 +3787,7 @@ class DataProcessor:
|
||||
# process_contact_search method... (kommt in Teil 13)
|
||||
# process_wiki_updates_from_chatgpt method... (kommt in Teil 14)
|
||||
# process_wiki_reextract_missing_an method... (kommt in Teil 14)
|
||||
# ==========================================================================
|
||||
# ==========================================================================
|
||||
# === Batch Processing Methods =============================================
|
||||
# ==========================================================================
|
||||
|
||||
@@ -4133,22 +4261,22 @@ class DataProcessor:
|
||||
|
||||
# --- Worker-Funktion für Scraping ---
|
||||
# Diese Funktion läuft in einem separaten Thread
|
||||
def scrape_raw_text_task(task_info):
|
||||
row_num = task_info['row_num']
|
||||
url = task_info['url']
|
||||
raw_text = "k.A."
|
||||
error = None
|
||||
try:
|
||||
# Nutzt die globale Funktion get_website_raw mit Retry Decorator
|
||||
raw_text = get_website_raw(url) # Annahme: get_website_raw in utils.py
|
||||
except Exception as e:
|
||||
# Fängt Fehler beim Scraping, damit der Thread nicht abstürzt
|
||||
error = f"Scraping Fehler Zeile {row_num} ({url}): {e}"
|
||||
self.logger.error(error)
|
||||
raw_text = "k.A. (Fehler)" # Setze einen Fehlerwert in den Rohtext
|
||||
# def scrape_raw_text_task(task_info):
|
||||
# row_num = task_info['row_num']
|
||||
# url = task_info['url']
|
||||
# raw_text = "k.A."
|
||||
# error = None
|
||||
# try:
|
||||
# # Nutzt die globale Funktion get_website_raw mit Retry Decorator
|
||||
# raw_text = get_website_raw(url) # Annahme: get_website_raw in utils.py
|
||||
# except Exception as e:
|
||||
# # Fängt Fehler beim Scraping, damit der Thread nicht abstürzt
|
||||
# error = f"Scraping Fehler Zeile {row_num} ({url}): {e}"
|
||||
# self.logger.error(error)
|
||||
# raw_text = "k.A. (Fehler)" # Setze einen Fehlerwert in den Rohtext
|
||||
|
||||
#logger.debug(f"Scraping Task Zeile {row_num} abgeschlossen. Textlänge: {len(str(raw_text))}.") # Zu viel Lärm
|
||||
return {"row_num": row_num, "raw_text": raw_text, "error": error}
|
||||
# return {"row_num": row_num, "raw_text": raw_text, "error": error}
|
||||
|
||||
|
||||
# --- Hauptlogik: Iteriere und sammle Batches ---
|
||||
@@ -4226,7 +4354,7 @@ class DataProcessor:
|
||||
# Nutzt concurrent.futures für paralleles Scraping
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=max_scraping_workers) as executor:
|
||||
# Map tasks to futures
|
||||
future_to_task = {executor.submit(scrape_raw_text_task, task): task for task in tasks_for_processing_batch}
|
||||
future_to_task = {executor.submit(_scrape_raw_text_task_global, task): task for task in tasks_for_processing_batch} # Auf globalen Namen geändert
|
||||
|
||||
# Process results as they complete
|
||||
for future in concurrent.futures.as_completed(future_to_task):
|
||||
@@ -5493,7 +5621,7 @@ class DataProcessor:
|
||||
# process_website_details method... (kommt in Teil 16) # Optional/Experimentell
|
||||
# process_wiki_updates_from_chatgpt method... (kommt in Teil 16)
|
||||
# process_wiki_reextract_missing_an method... (kommt in Teil 16)
|
||||
# ==========================================================================
|
||||
# ==========================================================================
|
||||
# === Utility Methods (ML Data Prep & Training) ============================
|
||||
# ==========================================================================
|
||||
|
||||
|
||||
Reference in New Issue
Block a user