如何在 C# 中识别 Capy Puzzle 并提交全部三个字段

要在 C# 中识别 Capy Puzzle,先从页面上读出站点的 PUZZLE_ key,用这个 key 和页面 URL 调用 CapyAsync,然后把它返回的三个值分别填入 capy_captchakey、capy_challengekey 和 capy_answer 表单字段,一起提交,并且立刻提交。整个集成就这么多,容易让人栽跟头的是答案的形态。你以前识别过的大多数类型只返回一个 token。Capy 返回的是三个值,它们只能作为一组使用,而且其中一个很快就会过期。CapSkip 从 1.4.0 版开始支持 Capy Puzzle。本指南讲这几件事:key、调用、有意设置的两秒延迟,以及提交。
你需要什么
- 在 Windows 机器上运行的 CapSkip 1.4.0 或更高版本。Capy 支持就是在该版本中加入的,同时加入的还有 CaptchaFox 和 Friendly Captcha。
- CapSkip .NET 包 1.3.0 或更高版本,CapyAsync 就是在这个版本中加入的。它面向 .NET Standard 2.0,并依赖 System.Text.Json 8,因此支持 .NET Framework 4.6.2 及更高版本,以及 .NET 6 及更高版本。示例代码使用了顶级语句和 .NET 6 控制台项目的隐式 using,所以请在 .NET 6 或更高版本上运行。
- 目标页面上的两个值:以 PUZZLE_ 开头的 Capy key,以及小组件所在页面的 URL。第 1 步会告诉你 key 在哪里,从同一个地方还能看出你是否需要第三个可选值。
- 识别工具的地址。Local 模式只在 127.0.0.1 上响应,仅供本机使用;Server 模式监听你的网络地址或公网 IP,这样另一台机器就能通过 API 调用它。两者都位于 连接设置中,下文有一节会说明何时该切换。
# dotnet add package CapSkip dotnet add package CapSkip
第 1 步:找到 PUZZLE_ key 和 Capy 主机
在 C# 中识别 Capy Puzzle 所需的一切都来自目标页面。key 是公开的,对每个访问者都一样,而且它位于两个地方。小组件脚本 URL 里的 k 参数,就在服务器发送的 HTML 中,也就是页面源码和 HttpClient 都能看到的那份 HTML。小组件在浏览器里运行过之后,key 还会出现在小组件写入表单的一个隐藏 capy_captchakey input 里,DevTools 的 Elements 面板里显示的就是这个位置。
<!-- In the HTML the server sends: the k parameter of the widget script --> <script src="https://jp.api.capy.me/puzzle/get_js/?k=PUZZLE_YOUR_KEY"></script> <!-- Written into the form by the widget once it has run (DevTools only) --> <input type="hidden" name="capy_captchakey" value="PUZZLE_YOUR_KEY" />
原样复制 key,连前缀一起。看那个 script 标签的时候,顺便记下它的主机。script URL 中 /puzzle/get_js/ 之前的部分,就是这个 key 背后的 Capy API,CapSkip 把它叫作 api_server。它的默认值是 https://jp.api.capy.me,也就是线上服务实际运行的地方,所以只有当页面从别处加载小组件时,你才需要传入它。
这里有个坑,来自其他识别工具的文档。其中好几家仍然写着 api.capy.me,没有地区前缀,而这个主机已经无法解析了。如果你是从旧示例里复制过来的,就删掉这个选项,让 CapSkip 使用默认值。
第 2 步:调用 CapyAsync
这个方法接受 key、页面 URL,以及一个可选的选项字典。对于使用默认主机的页面,只需要前两个就够了。
// dotnet add package CapSkip
using CapSkip;
var solver = new CapSkipClient(host: "127.0.0.1", port: 8080);
// The PUZZLE_ key, and the page the widget runs on.
var result = await solver.CapyAsync(
"PUZZLE_YOUR_KEY",
"https://example.com/login");
Console.WriteLine(result.CaptchaKey); // capy_captchakey
Console.WriteLine(result.ChallengeKey); // capy_challengekey
Console.WriteLine(result.Answer); // capy_answer读取这三个具名属性。Code 属性以原始 JSON 的形式保存同一个答案,方便记日志;RespKey 返回为空,因为它只是为了与其他服务兼容而存在。Answer 是一个很长的字符串,开头大致像 0xax8ex0xax84x 这样。它是小组件在拼图块移动时本会记录下来的拖动轨迹,而不是一个坐标。
选项字典接受 api_server、proxy、proxytype 和 useragent,另外还有以秒为单位的单次调用超时。如果你设置了 user agent,它会随 CapSkip 拉取拼图时发出的那唯一一个请求一起发送,不过你很少会用到它。还有一个 version 选项,但只接受 puzzle:Capy 的另一个系列 avatar 是另一种挑战,背后是另一个接口,所以 SDK 会用 ValidationException 拒绝它,而不是返回一个站点会拒收的答案。未知的选项名、为空的 PUZZLE_ key、为空的页面 URL 或不支持的 proxytype,都会在发送任何内容之前抛出同样的异常。值为 null 的选项会被直接丢弃,这样你传入可选主机时就不必写 if 语句。
// The script tag's host. jp.api.capy.me is the default, so pass
// this only when the page loads the widget from somewhere else.
var result = await solver.CapyAsync(
"PUZZLE_YOUR_KEY",
"https://example.com/login",
new Dictionary<string, object?>
{
["api_server"] = "https://jp.api.capy.me/",
});为什么一次 Capy 识别大约要两秒
检测本身很快。CapSkip 拉取拼图图片,用一些像素运算找到缺口,再生成拖动轨迹,全程不需要浏览器,也不需要模型。然后它会有意地等一等。
Capy 会测量从下发拼图到收到答案之间的时间,凡是比真人拖动拼图块还快的答案,一律拒绝。CapSkip 自己针对 Capy 做的测量显示,下限大约是一秒:半秒送达的答案被拒,从一秒到四秒(测试过的最长时间)送达的答案都被接受。这种拒绝用的提示和答错时一模一样,所以一个正确但送得太快的识别结果,看起来和识别工具坏了没有任何区别。因此,CapSkip 会把每个 Capy 结果压住,直到拉取拼图满两秒之后才放行。这段等待只是 sleep,所以只增加延迟,不消耗 CPU;而且 CapyAsync 会替你轮询,你看到的只是一次要花几秒钟的调用。
为什么是几秒而不是两秒:SDK 的轮询一开始间隔四分之一秒,然后逐步退避,每次把间隔翻倍,直到达到 pollingInterval,默认是 5 秒。在默认设置下,答案通常在第四秒左右被取回。如果某个客户端识别的主要是 Capy,构造它时传入 pollingInterval: 0.5,调用就会在更接近两秒的时候返回。
这带来两个实际影响。提交前不要自己再加延迟,因为这段等待已经替你等过了。也不要想办法把这段时间省掉,因为答案能通过,靠的正是这段等待。
第 3 步:在一个请求中提交全部三个值
这三个值要填进小组件本来会自己写入表单的那几个字段里。把它们一起发送,和表单的其余内容放在同一个请求里。
// http is your HttpClient; result comes from Step 2.
var form = new FormUrlEncodedContent(new Dictionary<string, string>
{
["username"] = "someone",
["capy_captchakey"] = result.CaptchaKey!,
["capy_challengekey"] = result.ChallengeKey!,
["capy_answer"] = result.Answer!,
});
var response = await http.PostAsync("https://example.com/login", form);答案要按返回的原样提交。它就是拖动轨迹,站点后端会拿它和当时下发的拼图进行比对,所以截断它、把它拆开再拼回去,或者以任何方式整理它,都会让它失效。为了放进表单请求体而对它做百分号编码是没问题的,因为那只是传输层面的处理,服务器会先解码。
然后要尽快提交。CapSkip 每次识别都会生成一个新的挑战 key,拼图就绑定在它上面,所以这个 key 只能用一次,而且寿命很短。一次识别只对应一次提交。如果表单填到一半被放下、之后又继续,就重新识别,而不是去用你留着的那几个值。
照搬真实表单发送的内容。大多数页面还带有自己的隐藏字段,比如防伪造 token;有些页面则通过脚本用 JSON 请求体提交,而不是表单提交。很多站点还会拒绝没有 User-Agent 请求头的请求,而除非你自己添加,否则 HttpClient 不会发送这个请求头。打开 DevTools 手动提交一次,把请求复制下来,然后把三个 Capy 值放到页面放它们的位置。
第 4 步:失败、重试与大批量并发
只要在 C# 中识别 Capy Puzzle 有一定的量,就总会有几次识别失败。失败的 Capy 识别会以 ApiException 的形式返回,而在两种要紧的情况下,通过 SDK 看到的都是无法识别。这两种情况需要截然相反的应对,而区分它们的依据是出现的频率。两者都会提前失败,赶在两秒延迟之前,因为 CapSkip 只压住已经识别成功的答案。
| 发生了什么 | 表现 | 该怎么做 |
|---|---|---|
| 没有定位到缺口 | 偶尔出现,下一次尝试通常就会成功 | 重试。每次尝试都会拿到一张基于不同照片的全新拼图,所以重试是一次真正独立的尝试 |
| Capy API 拒绝了这个 key | 这一个 key 每次尝试都失败,CapSkip 里的 Capy 任务列表显示 Invalid captcha key | 检查 key 和 api_server。CapSkip 不会重试被拒的 key,因为它会以完全相同的方式再被拒一次 |
失手很少见,所以重试一次几乎就能覆盖所有情况。你可以像完整示例那样在自己的代码里重试,也可以在 CapSkip 设置的 Capy 部分设置 Retries,它默认为 0,每个任务最多允许重试三次。
CapyAsync 按 defaultTimeout 轮询,即 120 秒,因为一次 Capy 识别只是一次拉取加上一些运算,而不是一个浏览器会话。这个时长足以覆盖一次正常识别好多倍。如果你调高了 Retries,或者排队的识别远多于 CapSkip 能同时运行的数量,就在构造函数里调高它。Capy 部分的 Max. Threads 设置默认为 10,你可以调高它,多出来的任务会等待空闲线程。
并行运行大量识别时,要让每次识别和它的提交始终配对。用 Task.WhenAll 一次启动一百个 CapyAsync 调用、之后再统一提交结果,会让最早拿到的挑战 key 在等最后几次识别完成时不断变旧。把识别和提交包进同一个任务,并用一个按线程数设定容量的 SemaphoreSlim 限制同时进行的数量。量大时还要加代理。每次识别都会从 Capy API 拉取一张新拼图,而从同一个地址源源不断地拉取,正是限流机制要抓的那种模式。在 CapSkip 里配置代理池,或者为每个请求单独传入代理。代理只作用于拉取拼图这一步,而这也是一次 Capy 识别唯一会发出的请求。
把识别工具放到别处运行
示例使用 127.0.0.1,因为当你的代码和 CapSkip 在同一台机器上时,这样写是对的。一旦 .NET 应用跑在别处,比如容器、构建代理、VPS 或应用服务,回环地址就指向了错误的机器,第一次识别就会抛出 NetworkException。把 CapSkip 切换到 Server 模式,它就会监听你的网络地址或公网 IP,上面这些环境都能通过 API 访问到它。如果链路要经过公网,请使用静态公网 IP,打开 API 密钥校验,并用一条 Windows Firewall 规则把端口限制在你预期的地址上。它仍然是你自己的 Windows 机器,识别也仍然不计量。
客户端不会自己读取环境变量。像完整示例那样,在你自己的代码里读取 CAPSKIP_HOST 和 CAPSKIP_API_KEY 并传给构造函数,这样同一个构建在你的桌面机和服务器上都能运行。
完整可运行示例
// dotnet add package CapSkip
using System.Text.RegularExpressions;
using CapSkip;
var pageUrl = "https://example.com/login";
var http = new HttpClient();
var solver = new CapSkipClient(
apiKey: Environment.GetEnvironmentVariable("CAPSKIP_API_KEY") ?? "capskip",
host: Environment.GetEnvironmentVariable("CAPSKIP_HOST") ?? "127.0.0.1",
port: 8080,
// Capy answers after a two second hold; poll often enough to catch it.
pollingInterval: 0.5);
// The widget script's k parameter. The capy_captchakey input only
// exists once the widget has run in a browser.
var html = await http.GetStringAsync(pageUrl);
var key = Regex.Match(html, "PUZZLE_[A-Za-z0-9_-]+").Value;
if (key.Length == 0)
throw new InvalidOperationException("No PUZZLE_ key in the HTML; check DevTools.");
// The script URL up to /puzzle/get_js/, when there is one; a null value is dropped.
var host = Regex.Match(html, @"(https://[^""'\s<>]+?)/puzzle/get_js/").Groups[1].Value;
var options = new Dictionary<string, object?>
{
["api_server"] = host.Length > 0 ? host : null,
};
try
{
// A missed hole comes back unsolvable; a retry draws a new puzzle.
SolveResult? result = null;
for (var attempt = 1; result is null; attempt++)
{
try
{
result = await solver.CapyAsync(key, pageUrl, options);
}
catch (ApiException ex) when (attempt < 3 && ex.Message.Contains("UNSOLVABLE"))
{
Console.WriteLine($"attempt {attempt}: {ex.Message}");
}
}
// All three together, straight away: the challenge key is single-use.
var form = new FormUrlEncodedContent(new Dictionary<string, string>
{
["username"] = "someone",
["capy_captchakey"] = result.CaptchaKey!,
["capy_challengekey"] = result.ChallengeKey!,
["capy_answer"] = result.Answer!,
});
// Post wherever the form's action attribute points.
var response = await http.PostAsync(pageUrl, form);
Console.WriteLine((int)response.StatusCode);
}
catch (CapSkip.ValidationException ex)
{
// An empty key or URL, avatar as the version, or an unknown option.
Console.WriteLine($"not sent: {ex.Message}");
}
catch (CapSkip.TimeoutException)
{
Console.WriteLine("gave up waiting; defaultTimeout is 120 seconds");
}
catch (CapSkipError ex)
{
// The third miss, a refused key, or CapSkip unreachable.
Console.WriteLine($"gave up: {ex.Message}");
}匹配 key 的正则会在整个 HTML 中查找 PUZZLE_ 前缀,所以不用解析标签,就能从小组件脚本的 URL 里取出 key。下载到的 HTML 里根本没有 capy_captchakey input,因为小组件只有在浏览器里运行时才会写入它。匹配主机的正则会保留 https script URL 中 /puzzle/get_js/ 之前的部分,路径也包括在内;如果什么都没匹配到,null 值会被丢弃,CapSkip 就使用默认值。如果三次尝试全部失败,先检查 key,因为被拒的 key 每次都以同样的方式失败,而失手几乎不会连续发生三次。如果页面是用打包后的脚本构建小组件的,两个正则都匹配不到,就打开 Network 标签页,从小组件自己发出的请求里复制这两个值。这个方法背后的原始接口记录在 API 参考文档中;这个包能识别的其他所有验证码类型,都列在 C# 验证码识别页面.
常见错误及其含义
| 你所看到的 | 原因 | 修复 |
|---|---|---|
| CapSkip 返回了全部三个值,站点却拒绝了提交 | 答案被改动过,或者只有其中一两个值到达了表单 | 把 result.Answer 原样连同另外两个值,放在一个请求里提交 |
| 第一次成功的提交,第二次却失败 | 挑战 key 只能用一次,而且寿命很短 | 每次提交都重新识别,并且识别完立刻提交 |
| 某个 key 每次尝试都抛出 ApiException | Capy API 拒绝了这个 key,或者 api_server 指向了错误的主机 | 重新复制 key,连前缀一起,并检查 script 标签的主机 |
| 偶尔抛出 ApiException,提示验证码无法识别 | CapSkip 没能在那张拼图里定位到缺口 | 重试;下一次尝试会拿到另一张拼图 |
| 从其他服务复制示例之后,每次识别都失败 | 示例把 api_server 设成了 api.capy.me,而这个主机已经无法解析 | 删掉这个选项,使用默认主机 |
| 还没发送任何内容就抛出 ValidationException | key 或页面 URL 为空、version 设成了 avatar,或者传了这个方法不接受的选项 | 确认正则找到了 key,并去掉错误信息中指出的那个选项 |
| 长时间运行时,失败越来越多 | 每张拼图都是从同一个地址拉取的 | 用 CapSkip 中配置的代理池分散识别请求 |
| 高并行负载下抛出 TimeoutException | 排队的任务多于 Max. Threads 在 120 秒内能处理完的数量 | 用 SemaphoreSlim 限制并发数,或者调高 defaultTimeout |
| 第一次识别时抛出 NetworkException | CapSkip 没有在运行,或者主机和端口不对 | 启动 CapSkip,然后确认它应该处于 Local 模式还是 Server 模式 |
| 编译失败,报 TimeoutException 有歧义 | CapSkip 和 System 都定义了这个短名称 | 把 CapSkip.TimeoutException 写全,或者改为捕获 CapSkipError |
常见问题
为什么 Capy 识别返回的是三个值而不是一个 token?
因为 Capy 表单提交的就是这些。整个过程中,从来没有哪个服务器签发过 token。小组件自己生成挑战 key,拉取属于它的拼图,再记录拖动过程;之后站点用自己的私钥,把挑战 key 和答案发给 Capy 进行核验。CapSkip 扮演的是小组件的角色,所以它返回的,就是小组件本来会写进表单的内容。
跑在托管平台上的 .NET 程序能连到识别工具吗?
可以。在连接设置里把 CapSkip 切换到 Server 模式,让它监听网络地址而不是回环地址,然后在你的代码里从 CAPSKIP_HOST 读取这个地址,并传给客户端。容器宿主机、VPS、CI 代理和托管应用服务的连接方式都一样,走的是同一套 HTTP API。如果链路要经过公网,请使用静态公网 IP 并配上防火墙规则。识别工具始终运行在你自己的硬件上,因此授权和识别次数都不会有任何变化。
这和在 C# 中识别极验(GeeTest)v3 有什么不同?
两者都是滑块拼图,都会返回几个需要一起提交的值,但它们的起点不同。极验 v3 从站点下发的 challenge 开始,你必须在识别之前的那一刻去取它,因为它大约一分钟内就会过期。Capy 的起点只有 key,因为挑战 key 是在识别时才生成的,所以没有什么需要预先获取,唯一要紧的计时是在识别之后才开始。极验这一侧的内容见 C# 极验 v3 指南.
提交时需要像 CaptchaFox 那样带上 user agent 吗?
不需要。CaptchaFox 的 token 绑定在签发它的浏览器上,所以提交时必须带上那个浏览器的 user agent,正如 C# CaptchaFox 指南 所述。Capy 的答案则完全不绑定浏览器。CapSkip 不会为它返回 user agent,而你可以传入的那个可选 user agent,也只影响拉取拼图的那个请求。
简短版结论
要在 C# 中识别 Capy Puzzle,先从小组件脚本的 URL 或 capy_captchakey input 中复制 PUZZLE_ key,并记下脚本的主机,以防它不是默认主机。用 key 和真实的页面 URL 调用 CapyAsync,让它花上那几秒钟。把 CaptchaKey、ChallengeKey 和 Answer 分别放进 capy_captchakey、capy_challengekey 和 capy_answer,原样、一起、立刻提交,并且每次提交都重新识别。偶尔失手就重试,用量增长时加上代理,一旦调用方代码离开识别工具所在的机器,就切换到 Server 模式。
- 这个类型如何运作、识别工具覆盖哪些内容: Capy Puzzle 验证码识别页面.
- 这个 .NET 包能识别的其他所有验证码类型: C# 与 .NET 验证码识别页面.
关于重试,最后再说一点。因为每次尝试都会拿到一张不同的拼图,所以遇到失手,再试一次就是正确的解决办法;而当 本地验证码识别工具 就在你自己的机器上时,第二次尝试只多花几秒钟,而不是再多一次计费的识别。
