如何在 httpx 和 aiohttp 中识别验证码(无需浏览器)

httpx 的验证码流程只有三个步骤、大约二十行代码,而大家做错的原因跟识别工具毫无关系。这套技术栈里没有浏览器。没有任何东西去执行网站的 JavaScript,因此没有东西去创建那个用来承载 token 的隐藏字段,也没有东西替你提交表单。这三件事都得你自己做:从 HTML 中读出 sitekey,把它识别出来,然后用抓取该页面的同一个客户端,把 token 作为一个普通的表单字段发回去。
你需要什么
- Python 3.10 或更高版本,以及 httpx 或 aiohttp 其中之一。CapSkip 的 Python SDK 两者都支持。
- 受保护页面的 URL。你不需要提前拿到 sitekey,因为第一步就会把它读出来。
- 如果脚本和识别工具在同一台机器上,就让 CapSkip 以 Local 模式运行;如果不在同一台机器上,就用 Server 模式。两种模式都写在 连接设置.
# pip install capskip pip install capskip httpx # Using aiohttp instead? Install it as well. See the note below # about which HTTP libraries you end up with either way. pip install capskip aiohttp
SDK 本身就自带 httpx
在清点依赖之前值得先了解一下。CapSkip 的 Python 包把 requests、httpx 和 aiofiles 声明为硬性运行时依赖,所以无论你要不要,安装 SDK 都会一并装上 httpx。如果 httpx 本来就是你的爬虫客户端,那么识别工具不会给你的环境引入任何新的 HTTP 库,AsyncCapSkip 还会共用你的事件循环。如果你用的是 aiohttp,那么 httpx 会和它一起装进来,同一个 virtualenv 里就有了两个客户端。这不会造成任何故障,但属于依赖评审会问到的那类事情。
没有浏览器时会有什么不同
在 Selenium 或 Playwright 中,你只需给 reCAPTCHA 渲染出的那个隐藏 textarea 赋值,页面自带的提交处理函数就会把它一起带走。这里这些统统不存在。HTTP 客户端只是取回字节,没有任何东西去执行 script 标签,因此小组件不会渲染,textarea 不会被创建,也没有提交处理函数可以运行。
取而代之的机制更简单,也更容易理清。token 只是一个字符串,网站期望它出现在 POST 请求体的某个字段名下。对 reCAPTCHA v2 来说,这个字段叫 g-recaptcha-response,无论是浏览器填的还是你填的,名字都一样。Turnstile 用的是 cf-turnstile-response。极验(GeeTest)提交的是三个字段,而不是一个。校验发生在网站的服务器上,由它去向 Google 或 Cloudflare 求证,所以对面没有任何办法分辨这个字符串是你的哪个进程生成的。
第 1 步:用之后要提交的那个客户端读取 sitekey
整个流程只用一个客户端。这不是为了代码整洁。这正是关键所在:cookie jar 和连接池才是让 POST 看起来和 GET 来自同一位访客的东西。为提交另建一个新客户端,就等于把页面设置的所有会话 cookie 全丢掉,而这正是一个正确的 token 依然失败的常见原因。
# pip install capskip httpx
import re
import httpx
PAGE_URL = "https://example.com/page-with-recaptcha"
# One client for the whole flow. Its cookie jar is what makes
# the POST later look like the same visitor as this GET.
client = httpx.Client(timeout=30, follow_redirects=True)
html = client.get(PAGE_URL).text
match = re.search(r'data-sitekey=["\']([^"\']+)', html)
if not match:
raise RuntimeError("No data-sitekey in the HTML.")
sitekey = match.group(1) # this is what the solver needs如果这段正则什么也没匹配到,说明小组件是由 JavaScript 注入的,而不是随 HTML 一起下发的,HTTP 客户端永远看不到它。用浏览器打开页面,从网络面板里把 sitekey 取出来一次,然后硬编码进去。sitekey 是公开且稳定的,所以走这条捷径你完全不必有心理负担。
第 2 步:在你自己的机器上识别它
识别就是向监听在你自己硬件上的服务发起一次调用。reCAPTCHA 的各个变体用的都是同一个方法,只是关键字参数不同:把 invisible 设为 1、把 enterprise 设为 1,或者把 version 设为 v3 并带上一个 action。Turnstile 和极验有各自的方法,形式完全一样。完整的参数列表见 CapSkip API 文档.
# CapSkip listens on your machine, so this is a loopback call. from capskip import CapSkip solver = CapSkip(host="127.0.0.1", port=8080) token = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)["code"]
这个调用在轮询期间会阻塞。SDK 并不是按固定间隔轮询:它从 250 毫秒开始,逐步回退到 pollingInterval,这也是 SDK 的一次识别通常比手写的原始接口轮询循环更早返回的原因。上限是 recaptchaTimeout,默认为 300 秒。
第 3 步:把 token 作为表单字段提交
现在用同一个客户端、按浏览器本来会提交的那个表单把它发回去。表单携带的其他字段也要一并带上,包括你在第一步从 HTML 中读到的隐藏 CSRF 值或 nonce 值。
# g-recaptcha-response is an ordinary form field. Same client,
# so the session cookies from the GET go along with it.
reply = client.post(
PAGE_URL,
data={
"username": "demo",
"g-recaptcha-response": token,
},
)
print(reply.status_code)拿到 token 就立刻发出去。一个 reCAPTCHA token 的有效期大约两分钟,而识别与提交之间的队列、重试退避或 sleep,会在你毫无察觉的情况下把这点预算烧光。这个有效期以及它在实际中意味着什么,详见 reCAPTCHA token 过期指南.
完整可运行示例:异步版
同样的三步,改用异步客户端。Python 中的 AsyncCapSkip 是真正的异步实现,而不是同步版的别名,所以它会共用你的事件循环,轮询时也不会占住一个线程。
# pip install capskip httpx
import asyncio
import re
import httpx
from capskip import AsyncCapSkip
PAGE_URL = "https://example.com/page-with-recaptcha"
SITEKEY_RE = re.compile(r'data-sitekey=["\']([^"\']+)')
async def submit_once(client, solver):
html = (await client.get(PAGE_URL)).text
match = SITEKEY_RE.search(html)
if not match:
raise RuntimeError("No data-sitekey in the HTML.")
result = await solver.recaptcha(sitekey=match.group(1), url=PAGE_URL)
reply = await client.post(
PAGE_URL,
data={"g-recaptcha-response": result["code"]},
)
return reply.status_code
async def main():
solver = AsyncCapSkip(host="127.0.0.1", port=8080)
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
print(await submit_once(client, solver))
asyncio.run(main())要同时跑多个,就用 gather 把它们并起来。整批共用一个识别工具实例就够了,而客户端通常按身份各建一个,因为共用 cookie jar 就意味着共用会话。批量处理这一面,详细写在 用 Python 并行识别验证码的指南.
aiohttp 版本
上面所有内容都成立。只有三个名字变了:会话类型、读取响应体的方式,以及保存状态码的那个属性。
# pip install capskip aiohttp
import aiohttp
from capskip import AsyncCapSkip
async def submit_once(session, solver):
async with session.get(PAGE_URL) as reply:
html = await reply.text()
match = SITEKEY_RE.search(html)
result = await solver.recaptcha(sitekey=match.group(1), url=PAGE_URL)
async with session.post(
PAGE_URL,
data={"g-recaptcha-response": result["code"]},
) as submitted:
return submitted.status # not status_code
async def main():
solver = AsyncCapSkip(host="127.0.0.1", port=8080)
async with aiohttp.ClientSession() as session:
print(await submit_once(session, solver))ClientSession 自带 cookie jar,所以“只用一个客户端”这条规则原样适用。aiohttp 不支持 HTTP/2,而在这件事上,这一点不会让你付出任何代价。
代理,以及一个只坑 HTTP 客户端的陷阱
如果网站会检查 token 是否由提交它的那个地址生成,那么识别和提交就必须从同一个代理出去。对 reCAPTCHA、Turnstile 和极验,CapSkip 支持为每个任务单独指定代理。图片验证码不接受代理,也不需要代理。
# Same exit address for the solve and for the submit.
result = await solver.recaptcha(
sitekey=sitekey,
url=PAGE_URL,
proxy={"type": "HTTP", "uri": "user:[email protected]:3128"},
)
# httpx 0.26 and newer spell this proxy=. Before that it was
# proxies=, which 0.28 removed outright.
async with httpx.AsyncClient(proxy="http://user:[email protected]:3128") as client:
await client.post(PAGE_URL, data={"g-recaptcha-response": result["code"]})下面这点最容易把人绊倒。CapSkip 接受 SOCKS5 和 SOCKS5H 作为代理类型,但 aiohttp 只会说 HTTP 代理,外加通过 CONNECT 隧道走 HTTPS,而 httpx 必须先装上 socks 附加依赖才肯支持 SOCKS。把一个你的客户端根本用不了的 SOCKS5 代理交给识别工具,结果就是识别从一个地址成功、提交却从另一个地址出去,这看起来和 token 无效一模一样,其实并不是。让两端都用一种双方都会说的代理类型。代理类型之间如何取舍,详见 识别过程中轮换代理的指南.
还有一个 aiohttp 特有的问题:和 requests 不同,除非你在构造会话时把 trust_env 设为 True,否则它会忽略 HTTP_PROXY 和 HTTPS_PROXY 环境变量。你以为已经生效的代理,其实根本没有生效,而且什么都没有提示你。
HTTP/2,以及开启它会改变什么
httpx 会说 HTTP/2,但既不是默认开启,也离不开附加依赖:安装 httpx 时带上 http2 附加项,并在构建客户端时把 http2 设为 True。值得注意的是,这会改变你的流量在链路上的样子,因为协议协商和请求头顺序都与 HTTP/1.1 不同。这属于指纹问题而不是识别问题,是另一个话题,大致介绍见 关于 Python 中 TLS 指纹的文章.
把识别工具跑在另一台机器上
脚本是会挪地方的。容器、VPS 或定时任务节点并不是装着识别工具的那台机器,在那里访问回环地址指向的是容器本身,而容器上根本没有东西在监听。
CapSkip 有两种连接模式。Local 模式绑定 127.0.0.1,只响应本机。Server 模式绑定你的内网或公网 IP,这样容器、虚拟机或托管的工作节点就能通过 API 访问同一台 Windows 机器。固定公网 IP 可以让地址保持不变。两种模式用的都是你自己的硬件,也都不限量,所以再忙的一天,两种模式花的钱都一样。
# The SDK reads these three itself, so the same code works # whether the solver is local or on another machine: # CAPSKIP_HOST=192.0.2.10 # CAPSKIP_PORT=8080 # CAPSKIP_API_KEY=your-key solver = AsyncCapSkip()
一旦识别工具开始监听网络地址,就把密钥校验打开,并给每个工作节点分配各自的密钥,这样吊销其中一个也不会牵动其余的。两种模式的详细说明见 CapSkip 设置指南.
常见错误及其含义
| 你所看到的 | 原因 | 修复 |
|---|---|---|
| 正则匹配不到 data-sitekey | 小组件由 JavaScript 注入,因此不在服务器下发的 HTML 里 | 在浏览器里读一次 sitekey 并硬编码。它是公开且稳定的 |
| 表单又带着挑战返回了 | 为 POST 另建了一个客户端,会话 cookie 因此丢失 | GET 和 POST 使用同一个客户端 |
| 有效的 token 被拒绝 | 识别和提交从不同的地址发出 | 两端使用同一个代理,且类型是两端都会说的 |
| 有效的 token 延迟一会儿后被拒绝 | 它在识别与提交之间的队列里过期了 | 尽量晚一点识别,拿到就立刻提交 |
| aiohttp 上的代理似乎被忽略 | aiohttp 默认不读取代理环境变量 | 显式传入 proxy=,或在构建会话时把 trust_env 设为 True |
| httpx 客户端构造函数抛出 TypeError | proxies= 在 httpx 0.28 中已被移除 | 改用 proxy=,从 0.26 起就是这个名字 |
| NetworkException | CapSkip 没有在运行,或者主机和端口不对 | 启动应用,或把 CAPSKIP_HOST 指向服务器地址 |
| ValidationException | 该验证码类型下 SDK 不接受的参数 | 检查类型。在 v2 上发送 action,或在 v3 上发送 invisible,都会触发它 |
常见问题
reCAPTCHA v2 真的能在没有浏览器的情况下搞定吗?
可以。网站的服务器会拿 token 去 Google 那里校验,而这次校验只看 token、sitekey 以及它来自哪个地址。这次校验没有任何办法追问这个字符串是由什么进程生成的。浏览器从来都只是放置那个字段的一个方便位置而已。
安装 SDK 会强行给我装上 httpx 吗?
会。requests、httpx 和 aiofiles 都是这个包声明的依赖,所以即使在 aiohttp 项目里,httpx 也会跟着装进来。这只是环境里多了第二个 HTTP 客户端,而不是冲突,你自己的代码完全不必用它。
reCAPTCHA v3 该怎么做?
把 version 设为 v3,并带上页面使用的 action,然后按网站自己的脚本写入 token 的那个字段名把 token 提交上去。这个名字不像 g-recaptcha-response 那样有统一标准,所以要先从页面里读一次。action 必须对得上,否则即便 token 是真的,服务器端的校验照样会失败。
这件事我该用 httpx 还是 aiohttp?
都行。httpx 已经随 SDK 装好,装上相应的附加依赖后还能支持 HTTP/2 和 SOCKS,选它就不会给你的环境增加任何新东西。aiohttp 在极高并发下更快,而且很多现成的爬虫本来就建立在它之上。两者的识别代码完全一样,所以请按爬虫本身的优劣来选,而不是按验证码这一侧来选。
简短版结论
从服务器下发的 HTML 中读出 sitekey,在本地识别,然后用抓取该页面的那个客户端,把 token 作为一个普通表单字段提交上去。整个流程只用一个客户端,cookie 才保得住。识别和提交使用同一个出口地址,代理类型要选两端都真的会说的那种。拿到就马上提交,因为 token 的寿命很短。
更完整的 Python 相关内容见 Python 验证码识别页面。reCAPTCHA v2 的细节见 reCAPTCHA v2 识别页面。同样的三个调用在 Node.js、PHP 和 C# 里也都有,它们列在 验证码识别 SDK 页面.
在把它用到大规模爬取之前,最后还有一点值得知道。CapSkip 是一款 无限量验证码识别工具 ,运行在你本来就拥有的硬件上,所以一次识别五万个挑战的爬取,花的钱和只识别五十个的爬取完全一样。
