Files
Brancheneinstufung2/scrape_fotograf.py
2025-07-16 12:01:23 +00:00

233 lines
9.4 KiB
Python

import json
import os
import time
import csv
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
# --- Konfiguration & Konstanten ---
CREDENTIALS_FILE = 'fotograf_credentials.json'
OUTPUT_DIR = 'output'
OUTPUT_FILE = os.path.join(OUTPUT_DIR, 'nutzer_ohne_logins.csv')
LOGIN_URL = 'https://app.fotograf.de/login/login'
# --- Selektoren (zentral verwaltet für einfache Anpassung) ---
SELECTORS = {
"login_user": "#FotografEmail",
"login_pass": "#FotografPassword",
"login_button": "button[type='submit']",
"job_name": "h1",
"album_rows": "div.table-row-group > div.table-row", # Container für jede Album-Zeile
"album_link": "a.text-legacy-azure-200",
"child_name": "div:nth-of-type(4) strong",
"login_count": "div:nth-of-type(6) strong",
"buyer_link": "a.block:has(span:contains('Käufer'))", # Modernerer Selector
"buyer_email": "div.flex:nth-of-type(4) span"
}
def setup_driver():
"""Initialisiert und konfiguriert den Chrome WebDriver."""
print("Initialisiere Chrome WebDriver...")
options = Options()
options.add_argument('--headless') # Headless für Docker-Umgebungen empfohlen
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--window-size=1920,1200')
options.binary_location = '/usr/bin/google-chrome'
try:
driver = webdriver.Chrome(options=options)
return driver
except Exception as e:
print(f"Fehler bei der Initialisierung des WebDrivers: {e}")
print("Stelle sicher, dass Google Chrome und der passende Chromedriver im Docker-Image korrekt installiert sind.")
return None
def load_credentials(profile_name):
"""Lädt Anmeldedaten für ein bestimmtes Profil aus der JSON-Datei."""
print(f"Lade Anmeldedaten für Profil: {profile_name}")
try:
with open(CREDENTIALS_FILE, 'r') as f:
all_credentials = json.load(f)
if profile_name in all_credentials:
return all_credentials[profile_name]
else:
print(f"Fehler: Profil '{profile_name}' nicht in {CREDENTIALS_FILE} gefunden.")
return None
except FileNotFoundError:
print(f"Fehler: {CREDENTIALS_FILE} nicht gefunden.")
return None
except json.JSONDecodeError:
print(f"Fehler: {CREDENTIALS_FILE} ist keine gültige JSON-Datei.")
return None
def login(driver, username, password):
"""Führt den Login-Vorgang auf fotograf.de durch."""
print("Starte Login-Vorgang...")
try:
driver.get(LOGIN_URL)
wait = WebDriverWait(driver, 15)
# Warten und Anmeldedaten eingeben
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, SELECTORS["login_user"]))).send_keys(username)
driver.find_element(By.CSS_SELECTOR, SELECTORS["login_pass"]).send_keys(password)
driver.find_element(By.CSS_SELECTOR, SELECTORS["login_button"]).click()
# Warten auf erfolgreichen Login (z.B. durch prüfen der URL oder eines Dashboard-Elements)
wait.until(EC.url_contains('/config_dashboard/index'))
print("Login erfolgreich!")
return True
except TimeoutException:
print("Login fehlgeschlagen. Timeout beim Warten auf Elemente oder die nächste Seite.")
return False
except Exception as e:
print(f"Ein unerwarteter Fehler beim Login ist aufgetreten: {e}")
return False
def process_job(driver, job_url):
"""Verarbeitet einen einzelnen Fotoauftrag."""
print(f"\nVerarbeite Job-URL: {job_url}")
# 1. Zur Job-Seite navigieren und Job-Namen ausgeben
# Die URL muss zur Alben-Übersicht führen. Wir leiten sie aus der Job-URL ab.
job_id = job_url.split('/')[-1]
albums_url = f"https://app.fotograf.de/config_jobs_photos/index/{job_id}"
settings_url = f"https://app.fotograf.de/config_jobs_settings/index/{job_id}"
driver.get(settings_url)
wait = WebDriverWait(driver, 15)
try:
job_name = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, SELECTORS["job_name"]))).text
print(f"Auftragsname: '{job_name}'")
except TimeoutException:
print("Konnte den Auftragsnamen nicht finden. Überprüfe den Selector oder die Seite.")
return []
# 2. Zur Alben-Übersicht navigieren
print(f"Navigiere zur Alben-Übersicht: {albums_url}")
driver.get(albums_url)
# 3. Alben mit 0 Logins identifizieren und deren Daten sammeln
albums_to_process = []
try:
# Warten, bis die Album-Zeilen geladen sind
album_rows = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, SELECTORS["album_rows"])))
print(f"{len(album_rows)} Alben gefunden. Prüfe auf Logins...")
for row in album_rows:
try:
login_count_text = row.find_element(By.CSS_SELECTOR, SELECTORS["login_count"]).text
if int(login_count_text) == 0:
child_name = row.find_element(By.CSS_SELECTOR, SELECTORS["child_name"]).text
album_link = row.find_element(By.CSS_SELECTOR, SELECTORS["album_link"]).get_attribute('href')
albums_to_process.append({
"child_name": child_name,
"album_detail_url": album_link
})
print(f" -> Gefunden: Kind '{child_name}' mit 0 Logins. Link wird zur späteren Verarbeitung gespeichert.")
except (NoSuchElementException, ValueError) as e:
# Ignoriere Zeilen, die nicht dem erwarteten Format entsprechen (z.B. Kopfzeile)
# print(f" -> Zeile übersprungen, konnte Daten nicht extrahieren: {e}")
pass
except TimeoutException:
print("Keine Alben auf der Seite gefunden oder Timeout beim Warten.")
return []
# 4. Käuferdetails für die identifizierten Alben abrufen
results = []
print(f"\nVerarbeite {len(albums_to_process)} Alben mit 0 Logins im Detail...")
for album in albums_to_process:
try:
print(f" Rufe Detailseite für '{album['child_name']}' auf...")
driver.get(album["album_detail_url"])
# Käufer-Link finden und aufrufen
buyer_link_element = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, SELECTORS["buyer_link"])))
buyer_name = buyer_link_element.text.replace('Käufer ', '').strip()
buyer_page_url = buyer_link_element.get_attribute('href')
print(f" Käufer gefunden: '{buyer_name}'. Rufe Käuferseite auf...")
driver.get(buyer_page_url)
# E-Mail-Adresse extrahieren
buyer_email = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, SELECTORS["buyer_email"]))).text
print(f" E-Mail gefunden: {buyer_email}")
results.append({
"Auftragsname": job_name,
"Kind Vorname": album["child_name"],
"Käufer Name": buyer_name,
"Käufer E-Mail": buyer_email,
})
time.sleep(1) # Kurze Pause, um die Server nicht zu überlasten
except TimeoutException:
print(f" Fehler: Timeout beim Verarbeiten der Detailseite für '{album['child_name']}'. Element nicht gefunden.")
except Exception as e:
print(f" Ein unerwarteter Fehler bei der Detailverarbeitung für '{album['child_name']}' ist aufgetreten: {e}")
return results
def save_results_to_csv(results):
"""Speichert die gesammelten Daten in einer CSV-Datei."""
if not results:
print("\nKeine Daten zum Speichern vorhanden.")
return
print(f"\nSpeichere {len(results)} Ergebnisse in '{OUTPUT_FILE}'...")
# Sicherstellen, dass der output-Ordner existiert
os.makedirs(OUTPUT_DIR, exist_ok=True)
# CSV schreiben
with open(OUTPUT_FILE, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=results[0].keys())
writer.writeheader()
writer.writerows(results)
print("Speichern erfolgreich!")
def main():
"""Hauptfunktion des Skripts."""
print("--- Fotograf.de Scraper für Nutzer ohne Logins ---")
# Profil abfragen
profile_name = input("Bitte gib den Namen des zu verwendenden Profils ein (z.B. 'Kindergarten'): ")
credentials = load_credentials(profile_name)
if not credentials:
return
# Job-URL abfragen
job_url = input("Bitte gib die URL des zu bearbeitenden Fotoauftrags ein: ")
if "fotograf.de" not in job_url:
print("Dies scheint keine gültige fotograf.de URL zu sein.")
return
driver = setup_driver()
if not driver:
return
try:
if login(driver, credentials['username'], credentials['password']):
# Verarbeitung starten
all_results = process_job(driver, job_url)
# Ergebnisse speichern
save_results_to_csv(all_results)
else:
print("Skript wird beendet, da der Login fehlgeschlagen ist.")
finally:
print("\nSkript beendet. Schließe WebDriver.")
driver.quit()
if __name__ == "__main__":
main()