Как решить ALTCHA на Python без запуска браузера

solve altcha in python - How to Solve ALTCHA in Python Without Running a Browser

Решить ALTCHA на Python можно одним HTTP-клиентом и больше ничем. ALTCHA построена на доказательстве работы, а не на распознавании: сайт выдаёт задачу, и тот, кто хочет пройти, обязан хэшировать, пока не найдёт число, которое ей удовлетворяет. Смотреть здесь не на что, поэтому ни браузер, ни WebDriver, ни user agent не нужны, а ответ вычисляется, а не угадывается. Значит, и решение получается детерминированным. Либо число находится за миллисекунды, либо сама задача была некорректной или уже истекла. CapSkip добавил этот тип в версии 1.2.6, а Python SDK предоставляет для него один метод.

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

  • CapSkip 1.2.6 или новее на машине с Windows. Именно в этом выпуске появилась ALTCHA.
  • Python 3.10 или новее и пакет CapSkip.
  • URL страницы, на которой стоит виджет, плюс эндпоинт, откуда виджет забирает свой challenge.
  • Адрес решателя. Режим Local слушает 127.0.0.1 и обслуживает только это устройство; режим Server слушает ваш сетевой адрес или публичный IP, чтобы к нему могла обратиться другая машина. Оба задаются в одном месте: Настройки подключения.
# pip install capskip
pip install capskip requests

Шаг 1: вызов решения и адрес, на который его направить

Передайте URL страницы и эндпоинт challenge. CapSkip сам получит challenge, будет хешировать, пока не найдёт счётчик, и вернёт ту полезную нагрузку, которую ждёт форма.

# pip install capskip
from capskip import CapSkip

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

# CapSkip fetches the challenge, then brute-forces the counter.
result = solver.altcha(
    url="https://example.com/signup",
    challenge_url="https://example.com/altcha/challenge",
)

print(result["token"])    # base64 payload for the form field
print(result["number"])   # the counter that satisfied it

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

Счётчик возвращается для обоих поколений ALTCHA, и это полезнее, чем кажется, потому что их полезные нагрузки расходятся в том, где он лежит. Старая нагрузка держит его на верхнем уровне, а нагрузка доказательства работы v2 его там не держит и прячет внутрь объекта solution. CapSkip достаёт его из объекта solution в собственном ответе API, поэтому вы получаете одно и то же поле, какое бы поколение ни стояло на сайте.

Loopback верен только пока вы делите одну машину

Именно аргумент с хостом нужно поменять, когда скрипт переезжает. 127.0.0.1 верен, пока процесс Python и решатель стоят на одной машине. Как только скрипт начинает работать в контейнере, на VPS, на раннере CI или как задача по расписанию на управляемой платформе, loopback указывает уже на это окружение, и первый же вызов поднимает NetworkException.

Решение здесь одно: режим Server. Он заставляет CapSkip слушать адрес в вашей сети или публичный IP вместо loopback, так что любой из этих вариантов дотянется до того же решателя через API. Если трафик пересекает интернет, возьмите статический публичный IP и правило файрвола, ограниченное ожидаемыми адресами. Больше ничего не меняется: решатель по-прежнему работает на вашем железе и по-прежнему безлимитный, отличается только его адрес. Читайте значение из окружения, а не прописывайте в коде, поскольку клиент уже сам ищет CAPSKIP_HOST, CAPSKIP_PORT и CAPSKIP_API_KEY.

Шаг 2: откуда берётся challenge и два способа его передать

Откройте DevTools, перейдите на вкладку Network и перезагрузите страницу. Виджет делает один запрос за своим challenge, обычно по пути, в котором есть altcha. Именно этот URL вы и передаёте. JSON, который приходит в ответ, и есть документ challenge, и его можно передать вместо URL.

Читайте исходный код страницы, а не угадывайте атрибут: он менялся от одного поколения виджета к другому.

Поколение виджетаАтрибут, который задаёт challenge
v1 и v2challengeurl для эндпоинта плюс отдельный атрибут challengejson, когда challenge встроен в страницу
v3 и новееchallenge, причём этот единственный атрибут принимает либо URL, либо сами данные challenge

Оформление виджета здесь не имеет значения. Варианты native, checkbox и switch отправляют одну и ту же полезную нагрузку, и разница никогда не доходит до решателя, так что определять её не нужно. Если нужно описание каждого поля challenge, у ALTCHA есть своя документация по виджету и серверной части.

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

# The document is already here, so no request goes out.
result = solver.altcha(
    url="https://example.com/signup",
    challenge_json={
        "algorithm": "SHA-256",
        "challenge": "YOUR_CHALLENGE_HASH",
        "salt": "YOUR_SALT",
        "signature": "YOUR_SIGNATURE",
        "maxnumber": 1000000,
    },
)

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

Какие алгоритмы покрывает решатель

Оба поколения обслуживает один метод. Старая схема закрыта алгоритмами SHA-1, SHA-256, SHA-384 и SHA-512, а доказательство работы v2 закрыто PBKDF2 и итеративным SHA. PBKDF2 стоит по умолчанию и рекомендован самой ALTCHA, а это подавляющее большинство живых сайтов.

Argon2id и scrypt составляют исключение. Они отклоняются, а не берутся в работу: challenge, который просит любой из них, возвращается примерно за треть секунды как ERROR_CAPTCHA_UNSOLVABLE и никогда не повторяется, потому что функцию, требовательную к памяти, повторной попыткой не победить.

Шаг 3: отправляем токен обратно нетронутым, пока он не истёк

Виджет отправляет свою полезную нагрузку в поле формы с именем altcha. Отправляйте строку точно в том виде, в каком она пришла.

import requests

# No strip(), no re-encoding, no rebuilding the JSON.
r = requests.post("https://example.com/signup", data={
    "email": "someone@example.com",
    "altcha": result["token"],
})

Эта полезная нагрузка представляет собой base64 от документа JSON, поля которого покрыты HMAC-подписью сервера, поэтому любая правка делает её недействительной. Вызов strip() на ней, декодирование с последующим кодированием, пересборка словаря с ключами в другом порядке: всё это даёт токен, который сайт отклонит. Некоторые интеграции читают его из поля тела JSON, а не из поля формы, так что посмотрите, что отправляет собственная форма страницы, и повторите это.

Если сайт отклоняет token, который в вашем логе помечен как решённый, истёкшая задача вероятнее испорченного токена. Окна задач короткие, часть из них закрывается быстрее двух минут, а истёкшая задача возвращается обычной ошибкой проверки, неотличимой от неверного ответа. Загрузку, решение и отправку выполняйте одной операцией и никогда не держите token, пока человек заполняет форму.

Ограничивают вас здесь не собственные таймауты опроса клиента, ведь окно задачи закрывается задолго до любого из них. Знать, какой из них действует, всё же стоит, потому что ALTCHA попадает на короткую сторону этого деления. Это нагрузка на процессор, а не сессия браузера, поэтому используется обычный таймаут опроса, а не удлинённый таймаут для reCAPTCHA.

Параметр конструктораПо умолчаниюЧто он покрывает
defaultTimeout120 секундОпрос ALTCHA и графической капчи
recaptchaTimeout300 секундОпрос reCAPTCHA, Turnstile и GeeTest
pollingIntervalМаксимум 5 секундОпрос начинается с 0,25 секунды и увеличивает интервал до этого значения

Шаг 4: решаем пакет задач так, чтобы challenge не успевали устареть

Python остаётся единственным SDK, где асинхронный клиент представляет собой отдельную реализацию: AsyncCapSkip здесь работает на настоящем asyncio. В пакетах для Node.js и .NET это имя служит псевдонимом обычного клиента, методы которого и так асинхронны, а PHP синхронен целиком.

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

# pip install capskip
import asyncio
from capskip import AsyncCapSkip

async def solve_one(solver, page_url, challenge_url):
    # One fresh challenge per page, fetched and solved together.
    result = await solver.altcha(url=page_url, challenge_url=challenge_url)
    return page_url, result["token"]

async def main():
    solver = AsyncCapSkip()
    pages = [
        ("https://example.com/signup", "https://example.com/altcha/challenge"),
        ("https://example.com/contact", "https://example.com/altcha/challenge"),
    ]
    for page_url, token in await asyncio.gather(
        *(solve_one(solver, p, c) for p, c in pages)
    ):
        print(page_url, token[:24])

asyncio.run(main())

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

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

# pip install capskip
import os
import requests
from capskip import CapSkip, ApiException, NetworkException, TimeoutException

