fix: добавление обхода для recaptcha
This commit is contained in:
1 parent
b77f25d3cd
commit
4cc068a616
5 files changed
+130
-49
No files matched your search
@@ -0,0 +1 @@
|
||||
from .known_providers import recaptcha
|
||||
+35
-1
@@ -6,12 +6,46 @@
|
||||
- селектор со страницы (достаточно общий чтобы покрыть весь сайт, но не страницу антибот защиты)
|
||||
- ключ сервиса распознавания капчи
|
||||
"""
|
||||
from copy import deepcopy
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Type
|
||||
|
||||
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):
|
||||
|
||||
@@ -2,7 +2,9 @@ import logging
|
||||
|
||||
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 ..registry import register
|
||||
from ..exceptions import IframeLoadError
|
||||
@@ -14,62 +16,110 @@ log.addHandler(logging.StreamHandler())
|
||||
|
||||
class ReCaptchaSolver(Solver):
|
||||
|
||||
def __init__(self, captcha_solver) -> None:
|
||||
super().__init__(captcha_solver=captcha_solver)
|
||||
self.captcha_solver = self._opt["captcha_solver"]
|
||||
def __init__(self, captcha_solver: Captcha, **kwargs) -> None:
|
||||
super().__init__(captcha_solver=captcha_solver, **kwargs)
|
||||
self.captcha_solver: Captcha = self._opt["captcha_solver"]
|
||||
|
||||
async def solve(self, page: Page, info: ChallengeInfo):
|
||||
log.debug("Начинаю решение recaptcha антибот преграды")
|
||||
if info.stage == Stage.INTERSTITIAL:
|
||||
await page.wait_for_timeout(15)
|
||||
return
|
||||
elif info.stage == Stage.CHECKBOX:
|
||||
log.debug("Нужно тыкнуть чекбокс")
|
||||
checkbox_selector = "#recaptcha-anchor"
|
||||
element = page.locator(checkbox_selector)
|
||||
async with page.expect_response("**/recaptcha/enterprise/reload**", timeout=30_000):
|
||||
await element.click()
|
||||
element = page.frame_locator("iframe[title='reCAPTCHA']").locator(checkbox_selector)
|
||||
interactive_marker_selector = "xpath=//div[@class='g-recaptcha-bubble-arrow'][1]"
|
||||
challenge_locator = page.locator(interactive_marker_selector)
|
||||
result = await click_and_race(page, element, challenge_locator)
|
||||
log.debug(f"Ожидание завершено: {result}")
|
||||
log.debug("Возврат к детекту")
|
||||
return
|
||||
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():
|
||||
await element.click()
|
||||
|
||||
self.captcha_solver.recaptcha(page)
|
||||
# TODO: execute js
|
||||
log.debug("Решаем капчу...")
|
||||
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):
|
||||
|
||||
async def detect(self, page: Page, info: ChallengeInfo):
|
||||
log.debug("ReCaptchaDetector: попытка определить есть ли преграда")
|
||||
info.meta = {}
|
||||
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"
|
||||
if not await page.query_selector(marker_selector):
|
||||
log.debug("Page is not a challenge form")
|
||||
info.detect_locator = marker_selector
|
||||
|
||||
log.debug(f"ищем по detect: {marker_selector} success: {info.success_locator}")
|
||||
if await page.locator(marker_selector).count() == 0:
|
||||
log.debug("На странице нет маркера преграды")
|
||||
if info.success_locator:
|
||||
info.success = bool(await page.query_selector(info.success_locator))
|
||||
info.meta["reason"] = [
|
||||
"Success locator not present and marker too.",
|
||||
"Marker not present but success locator is it!"
|
||||
"Нет маркера что тут преграда и нет маркера что это искомая страница",
|
||||
"Нет маркера что это преграда, зато есть маркер что это нужная страница"
|
||||
][info.success]
|
||||
else:
|
||||
info.success = True
|
||||
# noinspection unresolved-references
|
||||
info.meta["reason"] = "Page is not a challenge form"
|
||||
info.meta["reason"] = "нет маркера на это указывающего"
|
||||
log.debug(f"Решение не требуется, потому что: {info.meta['reason']}")
|
||||
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")
|
||||
# noinspection unresolved-references
|
||||
info.meta["reason"] = "Checkbox not present"
|
||||
raise IframeLoadError("Checkbox not present")
|
||||
else:
|
||||
# нужно сделать клик
|
||||
log.debug("Чек бокс найден")
|
||||
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
|
||||
else:
|
||||
log.debug("Попробую просто кликнуть")
|
||||
|
||||
return
|
||||
|
||||
|
||||
+24
-28
@@ -1,16 +1,8 @@
|
||||
"""
|
||||
|
||||
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"]
|
||||
|
||||
|
||||
Модуль для решения капчи от reCaptcha.
|
||||
"""
|
||||
import logging
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
from .base import Captcha, Page
|
||||
|
||||
@@ -23,31 +15,35 @@ class SolverReCaptcha(Captcha):
|
||||
ctype = "recaptcha"
|
||||
|
||||
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)
|
||||
await div_container.wait_for()
|
||||
|
||||
if not div_container:
|
||||
return None
|
||||
|
||||
site_key = await div_container.get_attribute("data-sitekey")
|
||||
is_invisible = bool(await div_container.get_attribute("data-invisible"))
|
||||
site_key_input_element = page.locator("iframe[title='reCAPTCHA']")
|
||||
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)")
|
||||
await iframe.wait_for()
|
||||
textarea_id_postfix = await iframe.get_attribute("data-hcaptcha-widget-id")
|
||||
textarea_element = page.locator("#g-recaptcha-response")
|
||||
|
||||
textarea_g = await page.query_selector(f"#g-recaptcha-response-{textarea_id_postfix}")
|
||||
textarea_h = await page.query_selector(f"#h-captcha-response-{textarea_id_postfix}")
|
||||
code = await self.recaptcha(page.url, site_key)
|
||||
|
||||
code = await self.recaptcha(page.url, site_key, is_invisible)
|
||||
await textarea_g.evaluate((
|
||||
"(el, value) => {"
|
||||
"el.value = value;"
|
||||
"el.dispatchEvent(new Event('input', {bubbles: true}));"
|
||||
"}"
|
||||
), code)
|
||||
await textarea_h.evaluate((
|
||||
await textarea_element.evaluate((
|
||||
"(el, value) => {"
|
||||
"el.value = value;"
|
||||
"el.dispatchEvent(new Event('input', {bubbles: true}));"
|
||||
@@ -55,7 +51,7 @@ class SolverReCaptcha(Captcha):
|
||||
), code)
|
||||
return None
|
||||
|
||||
async def recaptcha(self, url, site_key, is_invisible):
|
||||
resp = self._solver.recaptcha(site_key, url, invisible=is_invisible)
|
||||
async def recaptcha(self, url, site_key):
|
||||
resp = self._solver.recaptcha(site_key, url)
|
||||
logger.debug(f"recaptcha response: {resp}")
|
||||
return resp["code"]
|
||||
@@ -113,4 +113,4 @@ async def execute(
|
||||
if result:
|
||||
new_line["result"] = result
|
||||
actions_result.append(new_line)
|
||||
return actions_result
|
||||
return actions_result if len(actions_result) else [{"status": "pass"}]
|
||||
Reference in new issue
Block a user