C# 的 async/await 和 Task.WhenAll 使并发验证码解决变得简单。本教程展示了如何并行向 CaptchaAI 提交多个验证码、同时轮询结果以及收集所有解决方案 - 优雅地处理部分失败。
先决条件
dotnet new console -n CaptchaSolver
cd CaptchaSolver
dotnet add package System.Text.Json
无需额外的软件包 - HttpClient 和 Task.WhenAll 内置于 .NET 中。
CaptchaAI核心客户端
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
public class CaptchaAiClient : IDisposable
{
private readonly HttpClient _client;
private readonly string _apiKey;
private const string SubmitUrl = "https://ocr.captchaai.com/in.php";
private const string ResultUrl = "https://ocr.captchaai.com/res.php";
public CaptchaAiClient(string apiKey)
{
_apiKey = apiKey;
_client = new HttpClient();
}
public async Task<string> SolveCaptchaAsync(string sitekey, string pageurl)
{
// Submit
var submitParams = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("key", _apiKey),
new KeyValuePair<string, string>("method", "userrecaptcha"),
new KeyValuePair<string, string>("googlekey", sitekey),
new KeyValuePair<string, string>("pageurl", pageurl),
new KeyValuePair<string, string>("json", "1")
});
var submitResp = await _client.PostAsync(SubmitUrl, submitParams);
var submitJson = await submitResp.Content.ReadAsStringAsync();
var submitData = JsonSerializer.Deserialize<ApiResponse>(submitJson);
if (submitData.Status != 1)
throw new Exception($"Submit failed: {submitData.Request}");
var captchaId = submitData.Request;
// Poll for result
for (int i = 0; i < 60; i++)
{
await Task.Delay(5000);
var resultResp = await _client.GetAsync(
$"{ResultUrl}?key={_apiKey}&action=get&id={captchaId}&json=1"
);
var resultJson = await resultResp.Content.ReadAsStringAsync();
var resultData = JsonSerializer.Deserialize<ApiResponse>(resultJson);
if (resultData.Status == 1)
return resultData.Request;
if (resultData.Request != "CAPCHA_NOT_READY")
throw new Exception($"Solve failed: {resultData.Request}");
}
throw new TimeoutException("Solve timeout after 300s");
}
public void Dispose() => _client.Dispose();
}
public class ApiResponse
{
public int Status { get; set; }
public string Request { get; set; }
}
使用 Task.WhenAll 并行求解
public class BatchSolver
{
private readonly CaptchaAiClient _client;
public BatchSolver(string apiKey)
{
_client = new CaptchaAiClient(apiKey);
}
public async Task<BatchResult> SolveAllAsync(
IReadOnlyList<CaptchaTask> tasks)
{
var solveTasks = new Task<TaskResult>[tasks.Count];
for (int i = 0; i < tasks.Count; i++)
{
var task = tasks[i];
solveTasks[i] = SolveSingleAsync(task);
}
// Wait for ALL tasks — no short-circuiting on failure
var results = await Task.WhenAll(solveTasks);
return new BatchResult
{
Solved = Array.FindAll(results, r => r.Solution != null),
Failed = Array.FindAll(results, r => r.Error != null)
};
}
private async Task<TaskResult> SolveSingleAsync(CaptchaTask task)
{
try
{
var solution = await _client.SolveCaptchaAsync(
task.Sitekey, task.Pageurl);
return new TaskResult
{
TaskId = task.TaskId,
Solution = solution
};
}
catch (Exception ex)
{
return new TaskResult
{
TaskId = task.TaskId,
Error = ex.Message
};
}
}
}
public record CaptchaTask(string TaskId, string Sitekey, string Pageurl);
public class TaskResult
{
public string TaskId { get; set; }
public string Solution { get; set; }
public string Error { get; set; }
}
public class BatchResult
{
public TaskResult[] Solved { get; set; }
public TaskResult[] Failed { get; set; }
}
使用 SemaphoreSlim 控制并发
public async Task<BatchResult> SolveWithLimitAsync(
IReadOnlyList<CaptchaTask> tasks,
int maxConcurrency = 10)
{
var semaphore = new SemaphoreSlim(maxConcurrency);
var solveTasks = new Task<TaskResult>[tasks.Count];
for (int i = 0; i < tasks.Count; i++)
{
var task = tasks[i];
solveTasks[i] = ThrottledSolveAsync(task, semaphore);
}
var results = await Task.WhenAll(solveTasks);
return new BatchResult
{
Solved = Array.FindAll(results, r => r.Solution != null),
Failed = Array.FindAll(results, r => r.Error != null)
};
}
private async Task<TaskResult> ThrottledSolveAsync(
CaptchaTask task, SemaphoreSlim semaphore)
{
await semaphore.WaitAsync();
try
{
return await SolveSingleAsync(task);
}
finally
{
semaphore.Release();
}
}
完整程序示例
class Program
{
static async Task Main(string[] args)
{
var apiKey = Environment.GetEnvironmentVariable("CAPTCHAAI_API_KEY")
?? throw new Exception("Set CAPTCHAAI_API_KEY");
var solver = new BatchSolver(apiKey);
// Create 20 tasks
var tasks = new List<CaptchaTask>();
for (int i = 0; i < 20; i++)
{
tasks.Add(new CaptchaTask(
$"task_{i}",
"6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
$"https://example.com/page/{i}"
));
}
Console.WriteLine($"Solving {tasks.Count} CAPTCHAs with concurrency=10...");
var start = DateTime.UtcNow;
var result = await solver.SolveWithLimitAsync(tasks, maxConcurrency: 10);
var elapsed = DateTime.UtcNow - start;
Console.WriteLine($"\nDone in {elapsed.TotalSeconds:F1}s");
Console.WriteLine($" Solved: {result.Solved.Length}");
Console.WriteLine($" Failed: {result.Failed.Length}");
foreach (var s in result.Solved)
Console.WriteLine($" ✓ {s.TaskId}: {s.Solution[..Math.Min(30, s.Solution.Length)]}...");
foreach (var f in result.Failed)
Console.WriteLine($" ✗ {f.TaskId}: {f.Error}");
}
}
取消支持
全局超时后取消所有挂起的任务:
public async Task<BatchResult> SolveWithTimeoutAsync(
IReadOnlyList<CaptchaTask> tasks,
int maxConcurrency = 10,
TimeSpan? timeout = null)
{
using var cts = new CancellationTokenSource(
timeout ?? TimeSpan.FromMinutes(10));
try
{
return await SolveWithLimitAsync(tasks, maxConcurrency);
}
catch (OperationCanceledException)
{
Console.WriteLine("Batch operation timed out.");
return new BatchResult
{
Solved = Array.Empty<TaskResult>(),
Failed = Array.Empty<TaskResult>()
};
}
}
Task.WhenAll 与 Parallel.ForEachAsync (.NET 6+)
// .NET 6+ alternative
await Parallel.ForEachAsync(tasks,
new ParallelOptions { MaxDegreeOfParallelism = 10 },
async (task, ct) =>
{
var result = await SolveSingleAsync(task);
// Process result immediately
});
| 方法 | 收集所有结果 | 内置并发限制 | .NET版本 |
|---|---|---|---|
| Task.WhenAll + SemaphoreSlim | 是的 | 手册(SemaphoreSlim) | .NET 核心 1.0+ |
| Parallel.ForEachAsync | 内联处理 | 内置 | .NET 6+ |
故障排除
| 问题 | 原因 | 处理方式 |
|---|---|---|
HttpClient 套接字耗尽 |
每个请求创建新的 HttpClient | 使用单个共享 HttpClient(如上所示) |
| Task.WhenAll 抛出第一个错误 | 不在 try/catch 中包装单个任务 | 在 SolveSingleAsync 内捕获(如上所示) |
| 高内存,可处理 1000 多个任务 | 所有任务立即开始 | 使用SemaphoreSlim控制并发 |
| SSL/TLS 错误 | 旧版 .NET 针对 TLS 1.0 | 设置 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 |
常问问题
我应该使用单个 HttpClient 还是每个任务一个?
始终是单个共享的 HttpClient。每个请求创建一个新的套接字会导致套接字耗尽。 HttpClient 是线程安全的,并且是为重用而设计的。
C# 的最佳并发数是多少?
从 10-20 开始。C# 的异步模型是轻量级的——每个并发任务使用最少的资源。增加直到 CaptchaAI 的容量或您的网络成为瓶颈。
Task.WhenAll 与 Task.WhenAny?
WhenAll 等待每个任务。当第一个任务完成时,WhenAny 返回 - 对于“第一次成功获胜”场景很有用,但不适用于需要所有结果的批量求解。
相关文章
下一步
在 C# 中同时解决验证码 –”获取您的 CaptchaAI API 密钥并实现并行求解。
相关指南: