如何在 hrequests 中识别验证码(Python TLS 客户端)

在 hrequests 里识别验证码的代码很短,因为读验证码的并不是 hrequests。它通过一个 Go 写的 TLS 客户端发送请求,让你的握手看起来像真正的 Chrome 或 Firefox,从而通过那道在任何挑战被画出来之前就触发的检查。当站点还是弹出了挑战时,你需要一个能把 sitekey 变成 token 的东西。真正值得做对的是两者之间那一环:token 必须从当初取回页面的那个 session 发回去,而不是新开一个。
你需要什么
- Python 3.10 或更高版本,并装好 hrequests。只有当你确实需要渲染时,才加装 browser 额外依赖。
- CapSkip 运行在一台 Windows 机器上。脚本跑在这台机器上就用 Local 模式,跑在别处就用 Server 模式。
- 目标站点的 sitekey 和页面 URL。第 1 步会从页面里把 sitekey 解析出来,而不是硬编码。
- 如果目标站点除了握手之外还在意你的 IP,就再准备一个代理。hrequests 以 URL 字符串的形式接收代理。
# pip install capskip pip install -U hrequests capskip # Only if you need the browser. It downloads a browser build. pip install -U hrequests[all] python -m hrequests install
两种不同的拦截,两种不同的工具
这一点值得说白了,因为这两件事老是被搞混。hrequests 复刻浏览器的 TLS 指纹并生成匹配的请求头。它不会去看一张图片,也不会产生 reCAPTCHA token。CapSkip 产生 token,但完全不碰你的 TLS 指纹。还没看到挑战就被拦下,那是握手层面的问题,而 Python 中的 TLS 指纹 就是针对这一点的说明。而被弹出挑战之后该怎么办,是本页其余部分要讲的。
在挑浏览器之前,hrequests 文档里有一个细节值得知道。创建 session 时可以传一个 browser 参数,取值是 firefox 或 chrome,而 README 的正文和它的参数表对默认值是哪一个说法并不一致。显式传这个参数,歧义就没有了。
第 1 步:取回页面并解析出 sitekey
用 session,而不是裸的 get,因为 session 才是累积 cookie 的那个东西,也是你稍后回发 token 要用的那个东西。hrequests 在响应上自带一个快速 HTML 解析器,所以你可以直接从标记里读出 sitekey,不用靠猜。
# pip install hrequests
import hrequests
SITE = "https://example.com/page-with-recaptcha"
# Name the browser. Headers are generated to match it and the OS.
session = hrequests.Session(browser="chrome", os="win")
resp = session.get(SITE)
# The parser is selectolax under the hood, so this is cheap.
widget = resp.html.find(".g-recaptcha")
sitekey = widget.attrs["data-sitekey"]
print(resp.status_code, sitekey)如果小组件不在初始 HTML 里,说明它是由 JavaScript 注入的,你就需要渲染。不过在动手渲染之前,先看下面的浏览器一节,因为大多数 reCAPTCHA 和 Turnstile 小组件都在服务端返回的标记里,白白渲染一次要多花一个浏览器进程。
第 2 步:用 CapSkip 识别
一次调用。SDK 会提交挑战并轮询答案,从 250 毫秒开始逐步退避,这就是为什么它通常比你手写一个直接打裸接口的循环更快。
# pip install capskip from capskip import CapSkip # 127.0.0.1 only if this script runs on the solver machine. solver = CapSkip(host="127.0.0.1", port=8080) result = solver.recaptcha(sitekey=sitekey, url=SITE) token = result["code"] # the g-recaptcha-response value
CapSkip 支持的其他每种类型都是同样的形态。加上 invisible 或 enterprise 设为 1,或者 version 设为 v3 并带上 action 名称。Turnstile 和极验有各自的 method,其中 Turnstile 最该读一读注意事项,因为一个挑战页需要两个额外的值,以及随 token 一起返回的 user agent。这些额外的值见 Cloudflare Turnstile 识别器页面。每种类型的每一个参数都列在 CapSkip API 文档.
第 3 步:用同一个 session 回发 token
这一步是大家最容易做错的,也正是应该用 hrequests 而不是普通 HTTP 客户端的全部理由。取回页面的那个 session 有它的 TLS 指纹、一套匹配生成的请求头,以及站点发下来的各种 cookie。用同一个 session 提交 token,这次提交看起来就来自同一个客户端。换一个新的 session 提交,或者更糟,用标准库提交,握手就在中途变了,这本身就是一个信号。
# Same session object, so the fingerprint, headers and cookies
# are the ones the site already saw on the GET.
posted = session.post(
SITE,
data={
"username": "YOUR_USERNAME",
"g-recaptcha-response": token,
},
timeout=30,
)
print(posted.status_code)
session.close()要立刻做。一个 reCAPTCHA token 大约有两分钟有效期,所以夹在识别和提交之间的任何事情都在消耗这份预算。它的失败表现,是一个看起来毫无理由就被拒绝的 token,如果你碰上了,别处有完整的说明。
hrequests 的请求超时默认是 30 秒。这在这里没问题,因为它管的是表单提交,而不是识别本身。识别在 SDK 里有自己的上限:图片验证码 120 秒,reCAPTCHA、Turnstile 和极验是 300 秒。
把识别工具跑在另一台机器上
上面那个 host 参数,是脚本和识别程序不再共用一台机器时唯一要改的东西。CapSkip 有两种连接模式。Local 绑定 127.0.0.1,只服务本机。Server 绑定你的网络地址或公网 IP,这样另一台机器上的爬虫、一台 VPS 或者一个容器就能通过 API 访问同一台 Windows 机器。两者的设置都在 连接设置里,如果调用方在你的网络之外,还值得配一个静态公网 IP。Server 模式只改变识别程序监听在哪个地址上:同样的硬件,同样的机器,同样不计量的识别。
import os
from capskip import CapSkip
# Same script on a laptop and on a scraping box. The env var
# decides; CAPSKIP_HOST and CAPSKIP_PORT are read by the SDK too.
solver = CapSkip(
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
port=8080,
)代理在这里值得说一句,因为 hrequests 和 CapSkip 各自单独接收代理。你给 hrequests 的代理,决定你的页面请求从哪里发出。你给识别程序的代理,决定挑战从哪里被识别,CapSkip 对 reCAPTCHA、Turnstile 和极验接受代理,对图片验证码则不接受。在那些把 token 和地址绑定的站点上,两者对上很重要,其中的道理写在 验证码代理轮换指南.
什么时候才真的需要浏览器,以及其中的坑
hrequests 可以用一次 render 调用把响应交给一个真正的浏览器,它的吸引力在于 cookie 是双向流通的:浏览器会话会继承 session 的 cookie,关闭页面时新产生的 cookie 又会合并回去。对于必须点击点什么的流程,这确实很有用。
下面这部分能让人白搭一个下午。hrequests 里的 Firefox 引擎是 Camoufox,直接从 Camoufox 的 Python 包启动,而 Camoufox 会在一个隔离作用域里运行页面脚本。隔离作用域能读 DOM,却不能改 DOM,所以那个看起来理所当然的做法,也就是执行一段脚本把 token 写进响应用的 textarea,什么都不会发生,而且悄无声息。hrequests 只暴露了一个普通的 evaluate,接收一段脚本和一个参数,没有办法要求在主世界里执行。
出路在于 hrequests 会把多余的关键字参数原样转发给 Camoufox,所以 Camoufox 自己的开关你也能用。在启动时打开主世界求值,然后给脚本加上前缀。
import hrequests
# The kwargs go through to Camoufox. Without main_world_eval the
# write below is discarded and nothing tells you.
page = hrequests.BrowserSession(headless=True, main_world_eval=True)
page.goto(SITE)
SCRIPT = (
"mw:(t) => { "
"document.getElementById('g-recaptcha-response').value = t; }"
)
# The second argument arrives as t inside the function.
page.evaluate(SCRIPT, token)
page.click("#submit")
page.close() # merges cookies back into the session有两种办法可以完全绕开这个问题。一是用 Chrome 引擎,它就是普通的 Playwright 语义,代价是失去 hrequests 声称只有 Firefox 才支持的指纹轮换和拟人光标模拟。二是照第 3 步的做法,根本不去注入:把 token 拿回 TLS session,自己提交表单。对登录或搜索表单来说,这样既更简单也更快,这也是本文把浏览器放在最后讲的原因。如果你是直接驱动 Camoufox 而不是通过 hrequests,隔离作用域的行为及其他相关影响写在 Camoufox 验证码指南.
同时识别多个
hrequests 给了你三种让请求并行的办法,CapSkip 给了你一种让识别并行的办法。它们可以组合,但不是一回事。
| 你想并行的是什么 | 由哪个工具来做 |
|---|---|
| 少量页面请求,先发出去,稍后再读结果 | 把 nohup 设为 true,需要时再读取相应的属性 |
| 一次调用处理一批 URL | 把这个列表直接交给请求方法 |
| 大量请求,而且要限制并发数 | 先构造未发送的请求,再带上数量上限做映射 |
| 同时进行多次验证码识别 | Python SDK 的异步客户端,它是真正的独立实现,而不是一个别名 |
最后那一行值得多读一点,因为在所有 CapSkip SDK 里,只有 Python 的异步客户端是独立实现,而不是同一个类的另一个名字。批量处理的写法写在 用 Python 并行识别验证码的指南。在抓取这一半上,要在 hrequests 和主流异步客户端之间取舍是另一个问题, httpx 与 aiohttp 指南 把其中的权衡讲清楚了。
常见错误及其含义
| 你所看到的 | 原因 | 修复 |
|---|---|---|
| SDK 抛出 NetworkException | CapSkip 没在运行,或者从这台机器访问不到 | 启动它,或者切换到 Server 模式并设置 host |
| token 写进去了,但小组件仍然是未识别状态 | Camoufox 的隔离作用域丢弃了这次 DOM 写入 | 启动时打开主世界求值,并给脚本加上前缀 |
| 调用 render 时抛出 MissingLibraryException | 安装 hrequests 时没有装 browser 额外依赖 | 装上这个额外依赖,然后运行该库自带的安装命令 |
| 正确的 token 被站点拒绝 | 提交是从另一个 session 或另一个客户端发出的 | 用取回页面的那个 session 提交 |
| 隔了很久之后,正确的 token 被拒绝 | 它在提交之前就过期了 | 识别与提交紧挨着做,中间什么都别夹 |
| 响应里出现 ERROR_GOOGLEKEY | 从页面解析出来的 sitekey 不是小组件里实际用的那个 | 读取 data-sitekey 属性,而不是某个 script 标签 |
| 300 秒后抛出 TimeoutException | sitekey 和页面 URL 不是该小组件上的那一对,所以识别会一直跑到 300 秒的上限 | 检查 sitekey 和页面 URL 是不是小组件实际使用的那一对 |
| 浏览器会话一直不释放 | 用非上下文管理器方式创建的页面没有被关闭 | 改用 with 写法,或者在 finally 块里调用 close |
常见问题
hrequests 自己能识别验证码吗?
不能。它复刻浏览器的 TLS 指纹并生成匹配的请求头,这能在挑战出现之前挡下很多拦截,它还能在渲染出来的页面里模拟人类的鼠标移动和键盘输入。但这些都不会去读一张扭曲的图片,也不会产生 reCAPTCHA token。那是另一类问题,需要一个识别程序。
我到底需不需要 browser 额外依赖?
只有必须渲染时才需要。这个额外依赖会拉进整套浏览器组件,装完之后还要再跑一条单独的安装命令,而且下载量很大。对常见流程来说,也就是取页面、读 sitekey、识别、提交表单,普通安装就够了,整件事都以 HTTP 请求完成。只有当小组件确实不在服务端返回的标记里,或者表单只能通过真实点击提交时,才去加装它。
session 应该自称是哪种浏览器?
你显式指定的那种。hrequests 接受 firefox 或 chrome,并生成相应的请求头,而且它有意不让请求头的版本与 TLS 版本保持一致,理由是检测系统很少把这两者关联起来,多出来的差异反而看起来像更多不同的客户端。渲染方面这个库推荐 Firefox,因为那里的 Chrome 既不支持指纹轮换,也不支持拟人光标模拟。
识别程序可以和爬虫跑在不同的机器上吗?
可以,而且一旦爬虫从笔记本上挪走,这就是常规做法。把 CapSkip 设为 Server 模式,让它监听你的网络地址或一个公网 IP,而不是回环地址,然后把 host 参数指向它。当调用方在你的网络之外时,建议使用静态公网 IP。还是同一台 Windows 机器在做同样不计量的识别,变的只是地址。
简短版结论
用一个命名的 session 取页面,这样指纹和 cookie 都是稳定的。用内置的解析器从响应里读出 sitekey。用一次 SDK 调用识别它。立刻用同一个 session 把 token 回发,只有当小组件不在标记里时,才动用渲染浏览器。如果确实要渲染,记住 Firefox 引擎就是 Camoufox,而来自隔离作用域的 DOM 写入会无声无息地消失,所以要么打开主世界求值,要么干脆不做注入。
- Python SDK 本身的说明见 Python 验证码识别页面.
- 勾选框挑战的说明见 reCAPTCHA v2 识别页面.
在把一个经常撞上挑战的爬虫扩大规模之前,有一点值得知道:CapSkip 提供的是 验证码绕过 ,运行在你已经拥有的硬件上,而且从不按次收费,所以一次撞上 10 个挑战的运行和一次撞上 10000 个挑战的运行,花费完全相同。
