Update changes
Dieser Commit ist enthalten in:
@ -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:
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren