diff --git a/src/antibot/__init__.py b/src/antibot/__init__.py index e69de29..c08363c 100644 --- a/src/antibot/__init__.py +++ b/src/antibot/__init__.py @@ -0,0 +1 @@ +from .known_providers import recaptcha diff --git a/src/antibot/base.py b/src/antibot/base.py index be54198..8382a0e 100644 --- a/src/antibot/base.py +++ b/src/antibot/base.py @@ -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): diff --git a/src/antibot/known_providers/recaptcha.py b/src/antibot/known_providers/recaptcha.py index 6d2b396..ea0acdb 100644 --- a/src/antibot/known_providers/recaptcha.py +++ b/src/antibot/known_providers/recaptcha.py @@ -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 diff --git a/src/captcha/recaptcha.py b/src/captcha/recaptcha.py index 0ff8711..daebf7a 100644 --- a/src/captcha/recaptcha.py +++ b/src/captcha/recaptcha.py @@ -1,16 +1,8 @@ """ - -dl.formRow:nth-child(3) > dd:nth-child(2) > div:nth-child(1) -
- -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 + + + #g-recaptcha-response + + + :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"] diff --git a/src/engine/actions.py b/src/engine/actions.py index 7e74189..17f8328 100644 --- a/src/engine/actions.py +++ b/src/engine/actions.py @@ -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"}]