如何用 Python 识别 DataDome 验证码(requests)

datadome captcha in python - How to Solve DataDome CAPTCHA in Python (requests)

要在 Python 中识别 DataDome 验证码,你需要读取 403 封锁页面,由它构造出挑战地址,并用 datadome() 把该地址发送给 CapSkip DataDome 识别工具 来识别,再用它返回的 cookie 重复你的请求。得到的答案是一个 cookie,而不是用于表单的 token,并且 CapSkip 文档说明,它只能配合获取它时所用的 User-Agent 和 IP 地址使用。本指南用 CapSkip Python SDK 和 requests,以大约 90 行代码跑完整个流程。CapSkip 运行在你自己的 Windows PC 上,没有按次识别的费用,所以重试不花一分钱。

你需要什么

  • CapSkip 3.2.0 或更高版本,运行在 Windows PC 上。该版本新增了对滑块和设备检查的 DataDome 识别支持。 设置指南 详细介绍了首次启动的步骤。
  • Python 3.10 或更高版本,以及 1.5.0 或更高版本的 capskip 包。更早的版本没有 datadome 方法。下面的代码在 Python 3.11.7 和 3.14.0 上运行过,使用 capskip 1.5.0 和 requests 2.34.2。这里没有试过 Python 3.10。
  • 一个 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 是真实的 capskip 1.5.0。CapSkip 应用没有在我的 PC 上运行,所以这里没有任何内容能说明一次真实识别要花多久,也不能说明线上的 DataDome 站点接受什么。关于 cookie 的规则来自 CapSkip 文档。DataDome 自己的 cookie 文档 称 datadome cookie 的有效期为一年,并提醒不要更改它的 Domain 或 Path。但它没有说明这个 cookie 绑定到什么。

第 1 步:读取封锁页面并构造 captcha_url

当 DataDome 对某个请求存疑时,站点会返回状态码 403(有时是 401)和一个小小的 HTML 页面,而不是真正的内容。该页面里有一个名为 dd 的脚本对象,并在一个 iframe 中从 geo.captcha-delivery.com 加载挑战。CapSkip 需要的就是这个 iframe 的地址,文档把它称为 captcha_url。如果页面里已经包含这个 iframe,可以直接取它的 src。大多数封锁页面是用脚本来构建 iframe 的,所以下面的函数会根据 dd 对象拼出同样的地址。

rt 的值决定路径。c 表示 /captcha/ 下的滑块,i 表示 /interstitial/ 下的设备检查。t 的值也很重要:fe 表示这是一个挑战,bv 表示 DataDome 已经封禁了获取该页面的 IP 地址。第一个请求使用一个普通的 Chrome User-Agent 发出。这个字符串从不发送给 CapSkip,所以它的 Chrome 版本不必与任何东西匹配。我没有测试过线上的 DataDome 站点会不会把同样的挑战发给不同的 User-Agent,所以这部分请当作未经测试。

# pip install capskip requests
import json
import re
from urllib.parse import urlencode

import requests

PAGE_URL = "https://www.example.com/"  # a page behind DataDome
BROWSER = {  # for the first request only; the cookie comes with its own User-Agent
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36",
}


def get(headers, via=None):
    proxies = {"http": via, "https": via} if via else None
    return requests.get(PAGE_URL, headers=headers, proxies=proxies, timeout=30)


def captcha_url_from(resp):
    """Build the challenge address from a DataDome block page, or None."""
    if resp.status_code not in (401, 403):
        return None
    found = re.search(r"var\s+dd\s*=\s*(\{[^}]*\})", resp.text)
    if not found:
        return None
    dd = json.loads(found.group(1).replace("'", '"'))
    path = "interstitial" if dd["rt"] == "i" else "captcha"
    query = {"initialCid": dd["cid"], "hash": dd["hsh"], "cid": dd["cookie"]}
    if dd.get("t"):
        query["t"] = dd["t"]  # fe is a challenge, bv means the address is banned
    query.update(referer=PAGE_URL, s=dd["s"])
    for key in ("e", "b"):
        if dd.get(key):
            query[key] = dd[key]
    query["dm"] = "cd"
    return f"https://{dd.get('host', 'geo.captcha-delivery.com')}/{path}/?{urlencode(query)}"

对于不是封锁页面的任何响应,该函数都返回 None,这样脚本就能区分 “nothing to solve”(无需识别)和 “solved”(已识别)。每个封锁页面都是一个全新的挑战。请尽快使用它的 captcha_url,下一个任务要重新请求页面,而不是复用旧的地址。

第 2 步:用 datadome() 识别

SDK 方法是 datadome(captcha_url, pageurl, userAgent, proxy),参数顺序与 2Captcha 一致。前两个是必需的。在 CapSkip 中,userAgent 和 proxy 是可选的。如果你不发送 User-Agent,CapSkip 会使用它自己浏览器的那个,并在结果中以 userAgent 返回。如果你确实要发送,它必须是 Windows 版 Chrome,并且主版本号与 CapSkip 的浏览器相同,所以省略它是最省事的选择。因为 CapSkip 是在它自己运行的真实浏览器中通过挑战的,所以该方法使用与 reCAPTCHA 相同的、更长的 300 秒超时来等待。API 文档说一次识别通常需要 3 到 20 秒,而我没有计时过。

