如何在 Python 爬虫中复用 cf_clearance Cookie

cf_clearance cookie - How to Reuse cf_clearance Cookies in a Python Scraper

解决 Cloudflare 的挑战只完成了一半工作。解决挑战换来的是一个 cf_clearance cookie,真正让后续请求不再被重新挑战的,正是这个 cookie。它过期得很快,绑定的东西比大多数人以为的要多,而且一旦你的客户端和当初获得它的那个客户端不再一致,它就会立刻失效。这篇文章会讲清楚这个 cookie 是什么、如何获取它、什么会在不知不觉中让它失效,以及如何区分一个过期的 cookie 和一个损坏的 cookie。

你需要什么

  • Python 3.10 或更新版本,并配备一个支持 session 的 HTTP 客户端。示例使用 session,以便 cookie 能在多个请求之间保留。
  • 一个真正会发出挑战的目标站点。一个从不对你发起挑战的网站永远不会设置这个 cookie,也就没有东西可供测试。
  • CapSkip 处于运行状态,可以是在回环地址上以 Local 模式运行,也可以是在你的 worker 能访问到的机器上以 Server 模式运行。两者都记录在 连接设置一节,所以开始之前先选一种。

cf_clearance cookie 到底是什么

它是一张回执。Cloudflare 的参考文档将它描述为 保存着挑战通过证明的cookie,其作用是让 cookie 存在时不再重复发出挑战。它也是浏览器 JavaScript 检测结果存放的 cookie,并且被设置为 SameSite None、Secure 和 Partitioned,以便这个状态能在跨站请求中保留下来。

这个有效期不是由你决定的。它来自你所访问站点上的 Challenge Passage 设置,而 Cloudflare 在文档中明确写出了默认值:cf_clearance cookie 的有效期为 30 分钟,建议的合理范围是 15 到 45 分钟。有些网站会把它调短,有些会调长。你无法从外部读取这个配置值,所以要把每一个 clearance 都当作短命的,为刷新做好准备,而不是寄希望于它能一直有效。

由此可以得出三个实际结论。你应该保留这个 cookie,因为每次请求都重新识别既慢又浪费。你不应该假设它能在重启后依然有效。并且你应该有一条代码路径,能察觉它已经过期,并悄悄地换取一个新的。

第一步:解决挑战,然后保留整个客户端

首先要避免的一个错误,是把 token 当成最终目标。它不是。你从识别 Turnstile 挑战中得到的 token,只是用来换取 clearance 的东西,之后真正有价值的是你换回来的那个 cookie。所以正确的顺序是:识别、提交,然后保留住接收响应的那个 session。

# pip install capskip
from capskip import CapSkip

solver = CapSkip(host="127.0.0.1", port=8080)

# A challenge page needs two more values than a plain widget does.
result = solver.turnstile(
    sitekey="YOUR_SITEKEY",
    url="https://example.com/protected",
    data="YOUR_CDATA",
    pagedata="YOUR_CHLPAGEDATA",
)

token = result["code"]
agent = result["userAgent"]   # not optional, see the next section

用页面本身会使用的方式提交这个 token,并从一个你打算保留下来的 session 发出提交。响应返回之后,clearance cookie 就已经躺在这个 session 的 cookie jar 里,你可以直接把它读出来。

# pip install requests
import requests

s = requests.Session()
s.headers["User-Agent"] = agent      # the exact agent the solver returned

# ... submit the token here, exactly as the challenge page does ...

clearance = s.cookies.get("cf_clearance")
print(bool(clearance))               # True once the challenge is cleared

第二步:了解哪些东西会悄悄让它失效

Cloudflare 没有公开确切的绑定规则,所以下面这张表来自实际观察到的行为,而不是有文档记载的约定。它足够稳定,可以作为依据,而表里的每一行都曾经让某个人白白搭进去一个下午。

变化的内容clearance 能否保留原因
你的 user agent 字符串clearance 是签发给某个特定浏览器身份的
你的源 IP 地址一个跑到新地址上的 cookie,是重放攻击的经典特征
你的 TLS 指纹通常不能握手会在 cookie 之前被读取,所以不匹配会更早被发现
你发送到的主机名clearance 是按站点划分的,不是按账户,也不是按网络
时间流逝只能保留到 Challenge Passage 过期为止默认是 30 分钟,网站可以更改这个值
添加无关的 cookieclearance 校验会忽略其他 cookie

前三行其实是同一条规则的三种表现:clearance 属于某个客户端,而不属于你。所以要把获得它的那个身份固定下来,也就是说,在这个 cookie 的整个生命周期里,都用同一个 user agent、同一个出口 IP 和同一个 TLS 画像。在 session 进行到一半时更换代理,会白白扔掉你已经付出代价换来的 clearance,这也是一个原本正常工作的爬虫毫无缘由地重新开始被挑战的最常见原因。

这也是为什么识别器返回的不只是一个 token,还有一个 user agent。Turnstile 会把 token 和生成它的浏览器身份绑定在一起,所以用不同的 agent 提交 token,即使 token 本身完全有效也会被拒绝。请原样使用返回的这个值,并在之后每一个携带该 cookie 的请求中继续使用它。 Turnstile 挑战页面演练 讲清楚了另外两个输入值的来源,这正是大家容易弄错的另一半。

第三步:让它跨运行持续存在

一个随着进程结束就消失的 clearance cookie,价值远不如一个能在重启后依然存活的。要把 cookie jar 和与它配套的身份一起存起来,因为如果你用不同的 agent 或不同的代理把这个 cookie 重新加载进来,它本身是没有用的。

# pip install requests
import json, time

