Как решить Capy Puzzle на Python: от ключа до отправки формы

solve capy puzzle in python - How to Solve Capy Puzzle in Python, From Key to Form Post

Чтобы решить Capy Puzzle на Python, считайте со страницы ключ PUZZLE_ сайта, вызовите solver.capy() из пакета capskip с этим ключом и URL страницы, а затем отправьте три возвращённых значения в полях формы capy_captchakey, capy_challengekey и capy_answer, все вместе и сразу же. Загружайте страницу и отправляйте форму через одну и ту же сессию requests, чтобы cookies сайта вернулись к нему вместе с ответом. Неожиданной для многих оказывается форма результата. Большинство типов капчи возвращают один токен; Capy возвращает три значения, которые работают только в наборе, и одно из них быстро истекает. CapSkip добавил Capy Puzzle в версии 1.4.0. В этом руководстве разобраны ключ, вызов, намеренное удержание ответа на две секунды, отправка, повторы и запуск многих решений одновременно.

Что понадобится

  • CapSkip 1.4.0 или новее на машине с Windows. Поддержка Capy появилась именно в этом выпуске, вместе с CaptchaFox и Friendly Captcha.
  • Python 3.10 или новее и пакет capskip версии 1.3.0 или новее: это первый выпуск, в котором есть метод capy(). В примерах также используется requests, а в разделе о параллельных решениях используется httpx.
  • Два значения с целевой страницы: ключ Capy, который начинается с PUZZLE_, и URL страницы, на которой работает виджет. Где лежит ключ, показывает шаг 1, и там же видно, нужно ли вам третье, необязательное значение.
  • Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства; в режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы скрипт на другой машине мог обращаться к нему через API. Оба режима задаются в разделе Настройки подключения, а о том, когда переключаться, рассказывает отдельный раздел ниже.
# Quoted, so cmd.exe does not read >= as a redirect
pip install "capskip>=1.3.0" requests httpx

Шаг 1: находим ключ PUZZLE_ и хост Capy

Всё нужное берётся с целевой страницы, а ключ публичен и одинаков для всех посетителей. На сайтах он встречается в двух местах. Параметр k в URL скрипта виджета есть в HTML, который скачивает requests. После того как виджет отработал в браузере, ключ также лежит в скрытом поле capy_captchakey, которое виджет записывает в форму, и именно там его показывают DevTools. Одно регулярное выражение для префикса PUZZLE_ находит его в любом из этих мест.

import re

import requests

PAGE_URL = "https://example.com/login"
session = requests.Session()
html = session.get(PAGE_URL, timeout=30).text

# The widget script's k= parameter. The capy_captchakey input only
# exists once the widget has run in a browser.
key = re.search(r"PUZZLE_[A-Za-z0-9_-]+", html)

# The widget script's own host; None means the default one.
host = re.search(r"(https://[^\"'\s<>]+?)/puzzle/get_js/", html)
api_server = host.group(1) if host else None
print(key and key.group(0), api_server)

Раз уж вы смотрите на тег script, запомните его хост. Всё, что стоит в URL скрипта до /puzzle/get_js/, указывает на Capy API, за которым стоит ключ, и CapSkip называет его api_server. По умолчанию это https://jp.api.capy.me, где и работает действующий сервис, поэтому он нужен, только если страница загружает виджет откуда-то ещё. Одна ловушка идёт из документации других решателей: в некоторых из них до сих пор указан api.capy.me без регионального префикса, а этот хост больше не резолвится. Если старый пример его задаёт, удалите опцию.

Загрузка идёт через requests.Session намеренно. Форма, которую вы отправляете на шаге 3, часто зависит от сессионной cookie, установленной страницей, а объект сессии отправляет её обратно без всякого дополнительного кода.

Шаг 2: вызов capy()

Чтобы решить Capy Puzzle на Python, нужен один метод. Он принимает ключ и URL страницы, а также необязательные именованные аргументы, и для страницы на хосте по умолчанию ему хватает этих двух значений.

# pip install "capskip>=1.3.0"
from capskip import CapSkip

solver = CapSkip(host="127.0.0.1", port=8080)

result = solver.capy("PUZZLE_YOUR_KEY", "https://example.com/login")

print(result["captchakey"])     # goes in capy_captchakey
print(result["challengekey"])   # goes in capy_challengekey
print(result["answer"])         # goes in capy_answer

