fix: reCAPTCHA — окно с картинками определяется по положению (#10)

- Маркер интерактивного челленджа: iframe bframe, контейнер которого не стоит
  за экраном (top: -10000px); click_and_race ждёт видимости, а не появления в DOM.
- Маркер наличия капчи берётся из исходного HTML (api.js / enterprise.js,
  .g-recaptcha, iframe чекбокса), проверка мгновенная, без ожидания iframe.
- SolverReCaptcha ждёт detect_locator в DOM, а не видимости (script невидим).
- test_main.http: секреты вынесены в http-client.private.env.json, запрос
  на стенд zennolab, исправлены XPath с @class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
protokeyandClaude Opus 5.5 committed 2026-10-14 00:30:29 +04:00
1 parent fdee12e2d8
commit 4d90dc4a21
7 files changed
+67 -19

No files matched your search

+1 -1
View File
@@ -12,5 +12,5 @@ tab_width = 4
[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}]
indent_size = 2
[{*.yaml,*.yml,environment.yaml,environment.yml}]
[{*.yaml,*.yml,*.http,environment.yaml,environment.yml}]
indent_size = 2
+1
View File
@@ -9,3 +9,4 @@ build/
client/dist/
client/build/
client/src/*.egg-info/
http-client.private.env.json
+3 -1
View File
@@ -23,8 +23,10 @@ async def click_and_race(page, element, challenge_locator, timeout=30_000):
timeout=timeout,
)
)
# challenge_locator должен совпадать только с открытым окном челленджа: элементы челленджа могут быть в DOM
# заранее (reCAPTCHA держит их за экраном), поэтому ждём видимости, а не появления
challenge_task = asyncio.create_task(
challenge_locator.wait_for(state="attached", timeout=timeout)
challenge_locator.first.wait_for(state="visible", timeout=timeout)
)
await element.click()
+18 -7
View File
@@ -12,6 +12,20 @@ from ..exceptions import IframeLoadError
log = logging.getLogger(__name__)
# iframe окна с картинками (bframe) вставляется в DOM вместе с чекбоксом, но его контейнер стоит за экраном:
# style="visibility: hidden; ... top: -10000px". Когда выпадает челлендж, контейнер переезжает на экран и
# становится видимым — локатор совпадает только с открытым окном
INTERACTIVE_MARKER_SELECTOR = (
"xpath=//iframe[contains(@src, '/bframe')][not(ancestor::div[contains(@style, 'top: -10000px')])]"
)
# признак reCAPTCHA, который есть уже в исходном HTML, до загрузки iframe с чекбоксом (он грузится долго):
# скрипт api.js / enterprise.js, контейнер .g-recaptcha (у render=explicit его может не быть) или сам iframe.
# #recaptcha-token не подходит — он внутри iframe чекбокса, с основной страницы его не видно
DETECT_MARKER_SELECTOR = (
"script[src*='/recaptcha/api.js'], script[src*='/recaptcha/enterprise.js'], "
".g-recaptcha, iframe[title='reCAPTCHA']"
)
JS_CHALLENGE_SUBMIT = """(token) => {
const clients = window.___grecaptcha_cfg && window.___grecaptcha_cfg.clients;
@@ -55,9 +69,7 @@ class ReCaptchaSolver(Solver):
log.debug("Нужно тыкнуть чекбокс")
checkbox_selector = "#recaptcha-anchor"
element = page.frame_locator("iframe[title='reCAPTCHA']").locator(checkbox_selector)
# TODO: маркер всегда присутствует
interactive_marker_selector = "//div[@class='g-recaptcha-bubble-arrow']/..//iframe"
challenge_locator = page.locator(interactive_marker_selector)
challenge_locator = page.locator(INTERACTIVE_MARKER_SELECTOR)
result = await click_and_race(page, element, challenge_locator)
log.debug(f"Ожидание завершено: {result}")
log.debug("Возврат к детекту")
@@ -87,10 +99,9 @@ class ReCaptchaDetector(Detector):
log.debug("ReCaptchaDetector: попытка определить есть ли преграда")
info.meta = {}
checkbox_selector = "#recaptcha-anchor"
interactive_marker_selector = "//div[@class='g-recaptcha-bubble-arrow']/..//iframe"
# попытка определить что мы вообще на странице блокировки
marker_selector = info.detect_locator or "#captcha-form"
marker_selector = info.detect_locator or DETECT_MARKER_SELECTOR
info.detect_locator = marker_selector
log.debug(f"ищем по detect: {marker_selector} success: {info.success_locator}")
@@ -111,7 +122,7 @@ class ReCaptchaDetector(Detector):
log.debug("Вижу что это страница с проверкой. Определяю состояние.")
iframe = page.frame_locator("iframe[title='reCAPTCHA']")
# ожидаем когда будет загружен чекбокс
# маркер есть — значит капча будет, ждём пока догрузится iframe с чекбоксом
info.stage = Stage.INTERSTITIAL
checkbox = iframe.locator(checkbox_selector)
@@ -126,7 +137,7 @@ class ReCaptchaDetector(Detector):
log.debug("Чек бокс найден")
info.stage = Stage.CHECKBOX
if await page.locator(interactive_marker_selector).count() > 0:
if await page.locator(INTERACTIVE_MARKER_SELECTOR).first.is_visible():
log.debug("Нужно решать капчу")
# нужно решение
info.stage = Stage.INTERACTIVE
+2 -2
View File
@@ -25,8 +25,8 @@ class SolverReCaptcha(Captcha):
:param page:
:return:
"""
div_container = page.locator(selector)
await div_container.wait_for()
# selector может указывать на невидимый элемент (script) и совпадать с несколькими
await page.locator(selector).first.wait_for(state="attached")
site_key_input_element = page.locator("iframe[title='reCAPTCHA']")
src_value = await site_key_input_element.get_attribute("src")
@@ -0,0 +1,7 @@
{
"dev": {
"api_key": "rbx_...",
"proxy": "http://user:pass@host:port",
"captcha_key": "<ключ 2captcha>"
}
}
+35 -8
View File
@@ -1,4 +1,6 @@
# Test your FastAPI endpoints
# Секреты (api_key, proxy, captcha_key) — в tests/http-client.private.env.json (не в git, шаблон —
# http-client.private.env.example.json); окружение dev выбирается в PyCharm над запросом.
GET http://127.0.0.1:8000/
Accept: application/json
@@ -13,7 +15,7 @@ Accept: application/json
POST http://127.0.0.1:8000/v1/solve
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
{
@@ -36,7 +38,7 @@ X-API-Key: <key>
POST http://127.0.0.1:8000/v1/solve
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
@@ -53,7 +55,7 @@ Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
POST http://127.0.0.1:8000/v1/solve
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
@@ -72,7 +74,7 @@ Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
POST http://127.0.0.1:8000/v1/solve
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
@@ -91,7 +93,7 @@ Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
POST http://127.0.0.1:8000/v1/solve
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
{
@@ -112,11 +114,11 @@ X-API-Key: <key>
POST http://localhost:8002/v2/tasks
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
{
"url": "https://freelancehunt.com/",
"proxy": "http://BQLo3X:poqAWX@168.81.43.154:8000",
"proxy": "{{proxy}}",
"actions": [
{
"type": "inner_text",
@@ -132,6 +134,31 @@ X-API-Key: <key>
GET http://localhost:8002/v2/tasks/4d8105f8-93a8-43ee-aa6e-db917451363b
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
X-API-Key: {{api_key}}
###
# @timeout 600
POST http://localhost:8010/v1/solve
Accept: application/json
Content-Type: application/json
X-API-Key: {{api_key}}
{
"url": "https://lessons.zennolab.com/captchas/recaptcha/v2_nosubmit.php?level=low",
"proxy": "{{proxy}}",
"actions": [
{
"type": "pass_challenge",
"value": "recaptcha"
},
{
"type": "inner_text",
"selector": "//button[contains(@class, 'actionItem_151') and @type='submit']"
}
],
"captcha": "{{captcha_key}}",
"captcha_type": "recaptcha",
"captcha_success_locator": "//code[contains(@class, 'hljs _successMessageCode')]"
}
###