换验证码识别服务,别看宣传页上的数字,让两家在你自己的流量上跑一周。并行运行就是把同一批任务同时发给现有平台和 CaptchaAI,成功率、识别耗时、错误码分开记账,一周后就有一张只属于自己业务的对照表。
为什么公开基准数字帮不上忙
营销页上的成功率跑在别人的流量上。你的 sitekey 分布、机房位置和并发高峰时段都不一样。
国内团队还多一层差异:业务里既有海外站点的 reCAPTCHA、Turnstile,也有国内常见的 GeeTest(极验)滑块。总体成功率会把类型间的差距抹平,所以要分类型统计。
成本口径也得对齐:CaptchaAI 按并发线程计费而不是按次数,BASIC $15/月 5 线程、STANDARD $30/月 15 线程,套餐内识别次数不限。
先定指标,再写代码
先把要记录的字段定死,错误码按码值分开计:
| 指标 | 测量方式 |
|---|---|
| 成功率 | 成功识别数 / 总提交数 |
| 平均识别耗时 | 提交到出结果的时间 |
| P95 识别耗时 | 第 95 百分位 |
| 单次成本 | 总支出 / 成功识别数 |
| token 有效性 | 回填后页面是否接受 |
最后一行最容易漏:拿到 token 不等于能用。
并行测试的整体结构
路由层负责分发,两家各跑各的,结果统一进收集器:
┌──────────────┐
│ Your App │
└──────┬───────┘
│
┌──────▼───────┐
│ CAPTCHA │
│ Router │
└──┬───────┬───┘
│ │
┌────────▼──┐ ┌──▼────────┐
│ Current │ │ CaptchaAI │
│ Provider │ │ │
└────────┬──┘ └──┬────────┘
│ │
┌──▼───────▼──┐
│ Metrics │
│ Collector │
└─────────────┘
Python 实现
统一封装两家的接口
CaptchaAI 沿用 in.php / res.php,同一个类换域名和 API Key 就能驱动两边:
import os
import time
import requests
from dataclasses import dataclass, field
from typing import Optional
from concurrent.futures import ThreadPoolExecutor
@dataclass
class SolveResult:
provider: str
success: bool
solution: Optional[str] = None
error: Optional[str] = None
elapsed: float = 0.0
cost: float = 0.0
class CaptchaProvider:
def __init__(self, name, submit_url, result_url, api_key):
self.name = name
self.submit_url = submit_url
self.result_url = result_url
self.api_key = api_key
self.session = requests.Session()
def solve_recaptcha(self, sitekey, pageurl):
start = time.time()
resp = self.session.post(self.submit_url, data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
return SolveResult(
provider=self.name, success=False,
error=data.get("request"), elapsed=time.time() - start
)
captcha_id = data["request"]
for _ in range(60):
time.sleep(5)
result = self.session.get(self.result_url, params={
"key": self.api_key, "action": "get",
"id": captcha_id, "json": 1
}).json()
if result.get("status") == 1:
return SolveResult(
provider=self.name, success=True,
solution=result["request"], elapsed=time.time() - start
)
if result.get("request") != "CAPCHA_NOT_READY":
return SolveResult(
provider=self.name, success=False,
error=result.get("request"), elapsed=time.time() - start
)
return SolveResult(
provider=self.name, success=False,
error="TIMEOUT", elapsed=time.time() - start
)
轮询 5 秒一次、最多 60 次。装包慢加镜像源:pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple。
并发提交同一批任务
两个线程同时提交,让两边面对同一时刻的风控状态。
class ParallelTestRunner:
def __init__(self, primary, challenger):
self.primary = primary
self.challenger = challenger
self.results = {"primary": [], "challenger": []}
def run_test(self, sitekey, pageurl, num_runs=20):
print(f"Running {num_runs} parallel solves...")
for i in range(num_runs):
with ThreadPoolExecutor(max_workers=2) as executor:
primary_future = executor.submit(
self.primary.solve_recaptcha, sitekey, pageurl
)
challenger_future = executor.submit(
self.challenger.solve_recaptcha, sitekey, pageurl
)
primary_result = primary_future.result()
challenger_result = challenger_future.result()
self.results["primary"].append(primary_result)
self.results["challenger"].append(challenger_result)
print(f" Run {i+1}/{num_runs}: "
f"{self.primary.name}={'OK' if primary_result.success else 'FAIL'} "
f"({primary_result.elapsed:.1f}s) | "
f"{self.challenger.name}={'OK' if challenger_result.success else 'FAIL'} "
f"({challenger_result.elapsed:.1f}s)")
return self.generate_report()
def generate_report(self):
report = {}
for label, results in self.results.items():
total = len(results)
successes = sum(1 for r in results if r.success)
times = [r.elapsed for r in results if r.success]
errors = [r.error for r in results if not r.success]
report[label] = {
"provider": results[0].provider if results else "unknown",
"total": total,
"successes": successes,
"success_rate": (successes / total * 100) if total else 0,
"avg_time": sum(times) / len(times) if times else 0,
"min_time": min(times) if times else 0,
"max_time": max(times) if times else 0,
"errors": errors
}
return report
# Usage
current = CaptchaProvider(
name="CurrentProvider",
submit_url="https://current-provider.com/in.php",
result_url="https://current-provider.com/res.php",
api_key="current_key"
)
captchaai = CaptchaProvider(
name="CaptchaAI",
submit_url="https://ocr.captchaai.com/in.php",
result_url="https://ocr.captchaai.com/res.php",
api_key=os.environ["CAPTCHAAI_API_KEY"]
)
runner = ParallelTestRunner(primary=current, challenger=captchaai)
report = runner.run_test(
sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl="https://example.com/form",
num_runs=20
)
for label, stats in report.items():
print(f"\n{stats['provider']}:")
print(f" Success rate: {stats['success_rate']:.1f}%")
print(f" Avg time: {stats['avg_time']:.1f}s")
print(f" Min/Max: {stats['min_time']:.1f}s / {stats['max_time']:.1f}s")
if stats['errors']:
print(f" Errors: {stats['errors']}")
num_runs=20 只够冒烟,正式对照每家至少 50 次。
从 10% 流量开始灰度
数据合格后再往生产放量,失败自动回落:
import random
class TrafficSplitter:
def __init__(self, primary, challenger, challenger_pct=10):
self.primary = primary
self.challenger = challenger
self.challenger_pct = challenger_pct
def solve(self, sitekey, pageurl):
if random.randint(1, 100) <= self.challenger_pct:
result = self.challenger.solve_recaptcha(sitekey, pageurl)
if not result.success:
# Fall back to primary on failure
return self.primary.solve_recaptcha(sitekey, pageurl)
return result
return self.primary.solve_recaptcha(sitekey, pageurl)
# Start with 10%, increase as confidence builds
splitter = TrafficSplitter(current, captchaai, challenger_pct=10)
result = splitter.solve(sitekey="...", pageurl="...")
回落是重点:灰度期的失败不该让用户看见。
JavaScript 实现
Node.js 侧用 Promise.all 同时发两路:
const axios = require("axios");
class CaptchaProvider {
constructor(name, submitUrl, resultUrl, apiKey) {
this.name = name;
this.submitUrl = submitUrl;
this.resultUrl = resultUrl;
this.apiKey = apiKey;
}
async solveRecaptcha(sitekey, pageurl) {
const start = Date.now();
try {
const submit = await axios.post(this.submitUrl, null, {
params: { key: this.apiKey, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
});
if (submit.data.status !== 1) {
return { provider: this.name, success: false, error: submit.data.request, elapsed: (Date.now() - start) / 1000 };
}
const captchaId = submit.data.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const poll = await axios.get(this.resultUrl, {
params: { key: this.apiKey, action: "get", id: captchaId, json: 1 },
});
if (poll.data.status === 1) {
return { provider: this.name, success: true, solution: poll.data.request, elapsed: (Date.now() - start) / 1000 };
}
if (poll.data.request !== "CAPCHA_NOT_READY") {
return { provider: this.name, success: false, error: poll.data.request, elapsed: (Date.now() - start) / 1000 };
}
}
return { provider: this.name, success: false, error: "TIMEOUT", elapsed: (Date.now() - start) / 1000 };
} catch (err) {
return { provider: this.name, success: false, error: err.message, elapsed: (Date.now() - start) / 1000 };
}
}
}
async function parallelTest(current, captchaai, sitekey, pageurl, runs = 20) {
const results = { current: [], captchaai: [] };
for (let i = 0; i < runs; i++) {
const [currentResult, captchaaiResult] = await Promise.all([
current.solveRecaptcha(sitekey, pageurl),
captchaai.solveRecaptcha(sitekey, pageurl),
]);
results.current.push(currentResult);
results.captchaai.push(captchaaiResult);
console.log(`Run ${i + 1}/${runs}: ${current.name}=${currentResult.success ? "OK" : "FAIL"} ` +
`(${currentResult.elapsed.toFixed(1)}s) | ${captchaai.name}=${captchaaiResult.success ? "OK" : "FAIL"} ` +
`(${captchaaiResult.elapsed.toFixed(1)}s)`);
}
for (const [label, data] of Object.entries(results)) {
const successes = data.filter((r) => r.success).length;
const times = data.filter((r) => r.success).map((r) => r.elapsed);
const avgTime = times.length ? times.reduce((a, b) => a + b, 0) / times.length : 0;
console.log(`\n${label}: ${successes}/${runs} success (${((successes / runs) * 100).toFixed(1)}%), avg ${avgTime.toFixed(1)}s`);
}
}
// Run
const currentProvider = new CaptchaProvider("CurrentProvider", "https://current-provider.com/in.php", "https://current-provider.com/res.php", "current_key");
const captchaai = new CaptchaProvider("CaptchaAI", "https://ocr.captchaai.com/in.php", "https://ocr.captchaai.com/res.php", process.env.CAPTCHAAI_API_KEY);
parallelTest(currentProvider, captchaai, "SITE_KEY", "https://example.com", 20);
四个阶段的切换计划
| 阶段 | 时长 | 流量分配 | 目标 |
|---|---|---|---|
| 1. 接口验证 | 1 天 | 只跑并行 | 确认参数与错误码 |
| 2. 影子测试 | 3 天 | 5%,带回落 | 采集基线指标 |
| 3. 逐步放量 | 1 周 | 25% → 50% → 75% | 每档观察一天 |
| 4. 全量切换 | —— | 100% | 下线旧平台 |
档位之间不要跳。放到 50% 时并发翻倍,先看线程够不够:排队会让识别耗时变长。
常见故障排查
| 现象 | 原因 | 处理方式 |
|---|---|---|
| CaptchaAI 更慢 | 测试机到接口延迟不同 | 从生产机房发起 |
| 成功率差距大 | 样本量太小 | 每家至少 50 次 |
| token 一边能用一边不行 | token 有有效期 | 拿到立刻用 |
| reCAPTCHA 用例超时 | 脚本来自 Google 域名,部分网络不可达 | 换到能正常加载该页面的机器上跑 |
常见问题
并行测试要跑多久才有说服力?
至少覆盖一个完整业务周期。通常 3 到 7 天,每家不少于 50 次识别。
并行期间费用会翻倍吗?
两边都要付费。CaptchaAI 按线程计费,BASIC $15/月 5 线程、次数不限,成本看开几个线程。
迁移前要先确认哪些验证码类型?
正式支持 reCAPTCHA v2/v3、Cloudflare Turnstile 与 Challenge、GeeTest v3、图片/OCR 与九宫格、BLS;CaptchaFox(测试版)、Friendly Captcha(测试版)、Lemin(测试版)。hCaptcha、FunCaptcha 不支持,GeeTest v4 即将支持。
灰度到什么程度可以下线旧平台?
全量流量稳定跑满一周且没有批量回落。在此之前保留旧平台的 API Key。
相关阅读
下一步
注册 CaptchaAI 账号,先跑 50 次并行对照。
迁移路线: