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

hrequests captcha - How to Solve CAPTCHAs in hrequests (Python TLS Client)

在 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 抛出 NetworkExceptionCapSkip 没在运行,或者从这台机器访问不到启动它,或者切换到 Server 模式并设置 host
token 写进去了,但小组件仍然是未识别状态Camoufox 的隔离作用域丢弃了这次 DOM 写入启动时打开主世界求值,并给脚本加上前缀
调用 render 时抛出 MissingLibraryException安装 hrequests 时没有装 browser 额外依赖装上这个额外依赖,然后运行该库自带的安装命令
正确的 token 被站点拒绝提交是从另一个 session 或另一个客户端发出的用取回页面的那个 session 提交
隔了很久之后,正确的 token 被拒绝它在提交之前就过期了识别与提交紧挨着做,中间什么都别夹
响应里出现 ERROR_GOOGLEKEY从页面解析出来的 sitekey 不是小组件里实际用的那个读取 data-sitekey 属性,而不是某个 script 标签
300 秒后抛出 TimeoutExceptionsitekey 和页面 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 写入会无声无息地消失,所以要么打开主世界求值,要么干脆不做注入。

在把一个经常撞上挑战的爬虫扩大规模之前,有一点值得知道:CapSkip 提供的是 验证码绕过 ,运行在你已经拥有的硬件上,而且从不按次收费,所以一次撞上 10 个挑战的运行和一次撞上 10000 个挑战的运行,花费完全相同。