Результат представляет собой обычный dict. Читайте три именованных ключа. Ключ code содержит весь объект ответа сервера, dict со всеми четырьмя полями, что удобно для логирования, а в respKey лежит пустая строка, которая нужна только для совместимости с другими сервисами. Ответ (answer) представляет собой длинную строку, которая начинается примерно так: 0xax8ex0xax84x. Это траектория перетаскивания, которую виджет записал бы при движении фрагмента, а не координата.

Опции, которые принимает метод

Метод capy() отбрасывает любой именованный аргумент со значением None ещё до отправки запроса, поэтому хост из шага 1 можно передавать при каждом вызове, а решение о том, уйдёт ли он, оставить регулярному выражению.

result = solver.capy(
    key.group(0),
    PAGE_URL,
    # None (no get_js script on the page) is dropped, and CapSkip
    # then uses its default host.
    api_server=api_server,
    # Poll every half second; see the next section for why.
    polling_interval=0.5,
)

Помимо api_server, capy() принимает proxy, proxytype и useragent, а также timeout на один вызов и polling_interval в секундах. User agent, если вы его зададите, отправляется в единственном запросе, которым CapSkip загружает головоломку, и нужен он редко. Есть ещё опция version, но принимается только значение puzzle. Другое семейство Capy, avatar, представляет собой иное задание за другим эндпоинтом, поэтому SDK отклоняет его с ValidationException, а не возвращает ответ, который сайт отверг бы. Неизвестный именованный аргумент, пустой ключ или URL страницы, а также тип прокси, отличный от HTTP, HTTPS, SOCKS5 или SOCKS5H, вызывают то же исключение ещё до какой-либо отправки.

Почему решение Capy занимает около двух секунд

Само распознавание происходит быстро. CapSkip загружает изображение головоломки, находит вырез с помощью несложной попиксельной математики и строит траекторию перетаскивания, без всякого браузера и без модели. А затем намеренно ждёт.

Capy измеряет время между выдачей головоломки и получением ответа и отклоняет всё, что приходит быстрее, чем человек успел бы перетащить фрагмент. По собственным тестам CapSkip на Capy эта нижняя граница составляет около одной секунды, а отказ приходит с тем же сообщением, что и при неверном ответе, так что верное решение, доставленное слишком быстро, выглядит в точности как сломанный решатель. Поэтому CapSkip придерживает каждый результат Capy, пока с момента получения головоломки не пройдут две секунды. Ожидание сделано через sleep: оно добавляет задержку, но не тратит CPU.

Почему с настройками по умолчанию вызов занимает ближе к четырём секундам: SDK опрашивает результат сразу после того, как отправил задачу в CapSkip, затем ещё раз через четверть секунды и дальше удваивает интервал до значения pollingInterval, по умолчанию 5 секунд. Опросы приходятся на 0, 0.25, 0.75, 1.75 и 3.75 секунды, так что ответ, готовый через две секунды, забирается на пятом опросе. С polling_interval=0.5 в вызове или pollingInterval=0.5 в конструкторе, если клиент решает в основном Capy, вызов возвращается чуть больше чем через две секунды после отправки задачи, в наших тестах примерно через 2.3. Не добавляйте собственную задержку перед тем, как отправить форму, и не пытайтесь срезать удержание: именно оно позволяет ответу пройти проверку.

Шаг 3: отправляем все три значения одним запросом

Три значения идут в те поля, которые виджет сам записал бы в форму. Отправляйте их вместе, в том же запросе, что и остальную форму, через ту же сессию, которая загружала страницу.

# session is the one Step 1 used, so the page's cookies go back too.
resp = session.post(
    PAGE_URL,  # or wherever the form's action attribute points
    data={
        "username": "YOUR_USERNAME",
        "capy_captchakey": result["captchakey"],
        "capy_challengekey": result["challengekey"],
        "capy_answer": result["answer"],
    },
    timeout=30,
)
print(resp.status_code)

Отправляйте ответ ровно в том виде, в каком он вернулся. Бэкенд сайта сверяет его с выданной головоломкой, поэтому обрезка, пересборка или любое другое наведение порядка делают его недействительным. Библиотека requests применяет к телу формы процентное кодирование, и это нормально, потому что сервер декодирует его перед проверкой. И отправляйте сразу. CapSkip генерирует новый ключ задания для каждого решения, и головоломка привязана к нему, поэтому ключ одноразовый и живёт недолго, а одно решение покрывает одну отправку.

