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

scrapling captcha - How to Solve CAPTCHAs in Scrapling With 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 被拒绝识别和抓取从不同的地址出去两边都配同一个代理,各按各自的写法
识别程序抛出 NetworkExceptionCapSkip 没有在运行,或者主机和端口不对启动应用,或者把 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 是一款 无限量验证码识别工具 ,运行在你已经拥有的硬件上,所以大规模爬取中验证码这一项是固定成本,而不是按次计费。