API Tutorials

Bash Script + cURL + CaptchaAI: автоматизация Shell CAPTCHA

Когда cron-задача, CI/CD-пайплайн или скрипт мониторинга упирается в CAPTCHA, ставить ради одного HTTP-вызова целый Python- или Node.js-рантайм — избыточно. HTTP API CaptchaAI прекрасно работает через обычный cURL: POST-запрос на in.php, опрос res.php — и никакой среды выполнения, кроме bash, curl и jq, которые и так есть на любом сервере Linux/macOS.

Ниже — рабочие bash-функции для reCAPTCHA v2/v3, Cloudflare Turnstile и графических CAPTCHA.

От одиночного вызова до готовой библиотеки, которую можно подключать в cron, Docker и CI/CD.


Когда Bash + curl удобнее полноценного рантайма

Ноль зависимостей: bash и curl входят в базовую поставку любого дистрибутива Linux/macOS, поэтому не нужен ни отдельный рантайм, ни менеджер пакетов, ни этап установки. Задачи, завязанные на CAPTCHA, планируются штатным cron без обёрток, а сам подход одинаково хорошо работает в Docker, GitHub Actions, Jenkins и GitLab CI — результат сразу прогоняется через jq, grep, awk и другие unix-утилиты.

Разница особенно заметна в Alpine-образах: полный набор — bash, curl и jq — весит около 10 МБ, тогда как Python-рантайм с зависимостями обычно набирает не один десяток мегабайт. Для QA-конвейера, который ночью прогоняет staging-логин через reCAPTCHA на арендованном европейском VPS (Hetzner — популярный выбор среди команд из России, Беларуси и Казахстана), это разница между секундной сборкой контейнера и минутной.


Что понадобится перед стартом

  1. Bash 4.0+
  2. curl (установлен по умолчанию в Linux/macOS)
  3. jq для разбора JSON: apt install jq или brew install jq
  4. API-ключ CaptchaAI (получить здесь)

Если ставить jq на сервере нельзя, значения можно вытащить grep/sed, но на нестандартных ответах API это менее надёжно — везде дальше используется именно jq.


Базовые функции: отправка задачи и опрос результата

Вся библиотека строится на двух примитивах API CaptchaAI: in.php принимает задачу и возвращает task_id, res.php отдаёт результат, когда он готов. Сначала — обёртка над отправкой.

Функция отправки задачи

#!/bin/bash

CAPTCHAAI_URL="https://ocr.captchaai.com"

submit_task() {
    local api_key="$1"
    shift
    local params=("$@")

    local response
    response=$(curl -s -X POST "${CAPTCHAAI_URL}/in.php" \
        -d "key=${api_key}" \
        -d "json=1" \
        "${params[@]}")

    local status
    status=$(echo "$response" | jq -r '.status')
    local request
    request=$(echo "$response" | jq -r '.request')

    if [ "$status" != "1" ]; then
        echo "ERROR: Submit failed: $request" >&2
        return 1
    fi

    echo "$request"
}

Функция читает API-ключ первым аргументом.

Любые дополнительные пары -d "..." для конкретного метода передаются как есть — не приходится дублировать логику curl под каждый тип CAPTCHA.

Функция опроса результата

poll_result() {
    local api_key="$1"
    local task_id="$2"
    local max_wait="${3:-300}"
    local interval="${4:-5}"

    local elapsed=0

    while [ "$elapsed" -lt "$max_wait" ]; do
        sleep "$interval"
        elapsed=$((elapsed + interval))

        local response
        response=$(curl -s "${CAPTCHAAI_URL}/res.php?key=${api_key}&action=get&id=${task_id}&json=1")

        local status
        status=$(echo "$response" | jq -r '.status')
        local request
        request=$(echo "$response" | jq -r '.request')

        if [ "$request" = "CAPCHA_NOT_READY" ]; then
            echo "Waiting... (${elapsed}s/${max_wait}s)" >&2
            continue
        fi

        if [ "$status" != "1" ]; then
            echo "ERROR: Solve failed: $request" >&2
            return 1
        fi

        echo "$request"
        return 0
    done

    echo "ERROR: Timeout after ${max_wait}s" >&2
    return 1
}

