API Tutorials

PowerShell + CaptchaAI: автоматизация решения CAPTCHA в Windows

Скрипт на PowerShell упёрся в reCAPTCHA или Cloudflare Turnstile посреди формы? Решить капчу можно прямо оттуда, без Python и без установки сторонних модулей: HTTP API CaptchaAI вызывается через встроенный Invoke-RestMethod, а результат подставляется обратно в тот же сценарий за несколько строк кода.

Это типичная ситуация для Windows-инфраструктуры: PowerShell — стандартный инструмент автоматизации в этой экосистеме, и на нём же чаще всего пишут веб-тестирование, заполнение форм и мониторинг для QA и DevOps. Ниже — готовые функции и полноценный модуль-солвер для reCAPTCHA v2/v3, Cloudflare Turnstile и графических CAPTCHA, которые можно вставить в существующий скрипт без переделки архитектуры.


Зачем решать CAPTCHA прямо в PowerShell, а не выносить в отдельный сервис

Ничего лишнего ставить не нужно: Invoke-RestMethod и Invoke-WebRequest входят в PowerShell 5.1 «из коробки» и сами парсят JSON, поэтому сценарий, зависящий от решения CAPTCHA, можно повесить прямо на встроенный Планировщик заданий Windows без внешнего оркестратора. Токен решения сразу передаётся в следующий шаг того же скрипта, без промежуточных файлов и очередей — а если раннер не Windows-based, те же функции без изменений запускаются под PowerShell 7+ на Linux или macOS.

Кому это в первую очередь пригодится: QA-инженерам, которые гоняют регрессионные проверки форм логина, и системным администраторам, которые автоматизируют мониторинг доступности сайтов через staging-окружения. Из инструментов нужен только PowerShell 5.1 (штатно есть в Windows) или PowerShell 7+ для кроссплатформенного запуска, а также API-ключ CaptchaAI — зарегистрируйте аккаунт и возьмите ключ в панели управления. Устанавливать под это отдельные модули не придётся: весь код ниже работает на встроенных командлетах.


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

Любой тип CAPTCHA в API CaptchaAI решается по одной и той же двухшаговой схеме: сначала запрос уходит на in.php и возвращает ID задачи, затем скрипт опрашивает res.php, пока задача не перейдёт в статус «решено». Обе функции ниже — общий фундамент для всех солверов дальше по статье.

Отправка задачи

function Submit-CaptchaTask {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [hashtable]$TaskParams
    )

    $body = @{
        key  = $ApiKey
        json = 1
    } + $TaskParams

    $response = Invoke-RestMethod -Uri "https://ocr.captchaai.com/in.php" `
        -Method Post `
        -Body $body `
        -ContentType "application/x-www-form-urlencoded"

    if ($response.status -ne 1) {
        throw "Submit failed: $($response.request)"
    }

    return $response.request
}

Опрос результата

Опрос обычно занимает несколько секунд для Turnstile и до пары десятков секунд для более тяжёлых типов reCAPTCHA — функция ниже сама ждёт нужный интервал и возвращает исключение, если время вышло:

function Get-CaptchaResult {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$TaskId,

        [int]$MaxWaitSeconds = 300,
        [int]$PollIntervalSeconds = 5
    )

    $deadline = (Get-Date).AddSeconds($MaxWaitSeconds)

    while ((Get-Date) -lt $deadline) {
        Start-Sleep -Seconds $PollIntervalSeconds

        $response = Invoke-RestMethod -Uri "https://ocr.captchaai.com/res.php" `
            -Method Get `
            -Body @{
                key    = $ApiKey
                action = "get"
                id     = $TaskId
                json   = 1
            }

        if ($response.request -eq "CAPCHA_NOT_READY") {
            Write-Verbose "Waiting for solution..."
            continue
        }

        if ($response.status -ne 1) {
            throw "Solve failed: $($response.request)"
        }

        return $response.request
    }

    throw "Timeout: CAPTCHA not solved within $MaxWaitSeconds seconds"
}

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

Функция ниже оборачивает обе базовые функции выше в один вызов — на вход идут SiteUrl и SiteKey со страницы с CAPTCHA, на выходе готовый токен для формы:

function Solve-RecaptchaV2 {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$SiteUrl,

        [Parameter(Mandatory)]
        [string]$SiteKey
    )

    Write-Host "Submitting reCAPTCHA v2 task..."
    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method    = "userrecaptcha"
        googlekey = $SiteKey
        pageurl   = $SiteUrl
    }
    Write-Host "Task ID: $taskId"

    Write-Host "Polling for solution..."
    $token = Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
    Write-Host "Solved! Token: $($token.Substring(0, [Math]::Min(50, $token.Length)))..."

    return $token
}

# Usage
$apiKey = "YOUR_API_KEY"
$token = Solve-RecaptchaV2 `
    -ApiKey $apiKey `
    -SiteUrl "https://staging.example.com/qa-login" `
    -SiteKey "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"

