API Tutorials

PowerShell + CaptchaAI:Windows 自动化验证码解决

想在 Windows 上用 PowerShell 直接调用 CaptchaAI 识别验证码?不用装任何第三方模块——Invoke-RestMethod 在 PowerShell 5.1 和 7+ 里都是内置的,几行代码就能提交任务、轮询结果、拿到 token。

  • 封装两个基础函数:提交任务、轮询结果
  • 分别识别 reCAPTCHA v2/v3、Turnstile、图片验证码
  • 打包成模块,接入 Windows 计划任务定时执行(运维、QA、DevOps 团队常用于 Web 测试与表单自动化)

Windows 环境下为什么首选 PowerShell 处理验证码

特性 说明
系统自带 Windows 内置 PowerShell 5.1,无需额外安装
Invoke-RestMethod 原生支持 REST 自动解析 JSON
任务计划程序 脚本可直接挂定时任务,不依赖第三方调度工具
管道友好 输出能直接传给下游逻辑
跨平台 PowerShell 7+ 同样能在 Linux/macOS 上运行

准备工作

  1. PowerShell 5.1(Windows 自带)或 PowerShell 7+(跨平台)
  2. 一个 CaptchaAI API Key(点此获取
  3. 不需要安装任何额外模块

两个基础函数:提交任务与轮询结果

提交任务:Submit-CaptchaTask

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
}

把 Key、json = 1 和任务参数拼成表单 POST 到 in.phpstatus 不是 1 就抛异常。

轮询结果:Get-CaptchaResult

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"
}

默认每 5 秒查一次 res.php,看到 CAPCHA_NOT_READY 就继续等,最长等 300 秒,可用 -PollIntervalSeconds / -MaxWaitSeconds 调整。


识别 reCAPTCHA v2:完整示例

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-"

SiteUrl 用的是自有的 staging.example.com/qa-loginSubstring 只是截断打印,避免日志里出现完整 token。


识别 Cloudflare Turnstile

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 不一样:站点标识是 key,不是 googlekey


识别 reCAPTCHA v3(按分数判定)

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
}

v3 没有点选框,返回的是分数;action 参数要跟页面里 grecaptcha.execute 用的 action 一致,否则分数会不准。


识别图片验证码

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"

methodbase64,图片转成字符串直接提交,不用额外的文件上传接口。

直接从 URL 识别

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
}

图片是远程地址时,用 Invoke-WebRequest 转 Base64,其余逻辑不变。


封装成完整的 PowerShell 模块

  • 复用前面所有函数逻辑,不用每个脚本重复写一遍
  • 统一管理 API Key、超时时间等配置
  • 另存为 CaptchaAI.psm1,跨脚本直接 import
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

调用模块

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))..."

封装之后只需 [CaptchaAISolver]::new() 一次,查余额、识别各类型都是同一对象的方法。


把 token 塞进表单一起提交

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)"

把 token 塞进 g-recaptcha-response 字段一起 POST 出去,字段名要和表单要求一致。


用 PowerShell Jobs 并行处理多个站点

$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

Start-Job 给每个站点起一个后台作业,互不阻塞,Wait-Job | Receive-Job 统一收集结果。站点多的时候比顺序调用快得多——国内电商团队做多站点监控常用这个模式,几十个站点并行几分钟就能跑完一轮。


失败重试:指数退避

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"
        }
    }
}

只有 ERROR_NO_SLOT_AVAILABLEERROR_CAPTCHA_UNSOLVABLE 才值得重试,其他错误直接抛出去。


接入任务计划程序,定时执行

# Create a scheduled task that runs CAPTCHA automation daily
$action = New-ScheduledTaskAction `
    -Execute "powershell.exe" `
    -Argument "-ExecutionPolicy Bypass -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"

-File 换成你自己脚本的实际路径即可,注册后每天定时自动运行,无需人工干预。


常见报错排查

错误 原因 处理方式
ERROR_WRONG_USER_KEY API 密钥无效 在仪表板上验证密钥
ERROR_ZERO_BALANCE 没有资金 充值账户
Invoke-RestMethod: SSL/TLS TLS 版本不匹配 添加[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
The response content cannot be parsed 非 JSON 响应 使用Invoke-WebRequest并手动解析
Execution policy 错误 脚本被阻止 运行Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
Cannot convert to double 平衡解析错误 使用[double]::Parse($response.request)

常见问题

Invoke-RestMethod 返回的 status 一直是 0,怎么排查?

response.request 的错误字符串:ERROR_WRONG_USER_KEY 是 Key 填错,ERROR_ZERO_BALANCE 是没钱,都不是网络问题。

API Key 该怎么在脚本里安全存放,不写死在源码里?

本地跑用环境变量 $env:CAPTCHAAI_KEY;计划任务或 CI 就存成安全变量,别明文提交进版本库。

轮询一直卡在 CAPCHA_NOT_READY,是超时设置的问题吗?

不一定,这是正常的中间状态,会按 PollIntervalSeconds 继续等;长时间不变就先查 pageurl/googlekey

用任务计划程序跑脚本时报 TLS 握手错误,要怎么处理?

脚本最前面加一行 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 即可解决。

没有管理员权限的 Windows Server,还能跑这套脚本吗?

能,识别验证码只是普通 HTTP 请求。真正要权限的是改执行策略,用 Set-ExecutionPolicy -Scope CurrentUser RemoteSigned 即可。


相关指南

  1. Bash/cURL + CaptchaAI Shell 自动化
  2. 用 C# 识别验证码
  3. CaptchaAI API 认证文档

马上开始: 获取你的 API Key,从 Windows 命令行动手写第一个脚本。

该文章已禁用评论。