Пока задача не готова, res.php отдаёт CAPCHA_NOT_READY (да, именно с такой опечаткой в самом API).

Функция просто ждёт interval секунд и опрашивает снова, пока не истечёт max_wait.


Решение reCAPTCHA v2 через curl

Дальше — специализированные обёртки поверх submit_task/poll_result под конкретный тип CAPTCHA.

Начнём с самого частого случая.

solve_recaptcha_v2() {
    local api_key="$1"
    local site_url="$2"
    local sitekey="$3"

    echo "Submitting reCAPTCHA v2..." >&2
    local task_id
    task_id=$(submit_task "$api_key" \
        -d "method=userrecaptcha" \
        -d "googlekey=${sitekey}" \
        -d "pageurl=${site_url}")

    if [ $? -ne 0 ]; then return 1; fi
    echo "Task ID: $task_id" >&2

    echo "Polling for solution..." >&2
    local token
    token=$(poll_result "$api_key" "$task_id")

    if [ $? -ne 0 ]; then return 1; fi
    echo "$token"
}

# Usage
API_KEY="YOUR_API_KEY"
TOKEN=$(solve_recaptcha_v2 "$API_KEY" \
    "https://staging.example.com/qa-login" \
    "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-")

echo "Token: ${TOKEN:0:50}..."

Решение Cloudflare Turnstile через curl

Turnstile устроен так же.

Единственное отличие от reCAPTCHA v2 на уровне запроса — site key передаётся через параметр key, а не googlekey.

solve_turnstile() {
    local api_key="$1"
    local site_url="$2"
    local sitekey="$3"

    local task_id
    task_id=$(submit_task "$api_key" \
        -d "method=turnstile" \
        -d "key=${sitekey}" \
        -d "pageurl=${site_url}")

    if [ $? -ne 0 ]; then return 1; fi

    poll_result "$api_key" "$task_id"
}

# Usage
TOKEN=$(solve_turnstile "$API_KEY" \
    "https://example.com/form" \
    "0x4AAAAAAAB5...")

Решение reCAPTCHA v3 через curl

reCAPTCHA v3 не показывает виджет и просто возвращает score.

К вызову добавляется только версия и action, под которым сработала защита на странице.