Решение Cloudflare Turnstile

Turnstile решается той же схемой submit → poll, только с методом turnstile — и в среднем быстрее, чем классическая reCAPTCHA v2:

function Solve-Turnstile {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$SiteUrl,

        [Parameter(Mandatory)]
        [string]$SiteKey
    )

    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method  = "turnstile"
        key     = $SiteKey
        pageurl = $SiteUrl
    }

    return Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
}

# Usage
$token = Solve-Turnstile `
    -ApiKey "YOUR_API_KEY" `
    -SiteUrl "https://example.com/form" `
    -SiteKey "0x4AAAAAAAB5..."

Решение reCAPTCHA v3

У reCAPTCHA v3 нет визуального челленджа — она возвращает score от 0 до 1, поэтому в задачу нужно передать версию и действие (action), под которое капча была выпущена на странице:

function Solve-RecaptchaV3 {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$SiteUrl,

        [Parameter(Mandatory)]
        [string]$SiteKey,

        [string]$Action = "verify",
    )

    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method    = "userrecaptcha"
        googlekey = $SiteKey
        pageurl   = $SiteUrl
        version   = "v3"
        action    = $Action
    }

    return Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
}

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

Для картиночных CAPTCHA API принимает файл в base64 — функция ниже читает файл с диска, кодирует и отправляет тем же методом Submit-CaptchaTask:

function Solve-ImageCaptcha {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$ImagePath
    )

    if (-not (Test-Path $ImagePath)) {
        throw "Image file not found: $ImagePath"
    }

    $imageBytes = [System.IO.File]::ReadAllBytes($ImagePath)
    $base64 = [Convert]::ToBase64String($imageBytes)

    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method = "base64"
        body   = $base64
    }

    return Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
}

# Usage
$text = Solve-ImageCaptcha -ApiKey "YOUR_API_KEY" -ImagePath "C:\captcha.png"
Write-Host "CAPTCHA text: $text"

Если изображение лежит по URL, а не на диске

Тот же принцип, только байты забираются через Invoke-WebRequest вместо чтения файла:

function Solve-ImageCaptchaFromUrl {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$ImageUrl
    )

    $imageBytes = (Invoke-WebRequest -Uri $ImageUrl).Content
    $base64 = [Convert]::ToBase64String($imageBytes)

    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method = "base64"
        body   = $base64
    }

    return Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
}

Готовый модуль-солвер для переиспользования

Если функции выше разбросаны по нескольким скриптам, удобнее собрать их в один класс и подключать модулем. Сохраните код ниже как CaptchaAI.psm1:

class CaptchaAISolver {
    [string]$ApiKey
    [string]$BaseUrl = "https://ocr.captchaai.com"
    [int]$PollInterval = 5
    [int]$MaxWait = 300

    CaptchaAISolver([string]$apiKey) {
        $this.ApiKey = $apiKey
    }

    [string] SolveRecaptchaV2([string]$siteUrl, [string]$siteKey) {
        return $this.Solve(@{
            method    = "userrecaptcha"
            googlekey = $siteKey
            pageurl   = $siteUrl
        })
    }

    [string] SolveTurnstile([string]$siteUrl, [string]$siteKey) {
        return $this.Solve(@{
            method  = "turnstile"
            key     = $siteKey
            pageurl = $siteUrl
        })
    }

    [string] SolveImage([string]$imagePath) {
        $bytes = [System.IO.File]::ReadAllBytes($imagePath)
        $base64 = [Convert]::ToBase64String($bytes)
        return $this.Solve(@{
            method = "base64"
            body   = $base64
        })
    }

    [double] GetBalance() {
        $response = Invoke-RestMethod -Uri "$($this.BaseUrl)/res.php" `
            -Body @{ key = $this.ApiKey; action = "getbalance"; json = 1 }
        return [double]$response.request
    }

    hidden [string] Solve([hashtable]$params) {
        $taskId = $this.Submit($params)
        return $this.Poll($taskId)
    }

    hidden [string] Submit([hashtable]$params) {
        $body = @{ key = $this.ApiKey; json = 1 } + $params
        $response = Invoke-RestMethod -Uri "$($this.BaseUrl)/in.php" `
            -Method Post -Body $body
        if ($response.status -ne 1) { throw "Submit: $($response.request)" }
        return $response.request
    }

    hidden [string] Poll([string]$taskId) {
        $deadline = (Get-Date).AddSeconds($this.MaxWait)
        while ((Get-Date) -lt $deadline) {
            Start-Sleep -Seconds $this.PollInterval
            $response = Invoke-RestMethod -Uri "$($this.BaseUrl)/res.php" `
                -Body @{ key = $this.ApiKey; action = "get"; id = $taskId; json = 1 }
            if ($response.request -eq "CAPCHA_NOT_READY") { continue }
            if ($response.status -ne 1) { throw "Solve: $($response.request)" }
            return $response.request
        }
        throw "Timeout"
    }
}