这里的代理,就是你之后的请求要用的那个。如果你的脚本通过代理出网,就传入它;如果请求是从运行 CapSkip 的那台机器发出的,就什么也不传。如果 CapSkip 在服务器上,那台机器就是服务器。

from capskip import CapSkip

# Local mode. In Server mode, use your server's address instead.
solver = CapSkip(host="127.0.0.1", port=8080)


def proxy_arg(proxy_url):
    if not proxy_url:
        return None  # your requests leave from the PC that runs CapSkip
    scheme, rest = proxy_url.split("://", 1)
    return {"type": scheme.upper(), "uri": rest}  # e.g. HTTP, user:pass@ip:port


def solve(captcha_url, proxy_url=None):
    # No userAgent argument: CapSkip uses its own browser's and reports it.
    return solver.datadome(captcha_url, PAGE_URL, proxy=proxy_arg(proxy_url))

如果设备检查无法通过,DataDome 会转到滑块。根据 CapSkip 文档,同一个调用会一路跟进到滑块,所以你不需要为这两种挑战写不同的代码。

第 3 步:把 cookie 连同它的 User-Agent 和 IP 地址一起发送

结果是一个字典,其中有四个重要字段。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://,因此假定列表中的代理不需要登录。如果你的需要,请自行补上。

DataDome 也可能在之后的响应中发来一个替换用的 cookie。函数会保留最新的那个,就像浏览器那样。

def send_with_cookie(result, proxy_url=None):
    # The cookie works only from the address and User-Agent that earned it.
    via = proxy_url
    if not via and result.get("proxy"):
        via = "http://" + result["proxy"]  # CapSkip solved through its own list
    headers = {"User-Agent": result["userAgent"], "Cookie": result["cookie"]}
    resp = get(headers, via)
    fresh = resp.cookies.get("datadome")  # DataDome may rotate the cookie
    if fresh:
        headers["Cookie"] = f"datadome={fresh}"
    return resp, headers, via

cookie 是手动设在请求头里的,所以 requests 不会替你管理它。这是有意为之:显式设置的请求头优先于会话的 cookie jar,并且它把 cookie、User-Agent 和代理放在同一个地方。代理那次运行显示了一点:协议前缀之后的文本会按你写的原样发给 CapSkip,所以代理的登录信息中不要包含需要百分号编码的字符。对于 socks5 代理,requests 还需要它的 socks 附加依赖,用 pip install requests[socks] 安装。

第 4 步:完整的可运行示例

最后一个代码块把 Python 中的 DataDome 验证码流程整合起来,并加上重试。把四个代码块按顺序保存在同一个名为 datadome_demo.py 的文件里,用 python datadome_demo.py 运行。每次尝试都会重新请求页面,所以每次都带有新的挑战。识别失败会抛出 ApiException,循环会再试一次。带着 cookie 仍然得到 403,也会让循环重新来一轮,并带上新的封锁页面。

from capskip import ApiException


def fetch_protected(proxy_url=None):
    for attempt in range(1, 4):
        resp = get(BROWSER, proxy_url)  # a fresh block page, a fresh challenge
        captcha_url = captcha_url_from(resp)
        if captcha_url is None:
            return resp, BROWSER, proxy_url  # no block page on this response
        try:
            result = solve(captcha_url, proxy_url)
        except ApiException as err:
            print(f"attempt {attempt}: {err}")  # the next attempt gets a new page
            continue
        resp, headers, via = send_with_cookie(result, proxy_url)
        if resp.status_code != 200:
            print(f"attempt {attempt}: HTTP {resp.status_code} with the cookie")
            continue
        return resp, headers, via
    raise RuntimeError("DataDome not passed in 3 attempts")


if __name__ == "__main__":
    page, headers, via = fetch_protected()
    print(page.status_code, page.text[:40])
    again = get(headers, via)  # reuse the cookie while it lasts
    print(again.status_code)

SDK 在发送任何东西之前,就会拒绝 t=bv 的 captcha_url,因为从那个地址发起的任何识别都不可能通过。这个 ValidationException 不是 ApiException,所以循环不会捕获它。让它抛出来:它是在提示你更换 IP 地址并重新开始。而如果是 CapSkip 报告的封禁,即 ERROR_PROXY_BANNED,它会以 ApiException 的形式到达,循环会在同一个 IP 地址上重试它,所以如果你在输出中看到这个错误,请先更换代理再运行。 代理轮换指南 介绍了轮换代理,而 区分验证码与封锁的文章 则介绍另一种情况,即页面根本不是挑战。

在模拟运行中,脚本的表现如下:

