如何在 Camoufox 中使用 Main World Eval 识别验证码

camoufox captcha - How to Solve CAPTCHA in Camoufox Using Main World Eval

Camoufox 里的验证码环节看上去和别处一模一样:读出 sitekey,发给识别工具,再把 token 写回页面。差别出在第三步。你交给 evaluate 的 JavaScript,会在一个页面看不见的隔离作用域里运行,而隔离作用域改不了页面的 DOM。你的写入不报错,textarea 还是空的,表单验证失败。解决办法是一个启动选项加一个两字符的前缀。

你需要什么

  • Python 3.10 或更高版本。Camoufox 0.5 自己锁定了 Playwright 的版本,交给 pip 去解析即可。
  • 用 fetch 命令下载一次 Camoufox 浏览器。它是一个 Firefox 构建版本,不是 Chromium。
  • 受保护表单的页面 URL。sitekey 在运行时读取。
  • 如果脚本和识别工具在同一台机器上,就让 CapSkip 以 Local 模式运行;如果不在同一台机器上,就用 Server 模式。两种模式都写在 连接设置.
# The geoip extra is optional and worth having if you use proxies.
pip install -U capskip "camoufox[geoip]"

# Downloads the browser itself. Run once per machine.
camoufox fetch

为什么平常那套 token 注入在这里不起作用

Camoufox 是一个打过补丁的 Firefox,外面套了一层很薄的 Python 封装,底下就是 Playwright。这个封装类是一个 Playwright 上下文管理器,所以你从它那里拿回来的就是一个普通的 Playwright 浏览器对象,你已经会用的每一个定位符、点击和跳转都照旧能用。这是好消息,而且这一部分就是这个库的大半。

例外是脚本执行。Camoufox 把所有 JavaScript 都放进一个页面看不见的隔离作用域里运行,这正是它存在的意义:站点看不到自动化工具在戳它。读取不受影响,因为 Playwright 自己的定位符方法走的是浏览器协议,而不是那个作用域。写入则会受影响,下面这句话记住就行:隔离作用域改不了 DOM。在那里给 token 赋值,会被悄无声息地丢掉。

Camoufox 给出的答案是一个通往主世界(main world)的逃生口。启动时传入 main_world_eval,然后给任何需要碰真实页面的脚本加上 mw: 前缀,它就会在页面自己的作用域里运行。这带来两个附加条件。站点能检测到在那里运行的代码,所以只用它来做注入,别的都不要放进去。另外,你没法把元素引用从主世界里返回出来,只能返回能以 JSON 形式存活的值。

第 1 步:启动时打开主世界

这个选项默认是关闭的,而且必须在启动时设置。之后没有办法只为某一次调用把它打开。

# pip install camoufox[geoip]
from camoufox.sync_api import Camoufox

with Camoufox(main_world_eval=True, headless=True) as browser:
    page = browser.new_page()
    page.goto("https://example.com/page-with-recaptcha")

    # browser is a normal Playwright Browser from here on.
    print(page.title())

在往下走之前,还有两个相邻的选项值得知道。humanize 让光标沿着一条像人的轨迹移动,横穿窗口最多要花大约 1.5 秒;如果你打算自己去点小组件而不是注入 token,这一点就有影响。而 disable_coop 会去掉 Cross-Origin-Opener-Policy,这样跨域 iframe 里的元素才点得动,Cloudflare Turnstile 的复选框就是其中之一。

第 2 步:从页面读出 sitekey

sitekey 在宿主文档上,不在小组件的 iframe 里。Google 的标记把它作为 data-sitekey 属性放在一个容器元素上,用一个普通的 Playwright 定位符就能读到。这里不需要主世界前缀,因为这条路径根本不经过隔离作用域。

# Locators wait by default, so this doubles as a wait
# condition for a widget that renders late.
holder = page.locator("div.g-recaptcha")
holder.wait_for(state="attached", timeout=15000)

sitekey = holder.get_attribute("data-sitekey")
print(sitekey)   # 6Lxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

有些站点根本不把 sitekey 暴露在宿主页面上,只在小组件的 iframe URL 里传。这种情况就从查询字符串里把它读出来。

# Fallback: the k= parameter on the anchor iframe.
from urllib.parse import urlparse, parse_qs

src = page.locator("iframe[src*='recaptcha/api2/anchor']").get_attribute("src")
sitekey = parse_qs(urlparse(src).query)["k"][0]

