如何在 Windows 上运行 FastAPI 验证码识别服务

要运行 FastAPI 验证码识别服务,就在应用启动时创建一个 AsyncCapSkip 客户端。提供一个接收 sitekey 和页面 URL 的 POST 端点。给每次识别加上 asyncio.timeout 和一个 Semaphore,并把 SDK 的异常映射为 HTTP 状态码。这样,用任何语言编写的爬虫、后台程序和脚本,只需调用一个内部地址,永远看不到 CapSkip 的端口或密钥。CapSkip 运行在你自己的 Windows 机器上,所以这个服务不会增加按次识别的费用。本指南用大约 100 行代码构建它,并展示在 CapSkip 响应慢、已关闭或毫无回应时,每个部分会怎么表现。
你需要什么
- 在 Windows PC 上运行的 CapSkip,并已开启 reCAPTCHA 识别功能。 设置指南 会带你完成首次启动的每一步。
- Python 3.11 或更高版本,因为代码用到了 asyncio.timeout。下面的所有内容都在 Windows 11 上运行过,使用 Python 3.14.0、capskip 1.3.0、fastapi 0.150.0 和 uvicorn 0.54.0。
- 一个 PowerShell 提示符。这些命令使用 PowerShell 语法。
- 一个带有 reCAPTCHA v2 复选框的页面。服务只需要它的 sitekey 和它的地址。
- CapSkip 的地址。在 Local 模式下,它只在 127.0.0.1 上响应,仅供本机使用。在 Server 模式下,它监听你的网络地址或公网 IP,这样另一台机器上的服务就能通过 API 调用它。两种模式的设置位置都是 连接设置.
本指南中的数据来自同一台 PC 上一个为测试搭建的模拟 CapSkip。FastAPI、uvicorn 和 SDK 都是真实的。模拟程序会让每次识别保持一段固定的时间,通常是两秒,所以你的真实识别耗时会有所不同。这里的一切都没有在真实的 CapSkip 或真实的站点上运行过。
为什么要在 CapSkip 前面放一个服务
如果只有一个 Python 脚本在识别验证码,直接在该脚本里调用 SDK 即可。 Python 验证码识别页面 对此有详细介绍, 并行识别指南 则介绍了在一个进程中进行多次识别。当多个调用方共用一个 CapSkip 时,服务才值得搭建。这时有四件事适合放在同一个地方处理。
- 一个时限。Python SDK 默认最多等待 300 秒。它的普通客户端完全不设置 HTTP 超时。不该让每个调用方各自带一个计时器。
- 一个上限。CapSkip 同时只运行固定数量的识别,多出来的任务要在它内部等待。你自己掌控的队列更容易理解和推断。
- 一个密钥。调用方拿到的是服务密钥。CapSkip 的地址和 API 密钥保留在服务的设置里。
- 任何语言。C# 后台程序、PHP 站点和 PowerShell 任务都能使用 HTTP 和 JSON。
第 1 步:安装这些包
如果你为每个项目各用一个虚拟环境,请先创建文件夹和虚拟环境。capskip 包会连带安装 httpx,异步客户端用的就是它。
pip install capskip fastapi uvicorn
第 2 步:把设置写成环境变量
SDK 自己不读取任何环境变量,所以由服务读取它们,再把每个值传进去。在将要启动服务的那个 PowerShell 窗口里设置它们。请替换两个占位符密钥。YOUR_CAPSKIP_KEY 是 CapSkip 设置中的 API 密钥,在 CapSkip 里的密钥校验关闭时,任何文本都可以。YOUR_API_KEY 是你自己为调用方想出来的一个密钥。
# Where CapSkip listens and which key it expects. $env:CAPSKIP_HOST = "127.0.0.1" $env:CAPSKIP_PORT = "8080" $env:CAPSKIP_API_KEY = "YOUR_CAPSKIP_KEY" # Match Max. Threads in the CapSkip settings. $env:CAPSKIP_MAX_THREADS = "10" # The key your callers must send to this service. $env:SOLVER_API_KEY = "YOUR_API_KEY" # Seconds a caller may wait, queue time included. $env:SOLVE_DEADLINE = "120"
主机请用 127.0.0.1。用这个客户端时,连接被拒绝在 127.0.0.1 上用了大约 2.4 秒,在 localhost 上用了大约 2.7 秒。在之前的一次测试中,普通的 requests 客户端在 localhost 上用了大约 4 秒。
第 3 步:读取设置
创建 main.py。第 3 步到第 10 步的代码块是这一个文件的连续各部分。结尾处的完整文件把它们的导入语句合并到顶部。
import asyncio
import os
from contextlib import asynccontextmanager
from capskip import AsyncCapSkip
from fastapi import FastAPI
# The SDK reads no env vars, so pass each setting in by hand.
HOST = os.getenv("CAPSKIP_HOST", "127.0.0.1")
PORT = int(os.getenv("CAPSKIP_PORT", "8080"))
KEY = os.getenv("CAPSKIP_API_KEY", "capskip")
MAX_THREADS = int(os.getenv("CAPSKIP_MAX_THREADS", "10"))
DEADLINE = float(os.getenv("SOLVE_DEADLINE", "120"))第 4 步:应用启动时构建一个客户端和一个闸门
lifespan 函数在启动时运行一次。它创建客户端和一个 Semaphore,并把两者都放到应用状态上,这样每个请求共用它们。给 Semaphore 的槽位数等于 CapSkip 的线程数,也就是 CapSkip 中的 Max. Threads 设置(默认为 10)。
AsyncCapSkip 是 Python 中真正的异步客户端。它基于 httpx,并用 asyncio.sleep 轮询,所以两次轮询之间的等待不会阻塞事件循环。这些调用本身确实要消耗一些 CPU,标题为“一个需要了解的代价”的那一节会解释这一点。普通的 CapSkip 客户端调用 requests 时不带超时,所以一个毫无回应的 CapSkip 会让线程永远卡住。把 recaptchaTimeout 设为时限,能让 SDK 自己的时钟与我们的保持一致。两者同时到期时,asyncio.timeout 先触发。
@asynccontextmanager
async def lifespan(app: FastAPI):
# One client and one gate, shared by every request.
app.state.cap = AsyncCapSkip(
apiKey=KEY, host=HOST, port=PORT,
recaptchaTimeout=DEADLINE,
)
app.state.slots = asyncio.Semaphore(MAX_THREADS)
yield
app = FastAPI(lifespan=lifespan)第 5 步:检查服务密钥
网络地址上不受保护的识别端点,对所有能访问该端口的人来说就是一个免费的识别工具。这个依赖项用 secrets.compare_digest 比较 X-API-Key 请求头,无论字节在哪里不同,它所用的时间都相同。在模拟程序上,缺失、为空、错误、过长和包含非 ASCII 字符的密钥都得到了 401。正确的密钥得到了 200。
顶部的检查让服务不会在密钥为空时启动。没有它的话,没有请求头的请求会默认变成空字符串,并且匹配成功。依赖项在检查正文之前运行,所以没有密钥且正文未通过校验的请求,得到的是 401,而不是 422。不是有效 JSON 的正文仍然得到 422,因为 FastAPI 先解码 JSON。没有密钥的调用方几乎了解不到你的数据结构。
import secrets
from fastapi import Depends, Header, HTTPException
SERVICE_KEY = os.environ["SOLVER_API_KEY"]
if not SERVICE_KEY:
raise SystemExit("SOLVER_API_KEY must not be empty")
def require_key(x_api_key: str = Header(default="")):
good = secrets.compare_digest(
x_api_key.encode(), SERVICE_KEY.encode())
if not good:
raise HTTPException(
401, "missing or wrong X-API-Key",
headers={"WWW-Authenticate": "APIKey"})第 6 步:校验请求并设置时限
请求模型会拒绝空的 sitekey,或者不以 http 开头的 URL。FastAPI 会用它自己的 422 响应错误的正文,并列出每一个问题。
时限是最重要的部分。asyncio.timeout 把等待槽位和识别一起包起来。负载较轻的服务总共在 DEADLINE 秒内响应。如果超时只包住识别,请求在它的时钟开始之前,可能已经排队好几分钟。负载很重时,响应可能来得更晚,标题为“一个需要了解的代价”的那一节会解释这一点。
from fastapi import Request
from pydantic import BaseModel, Field
class SolveRequest(BaseModel):
sitekey: str = Field(min_length=1)
url: str = Field(pattern=r"^https?://")
async def solve_v2(state, sitekey: str, url: str) -> str:
# The deadline also covers the wait for a free slot.
async with asyncio.timeout(DEADLINE):
async with state.slots:
result = await state.cap.recaptcha(sitekey, url)
return result["code"]第 7 步:添加识别端点
这个端点只有三行。先运行密钥依赖项,然后识别,最后把 token 以 JSON 返回。
@app.post("/solve", dependencies=[Depends(require_key)])
async def solve(body: SolveRequest, request: Request):
token = await solve_v2(request.app.state, body.sitekey, body.url)
return {"token": token}第 8 步:把 SDK 的异常映射为状态码
SDK 会抛出四个异常类,都在 CapSkipError 之下,而且都可以从 capskip 导入。基类上的一个处理程序把每一个异常转换成一个状态码和一个简短的错误名。详细信息写入日志,永远不发给调用方,所以 CapSkip 的消息保持私密。
import logging
from capskip import ApiException, CapSkipError, NetworkException
from capskip import TimeoutException, ValidationException
from fastapi.responses import JSONResponse
STATUS = {
NetworkException: (503, "capskip_unreachable"),
TimeoutException: (504, "capskip_timeout"),
ValidationException: (422, "bad_parameters"),
ApiException: (502, "capskip_error"),
}@app.exception_handler(CapSkipError)
async def capskip_error(request: Request, exc: CapSkipError):
# Log the detail here, keep it away from the caller.
logging.getLogger("uvicorn.error").warning("%r", exc)
status, code = STATUS.get(type(exc), (502, "capskip_error"))
if "UNSOLVABLE" in str(exc):
status, code = 422, "unsolvable"
return JSONResponse({"error": code}, status)下面每一行都是在模拟程序上故意触发过的。
| 状态码 | 发生了什么 | 用时 |
|---|---|---|
| 503 | CapSkip 已关闭,所以连接被拒绝 | 2.2 s |
| 503 | CapSkip 接受了连接,却从未应答提交 | 5.2 s |
| 422 | CapSkip 报告了 ERROR_CAPTCHA_UNSOLVABLE | 未计时 |
| 502 | CapSkip 拒绝了服务自己的 API 密钥 | 未计时 |
| 504 | 轮询毫无回应的 CapSkip 时时限到了(一个请求,时限 10 秒) | 10.0 s |
502 是有意为之的。被拒绝的 CapSkip 密钥是服务的错误,不是调用方的错误。5.2 秒那一行是 httpx 的默认超时 5 秒,异步客户端继承了它。对轮询毫无应答的情况则不同。SDK 把失败的轮询当作尚未就绪,并再试一次,所以只有时限才能结束它。只给出 sitekey 和 URL 时,不会出现 ValidationException。它需要额外的选项,比如在 v2 识别上带 action,而这个服务从不发送它。
第 9 步:单独捕获时限
asyncio.timeout 抛出的是 Python 内置的 TimeoutError。SDK 的 TimeoutException 是另一个类,捕获内置的那个并不会捕获它。第 8 步里的处理程序已经把 SDK 的 TimeoutException 映射为名为 capskip_timeout 的 504。第二个处理程序负责时限,它的错误名会告诉调用方是哪个时钟走完了。
@app.exception_handler(TimeoutError)
async def deadline_hit(request: Request, exc: TimeoutError):
# asyncio.timeout raises the built-in TimeoutError. The SDK's
# TimeoutException is a different class, so it needs its own entry.
return JSONResponse({"error": "deadline_exceeded"}, 504)第 10 步:添加健康检查
CapSkip 没有 ping 调用,所以这个检查会向它的端口打开一个 TCP 连接,三秒后放弃。CapSkip 关闭时,它大约在 2 秒后返回 503;模拟程序在运行时,它立刻返回 200。它能告诉监控脚本:服务在运行,而 CapSkip 没有。它不能证明识别能正常工作。
@app.get("/health")
async def health():
# CapSkip has no ping call, so check that its port accepts a
# connection.
try:
_, writer = await asyncio.wait_for(
asyncio.open_connection(HOST, PORT), timeout=3)
except (OSError, TimeoutError):
return JSONResponse({"ok": False}, 503)
writer.close()
return {"ok": True}在 Windows 上运行
从包含 main.py 的文件夹启动 uvicorn。绑定到 127.0.0.1 时,服务只在这台 PC 上响应。
python -m uvicorn main:app --host 127.0.0.1 --port 8000
如果调用方在其他机器上,就把主机改成 0.0.0.0,意思是所有网络适配器。在测试用的 PC 上,它通过这台 PC 的局域网地址响应了,而绑定到 127.0.0.1 时则被拒绝。先设置一个真正的服务密钥,并检查 Windows 为该端口创建了什么防火墙规则。服务密钥是以明文通过普通 HTTP 传输的,所以只在你信任的网络上这样用。这里没有观察到防火墙提示,也没有测试从另一台机器的访问。
只运行一个 worker。把 workers 选项设为 2、每个上限为 3 时,模拟程序最多同时看到 6 次识别。每个 worker 是一个独立的进程,有自己的 Semaphore,所以上限会成倍增加。你编辑代码时可以放心使用 reload 选项。在识别进行中修改文件,并没有丢掉正在运行的请求,1.5 秒后的新请求同样成功了。
文档页面默认是公开的。当服务监听网络地址时,创建应用时把 docs_url 和 openapi_url 设为 None,这样数据结构就不会公开。
把服务运行在 CapSkip 之外的机器上
服务不必和 CapSkip 在同一台 PC 上。把 CapSkip 放在你控制的一台 Windows 机器上以 Server 模式运行,最好有静态公网 IP。在启动 uvicorn 之前,把 CAPSKIP_HOST 和 CAPSKIP_PORT 设为这个地址。有三点要注意。在 CapSkip 中开启 API key validation,并在 CAPSKIP_API_KEY 中使用同一个密钥。SDK 使用普通 HTTP 通信,所以这个密钥会以明文在网络上传输。CapSkip 还会为它的端口添加一条允许任何地址的规则,所以要为每一个不该访问它的地址添加一条阻止规则。设置指南中的 连接设置 一节列出了这两种模式。
用 PowerShell 调用它
调用方发送 sitekey、页面 URL 和服务密钥。响应是一个 JSON 对象,只有一个字段 token。请在大约两分钟内使用这个 token,而且只能使用一次。 token 过期指南 详细讲解了这个计时机制。下面的地址是你的服务的占位符。
Invoke-RestMethod -Method Post -Uri http://192.168.1.50:8000/solve `
-Headers @{ "X-API-Key" = "YOUR_API_KEY" } `
-ContentType "application/json" `
-Body '{"sitekey":"YOUR_SITEKEY","url":"https://example.com/page-with-recaptcha"}'向 /solve/ 发送带尾部斜杠的 POST 请求,会得到 307 重定向。Windows PowerShell 5.1 上的 Invoke-RestMethod 跟随了它,并保留了正文和请求头。如果你更喜欢 curl.exe,就通过 stdin 传入 JSON。常见的单引号 JSON 参数在 PowerShell 5.1 上会失败,因为里面的双引号被去掉了,服务因此返回了一个 JSON 错误。
# curl.exe reads the JSON from stdin, so no quote escaping is needed.
$body = '{"sitekey":"YOUR_SITEKEY","url":"https://example.com/page-with-recaptcha"}'
$body | curl.exe -s http://192.168.1.50:8000/solve `
-H "X-API-Key: YOUR_API_KEY" `
-H "Content-Type: application/json" `
--data-binary "@-"闸门能做什么,不能做什么
模拟程序的线程上限是 3,十个请求同时到达,每个都需要一次 2 秒的识别。闸门让模拟程序同时只进行三次识别,十个请求全部返回了 200。没有一个有效的闸门时,十个任务全都送到了 CapSkip,并在那里排队。在模拟程序里,其中四个在等待超过它三秒的等待上限后放弃,并作为无法识别返回。这个响应是模拟程序自己的模型,没有在真实的 CapSkip 上确认过。
等待槽位放在时限之内,是有原因的。在时限 5 秒、识别 1 秒、十个请求同时到达的情况下,六个返回了 200,四个返回了 504。有一个请求始终没有送到 CapSkip,而送到的九个里,有三个在识别中途被放弃了。
槽位被占用的时间比识别本身稍长一点。SDK 先立即轮询一次,然后依次等待 0.25、0.5、1、2 和 4 秒,之后每两次轮询之间等待 5 秒。这样轮询就落在 0、0.25、0.75、1.75、3.75、7.75 和 12.75 秒,而槽位要到某次轮询读到答案时才会释放。恰好出现在一次轮询之后的结果,要等下一次轮询。7.75 秒的那次轮询比一次 8 秒的识别早了四分之一秒。在测试用的 PC 上,下一节所说的慢调用把那次轮询推迟到了 8 秒之后,所以答案在大约 8.4 到 8.9 秒时才被读到。去掉这一代价之后,同样的识别要等 12.75 秒的那次轮询,在默认 pollingInterval 为 5 时于 12.9 秒返回,在 pollingInterval 为 1 时于 8.9 秒返回。如果识别很长、队列也很长,调低 pollingInterval 能更快地释放槽位。
还剩下两个限制。断开连接的调用方不会取消它的处理程序,所以被遗弃的识别会一直占着它的槽位,直到结束。只有一个槽位、识别耗时 4 秒时,下一个请求总共用了 8.3 秒,而槽位空闲时是 4.7 秒。时限过去之后,模拟程序也会继续处理被放弃的任务,而 SDK 没有取消调用。因此,槽位计数可能会在几秒钟内领先于 CapSkip 的真实线程数。这一点在模拟程序里是建模过的。真实的应用没有测试过。
一个需要了解的代价
AsyncCapSkip 为它发出的每个请求,包括每一次轮询,都会新建一个 httpx 客户端。在测试用的 PC 上,这每次要花大约 141 毫秒的阻塞式 CPU 时间,主要用在加载证书包上,而普通 HTTP 地址根本不需要它。十个同时进行的 2 秒识别大约在 6 秒内完成,在这一波请求期间发给 /openapi.json 的请求最多等了大约 1.9 秒。同样的十个请求通过同步端点只用了 3.8 秒,最坏情况只有几毫秒,但这条路线无法结束一个挂起的调用。把时限包在一个线程外面,线程仍然会卡住。41 个请求挂起之后,服务里有 41 个卡死的线程,它们会一直留到进程重启。
被阻塞的循环还会让时钟在负载下出现漂移。面对时限为 2 秒、毫无回应的 CapSkip,12 个同时到达的请求里,有 8 个在 2.6 到 2.9 秒返回,有 4 个在 8.5 秒返回了名为 capskip_timeout 的错误。41 个请求时,最慢的用了 13.9 秒。单个请求则按时限完成。8.5 秒这些响应的原因没有被单独查明,但被阻塞的循环是其中一部分,而且仅用 SDK 也出现了同样的漂移。对于少数几个调用方,这种停顿是无害的。它会随同时进行的识别数增加而变大,所以让槽位数保持在 CapSkip 真正能运行的数量。
完整的 main.py
这是把第 3 步到第 10 步拼在一起的结果,导入语句集中放在顶部。把它保存为 main.py。
import asyncio
import logging
import os
import secrets
from contextlib import asynccontextmanager
from capskip import ApiException, AsyncCapSkip, CapSkipError
from capskip import NetworkException, TimeoutException, ValidationException
from fastapi import Depends, FastAPI, Header, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
# The SDK reads no env vars, so pass each setting in by hand.
HOST = os.getenv("CAPSKIP_HOST", "127.0.0.1")
PORT = int(os.getenv("CAPSKIP_PORT", "8080"))
KEY = os.getenv("CAPSKIP_API_KEY", "capskip")
MAX_THREADS = int(os.getenv("CAPSKIP_MAX_THREADS", "10"))
DEADLINE = float(os.getenv("SOLVE_DEADLINE", "120"))
@asynccontextmanager
async def lifespan(app: FastAPI):
# One client and one gate, shared by every request.
app.state.cap = AsyncCapSkip(
apiKey=KEY, host=HOST, port=PORT,
recaptchaTimeout=DEADLINE,
)
app.state.slots = asyncio.Semaphore(MAX_THREADS)
yield
app = FastAPI(lifespan=lifespan)
SERVICE_KEY = os.environ["SOLVER_API_KEY"]
if not SERVICE_KEY:
raise SystemExit("SOLVER_API_KEY must not be empty")
def require_key(x_api_key: str = Header(default="")):
good = secrets.compare_digest(
x_api_key.encode(), SERVICE_KEY.encode())
if not good:
raise HTTPException(
401, "missing or wrong X-API-Key",
headers={"WWW-Authenticate": "APIKey"})
class SolveRequest(BaseModel):
sitekey: str = Field(min_length=1)
url: str = Field(pattern=r"^https?://")
async def solve_v2(state, sitekey: str, url: str) -> str:
# The deadline also covers the wait for a free slot.
async with asyncio.timeout(DEADLINE):
async with state.slots:
result = await state.cap.recaptcha(sitekey, url)
return result["code"]
@app.post("/solve", dependencies=[Depends(require_key)])
async def solve(body: SolveRequest, request: Request):
token = await solve_v2(request.app.state, body.sitekey, body.url)
return {"token": token}
STATUS = {
NetworkException: (503, "capskip_unreachable"),
TimeoutException: (504, "capskip_timeout"),
ValidationException: (422, "bad_parameters"),
ApiException: (502, "capskip_error"),
}
@app.exception_handler(CapSkipError)
async def capskip_error(request: Request, exc: CapSkipError):
# Log the detail here, keep it away from the caller.
logging.getLogger("uvicorn.error").warning("%r", exc)
status, code = STATUS.get(type(exc), (502, "capskip_error"))
if "UNSOLVABLE" in str(exc):
status, code = 422, "unsolvable"
return JSONResponse({"error": code}, status)
@app.exception_handler(TimeoutError)
async def deadline_hit(request: Request, exc: TimeoutError):
# asyncio.timeout raises the built-in TimeoutError. The SDK's
# TimeoutException is a different class, so it needs its own entry.
return JSONResponse({"error": "deadline_exceeded"}, 504)
@app.get("/health")
async def health():
# CapSkip has no ping call, so check that its port accepts a
# connection.
try:
_, writer = await asyncio.wait_for(
asyncio.open_connection(HOST, PORT), timeout=3)
except (OSError, TimeoutError):
return JSONResponse({"ok": False}, 503)
writer.close()
return {"ok": True}FastAPI 验证码识别服务的常见错误
| 你所看到的 | 原因 | 修复 |
|---|---|---|
| 服务在启动时退出 | SOLVER_API_KEY 未设置或为空。这时 Python 会在 PowerShell 窗口里为它打印 KeyError。 | 在同一个窗口里设置它 |
| 每次调用都返回 401 | X-API-Key 请求头与 SOLVER_API_KEY 不相等 | 发送服务密钥,而不是 CapSkip 的密钥 |
| 504,附带 deadline_exceeded | 等待槽位加上识别超过了时限 | 调高 SOLVE_DEADLINE,或者同时调高 CapSkip 中的 Max. Threads 和 CAPSKIP_MAX_THREADS |
| 504,附带 capskip_timeout | SDK 的轮询上限先用完了,通常发生在负载较高时 | 减少同时进行的请求数,或者调高 SOLVE_DEADLINE |
| 503,附带 capskip_unreachable | 没有任何程序在 CapSkip 的端口上接受连接 | 启动 CapSkip,并检查 Local 模式或 Server 模式 |
| 上限比预期的高 | 不止一个 uvicorn worker | 只运行一个 worker |
| 来自 curl.exe 的 422,附带 json_invalid | PowerShell 5.1 去掉了引号 | 通过 stdin 把 JSON 管道传入 |
FastAPI 自带的 lifespan 文档 介绍了启动钩子,所有 CapSkip 错误代码请见 API 文档.
常见问题
如果只有一个脚本在识别验证码,还需要服务吗?
不需要。直接在该脚本里调用 SDK 即可。当多个调用方,或者使用其他语言的调用方,共用一个 CapSkip,而你想要一个时限、一个上限和一个密钥时,服务才有帮助。 reCAPTCHA v2 Python 指南 展示了直接调用的写法。
可以把服务托管在云平台上吗?
可以,但托管的服务运行在别人的机器上,所以它无法访问你 PC 上的 127.0.0.1。请在你控制的一台 Windows 机器上以 Server 模式运行 CapSkip,最好有静态公网 IP,并把 CAPSKIP_HOST 设为这个地址。开启 API key validation,为其他每个地址添加阻止规则,并记住这个密钥是通过普通 HTTP 以明文传输的。本指南没有测试任何托管平台。
端点应该改用普通的 def 函数吗?
不适合这个用途。普通的 def 端点在线程里运行,而普通的 CapSkip 客户端没有办法结束一个挂起的调用。挂起的调用永远不会返回,所以调用方要等到它自己的超时。把它包在时限里,响应能按时返回,但线程仍然卡住。异步客户端则能干净地结束。
同一个端点能识别 reCAPTCHA v3 吗?
按现在这样写,不行。服务调用的是 v2 方法。对于 v3,在调用中添加 version 和 action,SDK 的 reCAPTCHA 方法接受它们。不要发送 score 值,因为 CapSkip 没有 score 设置。 reCAPTCHA v2 识别工具页面 介绍了本指南所用的复选框类型。
简短版结论
要运行 FastAPI 验证码识别服务,就保留一个 AsyncCapSkip、一个 Semaphore,并用一个 asyncio.timeout 把等待和识别包起来。先检查服务密钥。把 SDK 的 TimeoutException 与内置的 TimeoutError 分开捕获。只运行一个 worker。如果 CapSkip 以 Server 模式运行,就在它前面放一条阻止规则。
- Python 包能处理的所有验证码类型: Python 验证码识别页面.
- 复选框识别工具是如何工作的: reCAPTCHA v2 识别页面.
- 在一个脚本中进行多次识别: 在 Python 中并行识别验证码.
一个共享服务可以供整个团队使用,不需要有人去数识别了多少次。CapSkip 的 验证码绕过 功能在你自己的 Windows 机器上运行,所以服务背后的每一次识别都不会产生按次计费的账单。
