Dieser Commit ist enthalten in:
Claude Project Manager
2026-01-18 18:15:34 +01:00
Ursprung 4e82d5ef8f
Commit a25a26a01a
47 geänderte Dateien mit 4756 neuen und 2956 gelöschten Zeilen

Datei anzeigen

@ -17,6 +17,7 @@ from infrastructure.services.browser_protection_service import BrowserProtection
# Konfiguriere Logger
logger = logging.getLogger("playwright_manager")
class PlaywrightManager:
"""
Verwaltet Browser-Sitzungen mit Playwright, einschließlich Stealth-Modus und Proxy-Einstellungen.
@ -25,6 +26,9 @@ class PlaywrightManager:
# Klassen-Variable: Zählt aktive Browser-Instanzen (Feature 5: Browser-Instanz Schutz)
_active_count = 0
# Klassen-Variable: Referenz auf die zuletzt gestartete Instanz (für Cleanup)
_current_instance: "PlaywrightManager" = None
def __init__(self,
headless: bool = False,
proxy: Optional[Dict[str, str]] = None,
@ -149,24 +153,30 @@ class PlaywrightManager:
# Feature 5: Browser-Instanz Schutz - Nur eine Instanz gleichzeitig
if PlaywrightManager._active_count >= 1:
# Safety-Check: Prüfe ob Counter "hängt" (Absturz-Schutz)
# Wenn ProcessGuard NICHT locked ist, aber Counter > 0, dann ist Counter "tot"
from utils.process_guard import get_guard
guard = get_guard()
# Es gibt noch einen alten Browser - prüfe ob er geschlossen werden kann
old_instance = PlaywrightManager._current_instance
if not guard.is_locked():
# Counter hängt! Process Guard ist frei, aber Counter sagt Browser läuft
if old_instance is not None and old_instance is not self:
# Alte Instanz existiert und ist nicht diese - schließen
logger.warning(
f"⚠️ BROWSER-CLEANUP: Alte Browser-Instanz wird geschlossen "
f"(Counter war {PlaywrightManager._active_count})"
)
try:
old_instance.close()
except Exception as e:
logger.warning(f"Fehler beim Schließen alter Browser-Instanz: {e}")
# Counter und Referenz zurücksetzen
PlaywrightManager._active_count = 0
PlaywrightManager._current_instance = None
else:
# Keine alte Instanz gefunden, aber Counter > 0 - Safety Reset
logger.warning(
f"⚠️ SAFETY-RESET: _active_count war {PlaywrightManager._active_count}, "
f"aber ProcessGuard ist nicht locked. Counter wird zurückgesetzt."
f"aber keine alte Instanz gefunden. Counter wird zurückgesetzt."
)
PlaywrightManager._active_count = 0
else:
# Guard ist locked UND Counter ist > 0 → echte parallele Instanz
raise RuntimeError(
"Browser bereits aktiv. Nur eine Browser-Instanz gleichzeitig erlaubt. "
"Beenden Sie den aktuellen Prozess."
)
try:
self.playwright = sync_playwright().start()
@ -281,8 +291,9 @@ class PlaywrightManager:
self.browser.on("disconnected", self._on_browser_disconnected)
logger.debug("Browser-Disconnect-Handler registriert")
# Feature 5: Browser-Instanz Counter erhöhen
# Feature 5: Browser-Instanz Counter erhöhen und aktuelle Instanz speichern
PlaywrightManager._active_count += 1
PlaywrightManager._current_instance = self
logger.info(f"Browser gestartet (aktive Instanzen: {PlaywrightManager._active_count})")
return self.page
@ -524,15 +535,22 @@ class PlaywrightManager:
key = f"fill_{selector}"
return self._retry_action(key, lambda: self.fill_form_field(selector, value, timeout))
def click_element(self, selector: str, force: bool = False, timeout: int = 5000) -> bool:
def click_element(self, selector: str, force: bool = False, timeout: int = 5000,
use_bezier_mouse: bool = True) -> bool:
"""
Klickt auf ein Element mit Anti-Bot-Bypass-Strategien.
Diese Methode simuliert menschliches Klick-Verhalten durch:
- Bézier-Kurven-Mausbewegungen zum Element
- Natürliches Scrolling (auch bei sichtbaren Elementen)
- Variable Verzögerungen
Args:
selector: Selektor für das Element
force: Force-Click verwenden
timeout: Timeout in Millisekunden
use_bezier_mouse: Ob Bézier-Mausbewegung vor dem Klick verwendet werden soll
Returns:
bool: True bei Erfolg, False bei Fehler
"""
@ -541,24 +559,164 @@ class PlaywrightManager:
element = self.wait_for_selector(selector, timeout)
if not element:
return False
# Scroll zum Element
self.page.evaluate("element => element.scrollIntoView({ behavior: 'smooth', block: 'center' })", element)
time.sleep(random.uniform(0.3, 0.7))
# Menschenähnliches Verhalten - leichte Verzögerung vor dem Klick
time.sleep(random.uniform(0.2, 0.5))
# Element klicken
element.click(force=force, delay=random.uniform(20, 100))
# Verbessertes Scrolling mit menschlichem Verhalten
self._human_scroll_to_element(element)
# Bézier-Mausbewegung zum Element (Anti-Detection)
if use_bezier_mouse:
self._move_mouse_to_element_bezier(element)
# Menschenähnliches Verhalten - variable Verzögerung vor dem Klick
time.sleep(random.uniform(0.15, 0.4))
# Element klicken mit variablem Delay
element.click(force=force, delay=random.uniform(30, 120))
logger.info(f"Element geklickt: {selector}")
return True
except Exception as e:
logger.error(f"Fehler beim Klicken auf {selector}: {e}")
# Bei Fehlern verwende robuste Click-Strategien
return self.robust_click(selector, timeout)
def _human_scroll_to_element(self, element) -> None:
"""
Scrollt zum Element mit menschlichem Verhalten.
Simuliert natürliches Scrollverhalten:
- Gelegentlich erst in falsche Richtung scrollen
- Auch bei sichtbaren Elementen leicht scrollen
- Variable Scroll-Geschwindigkeit
Args:
element: Das Playwright ElementHandle
"""
try:
# Gelegentlich erst in "falsche" Richtung scrollen (15% Chance)
if random.random() < 0.15:
wrong_direction = random.choice(['up', 'down'])
scroll_amount = random.randint(50, 150)
if wrong_direction == 'up':
self.page.evaluate(f"window.scrollBy(0, -{scroll_amount})")
else:
self.page.evaluate(f"window.scrollBy(0, {scroll_amount})")
time.sleep(random.uniform(0.2, 0.5))
logger.debug(f"Korrektur-Scroll: erst {wrong_direction}")
# Scroll-Verhalten zufällig wählen
scroll_behavior = random.choice(['smooth', 'smooth', 'auto']) # 66% smooth
scroll_block = random.choice(['center', 'center', 'nearest']) # 66% center
# Hauptscroll zum Element
scroll_script = f"""
(element) => {{
const rect = element.getBoundingClientRect();
const isFullyVisible = rect.top >= 0 && rect.bottom <= window.innerHeight;
// Auch wenn sichtbar, leicht scrollen für natürliches Verhalten (60% Chance)
if (isFullyVisible && Math.random() < 0.6) {{
const smallScroll = Math.floor(Math.random() * 60) - 30;
window.scrollBy(0, smallScroll);
}}
// Zum Element scrollen
element.scrollIntoView({{ behavior: '{scroll_behavior}', block: '{scroll_block}' }});
}}
"""
self.page.evaluate(scroll_script, element)
# Variable Wartezeit nach Scroll
time.sleep(random.uniform(0.4, 1.0))
except Exception as e:
logger.warning(f"Fehler beim Human-Scroll: {e}")
# Fallback: einfaches Scroll
try:
self.page.evaluate("element => element.scrollIntoView({ behavior: 'smooth', block: 'center' })", element)
time.sleep(random.uniform(0.3, 0.7))
except:
pass
def _move_mouse_to_element_bezier(self, element) -> None:
"""
Bewegt die Maus mit Bézier-Kurve zum Element.
Simuliert realistische menschliche Mausbewegungen:
- Kubische Bézier-Kurve mit 2 Kontrollpunkten
- Variable Geschwindigkeit (langsamer am Anfang/Ende)
- Leichtes Zittern für Natürlichkeit
- Gelegentliche Mikro-Pausen
Args:
element: Das Playwright ElementHandle
"""
try:
# Aktuelle Mausposition oder zufälliger Startpunkt
viewport = self.page.viewport_size
if viewport:
current_x = random.randint(100, viewport['width'] - 100)
current_y = random.randint(100, viewport['height'] - 100)
else:
current_x = random.randint(100, 1820)
current_y = random.randint(100, 980)
# Zielpunkt: Mitte des Elements mit leichter Variation
box = element.bounding_box()
if not box:
logger.debug("Kein Bounding-Box für Element, überspringe Bézier-Bewegung")
return
# Zielposition mit leichter Variation (nicht exakt Mitte)
target_x = box['x'] + box['width'] / 2 + random.uniform(-5, 5)
target_y = box['y'] + box['height'] / 2 + random.uniform(-5, 5)
# Entfernung berechnen
distance = ((target_x - current_x)**2 + (target_y - current_y)**2)**0.5
# Anzahl der Schritte basierend auf Entfernung (mehr Schritte = flüssiger)
steps = max(25, int(distance / 8))
# Kontrollpunkte für kubische Bézier-Kurve
ctrl_variance = distance / 4
ctrl1_x = current_x + (target_x - current_x) * random.uniform(0.2, 0.4) + random.uniform(-ctrl_variance, ctrl_variance)
ctrl1_y = current_y + (target_y - current_y) * random.uniform(0.1, 0.3) + random.uniform(-ctrl_variance, ctrl_variance)
ctrl2_x = current_x + (target_x - current_x) * random.uniform(0.6, 0.8) + random.uniform(-ctrl_variance, ctrl_variance)
ctrl2_y = current_y + (target_y - current_y) * random.uniform(0.7, 0.9) + random.uniform(-ctrl_variance, ctrl_variance)
# Mausbewegung ausführen
for i in range(steps + 1):
t = i / steps
# Kubische Bézier-Formel: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃
x = (1-t)**3 * current_x + 3*(1-t)**2*t * ctrl1_x + 3*(1-t)*t**2 * ctrl2_x + t**3 * target_x
y = (1-t)**3 * current_y + 3*(1-t)**2*t * ctrl1_y + 3*(1-t)*t**2 * ctrl2_y + t**3 * target_y
# Leichtes Zittern hinzufügen für Realismus
x += random.uniform(-1, 1)
y += random.uniform(-1, 1)
# Maus bewegen
self.page.mouse.move(x, y)
# Variable Geschwindigkeit: langsamer am Anfang/Ende, schneller in der Mitte
if i < steps * 0.15 or i > steps * 0.85:
# Langsamer am Anfang und Ende (Beschleunigung/Abbremsen)
time.sleep(random.uniform(0.008, 0.018))
else:
# Schneller in der Mitte
time.sleep(random.uniform(0.003, 0.008))
# Gelegentliche Mikro-Pause (5% Chance) - simuliert Zögern
if random.random() < 0.05:
time.sleep(random.uniform(0.02, 0.08))
logger.debug(f"Bézier-Mausbewegung: ({current_x:.0f},{current_y:.0f}) -> ({target_x:.0f},{target_y:.0f})")
except Exception as e:
logger.warning(f"Bézier-Mausbewegung fehlgeschlagen: {e}")
# Kein Fallback nötig - Klick funktioniert auch ohne Mausbewegung
def robust_click(self, selector: str, timeout: int = 5000) -> bool:
"""
@ -1017,6 +1175,10 @@ class PlaywrightManager:
else:
logger.warning("Browser disconnected aber Counter war bereits 0")
# Aktuelle Instanz-Referenz löschen wenn diese Instanz die aktuelle war
if PlaywrightManager._current_instance is self:
PlaywrightManager._current_instance = None
def close(self):
"""Schließt den Browser und gibt Ressourcen frei."""
try:
@ -1072,6 +1234,10 @@ class PlaywrightManager:
else:
logger.debug("Counter wurde bereits durch disconnected-Event dekrementiert")
# Aktuelle Instanz-Referenz löschen wenn diese Instanz die aktuelle war
if PlaywrightManager._current_instance is self:
PlaywrightManager._current_instance = None
logger.info("Browser-Sitzung erfolgreich geschlossen")
except Exception as e: