Как решать капчу в задаче Celery (очередь на Python)

У задачи Celery, решающей капчу, есть одно правило, которое определяет всё остальное: решать нужно с нуля при каждой попытке. Celery обеспечивает доставку не менее одного раза, поэтому задача может выполниться дважды, а результат CapSkip читается только один раз. Сохраните id капчи, продолжите с него после повтора, и вы не получите ничего. Решите капчу, используйте токен и завершите работу, всё это внутри одного тела задачи.
Что понадобится
- Celery 5 с брокером, Redis или RabbitMQ. Выбор брокера позже повлияет на одну настройку.
- CapSkip, запущенный на машине с Windows, и клиент на Python, установленный в образ воркера.
- Server mode практически в любом реальном развёртывании. Воркеры обычно работают в Linux-контейнерах, а решатель нет.
- Sitekey и URL страницы, переданные как аргументы задачи, а не зашитые в неё.
# pip install capskip pip install -U celery[redis] capskip
Шаг 1: сама задача
Всё решение умещается в один вызов. SDK отправляет запрос, опрашивает результат и возвращает токен, поэтому нет ни id, который надо тащить между шагами, ни чего-либо, что надо сохранять.
# pip install capskip
import os
from celery import Celery
from capskip import CapSkip, NetworkException
app = Celery("solves", broker="redis://redis:6379/0")
# CAPSKIP_HOST is the solver machine. Loopback only works if the
# worker runs on the same Windows box as CapSkip.
solver = CapSkip(
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
port=int(os.environ.get("CAPSKIP_PORT", "8080")),
apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
)
@app.task(
bind=True,
autoretry_for=(NetworkException,),
retry_backoff=True,
max_retries=3,
soft_time_limit=330,
time_limit=360,
)
def solve_and_submit(self, sitekey, page_url):
result = solver.recaptcha(sitekey=sitekey, url=page_url)
return submit_form(page_url, result["code"]) # token, used hereОбратите внимание на то, чего здесь нет. В возвращаемом значении нет id капчи, нет второй задачи для отправки токена, нет результата, отложенного на потом. Токен используется в той же задаче, которая его получила. Причина кроется в тайминге, а не в аккуратности, и разбирается она ниже, в разделе про повторы.
Этот вызов относится к reCAPTCHA v2. Остальные типы, которые поддерживает CapSkip, устроены точно так же: передайте invisible или enterprise со значением 1, либо version со значением v3 и действие, либо вызовите turnstile или geetest. Полная поверхность API описана на странице сервиса распознавания капч для Python.
Шаг 2: два лимита времени и куда их поставить
По умолчанию в Celery нет лимита времени ни в одной из двух настроек. Для задачи, которая ждёт ответа сетевого сервиса, такое значение по умолчанию не подходит, потому что зависшее решение капчи занимает слот воркера навсегда.
Задайте обе настройки и выставьте их выше того, что допускает сам SDK. CapSkip отводит на решение reCAPTCHA, Turnstile или GeeTest триста секунд, а на картиночную капчу сто двадцать, и оба значения настраиваются в клиенте. Если первым сработает мягкий лимит, Celery выбросит SoftTimeLimitExceeded внутри вашей задачи, и вы потеряете собственное исключение SDK TimeoutException, а оно полезнее, потому что говорит: до решателя дозвонились, но работу он не закончил.
| Какая настройка | Рекомендуемое значение для решения капчи | Зачем |
|---|---|---|
| soft_time_limit | 330 секунд | На тридцать секунд выше собственного потолка решателя, чтобы SDK сообщил об ошибке первым |
| time_limit | 360 секунд | Страховка. В этот момент воркер убивает процесс |
| recaptchaTimeout в клиенте | 300 секунд, значение по умолчанию | Уменьшите его, если предпочитаете быстро упасть, а не ждать |
| defaultTimeout в клиенте | 120 секунд, значение по умолчанию | Только для картиночных капч. Они редко занимают секунды, не говоря уже о минутах |
Если вы прогоняете картиночные капчи и reCAPTCHA через один и тот же воркер, разведите их по отдельным задачам с отдельными лимитами, а не сводите в одну задачу с более высокой парой значений. Потолок в триста секунд для работы, которая обычно завершается за секунду, прячет настоящие сбои на пять минут.
Шаг 3: правило повторов, специфичное именно для решения капчи
Автоматические повторы Celery идеально подходят для решателя, который перезапускается, и совершенно не подходят для токена, который у вас уже на руках. В этом различии стоит разобраться точно.
При retry_backoff со значением True первый повтор ждёт одну секунду, затем две, затем четыре, затем восемь, а джиттер включён по умолчанию, поэтому реальная задержка представляет собой случайное значение вплоть до этого максимума. Ограничение задаётся через retry_backoff_max и по умолчанию равно шестистам секундам. Сравните это с токеном reCAPTCHA, который живёт около двух минут.
Получается, что задача, которая успешно решила капчу, сохранила токен, упала на отправке и ушла в повтор, может возобновиться после задержки, в несколько раз превышающей срок жизни токена. Она упрётся в токен, который был совершенно действителен в момент создания, а лог обвинит целевой сайт. Этот сценарий отказа разобран в руководстве по истечению срока действия токена reCAPTCHA.
Исправление состоит в той форме задачи, что показана выше: решайте капчу внутри повтора, а не до него.
Ограничьте autoretry_for теми исключениями, которые описывают проблему транспорта. NetworkException означает, что CapSkip был недоступен, и такой случай стоит повторить. ApiException и ValidationException означают, что запрос был неверным и останется неверным. С TimeoutException придётся решать по обстоятельствам, и обычно он заслуживает одного повтора, а не трёх.
Шаг 4: acks_late и почему результат можно прочитать только один раз
По умолчанию Celery подтверждает сообщение прямо перед его выполнением, поэтому воркер, умерший посреди задачи, теряет работу. Включение task_acks_late переносит подтверждение на момент после завершения задачи, поэтому работа упавшего воркера доставляется повторно и выполняется снова. Обычно именно это и нужно для работы, которая где-то стоит денег, и именно из-за этого правило однократного чтения становится важным.
Результат CapSkip читается только один раз. Если первая попытка отправила капчу, прочитала токен и упала до подтверждения, повторно доставленная попытка уже не сможет перечитать этот id. Ей придётся отправлять заново. Задача выше делает именно так, потому что не хранит состояние между попытками, и повторное решение обходится в несколько секунд работы вашего собственного оборудования, а не во второе списание денег.
# Redelivery is safe here because the task resolves rather # than resuming. Pair it with reject_on_worker_lost so a # killed worker requeues instead of dropping the job. app.conf.task_acks_late = True app.conf.task_reject_on_worker_lost = True # Long tasks and a prefetch of 4 means idle workers sit on # queued jobs. Drop it to 1 for solve queues. app.conf.worker_prefetch_multiplier = 1
Последний пункт превращается в тихую проблему производительности в большинстве очередей на решение капчи. Множитель предвыборки по умолчанию равен четырём, поэтому каждый процесс воркера резервирует четыре сообщения заранее. Для задач, которые занимают миллисекунды, это даёт выигрыш. Для задач, которые ждут решения капчи, три из этих четырёх стоят зарезервированными за работой, которая не делает ничего, кроме опроса, пока другой воркер простаивает.
Шаг 5: настройка брокера, которая дублирует решения
Если ваш брокер Redis, есть ещё одно число. У Redis нет собственного механизма подтверждения, поэтому Celery эмулирует его через visibility timeout: количество секунд, которое он ждёт подтверждения задачи от воркера, прежде чем передать сообщение другому воркеру. По умолчанию значение равно одному часу, и находится оно внутри broker_transport_options, а не существует как отдельная настройка.
Один час с запасом превышает трёхсотсекундное решение капчи, поэтому значение по умолчанию безопасно. Проблемы начинаются, когда его снижают, чтобы упавшие работы восстанавливались быстрее, ведь собственная документация Celery предупреждает: задача, время выполнения которой превышает visibility timeout, выполняется снова и снова, по кругу. Тогда каждое медленное решение капчи запускается минимум дважды.
# Keep this above your hard time limit, not near it.
# 3600 is the default and it is fine. If you must lower it,
# stay well clear of the 360 second time_limit above.
app.conf.broker_transport_options = {"visibility_timeout": 3600}RabbitMQ подтверждает сообщения нативно и не имеет аналогичной настройки, и в этом состоит одна из причин предпочесть его для очередей, забитых медленными задачами.
Шаг 6: запуск решателя там, где воркеры смогут до него дотянуться
Воркеры Celery обычно работают в Linux-контейнерах на кластере. CapSkip работает на Windows. Поэтому на практике воркер и решатель находятся на разных машинах, и loopback здесь не подходит.
Для этого у CapSkip есть два режима подключения. Local привязывается к 127.0.0.1 и обслуживает только это устройство. Server привязывается к вашему сетевому адресу или публичному IP, поэтому другая машина, хост контейнеров или облачная платформа могут обратиться к той же машине с Windows по API. Оба режима находятся в разделе Настройки подключения, а Server mode меняет только то, по какому адресу слушает решатель. Оборудование по-прежнему ваше, и распознавание капчи по-прежнему безлимитное.
Именно последний пункт делает разумной саму идею очереди, которая срабатывает постоянно.
| Где работают воркеры | Какой режим подключения |
|---|---|
| На той же машине с Windows, что и CapSkip | Local mode, хост остаётся 127.0.0.1 |
| В Docker или на другой машине в вашей сети | Server mode с локальным адресом решателя |
| На управляемой платформе или в облачном кластере | Server mode со статическим публичным IP и правилом брандмауэра |
Читайте хост и ключ из окружения, а не из кода. Клиент на Python сам не читает ни CAPSKIP_HOST, ни CAPSKIP_PORT, ни CAPSKIP_API_KEY, именно поэтому задача из шага 1 читает их и передаёт клиенту. После этого контейнеру воркера нужны только эти переменные и больше ничего.
Решение множества капч одновременно
Способов два, и они подходят для разных форм работы. Обычный ответ выглядит так: одна задача на одну капчу, а параллелизм обеспечивает конкурентность воркера, и именно об этом замечание про предвыборку выше. Для пачки, которая приходит целиком, у клиента на Python есть настоящая асинхронная реализация, поэтому одна задача может собрать пачку внутри себя.
import asyncio
import os
from capskip import AsyncCapSkip
# AsyncCapSkip in Python is a real async client, not an alias.
async def solve_batch(pairs):
solver = AsyncCapSkip(
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"), port=8080)
return await asyncio.gather(*[
solver.recaptcha(sitekey=k, url=u) for k, u in pairs
])
@app.task(soft_time_limit=330, time_limit=360)
def solve_many(pairs):
return [r["code"] for r in asyncio.run(solve_batch(pairs))]Стоит знать, прежде чем переносить это на другой язык: клиенты для Node и .NET тоже называют класс AsyncCapSkip, но там он служит псевдонимом, а не второй реализацией. Python оказывается единственным языком, где это что-то значит. Полное сравнение приведено в руководстве по параллельному решению капчи на Python.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| NetworkException на каждой задаче | CapSkip привязан к loopback, а воркер находится в другом месте | Переключитесь на Server mode и укажите в CAPSKIP_HOST адрес решателя |
| SoftTimeLimitExceeded вместо TimeoutException | Мягкий лимит ниже собственного потолка клиента | Поднимите soft_time_limit выше 300 или уменьшите recaptchaTimeout |
| Одна и та же работа выполняется дважды при медленном решении | Значение visibility timeout в Redis короче самой задачи | Поднимите его заметно выше жёсткого лимита времени |
| Повтор падает с истёкшим токеном | Токен был получен до повтора, а не внутри него | Перенесите решение капчи внутрь тела задачи, как показано выше |
| Чтение того же id капчи ничего не возвращает | Результат CapSkip читается только один раз | Никогда не сохраняйте id между попытками. Решайте капчу заново |
| ERROR_WRONG_USER_KEY внутри ApiException | CAPSKIP_API_KEY не задан в окружении воркера | Задайте его в окружении воркера и перезапустите воркер |
| Воркеры простаивают, пока очередь растёт | Предвыборка резервирует длинные задачи за длинными задачами | Задайте worker_prefetch_multiplier равным 1 для очередей решения капчи |
| Работы исчезают, когда воркер убивают | Позднее подтверждение выключено | Включите task_acks_late и task_reject_on_worker_lost |
Об ошибках с ключом стоит почитать отдельно, потому что один и тот же ответ покрывает и отсутствующий ключ, и просто неверный: как исправить ERROR_WRONG_USER_KEY.
FAQ
Нужно ли разносить решение капчи и отправку по двум задачам?
Нет. Разделение выглядит соблазнительно, потому что две половины падают по разным причинам, а цепочка смотрится аккуратнее в мониторинге. Но токен reCAPTCHA живёт около двух минут, а задача в очереди может пролежать дольше, поэтому вторая половина регулярно работает с токеном, который уже истёк. Держите их вместе и позволяйте всей задаче повторяться целиком. Повторное решение капчи стоит вам нескольких секунд работы вашей собственной машины.
Безопасен ли acks_late при решении капчи?
Да, если задача решает капчу заново, а не продолжает с места остановки. Позднее подтверждение означает, что работа упавшего воркера доставляется повторно и выполняется второй раз, поэтому задача должна безопасно переноситься. Задача, которая каждый раз отправляет свежую капчу, такому требованию отвечает. Задача, которая сохранила id и пытается перечитать его, не отвечает, потому что результат читается только один раз. Вариант из этого руководства имеет безопасную форму.
Могут ли воркеры работать на Linux, если решатель работает на Windows?
Да, и это обычная схема. Воркеру нужно всего лишь дотянуться до HTTP-эндпоинта, поэтому он может быть Linux-контейнером в любой точке сети, пока решатель работает на машине с Windows в режиме Server mode. Направьте CAPSKIP_HOST на эту машину. Решение капчи при этом не становится ни платным по счётчику, ни удалённым в том смысле, который важен: оборудование по-прежнему ваше.
Чем это отличается от запуска решений капчи в Airflow?
Airflow планирует граф шагов и передаёт данные между ними, поэтому интересный вопрос там состоит в том, какую границу пересекает токен. Celery работает как очередь, поэтому интересный вопрос звучит иначе: что происходит, когда одно и то же сообщение доставляется дважды. Вызов решения капчи при этом идентичен. Вопрос границ полностью разобран в руководстве по DAG в Airflow.
Коротко
Поместите решение капчи и всё, что использует токен, в одну задачу. Задайте мягкий лимит 330 и жёсткий лимит 360, чтобы собственный таймаут клиента сообщил об ошибке первым. Повторяйте только по NetworkException и позволяйте повтору решать капчу заново, а не продолжать с места остановки, потому что задержка перед повтором может намного пережить токен, а результат читается только один раз. Включите позднее подтверждение, снизьте множитель предвыборки до 1 и держите visibility timeout в Redis намного выше жёсткого лимита. Запускайте CapSkip в режиме Server mode всякий раз, когда воркеры находятся не на той же машине, что и решатель.
- Сам чекбокс reCAPTCHA v2 разобран на странице сервиса распознавания reCAPTCHA v2.
- Сырые эндпоинты, которые стоят за клиентом, описаны в документации CapSkip API.
И последнее, что стоит взвесить перед выбором размера очереди. CapSkip представляет собой распознавание капчи на оборудовании, которое у вас уже есть, поэтому сто воркеров, долбящих его без остановки, и один воркер, неспешно перемалывающий те же задачи, обходятся одинаково: в ноль.
