如何用 C# 和 HttpClient 识别 DataDome 验证码

要在 C# 中识别 DataDome 验证码,你需要读取 403 封锁页面,由它构造出挑战地址,并用 DataDomeAsync 把该地址发送给 CapSkip DataDome 识别工具 来识别,再通过 HttpClient 用它返回的 cookie 重复请求。得到的答案是一个 cookie,而不是用于表单的 token,并且 CapSkip 文档说明,它只能配合获取它时所用的 User-Agent 和 IP 地址使用。本指南在一个控制台项目里,只用 CapSkip NuGet 包,不用其他任何东西,跑完整个流程。CapSkip 运行在你自己的 Windows PC 上,没有按次识别的费用,所以重试不花一分钱。
你需要什么
- CapSkip 3.2.0 或更高版本,运行在 Windows PC 上。该版本新增了对滑块和设备检查的 DataDome 识别支持。 设置指南 详细介绍了首次启动的步骤。
- 需要 .NET 8 SDK 或更高版本,以及 1.5.0 或更高版本的 CapSkip NuGet 包。更早的版本没有 DataDomeAsync 方法。下面的代码在 .NET SDK 10.0.100 上运行过,目标框架为 net10.0 和 net8.0,使用 CapSkip 1.5.0。
- 一个 PowerShell 提示符。这些命令使用 PowerShell 语法。
- 一个你拥有或被允许自动化操作的页面,并且它受 DataDome 保护。
- 识别工具的地址。在 Local 模式下,CapSkip 只在 127.0.0.1 上响应,仅供本机使用。在 Server 模式下,它监听你的网络地址或公网 IP,这样另一台 PC、VPS 或托管平台上的程序就能通过 API 访问它。两种模式的设置位置都是 连接设置.
- 过了测试阶段之后,每个任务使用一个粘性代理(始终保持同一个 IP)。cookie 与获取它的 IP 地址绑定,第 3 步会解释这一点。
测试方式:代码是针对我自己写的三个模拟服务器运行的,而不是针对真实的 DataDome 站点。其中一个照搬了 CapSkip API 文档中 DataDome 部分列出的检查,一个扮演会返回 403 封锁页面的受保护站点,它只接受 CapSkip 签发的 cookie,并且要求配合签发时所用的 User-Agent,还有一个扮演正向代理。SDK 是来自 NuGet 的真实 CapSkip 1.5.0 包。CapSkip 应用没有在我的 PC 上运行,所以这里没有任何内容能说明一次真实识别要花多久,也不能说明线上的 DataDome 站点接受什么。关于 cookie 的规则来自 CapSkip 文档。DataDome 自己的 cookie 文档 称 datadome cookie 的有效期为一年,并提醒不要更改它的 Domain 或 Path。但它没有说明这个 cookie 绑定到什么。
第 1 步:创建项目并读取封锁页面
创建一个控制台项目并添加该包。默认模板启用了隐式 using 和可空类型,代码依赖这两者。
dotnet new console -o datadome-demo cd datadome-demo dotnet add package CapSkip
当 DataDome 对某个请求存疑时,站点会返回状态码 403(有时是 401)和一个小小的 HTML 页面,而不是真正的内容。该页面里有一个名为 dd 的脚本对象,并在一个 iframe 中从 geo.captcha-delivery.com 加载挑战。CapSkip 需要的就是这个 iframe 的地址,文档把它称为 captcha_url。如果页面里已经包含这个 iframe,可以直接取它的 src。大多数封锁页面是用脚本来构建 iframe 的,所以第一个代码块会根据 dd 对象拼出同样的地址。第一个请求使用一个普通的 Chrome User-Agent 发出。这个字符串从不发送给 CapSkip,所以它的 Chrome 版本不必与任何东西匹配,而且我没有测试过线上的 DataDome 站点会不会把同样的挑战发给不同的 User-Agent。把 Program.cs 的内容替换为本指南的四个代码块,一个接一个,从这一个开始。
rt 的值决定路径。c 表示 /captcha/ 下的滑块,i 表示 /interstitial/ 下的设备检查。t 的值也很重要:fe 表示这是一个挑战,bv 表示 DataDome 已经封禁了获取该页面的 IP 地址。
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
using CapSkip;
static class BlockPage
{
// Build the challenge address from a DataDome block page, or return null.
public static string? CaptchaUrl(HttpStatusCode status, string html, string pageUrl)
{
if (status != HttpStatusCode.Forbidden && status != HttpStatusCode.Unauthorized) return null;
var found = Regex.Match(html, @"var\s+dd\s*=\s*(\{[^}]*\})");
if (!found.Success) return null;
using var doc = JsonDocument.Parse(found.Groups[1].Value.Replace('\'', '"'));
var dd = doc.RootElement;
string? Get(string name) => dd.TryGetProperty(name, out var v)
? (v.ValueKind == JsonValueKind.String ? v.GetString() : v.GetRawText())
: null;
var query = new List<(string Key, string? Value)>
{
("initialCid", Get("cid")), ("hash", Get("hsh")), ("cid", Get("cookie")),
};
if (!string.IsNullOrEmpty(Get("t"))) query.Add(("t", Get("t"))); // fe challenge, bv banned
query.Add(("referer", pageUrl));
query.Add(("s", Get("s")));
foreach (var key in new[] { "e", "b" })
if (!string.IsNullOrEmpty(Get(key))) query.Add((key, Get(key)));
query.Add(("dm", "cd"));
var path = Get("rt") == "i" ? "interstitial" : "captcha";
var qs = string.Join("&", query.Select(p => p.Key + "=" + Uri.EscapeDataString(p.Value ?? "")));
return $"https://{Get("host") ?? "geo.captcha-delivery.com"}/{path}/?{qs}";
}
}对于不是封锁页面的任何响应,该方法都返回 null,这样调用方就能区分 “nothing to solve”(无需识别)和 “solved”(已识别)。每个封锁页面都是一个全新的挑战。请尽快使用它的地址,下一个任务要重新请求页面,而不是复用旧的。
第 2 步:用 DataDomeAsync 识别
SDK 方法是 DataDomeAsync(captchaUrl, url, options),参数顺序与 2Captcha 一致。两个地址是位置参数,userAgent 和 proxy 放在 options 字典里。这两个在 CapSkip 中都是可选的。如果你不发送 User-Agent,CapSkip 会使用它自己浏览器的那个,并在结果中以 UserAgent 返回。如果你确实要发送,它必须是 Windows 版 Chrome,并且主版本号与 CapSkip 的浏览器相同,所以省略它是最省事的选择。因为 CapSkip 是在它自己运行的真实浏览器中通过挑战的,所以该方法使用与 reCAPTCHA 相同的、更长的 300 秒超时来等待。API 文档说一次识别通常需要 3 到 20 秒,而我没有计时过。
这里的代理,就是你之后的请求要用的那个。如果你的程序通过代理出网,就传入它;如果请求是从运行 CapSkip 的那台机器发出的,就什么也不传。如果 CapSkip 在服务器上,那台机器就是服务器。Proxy 类接受类型和地址,例如 HTTP 和 user:[email protected]:8080。
static class Solve
{
// Local mode. In Server mode, use your server's address instead.
static readonly CapSkipClient Solver = new(host: "127.0.0.1", port: 8080);
public static Task<SolveResult> Run(string captchaUrl, string pageUrl, string? proxyUrl)
{
var options = new Dictionary<string, object?>();
if (proxyUrl != null)
{
var i = proxyUrl.IndexOf("://", StringComparison.Ordinal);
options["proxy"] = new Proxy(proxyUrl[..i].ToUpperInvariant(), proxyUrl[(i + 3)..]);
}
// No userAgent option: CapSkip uses its own browser's and reports it.
return Solver.DataDomeAsync(captchaUrl, pageUrl, options);
}
}如果设备检查无法通过,DataDome 会转到滑块。根据 CapSkip 文档,同一个调用会一路跟进到滑块,所以你不需要为这两种挑战写不同的代码。
第 3 步:把 cookie 连同它的 User-Agent 和 IP 地址一起发送
结果是一个 SolveResult,其中有四个重要属性。结果的 Code 属性是 DataDome 设置的原样 cookie,带有 Max-Age、Domain 和其他属性,这是 cookie jar 所需要的。它的 Cookie 属性只是 datadome 的名称和值这一对,这是 Cookie 请求头所需要的。UserAgent 是获得这个 cookie 的 User-Agent。只有当你没有发送代理,并且 CapSkip 通过它自己的 DataDome 代理列表完成识别时,才会设置 Proxy 属性。CapSkip 文档说,DataDome 会拒绝来自其他 IP 地址或使用其他 User-Agent 的 cookie,所以每次都要发送同一个 User-Agent,并且当结果指明了代理时,要通过它发送请求。这个 Proxy 属性只有主机和端口,没有协议前缀,也没有登录信息。程序会加上 http://,因此假定列表中的代理不需要登录。如果你的需要,请自行补上。
下面的辅助方法手动设置 Cookie 请求头并关闭 UseCookies,这样 HttpClient 会原样发送它。它还会从响应中读取替换用的 cookie,因为 DataDome 可能在之后的响应中发来一个,而浏览器也会保留它。为了保持简短,它每次调用都创建一个 HttpClient。请求很多时,应改为每个代理保留一个客户端。
static class Web
{
static HttpClient Client(string? proxyUrl)
{
// UseCookies is off, so the Cookie header set by hand goes out as it is.
var handler = new HttpClientHandler { UseCookies = false };
if (proxyUrl != null)
{
var uri = new Uri(proxyUrl);
var proxy = new WebProxy($"{uri.Scheme}://{uri.Host}:{uri.Port}");
if (uri.UserInfo.Length > 0)
{
var login = uri.UserInfo.Split(':', 2);
proxy.Credentials = new NetworkCredential(
Uri.UnescapeDataString(login[0]),
login.Length > 1 ? Uri.UnescapeDataString(login[1]) : "");
}
handler.Proxy = proxy;
}
return new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
}
// Returns the status, the body and a replacement datadome cookie if the site sent one.
public static async Task<(HttpStatusCode Status, string Body, string? Fresh)> Get(
string url, string userAgent, string? cookie, string? proxyUrl)
{
using var http = Client(proxyUrl);
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.TryAddWithoutValidation("User-Agent", userAgent);
if (cookie != null) request.Headers.TryAddWithoutValidation("Cookie", cookie);
using var response = await http.SendAsync(request);
string? fresh = null;
if (response.Headers.TryGetValues("Set-Cookie", out var lines))
fresh = lines.Select(l => Regex.Match(l, "^datadome=[^;]+"))
.FirstOrDefault(m => m.Success)?.Value;
return (response.StatusCode, await response.Content.ReadAsStringAsync(), fresh);
}
}第 4 步:完整的可运行示例
最后一个代码块把 C# 中的 DataDome 验证码流程整合起来,并加上重试。用 dotnet run 运行它。每次尝试都会重新请求页面,所以每次都带有新的挑战。识别失败会抛出 ApiException,循环会再试一次。带着 cookie 仍然得到 403,也会让循环重新来一轮,并带上新的封锁页面。
static class Program
{
const string PageUrl = "https://www.example.com/"; // a page behind DataDome
const string Browser = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36";
// The proxy your requests use, e.g. "http://user:[email protected]:8080". Null means
// they leave from the PC that runs CapSkip.
static readonly string? ProxyUrl = null;
static async Task<int> Main()
{
for (var attempt = 1; attempt <= 3; attempt++)
{
var (status, body, _) = await Web.Get(PageUrl, Browser, null, ProxyUrl); // fresh block page
var captchaUrl = BlockPage.CaptchaUrl(status, body, PageUrl);
if (captchaUrl is null)
{
Console.WriteLine($"{(int)status}: no DataDome block page on this response");
return 0;
}
SolveResult result;
try { result = await Solve.Run(captchaUrl, PageUrl, ProxyUrl); }
catch (ApiException ex)
{
Console.WriteLine($"attempt {attempt}: {ex.Message}"); // the next try gets a new page
continue;
}
// The cookie works only from the address and User-Agent that earned it.
var via = ProxyUrl ?? (result.Proxy is null ? null : "http://" + result.Proxy);
var cookie = result.Cookie;
var (code, page, fresh) = await Web.Get(PageUrl, result.UserAgent!, cookie, via);
if (code != HttpStatusCode.OK)
{
Console.WriteLine($"attempt {attempt}: HTTP {(int)code} with the cookie");
continue;
}
cookie = fresh ?? cookie; // DataDome may rotate the cookie; keep the newest
Console.WriteLine($"{(int)code} {page[..Math.Min(40, page.Length)]}");
var (again, _, _) = await Web.Get(PageUrl, result.UserAgent!, cookie, via); // reuse it
Console.WriteLine((int)again);
return 0;
}
Console.WriteLine("DataDome not passed in 3 attempts");
return 1;
}
}SDK 在发送任何东西之前,就会拒绝 t=bv 的 captchaUrl,因为从那个地址发起的任何识别都不可能通过。这个 ValidationException 不是 ApiException,所以循环不会捕获它。让它抛出来:它是在提示你更换 IP 地址并重新开始。而如果是 CapSkip 报告的封禁,即 ERROR_PROXY_BANNED,它会以 ApiException 的形式到达,循环会在同一个 IP 地址上重试它,所以如果你在输出中看到这个错误,请先更换代理再运行。 代理轮换指南 介绍了轮换代理,而 C# 验证码检测器 则介绍了在选择方法之前,如何判断页面使用的是哪种验证码。
在模拟运行中,程序的表现如下:
| 模拟设置 | 程序的表现 |
|---|---|
| 滑块封锁页面,每次识别都得到了答案 | 一个任务,然后是带 cookie 的状态 200,复用时再次返回 200 |
| 设备检查封锁页面 | 运行的是同一份代码:captchaUrl 指向 /interstitial/,结果相同 |
| 前两次识别被拒绝 | 两行 attempt 输出,然后第三个任务返回状态 200 |
| 前三次识别被拒绝 | 三行 attempt 输出,然后是说明未通过 DataDome 的消息,以及退出码 1 |
| 结果指明了代理,就像 CapSkip 自己的列表那样 | 带 cookie 的请求通过该代理发出,得到 200 |
| 在 ProxyUrl 中设置你自己的代理 | in.php 收到了代理及其类型,并且请求用的就是它 |
| t=bv 的封锁页面 | 未处理的 ValidationException,并且模拟的 CapSkip 根本没有收到任何请求 |
代理那次运行显示了一点:协议前缀之后的文本会按你写的原样发给 CapSkip。在 ProxyUrl 中以百分号编码写的登录信息,例如用 pas 表示含美元符号的密码,到达 in.php 时仍然是编码后的样子。请让登录信息中不含需要编码的字符,或者自己用解码后的值来构建 Proxy 参数。
常见错误
| 错误 | 原因 | 修复 |
|---|---|---|
| ValidationException | 不是 DataDome 挑战地址的 captchaUrl、t=bv 的封锁页面、不是 http 或 https 的 url,或者不是 Windows 版 Chrome 的 userAgent。消息会说明属于其中哪一种 | 从新的封锁页面重新构造该地址,如果 IP 地址被封禁就更换它,并去掉 userAgent 选项 |
| 带有 ERROR_WRONG_USERAGENT 的 ApiException | User-Agent 是 Windows 版 Chrome,但它的主版本号与 CapSkip 的浏览器不同 | 不要发送 userAgent,改用结果中报告的那个 |
| 带 ERROR_CAPTCHA_UNSOLVABLE 的 ApiException | 没有通过挑战 | 重新请求页面,并提交它新的 captchaUrl |
| 带有 ERROR_PROXY_BANNED 的 ApiException | 根据 API 文档,CapSkip 会拒绝封锁页面显示 t=bv 的任务 | 更换代理,并重新请求页面 |
| 带有 ERROR_PROXY_CONNECT_REFUSED 的 ApiException | CapSkip 无法通过代理建立连接时会报告它。我没有复现过 | 检查代理的地址、登录信息和类型 |
| CapSkip.TimeoutException | CapSkip 在 300 秒的等待时间内没有给出答案。它不是 ApiException,所以循环不会捕获它。请写出完整名称,因为 System 中也有一个 TimeoutException | 检查 CapSkip 是否在运行并在识别,然后再次运行程序 |
| NetworkException | CapSkip 没有在运行,或者主机或端口不对 | 启动 CapSkip 并检查连接设置 |
| 带着 cookie 仍然返回 403 | 请求是从另一个 IP 地址发出的、用了另一个 User-Agent,或者 cookie 已过期 | 每次都发送结果中的 UserAgent 和 Proxy,或者带着新的封锁页面重新运行循环 |
把识别工具跑在服务器上
程序只需要 CapSkip 提供两样东西:主机和端口。在 Local 模式下,那就是 127.0.0.1 和你自己的 PC。在 Server 模式下,CapSkip 监听你的网络地址或公网 IP,所以另一台 PC、VPS 或托管平台上的程序可以通过同样的 API 调用它。在 CapSkipClient 构造函数中更改主机和端口,其他都不用变。建议使用静态公网 IP,以获得稳定的远程访问。Server 模式仍然是你自己的硬件,仍然不计量。它确实会对任何地址开放这个端口,所以请为不应访问它的地址添加 Windows Firewall 阻止规则。请记住,识别是在那台机器上运行的,所以 cookie 是从服务器的 IP 地址获得的。除非你传入代理,否则携带这个 cookie 的请求也必须从那台服务器发出。如果使用代理,请把同一个代理同时传给识别和你的请求。 连接设置 说明了如何切换模式。
常见问题
DataDomeAsync 会返回 token 吗?
不会。它返回的是 datadome cookie。你要把这个 cookie 放在被拦截请求的 Cookie 请求头里发送,并带上同一个 User-Agent。没有需要填写的表单字段。
使用 DataDome 需要代理吗?
测试时不需要。第一次运行用一个 IP 地址就可以。根据 CapSkip 文档,cookie 只在获得它的 IP 地址上有效,所以一旦发送大量请求,就请为每个任务规划一个粘性代理,并把同一个代理同时传给识别和你的请求。
托管平台能针对我的 CapSkip 运行这个流程吗?
可以,通过 Server 模式。把 CapSkip 设置为监听你的网络 IP 或公网 IP,让 CapSkipClient 构造函数指向那个主机和端口,循环的工作方式就一样,只有一处不同:识别此时在服务器上运行,所以要给识别和你的请求使用同一个代理。识别工具仍然在你自己的硬件上。
简短版结论
在 C# 中识别 DataDome 验证码分四步:读取封锁页面、构造 captchaUrl、调用 DataDomeAsync,然后带着 cookie、User-Agent 以及它所属的 IP 地址重复请求。在你自己的机器上重试是免费的,这正是你希望从 验证码识别工具 那里得到的,尤其是在受保护的站点不断给你新的挑战时。关于 SDK 的其余部分,请参阅 C# 验证码识别页面.