模拟设置脚本的表现
滑块封锁页面,每次识别都得到了答案一个任务,然后是带 cookie 的状态 200,复用时再次返回 200
设备检查封锁页面运行的是同一份代码:captcha_url 指向 /interstitial/,结果相同
前两次识别被拒绝两行 attempt 输出,然后第三个任务返回状态 200
前三次识别被拒绝三行 attempt 输出,然后抛出 RuntimeError
结果指明了代理,就像 CapSkip 自己的列表那样带 cookie 的请求通过该代理发出,得到 200
把你自己的代理传给识别in.php 收到了代理及其类型,并且请求用的就是它
t=bv 的封锁页面ValidationException,并且模拟的 CapSkip 根本没有收到任何请求
用另一个 User-Agent 发送 cookie模拟站点返回 403
从另一个 IP 地址发送 cookie对于 CapSkip 通过代理签发的 cookie,不经过该代理发送时,模拟站点返回 403

最后两行测试的是我的模拟服务器的规则,这些规则遵循 CapSkip 文档。它们不是关于真实服务的证据。

常见错误

错误原因修复
ValidationException不是 DataDome 挑战地址的 captcha_url、t=bv 的封锁页面、不是 http 或 https 的 pageurl,或者不是 Windows 版 Chrome 的 userAgent。消息会说明属于其中哪一种从新的封锁页面重新构造该地址,如果 IP 地址被封禁就更换它,并去掉 userAgent 参数
带有 ERROR_WRONG_USERAGENT 的 ApiExceptionUser-Agent 是 Windows 版 Chrome,但它的主版本号与 CapSkip 的浏览器不同不要发送 userAgent,改用结果中报告的那个
带 ERROR_CAPTCHA_UNSOLVABLE 的 ApiException没有通过挑战重新请求页面,并提交它新的 captcha_url
带有 ERROR_PROXY_BANNED 的 ApiException根据 API 文档,CapSkip 会拒绝封锁页面显示 t=bv 的任务更换代理,并重新请求页面
带有 ERROR_PROXY_CONNECT_REFUSED 的 ApiExceptionCapSkip 无法通过代理建立连接时会报告它。我没有复现过检查代理的地址、登录信息和类型
TimeoutExceptionCapSkip 在 300 秒的等待时间内没有给出答案。它不是 ApiException,所以循环不会捕获它检查 CapSkip 是否在运行并在识别,然后再次运行脚本
NetworkExceptionCapSkip 没有在运行,或者主机或端口不对启动 CapSkip 并检查连接设置
带着 cookie 仍然返回 403请求是从另一个 IP 地址发出的、用了另一个 User-Agent,或者 cookie 已过期每次都发送结果中的 userAgent 和 proxy,或者带着新的封锁页面重新运行循环

把识别工具跑在服务器上

脚本只需要 CapSkip 提供两样东西:主机和端口。在 Local 模式下,那就是 127.0.0.1 和你自己的 PC。在 Server 模式下,CapSkip 监听你的网络地址或公网 IP,所以另一台 PC、VPS 或托管平台上的脚本可以通过同样的 API 调用它。在 CapSkip 构造函数中更改主机和端口,其他都不用变。建议使用静态公网 IP,以获得稳定的远程访问。Server 模式仍然是你自己的硬件,仍然不计量。它确实会对任何地址开放这个端口,所以请为不应访问它的地址添加 Windows Firewall 阻止规则。请记住,识别是在那台机器上运行的,所以 cookie 是从服务器的 IP 地址获得的。除非你传入代理,否则携带这个 cookie 的请求也必须从那台服务器发出。如果使用代理,请把同一个代理同时传给识别和你的请求。 连接设置 说明了如何切换模式。

常见问题

DataDome 识别工具会返回 token 吗?

不会。它返回的是 datadome cookie。你要把这个 cookie 放在被拦截请求的 Cookie 请求头里发送,并带上同一个 User-Agent。没有需要填写的表单字段。

使用 DataDome 需要代理吗?

测试时不需要。第一次运行用一个 IP 地址就可以。根据 CapSkip 文档,cookie 只在获得它的 IP 地址上有效,所以一旦发送大量请求,就请为每个任务规划一个粘性代理,并把同一个代理同时传给识别和你的请求。

这对滑块和设备检查都适用吗?

适用,通过同一个调用。滑块的 captcha_url 位于 /captcha/ 下,设备检查的位于 /interstitial/ 下。CapSkip 文档说,如果设备检查升级为滑块,调用会一路跟进到滑块。

托管平台能针对我的 CapSkip 运行这个流程吗?

可以,通过 Server 模式。把 CapSkip 设置为监听你的网络 IP 或公网 IP,让 CapSkip 构造函数指向那个主机和端口,循环的工作方式就一样,只有一处不同:识别此时在服务器上运行,所以要给识别和你的请求使用同一个代理。识别工具仍然在你自己的硬件上。

简短版结论

在 Python 中识别 DataDome 验证码分四步:读取封锁页面、构造 captcha_url、调用 datadome(),然后带着 cookie、User-Agent 以及它所属的 IP 地址重复请求。在你自己的机器上重试是免费的,这正是你希望从 本地验证码识别工具 那里得到的,尤其是在受保护的站点不断给你新的挑战时。关于 SDK 的其余部分,请参阅 Python 验证码识别页面.