bugfix
This commit is contained in:
@@ -2079,151 +2079,155 @@ def search_linkedin_contacts(company_name, website, position_query, crm_kurzform
|
|||||||
# Basierend auf get_website_raw aus Teil 7. Global platziert.
|
# Basierend auf get_website_raw aus Teil 7. Global platziert.
|
||||||
# Nutzt globale Helfer: simple_normalize_url, clean_text, re, requests, BeautifulSoup, Config, getattr, logger, retry_on_failure.
|
# Nutzt globale Helfer: simple_normalize_url, clean_text, re, requests, BeautifulSoup, Config, getattr, logger, retry_on_failure.
|
||||||
@retry_on_failure # Wende den Decorator auf diese Funktion an
|
@retry_on_failure # Wende den Decorator auf diese Funktion an
|
||||||
def get_website_raw(url, max_length=20000, verify_cert=True): # Längeres Default Limit, SSL-Zertifikat standardmaessig pruefen
|
def get_website_raw(url, max_length=20000, verify_cert=True): # verify_cert Default bleibt True
|
||||||
"""
|
"""
|
||||||
Holt Textinhalt von einer Website, versucht Cookie-Banner zu umgehen.
|
Holt Textinhalt von einer Website, versucht Cookie-Banner zu umgehen.
|
||||||
Gibt den Rohtext zurück oder einen Fehlerwert ("k.A.", "k.A. (Fehler)", etc.).
|
Implementiert SSL-Fallback und gibt spezifischere Fehlerwerte zurueck.
|
||||||
"""
|
"""
|
||||||
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
|
logger = logging.getLogger(__name__)
|
||||||
logger = logging.getLogger(__name__) # <<< DIESE ZEILE HINZUFÜGEN
|
|
||||||
# Pruefen Sie auf ungueltige oder leere URLs, inklusive spezifischer Fehlerwerte wie "http:"
|
|
||||||
if not url or not isinstance(url, str) or url.strip().lower() in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"]:
|
if not url or not isinstance(url, str) or url.strip().lower() in ["k.a.", "kein artikel gefunden", "fehler bei suche", "http:"]:
|
||||||
logger.debug(f"get_website_raw skipped: Ungueltige oder leere URL '{url}'.")
|
logger.debug(f"get_website_raw skipped: Ungueltige oder leere URL '{url}'.")
|
||||||
return "k.A." # Gebe "k.A." zurueck bei ungueltigen Eingaben
|
return "k.A."
|
||||||
|
|
||||||
# Falls kein Schema vorhanden ist, fuegen Sie HTTPS als Standard hinzu
|
|
||||||
if not url.lower().startswith(("http://", "https://")):
|
if not url.lower().startswith(("http://", "https://")):
|
||||||
#logger.debug(f"Kein Schema in URL '{url}', fuege https:// hinzu.") # Zu viel Laerm im Debug
|
|
||||||
url = "https://" + url
|
url = "https://" + url
|
||||||
|
|
||||||
# Verwenden Sie eine Requests Session oder requests direkt.
|
|
||||||
# Eine Session in DataProcessor koennte besser sein, aber globale Funktion nutzt requests direkt.
|
|
||||||
headers = {
|
headers = {
|
||||||
# Nutzt den User-Agent aus Config oder einen Fallback
|
|
||||||
"User-Agent": getattr(Config, 'USER_AGENT', 'Mozilla/5.0 (compatible; UnternehmenSkript/1.0; +https://www.example.com/bot)')
|
"User-Agent": getattr(Config, 'USER_AGENT', 'Mozilla/5.0 (compatible; UnternehmenSkript/1.0; +https://www.example.com/bot)')
|
||||||
|
# TODO (Optional): User-Agent Rotation hier implementieren
|
||||||
}
|
}
|
||||||
|
# --- ANPASSUNG START: SSL Fallback & Spezifische Fehler ---
|
||||||
|
response = None
|
||||||
|
error_reason = "Unbekannter Fehler" # Default
|
||||||
|
|
||||||
|
for ssl_verify_attempt in [True, False]: # Erst mit True, dann mit False versuchen
|
||||||
|
if not ssl_verify_attempt and not verify_cert: # Wenn verify_cert schon False war, nicht nochmal versuchen
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
current_verify_setting = verify_cert if ssl_verify_attempt else False
|
||||||
|
if not ssl_verify_attempt:
|
||||||
|
logger.warning(f"SSL-Fehler bei verify=True. Versuche erneut mit verify=False fuer {url[:100]}...")
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
url,
|
||||||
|
timeout=getattr(Config, 'REQUEST_TIMEOUT', 30), # Timeout aus Config (erhöht auf 30s als Default)
|
||||||
|
headers=headers,
|
||||||
|
verify=current_verify_setting, # Aktuelle Einstellung verwenden
|
||||||
|
allow_redirects=True, # Redirects folgen
|
||||||
|
stream=False # Stream deaktivieren, da wir gesamten Inhalt brauchen
|
||||||
|
)
|
||||||
|
response.raise_for_status() # Wirft HTTPError fuer 4xx/5xx
|
||||||
|
error_reason = None # Kein Fehler, wenn bis hierhin erfolgreich
|
||||||
|
break # Erfolgreicher Request, Schleife verlassen
|
||||||
|
|
||||||
|
except requests.exceptions.SSLError as e_ssl:
|
||||||
|
error_reason = f"SSL Fehler: {str(e_ssl)[:100]}..."
|
||||||
|
logger.warning(f"SSL Fehler bei verify={current_verify_setting} fuer {url[:100]}...: {e_ssl}")
|
||||||
|
if ssl_verify_attempt: # Wenn es der erste Versuch (verify=True) war
|
||||||
|
verify_cert = False # Setze Flag für nächsten Versuch auf False
|
||||||
|
continue # Mache nächsten Versuch mit verify=False
|
||||||
|
else: # Wenn auch verify=False fehlschlägt
|
||||||
|
logger.error(f"Endgueltiger SSL Fehler auch bei verify=False fuer {url[:100]}...")
|
||||||
|
break # Beende Versuche nach SSL Fehler mit verify=False
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout as e_timeout:
|
||||||
|
error_reason = f"Timeout ({getattr(Config, 'REQUEST_TIMEOUT', 30)}s)"
|
||||||
|
logger.warning(f"{error_reason} fuer {url[:100]}...")
|
||||||
|
break # Timeout ist endgültig für diesen Call (Decorator macht Retries)
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError as e_conn:
|
||||||
|
error_reason = f"Connection Error: {str(e_conn)[:100]}..."
|
||||||
|
logger.warning(f"{error_reason} fuer {url[:100]}...")
|
||||||
|
break # Connection Error ist endgültig für diesen Call
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e_http:
|
||||||
|
status_code = e_http.response.status_code
|
||||||
|
error_reason = f"HTTP Error {status_code} ({e_http.response.reason})"
|
||||||
|
logger.warning(f"{error_reason} fuer {url[:100]}...")
|
||||||
|
# Non-retryable HTTP errors werden bereits im Decorator behandelt
|
||||||
|
break # HTTP Error ist endgültig für diesen Call
|
||||||
|
|
||||||
|
except Exception as e_gen:
|
||||||
|
error_reason = f"Allg. Fehler: {type(e_gen).__name__} - {str(e_gen)[:100]}..."
|
||||||
|
logger.error(f"Allgemeiner Fehler beim Abrufen von {url[:100]}...: {e_gen}")
|
||||||
|
logger.debug(traceback.format_exc())
|
||||||
|
break # Allgemeiner Fehler ist endgültig für diesen Call
|
||||||
|
|
||||||
|
# --- ANPASSUNG ENDE ---
|
||||||
|
|
||||||
|
# Wenn nach allen Versuchen keine gueltige Response erhalten wurde
|
||||||
|
if response is None or error_reason:
|
||||||
|
# Gebe spezifischen Fehlerwert zurueck
|
||||||
|
return f"k.A. ({error_reason})"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Ab hier: Verarbeitung der erfolgreichen Response ---
|
||||||
try:
|
try:
|
||||||
# Fuehren Sie die GET-Anfrage aus. Der retry_on_failure Decorator behandelt RequestsExceptions.
|
|
||||||
# Timeout sollte aus Config kommen. verify=verify_cert steuert die SSL-Pruefung.
|
|
||||||
response = requests.get(url, timeout=getattr(Config, 'REQUEST_TIMEOUT', 15), headers=headers, verify=verify_cert)
|
|
||||||
# Wirft HTTPError fuer 4xx/5xx Antworten. Wird vom Decorator gefangen und (je nach Status) wiederholt oder als endgueltiger Fehler gemeldet.
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
# Versuchen Sie, das Encoding aus dem Header oder dem Content zu erraten
|
|
||||||
response.encoding = response.apparent_encoding
|
response.encoding = response.apparent_encoding
|
||||||
|
|
||||||
# Parsen Sie den HTML-Inhalt mit BeautifulSoup
|
|
||||||
# Nutzt den konfigurierten Parser aus Config oder einen Fallback
|
|
||||||
soup = BeautifulSoup(response.text, getattr(Config, 'HTML_PARSER', 'html.parser'))
|
soup = BeautifulSoup(response.text, getattr(Config, 'HTML_PARSER', 'html.parser'))
|
||||||
|
|
||||||
# --- Versuch 1: Hauptinhalt-Tags finden ---
|
|
||||||
# Verwenden Sie eine Liste von gaengigen Selektoren fuer den Hauptinhaltbereich.
|
|
||||||
content_selectors = [
|
content_selectors = [
|
||||||
'main', 'article', '#content', '#main-content', '.main-content', '.content',
|
'main', 'article', '#content', '#main-content', '.main-content', '.content',
|
||||||
'div[role="main"]', 'div.page-content', 'div.container' # Weitere gaengige Selektoren
|
'div[role="main"]', 'div.page-content', 'div.container'
|
||||||
]
|
]
|
||||||
content_area = None # Initialisieren mit None
|
content_area = None
|
||||||
for selector in content_selectors:
|
for selector in content_selectors:
|
||||||
content_area = soup.select_one(selector) # Versuche, das erste Element zu finden
|
content_area = soup.select_one(selector)
|
||||||
if content_area:
|
if content_area: break
|
||||||
# logger.debug(f"Gezielten Inhaltsbereich gefunden mit Selektor '{selector}' fuer {url[:100]}...") # Logge den gefundenen Selektor (gekuerzt)
|
|
||||||
break # Hoere auf, sobald ein Bereich gefunden wurde
|
|
||||||
|
|
||||||
# Wenn kein spezifischer Inhaltsbereich gefunden wurde
|
|
||||||
if not content_area:
|
if not content_area:
|
||||||
# --- Fallback: Body nehmen, ABER Banner versuchen zu entfernen ---
|
content_area = soup.find('body')
|
||||||
# logger.debug(f"Kein spezifischer Inhaltsbereich gefunden fuer {url[:100]}... Nutze Body und versuche Banner zu entfernen.") # Logge den Fallback
|
|
||||||
content_area = soup.find('body') # Versuche, den Body-Tag zu finden
|
|
||||||
|
|
||||||
# Wenn der Body-Tag gefunden wurde
|
|
||||||
if content_area:
|
if content_area:
|
||||||
# Versuche, haeufige Cookie-Banner Strukturen zu entfernen
|
|
||||||
# Diese Selektoren sollten angepasst werden, wenn spezifische Banner Probleme machen
|
|
||||||
banner_selectors = [
|
banner_selectors = [
|
||||||
'[id*="cookie"]', '[class*="cookie"]', '[id*="consent"]', '[class*="consent"]',
|
'[id*="cookie"]', '[class*="cookie"]', '[id*="consent"]', '[class*="consent"]',
|
||||||
'.cookie-banner', '.consent-banner', '.modal', '#modal', '.popup', '#popup',
|
'.cookie-banner', '.consent-banner', '.modal', '#modal', '.popup', '#popup',
|
||||||
'[role="dialog"]', '[aria-modal="true"]' # Gaengige Rollen/Attribute fuer Dialoge
|
'[role="dialog"]', '[aria-modal="true"]'
|
||||||
]
|
]
|
||||||
banners_removed_count = 0
|
banners_removed_count = 0
|
||||||
# Gehe durch die gefundenen Elemente und versuche, Banner zu identifizieren und zu entfernen
|
|
||||||
for selector in banner_selectors:
|
for selector in banner_selectors:
|
||||||
try:
|
try:
|
||||||
# select findet alle passenden Elemente fuer den aktuellen Selektor
|
|
||||||
potential_banners = content_area.select(selector)
|
potential_banners = content_area.select(selector)
|
||||||
for banner in potential_banners:
|
for banner in potential_banners:
|
||||||
# Zusaetzliche Pruefung: Enthaehlt das Element typischen Banner-Text ODER relevante Klassen/IDs?
|
banner_text = banner.get_text(" ", strip=True).lower()
|
||||||
# Vermeiden Sie das Entfernen von echtem Inhalt, der zufaellig Banner-Keywords enthaelt.
|
keywords = ["cookie", "zustimm", "ablehnen", "einverstanden", "datenschutz", "privacy", "akzeptier", "einstellung", "partner", "analyse", "marketing"]
|
||||||
banner_text = banner.get_text(" ", strip=True).lower() # Text des Elements
|
element_id_class = (banner.get('id', '') + ' ' + ' '.join(banner.get('class', []))).lower()
|
||||||
keywords = ["cookie", "zustimm", "ablehnen", "einverstanden", "datenschutz", "privacy", "akzeptier", "einstellung", "partner", "analyse", "marketing"] # Gaengige Keywords
|
|
||||||
element_id_class = (banner.get('id', '') + ' ' + ' '.join(banner.get('class', []))).lower() # ID und Klassen des Elements
|
|
||||||
|
|
||||||
# Pruefe, ob ein Keyword im Text ODER in ID/Klassen vorkommt
|
|
||||||
if any(keyword in banner_text for keyword in keywords) or any(keyword in element_id_class for keyword in keywords):
|
if any(keyword in banner_text for keyword in keywords) or any(keyword in element_id_class for keyword in keywords):
|
||||||
# logger.debug(f"Entferne potenzielles Banner ({selector}) mit Text: {banner_text[:100]}... oder ID/Class: {element_id_class[:100]}...") # Logge Entfernung (gekuerzt)
|
banner.decompose()
|
||||||
banner.decompose() # Entferne das Element aus dem BeautifulSoup-Baum
|
|
||||||
banners_removed_count += 1
|
banners_removed_count += 1
|
||||||
except Exception as e_select:
|
except Exception as e_select:
|
||||||
# Logge Fehler bei der Banner-Entfernung auf Debug-Level
|
|
||||||
logger.debug(f"Fehler beim Versuch Banner mit Selektor '{selector}' zu entfernen: {e_select}")
|
logger.debug(f"Fehler beim Versuch Banner mit Selektor '{selector}' zu entfernen: {e_select}")
|
||||||
if banners_removed_count > 0:
|
if banners_removed_count > 0:
|
||||||
logger.debug(f"{banners_removed_count} potenzielle Banner-Elemente fuer {url[:100]}... entfernt.")
|
logger.debug(f"{banners_removed_count} potenzielle Banner-Elemente fuer {url[:100]}... entfernt.")
|
||||||
|
|
||||||
|
|
||||||
# --- Text extrahieren aus gefundenem Bereich (oder Body) ---
|
|
||||||
# Wenn ein Inhaltsbereich (oder der Body) gefunden wurde
|
|
||||||
if content_area:
|
if content_area:
|
||||||
# Entferne Skripte und Styles, bevor der Text extrahiert wird
|
|
||||||
for script_or_style in content_area(["script", "style"]):
|
for script_or_style in content_area(["script", "style"]):
|
||||||
script_or_style.decompose()
|
script_or_style.decompose()
|
||||||
|
|
||||||
# Extrahiere Text mit Leerzeichen als Trenner und bereinige Whitespace
|
|
||||||
text = content_area.get_text(separator=' ', strip=True)
|
text = content_area.get_text(separator=' ', strip=True)
|
||||||
text = re.sub(r'\s+', ' ', text).strip() # Reduziere multiple Leerzeichen und trimme Enden
|
text = re.sub(r'\s+', ' ', text).strip()
|
||||||
|
|
||||||
|
banner_keywords_strict = ["cookie", "zustimmen", "ablehnen", "einverstanden", "datenschutz", "privacy", "akzeptier", "einstellung", "partner", "analyse", "marketing"]
|
||||||
# --- Zusaetzliche Pruefung: Ist der extrahierte Text *nur* Banner-Text? ---
|
|
||||||
# Diese Heuristik ist eine Fallback-Massnahme, wenn die Decompose-Logik nicht perfekt war.
|
|
||||||
banner_keywords_strict = ["cookie", "zustimmen", "ablehnen", "einverstanden", "datenschutz", "privacy", "akzeptier", "einstellung", "partner", "analyse", "marketing"] # Striktere Keywords
|
|
||||||
text_lower = text.lower()
|
text_lower = text.lower()
|
||||||
# Zaehle, wie viele stricte Keywords im extrahierten Text vorkommen
|
|
||||||
keyword_hits = sum(1 for keyword in banner_keywords_strict if keyword in 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 enthaelt -> Verwerfen
|
|
||||||
# Passen Sie die Schwellenwerte an, um false positives zu vermeiden.
|
|
||||||
# Eine Laenge unter 500 Zeichen und mindestens 3 stricte Keywords koennte ein Banner sein.
|
|
||||||
if len(text) < 500 and keyword_hits >= 3:
|
if len(text) < 500 and keyword_hits >= 3:
|
||||||
logger.warning(f"WARNUNG: Extrahierter Text fuer {url[:100]}... scheint nur Cookie-Banner zu sein (Laenge {len(text)}, {keyword_hits} Keywords). Verwerfe Text.") # Logge die Warnung
|
logger.warning(f"WARNUNG: Extrahierter Text fuer {url[:100]}... scheint nur Cookie-Banner zu sein (Laenge {len(text)}, {keyword_hits} Keywords). Verwerfe Text.")
|
||||||
return "k.A. (Nur Cookie-Banner erkannt)" # Gebe spezifischen Fehlerwert zurueck
|
return "k.A. (Nur Cookie-Banner erkannt)"
|
||||||
|
|
||||||
# Wenn der Text nach Bereinigung immer noch sehr kurz ist (z.B. nur ein paar Worte),
|
|
||||||
# kann es sein, dass kein relevanter Inhalt gescrapt wurde. Logge dies optional.
|
|
||||||
if len(text.split()) < 10 or len(text) < 50:
|
if len(text.split()) < 10 or len(text) < 50:
|
||||||
# logger.debug(f"Extrahierter Text fuer {url[:100]}... ist sehr kurz ({len(text.split())} Worte, {len(text)} Zeichen).") # Zu viel Laerm im Debug
|
pass
|
||||||
pass # Behalte den Text, keine weitere Filterung
|
|
||||||
|
|
||||||
# Begrenzen Sie die Laenge des zurueckgegebenen Rohtextes (Optional, aber empfohlen)
|
|
||||||
result = text[:max_length]
|
result = text[:max_length]
|
||||||
# Loggen Sie den Erfolg des Scrapings
|
|
||||||
logger.debug(f"Website {url[:100]}... erfolgreich gescrapt. Extrahierter Text (Laenge {len(result)}).")
|
logger.debug(f"Website {url[:100]}... erfolgreich gescrapt. Extrahierter Text (Laenge {len(result)}).")
|
||||||
# logger.debug(f"Extrahierter Text Anfang: {result[:100]}...") # Logge den Anfang des Textes (gekuerzt)
|
|
||||||
# Gebe den gekuerzten Text zurueck, oder "k.A." wenn er leer ist
|
|
||||||
return result if result else "k.A. (Extraktion leer)"
|
return result if result else "k.A. (Extraktion leer)"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Wenn weder Body noch spezifischer Inhaltsbereich gefunden wurde
|
logger.warning(f"Kein <body> oder spezifischer Inhaltsbereich gefunden in {url[:100]}...")
|
||||||
logger.warning(f"Kein <body> oder spezifischer Inhaltsbereich gefunden in {url[:100]}...") # Logge die Warnung
|
return "k.A. (Kein Body gefunden)"
|
||||||
return "k.A. (Kein Body gefunden)" # Gebe spezifischen Fehlerwert zurueck
|
|
||||||
|
|
||||||
|
except Exception as e_parse:
|
||||||
# Exceptions (wie RequestsErrors) werden vom retry_on_failure Decorator behandelt.
|
logger.error(f"Fehler beim Parsen von HTML von {url[:100]}...: {type(e_parse).__name__} - {e_parse}")
|
||||||
# Wenn eine Exception hier durchkommt, hat der Decorator aufgegeben.
|
logger.debug(traceback.format_exc())
|
||||||
except Exception as e: # Fangen Sie alle verbleibenden Exceptions, die nicht vom Decorator behandelt wurden
|
return f"k.A. (Fehler Parsing: {str(e_parse)[:50]}...)"
|
||||||
# Logge den Fehler auf Error-Level
|
|
||||||
logger.error(f"Allgemeiner Fehler beim Scraping von {url[:100]}...: {type(e).__name__} - {e}")
|
|
||||||
# Die Exception wurde bereits vom retry_on_failure Decorator als finaler Fehler geloggt.
|
|
||||||
# Geben Sie einen Fehlerwert zurueck, der im Sheet gespeichert werden kann.
|
|
||||||
return f"k.A. (Fehler: {str(e)[:100]}...)" # Signalisiert Fehler (gekuerzt)
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user