solve_recaptcha_v3() {
    local api_key="$1"
    local site_url="$2"
    local sitekey="$3"
    local action="${4:-verify}"

    local task_id
    task_id=$(submit_task "$api_key" \
        -d "method=userrecaptcha" \
        -d "googlekey=${sitekey}" \
        -d "pageurl=${site_url}" \
        -d "version=v3" \
        -d "action=${action}" \

    if [ $? -ne 0 ]; then return 1; fi

    poll_result "$api_key" "$task_id"
}

Распознавание графических CAPTCHA (Image OCR)

Для текстовых и графических CAPTCHA изображение просто кодируется в base64 и отправляется телом запроса.

macOS-версия base64 не понимает флаг -w 0 — функция ниже пробует оба варианта синтаксиса, чтобы один и тот же скрипт работал и на Linux, и на macOS.

solve_image_captcha() {
    local api_key="$1"
    local image_path="$2"

    if [ ! -f "$image_path" ]; then
        echo "ERROR: File not found: $image_path" >&2
        return 1
    fi

    local base64_data
    base64_data=$(base64 -w 0 "$image_path" 2>/dev/null || base64 "$image_path")

    local task_id
    task_id=$(submit_task "$api_key" \
        -d "method=base64" \
        --data-urlencode "body=${base64_data}")

    if [ $? -ne 0 ]; then return 1; fi

    poll_result "$api_key" "$task_id"
}

# From URL
solve_image_from_url() {
    local api_key="$1"
    local image_url="$2"
    local tmp_file
    tmp_file=$(mktemp /tmp/captcha_XXXXXX.png)

    curl -s -o "$tmp_file" "$image_url"
    local result
    result=$(solve_image_captcha "$api_key" "$tmp_file")
    rm -f "$tmp_file"

    echo "$result"
}

# Usage
TEXT=$(solve_image_captcha "$API_KEY" "captcha.png")
echo "CAPTCHA text: $TEXT"

Готовая библиотека: captchaai.sh

Все функции выше удобно собрать в один файл и подключать через source — тогда каждый новый скрипт начинается с одной строки, а не с копипасты submit_task/poll_result.

Сохраните как captchaai.sh:

#!/bin/bash
# CaptchaAI Solver Library
# Source this file: source ./captchaai.sh

CAPTCHAAI_URL="https://ocr.captchaai.com"
CAPTCHAAI_POLL_INTERVAL=5
CAPTCHAAI_MAX_WAIT=300

captchaai_submit() {
    local api_key="$1"; shift
    local response
    response=$(curl -s -X POST "${CAPTCHAAI_URL}/in.php" \
        -d "key=${api_key}" -d "json=1" "$@")
    local status=$(echo "$response" | jq -r '.status')
    local request=$(echo "$response" | jq -r '.request')
    [ "$status" = "1" ] && echo "$request" || { echo "Submit: $request" >&2; return 1; }
}

captchaai_poll() {
    local api_key="$1" task_id="$2" elapsed=0
    while [ "$elapsed" -lt "$CAPTCHAAI_MAX_WAIT" ]; do
        sleep "$CAPTCHAAI_POLL_INTERVAL"
        elapsed=$((elapsed + CAPTCHAAI_POLL_INTERVAL))
        local resp=$(curl -s "${CAPTCHAAI_URL}/res.php?key=${api_key}&action=get&id=${task_id}&json=1")
        local req=$(echo "$resp" | jq -r '.request')
        local st=$(echo "$resp" | jq -r '.status')
        [ "$req" = "CAPCHA_NOT_READY" ] && continue
        [ "$st" = "1" ] && { echo "$req"; return 0; }
        echo "Solve: $req" >&2; return 1
    done
    echo "Timeout" >&2; return 1
}

captchaai_balance() {
    local api_key="$1"
    curl -s "${CAPTCHAAI_URL}/res.php?key=${api_key}&action=getbalance&json=1" | jq -r '.request'
}

captchaai_recaptcha_v2() {
    local key="$1" url="$2" sk="$3"
    local tid=$(captchaai_submit "$key" -d "method=userrecaptcha" -d "googlekey=$sk" -d "pageurl=$url") || return 1
    captchaai_poll "$key" "$tid"
}

captchaai_turnstile() {
    local key="$1" url="$2" sk="$3"
    local tid=$(captchaai_submit "$key" -d "method=turnstile" -d "sitekey=$sk" -d "pageurl=$url") || return 1
    captchaai_poll "$key" "$tid"
}

captchaai_image() {
    local key="$1" path="$2"
    local b64=$(base64 -w 0 "$path" 2>/dev/null || base64 "$path")
    local tid=$(captchaai_submit "$key" -d "method=base64" --data-urlencode "body=$b64") || return 1
    captchaai_poll "$key" "$tid"
}

Как использовать библиотеку в своих скриптах

API-ключ в примерах — плейсхолдер YOUR_API_KEY; в реальном пайплайне храните его в переменной окружения (export CAPTCHAAI_KEY="...") или в секретах CI/CD, а не в файле, который попадёт в git.

#!/bin/bash
source ./captchaai.sh

API_KEY="YOUR_API_KEY"

# Check balance
echo "Balance: $(captchaai_balance "$API_KEY")"

# Solve reCAPTCHA v2
TOKEN=$(captchaai_recaptcha_v2 "$API_KEY" \
    "https://staging.example.com/qa-login" \
    "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-")

echo "Token: ${TOKEN:0:50}..."

Отправка формы с готовым токеном

Токен сам по себе бесполезен, пока его не подставить в форму под тем же именем поля, которое ждёт защита сайта — для reCAPTCHA это g-recaptcha-response.

submit_form_with_token() {
    local url="$1"
    local token="$2"
    shift 2

    curl -s -X POST "$url" \
        -d "g-recaptcha-response=${token}" \
        "$@"
}

# Usage: solve then submit
TOKEN=$(captchaai_recaptcha_v2 "$API_KEY" \
    "https://staging.example.com/qa-login" "SITEKEY")

RESPONSE=$(submit_form_with_token "https://staging.example.com/qa-login" \
    "$TOKEN" \
    -d "[email protected]" \
    -d "password=password")

echo "Response: $RESPONSE"

Параллельное решение нескольких CAPTCHA в фоне

Если нужно пройти проверку сразу на нескольких сайтах — например, в мониторинге доступности или в regression-тестах, — bash умеет запускать решения параллельно через фоновые задания (& + wait), без отдельного воркер-пула.

#!/bin/bash
source ./captchaai.sh

API_KEY="YOUR_API_KEY"
RESULTS_DIR=$(mktemp -d)

# Define tasks
declare -A TASKS
TASKS["site-a"]="https://site-a.com|SITEKEY_A"
TASKS["site-b"]="https://site-b.com|SITEKEY_B"
TASKS["site-c"]="https://site-c.com|SITEKEY_C"

# Launch parallel solves
pids=()
for name in "${!TASKS[@]}"; do
    IFS='|' read -r url sitekey <<< "${TASKS[$name]}"
    (
        token=$(captchaai_recaptcha_v2 "$API_KEY" "$url" "$sitekey" 2>/dev/null)
        if [ $? -eq 0 ]; then
            echo "$token" > "${RESULTS_DIR}/${name}.token"
        else
            echo "FAILED" > "${RESULTS_DIR}/${name}.token"
        fi
    ) &
    pids+=($!)
done

# Wait for all
for pid in "${pids[@]}"; do
    wait "$pid"
done

# Collect results
echo "=== Results ==="
for name in "${!TASKS[@]}"; do
    token=$(cat "${RESULTS_DIR}/${name}.token")
    if [ "$token" = "FAILED" ]; then
        echo "$name: FAILED"
    else
        echo "$name: ${token:0:50}..."
    fi
done

rm -rf "$RESULTS_DIR"

Повторные попытки с экспоненциальной задержкой

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

solve_with_retry() {
    local api_key="$1"
    local solve_cmd="$2"
    shift 2
    local max_retries="${1:-3}"

    local retryable_errors=("ERROR_NO_SLOT_AVAILABLE" "ERROR_CAPTCHA_UNSOLVABLE")
    local attempt=0

    while [ "$attempt" -le "$max_retries" ]; do
        if [ "$attempt" -gt 0 ]; then
            local delay=$((2 ** attempt + RANDOM % 3))
            echo "Retry $attempt/$max_retries after ${delay}s..." >&2
            sleep "$delay"
        fi

        local result
        result=$($solve_cmd "$api_key" "${@:2}")

        if [ $? -eq 0 ]; then
            echo "$result"
            return 0
        fi

        # Check if error is retryable
        local is_retryable=0
        for err in "${retryable_errors[@]}"; do
            if echo "$result" | grep -q "$err"; then
                is_retryable=1
                break
            fi
        done

        if [ "$is_retryable" -eq 0 ]; then
            echo "$result"
            return 1
        fi

        attempt=$((attempt + 1))
    done

    echo "Max retries exceeded" >&2
    return 1
}

Запуск через cron

# Edit crontab: crontab -e
# Run daily at 8 AM
0 8 * * * /path/to/captcha-automation.sh >> /var/log/captcha.log 2>&1

Пример продакшен-скрипта для cron

Перед решением стоит проверять баланс — если на счету меньше порога, дешевле остановить job и получить алерт в лог, чем упереться в ERROR_ZERO_BALANCE посреди рабочего конвейера.

#!/bin/bash
source /path/to/captchaai.sh

API_KEY="YOUR_API_KEY"
LOG_FILE="/var/log/captcha-$(date +%Y%m%d).log"

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }

# Check balance first
BALANCE=$(captchaai_balance "$API_KEY")
log "Balance: $BALANCE"

if (( $(echo "$BALANCE < 1.0" | bc -l) )); then
    log "WARNING: Low balance!"
    exit 1
fi

# Solve and process
TOKEN=$(captchaai_recaptcha_v2 "$API_KEY" \
    "https://portal.example.com" "SITEKEY")

if [ $? -eq 0 ]; then
    log "Solved successfully"
    # Submit form, download data, etc.
    curl -s "https://portal.example.com/data" \
        -d "g-recaptcha-response=$TOKEN" \
        -o "/data/export-$(date +%Y%m%d).csv"
    log "Data exported"
else
    log "ERROR: Failed to solve CAPTCHA"
    exit 1
fi

Упаковка в Docker

Alpine-образ с bash, curl и jq — самый компактный способ доставить этот набор функций в контейнер: сборка занимает секунды, а сам образ весит около 10 МБ поверх базового Alpine.

FROM alpine:3.19

RUN apk add --no-cache bash curl jq

COPY captchaai.sh /usr/local/lib/captchaai.sh
COPY automation.sh /app/automation.sh

RUN chmod +x /app/automation.sh

CMD ["/app/automation.sh"]

Типичные ошибки и их устранение

