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

@ -1,11 +1,14 @@
"""
Cookie Consent Handler für Browser-Sessions
Behandelt Cookie-Consent-Seiten bei der Session-Wiederherstellung
Behandelt Cookie-Consent-Seiten bei der Session-Wiederherstellung.
Enthält Anti-Detection-Maßnahmen wie Lese-Pausen.
"""
import logging
from typing import Optional
import time
import random
from typing import Optional, Any
from playwright.sync_api import Page
logger = logging.getLogger(__name__)
@ -15,20 +18,25 @@ class CookieConsentHandler:
"""Behandelt Cookie-Consent-Dialoge verschiedener Plattformen"""
@staticmethod
def handle_instagram_consent(page: Page) -> bool:
def handle_instagram_consent(page: Page, human_behavior: Any = None) -> bool:
"""
Behandelt Instagram's Cookie-Consent-Seite
Behandelt Instagram's Cookie-Consent-Seite mit realistischer Lese-Zeit.
Diese Methode simuliert menschliches Verhalten durch eine Pause
bevor der Cookie-Dialog geklickt wird. Echte Menschen lesen den
Cookie-Text bevor sie auf einen Button klicken.
Args:
page: Playwright Page-Objekt
human_behavior: Optional HumanBehavior-Instanz für realistische Delays
Returns:
bool: True wenn Consent behandelt wurde, False sonst
"""
try:
# Warte kurz auf Seitenladung
page.wait_for_load_state('networkidle', timeout=5000)
# Prüfe ob wir auf der Cookie-Consent-Seite sind
consent_indicators = [
# Deutsche Texte
@ -57,7 +65,28 @@ class CookieConsentHandler:
button = page.locator(button_selector).first
if button.is_visible():
logger.info(f"Found consent decline button: {button_selector}")
# ANTI-DETECTION: Realistische Lese-Pause bevor Cookie-Dialog geklickt wird
# Simuliert das Lesen der Cookie-Informationen (3-8 Sekunden)
if human_behavior and hasattr(human_behavior, 'anti_detection_delay'):
logger.debug("Cookie-Banner erkannt - simuliere Lesen...")
human_behavior.anti_detection_delay("cookie_reading")
else:
# Fallback ohne HumanBehavior
read_delay = random.uniform(3.0, 8.0)
logger.debug(f"Cookie-Banner Lese-Pause: {read_delay:.1f}s")
time.sleep(read_delay)
# Gelegentlich etwas scrollen um "mehr zu lesen" (30% Chance)
if random.random() < 0.3:
try:
page.evaluate("window.scrollBy(0, 50)")
time.sleep(random.uniform(0.8, 1.5))
page.evaluate("window.scrollBy(0, -50)")
time.sleep(random.uniform(0.3, 0.6))
except:
pass
# Verwende robuste Click-Methoden für Cookie-Consent
success = False
try:
@ -233,19 +262,21 @@ class CookieConsentHandler:
return False
@staticmethod
def check_and_handle_consent(page: Page, platform: str = "instagram") -> bool:
def check_and_handle_consent(page: Page, platform: str = "instagram",
human_behavior: Any = None) -> bool:
"""
Prüft und behandelt Cookie-Consent für die angegebene Plattform
Prüft und behandelt Cookie-Consent für die angegebene Plattform.
Args:
page: Playwright Page-Objekt
platform: Plattform-Name (default: "instagram")
human_behavior: Optional HumanBehavior-Instanz für realistische Delays
Returns:
bool: True wenn Consent behandelt wurde, False sonst
"""
if platform.lower() == "instagram":
return CookieConsentHandler.handle_instagram_consent(page)
return CookieConsentHandler.handle_instagram_consent(page, human_behavior)
else:
logger.warning(f"No consent handler implemented for platform: {platform}")
return False

Datei anzeigen

@ -1,521 +0,0 @@
# Instagram Video Bypass - Emergency Deep Level Fixes
"""
Tiefgreifende Instagram Video Bypass Techniken
"""
import logging
import time
import random
from typing import Any, Dict, Optional
logger = logging.getLogger("instagram_video_bypass")
class InstagramVideoBypass:
"""Deep-level Instagram video bypass techniques"""
def __init__(self, page: Any):
self.page = page
def apply_emergency_bypass(self) -> None:
"""Wendet Emergency Deep-Level Bypass an"""
# 1. Complete Automation Signature Removal
automation_removal_script = """
() => {
// Remove ALL automation signatures
// 1. Navigator properties cleanup
delete navigator.__webdriver_script_fn;
delete navigator.__fxdriver_evaluate;
delete navigator.__driver_unwrapped;
delete navigator.__webdriver_unwrapped;
delete navigator.__driver_evaluate;
delete navigator.__selenium_unwrapped;
delete navigator.__fxdriver_unwrapped;
// 2. Window properties cleanup
delete window.navigator.webdriver;
delete window.webdriver;
delete window.chrome.webdriver;
delete window.callPhantom;
delete window._phantom;
delete window.__nightmare;
delete window._selenium;
delete window.calledSelenium;
delete window.$cdc_asdjflasutopfhvcZLmcfl_;
delete window.$chrome_asyncScriptInfo;
delete window.__webdriver_evaluate;
delete window.__selenium_evaluate;
delete window.__webdriver_script_function;
delete window.__webdriver_script_func;
delete window.__webdriver_script_fn;
delete window.__fxdriver_evaluate;
delete window.__driver_unwrapped;
delete window.__webdriver_unwrapped;
delete window.__driver_evaluate;
delete window.__selenium_unwrapped;
delete window.__fxdriver_unwrapped;
// 3. Document cleanup
delete document.__webdriver_script_fn;
delete document.__selenium_unwrapped;
delete document.__webdriver_unwrapped;
delete document.__driver_evaluate;
delete document.__webdriver_evaluate;
delete document.__fxdriver_evaluate;
delete document.__fxdriver_unwrapped;
delete document.__driver_unwrapped;
// 4. Chrome object enhancement
if (!window.chrome) {
window.chrome = {};
}
if (!window.chrome.runtime) {
window.chrome.runtime = {
onConnect: {addListener: function() {}},
onMessage: {addListener: function() {}},
connect: function() { return {postMessage: function() {}, onMessage: {addListener: function() {}}} }
};
}
if (!window.chrome.app) {
window.chrome.app = {
isInstalled: false,
InstallState: {DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed'},
RunningState: {CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running'}
};
}
// 5. Plugin array enhancement
const fakePlugins = [
{name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format'},
{name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: 'Portable Document Format'},
{name: 'Native Client', filename: 'internal-nacl-plugin', description: 'Native Client'}
];
Object.defineProperty(navigator, 'plugins', {
get: () => {
const pluginArray = [...fakePlugins];
pluginArray.length = fakePlugins.length;
pluginArray.item = function(index) { return this[index] || null; };
pluginArray.namedItem = function(name) { return this.find(p => p.name === name) || null; };
pluginArray.refresh = function() {};
return pluginArray;
},
configurable: true
});
}
"""
# 2. Instagram-specific video API spoofing
instagram_video_api_script = """
() => {
// Instagram Video API Deep Spoofing
// 1. MSE (Media Source Extensions) proper support
if (window.MediaSource) {
const originalIsTypeSupported = MediaSource.isTypeSupported;
MediaSource.isTypeSupported = function(type) {
const supportedTypes = [
'video/mp4; codecs="avc1.42E01E"',
'video/mp4; codecs="avc1.4D401F"',
'video/mp4; codecs="avc1.640028"',
'video/webm; codecs="vp8"',
'video/webm; codecs="vp9"',
'audio/mp4; codecs="mp4a.40.2"',
'audio/webm; codecs="opus"'
];
if (supportedTypes.includes(type)) {
return true;
}
return originalIsTypeSupported.call(this, type);
};
}
// 2. Encrypted Media Extensions deep spoofing
if (navigator.requestMediaKeySystemAccess) {
const originalRequestAccess = navigator.requestMediaKeySystemAccess;
navigator.requestMediaKeySystemAccess = function(keySystem, supportedConfigurations) {
if (keySystem === 'com.widevine.alpha') {
return Promise.resolve({
keySystem: 'com.widevine.alpha',
getConfiguration: () => ({
initDataTypes: ['cenc', 'keyids', 'webm'],
audioCapabilities: [
{contentType: 'audio/mp4; codecs="mp4a.40.2"', robustness: 'SW_SECURE_CRYPTO'},
{contentType: 'audio/webm; codecs="opus"', robustness: 'SW_SECURE_CRYPTO'}
],
videoCapabilities: [
{contentType: 'video/mp4; codecs="avc1.42E01E"', robustness: 'SW_SECURE_DECODE'},
{contentType: 'video/mp4; codecs="avc1.4D401F"', robustness: 'SW_SECURE_DECODE'},
{contentType: 'video/webm; codecs="vp9"', robustness: 'SW_SECURE_DECODE'}
],
distinctiveIdentifier: 'optional',
persistentState: 'required',
sessionTypes: ['temporary', 'persistent-license']
}),
createMediaKeys: () => Promise.resolve({
createSession: (sessionType) => {
const session = {
sessionId: 'session_' + Math.random().toString(36).substr(2, 9),
expiration: NaN,
closed: Promise.resolve(),
keyStatuses: new Map(),
addEventListener: function() {},
removeEventListener: function() {},
generateRequest: function(initDataType, initData) {
setTimeout(() => {
if (this.onmessage) {
this.onmessage({
type: 'message',
message: new ArrayBuffer(8)
});
}
}, 100);
return Promise.resolve();
},
load: function() { return Promise.resolve(false); },
update: function(response) {
setTimeout(() => {
if (this.onkeystatuseschange) {
this.onkeystatuseschange();
}
}, 50);
return Promise.resolve();
},
close: function() { return Promise.resolve(); },
remove: function() { return Promise.resolve(); }
};
// Add event target methods
session.dispatchEvent = function() {};
return session;
},
setServerCertificate: () => Promise.resolve(true)
})
});
}
return originalRequestAccess.apply(this, arguments);
};
}
// 3. Hardware media key handling
if (navigator.mediaSession) {
navigator.mediaSession.setActionHandler = function() {};
navigator.mediaSession.playbackState = 'playing';
} else {
navigator.mediaSession = {
metadata: null,
playbackState: 'playing',
setActionHandler: function() {},
setPositionState: function() {}
};
}
// 4. Picture-in-Picture API
if (!document.pictureInPictureEnabled) {
Object.defineProperty(document, 'pictureInPictureEnabled', {
get: () => true,
configurable: true
});
}
// 5. Web Audio API enhancement for video
if (window.AudioContext || window.webkitAudioContext) {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
const originalAudioContext = AudioCtx;
window.AudioContext = function(...args) {
const ctx = new originalAudioContext(...args);
// Override audio context properties for consistency
Object.defineProperty(ctx, 'baseLatency', {
get: () => 0.01,
configurable: true
});
Object.defineProperty(ctx, 'outputLatency', {
get: () => 0.02,
configurable: true
});
return ctx;
};
// Copy static methods
Object.keys(originalAudioContext).forEach(key => {
window.AudioContext[key] = originalAudioContext[key];
});
}
}
"""
# 3. Network request interception for video
network_interception_script = """
() => {
// Advanced network request interception for Instagram videos
const originalFetch = window.fetch;
window.fetch = function(input, init) {
const url = typeof input === 'string' ? input : input.url;
// Instagram video CDN requests
if (url.includes('instagram.com') || url.includes('fbcdn.net') || url.includes('cdninstagram.com')) {
const enhancedInit = {
...init,
headers: {
...init?.headers,
'Accept': '*/*',
'Accept-Encoding': 'identity;q=1, *;q=0',
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7',
'Cache-Control': 'no-cache',
'DNT': '1',
'Origin': 'https://www.instagram.com',
'Pragma': 'no-cache',
'Referer': 'https://www.instagram.com/',
'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Dest': 'video',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'User-Agent': navigator.userAgent,
'X-Asbd-Id': '129477',
'X-Fb-Lsd': document.querySelector('[name="fb_dtsg"]')?.value || '',
'X-Instagram-Ajax': '1'
}
};
// Remove problematic headers that might indicate automation
delete enhancedInit.headers['sec-ch-ua-arch'];
delete enhancedInit.headers['sec-ch-ua-bitness'];
delete enhancedInit.headers['sec-ch-ua-full-version'];
delete enhancedInit.headers['sec-ch-ua-full-version-list'];
delete enhancedInit.headers['sec-ch-ua-model'];
delete enhancedInit.headers['sec-ch-ua-wow64'];
return originalFetch.call(this, input, enhancedInit);
}
return originalFetch.apply(this, arguments);
};
// XMLHttpRequest interception
const originalXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
this._url = url;
return originalXHROpen.apply(this, arguments);
};
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(body) {
if (this._url && (this._url.includes('instagram.com') || this._url.includes('fbcdn.net'))) {
// Add video-specific headers
this.setRequestHeader('Accept', '*/*');
this.setRequestHeader('Accept-Language', 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7');
this.setRequestHeader('Cache-Control', 'no-cache');
this.setRequestHeader('Pragma', 'no-cache');
this.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
}
return originalXHRSend.apply(this, arguments);
};
}
"""
# 4. Timing and behavior normalization
timing_script = """
() => {
// Normalize timing functions to avoid detection
// Performance timing spoofing
if (window.performance && window.performance.timing) {
const timing = performance.timing;
const now = Date.now();
Object.defineProperty(performance.timing, 'navigationStart', {
get: () => now - Math.floor(Math.random() * 1000) - 1000,
configurable: true
});
Object.defineProperty(performance.timing, 'loadEventEnd', {
get: () => now - Math.floor(Math.random() * 500),
configurable: true
});
}
// Date/Time consistency
const originalDate = Date;
const startTime = originalDate.now();
Date.now = function() {
return startTime + (originalDate.now() - startTime);
};
// Remove timing inconsistencies that indicate automation
const originalSetTimeout = window.setTimeout;
window.setTimeout = function(fn, delay, ...args) {
// Add slight randomization to timing
const randomDelay = delay + Math.floor(Math.random() * 10) - 5;
return originalSetTimeout.call(this, fn, Math.max(0, randomDelay), ...args);
};
}
"""
# Apply all scripts in sequence
scripts = [
automation_removal_script,
instagram_video_api_script,
network_interception_script,
timing_script
]
for i, script in enumerate(scripts):
try:
self.page.add_init_script(script)
logger.info(f"Applied emergency bypass script {i+1}/4")
time.sleep(0.1) # Small delay between scripts
except Exception as e:
logger.error(f"Failed to apply emergency bypass script {i+1}: {e}")
logger.info("Emergency Instagram video bypass applied")
def inject_video_session_data(self) -> None:
"""Injiziert realistische Video-Session-Daten"""
session_script = """
() => {
// Inject realistic video session data
// 1. Video viewing history
localStorage.setItem('instagram_video_history', JSON.stringify({
last_viewed: Date.now() - Math.floor(Math.random() * 86400000),
view_count: Math.floor(Math.random() * 50) + 10,
preferences: {
autoplay: true,
quality: 'auto',
captions: false
}
}));
// 2. Media session state
localStorage.setItem('media_session_state', JSON.stringify({
hasInteracted: true,
lastInteraction: Date.now() - Math.floor(Math.random() * 3600000),
playbackRate: 1,
volume: 0.8
}));
// 3. DRM license cache simulation
sessionStorage.setItem('drm_licenses', JSON.stringify({
widevine: {
version: '4.10.2449.0',
lastUpdate: Date.now() - Math.floor(Math.random() * 604800000),
status: 'valid'
}
}));
// 4. Instagram session tokens
const csrfToken = document.querySelector('[name="csrfmiddlewaretoken"]')?.value ||
document.querySelector('meta[name="csrf-token"]')?.content ||
'missing';
if (csrfToken !== 'missing') {
sessionStorage.setItem('csrf_token', csrfToken);
}
}
"""
try:
self.page.evaluate(session_script)
logger.info("Video session data injected successfully")
except Exception as e:
logger.error(f"Failed to inject video session data: {e}")
def simulate_user_interaction(self) -> None:
"""Simuliert authentische Benutzerinteraktion"""
try:
# Random mouse movements
for _ in range(3):
x = random.randint(100, 800)
y = random.randint(100, 600)
self.page.mouse.move(x, y)
time.sleep(random.uniform(0.1, 0.3))
# Random scroll
self.page.mouse.wheel(0, random.randint(-200, 200))
time.sleep(random.uniform(0.2, 0.5))
# Click somewhere safe (not on video)
self.page.click('body', position={'x': random.randint(50, 100), 'y': random.randint(50, 100)})
time.sleep(random.uniform(0.3, 0.7))
logger.info("User interaction simulation completed")
except Exception as e:
logger.error(f"Failed to simulate user interaction: {e}")
def check_video_errors(self) -> Dict[str, Any]:
"""Überprüft Video-Fehler und DRM-Status"""
try:
result = self.page.evaluate("""
() => {
const errors = [];
const diagnostics = {
drm_support: false,
media_source: false,
codec_support: {},
video_elements: 0,
error_messages: []
};
// Check for DRM support
if (navigator.requestMediaKeySystemAccess) {
diagnostics.drm_support = true;
}
// Check Media Source Extensions
if (window.MediaSource) {
diagnostics.media_source = true;
diagnostics.codec_support = {
h264: MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E"'),
vp9: MediaSource.isTypeSupported('video/webm; codecs="vp9"'),
aac: MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.2"')
};
}
// Count video elements
diagnostics.video_elements = document.querySelectorAll('video').length;
// Look for error messages
const errorElements = document.querySelectorAll('[class*="error"], [class*="fail"]');
errorElements.forEach(el => {
if (el.textContent.includes('Video') || el.textContent.includes('video')) {
diagnostics.error_messages.push(el.textContent.trim());
}
});
// Console errors
const consoleErrors = [];
const originalConsoleError = console.error;
console.error = function(...args) {
consoleErrors.push(args.join(' '));
originalConsoleError.apply(console, arguments);
};
return {
diagnostics,
console_errors: consoleErrors,
timestamp: Date.now()
};
}
""")
logger.info(f"Video diagnostics: {result}")
return result
except Exception as e:
logger.error(f"Video error check failed: {e}")
return {}

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: