Как решать капчу в скрипте Windmill (Python)

Компактнее, чем решение капчи в Windmill, такая интеграция почти не бывает. Windmill читает импорты в начале вашего скрипта, сопоставляет их с пакетами PyPI и фиксирует версии в lockfile, поэтому CapSkip SDK появляется без шага установки и без файла зависимостей. Остаётся функция main, которая принимает sitekey и возвращает вам токен. Думать стоит не о коде, а о том, на какой машине работает воркер, потому что это решает, останется ли решатель на адресе loopback или ему придётся слушать вашу сеть.
Что понадобится
- Инстанс Windmill, свой или в их облаке, и workspace, куда вы можете задеплоить скрипт.
- CapSkip, запущенный на машине с Windows. Local mode подойдёт, если воркер работает на той же машине. Если воркеры живут в контейнерах или на другом хосте, переключитесь в Server mode.
- Sitekey и URL страницы сайта, который вы автоматизируете.
- Переменная Windmill с ключом решателя и переменная окружения воркера с его адресом.
Почему строка импорта и есть вся установка
Windmill разбирает импорты верхнего уровня, когда вы сохраняете скрипт, определяет, каким пакетам PyPI они соответствуют, и запускает задание зависимостей, которое пишет lockfile. Этот lockfile привязан к версии скрипта, поэтому та развёрнутая версия, которую вы протестировали, и будет работать через полгода. Нет файла зависимостей, который надо поддерживать, и нечего устанавливать на воркере руками.
Там же можно зафиксировать интерпретатор, комментарием в шапке скрипта. Развёрнутый скрипт, который не запрашивает версию, работает на Python 3.11.
# py312 # pip install capskip - Windmill resolves this import itself # and locks the version when the script is deployed. from capskip import CapSkip
Шаг 1: скрипт решения
Скрипт Windmill представляет собой функцию main. Её аргументы становятся схемой входных данных и формой, которую рисует Windmill, поэтому указывайте типы. Всё, что вы возвращаете, становится результатом скрипта, и следующий шаг flow читает его оттуда.
# py312
# pip install capskip - resolved from this import on save.
import wmill
from capskip import CapSkip
def main(sitekey: str, page_url: str) -> str:
# Host and port come from the worker environment. The key is
# a Windmill variable, so it is stored encrypted and never
# appears in the script body or in the run logs.
solver = CapSkip(
host=wmill.get_variable("u/admin/capskip_host"),
port=8080,
apiKey=wmill.get_variable("u/admin/capskip_key"),
)
result = solver.recaptcha(sitekey=sitekey, url=page_url)
return result["code"] # the token, for the next stepЭто вся интеграция для reCAPTCHA v2. Любой другой вариант сводится к тому же методу с дополнительным ключевым аргументом: invisible со значением 1, enterprise со значением 1 или version со значением v3 и именем действия. У Turnstile и GeeTest свои методы такой же формы, а полный список параметров лежит в документации CapSkip API.
Две вещи про SDK стоит знать, прежде чем браться за самописный цикл опроса. Он опрашивает за вас, начиная с 250 миллисекунд и увеличивая паузу, а не выжидая постоянный интервал, и это обычно быстрее опубликованных сроков ожидания сырого API. И его верхний предел для reCAPTCHA, Turnstile и GeeTest равен 300 секундам, он задан в recaptchaTimeout. Это число важно, когда вы выставляете таймаут скрипта на шаге 4.
Шаг 2: держите ключ в переменной Windmill
В Windmill есть полноценные переменные и секреты, и скрипт выше читает переменную напрямую. Есть второй способ, который лучше подходит для flow: передайте переменную в шаг аргументом, используя синтаксис ссылки, и Windmill разрешит её во время выполнения с правами вызывающего.
| Где лежит значение | Как скрипт его получает |
|---|---|
| Секретная переменная Windmill | Прочитать её в теле скрипта клиентом wmill, как выше |
| Переменная Windmill, переданная аргументом шага | Задайте аргументу значение dollar-var, а за ним путь к переменной |
| Ресурс Windmill, который держит несколько полей сразу | Задайте аргументу значение dollar-res, а за ним путь к ресурсу |
| Переменная окружения на хосте воркера | Прочитать её из окружения процесса, после того как воркеру разрешено её пробрасывать |
Эти ссылки разрешаются рекурсивно, в том числе внутри списков и вложенных объектов, поэтому шаг, который принимает список ключей, может держать ссылку в каждом элементе. Заведите этому workspace собственный ключ решателя вместо того, чтобы делить один на всё, что вы запускаете.
Шаг 3: место, где работает воркер, определяет режим подключения
Это тот вопрос, который на самом деле формирует всю настройку, и ошибиться в нём легко, потому что скрипт в обоих случаях выглядит одинаково. Воркер Windmill является самостоятельным процессом, который выполняет один скрипт за раз. Это может быть контейнер рядом с базой данных, процесс на виртуальной машине или процесс на вашем рабочем компьютере. Что бы это ни было, вызов SDK открывает сокет из воркера, поэтому решатель должен быть доступен именно оттуда и больше ниоткуда.
Именно для этого у CapSkip есть два режима подключения. Local слушает 127.0.0.1 и обслуживает только это устройство. Server слушает ваш сетевой адрес или публичный IP, поэтому другая машина, хост контейнеров или облачная платформа могут обратиться к той же машине Windows по API. Оба настраиваются в разделе Настройки подключения, а Server mode меняет только то, где работает решатель. Это по-прежнему ваше железо и по-прежнему без счётчика.
| Где работает ваш воркер | Какой режим и значение хоста |
|---|---|
| На той же машине с Windows, что и CapSkip | Local mode. Значение хоста остаётся 127.0.0.1 |
| В контейнере или на другой машине в вашей сети | Server mode. В значении хоста указан LAN-адрес машины с решателем |
| В облаке Windmill или на виртуальной машине вне вашей сети | Server mode со статическим публичным IP плюс правило фаервола |
Воркеры Windmill действительно работают на Windows, и это тот случай, который оставляет вас на адресе loopback. Там важна одна настройка. Изоляция PID-пространства имён на Linux по умолчанию имеет значение true, а собственная документация Windmill предписывает выставлять для воркеров на Windows значение false. Задайте переменной с именем ENABLE_UNSHARE_PID значение false на воркере с Windows, и он запустится нормально.
Для двух других строк адрес должен лежать в окружении воркера, а не в скрипте. Windmill по умолчанию отдаёт заданию не все переменные хоста, поэтому перечислите нужные через запятую в переменной с именем WHITELIST_ENVS на воркере. Группа воркеров также может нести собственные статические и динамические переменные окружения, задаваемые в интерфейсе, и это более аккуратный вариант, когда рядом с решателем стоит только часть ваших воркеров.
Шаг 4: таймаут и повтор
Windmill кладёт поле Timeout в настройки выполнения скрипта, рядом с ограничениями Cache и Concurrency. Ставьте его выше вашего самого медленного решения, а не ниже. Чекбокс reCAPTCHA v2 обычно укладывается заметно меньше чем в минуту, но страницы проверки Turnstile и GeeTest занимают больше, и SDK будет опрашивать до 300 секунд, прежде чем сдастся с TimeoutException. Таймаут скрипта ниже этого значения превращает медленное решение в убитое задание, в логе которого нет ничего полезного.
Как только скрипт становится шагом flow, появляется второй слой. Шаги flow в Windmill повторяются в двух формах, и экспоненциальная подходит решателю, который ненадолго занят.
| Форма повтора | Что вы настраиваете | Когда её использовать |
|---|---|---|
| Постоянная задержка | Максимальное число попыток и фиксированная задержка | Решатель, который иногда перезапускается, когда постоянного ожидания достаточно |
| Экспоненциальная задержка | Максимальное число попыток, основание в секундах и множитель | Всё, что может быть действительно занято, чтобы вы увеличивали паузу, а не долбили запросами |
Задержка для экспоненциальной формы рассчитывается как множитель, умноженный на основание в степени номера попытки, поэтому основание 3 с множителем 2 на пяти попытках растягивает ожидания с 6 секунд до 486. Есть ещё настройка Continue on error, которая позволяет flow идти дальше после исчерпания повторов и передаёт ошибку как результат шага: так и строится ветка, которая откатывается на запасной вариант, а не роняет весь запуск.
Полный рабочий пример
Один скрипт, который решает капчу и отправляет форму, чтобы токен не лежал в ожидании следующего шага. Последний пункт не про стиль. Токен reCAPTCHA живёт около двух минут, и flow, который решает капчу на одном шаге, ждёт согласования, а потом отправляет на другом, гарантированно его теряет. Подробнее об этом в руководстве по истечению срока действия токена reCAPTCHA.
# py312
# pip install capskip requests - both resolved from these imports.
import os
import requests
import wmill
from capskip import CapSkip
from capskip.exceptions import TimeoutException, NetworkException
SITE = "https://example.com/page-with-recaptcha"
def main(sitekey: str, username: str) -> dict:
# CAPSKIP_HOST is set on the worker and allowed through by
# WHITELIST_ENVS. It falls back to the loopback address so the
# same script still runs on a worker that sits next to CapSkip.
solver = CapSkip(
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
port=8080,
apiKey=wmill.get_variable("u/admin/capskip_key"),
)
try:
result = solver.recaptcha(sitekey=sitekey, url=SITE)
except TimeoutException:
# Let the flow's retry policy decide what happens next.
raise
except NetworkException:
raise RuntimeError("CapSkip is unreachable from this worker")
# Submit immediately. The token is short lived, and the field
# name below is the one the page's own form posts.
posted = requests.post(
SITE,
data={
"username": username,
"g-recaptcha-response": result["code"],
},
timeout=30,
)
return {"status": posted.status_code, "captcha_id": result["captchaId"]}Последний скрипт возвращает id капчи, а не токен, и сделано это намеренно. Id пригодится, когда вы позже разбираете историю запусков Windmill, а токен бесполезен: к тому моменту он уже истёк, а если положить его в сохранённый результат задания, он попадёт в ваши логи.
Решение нескольких сразу
Воркер Windmill выполняет один скрипт за раз и использует всю машину, которая у него есть. Поэтому параллельность здесь определяется тем, сколько воркеров вы запускаете, а не тем, что делает ваш скрипт. Есть два способа её получить, и они сочетаются.
- Запустить больше воркеров. Группу воркеров можно масштабировать независимо, а задания забирает тот воркер, который свободен.
- Решать капчи пачкой внутри одного скрипта. Python SDK поставляется с настоящим асинхронным клиентом, поэтому в одном задании может выполняться сразу несколько решений. Это имеет смысл, когда запуску нужно десять токенов, а не один, и этот приём разобран в руководстве по параллельному решению капчи на Python.
Поставьте скрипту ограничение параллельности, если хрупкой частью оказывается целевой сайт. Сам CapSkip не тарифицируется по счётчику, поэтому запуск большего числа таких задач ничего не добавляет к цене, а вот сайт, который вы автоматизируете, вполне может это заметить.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| NetworkException, отказ в соединении на порту 8080 | Воркер не на той машине, к которой привязан CapSkip | Переключитесь в Server mode и укажите в переменной с хостом адрес решателя |
| Переменная окружения с хостом внутри задания читается как пустая | Она есть на воркере, но её так и не разрешили пробросить | Добавьте её имя в WHITELIST_ENVS или задайте её на группе воркеров |
| ModuleNotFoundError на импорте capskip | Задание зависимостей для этой версии ещё не отработало | Сохраните и задеплойте скрипт, затем проверьте, что задание зависимостей завершилось |
| Задание убивают на середине решения | Таймаут скрипта короче, чем заняло решение | Поднимите Timeout в настройках выполнения скрипта выше 300 секунд |
| TimeoutException из SDK | Решение действительно вышло за recaptchaTimeout | Дайте flow повторить его и проверьте, что sitekey и URL страницы верные |
| ERROR_WRONG_USER_KEY в ответе | Переменная Windmill пуста, поэтому был отправлен пустой ключ | Проверьте путь к переменной, включая префикс workspace |
| Воркер на Windows не запускается | Изоляция PID-пространства имён для этого воркера не выключена | Задайте ENABLE_UNSHARE_PID значение false на этом воркере |
| Целевой сайт отклоняет действительный токен | Он истёк между шагом решения и шагом отправки | Решайте и отправляйте в одном скрипте или в соседних шагах без ожидания |
FAQ
Можно ли оставить CapSkip на адресе loopback вместе с Windmill?
Да, если воркер работает на той же машине с Windows, что и решатель. Это единственный вариант развёртывания, где Local mode выживает, и настроить его стоит осознанно: запустите на машине с решателем отдельный воркер, дайте ему собственный тег и направляйте скрипты с капчей на этот тег. Любая другая схема, включая облако Windmill и любой контейнер, требует Server mode, потому что воркер находится в другом месте.
Нужен ли файл зависимостей для SDK?
Нет. Windmill читает импорты верхнего уровня при сохранении скрипта, сопоставляет их с пакетами PyPI и генерирует lockfile для этой версии скрипта. Строка импорта и есть объявление зависимости. Если импорт падает во время выполнения, смотреть надо на то, завершилось ли задание зависимостей, а не на то, не потерялся ли файл.
Должен ли flow сам опрашивать результат?
Не для решения, которое умещается в одно задание. SDK уже опрашивает сам, и он увеличивает паузу начиная с 250 миллисекунд, а не спит фиксированный интервал, поэтому самодельный цикл из шагов ожидания медленнее и требует больше кода. Опрашивайте из flow только если вы намеренно разделили отправку и получение на два шага, и в этом случае проверяйте ответ об ожидании по имени. Он пишется CAPCHA_NOT_READY, без буквы T, и означает, что надо ждать дальше, а не что что-то пошло не так. Есть подробный разбор ответа CAPCHA_NOT_READY.
Чем это отличается от того же в Airflow, Dagster или n8n?
Windmill требует меньше всего кода из четырёх, потому что скрипт представляет собой обычную функцию, а его зависимости берутся из строки импорта. Airflow хочет задачу внутри DAG, и там надо думать про интервал планировщика, что разобрано в руководстве по DAG с капчей в Airflow. Dagster описывает ту же работу как asset, об этом в разборе Dagster. n8n работает как граф узлов, а не как среда выполнения кода, поэтому руководство по n8n строится вокруг его узла HTTP Request. Вопрос подключения во всех четырёх одинаковый.
Коротко
Импортируйте SDK в начале скрипта Windmill и дайте заданию зависимостей зафиксировать версию. Положите ключ решателя в переменную Windmill, а его адрес в переменную окружения воркера, которую пропускает WHITELIST_ENVS. Режим подключения выбирайте по тому, где работает воркер, а не по тому, где сидите вы: воркер на той же машине с Windows, где стоит решатель, сохраняет Local mode, а всему остальному нужен Server mode. Поставьте таймаут скрипта выше 300 секунд, добавьте экспоненциальную задержку на шаге flow и отправляйте токен в том же задании, которое его получило.
- Сам Python SDK описан на странице сервиса распознавания капч для Python.
- Капча с чекбоксом описана на странице сервиса распознавания reCAPTCHA v2.
- Эквивалентные вызовы на Node.js, PHP и C# перечислены на на странице SDK для распознавания капчи.
Одно соображение перед тем, как ставить это в расписание каждые пять минут: CapSkip работает как локальный сервис распознавания капчи на железе, которое у вас уже есть, поэтому постоянно срабатывающий flow стоит ровно столько же, сколько срабатывающий изредка.
