""" Модуль для имитации человеческого поведения Это Python-модуль (не JS!) для реалистичного взаимодействия с браузером. Используется для обхода поведенческого анализа AWS FWCIM. Особенности: - Случайные опечатки и исправления - Паузы "на подумать" перед вводом - Разная скорость для разных типов полей - Реалистичные движения мыши по кривой Безье """ import random import time import string class BehaviorSpoofModule: """ Имитация человеческого поведения при взаимодействии с браузером. Использование: behavior = BehaviorSpoofModule() behavior.human_delay() # Пауза между действиями behavior.human_type(element, "text", field_type="email") # Печать с задержками """ name = "behavior" description = "Human-like behavior simulation (Python)" # Типы полей и их характеристики скорости FIELD_SPEEDS = { 'email': {'delay': (0.03, 0.08), 'typo_prob': 0.01}, # Email - быстро, мало ошибок (знакомый текст) 'password': {'delay': (0.08, 0.18), 'typo_prob': 0.0}, # Пароль - медленнее, БЕЗ опечаток (критично!) 'name': {'delay': (0.05, 0.12), 'typo_prob': 0.01}, # Имя - средняя скорость 'code': {'delay': (0.12, 0.25), 'typo_prob': 0.0}, # Код верификации - БЕЗ опечаток! 'default': {'delay': (0.05, 0.15), 'typo_prob': 0.01} # По умолчанию } # Соседние клавиши для реалистичных опечаток NEARBY_KEYS = { 'q': 'wa', 'w': 'qeas', 'e': 'wrsd', 'r': 'etdf', 't': 'ryfg', 'y': 'tugh', 'u': 'yihj', 'i': 'uojk', 'o': 'iplk', 'p': 'ol', 'a': 'qwsz', 's': 'awedxz', 'd': 'serfcx', 'f': 'drtgvc', 'g': 'ftyhbv', 'h': 'gyujnb', 'j': 'huikmn', 'k': 'jiolm', 'l': 'kop', 'z': 'asx', 'x': 'zsdc', 'c': 'xdfv', 'v': 'cfgb', 'b': 'vghn', 'n': 'bhjm', 'm': 'njk', '1': '2q', '2': '13qw', '3': '24we', '4': '35er', '5': '46rt', '6': '57ty', '7': '68yu', '8': '79ui', '9': '80io', '0': '9p' } def __init__(self): # Настройки задержек self.typing_delay_range = (0.05, 0.15) # Между символами self.action_delay_range = (0.3, 1.0) # Между действиями self.think_delay_range = (0.5, 2.0) # "Думает" перед действием # Вероятности self.typo_probability = 0.02 # Вероятность опечатки self.pause_probability = 0.1 # Вероятность паузы при печати # Статистика сессии (для более реалистичного поведения) self._chars_typed = 0 self._typos_made = 0 self._session_start = time.time() def human_delay(self, min_delay: float = None, max_delay: float = None): """Человеческая задержка между действиями""" min_d = min_delay or self.action_delay_range[0] max_d = max_delay or self.action_delay_range[1] time.sleep(random.uniform(min_d, max_d)) def think_delay(self): """Задержка "размышления" перед действием""" time.sleep(random.uniform(*self.think_delay_range)) def typing_delay(self): """Задержка между нажатиями клавиш""" delay = random.uniform(*self.typing_delay_range) # Иногда делаем паузу if random.random() < self.pause_probability: delay += random.uniform(0.3, 0.8) time.sleep(delay) def simulate_reading(self, duration: float = None): """Симулирует чтение страницы""" if duration is None: duration = random.uniform(1.0, 3.0) time.sleep(duration) def human_type(self, element, text: str, clear_first: bool = True, field_type: str = 'default'): """ Печатает текст с человеческими задержками. Args: element: Элемент для ввода (DrissionPage element) text: Текст для ввода clear_first: Очистить поле перед вводом field_type: Тип поля ('email', 'password', 'name', 'code', 'default') """ # Получаем настройки для типа поля field_config = self.FIELD_SPEEDS.get(field_type, self.FIELD_SPEEDS['default']) delay_range = field_config['delay'] typo_prob = field_config['typo_prob'] # Пауза "на подумать" перед вводом (особенно для паролей и кодов) if field_type in ('password', 'code'): self.think_before_typing(field_type) element.click() self.human_delay(0.1, 0.3) if clear_first: element.clear() self.human_delay(0.1, 0.2) i = 0 while i < len(text): char = text[i] # Случайная пауза "на подумать" в середине ввода if random.random() < 0.03 and i > 0 and i < len(text) - 1: time.sleep(random.uniform(0.3, 0.8)) # Опечатка с реалистичным исправлением if random.random() < typo_prob and i < len(text) - 1: typo_char = self._get_typo_char(char) if typo_char: element.input(typo_char) self._chars_typed += 1 self._typos_made += 1 # Задержка перед осознанием ошибки time.sleep(random.uniform(0.1, 0.4)) # Иногда печатаем ещё 1-2 символа перед исправлением extra_chars = 0 if random.random() < 0.3 and i + 1 < len(text): extra_chars = random.randint(1, min(2, len(text) - i - 1)) for j in range(extra_chars): element.input(text[i + 1 + j]) time.sleep(random.uniform(*delay_range)) # Пауза "заметили ошибку" time.sleep(random.uniform(0.2, 0.5)) # Удаляем ошибочные символы for _ in range(1 + extra_chars): element.input('\b') time.sleep(random.uniform(0.05, 0.1)) # Вводим правильный символ element.input(char) self._chars_typed += 1 # Задержка между символами delay = random.uniform(*delay_range) # Дополнительная пауза после определённых символов if char in '.,!?@': delay += random.uniform(0.1, 0.3) elif char == ' ': delay += random.uniform(0.05, 0.15) time.sleep(delay) i += 1 def _get_typo_char(self, char: str) -> str | None: """Возвращает реалистичную опечатку для символа""" char_lower = char.lower() # Используем соседние клавиши if char_lower in self.NEARBY_KEYS: nearby = self.NEARBY_KEYS[char_lower] typo = random.choice(nearby) # Сохраняем регистр return typo.upper() if char.isupper() else typo # Для других символов - случайная буква if char.isalpha(): typo = random.choice(string.ascii_lowercase) return typo.upper() if char.isupper() else typo return None def think_before_typing(self, field_type: str = 'default'): """ Пауза "на подумать" перед вводом. Разная длительность для разных типов полей. """ if field_type == 'password': # Вспоминаем пароль time.sleep(random.uniform(0.8, 2.0)) elif field_type == 'code': # Смотрим на код в письме/SMS time.sleep(random.uniform(1.0, 2.5)) elif field_type == 'email': # Email обычно помним хорошо time.sleep(random.uniform(0.2, 0.5)) else: time.sleep(random.uniform(0.3, 0.8)) def human_click(self, element, pre_delay: bool = True): """Кликает с человеческой задержкой""" if pre_delay: self.human_delay(0.2, 0.5) element.click() self.human_delay(0.1, 0.3) def human_js_click(self, page, element, pre_delay: bool = True): """Кликает через JS с человеческой задержкой и скроллом""" if pre_delay: self.human_delay(0.15, 0.4) try: # Скроллим к элементу плавно page.run_js(''' arguments[0].scrollIntoView({behavior: "smooth", block: "center"}); ''', element) self.human_delay(0.1, 0.25) # Клик page.run_js('arguments[0].click()', element) except: try: element.click() except: pass self.human_delay(0.1, 0.3) def random_mouse_movement(self, browser, count: int = None): """ Случайные движения мыши по странице. Args: browser: BrowserAutomation instance (должен иметь .page) count: Количество движений """ if count is None: count = random.randint(2, 5) try: for _ in range(count): x = random.randint(100, 800) y = random.randint(100, 600) browser.page.run_js(f''' const event = new MouseEvent('mousemove', {{ clientX: {x}, clientY: {y}, bubbles: true }}); document.dispatchEvent(event); ''') time.sleep(random.uniform(0.1, 0.3)) except Exception: pass def scroll_page(self, browser, direction: str = 'down', amount: int = None): """ Прокручивает страницу. Args: browser: BrowserAutomation instance direction: 'up' или 'down' amount: Количество пикселей """ if amount is None: amount = random.randint(100, 400) if direction == 'up': amount = -amount try: browser.page.run_js(f'window.scrollBy(0, {amount});') self.human_delay(0.2, 0.5) except Exception: pass def bezier_mouse_move(self, page, start_x: int, start_y: int, end_x: int, end_y: int, steps: int = 20): """ Движение мыши по кривой Безье (более реалистично). Args: page: DrissionPage page instance start_x, start_y: Начальная позиция end_x, end_y: Конечная позиция steps: Количество шагов """ import math # Контрольные точки для кривой Безье # Добавляем случайное отклонение ctrl1_x = start_x + (end_x - start_x) * 0.3 + random.randint(-50, 50) ctrl1_y = start_y + (end_y - start_y) * 0.1 + random.randint(-30, 30) ctrl2_x = start_x + (end_x - start_x) * 0.7 + random.randint(-50, 50) ctrl2_y = start_y + (end_y - start_y) * 0.9 + random.randint(-30, 30) def bezier(t, p0, p1, p2, p3): """Кубическая кривая Безье""" return ( (1-t)**3 * p0 + 3 * (1-t)**2 * t * p1 + 3 * (1-t) * t**2 * p2 + t**3 * p3 ) try: for i in range(steps + 1): t = i / steps x = int(bezier(t, start_x, ctrl1_x, ctrl2_x, end_x)) y = int(bezier(t, start_y, ctrl1_y, ctrl2_y, end_y)) page.run_js(f''' const event = new MouseEvent('mousemove', {{ clientX: {x}, clientY: {y}, bubbles: true }}); document.dispatchEvent(event); ''') # Случайная задержка между шагами time.sleep(random.uniform(0.005, 0.02)) except Exception: pass def human_click_with_movement(self, page, element, from_pos: tuple = None): """ Кликает по элементу с реалистичным движением мыши. Args: page: DrissionPage page instance element: Элемент для клика from_pos: Начальная позиция (x, y), если None - случайная """ try: # Получаем позицию элемента rect = page.run_js(''' const rect = arguments[0].getBoundingClientRect(); return {x: rect.x + rect.width/2, y: rect.y + rect.height/2}; ''', element) end_x = int(rect['x']) end_y = int(rect['y']) # Начальная позиция if from_pos: start_x, start_y = from_pos else: start_x = random.randint(100, 800) start_y = random.randint(100, 600) # Движение к элементу self.bezier_mouse_move(page, start_x, start_y, end_x, end_y) # Небольшая пауза перед кликом time.sleep(random.uniform(0.05, 0.15)) # Клик element.click() return (end_x, end_y) # Возвращаем позицию для следующего движения except Exception as e: # Fallback на обычный клик element.click() return None # ======================================================================== # ADVANCED HUMAN SIMULATION # ======================================================================== def simulate_page_reading(self, page, duration: float = None): """ Симулирует чтение страницы: движения глаз (мыши), скролл, паузы. Args: page: DrissionPage instance duration: Длительность симуляции (None = случайная 2-5 сек) """ if duration is None: duration = random.uniform(2.0, 5.0) start_time = time.time() while time.time() - start_time < duration: action = random.choice(['mouse_move', 'scroll', 'pause']) if action == 'mouse_move': # Движение мыши как при чтении (сверху вниз, слева направо) x = random.randint(200, 900) y = random.randint(150, 500) try: page.run_js(f''' document.dispatchEvent(new MouseEvent('mousemove', {{ clientX: {x}, clientY: {y}, bubbles: true }})); ''') except: pass time.sleep(random.uniform(0.1, 0.3)) elif action == 'scroll': # Небольшой скролл scroll_amount = random.randint(50, 150) direction = random.choice([1, -1]) try: page.run_js(f'window.scrollBy(0, {scroll_amount * direction});') except: pass time.sleep(random.uniform(0.2, 0.5)) else: # pause time.sleep(random.uniform(0.3, 0.8)) def simulate_form_hesitation(self, page): """ Симулирует колебание перед заполнением формы. Человек обычно осматривает форму перед вводом. """ # Движения мыши по форме form_positions = [ (300, 200), (500, 200), (300, 300), (500, 300), (400, 400) ] for x, y in random.sample(form_positions, k=random.randint(2, 4)): x += random.randint(-30, 30) y += random.randint(-30, 30) try: page.run_js(f''' document.dispatchEvent(new MouseEvent('mousemove', {{ clientX: {x}, clientY: {y}, bubbles: true }})); ''') except: pass time.sleep(random.uniform(0.1, 0.25)) # Пауза "на подумать" time.sleep(random.uniform(0.3, 0.8)) def simulate_distraction(self, page, probability: float = 0.15): """ Симулирует отвлечение пользователя (с заданной вероятностью). Человек иногда отвлекается во время заполнения форм. Args: page: DrissionPage instance probability: Вероятность отвлечения (0.0 - 1.0) """ if random.random() > probability: return distraction_type = random.choice(['long_pause', 'scroll_away', 'mouse_wander']) if distraction_type == 'long_pause': # Просто долгая пауза (отвлёкся на телефон/чат) time.sleep(random.uniform(2.0, 5.0)) elif distraction_type == 'scroll_away': # Скролл в сторону и обратно try: page.run_js(f'window.scrollBy(0, {random.randint(100, 300)});') time.sleep(random.uniform(0.5, 1.5)) page.run_js(f'window.scrollBy(0, {random.randint(-300, -100)});') except: pass time.sleep(random.uniform(0.3, 0.6)) elif distraction_type == 'mouse_wander': # Мышь уходит в угол экрана corners = [(50, 50), (1200, 50), (50, 700), (1200, 700)] corner = random.choice(corners) try: page.run_js(f''' document.dispatchEvent(new MouseEvent('mousemove', {{ clientX: {corner[0]}, clientY: {corner[1]}, bubbles: true }})); ''') except: pass time.sleep(random.uniform(1.0, 3.0)) def hover_before_click(self, page, element, duration: float = None): """ Наводит мышь на элемент перед кликом (как реальный пользователь). Args: page: DrissionPage instance element: Элемент для наведения duration: Время наведения перед кликом """ if duration is None: duration = random.uniform(0.1, 0.4) try: # Получаем позицию элемента rect = page.run_js(''' const rect = arguments[0].getBoundingClientRect(); return { x: rect.x + rect.width/2 + (Math.random() - 0.5) * rect.width * 0.3, y: rect.y + rect.height/2 + (Math.random() - 0.5) * rect.height * 0.3 }; ''', element) # Hover event page.run_js(f''' const el = arguments[0]; el.dispatchEvent(new MouseEvent('mouseenter', {{ bubbles: true }})); el.dispatchEvent(new MouseEvent('mouseover', {{ bubbles: true }})); ''', element) time.sleep(duration) except: pass def natural_scroll_to_element(self, page, element): """ Плавно скроллит к элементу (не мгновенно). Args: page: DrissionPage instance element: Элемент к которому скроллить """ try: # Получаем текущую позицию и позицию элемента positions = page.run_js(''' const el = arguments[0]; const rect = el.getBoundingClientRect(); return { currentY: window.scrollY, targetY: window.scrollY + rect.top - window.innerHeight / 3, viewportHeight: window.innerHeight }; ''', element) current_y = positions['currentY'] target_y = positions['targetY'] # Скроллим пошагово distance = target_y - current_y steps = max(5, abs(int(distance / 50))) for i in range(steps): progress = (i + 1) / steps # Easing function (ease-out) eased = 1 - (1 - progress) ** 2 new_y = current_y + distance * eased page.run_js(f'window.scrollTo(0, {int(new_y)});') time.sleep(random.uniform(0.02, 0.05)) # Финальная позиция page.run_js(f'window.scrollTo(0, {int(target_y)});') time.sleep(random.uniform(0.1, 0.2)) except Exception: # Fallback на обычный скролл try: page.run_js('arguments[0].scrollIntoView({behavior: "smooth", block: "center"});', element) time.sleep(0.3) except: pass def random_micro_movements(self, page, count: int = None): """ Микро-движения мыши (тремор руки, небольшие корректировки). Делает поведение более человечным. Args: page: DrissionPage instance count: Количество микро-движений """ if count is None: count = random.randint(3, 8) try: # Получаем текущую позицию (примерно центр экрана) base_x = random.randint(400, 800) base_y = random.randint(300, 500) for _ in range(count): # Небольшое отклонение (1-5 пикселей) dx = random.randint(-5, 5) dy = random.randint(-5, 5) page.run_js(f''' document.dispatchEvent(new MouseEvent('mousemove', {{ clientX: {base_x + dx}, clientY: {base_y + dy}, bubbles: true }})); ''') time.sleep(random.uniform(0.02, 0.08)) base_x += dx base_y += dy except: pass # ======================================================================== # FWCIM-COMPATIBLE INPUT (Critical for AWS detection bypass) # ======================================================================== def fwcim_type(self, page, element, text: str, field_type: str = 'default', fast: bool = False): """ Печатает текст с генерацией правильных событий для FWCIM. FWCIM собирает: - keyCycles: [{startEventTime, endEventTime, which}, ...] - время удержания клавиши - keyPressTimeIntervals: время между нажатиями - keyPresses: количество нажатий Args: page: DrissionPage instance element: Элемент для ввода text: Текст для ввода field_type: Тип поля ('email', 'password', 'name', 'code', 'default') fast: Быстрый режим (меньше задержек, но всё ещё генерирует события) """ # Получаем настройки для типа поля field_config = self.FIELD_SPEEDS.get(field_type, self.FIELD_SPEEDS['default']) delay_range = field_config['delay'] # В быстром режиме уменьшаем задержки if fast: delay_range = (0.01, 0.03) # Пауза "на подумать" перед вводом (только в медленном режиме) if not fast and field_type in ('password', 'code'): self.think_before_typing(field_type) # Фокус на элементе с правильными событиями self.fwcim_focus(page, element) time.sleep(random.uniform(0.05, 0.1) if fast else random.uniform(0.1, 0.2)) # Очищаем поле через select + delete (работает с React) page.run_js(''' const el = arguments[0]; el.select(); ''', element) time.sleep(0.02) # Генерируем Delete/Backspace для очистки (FWCIM видит это) page.run_js(''' const el = arguments[0]; if (el.value) { el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', code: 'Delete', keyCode: 46, which: 46, bubbles: true })); el.value = ''; el.dispatchEvent(new InputEvent('input', { inputType: 'deleteContentBackward', bubbles: true })); el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Delete', code: 'Delete', keyCode: 46, which: 46, bubbles: true })); } ''', element) time.sleep(0.02) for i, char in enumerate(text): # Случайная пауза "на подумать" в середине ввода (только в медленном режиме) if not fast and random.random() < 0.03 and i > 0 and i < len(text) - 1: time.sleep(random.uniform(0.3, 0.8)) # Генерируем keydown -> keypress -> input -> keyup self._dispatch_key_events(page, element, char, fast=fast) # Задержка между символами (inter-key interval) delay = random.uniform(*delay_range) # Дополнительная пауза после определённых символов (только в медленном режиме) if not fast: if char in '.,!?@': delay += random.uniform(0.1, 0.3) elif char == ' ': delay += random.uniform(0.05, 0.15) time.sleep(delay) def _dispatch_key_events(self, page, element, char: str, fast: bool = False): """ Генерирует полную последовательность событий клавиши для FWCIM. Последовательность: keydown -> (hold time) -> keyup FWCIM записывает startEventTime (keydown) и endEventTime (keyup) ВАЖНО: Использует нативный setter для совместимости с React! """ # Время удержания клавиши (50-150ms для обычного набора, меньше в быстром режиме) hold_time = random.uniform(0.02, 0.05) if fast else random.uniform(0.05, 0.15) # Получаем keyCode для символа key_code = ord(char.upper()) if char.isalpha() else ord(char) # Определяем правильный code для клавиши if char.isalpha(): code = f'Key{char.upper()}' elif char.isdigit(): code = f'Digit{char}' elif char == ' ': code = 'Space' elif char == '@': code = 'Digit2' # Shift+2 на US keyboard elif char == '.': code = 'Period' elif char == '-': code = 'Minus' elif char == '_': code = 'Minus' # Shift+Minus else: code = f'Key{char}' # keydown + keypress + input (всё в одном JS вызове для атомарности) page.run_js(''' const el = arguments[0]; const char = arguments[1]; const keyCode = arguments[2]; const code = arguments[3]; // keydown el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: code, keyCode: keyCode, which: keyCode, bubbles: true, cancelable: true })); // keypress (deprecated but FWCIM may still listen) el.dispatchEvent(new KeyboardEvent('keypress', { key: char, charCode: char.charCodeAt(0), keyCode: keyCode, which: keyCode, bubbles: true, cancelable: true })); // КРИТИЧНО: Используем нативный setter для React-совместимости // React перехватывает setter и обновляет state const nativeInputValueSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value' ).set; nativeInputValueSetter.call(el, el.value + char); // Input event (React слушает это) el.dispatchEvent(new InputEvent('input', { data: char, inputType: 'insertText', bubbles: true, cancelable: true })); ''', element, char, key_code, code) # Hold time (FWCIM measures this!) time.sleep(hold_time) # keyup event page.run_js(''' const el = arguments[0]; const char = arguments[1]; const keyCode = arguments[2]; const code = arguments[3]; el.dispatchEvent(new KeyboardEvent('keyup', { key: char, code: code, keyCode: keyCode, which: keyCode, bubbles: true, cancelable: true })); ''', element, char, key_code, code) def fwcim_click(self, page, element): """ Кликает с генерацией правильных событий для FWCIM. FWCIM собирает: - mouseCycles: [{startEventTime, endEventTime}, ...] - время удержания клика - mouseClickPositions: позиции кликов - clicks: количество кликов """ # Время удержания кнопки мыши (80-200ms для обычного клика) hold_time = random.uniform(0.08, 0.2) # Получаем позицию элемента для реалистичного клика try: rect = page.run_js(''' const el = arguments[0]; const rect = el.getBoundingClientRect(); // Случайная позиция внутри элемента return { x: rect.left + rect.width * (0.3 + Math.random() * 0.4), y: rect.top + rect.height * (0.3 + Math.random() * 0.4) }; ''', element) client_x = int(rect['x']) client_y = int(rect['y']) except: client_x = 400 client_y = 300 # mousedown event page.run_js(''' const el = arguments[0]; const x = arguments[1]; const y = arguments[2]; el.dispatchEvent(new MouseEvent('mousedown', { clientX: x, clientY: y, button: 0, buttons: 1, bubbles: true, cancelable: true })); ''', element, client_x, client_y) # Hold time (FWCIM measures this!) time.sleep(hold_time) # mouseup event page.run_js(''' const el = arguments[0]; const x = arguments[1]; const y = arguments[2]; el.dispatchEvent(new MouseEvent('mouseup', { clientX: x, clientY: y, button: 0, buttons: 0, bubbles: true, cancelable: true })); // click event (follows mouseup) el.dispatchEvent(new MouseEvent('click', { clientX: x, clientY: y, button: 0, bubbles: true, cancelable: true })); ''', element, client_x, client_y) def fwcim_focus(self, page, element): """ Фокусируется на элементе с правильными событиями для FWCIM. FWCIM отслеживает: - firstFocusTime: время первого фокуса - totalFocusTime: общее время фокуса """ page.run_js(''' const el = arguments[0]; // focusin event (bubbles) el.dispatchEvent(new FocusEvent('focusin', { bubbles: true, cancelable: false })); // focus event (doesn't bubble) el.dispatchEvent(new FocusEvent('focus', { bubbles: false, cancelable: false })); // Actually focus the element el.focus(); ''', element)