Повторяйте то, что отправляет настоящая форма. На большинстве страниц есть собственные скрытые поля, например токен защиты от подделки запросов, а некоторые отправляют данные скриптом в теле JSON, а не обычной отправкой формы. Отправьте форму один раз вручную с открытыми DevTools, скопируйте запрос и поставьте три значения Capy туда, куда их ставит страница.

Шаг 4: повторы при неудачном решении

Когда вы решаете Capy Puzzle на Python в сколько-нибудь заметном объёме, часть решений будет проваливаться. Неудачное решение приходит в виде ApiException, в сообщении которого есть ERROR_CAPTCHA_UNSOLVABLE, и через res.php, эндпоинт, который опрашивает SDK, этот единственный код покрывает две разные ситуации. Различить их можно по тому, как часто они случаются.

Что произошлоКак это выглядитЧто делать
CapSkip не нашёл вырезИзредка, и следующая попытка обычно проходитПовторите. Каждая попытка получает совершенно новую головоломку на другой фотографии
Capy API отклонил ключПри каждой попытке для этого ключа, а в списке задач Capy в CapSkip видно Invalid captcha keyПроверьте ключ и api_server. CapSkip никогда не повторяет отклонённый ключ, потому что его отклонят точно так же

Промахи редки, поэтому одного повтора хватает почти всегда. Задайте Retries в разделе Capy в настройках CapSkip (по умолчанию там 0, а максимум составляет три повтора на задачу) или повторяйте в своём коде:

from capskip import ApiException


def solve_capy(key, url, attempts=3, **options):
    for attempt in range(1, attempts + 1):
        try:
            return solver.capy(key, url, **options)
        except ApiException as exc:
            # A missed hole reads as unsolvable, and the next try
            # draws a new puzzle. Anything else is final.
            if "UNSOLVABLE" not in str(exc) or attempt == attempts:
                raise

Метод capy() опрашивает результат с таймаутом клиента defaultTimeout, 120 секунд, потому что решение Capy сводится к одной загрузке и небольшой арифметике, а не к браузерной сессии. У CapSkip есть и собственный отсчёт времени: в разделе Capy задача может ждать до 250 секунд (Wait Timeout) одного из 10 потоков (Max. Threads), а на одну попытку отводится 60 секунд (Row Timeout). Обычное решение укладывается во всё это с многократным запасом.

Решение многих головоломок Capy одновременно

AsyncCapSkip в Python представляет собой настоящий клиент на asyncio, а не псевдоним, поэтому asyncio.gather запускает несколько решений бок о бок. Держите каждое решение в паре с его собственной отправкой. Если сначала собрать сотню решений, а отправлять потом, самые ранние ключи заданий будут стареть, пока завершаются последние решения.

import asyncio

import httpx
from capskip import AsyncCapSkip

solver = AsyncCapSkip(host="127.0.0.1", port=8080, pollingInterval=0.5)
slots = asyncio.Semaphore(10)  # CapSkip's Capy Max. Threads


async def solve_and_submit(key, url, form):
    # One client per job, so each form session keeps its own cookies.
    async with slots, httpx.AsyncClient(
            timeout=30, follow_redirects=True) as client:
        await client.get(url)
        result = await solver.capy(key, url)
        return await client.post(url, data={
            **form,
            "capy_captchakey": result["captchakey"],
            "capy_challengekey": result["challengekey"],
            "capy_answer": result["answer"],
        })


async def main(jobs):
    return await asyncio.gather(
        *(solve_and_submit(*job) for job in jobs), return_exceptions=True)

Размер семафора равен числу потоков CapSkip, поэтому ни одна задача не простаивает в очереди CapSkip с открытой сессией, а сам семафор входит в стандартную библиотеку Python и описан в разделе asyncio.Semaphore. На объёме добавьте ещё и прокси. Каждое решение получает новую головоломку от Capy API, а равномерный поток таких запросов с одного адреса и есть тот шаблон, ради которого существует ограничение частоты запросов. Настройте пул прокси в разделе Capy настроек CapSkip или передавайте в каждом вызове словарь proxy с ключами type и uri; он влияет только на загрузку головоломки, единственный запрос, который делает решение Capy. Больше приёмов работы с asyncio собрано в руководстве по параллельному решению капчи на Python.

Запуск решателя на другой машине