def save_clearance(session, agent, proxy, path="clearance.json"):
    """Store the cookie with the identity that earned it."""
    blob = {
        "cf_clearance": session.cookies.get("cf_clearance"),
        "user_agent": agent,
        "proxy": proxy,
        "stored_at": time.time(),
    }
    with open(path, "w") as fh:
        json.dump(blob, fh)

重新加载是这个过程的镜像,只是多了一步检查。主动让记录过期,而不是等着被重新挑战,因为主动刷新只需要一次识别,被动刷新则要先付出一次失败的请求作为代价。

# pip install requests
import json, time, requests

def load_clearance(path="clearance.json", max_age=900):
    """Return a ready session, or None if the record is too old."""
    with open(path) as fh:
        blob = json.load(fh)

    # 15 minutes, comfortably inside a 30 minute default.
    if time.time() - blob["stored_at"] > max_age:
        return None

    s = requests.Session()
    s.headers["User-Agent"] = blob["user_agent"]
    s.proxies = {"https": blob["proxy"]} if blob["proxy"] else {}
    s.cookies.set("cf_clearance", blob["cf_clearance"])
    return s

十五分钟是一个刻意保守的上限。你看不到网站配置的 Challenge Passage 值,默认是 30 分钟,在中途刷新只需要付出一次廉价的识别,而不是让整批任务都失败。如果识别器运行在你自己的硬件上、不按次收费,提前刷新就是免费的。

第四步:不靠猜测就能发现过期的 clearance

一个失效的 clearance 不会用一个干净利落的错误来通知你。你通常会得到一个看起来很正常的 403,或者一个携带 HTML 插页而不是你想要的 JSON 的 200。只检查状态码抓不住第二种情况,而一个只盯着状态码的重试循环,会心安理得地在这上面无限循环下去。

# pip install requests
def needs_new_clearance(response):
    """True when this response is a challenge rather than content."""
    if response.status_code in (403, 503):
        return True
    body = response.text[:4000].lower()
    markers = ("cf-turnstile", "challenge-platform", "just a moment")
    return any(m in body for m in markers)

把这个检测接在你的解析器之前,而不是之后。它返回 true 时,就丢弃已保存的记录,识别一次,用新的 session 重试。不要用旧 cookie 加更长的休眠时间去重试,因为问题根本不在于时间。同样的区别在 reCAPTCHA token 上也存在,只是它的有效期窗口还要短得多,关于这一点, reCAPTCHA token 的有效期能持续多久 这篇文章讲清楚了这方面的内容。

改成把求解器跑在服务器上

clearance cookie 是按身份划分的,所以一支 worker 队伍就需要一整套 clearance,而每一个都需要经过识别。这项工作不必发生在 worker 本身上。connection settings 覆盖了这两种安排:

模式监听地址适用场景
本地127.0.0.1,仅限该设备你的爬虫和求解器跑在同一台机器上
服务器你的内网地址或公网 IPWorker、VPS 或托管平台通过 API 调用

把 SDK 的 host 指向识别器所在的机器,代码的其他部分不需要任何改动,这样二十个 worker 就能共享同一个实例,同时各自保留自己的 cookie jar。如果调用方位于你自己的网络之外,建议使用静态公网 IP。详情记录在 连接设置中。Server 模式用的仍然是你自己的硬件,仍然不按次计费:它改变的只是识别器运行的位置,而不是它归谁所有。

常见问题

cf_clearance cookie 能保存多久?

默认是三十分钟,网站所有者可以更改这个值。Cloudflare 把它作为 Challenge Passage 设置开放出来,并建议保持在 15 到 45 分钟之间,所以你遇到的大多数网站都会落在这个区间里。你无法从外部读取实际配置的值,这就是为什么用你自己掌控的定时器来刷新,比等着被重新挑战要更好。

我能让多个 worker 共用同一个 clearance cookie 吗?

只有当它们共享获得这个 cookie 的那个身份时才行,实际上就是要求相同的出口 IP、相同的 user agent 和相同的 TLS 画像。在单个代理背后是可以做到这一点的,也是节省识别次数的好办法。一旦两个 worker 使用了不同的出口地址,共享的 cookie 就会开始对其中一个失效,而这些失败在你注意到它们具体落在哪个 worker 上之前,会显得毫无规律。

为什么我一轮换代理,clearance 就失效了?

因为这个 clearance 是签发给旧地址的。一个从新 IP 到达的 cookie,正是重放防护机制存在的目的就是要抓住的那种模式,所以它会被丢弃,你也就会被重新挑战。应该改成在 session 边界处轮换:一个代理换来一个 clearance,用到它过期为止,下一个 session 再用新地址和新的识别重新开始。

我需要一个真实浏览器来持有 clearance cookie 吗?

不需要。任何带 cookie jar 的 HTTP 客户端都能携带它,只要围绕它的身份保持不变。浏览器免费给你的是一个可信的 TLS 握手和一个可信的请求头顺序,而一个普通的 HTTP 客户端默认两者都没有。所以难的不是持有这个 cookie,难的是保持一致,而一个伪装客户端能在不承担浏览器内存开销的情况下做到这一点。

最短的版本

把 cf_clearance cookie 当作绑定在单个客户端上的一张短命回执。从通过挑战的那个 session 里获取它,连同获得它的 user agent 和代理一起存起来,并按大约十五分钟一次的节奏刷新,而不是等着被拦截。要检查响应体,而不只是状态码,因为挑战有时会附带一个完全健康的 200 一起出现。一旦它真的过期,只需要一次新的识别就能彻底解决,而一个 验证码识别工具 运行在你自己硬件上的话,这个成本低到可以提前去做。至于 Turnstile 识别可以接受的两种输入形式,请阅读 Cloudflare Turnstile 识别器页面 然后再动手接入。worker 池的接入方式单独记录在 网络爬虫验证码识别工具中,如果你运行多个 worker,花二十分钟看一下是值得的。