在花掉一次识别之前先检查这个值。空的 sitekey 会一路传到识别工具,返回的结果是 ERROR_GOOGLEKEY,离真正出问题的那个定位符已经很远了。

第 3 步:在你自己的机器上完成识别

Python SDK 通过 8080 端口和 CapSkip 通信,直接把 token 作为普通字符串返回。一个方法就覆盖了 reCAPTCHA v2、Invisible、Enterprise 和 v3,各种变体是以选项的形式传进去的,不用分开调用。

# pip install capskip
from capskip import CapSkip

solver = CapSkip(host="127.0.0.1", port=8080)

# Invisible v2 takes invisible=1, Enterprise takes enterprise=1,
# and v3 takes version="v3" with an action.
result = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)

token = result["code"]   # the g-recaptcha-response value

Turnstile 和极验(GeeTest)各有自己的方法,调用形态也一样。Turnstile 还会额外返回这次识别所用的 user agent;挑战页面会要求你把这个 user agent 一起发回去,否则就拒绝这个 token。完整参数列表见 CapSkip API 文档.

Camoufox 也提供了异步类,而 Python SDK 里的 AsyncCapSkip 是真正的 asyncio 实现,不是同步版的一个别名。当你同时驱动多个上下文时,把两者配在一起用,这样一个页面上的识别就不会把其他页面卡住。

第 4 步:在主世界里注入 token

下面就是这个浏览器特有的部分。响应用的 textarea 被 display:none 隐藏了,所以任何自动化工具都没法往里输入,你只能用 JavaScript 给它赋值。给脚本加上 mw: 前缀,让它在页面自己的作用域里运行;字符串用 json.dumps 拼,而不是用 f-string,因为一个 JSON 字符串字面量同时也是合法的 JavaScript 字符串字面量,引号和转义都算在内。

# The mw: prefix is what makes this write land.
import json

page.evaluate(
    "mw:document.getElementById('g-recaptcha-response').value = "
    + json.dumps(token)
)

page.click("button[type=submit]")

验证它有没有写进去,有一个小麻烦。你没法把元素从主世界里交出来,所以要返回一个值而不是节点。返回长度就够了,它恰好能告诉你想知道的事情。

# Return a number, never the element itself.
length = page.evaluate(
    "mw:document.getElementById('g-recaptcha-response').value.length"
)
print(length)   # 0 means the injection did not land

如果站点不是在提交表单时读 textarea,而是定义了一个成功回调,那就在设置完值之后调用它。这正是主世界不只是方便、而是必需的场景,因为页面定义的函数在隔离作用域里根本不存在。函数名要从页面自身的标记里读出来,不要靠猜。两种提交方式底下都仍然是普通的 reCAPTCHA v2,而这两种方式的详细讲解,见 reCAPTCHA v2 识别页面.

完整可运行示例

上面所有内容,合成一个脚本。识别工具只创建一次,代码块结束时浏览器自己关闭,而且在花掉一次识别之前会先检查 sitekey。

# pip install capskip camoufox[geoip]
import json
from camoufox.sync_api import Camoufox
from capskip import CapSkip

PAGE_URL = "https://example.com/page-with-recaptcha"

solver = CapSkip(host="127.0.0.1", port=8080)

with Camoufox(main_world_eval=True, headless=True) as browser:
    page = browser.new_page()
    page.goto(PAGE_URL)

    holder = page.locator("div.g-recaptcha")
    holder.wait_for(state="attached", timeout=15000)
    sitekey = holder.get_attribute("data-sitekey")
    if not sitekey:
        raise RuntimeError("Widget found but data-sitekey was empty.")

    result = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)
    page.evaluate(
        "mw:document.getElementById('g-recaptcha-response').value = "
        + json.dumps(result["code"])
    )

    page.click("button[type=submit]")
    page.wait_for_load_state("networkidle")
    print(page.url)   # where you land after submitting

把识别工具跑在另一台机器上

Camoufox 迟早会跑到一台比你写脚本时那台更大的机器上,到那时有一个平台细节要弄对。把 headless 传成字符串 virtual 会启动一个 Xvfb 显示,这是 Linux 独有的功能:在 Windows 和 macOS 上,这个值只会抛出一个不支持的错误。普通的 headless 到哪里都能用,所以除非你是有意跑在 Linux 上,否则就用布尔值。

识别工具不必跟着浏览器一起搬。CapSkip 有两种连接模式。Local 模式绑定到 127.0.0.1,只有该设备本身能访问;当你就在运行应用的那台机器上写脚本时,用它正合适。Server 模式绑定到你的内网或公网 IP,这样一台采集用的虚拟机、第二台工作站,或者一台跑着 Camoufox 的 Linux 机器,都能通过 API 调用同一台 Windows 机器。固定公网 IP 能让这个地址保持稳定。代码里除了主机地址之外什么都不用改,价格也不变,因为硬件还是你自己的。

# Same SDK, same call. Only the host moves.
solver = CapSkip(host="10.0.0.12", port=8080, apiKey="YOUR_API_KEY")

识别工具一旦监听在网络地址上,就把密钥校验打开,并且给每台机器发各自的密钥,这样吊销其中一个不会动到其他机器。两种模式都完整走了一遍,见 CapSkip 设置指南.

常见错误及其含义

你所看到的原因修复
token 设上了,表单还是失败写入是在隔离作用域里执行的,被丢弃了启动时加上 main_world_eval,并给脚本加 mw: 前缀
evaluate 报错说返回值不受支持有一个节点引用被从主世界里返回出来了改为返回长度或字符串,不要返回元素
回调函数是 undefined页面的全局变量在隔离作用域里不存在用同一个前缀,从主世界里调用它
Turnstile 的复选框点不动它在一个跨域 iframe 里启动时加上 disable_coop,或者直接注入 token 而不去点它
ERROR_GOOGLEKEY一个空的 sitekey 传到了识别工具在调用 recaptcha 之前先断言这个值
NetworkExceptionCapSkip 没在运行,或者 host 填错了启动应用,或者把 host 指向服务器地址
TimeoutException识别耗时超过了 recaptchaTimeout把它调到默认的 300 秒以上
虚拟显示不受支持在非 Linux 平台上把 headless 设成了 virtual改用 headless=True

常见问题

用 main world eval 会让我更容易被检测到吗?

在主世界里运行的任何东西对页面都是可见的,所以原则上确实会。但实际暴露出去的只是一次赋值,持续时间是微秒级的,而且它和真人通过挑战时小组件自己的脚本所做的事情看起来一模一样。把其他所有脚本都留在隔离作用域里,注入用一次调用而不是好几次完成,你交出去的东西并不多。

我现有的 Playwright 代码还能接着用吗?

几乎全都能用。启动器交还给你的是一个真正的 Playwright 浏览器,所以定位符、上下文、路由和等待的行为都和以前一样。要重新检查的只有两处:任何通过 evaluate 往 DOM 里写东西的调用,它需要加前缀;以及任何假定了 Chromium 的地方,因为这里是 Firefox。关于这个引擎更完整的图景,见 Playwright 验证码识别页面.

我该去点小组件,而不是注入 token 吗?

只有 Turnstile 才考虑,而且也只是有时候。managed 模式下的 Turnstile 复选框,如果浏览器看起来足够可信,是能自己通过的,而这正是 Camoufox 的用武之地;要点它,得先把 Cross-Origin-Opener-Policy 去掉。点 reCAPTCHA 的复选框只会打开一个图片挑战,那边没什么好处可拿。小组件这一侧的细节见 Cloudflare Turnstile 识别器页面.

我的爬虫跑在一台 Linux VPS 上,CapSkip 该放哪?

放在一台你自己掌控的 Windows 机器上,并打开 Server 模式。VPS 之后就像调用任何内部服务那样通过 API 调它,所以 Camoufox 和识别工具既不需要共用同一个操作系统,也不需要待在同一个网段里。把 host 参数指向那个地址,打开密钥校验,再给这台 VPS 发一个自己的密钥。

简短版结论

启动 Camoufox 时打开 main_world_eval,用一个普通的定位符读出 sitekey,在 127.0.0.1:8080 上通过 CapSkip 完成识别,然后用一个加了 mw: 前缀的 evaluate 调用把 token 注入进去并提交。前缀就是全部诀窍,因为没有它,你的写入会落进一个页面永远看不到的作用域。想了解 Python 这边其余的图景,包括 Selenium 和 Playwright,见 Python 验证码识别页面.

在把抓取规模扩大之前,还有一件事值得知道。CapSkip 是一个 本地验证码识别工具 ,跑在你已经拥有的硬件上,所以重试一千个页面的抓取,和只重试十个页面的抓取,花的钱完全一样。