import os import json import time import logging from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.chrome.options import Options as ChromeOptions from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException, NoSuchElementException from selenium.webdriver.common.keys import Keys import pandas as pd from config import Config, DEALFRONT_LOGIN_URL, DEALFRONT_CREDENTIALS_FILE, DEALFRONT_TARGET_URL, TARGET_SEARCH_NAME OUTPUT_DIR = "/app/output" # Logging-Konfiguration LOG_LEVEL = logging.DEBUG if Config.DEBUG else logging.INFO LOG_FORMAT = '%(asctime)s - %(levelname)-8s - %(name)-25s - %(message)s' # Root-Logger konfigurieren (gilt für alle Bibliotheken) logging.basicConfig(level=LOG_LEVEL, format=LOG_FORMAT, force=True) # Logger für Selenium auf INFO setzen, um Spam zu vermeiden logging.getLogger("selenium").setLevel(logging.INFO) # Logger für unser eigenes Skript logger = logging.getLogger(__name__) # FileHandler hinzufügen, um in eine Datei zu loggen log_filename = f"dealfront_run_{time.strftime('%Y%m%d-%H%M%S')}.log" log_filepath = os.path.join(OUTPUT_DIR, log_filename) file_handler = logging.FileHandler(log_filepath, mode='w', encoding='utf-8') file_handler.setFormatter(logging.Formatter(LOG_FORMAT)) logging.getLogger().addHandler(file_handler) OUTPUT_DIR = "/app/output" class DealfrontScraper: def __init__(self): logger.info("Initialisiere DealfrontScraper und Chrome WebDriver.") chrome_options = ChromeOptions() # NEU: Lade-Optimierungen prefs = {"profile.managed_default_content_settings.images": 2} chrome_options.add_experimental_option("prefs", prefs) chrome_options.add_argument("--headless") chrome_options.add_argument("--no-sandbox") chrome_options.add_argument("--disable-dev-shm-usage") chrome_options.add_argument("--window-size=1920,1080") chrome_options.add_argument("--disable-blink-features=AutomationControlled") chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"]) chrome_options.add_experimental_option('useAutomationExtension', False) try: self.driver = webdriver.Chrome(options=chrome_options) self.driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})") except Exception as e: logger.critical(f"WebDriver konnte nicht initialisiert werden. Fehler: {e}", exc_info=True) self.driver = None raise self.wait = WebDriverWait(self.driver, 30) logger.info("WebDriver erfolgreich initialisiert (Bild-Laden deaktiviert).") def _load_credentials(self): # (Diese Methode bleibt unverändert) try: with open(DEALFRONT_CREDENTIALS_FILE, 'r') as f: creds = json.load(f) username = creds.get("username") password = creds.get("password") if not username or "DEIN_DEALFRONT_BENUTZERNAME" in username or not password or "DEIN_DEALFRONT_PASSWORT" in password: logger.error(f"Zugangsdaten in '{DEALFRONT_CREDENTIALS_FILE}' sind ungültig.") return None, None return username, password except FileNotFoundError: logger.error(f"Credentials-Datei nicht gefunden: '{DEALFRONT_CREDENTIALS_FILE}'") return None, None except json.JSONDecodeError: logger.error(f"Fehler beim Parsen der Credentials-Datei: '{DEALFRONT_CREDENTIALS_FILE}'") return None, None def _save_debug_artifacts(self): # (Diese Methode bleibt unverändert, aber mit neuem Namen) try: os.makedirs(OUTPUT_DIR, exist_ok=True) timestamp = time.strftime("%Y%m%d-%H%M%S") screenshot_filepath = os.path.join(OUTPUT_DIR, f"error_{timestamp}.png") self.driver.save_screenshot(screenshot_filepath) logger.error(f"Screenshot '{screenshot_filepath}' wurde für die Analyse gespeichert.") html_filepath = os.path.join(OUTPUT_DIR, f"error_{timestamp}.html") with open(html_filepath, "w", encoding="utf-8") as f: f.write(self.driver.page_source) logger.error(f"HTML-Quellcode '{html_filepath}' wurde für die Analyse gespeichert.") except Exception as e: logger.error(f"Konnte Debug-Artefakte nicht speichern: {e}") def login_and_navigate_to_target(self): """ Führt den Login durch und klickt dann auf dem Dashboard auf den "Quick Link", um zur Target-Seite zu gelangen. """ if not self.driver: return False username, password = self._load_credentials() if not username or not password: return False try: # SCHRITT 1: LOGIN logger.info(f"Navigiere zur Login-Seite: {DEALFRONT_LOGIN_URL}") self.driver.get(DEALFRONT_LOGIN_URL) self.wait.until(EC.visibility_of_element_located((By.NAME, "email"))).send_keys(username) self.driver.find_element(By.CSS_SELECTOR, "input[type='password']").send_keys(password) self.driver.find_element(By.XPATH, "//button[normalize-space()='Log in']").click() logger.info("Login-Befehl gesendet.") # SCHRITT 2: AUF QUICK-LINK-KACHEL WARTEN UND KLICKEN logger.info("Warte auf Dashboard und 'Prospects finden'-Link in den Quick Links.") # Dieser XPath ist sehr spezifisch für den Link in der "Quick links"-Kachel prospects_link_selector = (By.XPATH, "//a[@data-test-target-product-tile]") prospects_link = self.wait.until(EC.element_to_be_clickable(prospects_link_selector)) logger.info("'Prospects finden'-Link gefunden. Klicke darauf...") prospects_link.click() # SCHRITT 3: NAVIGATION VERIFIZIEREN logger.info("Warte auf die finale Target-Seite...") verification_target_selector = (By.XPATH, "//button[normalize-space()='+ Neue Suche']") self.wait.until(EC.visibility_of_element_located(verification_target_selector)) logger.info("'Target'-Seite erfolgreich und vollständig geladen.") return True except Exception as e: logger.critical(f"Login- oder Navigationsprozess fehlgeschlagen: {type(e).__name__}", exc_info=True) self._save_debug_artifacts() return False def navigate_to_target(self): """ Navigiert zum 'Target'-Bereich und verifiziert den Erfolg in drei Schritten. Dieser Ansatz ist maximal robust gegen Timing-Probleme von SPAs. """ try: # SCHRITT 1: Befehl zur Navigation geben logger.info(f"Gebe Navigationsbefehl zur Target-URL: {Config.DEALFRONT_TARGET_URL}") self.driver.get(Config.DEALFRONT_TARGET_URL) # SCHRITT 2: Warten, bis die URL in der Adresszeile sich tatsächlich ändert. url_part_to_wait_for = "/t/prospector/" logger.info(f"Warte, bis die Browser-URL '{url_part_to_wait_for}' enthält...") self.wait.until(EC.url_contains(url_part_to_wait_for)) logger.info(f"URL-Wechsel bestätigt. Aktuelle URL: {self.driver.current_url}") # SCHRITT 3: ERST JETZT auf ein sichtbares Element auf der neuen Seite warten. verification_selector = (By.XPATH, "//button[normalize-space()='+ Neue Suche']") logger.info(f"Warte auf Sichtbarkeit des Verifizierungs-Elements auf der Target-Seite: {verification_selector}") self.wait.until(EC.visibility_of_element_located(verification_selector)) logger.info("'Target'-Seite erfolgreich und vollständig geladen.") return True except Exception as e: logger.critical(f"Navigation zur 'Target'-Seite endgültig fehlgeschlagen: {type(e).__name__}", exc_info=True) self._save_debug_artifacts() return False def handle_overlays(self): """Sucht nach bekannten Popups/Overlays und schließt sie.""" try: # Wir geben der Seite einen Moment, um mögliche Popups zu laden. # Timeout auf 5 Sekunden gesetzt, da wir nicht ewig warten wollen. short_wait = WebDriverWait(self.driver, 5) # Selektor für den Schließen-Button (das 'x') des CRM-Popups # Der XPath sucht nach einem beliebigen Button, der ein SVG-Icon mit einer passenden "d"-Path-Definition enthält. # Dies ist sehr robust gegen Klassen-Änderungen. close_button_xpath = "//button[@aria-label='Schließen' or @aria-label='Close'] | //div[contains(@class, 't-prospector-backdrop')]//button" logger.info("Suche nach bekannten Overlays/Popups...") close_button = short_wait.until(EC.element_to_be_clickable((By.XPATH, close_button_xpath))) logger.info("Schließen-Button für Overlay gefunden. Klicke darauf.") close_button.click() time.sleep(1) # Kurze Pause, damit das Overlay verschwindet. except TimeoutException: # Das ist der Normalfall, wenn kein Popup da ist. logger.info("Kein Overlay/Popup gefunden. Fahre fort.") except Exception as e: logger.warning(f"Fehler beim Schließen des Overlays, ignoriere und fahre fort: {e}") self._save_debug_artifacts() def load_search(self, search_name): """Lädt eine vordefinierte Suche anhand ihres Namens.""" try: logger.info(f"Warte, bis die Liste der gespeicherten Suchen sichtbar ist...") # Wir warten auf den Container, der alle Such-Items enthält. # Ein guter Selektor ist das `ul`-Element mit der ID, die mit `eb-popup` beginnt. # Oder allgemeiner: Ein Element, das den Text einer bekannten Suche enthält. search_list_container_selector = (By.XPATH, f"//div[normalize-space()='{search_name}']") self.wait.until(EC.visibility_of_element_located(search_list_container_selector)) logger.info(f"Liste gefunden. Klicke jetzt auf die vordefinierte Suche: '{search_name}'") # Der XPath sucht nach einem Element (div, a, button), dessen Text exakt übereinstimmt. search_link_selector = (By.XPATH, f"//*[normalize-space()='{search_name}']") search_link = self.wait.until(EC.element_to_be_clickable(search_link_selector)) search_link.click() # Verifizieren, dass die Ergebnisse geladen sind, indem wir auf die Ergebnistabelle warten. results_table_header_selector = (By.XPATH, "//th[normalize-space()='Firma']") self.wait.until(EC.visibility_of_element_located(results_table_header_selector)) logger.info(f"Suche '{search_name}' erfolgreich geladen und Ergebnisse angezeigt.") time.sleep(3) # Feste Wartezeit, damit alle Daten nachgeladen sind. return True except Exception as e: logger.critical(f"Laden der Suche '{search_name}' fehlgeschlagen: {type(e).__name__}", exc_info=True) self._save_debug_artifacts() return False def extract_current_page_results(self): """ Extrahiert Firmennamen und Webseiten direkt von der Seite mithilfe der präzisen, von Ihnen verifizierten CSS-Selektoren. """ try: logger.info("Extrahiere Ergebnisse mit direkten CSS-Selektoren...") results = [] # Warten, bis das erste Element, das wir suchen, vorhanden ist. # Das ist ein guter Indikator, dass die Liste geladen ist. company_name_selector = ".sticky-column a.t-highlight-text" self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, company_name_selector))) time.sleep(3) # Kurze Pause, um sicherzustellen, dass alle Elemente gerendert sind. # === DIREKTE EXTRAKTION MIT IHREN SELEKTOREN === company_elements = self.driver.find_elements(By.CSS_SELECTOR, company_name_selector) website_elements = self.driver.find_elements(By.CSS_SELECTOR, "a.text-gray-400.t-highlight-text") logger.info(f"{len(company_elements)} Firmennamen und {len(website_elements)} Webseiten-Elemente gefunden.") # Wir gehen davon aus, dass die Anzahl der Elemente übereinstimmt und iterieren # über die kürzere der beiden Listen, um Fehler zu vermeiden. num_results = min(len(company_elements), len(website_elements)) if num_results == 0: logger.warning("Keine Firmen oder Webseiten mit den angegebenen Selektoren gefunden.") self._save_debug_artifacts() return [] for i in range(num_results): try: company_name = company_elements[i].get_attribute("title").strip() website = website_elements[i].text.strip() if company_name and website: results.append({'name': company_name, 'website': website}) else: logger.warning(f"Element {i+1}: Unvollständige Daten.") except Exception as e: logger.warning(f"Fehler bei der Verarbeitung von Element {i+1}: {e}") continue logger.info(f"Extraktion abgeschlossen. {len(results)} Firmen erfolgreich zugeordnet.") return results except Exception as e: logger.error(f"Ein schwerwiegender Fehler ist bei der Extraktion der Ergebnisse aufgetreten: {type(e).__name__}", exc_info=True) self._save_debug_artifacts() return [] def close(self): if self.driver: logger.info("Schließe den WebDriver.") self.driver.quit() if __name__ == "__main__": logger.info("Starte Dealfront Automatisierung - Finaler Workflow") scraper = None try: scraper = DealfrontScraper() if not scraper.driver: raise Exception("WebDriver konnte nicht initialisiert werden.") # === EINZIGER AUFRUF FÜR LOGIN UND NAVIGATION === if not scraper.login_and_navigate_to_target(): raise Exception("Login und Navigation fehlgeschlagen.") # Ab hier geht es weiter wie geplant... if not scraper.load_search(Config.TARGET_SEARCH_NAME): raise Exception(f"Laden der Suche '{Config.TARGET_SEARCH_NAME}' fehlgeschlagen.") scraper.handle_overlays() companies = scraper.extract_current_page_results() if companies: df = pd.DataFrame(companies) pd.set_option('display.max_rows', None) pd.set_option('display.max_columns', None) pd.set_option('display.width', 1000) pd.set_option('display.max_colwidth', None) print("\n" + "="*80) print(" EXTRAHIERTE FIRMEN (ERSTE SEITE) ".center(80, "=")) print("="*80) print(df.to_string(index=False)) print("="*80 + "\n") else: logger.warning("Keine Firmen auf der ersten Seite extrahiert oder gefunden.") logger.info("Test erfolgreich abgeschlossen. Warte vor dem Schließen...") time.sleep(10) except Exception as e: logger.critical(f"Ein kritischer Fehler ist im Hauptprozess aufgetreten: {e}", exc_info=False) finally: if scraper: scraper.close() logger.info("Dealfront Automatisierung beendet.")