solver = CapSkip(host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"), port=8080)

try:
    result = solver.altcha(
        url="https://example.com/signup",
        challenge_url="https://example.com/altcha/challenge",
    )
    # Submit here, while the challenge is still fresh.
    r = requests.post("https://example.com/signup", data={
        "email": "someone@example.com",
        "altcha": result["token"],
    })
    print(r.status_code, "solved with counter", result["number"])
except ApiException as exc:
    # ERROR_CAPTCHA_UNSOLVABLE here means Argon2id or scrypt.
    print("refused:", exc)
except NetworkException:
    print("CapSkip is not answering on that host and port")
except TimeoutException:
    print("gave up waiting, which on this type means something is wrong")

Остальные типы устроены так же, только метод другой. Есть один вызов для reCAPTCHA v2, v3 и Enterprise, один для Turnstile, один для GeeTest v3 и один для графической капчи, а полный список собран здесь: странице сервиса распознавания капч для Python.

Пакеты для Node.js, PHP и .NET предоставляют те же имена методов. Все SDK CapSkip описаны здесь: странице SDK.

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

Что вы видитеПричинаИсправить
Сайт отвечает обычной ошибкой проверки на token, который решился без замечанийChallenge истёк до того, как форма ушлаПолучайте challenge, решайте и отправляйте внутри одной функции
ERROR_CAPTCHA_UNSOLVABLE в ApiException через треть секундыChallenge просит Argon2id или scryptПовторять нечего. Эти два отклоняются специально
ValidationException при вызовеНе передан ни один из параметров задачи, либо передан такой, которого ALTCHA не принимаетПередайте эндпоинт challenge или документ challenge, а остальное уберите
NetworkException до того, как началось хешированиеCapSkip не запущен либо неверны хост и портЗапустите CapSkip и проверьте, в каком режиме он должен работать: Local или Server
KeyError на ключе tokenЭтот ключ есть только в результате ALTCHAВызовите метод altcha или прочитайте ключ code, в котором лежит та же строка
Первые решения в пакете падают, а последние проходятChallenge были получены заранее и истекли, пока стояли в очередиПолучайте challenge внутри каждой задачи, а не до gather
Токен, который сайт отклоняет каждый разЧто-то обрезало или перекодировало полезную нагрузкуПередавайте строку нетронутой

FAQ

Нужен ли Selenium или Playwright для страницы с ALTCHA?

Для самой ALTCHA нет. Challenge сводится к задаче на хеширование, а ответом служит строка, которую вы отправляете в поле формы, так что достаточно requests или httpx, и решение заканчивается за миллисекунды. Браузер по-прежнему нужен, если он нужен остальной странице, например когда cookie сессии ставит JavaScript или форма отрисовывается на клиенте. В этом случае оставьте браузер для навигации, а за токеном обращайтесь к решателю напрямую, вместо того чтобы заставлять работать сам виджет.

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

Да. Включите режим Server в настройках подключения, чтобы CapSkip слушал сетевой адрес, а не loopback, затем направьте на него CAPSKIP_HOST. Контейнер Docker, VPS, задача по расписанию на управляемой платформе и раннер CI подключаются одинаково, через один и тот же HTTP API. Если маршрут пересекает интернет, возьмите статический публичный IP и правило файрвола, которое пропускает только ожидаемые адреса. Решатель в любом случае продолжает работать на вашем железе, поэтому ни лицензия, ни безлимитное решение капчи не затрагиваются.

Почему ALTCHA решается намного быстрее, чем reCAPTCHA?

Потому что они просят разного. reCAPTCHA и Turnstile хотят доказательства, что присутствует браузер с правдоподобной историей, а на это нужна настоящая сессия и настоящее время. ALTCHA хочет лишь подтверждения, что было потрачено время CPU, поэтому работа сводится к циклу хеширования с известным условием остановки. По этой же причине ответ детерминирован, а не является суждением: существует число, которое удовлетворяет challenge, и оно либо находится, либо challenge был сломан. Расплата в том, что через минуту или две это число уже ничего не стоит.

Можно ли переиспользовать токен между запросами?

Нет, и стоит прямо сказать почему. Полезная нагрузка подписана под один конкретный challenge, этот challenge выдаётся один раз, и сервер его отслеживает, поэтому вторая отправка того же токена и есть ровно тот повтор, против которого придумана вся схема. Решайте по одному разу на каждую отправку. Здесь это доступно так, как не бывает на сервисе с тарификацией, потому что работа занимает несколько миллисекунд вашего собственного CPU, а не оплаченный вызов, так что нет причин кешировать то, что можно просто пересчитать.

Коротко

Считайте эндпоинт challenge с виджета, передайте его вместе с URL страницы в единственный метод ALTCHA и отправьте токен в поле с именем altcha, не редактируя его. Держите получение challenge, решение и отправку вместе, потому что challenge может умереть быстрее двух минут, а истёкший сообщается как обычная ошибка проверки. Для пакета распараллельте работу асинхронным клиентом и получайте каждый challenge внутри его собственной задачи, а не собирайте их заранее. ERROR_CAPTCHA_UNSOLVABLE ждите только от Argon2id и scrypt.

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