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:
1 parent
fdee12e2d8
commit
4d90dc4a21
7 files changed
+67
-19
No files matched your search
+1
-1
@@ -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
|
||||
@@ -9,3 +9,4 @@ build/
|
||||
client/dist/
|
||||
client/build/
|
||||
client/src/*.egg-info/
|
||||
http-client.private.env.json
|
||||
+3
-1
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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')]"
|
||||
}
|
||||
|
||||
###
|
||||
Reference in new issue
Block a user