feature!: перенимаем реализацию прохождения cf_turnstile у scrapling

https://github.com/D4Vinci/Scrapling

BREAKING_CHANGES: клик отрабатывает, но прохода не случается
This commit is contained in:
protokey committed 2026-09-23 11:21:18 +04:00
1 parent f2d4d441f4
commit 8f7645b2b9
11 files changed
+193 -30

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
import asyncio
from playwright.async_api import Page
from patchright.async_api import Page
from antibot.base import click_and_race
from antibot.orchestrator import pass_challenges
+120
View File
@@ -0,0 +1,120 @@
"""
Launch/context-флаги для Chromium, снижающие детект автоматизации на уровне ниже
JS-фингерпринта (который уже покрывает browserforge, см. engine/fingerprint.py).
Портировано из scrapling.engines.constants / _browsers._base.BaseSessionMixin.
"""
HARMFUL_ARGS = (
# Playwright сам добавляет эти флаги по умолчанию — они одни из самых дешёвых
# и надёжных сигналов автоматизации, поэтому их нужно гасить через ignore_default_args
"--enable-automation",
"--disable-popup-blocking",
"--disable-component-update",
"--disable-default-apps",
"--disable-extensions",
)
DEFAULT_ARGS = (
"--no-pings",
"--no-first-run",
"--disable-infobars",
"--disable-breakpad",
"--no-service-autorun",
"--homepage=about:blank",
"--password-store=basic",
"--disable-hang-monitor",
"--no-default-browser-check",
"--disable-session-crashed-bubble",
"--disable-search-engine-choice-screen",
)
STEALTH_ARGS = (
"--test-type",
"--mute-audio",
"--disable-sync",
"--hide-scrollbars",
"--disable-logging",
"--start-maximized", # обход headless-детекта по размеру окна
"--enable-async-dns",
"--use-mock-keychain",
"--disable-translate",
"--disable-voice-input",
"--window-position=0,0",
"--disable-wake-on-wifi",
"--ignore-gpu-blocklist",
"--enable-tcp-fast-open",
"--enable-web-bluetooth",
"--disable-cloud-import",
"--disable-print-preview",
"--disable-dev-shm-usage",
"--metrics-recording-only",
"--disable-crash-reporter",
"--disable-partial-raster",
"--disable-gesture-typing",
"--disable-checker-imaging",
"--disable-prompt-on-repost",
"--force-color-profile=srgb",
"--font-render-hinting=none",
"--aggressive-cache-discard",
"--disable-cookie-encryption",
"--disable-domain-reliability",
"--disable-threaded-animation",
"--disable-threaded-scrolling",
"--enable-simple-cache-backend",
"--disable-background-networking",
"--enable-surface-synchronization",
"--disable-image-animation-resync",
"--disable-renderer-backgrounding",
"--disable-ipc-flooding-protection",
"--prerender-from-omnibox=disabled",
"--safebrowsing-disable-auto-update",
"--disable-offer-upload-credit-cards",
"--disable-background-timer-throttling",
"--disable-new-content-rendering-timeout",
"--run-all-compositor-stages-before-draw",
"--disable-client-side-phishing-detection",
"--disable-backgrounding-occluded-windows",
"--disable-layer-tree-host-memory-pressure",
"--autoplay-policy=user-gesture-required",
"--disable-offer-store-unmasked-wallet-cards",
"--disable-blink-features=AutomationControlled",
"--disable-component-extensions-with-background-pages",
"--enable-features=NetworkService,NetworkServiceInProcess,TrustTokens,TrustTokensAlwaysAllowIssuance",
"--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4",
"--disable-features=AudioServiceOutOfProcess,TranslateUI,BlinkGenPropertyTrees",
)
_WEBRTC_ARGS = (
"--webrtc-ip-handling-policy=disable_non_proxied_udp",
"--force-webrtc-ip-handling-policy",
)
def launch_args(locale: str | None = None, block_webrtc: bool = True) -> list[str]:
"""Флаги запуска Chromium: скорость + анти-детект + (опционально) защита от WebRTC-утечки
реального IP мимо прокси + (если передана locale) выставление языка браузера на уровне
лаунча, а не только через JS-контекст — иначе Web Workers/Intl внутри браузера остаются
на языке хоста, даже когда browserforge уже подменил navigator.language на странице."""
args = list(DEFAULT_ARGS) + list(STEALTH_ARGS)
if block_webrtc:
args += list(_WEBRTC_ARGS)
if locale:
base_lang = locale.split("-")[0].lower()
accept_lang = f"{locale},{base_lang}" if base_lang != locale.lower() else locale
args += [f"--lang={locale}", f"--accept-lang={accept_lang}"]
return args
def stealth_context_options() -> dict:
"""Context-опции, не пересекающиеся с тем, что уже выставляет browserforge
(user_agent/viewport/device_scale_factor) — их сюда специально не кладём, чтобы
fingerprint оставался единственным источником истины для этих полей."""
return {
"color_scheme": "dark", # обходит проверку prefersLightColor в creepjs
"is_mobile": False,
"has_touch": False,
"ignore_https_errors": True,
}
+31 -8
View File
@@ -18,13 +18,32 @@ import time
from pathlib import Path
import httpx
from playwright.async_api import ProxySettings
from patchright.async_api import ProxySettings
log = logging.getLogger(__name__)
IPINFO_URL = "https://ipinfo.io/json"
DEFAULT_CACHE_FILE = Path(__file__).resolve().parent.parent.parent / "extra" / "geoip_cache.json"
DEFAULT_TTL = 24 * 3600 # прокси может сменить exit-IP — не кэшируем навечно
DEFAULT_LOCALE = "en-US"
# Страна exit-IP -> основная локаль. Раньше это (как и timezone/geolocation) под капотом
# делал Camoufox(geoip=True); теперь резолвим сами и прокидываем в browserforge (locale
# согласованно выставляет navigator.language/languages и Accept-Language header) и в
# launch-флаги Chromium --lang=/--accept-lang= (engine/browser_launch.py). Не более чем
# точечная таблица под реально используемые прокси-гео — не претендует на полноту ISO-3166.
_COUNTRY_LOCALE: dict[str, str] = {
"RU": "ru-RU", "BY": "ru-BY", "KZ": "ru-KZ", "UA": "uk-UA",
"US": "en-US", "GB": "en-GB", "CA": "en-CA", "AU": "en-AU", "IE": "en-IE", "IN": "en-IN",
"DE": "de-DE", "AT": "de-AT", "CH": "de-CH",
"FR": "fr-FR", "BE": "fr-BE",
"ES": "es-ES", "MX": "es-MX", "AR": "es-AR",
"IT": "it-IT", "PT": "pt-PT", "BR": "pt-BR", "NL": "nl-NL",
"PL": "pl-PL", "CZ": "cs-CZ", "SK": "sk-SK", "RO": "ro-RO", "HU": "hu-HU",
"SE": "sv-SE", "NO": "nb-NO", "FI": "fi-FI", "DK": "da-DK",
"TR": "tr-TR", "GR": "el-GR",
"CN": "zh-CN", "TW": "zh-TW", "JP": "ja-JP", "KR": "ko-KR",
}
_cache_lock = asyncio.Lock()
@@ -59,14 +78,16 @@ async def resolve_geo(
cache_file: Path = DEFAULT_CACHE_FILE,
ttl: float = DEFAULT_TTL,
) -> dict | None:
"""Возвращает {"timezone_id", "geolocation": {"latitude", "longitude"}, "country", "ip"}
"""Возвращает {"timezone_id", "geolocation": {"latitude", "longitude"}, "country", "ip", "locale"}
по IP, который виден снаружи через прокси, или None, если резолв не удался и в кэше
тоже ничего нет — в этом случае вызывающий код просто не переопределяет timezone/geolocation.
тоже ничего нет — в этом случае вызывающий код просто не переопределяет timezone/geolocation/locale.
Язык (locale) сюда намеренно не подмешиваем: Accept-Language — это предпочтение пользователя,
а не производная от геолокации IP, и его уже согласованно генерирует fingerprint (см.
engine/fingerprint.py). Подменять его на "en-<COUNTRY>" было бы отдельным, никак не
связанным с реальным поведением допущением."""
"locale" — основная локаль страны exit-IP (см. _COUNTRY_LOCALE), с фолбэком DEFAULT_LOCALE
для неизвестных/отсутствующих кодов страны. Реальный человек с российским прокси не сидит
с английской раскладкой браузера, поэтому locale — такой же производный от IP сигнал, как
timezone/geolocation, а не независимое "предпочтение пользователя". Прокидывается вызывающим
кодом в browserforge (согласованно выставляет navigator.language/languages и Accept-Language,
см. engine/fingerprint.py) и в launch-флаги Chromium (engine/browser_launch.py)."""
key = proxy_settings_to_url(proxy)
async with _cache_lock:
@@ -98,11 +119,13 @@ async def resolve_geo(
except ValueError:
geolocation = None
country = data.get("country") # "RU", "US", ...
result = {
"ip": data.get("ip"),
"country": data.get("country"), # "RU", "US", ...
"country": country,
"timezone_id": data.get("timezone"),
"geolocation": geolocation,
"locale": _COUNTRY_LOCALE.get(country, DEFAULT_LOCALE),
}
async with _cache_lock: