From 4d90dc4a21618d4676799dd331ba51cc1a299d86 Mon Sep 17 00:00:00 2001 From: protokey Date: Wed, 14 Oct 2026 00:30:29 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20reCAPTCHA=20=E2=80=94=20=D0=BE=D0=BA?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=81=20=D0=BA=D0=B0=D1=80=D1=82=D0=B8=D0=BD?= =?UTF-8?q?=D0=BA=D0=B0=D0=BC=D0=B8=20=D0=BE=D0=BF=D1=80=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20=D0=BF=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D1=8E=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Маркер интерактивного челленджа: 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 --- .editorconfig | 2 +- .gitignore | 1 + src/antibot/base.py | 4 +- src/antibot/known_providers/recaptcha.py | 25 +++++++++---- src/captcha/recaptcha.py | 4 +- tests/http-client.private.env.example.json | 7 ++++ tests/test_main.http | 43 ++++++++++++++++++---- 7 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 tests/http-client.private.env.example.json diff --git a/.editorconfig b/.editorconfig index b33fd16..84ae73d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore index 030a8c6..c12fddf 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build/ client/dist/ client/build/ client/src/*.egg-info/ +http-client.private.env.json diff --git a/src/antibot/base.py b/src/antibot/base.py index 87f0849..8634c00 100644 --- a/src/antibot/base.py +++ b/src/antibot/base.py @@ -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() diff --git a/src/antibot/known_providers/recaptcha.py b/src/antibot/known_providers/recaptcha.py index 2a03f31..908fe0e 100644 --- a/src/antibot/known_providers/recaptcha.py +++ b/src/antibot/known_providers/recaptcha.py @@ -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 diff --git a/src/captcha/recaptcha.py b/src/captcha/recaptcha.py index 269992f..1b9fd3a 100644 --- a/src/captcha/recaptcha.py +++ b/src/captcha/recaptcha.py @@ -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") diff --git a/tests/http-client.private.env.example.json b/tests/http-client.private.env.example.json new file mode 100644 index 0000000..5aa441d --- /dev/null +++ b/tests/http-client.private.env.example.json @@ -0,0 +1,7 @@ +{ + "dev": { + "api_key": "rbx_...", + "proxy": "http://user:pass@host:port", + "captcha_key": "<ключ 2captcha>" + } +} diff --git a/tests/test_main.http b/tests/test_main.http index be5b8df..d36f82b 100644 --- a/tests/test_main.http +++ b/tests/test_main.http @@ -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: +X-API-Key: {{api_key}} { @@ -36,7 +38,7 @@ X-API-Key: POST http://127.0.0.1:8000/v1/solve Accept: application/json Content-Type: application/json -X-API-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: +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: +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: +X-API-Key: {{api_key}} { @@ -112,11 +114,11 @@ X-API-Key: POST http://localhost:8002/v2/tasks Accept: application/json Content-Type: application/json -X-API-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: GET http://localhost:8002/v2/tasks/4d8105f8-93a8-43ee-aa6e-db917451363b Accept: application/json Content-Type: application/json -X-API-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')]" +} ###