# Export
Export-ModuleMember

Как использовать модуль

После импорта доступны три вещи:

  • проверка баланса потоков (GetBalance)
  • решение reCAPTCHA v2 и Turnstile одним вызовом метода
  • тот же тайм-аут и интервал опроса, что и в отдельных функциях выше — их можно переопределить через свойства $solver.MaxWait / $solver.PollInterval
using module .\CaptchaAI.psm1

$solver = [CaptchaAISolver]::new("YOUR_API_KEY")

# Check balance
$balance = $solver.GetBalance()
Write-Host "Balance: `$$balance"

# Solve reCAPTCHA v2
$token = $solver.SolveRecaptchaV2("https://staging.example.com/qa-login", "SITEKEY")
Write-Host "Token: $($token.Substring(0, 50))..."

Отправка формы с полученным токеном

Решённый токен нужно подставить в то же скрытое поле, которое ждёт фронтенд — в примере ниже это g-recaptcha-response. Дальше форма отправляется обычным POST-запросом через Invoke-WebRequest:

function Submit-FormWithToken {
    param(
        [string]$Url,
        [string]$Token,
        [hashtable]$FormData
    )

    $body = $FormData + @{
        "g-recaptcha-response" = $Token
    }

    $response = Invoke-WebRequest -Uri $Url `
        -Method Post `
        -Body $body `
        -ContentType "application/x-www-form-urlencoded"

    return $response
}

# Usage
$token = Solve-RecaptchaV2 -ApiKey "YOUR_API_KEY" `
    -SiteUrl "https://staging.example.com/qa-login" `
    -SiteKey "SITEKEY"

$result = Submit-FormWithToken `
    -Url "https://staging.example.com/qa-login" `
    -Token $token `
    -FormData @{
        username = "[email protected]"
        password = "password"
    }

Write-Host "Response: $($result.StatusCode)"

Параллельная обработка через PowerShell Jobs

Когда сайтов с CAPTCHA несколько (например, регрессионные проверки формы логина сразу на нескольких стендах), их можно решать параллельно через Start-Job — каждая задача уходит в CaptchaAI независимо, а скрипт просто ждёт все job'ы разом.

Здесь важно не упереться в лимит потоков тарифа: план BASIC ($15/мес, 5 потоков) держит до пяти CAPTCHA одновременно, а агентство, ведущее параллельно десятки стендов, обычно берёт ADVANCE ($90/мес, 50 потоков) или выше — тарифы CaptchaAI считаются по числу потоков, а не по числу решений, так что внутри лимита их сколько угодно. Фиксированная цена в долларах удобна командам, выставляющим счета не в рублях или тенге:

$apiKey = "YOUR_API_KEY"

$tasks = @(
    @{ Url = "https://site-a.com"; Key = "SITEKEY_A" },
    @{ Url = "https://site-b.com"; Key = "SITEKEY_B" },
    @{ Url = "https://site-c.com"; Key = "SITEKEY_C" }
)

$jobs = $tasks | ForEach-Object {
    $task = $_
    Start-Job -ScriptBlock {
        param($ApiKey, $Url, $SiteKey)

        $taskId = (Invoke-RestMethod -Uri "https://ocr.captchaai.com/in.php" -Method Post -Body @{
            key = $ApiKey; json = 1; method = "userrecaptcha"
            googlekey = $SiteKey; pageurl = $Url
        }).request

        $deadline = (Get-Date).AddSeconds(300)
        while ((Get-Date) -lt $deadline) {
            Start-Sleep -Seconds 5
            $result = Invoke-RestMethod -Uri "https://ocr.captchaai.com/res.php" -Body @{
                key = $ApiKey; action = "get"; id = $taskId; json = 1
            }
            if ($result.request -ne "CAPCHA_NOT_READY" -and $result.status -eq 1) {
                return @{ Url = $Url; Token = $result.request }
            }
        }
        return @{ Url = $Url; Error = "Timeout" }
    } -ArgumentList $apiKey, $task.Url, $task.Key
}

# Wait and collect results
$results = $jobs | Wait-Job | Receive-Job
$results | ForEach-Object {
    if ($_.Token) {
        Write-Host "$($_.Url): $($_.Token.Substring(0, 50))..."
    } else {
        Write-Host "$($_.Url): $($_.Error)" -ForegroundColor Red
    }
}
$jobs | Remove-Job

Повторные попытки и обработка ошибок

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

function Solve-WithRetry {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [hashtable]$TaskParams,

        [int]$MaxRetries = 3
    )

    $retryableErrors = @(
        "ERROR_NO_SLOT_AVAILABLE",
        "ERROR_CAPTCHA_UNSOLVABLE"
    )

    for ($attempt = 0; $attempt -le $MaxRetries; $attempt++) {
        if ($attempt -gt 0) {
            $delay = [Math]::Pow(2, $attempt) + (Get-Random -Maximum 3)
            Write-Host "Retry $attempt/$MaxRetries after $($delay)s..."
            Start-Sleep -Seconds $delay
        }

        try {
            $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams $TaskParams
            $result = Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
            return $result
        }
        catch {
            $errorMsg = $_.Exception.Message
            $isRetryable = $retryableErrors | Where-Object { $errorMsg -like "*$_*" }

            if (-not $isRetryable -or $attempt -eq $MaxRetries) {
                throw
            }
            Write-Warning "Retryable error: $errorMsg"
        }
    }
}

