如何在 Scrapling 中用 page_action 识别验证码

Scrapling 里的验证码任务可以干净地一分为二,弄清楚自己在哪一半能省下一个下午。Cloudflare 有自己的开关:给 StealthyFetcher 传入 solve_cloudflare,Turnstile 或过渡页挑战就会被自动处理。其余的都归你自己。reCAPTCHA v2、reCAPTCHA v3、极验(GeeTest)和图片验证码要在 fetcher 之外识别,再通过 page_action 钩子注入页面。本文讲的就是第二半,包括那个会悄无声息地让它失效的细节。
你需要什么
- Python 3.10 或更高版本。Scrapling 要求如此。
- 安装带 fetchers 扩展的 Scrapling,以及它一次性的浏览器下载步骤。
- 受保护页面的 URL。你不需要事先拿到 sitekey,因为第一趟请求会读出它。
- 如果爬虫和识别程序在同一台机器上,就以 Local 模式运行 CapSkip;如果不在,就用 Server 模式。两者都在 连接设置.
# pip install capskip pip install capskip pip install "scrapling[fetchers]" # Scrapling needs one more step. The bare package is the parser # engine only, and importing from scrapling.fetchers without this # raises ModuleNotFoundError. scrapling install
solve_cloudflare 覆盖什么,不覆盖什么
值得说清楚,因为这个名字听上去比实际功能宽泛。StealthyFetcher 的 solve_cloudflare 参数会先处理掉 Cloudflare 的 Turnstile 和过渡页挑战,再把响应交给你。这确实很方便,你应该用它。但它能做的也就这些。
| 页面上的挑战 | 由谁处理 |
|---|---|
| Cloudflare Turnstile,以及整页过渡挑战 | Scrapling,通过 solve_cloudflare 参数 |
| reCAPTCHA v2,包括 invisible 和 enterprise 变体 | 你自己,通过 page_action 钩子 |
| 带 action 名称的 reCAPTCHA v3 | 你自己,通过 page_action 钩子 |
| 极验 v3,答案含三个字段的滑块 | 你自己,通过 page_action 钩子 |
| 扭曲文字的图片验证码 | 你自己,通过 page_action 钩子 |
所以工作的形态每次都一样:从页面里取出挑战参数,在别处识别,再把答案放回 DOM,让站点自己的表单带着它走。Scrapling 只给你一个地方来做中间那步,就是 page_action。
第 1 步:不启动浏览器就读出 sitekey
Scrapling 有三个 fetcher,它们不能互换。Fetcher 是纯 HTTP。DynamicFetcher 驱动一个 Playwright 浏览器。StealthyFetcher 是加固过的那个,从 0.3.13 版起它运行在 Patchright 上,而不是以前用的 Camoufox 构建。如果你在照着较早的教程操作,这点值得知道。
拿 sitekey 几乎从不需要浏览器。这个值以 data 属性的形式写在标记里,所以那个便宜的 fetcher 就能读到。
# pip install capskip
from scrapling.fetchers import Fetcher
PAGE_URL = "https://example.com/page-with-recaptcha"
# Plain HTTP. No browser starts, so this costs almost nothing
# and tells you which CAPTCHA you are actually facing.
page = Fetcher.get(PAGE_URL)
sitekey = page.css("[data-sitekey]::attr(data-sitekey)").get()
print(sitekey)如果返回为空,说明小组件是由 JavaScript 写入的,不在服务端返回的 HTML 里。用 DynamicFetcher 抓一次同一个页面,从渲染后的 DOM 里读;或者在浏览器里打开它,把值硬编码进去。sitekey 是公开的,而且很稳定,所以硬编码不是什么需要你道歉的偷懒做法。
第 2 步:在你自己的硬件上识别
一次调用。每种 reCAPTCHA 变体都是同一个方法,只是关键字参数不同:invisible 设为 1、enterprise 设为 1,或者 version 设为 v3 并带上 action 名称。Turnstile 和极验有各自的方法,形态相同,完整参数列表见 CapSkip API 文档.
# CapSkip listens on your own 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,所以它通常比手写的裸接口轮询更快返回。上限是 recaptchaTimeout,默认 300 秒。
第 3 步:在 page_action 中注入 token
page_action 接受一个函数,收到 Playwright 的 page 对象,在导航之后、network_idle 等待之后运行,但在 wait_selector 之前。这个顺序正是有用的地方:你的函数运行时小组件已经渲染完毕,而之后你等待的任何东西都能看到你所做操作的结果。
from playwright.sync_api import Page
def inject_token(page: Page):
# The response textarea is hidden, so set the value directly
# rather than trying to type into it.
page.evaluate(
"(t) => document.getElementById('g-recaptcha-response').value = t",
token,
)
page.click("button[type=submit]")这个函数不需要返回任何东西。较早版本的 Scrapling 要求把 page 对象返回,现在的文档不要求,所以两种情况下什么都不返回都是安全的写法。
隔离执行上下文,这才是会咬人的部分
StealthyFetcher 默认在 Patchright 的隔离执行上下文中执行你的 JavaScript。DOM 是共享的,所以读写元素完全符合预期。页面自身的 JavaScript 全局变量不共享,因此站点挂在 window 上的任何东西都直接不存在。
这个区别决定了你的注入能否生效。给响应 textarea 赋值只碰 DOM,在隔离世界里没问题。调用站点的 reCAPTCHA 回调则不行,因为那个回调挂在页面自身脚本创建的全局对象上。用回调而不是普通表单提交的站点,看上去就像是无视了一个完全有效的 token。
def inject_and_fire_callback(page: Page):
# isolated_context=False drops into the page's own world,
# where the site's grecaptcha object actually exists.
page.evaluate(
"""(t) => {
document.getElementById('g-recaptcha-response').value = t;
window.onCaptchaSuccess(t);
}""",
token,
isolated_context=False,
)写这段之前先从小组件上读出回调名。它就是 reCAPTCHA 元素上 data-callback 属性写的值,每个站点都不一样。
完整可运行示例
先用便宜的 HTTP 请求读出 sitekey,识别一次,然后用一次 stealthy 抓取完成注入和提交。注意识别发生在抓取之前,所以 page_action 运行时 token 已经在手上了。
# pip install capskip
from capskip import CapSkip
from playwright.sync_api import Page
from scrapling.fetchers import Fetcher, StealthyFetcher
PAGE_URL = "https://example.com/page-with-recaptcha"
solver = CapSkip(host="127.0.0.1", port=8080)
sitekey = Fetcher.get(PAGE_URL).css("[data-sitekey]::attr(data-sitekey)").get()
token = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)["code"]
def inject_token(page: Page):
page.evaluate(
"(t) => document.getElementById('g-recaptcha-response').value = t",
token,
)
page.click("button[type=submit]")
page = StealthyFetcher.fetch(
PAGE_URL,
headless=True,
network_idle=True,
page_action=inject_token,
wait_selector=".dashboard",
)
print(page.css(".dashboard h1::text").get())这里有两个参数是承重的。network_idle 让 fetcher 一直等到 500 毫秒内没有网络连接为止,这通常足够小组件渲染完成。wait_selector 则用来证明提交成功了:把它指向只存在于表单另一侧的东西,提交失败就会变成超时,而不是一次悄无声息的假成功。
用 session 处理多个页面
每次类级别的 fetch 都会启动一个浏览器然后丢掉。页面超过两三个时,改用 session,在多次请求之间保留浏览器。大多数 fetch 参数都可以在 session 上设置一次,并按请求覆盖,包括 page_action、wait_selector 和 solve_cloudflare。
from scrapling.fetchers import StealthySession
# solve_cloudflare here handles the Cloudflare layer for every
# page in the session. Anything else still goes through
# page_action, per request.
with StealthySession(headless=True, solve_cloudflare=True) as session:
for url in urls:
page = session.fetch(url, page_action=inject_token)
print(page.css("title::text").get())尽可能晚地识别。一个 reCAPTCHA token 大约只有两分钟有效期,所以先识别二十个再去走队列,大部分都会过期。在循环里识别,就放在要用它的那次抓取之前。这个有效期在实践中意味着什么,见 reCAPTCHA token 过期指南.
把识别工具跑在另一台机器上
爬虫是会挪地方的。VPS、容器或定时任务的 worker 都不是装着识别程序的那台机器,而那里的回环地址指向 worker 自己,那上面没有任何东西在监听。
CapSkip 正为此提供了两种连接模式。Local 绑定 127.0.0.1,只响应本机。Server 绑定你的网络地址或公网 IP,这样任何地方的爬虫都能通过 API 访问同一台 Windows 机器。静态公网 IP 能让地址保持稳定。无论哪种模式都是你自己的硬件,也都不计量,所以识别五万次挑战的一趟运行,和识别五十次的花费完全相同。
# The SDK reads these three itself, so the same script runs # whether the solver is on this box or on another one: # CAPSKIP_HOST=192.0.2.10 # CAPSKIP_PORT=8080 # CAPSKIP_API_KEY=your-key solver = CapSkip()
一旦识别程序监听在网络地址上,就打开密钥校验,并给每个 worker 各自的密钥,这样吊销其中一个不会影响其余的。两种模式的完整步骤见 CapSkip 设置指南.
两端都要配代理
如果站点会检查 token 是否来自提交它的那个地址,那么识别和抓取就必须从同一个出口出去。Scrapling 在 fetcher 上接受 proxy 参数,CapSkip 则针对 reCAPTCHA、Turnstile 和极验按任务接受一个代理。图片验证码不接受代理,也不需要代理。
PROXY = "http://user:[email protected]:3128" token = solver.recaptcha( sitekey=sitekey, url=PAGE_URL, proxy={"type": "HTTP", "uri": "user:[email protected]:3128"}, )["code"] page = StealthyFetcher.fetch(PAGE_URL, proxy=PROXY, page_action=inject_token)
两种写法故意不同:识别程序把类型和 URI 作为两个独立字段接收,fetcher 接收一个 URL 字符串。写错其中一个,就会出现识别从你的真实地址出去、抓取从代理出去的情况,这看起来完全像是 token 无效,其实并不是。代理类型怎么选,见 识别过程中轮换代理的指南.
常见错误及其含义
| 你所看到的 | 原因 | 修复 |
|---|---|---|
| 导入 fetchers 时报 ModuleNotFoundError | 裸装的包只有解析引擎 | 安装时带上 fetchers 扩展,然后运行 scrapling install 命令 |
| sitekey 选择器什么都没返回 | 小组件由 JavaScript 注入,因此不在服务端返回的 HTML 里 | 用 DynamicFetcher 读一次,或者直接硬编码 |
| token 落进了字段,但表单始终没提交 | 站点用的是回调,而那个全局对象在隔离世界里不存在 | 在 evaluate 调用上把 isolated_context 设为 False |
| 识别顺利完成后 wait_selector 却超时 | 提交失败了,所以另一侧的元素从未出现 | 在 page_action 里截图,看看页面到底说了什么 |
| 有效的 token 被拒绝 | 识别和抓取从不同的地址出去 | 两边都配同一个代理,各按各自的写法 |
| 识别程序抛出 NetworkException | CapSkip 没有在运行,或者主机和端口不对 | 启动应用,或者把 host 环境变量指向服务器地址 |
| 识别程序抛出 ValidationException | 该验证码类型不接受的参数 | 检查类型。在 v2 上用 action,或在 v3 上用 invisible,都会触发它 |
常见问题
solve_cloudflare 也能处理 reCAPTCHA 吗?
不能。它覆盖的是 Cloudflare 的 Turnstile 小组件和 Cloudflare 的过渡挑战页,这两个都是 Cloudflare 的产品。reCAPTCHA 属于 Google,极验自成一家,图片验证码则是站点自己画的。这三类都要走 page_action,带上你自己识别出来的 token。
StealthyFetcher 还是基于 Camoufox 构建的吗?
从 0.3.13 版起就不是了。它现在跑在 Patchright 上,那是 Chromium 技术栈,而不是 Firefox 的。文档里仍然写着通过继承 session 来切回 Camoufox 的可选做法,但默认值已经变了,所以任何告诉你 StealthyFetcher 是 Camoufox 封装的教程,讲的都是更早的版本。
我可以在 page_action 里识别,而不是在抓取之前吗?
可以,而且对 reCAPTCHA v3 或极验来说你往往必须这么做,因为参数只有在页面运行之后才存在。只要记住整个识别过程中浏览器都在空转。如果 sitekey 就在服务端返回的 HTML 里,先识别、后注入能让浏览器打开的时间少得多。
异步 fetcher 会改变这些吗?
只是写法不同。用异步的 fetch 方法,并把 page_action 写成接收异步 Playwright page 的 async 函数。识别这一侧用 AsyncCapSkip,它在 Python 中是真正的异步实现,而不是别名,因此它共用你的事件循环,而不是占住一个线程。
简短版结论
让 Scrapling 用 solve_cloudflare 处理 Cloudflare,其余的都在 page_action 里自己处理。用纯 HTTP 的 fetcher 读出 sitekey,识别它,然后在钩子里设置响应字段。如果站点触发的是回调而不是提交表单,就关掉隔离上下文再执行脚本,否则 token 会落在页面看不见的世界里。把 wait_selector 指向只有提交成功后才存在的东西,这样失败就会闹出动静。
更完整的 Python 说明见 Python 验证码识别页面,复选框挑战的具体细节见 reCAPTCHA v2 识别页面,Cloudflare 那一侧见 Cloudflare Turnstile 识别器页面。同样这三个调用在 Node.js、PHP 和 C# 中也都有,列在 验证码识别 SDK 页面.
在把它用到真实爬取之前,有一点值得权衡。CapSkip 是一款 无限量验证码识别工具 ,运行在你已经拥有的硬件上,所以大规模爬取中验证码这一项是固定成本,而不是按次计费。
