chore: добавлена поддержка решения капчи hcaptcha на формах

This commit is contained in:
protokey committed 2026-09-11 22:42:34 +04:00
1 parent 89b4dd2677
commit f5871710ab
9 files changed
+186 -32

No files matched your search

+3
View File
@@ -143,3 +143,6 @@ uvicorn app.main:app --reload
``` ```
## TODO: ## TODO:
-[ ] добавить пакет обертку для python
-[ ]
+18
View File
@@ -1,3 +1,5 @@
2captcha-python==2.1.1
aiofiles==25.1.0
aiohappyeyeballs==2.7.1 aiohappyeyeballs==2.7.1
aiohttp==3.14.3 aiohttp==3.14.3
aiosignal==1.4.0 aiosignal==1.4.0
@@ -12,26 +14,41 @@ camoufox==0.5.6
certifi==2026.7.22 certifi==2026.7.22
charset-normalizer==3.5.1 charset-normalizer==3.5.1
click==8.5.0 click==8.5.0
distro==1.9.0
editor==1.8.0 editor==1.8.0
fastapi==0.141.1 fastapi==0.141.1
frozenlist==1.8.0 frozenlist==1.8.0
geoip2==5.3.0 geoip2==5.3.0
greenlet==3.5.5 greenlet==3.5.5
h11==0.16.0 h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.19 idna==3.19
inquirer==3.4.1 inquirer==3.4.1
jinxed==2.1.0 jinxed==2.1.0
language-tags==1.3.1 language-tags==1.3.1
lxml==6.1.3 lxml==6.1.3
marionette_driver==3.7.1
markdown-it-py==4.2.0 markdown-it-py==4.2.0
maxminddb==3.1.1 maxminddb==3.1.1
mdurl==0.1.2 mdurl==0.1.2
mozdevice==4.2.0
mozfile==3.0.0
mozinfo==1.2.3
mozlog==8.1.0
mozprocess==1.4.0
mozprofile==3.0.0
mozrunner==8.4.0
mozsystemmonitor==1.0.1
mozterm==1.0.0
mozversion==2.4.0
multidict==6.7.1 multidict==6.7.1
numpy==2.5.3 numpy==2.5.3
orjson==3.12.0 orjson==3.12.0
platformdirs==4.11.7 platformdirs==4.11.7
playwright==1.62.0 playwright==1.62.0
propcache==0.5.2 propcache==0.5.2
psutil==7.2.2
pydantic==2.13.5 pydantic==2.13.5
pydantic_core==2.46.5 pydantic_core==2.46.5
pyee==13.0.1 pyee==13.0.1
@@ -49,6 +66,7 @@ rich-click==1.9.9
runs==1.3.0 runs==1.3.0
screeninfo==0.8.1 screeninfo==0.8.1
shiboken6==6.11.2 shiboken6==6.11.2
six==1.17.0
starlette==1.6.0 starlette==1.6.0
typing-inspection==0.4.4 typing-inspection==0.4.4
typing_extensions==4.16.0 typing_extensions==4.16.0
+2
View File
@@ -0,0 +1,2 @@
from .hcaptcha import SolverHCaptcha
from .base import load_captcha
+25
View File
@@ -0,0 +1,25 @@
from playwright.async_api import Page
from twocaptcha import TwoCaptcha
class Captcha:
ctype = "captcha"
def __init__(self, api_key: str):
self._api_key: str = api_key
self._solver = TwoCaptcha(api_key)
def get_balance(self) -> float:
return self._solver.balance()
async def solve(self, selector: str, page: Page):
raise NotImplementedError()
def load_captcha(key, captcha_type) -> Captcha:
for cls in Captcha.__subclasses__():
if cls.ctype == captcha_type:
break
else:
raise Exception("Invalid captcha type")
return cls(key)
+60
View File
@@ -0,0 +1,60 @@
"""
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"]
"""
import logging
from .base import Captcha, Page
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logger.addHandler(logging.StreamHandler())
class SolverHCaptcha(Captcha):
ctype = "hcaptcha"
async def solve(self, selector, page: Page):
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"))
iframe = div_container.locator("iframe:nth-child(1)")
await iframe.wait_for()
textarea_id_postfix = await iframe.get_attribute("data-hcaptcha-widget-id")
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.hcaptcha(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((
"(el, value) => {"
"el.value = value;"
"el.dispatchEvent(new Event('input', {bubbles: true}));"
"}"
), code)
return None
async def hcaptcha(self, url, site_key, is_invisible):
resp = self._solver.hcaptcha(site_key, url, invisible=is_invisible)
logger.debug(f"hcaptcha response: {resp}")
return resp["code"]
+21 -8
View File
@@ -2,6 +2,7 @@ import asyncio
from playwright.async_api import Page from playwright.async_api import Page
from captcha import load_captcha
from engine.schemas import ActionRequest from engine.schemas import ActionRequest
_actions: dict = {} _actions: dict = {}
@@ -13,54 +14,66 @@ def register(fun) -> None:
@register @register
async def fill(page: Page, act: ActionRequest) -> str | None: async def fill(page: Page, act: ActionRequest, **kwargs) -> str | None:
assert act.selector assert act.selector
assert act.value assert act.value
await page.fill(act.selector, act.value) await page.fill(act.selector, act.value)
@register @register
async def delay(page: Page, act: ActionRequest) -> str | None: async def captcha(page: Page, act: ActionRequest, captcha_key: str, **kwargs) -> str | None:
assert act.selector
assert act.value
assert captcha_key
captcha_solver = load_captcha(captcha_key, act.value)
await captcha_solver.solve(act.selector, page)
@register
async def delay(page: Page, act: ActionRequest, **kwargs) -> str | None:
assert act.value assert act.value
await asyncio.sleep(int(act.value)) await asyncio.sleep(int(act.value))
@register @register
async def mouse_move(page: Page, act: ActionRequest) -> str | None: async def mouse_move(page: Page, act: ActionRequest, **kwargs) -> str | None:
assert act.selector assert act.selector
element = await page.query_selector(act.selector) element = await page.query_selector(act.selector)
await element.hover(scroll="auto") await element.hover(scroll="auto")
@register @register
async def click(page: Page, act: ActionRequest) -> str | None: async def click(page: Page, act: ActionRequest, **kwargs) -> str | None:
assert act.selector assert act.selector
element = await page.query_selector(act.selector) element = await page.query_selector(act.selector)
await element.click() await element.click()
@register @register
async def inner_html(page: Page, act: ActionRequest) -> str | None: async def inner_html(page: Page, act: ActionRequest, **kwargs) -> str | None:
assert act.selector assert act.selector
element = await page.query_selector(act.selector) element = await page.query_selector(act.selector)
return await element.inner_html() return await element.inner_html()
@register @register
async def wait_for(page: Page, act: ActionRequest) -> str | None: async def wait_for(page: Page, act: ActionRequest, **kwargs) -> str | None:
assert act.selector assert act.selector
assert 1 <= act.timeout <= 7200 assert 1 <= act.timeout <= 7200
await page.wait_for_selector(act.selector, timeout=act.timeout * 1000) await page.wait_for_selector(act.selector, timeout=act.timeout * 1000)
async def execute(page: Page, actions: list[ActionRequest]) -> list[dict]: async def execute(page: Page, actions: list[ActionRequest], captcha: str | None = None) -> list[dict]:
actions_result = [] actions_result = []
ctx = {
"captcha_key": captcha,
}
for act in actions: for act in actions:
new_line = { new_line = {
"type": act.type, "type": act.type,
} }
try: try:
result = await _actions[act.type](page, act) result = await _actions[act.type](page, act, **ctx)
except Exception as e: except Exception as e:
new_line["ok"] = False new_line["ok"] = False
new_line["result"] = str(e) new_line["result"] = str(e)
+7 -1
View File
@@ -20,8 +20,14 @@ class SolveRequest(BaseModel):
""" """
url: str url: str
proxy: str proxy: str
actions: list[ActionRequest]
timeout: int | None = None timeout: int | None = None
screen: str = "1280x720" screen: str = "1280x720"
user_agent: str | None = None user_agent: str | None = None
actions: list[ActionRequest]
html: bool = False html: bool = False
captcha: str | None = None
captcha_selector: str | None = None
captcha_type: str | None = None
+31 -21
View File
@@ -1,7 +1,6 @@
import json import json
import logging import logging
import uuid import uuid
from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Annotated from typing import Annotated
from urllib.parse import urlsplit from urllib.parse import urlsplit
@@ -16,6 +15,7 @@ from fastapi import Response
from playwright.async_api import ProxySettings from playwright.async_api import ProxySettings
from ua_parser import user_agent_parser from ua_parser import user_agent_parser
import captcha
from engine import actions from engine import actions
from engine.schemas import SolveRequest from engine.schemas import SolveRequest
@@ -80,7 +80,9 @@ async def solve(
response: Response, response: Response,
session_id: Annotated[str | None, Cookie()] = None session_id: Annotated[str | None, Cookie()] = None
): ):
logger.info("Incoming payload: %s", payload) api_key = request.headers.get('X-API-Key')
logger.info("Incoming payload: [%s] %s", session_id, payload)
global BROWSER_SLOTS global BROWSER_SLOTS
if BROWSER_SLOTS <= 0: if BROWSER_SLOTS <= 0:
@@ -91,57 +93,65 @@ async def solve(
else: else:
BROWSER_SLOTS -= 1 BROWSER_SLOTS -= 1
api_key = request.headers.get('X-API-Key')
if not session_id: if not session_id:
session_id = str(uuid.uuid4()) session_id = str(uuid.uuid4())
response.set_cookie("session_id", session_id) response.set_cookie("session_id", session_id)
logger.info("Setup new session with session_id: %s", session_id)
scr_w, scr_h = payload.screen.split("x") scr_w, scr_h = payload.screen.split("x")
scr_size = int(scr_w), int(scr_h) scr_size = int(scr_w), int(scr_h)
# TODO: распарс proxy-урл и заполнение объекта proxy = parse_proxy(payload.proxy)
# proxy = ProxySettings(socks5://host:port, username, password)
proxy = None
user_data_dir = Path(__file__).parent / f"../extra/user_data_dir/{session_id}" user_data_dir = Path(__file__).parent / f"../extra/user_data_dir/{session_id}"
if not user_data_dir.exists(): if not user_data_dir.exists():
user_data_dir.mkdir(parents=True) user_data_dir.mkdir(parents=True)
ctx_opts = { config = load_or_create_identity(user_data_dir, user_agent=payload.user_agent)
# "locale": payload.locale,
# "timezone": payload.time_zone
}
config = load_or_create_identity(user_data_dir, user_agent=payload.user_agent, **ctx_opts)
ctx_opts = {**config["context_options"], "config": config["config"]} ctx_opts = {**config["context_options"], "config": config["config"]}
if payload.captcha:
# "dl.formRow:nth-child(3) > dd:nth-child(2) > div:nth-child(1)"
# hcaptcha
# rucaptcha api_key
captcha_solver = captcha.load_captcha(
payload.captcha,
payload.captcha_type
)
else:
captcha_solver = None
async with AsyncCamoufox( async with AsyncCamoufox(
persistent_context=True, persistent_context=True,
user_data_dir=user_data_dir, user_data_dir=user_data_dir,
proxy=proxy, proxy=proxy,
window=scr_size, window=scr_size,
i_know_what_im_doing=True, i_know_what_im_doing=True,
geoip=True,
**ctx_opts, **ctx_opts,
) as camoufo: ) as camoufo:
await camoufo.add_init_script(config["init_script"]) await camoufo.add_init_script(config["init_script"])
page = camoufo.pages[0] if camoufo.pages else await camoufo.new_page() page = camoufo.pages[0] if camoufo.pages else await camoufo.new_page()
page.set_default_timeout(30_000) page.set_default_timeout(30_000)
await page.goto(url=payload.url) await page.goto(url=payload.url)
actions_result = await actions.execute(page, payload.actions) # if captcha_solver:
# assert payload.captcha_selector
# await captcha_solver.solve(payload.captcha_selector, page)
actions_result = await actions.execute(page, payload.actions, payload.captcha)
status = not "error" in actions_result[-1] status = not "error" in actions_result[-1]
error = ["Something error", None][status] error = ["Something error", None][status]
cookies = await camoufo.cookies() cookies = await camoufo.cookies()
html = await page.inner_html('html') html = await page.inner_html('html')
await page.screenshot( # await page.screenshot(
path=( # path=(
f"/home/sokol/PycharmProjects/WebRoboApi/extra/screenshots/" # f"/home/sokol/PycharmProjects/WebRoboApi/extra/screenshots/"
f"{datetime.timestamp(datetime.now())}.png" # f"{datetime.timestamp(datetime.now())}.png"
) # )
) # )
BROWSER_SLOTS += 1 BROWSER_SLOTS += 1
result = { result = {
"status": ["err", "ok"][status], "status": ["err", "ok"][status],
"user_agent": ctx_opts["user_agent"],
"cookies": cookies, "cookies": cookies,
"actions": actions_result, "actions": actions_result,
} }
+19 -2
View File
@@ -92,7 +92,6 @@ POST http://127.0.0.1:8000/v1/solve
Accept: application/json Accept: application/json
Content-Type: application/json Content-Type: application/json
X-API-Key: <key> X-API-Key: <key>
Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
{ {
@@ -107,4 +106,22 @@ Cookie: session_id=2f2100be-6ce7-40d5-bae5-61424b2504c8; Path=/; SameSite=lax
] ]
} }
###
### 2f2100be-6ce7-40d5-bae5-61424b2504c8
POST http://127.0.0.1:8000/v1/solve
Accept: application/json
Content-Type: application/json
X-API-Key: <key>
{
"url": "https://ifconfig.me/",
"proxy": "http://7LLkWF:yecUzT@200.10.32.226:8000",
"actions": [
{"type": "wait_for", "selector": "#ip_address", "timeout": 10},
{"type": "inner_html", "selector": "#ip_address"}
]
}
### b8f770a3-38c2-45f2-944b-2703798e3503