一批 500 个验证码任务的完成情况不均匀 - 有些在 8 秒内解决,有些需要 45 秒。在处理结果之前等待每个任务完成会浪费第一个和最后一个解决方案之间的时间。流式处理让您的下游管道在每个结果到达时就使用它。
流式传输与批处理
| 方法 | 获得第一个结果的时间 | 记忆 | 管道延迟 |
|---|---|---|---|
| 等待所有 | 最慢的任务之后 | 所有结果都在内存中 | 高的 |
| 已解决的流式传输 | 最快完成任务后 | 一次一个结果 | 低的 |
| 微批次(10 块) | 第一个块之后 | 一次 10 个结果 | 中等的 |
Python:用于流结果的异步生成器
使用 asyncio 和 aiohttp,每个解决方案都会通过异步生成器立即产生:
import asyncio
import aiohttp
import time
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
async def submit_task(session, task_data):
"""Submit a single CAPTCHA task."""
params = {
"key": API_KEY,
"method": task_data.get("method", "userrecaptcha"),
"json": 1,
}
if params["method"] == "userrecaptcha":
params["googlekey"] = task_data["sitekey"]
params["pageurl"] = task_data["pageurl"]
elif params["method"] == "turnstile":
params["sitekey"] = task_data["sitekey"]
params["pageurl"] = task_data["pageurl"]
async with session.post(SUBMIT_URL, data=params) as resp:
result = await resp.json(content_type=None)
if result.get("status") != 1:
return None, result.get("request", "unknown")
return result["request"], None
async def poll_task(session, task_id, timeout=300):
"""Poll until solved or timeout."""
start = time.monotonic()
while time.monotonic() - start < timeout:
await asyncio.sleep(5)
params = {"key": API_KEY, "action": "get", "id": task_id, "json": 1}
async with session.get(RESULT_URL, params=params) as resp:
result = await resp.json(content_type=None)
if result.get("request") == "CAPCHA_NOT_READY":
continue
if result.get("status") == 1:
return result["request"], None
return None, result.get("request", "unknown")
return None, "TIMEOUT"
async def solve_one(session, index, task_data, semaphore):
"""Solve a single task within concurrency limits."""
async with semaphore:
start = time.monotonic()
task_id, error = await submit_task(session, task_data)
if error:
return {"index": index, "status": "failed", "error": error, "time": 0}
token, error = await poll_task(session, task_id)
elapsed = time.monotonic() - start
if token:
return {"index": index, "status": "solved", "token": token, "time": round(elapsed, 1)}
return {"index": index, "status": "failed", "error": error, "time": round(elapsed, 1)}
async def stream_results(tasks, max_concurrent=20):
"""
Async generator that yields each result as it completes.
Results arrive in completion order, not submission order.
"""
semaphore = asyncio.Semaphore(max_concurrent)
async with aiohttp.ClientSession() as session:
pending = set()
for i, task in enumerate(tasks):
coro = solve_one(session, i, task, semaphore)
pending.add(asyncio.ensure_future(coro))
while pending:
done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
for future in done:
yield future.result()
async def main():
tasks = [
{"sitekey": "SITE_KEY", "pageurl": f"https://example.com/page{i}"}
for i in range(50)
]
solved = 0
failed = 0
async for result in stream_results(tasks, max_concurrent=15):
# Process each result immediately
if result["status"] == "solved":
solved += 1
print(f" [{solved + failed}/{len(tasks)}] Task {result['index']} SOLVED in {result['time']}s")
# Use token immediately — don't wait for batch
# await submit_form(result["token"])
# await save_to_database(result)
else:
failed += 1
print(f" [{solved + failed}/{len(tasks)}] Task {result['index']} FAILED: {result['error']}")
print(f"\nDone: {solved} solved, {failed} failed")
asyncio.run(main())
安装依赖项:
pip install aiohttp
JavaScript:EventEmitter 流模式
Node.js 使用事件驱动的方法——在解析时发出每个结果:
const { EventEmitter } = require("events");
const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
class CaptchaStream extends EventEmitter {
constructor(maxConcurrent = 15) {
super();
this.maxConcurrent = maxConcurrent;
this.active = 0;
this.queue = [];
this.total = 0;
this.completed = 0;
}
async submitAndPoll(index, taskData) {
const params = new URLSearchParams({
key: API_KEY,
method: taskData.method || "userrecaptcha",
googlekey: taskData.sitekey,
pageurl: taskData.pageurl,
json: "1",
});
const start = Date.now();
const submitResp = await (await fetch(SUBMIT_URL, { method: "POST", body: params })).json();
if (submitResp.status !== 1) {
return { index, status: "failed", error: submitResp.request, time: 0 };
}
const taskId = submitResp.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
const poll = await (await fetch(url)).json();
if (poll.request === "CAPCHA_NOT_READY") continue;
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
if (poll.status === 1) return { index, status: "solved", token: poll.request, time: elapsed };
return { index, status: "failed", error: poll.request, time: elapsed };
}
return { index, status: "failed", error: "TIMEOUT", time: ((Date.now() - start) / 1000).toFixed(1) };
}
async processNext() {
if (this.queue.length === 0 || this.active >= this.maxConcurrent) return;
const { index, taskData } = this.queue.shift();
this.active++;
try {
const result = await this.submitAndPoll(index, taskData);
this.emit("result", result);
} catch (err) {
this.emit("result", { index, status: "failed", error: err.message });
} finally {
this.active--;
this.completed++;
if (this.completed === this.total) {
this.emit("done");
} else {
this.processNext();
}
}
}
start(tasks) {
this.total = tasks.length;
this.queue = tasks.map((taskData, index) => ({ index, taskData }));
// Launch initial batch
const initial = Math.min(this.maxConcurrent, tasks.length);
for (let i = 0; i < initial; i++) {
this.processNext();
}
return this;
}
}
// Usage
const tasks = Array.from({ length: 50 }, (_, i) => ({
sitekey: "SITE_KEY",
pageurl: `https://example.com/page${i}`,
}));
const stream = new CaptchaStream(15);
let solved = 0, failed = 0;
stream.on("result", (result) => {
if (result.status === "solved") {
solved++;
console.log(`[${solved + failed}/${tasks.length}] Task ${result.index} SOLVED (${result.time}s)`);
// Use token immediately
// submitForm(result.token);
} else {
failed++;
console.log(`[${solved + failed}/${tasks.length}] Task ${result.index} FAILED: ${result.error}`);
}
});
stream.on("done", () => {
console.log(`\nComplete: ${solved} solved, ${failed} failed`);
});
stream.start(tasks);
何时使用流式处理与全部收集
| 设想 | 方法 |
|---|---|
| 使用令牌提交表单 | Stream – 令牌到达后立即提交每个表单 |
| 所有结果的 CSV 导出 | 收集全部 - 批次完成时写入一次 |
| 带有实时进度的仪表板 | Stream – 更新每个结果事件的 UI |
| 具有任务间依赖性的批处理 | 收集全部——完成后按顺序处理 |
| 大批量(1,000+) | Stream – 减少峰值内存使用 |
故障排除
| 问题 | 原因 | 处理方式 |
|---|---|---|
| 结果按随机顺序到达 | 正常 - 流媒体首先产生最快的结果 | 使用 result.index 映射回原始任务 |
| 流期间内存仍然增长 | 将所有结果存储在数组中 | 在处理程序中处理和丢弃结果 |
| 第一个结果花费的时间太长 | 所有任务同时提交 | 使用信号量或并发限制交错提交 |
| EventEmitter 警告:MaxListenersExceeded | 直播听众太多 | 使用 setMaxListeners() 或确保每种事件类型有一个侦听器 |
| 异步生成器挂起 | 待处理集中未解决的任务 | poll_task添加超时;确保所有 future 完整或错误 |
常问问题
与批处理相比,流式处理是否会增加 API 调用?
不会——无论哪种方式都会发生相同数量的提交和轮询调用。流仅在应用程序处理每个结果时发生变化,而不是进行了多少次 API 调用。
流式传输时如何保持任务顺序?
每个结果都带有其原始的 index。如果顺序对于下游处理很重要,则缓冲区会产生排序结构并刷新连续运行(如 TCP 数据包重组)。
我可以将流式传输与检查点结合起来吗?
是的。在每个结果到达时将其附加到检查点文件。恢复时,加载检查点,过滤掉已完成的索引,并仅重新处理剩余的任务。
相关文章
下一步
在验证码解决方案到达后立即对其进行处理 –”获取您的 CaptchaAI API 密钥并构建流媒体管道。
相关指南: