fix: reCAPTCHA — закрытие окна с картинками и вызов callback (#10)

- Окно с картинками закрывается кликом в угол подложки: в её центре лежит
  само окно и перехватывает клик (Locator.click падал по таймауту 60 с).
- form_submit выполняется в основном мире страницы (isolated_context=False):
  префикс "mw:" patchright не поддерживает, в изолированном мире
  ___grecaptcha_cfg не виден — "Recaptcha callback function not found".
- Воркер: LOG_LEVEL выставляется корневому логгеру явно — dramatiq настраивает
  логирование раньше, и basicConfig ничего не делал. make dev — LOG_LEVEL=DEBUG.
- test_main.http: запрос на zennolab с проверкой результата по #main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
protokeyandClaude Opus 5.5 committed 2026-10-14 01:17:06 +04:00
1 parent 4d90dc4a21
commit 0e10fb671d
5 files changed
+19 -13

No files matched your search

+2 -1
View File
@@ -33,7 +33,8 @@ export DEV_RABBITMQ_PORT DEV_REDIS_PORT DEV_POSTGRES_PORT RABBITMQ_PASSWORD POST
DEV_ENV := RABBITMQ_URL=amqp://robotex:$(RABBITMQ_PASSWORD)@localhost:$(DEV_RABBITMQ_PORT)/ \ DEV_ENV := RABBITMQ_URL=amqp://robotex:$(RABBITMQ_PASSWORD)@localhost:$(DEV_RABBITMQ_PORT)/ \
REDIS_URL=redis://localhost:$(DEV_REDIS_PORT)/0 \ REDIS_URL=redis://localhost:$(DEV_REDIS_PORT)/0 \
DATABASE_URL=postgresql+asyncpg://robotex:$(POSTGRES_PASSWORD)@localhost:$(DEV_POSTGRES_PORT)/robotex \ DATABASE_URL=postgresql+asyncpg://robotex:$(POSTGRES_PASSWORD)@localhost:$(DEV_POSTGRES_PORT)/robotex \
BROWSER_SLOTS=$(DEV_SLOTS) BROWSER_SLOTS=$(DEV_SLOTS) \
LOG_LEVEL=DEBUG
help: help:
@echo "make install <pkg> [<pkg> ...] - установить пакет(ы) в $(VENV), обновить $(REQUIREMENTS), собрать wheel в $(WHEELS_DIR)/" @echo "make install <pkg> [<pkg> ...] - установить пакет(ы) в $(VENV), обновить $(REQUIREMENTS), собрать wheel в $(WHEELS_DIR)/"
+10 -5
View File
@@ -1,4 +1,3 @@
import json
import logging import logging
from patchright.async_api import Page from patchright.async_api import Page
@@ -6,7 +5,7 @@ from patchright.async_api import Page
from captcha.base import Captcha from captcha.base import Captcha
from ..base import Detector, ChallengeInfo, Solver, AntiBotProvider, click_and_race from ..base import Detector, ChallengeInfo, Solver, AntiBotProvider, click_and_race
from ..base import Stage from ..base import Stage, PWTimeoutError
from ..registry import register from ..registry import register
from ..exceptions import IframeLoadError from ..exceptions import IframeLoadError
@@ -56,8 +55,8 @@ class ReCaptchaSolver(Solver):
self.captcha_solver: Captcha = self._opt["captcha_solver"] self.captcha_solver: Captcha = self._opt["captcha_solver"]
async def form_submit(self, token: str) -> bool: async def form_submit(self, token: str) -> bool:
# noinspection UnnecessaryLabelJS # ___grecaptcha_cfg живёт в основном мире страницы, а patchright по умолчанию выполняет скрипт в изолированном
return await self.page.evaluate(f"mw:({JS_CHALLENGE_SUBMIT})({json.dumps(token)})") return await self.page.evaluate(JS_CHALLENGE_SUBMIT, token, isolated_context=False)
async def solve(self, page: Page, info: ChallengeInfo): async def solve(self, page: Page, info: ChallengeInfo):
self.page = page self.page = page
@@ -76,9 +75,15 @@ class ReCaptchaSolver(Solver):
return return
else: else:
log.debug("Нужно решать капчу. Закрою окошко с картинками.") log.debug("Нужно решать капчу. Закрою окошко с картинками.")
# полупрозрачная подложка на весь экран (position: fixed), клик по ней закрывает окно. В её центре лежит
# само окно с картинками и перехватывает клик — поэтому кликаем в угол
element = page.locator("xpath=//div[@class='g-recaptcha-bubble-arrow']/../div[1]") 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() try:
await element.click(position={"x": 1, "y": 1}, timeout=5_000)
except PWTimeoutError:
# не закрылось — не страшно, токен уходит через callback
log.debug("Окошко с картинками закрыть не удалось")
log.debug("Решаем капчу...") log.debug("Решаем капчу...")
assert info.detect_locator assert info.detect_locator
+1
View File
@@ -30,6 +30,7 @@ class Captcha:
return resp return resp
async def turnstile(self, url, site_key): async def turnstile(self, url, site_key):
# TODO: not finished!
resp = await self._run(self._solver.turnstile, site_key, url) resp = await self._run(self._solver.turnstile, site_key, url)
return resp return resp
+4 -1
View File
@@ -10,10 +10,13 @@ import logging
import os import os
import socket import socket
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
logging.basicConfig( logging.basicConfig(
level=os.getenv("LOG_LEVEL", "INFO").upper(), level=LOG_LEVEL,
format="%(asctime)s %(levelname)s %(name)s: %(message)s", format="%(asctime)s %(levelname)s %(name)s: %(message)s",
) )
# под dramatiq логирование уже настроено им самим (уровень INFO) и basicConfig ничего не делает — уровень явно
logging.getLogger().setLevel(LOG_LEVEL)
import dramatiq # noqa: E402 import dramatiq # noqa: E402
from dramatiq.asyncio import get_event_loop_thread # noqa: E402 from dramatiq.asyncio import get_event_loop_thread # noqa: E402
+2 -6
View File
@@ -147,18 +147,14 @@ X-API-Key: {{api_key}}
"url": "https://lessons.zennolab.com/captchas/recaptcha/v2_nosubmit.php?level=low", "url": "https://lessons.zennolab.com/captchas/recaptcha/v2_nosubmit.php?level=low",
"proxy": "{{proxy}}", "proxy": "{{proxy}}",
"actions": [ "actions": [
{
"type": "pass_challenge",
"value": "recaptcha"
},
{ {
"type": "inner_text", "type": "inner_text",
"selector": "//button[contains(@class, 'actionItem_151') and @type='submit']" "selector": "//div[@id='main']/div/h2"
} }
], ],
"captcha": "{{captcha_key}}", "captcha": "{{captcha_key}}",
"captcha_type": "recaptcha", "captcha_type": "recaptcha",
"captcha_success_locator": "//code[contains(@class, 'hljs _successMessageCode')]" "captcha_success_locator": "//div[@id='main']"
} }
### ###