Потолок параллелизма в CaptchaAI задаёт не ваш код, а тариф: BASIC ($15/мес, 5 потоков) удерживает пять задач одновременно, ADVANCE ($90/мес, 50 потоков) — пятьдесят. Семафор нужен ровно для того, чтобы приложение соблюдало эту границу само, а не узнавало о ней из ответов с ошибками и растущих очередей.
Ниже — шаблоны на Python (asyncio.Semaphore) и Node.js, приём с раздельными лимитами на отправку и опрос, адаптивный семафор и разбор типичных сбоев.
Потоки тарифа задают потолок, семафор его соблюдает
Тарификация CaptchaAI построена на потоках, а не на числе решений. Поток — это одна задача CAPTCHA «в полёте»: как только решение возвращается, поток берёт следующую. Число решений на поток внутри месяца не ограничено, отдельной платы за каждую CAPTCHA нет.
Отсюда правило: значение семафора выставляют по числу потоков плана, а не по числу ядер или длине списка ссылок.
| Тариф | Цена | Потоков | Ориентир для семафора |
|---|---|---|---|
| BASIC | $15/мес | 5 | 5 |
| STANDARD | $30/мес | 15 | 12–15 |
| ADVANCE | $90/мес | 50 | 40–50 |
| PREMIUM | $170/мес | 100 | 80–100 |
| CORPORATE | $240/мес | 150 | 120–150 |
Выше идут ENTERPRISE ($300/мес, 200 потоков) и VIP-3 ($7,500/мес, 5000 потоков). Небольшой запас относительно лимита полезен: часть потоков может занимать соседний воркер или cron-задача.
Пропускная способность зависит от времени решения: 50 потоков при цикле в 20 с дают около 9000 задач в час. Скорость различается по типам — Cloudflare Turnstile решается менее чем за 10 с, GeeTest v3 — менее чем за 12 с, reCAPTCHA v2 — менее чем за 60 с.
Что меняется, когда параллелизм ограничен
| Без семафора | С семафором |
|---|---|
| 100 запросов уходят одной волной | Ровно 20 задач в работе |
Ответы 429 и потерянные задачи |
Частота запросов остаётся в пределах лимита |
| Плавающее время выполнения пакета | Предсказуемая пропускная способность |
| Скачки потребления памяти | Ровный расход ресурсов |
Механика простая: семафор — это счётчик. Задача занимает слот и уменьшает счётчик, а завершаясь, возвращает его обратно. Пока счётчик равен нулю, новые задачи ждут в очереди, а не уходят в сеть. Сложность не в счётчике, а в том, чтобы слот возвращался даже при исключении.
Python: asyncio.Semaphore
Базовый шаблон: один семафор на весь цикл задачи
Самый прямой вариант — держать семафор захваченным на протяжении всей задачи: и на отправке в in.php, и на опросе res.php. Конструкция async with sem освобождает счётчик автоматически, в том числе при исключении, поэтому «залипших» слотов не остаётся.
import asyncio
import aiohttp
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
async def solve_one(session, sem, sitekey, page_url):
"""Solve one CAPTCHA within the semaphore limit."""
async with sem:
# Submit
async with session.post(SUBMIT_URL, data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1",
}) as resp:
data = await resp.json()
if data["status"] != 1:
return {"url": page_url, "error": data["request"]}
task_id = data["request"]
# Poll (still within semaphore)
for _ in range(24):
await asyncio.sleep(5)
async with session.get(RESULT_URL, params={
"key": API_KEY, "action": "get", "id": task_id, "json": "1"
}) as resp:
result = await resp.json()
if result["status"] == 1:
return {"url": page_url, "token": result["request"]}
if result["request"] != "CAPCHA_NOT_READY":
return {"url": page_url, "error": result["request"]}
return {"url": page_url, "error": "TIMEOUT"}
async def solve_batch(tasks, max_concurrent=20):
sem = asyncio.Semaphore(max_concurrent)
async with aiohttp.ClientSession() as session:
coros = [
solve_one(session, sem, t["sitekey"], t["url"])
for t in tasks
]
results = await asyncio.gather(*coros)
solved = sum(1 for r in results if "token" in r)
print(f"Solved {solved}/{len(results)}")
return results
Здесь max_concurrent — это и есть ваш потолок. Двадцать одновременных задач соответствуют примерно половине лимита ADVANCE, то есть у пайплайна остаётся запас на пиковые часы.
Раздельные семафоры на отправку и опрос
Отправка занимает доли секунды, а ожидание результата — десятки секунд, поэтому при общем семафоре слот почти всё время простаивает. Разделите лимиты: жёсткий на отправку и более свободный на опрос, поскольку GET-запросы к res.php дёшевы.
async def solve_split_sems(session, submit_sem, poll_sem, sitekey, page_url):
# Submit phase — short, limited to 30 concurrent
async with submit_sem:
async with session.post(SUBMIT_URL, data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1",
}) as resp:
data = await resp.json()
if data["status"] != 1:
return {"error": data["request"]}
task_id = data["request"]
# Poll phase — longer, limited to 50 concurrent
async with poll_sem:
for _ in range(24):
await asyncio.sleep(5)
async with session.get(RESULT_URL, params={
"key": API_KEY, "action": "get", "id": task_id, "json": "1"
}) as resp:
result = await resp.json()
if result["status"] == 1:
return {"token": result["request"]}
if result["request"] != "CAPCHA_NOT_READY":
return {"error": result["request"]}
return {"error": "TIMEOUT"}
async def main(tasks):
submit_sem = asyncio.Semaphore(30) # 30 concurrent submits
poll_sem = asyncio.Semaphore(50) # 50 concurrent polls
async with aiohttp.ClientSession() as session:
coros = [
solve_split_sems(session, submit_sem, poll_sem, t["sitekey"], t["url"])
for t in tasks
]
return await asyncio.gather(*coros)
На нестабильных каналах схема ведёт себя ровнее: медленный опрос одной задачи не блокирует отправку остальных.
Node.js: собственный семафор на промисах
Встроенного семафора в Node.js нет, но он собирается из очереди resolve-функций. Класс ниже повторяет поведение asyncio.Semaphore: acquire() возвращает промис, который выполняется сразу или встаёт в очередь, а release() передаёт слот следующему ожидающему.
class Semaphore {
constructor(max) {
this.max = max;
this.current = 0;
this.queue = [];
}
acquire() {
return new Promise(resolve => {
if (this.current < this.max) {
this.current++;
resolve();
} else {
this.queue.push(resolve);
}
});
}
release() {
this.current--;
if (this.queue.length > 0) {
this.current++;
const next = this.queue.shift();
next();
}
}
}
Подключение семафора к решению задач
Ключевой момент — блок try/finally. Без него любое исключение внутри задачи навсегда съедает слот, и после нескольких ошибок пайплайн останавливается целиком, хотя лимиты API не превышены.
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
const sem = new Semaphore(20);
async function solveOne(sitekey, pageUrl) {
await sem.acquire();
try {
const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
params: {
key: API_KEY,
method: 'userrecaptcha',
googlekey: sitekey,
pageurl: pageUrl,
json: 1,
},
});
if (submit.data.status !== 1) {
return { url: pageUrl, error: submit.data.request };
}
const taskId = submit.data.request;
for (let i = 0; i < 24; i++) {
await new Promise(r => setTimeout(r, 5000));
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: taskId, json: 1 },
});
if (poll.data.status === 1) {
return { url: pageUrl, token: poll.data.request };
}
if (poll.data.request !== 'CAPCHA_NOT_READY') {
return { url: pageUrl, error: poll.data.request };
}
}
return { url: pageUrl, error: 'TIMEOUT' };
} finally {
sem.release();
}
}
// Solve 100 tasks with max 20 concurrent
async function solveBatch(tasks) {
const results = await Promise.all(
tasks.map(t => solveOne(t.sitekey, t.url))
);
const solved = results.filter(r => r.token).length;
console.log(`Solved: ${solved}/${results.length}`);
return results;
}
Адаптивный семафор: подстройка под долю ошибок
Фиксированное значение хорошо работает на стабильной нагрузке. Если целевые страницы, сеть или время суток меняются, полезнее семафор, который сам двигает планку: снижает параллелизм при росте доли ошибок и осторожно поднимает его, когда пакет проходит чисто.
class AdaptiveSemaphore:
def __init__(self, initial=20, min_val=5, max_val=50):
self.value = initial
self.min_val = min_val
self.max_val = max_val
self.sem = asyncio.Semaphore(initial)
self.success_count = 0
self.error_count = 0
async def acquire(self):
await self.sem.acquire()
def release(self, success=True):
self.sem.release()
if success:
self.success_count += 1
else:
self.error_count += 1
total = self.success_count + self.error_count
if total % 20 == 0:
self._adjust()
def _adjust(self):
error_rate = self.error_count / (self.success_count + self.error_count)
if error_rate > 0.2 and self.value > self.min_val:
self.value = max(self.min_val, self.value - 5)
self.sem = asyncio.Semaphore(self.value)
print(f"Reduced concurrency to {self.value}")
elif error_rate < 0.05 and self.value < self.max_val:
self.value = min(self.max_val, self.value + 5)
self.sem = asyncio.Semaphore(self.value)
print(f"Increased concurrency to {self.value}")
self.success_count = 0
self.error_count = 0
Пересчёт раз в двадцать задач намеренно инертен: слишком частая подстройка сама становится источником колебаний. Логи Reduced concurrency и Increased concurrency стоит писать в метрики — по ним видно, во что упирается пайплайн.
Сценарий: ночной парсинг у команды из Алматы
Команда собирает каталог из 40 000 страниц в окно с 01:00 до 05:00; reCAPTCHA v2 встречается примерно на каждой десятой странице — около 1000 задач в час. При цикле решения в 40 с один поток закрывает 90 задач в час, значит нужно порядка 12 потоков: тариф STANDARD ($30/мес, 15 потоков) подходит с запасом.
Семафор выставляется на 12, отправка ограничивается отдельно, значение параллелизма пишется в метрики. Для команд, получающих выручку в тенге, рублях или гривне, важно и другое: месячная стоимость плана фиксирована в USD и не зависит от числа задач за ночь.
И отдельная оговорка про данные: собирайте только те сведения, которые вы вправе обрабатывать. Для проектов с российскими пользователями ориентиром служит 152-ФЗ «О персональных данных», для трансграничных — практики уровня GDPR. Семафор ограничивает нагрузку, но не отвечает за правомерность сбора.
Что чаще всего ломается
| Симптом | Причина | Что сделать |
|---|---|---|
| Пайплайн замер, задачи не стартуют | Слот не вернулся после исключения | Обернуть работу в try/finally и всегда вызывать release() |
Ответы 429 не исчезли |
Значение семафора выше лимита потоков | Снизить значение до числа потоков плана |
| Пропускная способность просела | Значение слишком низкое | Поднять лимит или разделить отправку и опрос |
| Память растёт на длинных пакетах | Задачи стоят в очереди бесконечно | Добавить тайм-аут на acquire() и отбраковку просроченных задач |
Часть задач уходит в TIMEOUT |
Цикл опроса короче времени решения типа CAPTCHA | Увеличить число итераций опроса под конкретный тип |
Чек-лист перед боевым запуском
- Значение семафора не превышает число потоков вашего плана.
- Слот освобождается в
finally, а не в конце «счастливого пути». - Отправка и опрос ограничены раздельно, если время решения превышает 10 с.
- Значение параллелизма и доля ошибок пишутся в метрики.
- Все воркеры делят один общий лимит.
Часто задаваемые вопросы
Как связать число потоков тарифа со значением семафора?
Один поток — одна задача в работе, поэтому значение семафора не должно превышать число потоков плана. На STANDARD ($30/мес, 15 потоков) разумно ставить 12–15, оставляя запас на отладочные запуски.
Что произойдёт, если задач окажется больше, чем потоков?
Параллельно они всё равно не выполнятся: часть запросов упрётся в ограничение частоты, и их придётся повторять. Семафор переносит эту очередь с API внутрь вашего процесса, где ею можно управлять.
Как ограничить параллелизм, если воркеров несколько?
Локальный семафор действует только внутри процесса. При нескольких воркерах общий лимит выносят во внешний счётчик, например в Redis, а локальные значения делают долей от общего.
Работают ли эти шаблоны с другими типами CAPTCHA?
Да, схема от типа не зависит: меняются только параметр method и ожидаемое время решения. Она применима к reCAPTCHA v2 и v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3 и задачам распознавания изображений. Учтите, что hCaptcha, FunCaptcha и GeeTest v4 сервисом не поддерживаются, а CaptchaFox (beta), Friendly Captcha (beta) и Lemin (beta) доступны только в бета-режиме.
Подключите решение CAPTCHA к своему пайплайну
Получите API-ключ и сверьте число потоков в тарифах на сайте CaptchaAI, затем выставьте семафор по этому числу.