Автозапуск через Планировщик заданий Windows

Для мониторинга или регулярного парсинга скрипт удобно повесить на Register-ScheduledTask — он отработает без интерактивного входа в систему, в том числе на выделенном Windows Server в датацентре. Если сценарий параллельно собирает данные формы (см. предыдущий раздел), стоит заранее убедиться, что вы вправе обрабатывать эти данные: для аудитории из РФ это прямо касается 152-ФЗ «О персональных данных», для остальных — эквивалентных требований вроде GDPR; собирайте только то, что действительно нужно для теста.

# Create a scheduled task that runs CAPTCHA automation daily
$action = New-ScheduledTaskAction `
    -Execute "powershell.exe" `
    -Argument "-ExecutionPolicy RemoteSigned -File C:\Scripts\captcha-automation.ps1"

$trigger = New-ScheduledTaskTrigger -Daily -At "08:00"

Register-ScheduledTask `
    -TaskName "CaptchaAutomation" `
    -Action $action `
    -Trigger $trigger `
    -Description "Run daily CAPTCHA automation with CaptchaAI"

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

Большинство сбоев в PowerShell-интеграции сводятся к шести причинам — от неверного ключа до устаревшего протокола TLS в самой Windows:

  • ERROR_WRONG_USER_KEY — неверный ключ API. Сверьте ключ в панели управления.
  • ERROR_ZERO_BALANCE — на балансе нет средств. Пополните счёт.
  • Invoke-RestMethod: SSL/TLS — Windows использует устаревшую версию TLS. Добавьте [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12.
  • The response content cannot be parsed — ответ пришёл не в формате JSON. Используйте Invoke-WebRequest и разберите тело ответа вручную.
  • Ошибка Execution policy — Windows блокирует запуск скрипта. Выполните Set-ExecutionPolicy -Scope CurrentUser RemoteSigned.
  • Cannot convert to double — не удалось разобрать баланс. Используйте [double]::Parse($response.request).

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


Часто задаваемые вопросы

Сколько потоков нужно, чтобы решать несколько CAPTCHA параллельно через Start-Job?

Ровно столько, сколько job'ов запущено одновременно: один поток = одна CAPTCHA в моменте. Если параллельных стендов пять и меньше, хватает BASIC ($15/мес, 5 потоков); для более крупных прогонов берите план с бóльшим числом потоков — свободные потоки просто ждут следующую задачу.

Как избавиться от ошибки TLS/SSL при вызове Invoke-RestMethod?

Добавьте [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 в начало скрипта — это принудительно включает TLS 1.2 в старых версиях .NET Framework, на которых до сих пор работает часть Windows-серверов.

Можно ли запускать эти скрипты в Планировщике заданий на Windows Server без интерактивного входа?

Да, Register-ScheduledTask рассчитан именно на это. Что для этого нужно:

  • задача должна выполняться от системной учётной записи или сервисного аккаунта, а не от интерактивного пользователя
  • политика выполнения должна разрешать сценарий (RemoteSigned или мягче)
  • если скрипт собирает данные форм, заранее проверьте, что вы вправе их обрабатывать — см. раздел про Планировщик выше

Чем Invoke-WebRequest отличается от Invoke-RestMethod при отправке токена в форму?

Invoke-RestMethod ожидает JSON и сразу парсит его в объект PowerShell — это удобно для вызовов in.php/res.php. Но целевая форма обычно отвечает HTML, а не JSON, и на таком ответе Invoke-RestMethod падает с ошибкой парсинга. Поэтому для самой отправки формы (как в разделе про отправку токена) используется Invoke-WebRequest — он возвращает сырой ответ с кодом статуса и телом, которые разбираются вручную.

Нужны ли дополнительные модули PowerShell для работы с API CaptchaAI?

Нет. Весь код в этой статье работает на встроенных командлетах PowerShell 5.1+ — внешние модули не устанавливаются и не требуются.


Переносите решение CAPTCHA в командную строку Windows — получите API-ключ CaptchaAI и подключите его к своим PowerShell-скриптам.

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