Адрес 127.0.0.1 подходит, пока скрипт и CapSkip работают на одном ПК с Windows. Когда код на Python переезжает на VPS, в контейнер, на раннер CI или в облачный ноутбук, локальная петля указывает не на ту машину, и первое же решение вызывает NetworkException. Переключите CapSkip в режим Server, и он начнёт слушать ваш сетевой адрес или публичный IP, так что любой из этих вариантов сможет обращаться к нему через тот же API. Если маршрут идёт через интернет, используйте статический публичный IP, включите проверку API-ключа и ограничьте порт ожидаемыми адресами с помощью правила Windows Firewall. Решателем по-прежнему остаётся ваша собственная машина с Windows, и решения по-прежнему не тарифицируются.

SDK сам не читает переменные окружения. Читайте CAPSKIP_HOST, CAPSKIP_PORT и CAPSKIP_API_KEY в своём коде и передавайте их в конструктор, как это делает полный пример, чтобы один и тот же скрипт работал и у вас на столе, и на сервере.

Полный рабочий пример

# pip install "capskip>=1.3.0" requests
import os
import re

import requests
from capskip import (ApiException, CapSkip, CapSkipError,
                     TimeoutException, ValidationException)

PAGE_URL = "https://example.com/login"

solver = CapSkip(
    apiKey=os.getenv("CAPSKIP_API_KEY", "capskip"),
    host=os.getenv("CAPSKIP_HOST", "127.0.0.1"),
    port=int(os.getenv("CAPSKIP_PORT", "8080")),
    # Capy answers after a two second hold; poll often enough to catch it.
    pollingInterval=0.5,
)
session = requests.Session()

html = session.get(PAGE_URL, timeout=30).text
key = re.search(r"PUZZLE_[A-Za-z0-9_-]+", html)
if not key:
    raise SystemExit("No PUZZLE_ key in the HTML; find it in DevTools.")
host = re.search(r"(https://[^\"'\s<>]+?)/puzzle/get_js/", html)

try:
    for attempt in range(1, 4):
        try:
            result = solver.capy(key.group(0), PAGE_URL,
                                 api_server=host.group(1) if host else None)
            break
        except ApiException as exc:
            # A missed hole reads as unsolvable; the next try draws a new puzzle.
            if "UNSOLVABLE" not in str(exc) or attempt == 3:
                raise
            print(f"attempt {attempt}: {exc}")
except ValidationException as exc:
    raise SystemExit(f"not sent: {exc}")
except TimeoutException:
    raise SystemExit("gave up waiting; defaultTimeout is 120 seconds")
except CapSkipError as exc:
    # A third miss, a refused key, or CapSkip unreachable.
    raise SystemExit(f"solve failed: {exc!r}")

# All three together, straight away: the challenge key is single-use.
resp = session.post(
    PAGE_URL,  # or wherever the form's action attribute points
    data={
        "username": "YOUR_USERNAME",
        "capy_captchakey": result["captchakey"],
        "capy_challengekey": result["challengekey"],
        "capy_answer": result["answer"],
    },
    timeout=30,
)
print(resp.status_code)

Если провалились все три попытки, первым делом проверьте ключ: отклонённый ключ каждый раз отказывает одинаково, а промах почти никогда не случается три раза подряд. Если страница собирает виджет из бандла и регулярное выражение ничего не находит, откройте вкладку Network и скопируйте ключ и хост из собственного запроса виджета. Сырой эндпоинт, который стоит за этим методом, описан в разделе Capy справочника API.

Частые ошибки и что они означают

