feature: переезд на хром

This commit is contained in:
protokey committed 2026-09-22 22:41:14 +04:00
1 parent 714cf9f922
commit f2d4d441f4
6 files changed
+355 -102

No files matched your search

+100 -27
View File
@@ -1,34 +1,11 @@
"""
человекоподобные движения мыши (безье+ускорение), тайминги кликов
Camoufox — это форк Firefox, заточенный под антидетект-автоматизацию (спуфинг fingerprint'а, WebGL, canvas,
шрифтов и т.д.), с Python-обёрткой поверх Playwright-протокола (Juggler).
У него есть параметр humanize при запуске:
```
from camoufox.sync_api import Camoufox
with Camoufox(humanize=True) as browser:
...
```
Или с указанием максимального времени на движение:
```
Camoufox(humanize=1.5) # курсор доберётся до цели не более чем за 1.5 сек
```
Когда humanize включён, при вызовах вроде page.click() / locator.click() библиотека:
- строит траекторию курсора от текущей позиции до цели не по прямой, а по кривой (используется подход,
похожий на ghost-cursor — Безье-путь с шумом),
- разбивает движение на промежуточные точки с нелинейной скоростью (ускорение/замедление к цели, как у реальной руки),
- добавляет небольшую случайную задержку перед самим кликом.
Это реализовано на уровне их собственного форка драйвера, а не через голый Playwright API — обычный Playwright (
page.click()) телепортирует курсор мгновенно и такого не делает.
Раньше это частично закрывал Camoufox (форк Firefox с движением курсора по кривой при humanize=True) —
после перехода на голый Playwright + Chromium это ушло: обычный page.click()/locator.click() телепортирует
курсор мгновенно. Функции ниже — единственный источник человекоподобности сейчас.
"""
import math
import random
import string
from typing import Optional
@@ -36,6 +13,102 @@ from typing import Optional
from playwright.async_api import TimeoutError
from playwright.async_api import Page
# последняя известная позиция курсора на страницу — Playwright не даёт её прочитать из браузера,
# поэтому считаем сами, чтобы следующее движение начиналось не из (0, 0), а оттуда, где реально
# остановились в прошлый раз
_last_mouse_pos: dict[int, tuple[float, float]] = {}
def _ease_in_out(t: float) -> float:
"""smoothstep: медленный старт, разгон к середине, торможение к цели — как у реальной руки."""
return t * t * (3 - 2 * t)
def _cubic_bezier(p0, c1, c2, p1, t: float) -> tuple[float, float]:
mt = 1 - t
x = mt ** 3 * p0[0] + 3 * mt ** 2 * t * c1[0] + 3 * mt * t ** 2 * c2[0] + t ** 3 * p1[0]
y = mt ** 3 * p0[1] + 3 * mt ** 2 * t * c1[1] + 3 * mt * t ** 2 * c2[1] + t ** 3 * p1[1]
return x, y
def _control_points(x0, y0, x1, y1):
"""Два контрольных узла, смещённых перпендикулярно линии движения — даёт дугу вместо прямой,
как у настоящего движения руки/трекпада, а не у синтетического teleport'а."""
dx, dy = x1 - x0, y1 - y0
dist = math.hypot(dx, dy) or 1.0
nx, ny = -dy / dist, dx / dist
curve = random.uniform(0.08, 0.28) * dist * random.choice((-1, 1))
t1 = random.uniform(0.2, 0.4)
t2 = random.uniform(0.6, 0.8)
c1 = (x0 + dx * t1 + nx * curve, y0 + dy * t1 + ny * curve)
c2 = (x0 + dx * t2 + nx * curve * random.uniform(0.5, 1.0), y0 + dy * t2 + ny * curve * random.uniform(0.5, 1.0))
return c1, c2
def _random_start_position(page: Page) -> tuple[float, float]:
vp = page.viewport_size or {"width": 1280, "height": 720}
return (
random.uniform(vp["width"] * 0.3, vp["width"] * 0.7),
random.uniform(vp["height"] * 0.3, vp["height"] * 0.7),
)
async def move_mouse_human(
page: Page,
x: float,
y: float,
*,
steps: Optional[int] = None,
min_duration: float = 0.25,
max_duration: float = 0.9,
) -> None:
"""Двигает курсор к (x, y) по кривой Безье с ease-in-out таймингом и лёгким дрожанием на
промежуточных точках, вместо page.mouse.move(x, y), которое телепортирует курсор мгновенно.
Начальная точка — последняя известная позиция курсора на этой странице (или случайная точка
у центра вьюпорта, если это первое движение в сессии)."""
start = _last_mouse_pos.get(id(page)) or _random_start_position(page)
c1, c2 = _control_points(start[0], start[1], x, y)
dist = math.hypot(x - start[0], y - start[1])
if steps is None:
steps = max(8, min(60, int(dist / 8)))
duration = random.uniform(min_duration, max_duration)
step_delay_ms = (duration / steps) * 1000
for i in range(1, steps + 1):
t = _ease_in_out(i / steps)
px, py = _cubic_bezier(start, c1, c2, (x, y), t)
if i < steps:
px += random.uniform(-1.5, 1.5)
py += random.uniform(-1.5, 1.5)
print("Move:", px, py)
await page.mouse.move(px, py)
await page.wait_for_timeout(step_delay_ms * random.uniform(0.7, 1.3))
_last_mouse_pos[id(page)] = (x, y)
async def click_human(
page: Page,
x: float,
y: float,
*,
button: str = "left",
**move_kwargs,
) -> None:
"""move_mouse_human + клик с человеческой задержкой между наведением и down/up, вместо
мгновенного page.mouse.click(x, y)."""
await move_mouse_human(page, x, y, **move_kwargs)
await page.wait_for_timeout(random.uniform(50, 180))
await page.mouse.down(button=button)
await page.wait_for_timeout(random.uniform(40, 99))
await page.mouse.up(button=button)
await move_mouse_human(page, x-50, y+50, **move_kwargs)
async def wait_for_networkidle(page: Page, timeout: Optional[int] = None):
"""Wait for the page to become idle (no network activity) even if there are never-ending requests."""