fix: добавление обхода для recaptcha

This commit is contained in:
protokey committed 2026-09-18 19:15:44 +04:00
1 parent b77f25d3cd
commit 4cc068a616
5 files changed
+130 -49

No files matched your search

+1
View File
@@ -0,0 +1 @@
from .known_providers import recaptcha
+35 -1
View File
@@ -6,12 +6,46 @@
- селектор со страницы (достаточно общий чтобы покрыть весь сайт, но не страницу антибот защиты) - селектор со страницы (достаточно общий чтобы покрыть весь сайт, но не страницу антибот защиты)
- ключ сервиса распознавания капчи - ключ сервиса распознавания капчи
""" """
from copy import deepcopy import asyncio
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Type from typing import Type
from playwright.async_api import Page from playwright.async_api import Page
from playwright.async_api import TimeoutError as PWTimeoutError
async def click_and_race(page, element, challenge_locator, timeout=30_000):
nav_task = asyncio.create_task(
page.wait_for_event(
"framenavigated",
predicate=lambda frame: frame == page.main_frame,
timeout=timeout,
)
)
challenge_task = asyncio.create_task(
challenge_locator.wait_for(state="attached", timeout=timeout)
)
await element.click()
done, pending = await asyncio.wait(
{nav_task, challenge_task}, return_when=asyncio.FIRST_COMPLETED
)
for t in pending:
t.cancel()
if nav_task in done and nav_task.exception() is None:
return "navigation"
if challenge_task in done and challenge_task.exception() is None:
return "challenge"
# оба протухли по таймауту / или упали с другой ошибкой
for t in done:
exc = t.exception()
if exc and not isinstance(exc, PWTimeoutError):
raise exc
raise PWTimeoutError("Ни навигация, ни challenge-iframe не появились вовремя")
class Stage(str, Enum): class Stage(str, Enum):
+69 -19
View File
@@ -2,7 +2,9 @@ import logging
from playwright.async_api import Page from playwright.async_api import Page
from ..base import Detector, ChallengeInfo, Solver, AntiBotProvider from captcha.base import Captcha
from ..base import Detector, ChallengeInfo, Solver, AntiBotProvider, click_and_race
from ..base import Stage from ..base import Stage
from ..registry import register from ..registry import register
from ..exceptions import IframeLoadError from ..exceptions import IframeLoadError
@@ -14,62 +16,110 @@ log.addHandler(logging.StreamHandler())
class ReCaptchaSolver(Solver): class ReCaptchaSolver(Solver):
def __init__(self, captcha_solver) -> None: def __init__(self, captcha_solver: Captcha, **kwargs) -> None:
super().__init__(captcha_solver=captcha_solver) super().__init__(captcha_solver=captcha_solver, **kwargs)
self.captcha_solver = self._opt["captcha_solver"] self.captcha_solver: Captcha = self._opt["captcha_solver"]
async def solve(self, page: Page, info: ChallengeInfo): async def solve(self, page: Page, info: ChallengeInfo):
log.debug("Начинаю решение recaptcha антибот преграды")
if info.stage == Stage.INTERSTITIAL: if info.stage == Stage.INTERSTITIAL:
await page.wait_for_timeout(15) await page.wait_for_timeout(15)
return
elif info.stage == Stage.CHECKBOX: elif info.stage == Stage.CHECKBOX:
log.debug("Нужно тыкнуть чекбокс")
checkbox_selector = "#recaptcha-anchor" checkbox_selector = "#recaptcha-anchor"
element = page.locator(checkbox_selector) element = page.frame_locator("iframe[title='reCAPTCHA']").locator(checkbox_selector)
async with page.expect_response("**/recaptcha/enterprise/reload**", timeout=30_000): interactive_marker_selector = "xpath=//div[@class='g-recaptcha-bubble-arrow'][1]"
await element.click() challenge_locator = page.locator(interactive_marker_selector)
result = await click_and_race(page, element, challenge_locator)
log.debug(f"Ожидание завершено: {result}")
log.debug("Возврат к детекту")
return
else: else:
element = page.locator("//div[@class='g-recaptcha-bubble-arrow']/../div[1]") log.debug("Нужно решать капчу. Закрою окошко с картинками.")
element = page.locator("xpath=//div[@class='g-recaptcha-bubble-arrow']/../div[1]")
if await element.is_visible(): if await element.is_visible():
await element.click() await element.click()
self.captcha_solver.recaptcha(page) log.debug("Решаем капчу...")
# TODO: execute js assert info.detect_locator
token = await self.captcha_solver.solve(info.detect_locator, page)
log.debug("Капча решена. Возбуждаем JS")
try:
async with page.expect_navigation(wait_until='load'):
await page.evaluate("""
try {
const clients = window.___grecaptcha_cfg && window.___grecaptcha_cfg.clients;
for (const cid in clients) {
for (const key in clients[cid]) {
const obj = clients[cid][key];
for (const prop in obj) {
const cb = obj[prop] && obj[prop].callback;
if (typeof cb === 'function') { cb(token); return; }
if (typeof cb === 'string' && typeof window[cb] === 'function') {
window[cb](token); return;
}
}
}
}
} catch (e) {}""", token)
except Exception as e:
log.warning("JS navigation error", exc_info=True)
log.debug("Все готово. Проверяем еще раз.")
return
class ReCaptchaDetector(Detector): class ReCaptchaDetector(Detector):
async def detect(self, page: Page, info: ChallengeInfo): async def detect(self, page: Page, info: ChallengeInfo):
log.debug("ReCaptchaDetector: попытка определить есть ли преграда")
info.meta = {} info.meta = {}
checkbox_selector = "#recaptcha-anchor" checkbox_selector = "#recaptcha-anchor"
interactive_marker_selector = "#rc-imageselect" interactive_marker_selector = "//div[@class='g-recaptcha-bubble-arrow']/..//iframe"
# попытка определить что мы вообще на странице блокировки # попытка определить что мы вообще на странице блокировки
marker_selector = info.detect_locator or "#captcha-form" marker_selector = info.detect_locator or "#captcha-form"
if not await page.query_selector(marker_selector): info.detect_locator = marker_selector
log.debug("Page is not a challenge form")
log.debug(f"ищем по detect: {marker_selector} success: {info.success_locator}")
if await page.locator(marker_selector).count() == 0:
log.debug("На странице нет маркера преграды")
if info.success_locator: if info.success_locator:
info.success = bool(await page.query_selector(info.success_locator)) info.success = bool(await page.query_selector(info.success_locator))
info.meta["reason"] = [ info.meta["reason"] = [
"Success locator not present and marker too.", "Нет маркера что тут преграда и нет маркера что это искомая страница",
"Marker not present but success locator is it!" "Нет маркера что это преграда, зато есть маркер что это нужная страница"
][info.success] ][info.success]
else: else:
info.success = True info.success = True
# noinspection unresolved-references # noinspection unresolved-references
info.meta["reason"] = "Page is not a challenge form" info.meta["reason"] = "нет маркера на это указывающего"
log.debug(f"Решение не требуется, потому что: {info.meta['reason']}")
return return
log.debug("Вижу что это страница с проверкой. Определяю состояние.")
iframe = page.frame_locator("iframe[title='reCAPTCHA']")
# ожидаем когда будет загружен чекбокс # ожидаем когда будет загружен чекбокс
if not await page.wait_for_selector(checkbox_selector, timeout=30_000):
checkbox = iframe.locator(checkbox_selector)
try:
await checkbox.wait_for(state="attached")
except Exception:
log.debug("Checkbox not present") log.debug("Checkbox not present")
# noinspection unresolved-references # noinspection unresolved-references
info.meta["reason"] = "Checkbox not present" info.meta["reason"] = "Checkbox not present"
raise IframeLoadError("Checkbox not present") raise IframeLoadError("Checkbox not present")
else: else:
# нужно сделать клик log.debug("Чек бокс найден")
info.stage = Stage.CHECKBOX info.stage = Stage.CHECKBOX
if await page.wait_for_selector(interactive_marker_selector, timeout=15_000): if await page.locator(interactive_marker_selector).count() > 0:
log.debug("Нужно решать капчу")
# нужно решение # нужно решение
info.stage = Stage.INTERACTIVE info.stage = Stage.INTERACTIVE
else:
log.debug("Попробую просто кликнуть")
return return
+24 -28
View File
@@ -1,16 +1,8 @@
""" """
Модуль для решения капчи от reCaptcha.
dl.formRow:nth-child(3) > dd:nth-child(2) > div:nth-child(1)
<div data-xf-init="h-captcha" data-sitekey="83433494-1c9f-446b-85c3-e27b8b7375e1" data-invisible="">
iframe[data-hcaptcha-widget-id="0dm47zf0gn8t"]
textarea[id="g-recaptcha-response-0dm47zf0gn8t"]
textarea[id="h-captcha-response-0dm47zf0gn8t"]
""" """
import logging import logging
from urllib.parse import urlparse, parse_qs
from .base import Captcha, Page from .base import Captcha, Page
@@ -23,31 +15,35 @@ class SolverReCaptcha(Captcha):
ctype = "recaptcha" ctype = "recaptcha"
async def solve(self, selector, page: Page): async def solve(self, selector, page: Page):
# TODO: реализовать решение капчи """
#recaptcha-token
<input type="hidden" id="recaptcha-token" value="03AFcWeA616y0mA8rDFLMgCol7SoloUIVOOwzuqE50zb1Ft93vP_i2o...">
#g-recaptcha-response
<textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid rgb(193, 193, 193); margin: 10px 25px; padding: 0px; resize: none; display: none;"></textarea>
:param selector:
:param page:
:return:
"""
div_container = page.locator(selector) div_container = page.locator(selector)
await div_container.wait_for() await div_container.wait_for()
if not div_container: if not div_container:
return None return None
site_key = await div_container.get_attribute("data-sitekey") site_key_input_element = page.locator("iframe[title='reCAPTCHA']")
is_invisible = bool(await div_container.get_attribute("data-invisible")) src_value = await site_key_input_element.get_attribute("src")
url_splitted = urlparse(src_value)
query_params = parse_qs(url_splitted.query)
site_key = query_params["k"][0]
iframe = div_container.locator("iframe:nth-child(1)") textarea_element = page.locator("#g-recaptcha-response")
await iframe.wait_for()
textarea_id_postfix = await iframe.get_attribute("data-hcaptcha-widget-id")
textarea_g = await page.query_selector(f"#g-recaptcha-response-{textarea_id_postfix}") code = await self.recaptcha(page.url, site_key)
textarea_h = await page.query_selector(f"#h-captcha-response-{textarea_id_postfix}")
code = await self.recaptcha(page.url, site_key, is_invisible) await textarea_element.evaluate((
await textarea_g.evaluate((
"(el, value) => {"
"el.value = value;"
"el.dispatchEvent(new Event('input', {bubbles: true}));"
"}"
), code)
await textarea_h.evaluate((
"(el, value) => {" "(el, value) => {"
"el.value = value;" "el.value = value;"
"el.dispatchEvent(new Event('input', {bubbles: true}));" "el.dispatchEvent(new Event('input', {bubbles: true}));"
@@ -55,7 +51,7 @@ class SolverReCaptcha(Captcha):
), code) ), code)
return None return None
async def recaptcha(self, url, site_key, is_invisible): async def recaptcha(self, url, site_key):
resp = self._solver.recaptcha(site_key, url, invisible=is_invisible) resp = self._solver.recaptcha(site_key, url)
logger.debug(f"recaptcha response: {resp}") logger.debug(f"recaptcha response: {resp}")
return resp["code"] return resp["code"]
+1 -1
View File
@@ -113,4 +113,4 @@ async def execute(
if result: if result:
new_line["result"] = result new_line["result"] = result
actions_result.append(new_line) actions_result.append(new_line)
return actions_result return actions_result if len(actions_result) else [{"status": "pass"}]