Что вы видитеПричинаИсправить
Сайт отклоняет отправку, хотя capy() вернул все три значенияОтвет был изменён, или до формы дошли только одно или два значенияОтправьте result["answer"] без изменений, вместе с двумя другими значениями, одним запросом
Сайт отклоняет отправку и сообщает, что сессия истеклаСтраница загружена через requests.get(), а форма отправлена новым запросом, поэтому cookies страницы так и не вернулисьЗагружайте страницу и отправляйте форму через один requests.Session
Отправка, которая сработала один раз, не проходит при второй попыткеКлюч задания одноразовый и живёт недолгоРешайте заново для каждой отправки и отправляйте сразу
ApiException при каждой попытке для одного ключаCapy API отклонил ключ, или api_server указывает не на тот хостСкопируйте ключ заново, вместе с префиксом, и проверьте хост в теге script
Время от времени ApiException с ERROR_CAPTCHA_UNSOLVABLECapSkip не смог найти вырез в этой головоломкеПовторите; следующая попытка получит другую головоломку
Все решения проваливаются после того, как вы скопировали пример от другого сервисаВ примере api_server задан как api.capy.me, а этот хост больше не резолвитсяУберите опцию и используйте хост по умолчанию
ValidationException ещё до какой-либо отправкиПустой ключ или URL страницы, avatar в качестве version, тип прокси, отличный от HTTP, HTTPS, SOCKS5 или SOCKS5H, либо именованный аргумент, который метод не принимаетУбедитесь, что ключ не пустой, и исправьте или уберите аргумент, названный в сообщении
ApiException с ERROR_WRONG_USER_KEY или ERROR_KEY_DOES_NOT_EXISTВ CapSkip включена проверка API-ключа, а скрипт отправил ключ по умолчанию или не отправил никакогоЗадайте в CAPSKIP_API_KEY ключ, настроенный в CapSkip
TimeoutException при высокой параллельной нагрузкеВ очереди больше задач, чем 10 потоков успевают обработать за 120 секунд, или CapSkip перезапустился посреди решенияОграничьте параллельность семафором или поднимите defaultTimeout
NetworkException при первом вызовеCapSkip не запущен либо неверны хост и портЗапустите CapSkip и проверьте, в каком режиме он должен работать: Local или Server

FAQ

Почему capy() возвращает три значения, а не токен?

Потому что именно это отправляет форма Capy. Никакой сервер ни на одном этапе не выдаёт токен. Виджет сам генерирует ключ задания, загружает принадлежащую ему головоломку и записывает перетаскивание, а сайт затем отправляет ключ задания и ответ в Capy вместе со своим приватным ключом, чтобы их проверили. CapSkip играет роль виджета, поэтому возвращает то, что виджет записал бы в форму.

Не отклонят ли ответ из-за user agent python-requests?

Capy не отклонит. Ответ Capy не привязан к браузеру, CapSkip не возвращает для него user agent, а необязательный user agent, который можно передать, влияет только на запрос, загружающий головоломку. Сам сайт всё же может по своим причинам отклонить запрос, который представляется как python-requests. В таком случае один раз задайте для сессии браузерный User-Agent, и каждый её запрос, включая отправку формы, будет нести этот заголовок.

Можно ли использовать ответ в сессии Selenium или Playwright?

Да. Решайте с URL страницы, открытой в браузере, затем коротким скриптом запишите три значения в скрытые поля формы capy_captchakey, capy_challengekey и capy_answer, добавив те поля, которые виджет ещё не создал, и отправьте форму так, как это обычно делает страница. Не перетаскивайте фрагмент ещё и в браузере: это дало бы второй, другой ответ. Если браузер нужен только для того, чтобы пройти головоломку, описанный выше путь через requests проще и быстрее.

Может ли скрипт на Python на хостинг-платформе достучаться до решателя?

Да. Переведите CapSkip в режим Server в настройках подключения, чтобы он слушал сетевой адрес, а не локальную петлю, прочитайте этот адрес из CAPSKIP_HOST в своём скрипте и передайте его в CapSkip(). VPS, хост контейнеров, раннер CI и облачный блокнот подключаются через один и тот же HTTP API. Если маршрут идёт через интернет, используйте статический публичный IP и правило файрвола. Решатель остаётся на вашем собственном железе, поэтому в том, как считаются решения, ничего не меняется. Тот же сценарий для .NET разобран в руководстве по Capy Puzzle на C#.

Коротко

Чтобы решить Capy Puzzle на Python, загрузите страницу через сессию requests, вытащите ключ PUZZLE_ одним регулярным выражением и запомните хост скрипта виджета на случай, если он не стандартный. Вызовите solver.capy() с ключом и настоящим URL страницы и дайте ему отработать свои несколько секунд, около 2.3 с polling_interval=0.5. Отправьте captchakey, challengekey и answer в три поля capy_ без изменений, вместе и сразу, через ту же сессию. Повторяйте редкие промахи, добавляйте прокси по мере роста объёма и переходите в режим Server, как только скрипт покидает машину решателя.

И последнее о повторной попытке. Каждая попытка получает другую головоломку, поэтому при промахе правильнее всего просто попробовать снова, а безлимитный сервис распознавания капчи на вашей собственной машине означает, что вторая попытка стоит пару секунд, а не ещё одно оплачиваемое решение.