Большинство сбоев в shell-автоматизации CAPTCHA сводится к одной из шести причин — таблица ниже экономит время на диагностике.

Ошибка Причина Исправить
ERROR_WRONG_USER_KEY Неверный ключ API Подтвердите ключ на панели управления
ERROR_ZERO_BALANCE Нет средств Пополнить счет
curl: (60) SSL certificate Пакет CA отсутствует Добавьте --cacert /path/to/ca-bundle.crt или -k для тестирования.
jq: command not found jq не установлен apt install jq или brew install jq
base64: invalid option -- 'w' синтаксис macOS base64 Используйте base64 file вместо base64 -w 0 file.
Пустой ответ Проблема с сетью Добавьте флаг -v в Curl для отладки.

Совет: в cron-скрипте логируйте не только результат, но и сырой ответ res.php при ошибке — это экономит время при разборе редких сбоев, которых нет в таблице.


Частые вопросы

Нужен ли jq, или можно обойтись без него?

Для надёжного разбора JSON — да. Теоретически значения можно вытащить grep/sed, но на нестандартных или многострочных ответах API это ломается; jq заметно устойчивее.

Подходит ли этот набор функций для Alpine-контейнеров в CI/CD?

Да, это один из основных сценариев: Alpine с bash, curl и jq даёт контейнер около 10 МБ, который одинаково хорошо запускается в GitHub Actions, GitLab CI и Jenkins.

Сколько потоков нужно, чтобы решать капчи на нескольких сайтах параллельно?

Столько же, сколько запускается фоновых заданий одновременно — при трёх параллельных captchaai_recaptcha_v2 в примере выше нужно минимум три свободных потока в тарифе, иначе часть задач встанет в очередь и увеличит общее время выполнения.

Что делать, если retry-скрипт всё равно упирается в ERROR_CAPTCHA_UNSOLVABLE?

Если ошибка повторяется даже после нескольких попыток с задержкой, дело обычно не в перегрузке, а в самом изображении или sitekey — проверьте, что pageurl совпадает с реальным адресом страницы и что sitekey актуален для неё.

Как логировать процесс решения капчи в cron-задачах, чтобы потом разбирать сбои?

Пишите баланс, task_id и итоговый статус в отдельный лог-файл с меткой времени (как в примере cron-скрипта выше) — этого обычно достаточно, чтобы за секунды найти, на каком шаге упал ночной прогон.


Похожие руководства

Автоматизацию на других платформах и языках смотрите в статьях PowerShell + CaptchaAI: автоматизация Windows, Решение CAPTCHA на Perl и Настройка API-ключа CaptchaAI.

Решайте CAPTCHA прямо из терминала — получите API-ключ и настройте автоматизацию на Bash.

Комментарии для этой статьи отключены.