☰
Python爬虫实战:Playwright混合模式应对reCAPTCHA v3与429限流
2026/9/29 19:27:41 网站建设 项目流程

1. 从一次数据采集任务说起:为什么 reCAPTCHA v3 让人头疼

去年接了一个跨境电商选品分析的项目,需要定期抓取几个海外电商平台的商品列表和价格波动数据。刚开始用requests加代理池跑得挺顺,结果第三天开始,目标站点陆续返回 403,日志里清一色是exceeded retry limit, last status: 429 too many requests。排查了半天才发现,对方上了 reCAPTCHA v3。

和 v2 那种"点选红绿灯、勾选我不是机器人"的显式验证不同,v3 是无感验证——它不弹窗、不打断用户操作,而是在后台给每次交互打一个 0.0 到 1.0 的分数。分数低于阈值,站点就直接拒绝你的请求,或者返回一个空壳页面。你甚至不知道自己什么时候被判定成了机器人。

这就是 v3 最恶心的地方:它不给你明确的失败信号。v2 失败了你能看到验证码图片,v3 失败了可能只是数据变少、接口返回 200 但内容是空的。很多新手会以为是选择器写错了,反复调试 CSS 选择器,实际上问题出在浏览器指纹和交互行为上。

这篇内容就是把我这段时间踩过的坑、试过的方案、最终跑通的策略完整梳理一遍。核心围绕Python + Playwright + requests这套组合,讲清楚 reCAPTCHA v3 的评分逻辑、如何用真实浏览器环境降低风险分、以及在高频请求下怎么控制节奏避免触发 429。适合有一定 Python 基础、做过爬虫或自动化测试、正在被 v3 卡住的同学。如果你还在python安装教程阶段,建议先把requests和playwright的基础用法过一遍再来看。

需要先说明一点:reCAPTCHA v3 没有"破解"一说。它的评分模型跑在服务端,你无法直接篡改分数。我们能做的是让自动化行为尽可能接近真实用户,把分数维持在阈值以上。任何声称能"百分百绕过"的方案,要么是过时的,要么是在骗你。

2. reCAPTCHA v3 的评分机制与自动化策略选型

2.1 v3 到底在看什么:分数背后的信号维度

要制定策略,先得搞清楚 v3 在评估什么。根据公开资料和大量实测,v3 的评分主要依赖以下几类信号:

  • 浏览器环境完整性:navigator.webdriver是否为 true、window.chrome是否存在、插件列表、语言、时区、屏幕分辨率、Canvas 指纹、WebGL 指纹等。
  • 行为轨迹:鼠标移动是否自然、是否有加速度变化、点击位置是否落在元素中心、滚动是否平滑、键盘输入间隔是否规律。
  • 请求上下文:IP 信誉、请求头顺序、Cookie 历史、Referer 链路、请求频率。
  • 会话连续性:同一会话内的操作是否符合人类逻辑,比如先访问首页再进详情页,而不是直接怼接口。

v3 会给每个页面加载和关键操作生成一个 token,这个 token 随表单或 XHR 请求发给 Google 验证,返回分数。分数低于站点设定的阈值(常见 0.5 或 0.7),请求就被判定为可疑。

注意:分数是浮动的,同一个 IP 同一套环境,不同时间跑出来的分数可能不一样。不要指望一次调通就永远稳定。

2.2 三种主流方案对比:requests、Playwright、混合模式

市面上针对 v3 的自动化方案大致分三类,我逐一试过,直接上对比表:

方案原理优点缺点适用场景
纯 requests直接构造 HTTP 请求,手动带 token速度快、资源占用低无法生成有效 token,指纹极易被识别仅适合无 v3 保护的接口
纯 Playwright真实浏览器执行,自动生成 token环境真实、token 有效速度慢、内存高、并发受限中小规模、需要真实交互
混合模式Playwright 取 token + requests 发请求兼顾速度与真实性需要维护会话同步大规模采集首选

纯requests方案在 v3 面前基本没戏,因为 token 是 Google 的 JS 在浏览器里动态生成的,你没法在 Python 里凭空造一个。我试过用execjs跑 Google 的脚本,结果生成的 token 验证直接失败——因为脚本依赖大量浏览器 API。

纯 Playwright 能跑通,但一个浏览器实例占几百 MB 内存,开十个并发机器就吃不消了。而且 Playwright 默认的navigator.webdriver是 true,不改的话分数低得可怜。

最终我采用的是混合模式:用 Playwright 维护一个真实浏览器会话,负责加载页面、执行 JS、拿到 v3 token 和 Cookie,然后把 token 和 Cookie 交给requests去发实际的数据请求。这样既保证了 token 有效,又把高频请求的压力从浏览器转移到了轻量的 HTTP 客户端上。

2.3 为什么选 Playwright 而不是 Selenium

playwright自动化框架这两年热度很高,我对比过 Selenium 和 Playwright 在反检测场景下的表现:

  • 启动速度:Playwright 明显更快,Chromium 启动到可交互状态通常 1-2 秒,Selenium 要 3-5 秒。
  • API 设计:Playwright 的page.route可以拦截和修改请求,page.add_init_script可以在页面加载前注入脚本,这两点对反检测至关重要。Selenium 做同样的事要绕很多弯。
  • 自动等待:Playwright 的元素操作自带等待,减少了因时序问题导致的失败。
  • 多浏览器支持:一套 API 同时支持 Chromium、Firefox、WebKit,方便做环境多样性。

Selenium 的优势在于生态老、资料多,但如果你要做反检测,Playwright 的add_init_script和route是刚需。我现在的项目基本全面转向 Playwright。

3. 环境搭建与浏览器指纹伪装实操

3.1 安装与基础配置:避开新手常踩的坑

先把环境搭起来。假设你已经装好 Python(建议 3.10 以上),执行:

pip install playwright requests playwright install chromium

playwright install这一步会下载浏览器二进制,国内网络可能很慢,可以设置镜像。装完之后验证一下:

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto("https://example.com") print(page.title()) browser.close()

能打印出标题就说明环境 OK。这里有个新手常犯的错误:一上来就用 headless=True。无头模式虽然快,但指纹特征和真实浏览器差异很大,v3 很容易识别。调试阶段一定用headless=False,等策略稳定了再考虑无头。

提示:如果你在服务器上跑,没有图形界面,可以用xvfb-run包一层虚拟显示,但指纹仍然不如真实桌面环境。有条件的话用带桌面的机器。

3.2 注入脚本抹掉自动化痕迹

Playwright 默认会暴露一些自动化特征,最典型的就是navigator.webdriver === true。我们需要在页面加载前注入脚本覆盖掉这些特征。核心代码如下:

STEALTH_SCRIPT = """ Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] }); Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en'] }); window.chrome = { runtime: {} }; const originalQuery = window.navigator.permissions.query; window.navigator.permissions.query = (parameters) => ( parameters.name === 'notifications' ? Promise.resolve({ state: Notification.permission }) : originalQuery(parameters) ); """ context = browser.new_context( viewport={"width": 1920, "height": 1080}, user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", locale="zh-CN", timezone_id="Asia/Shanghai", ) context.add_init_script(STEALTH_SCRIPT)

这段脚本做了几件事:把webdriver设为 undefined、伪造插件列表、伪造语言、补上window.chrome对象、修正权限查询的返回值。这些都是 v3 会检查的点。

实测下来,光加这一段,分数能从 0.1 提升到 0.5 左右。但要稳定过 0.7,还需要更多处理。

3.3 Canvas 与 WebGL 指纹的处理

Canvas 指纹是 v3 的重要信号。原理是让浏览器绘制一段文字或图形,不同硬件和驱动渲染出的像素有细微差异,形成唯一标识。自动化环境里 Canvas 渲染结果往往和真实机器不同,容易被标记。

处理方式有两种:一是随机化,每次会话生成略微不同的 Canvas 输出;二是伪装成常见配置,让指纹看起来像一台普通机器。我倾向于后者,因为随机化本身也是一种异常特征。

CANVAS_SCRIPT = """ const toBlob = HTMLCanvasElement.prototype.toBlob; const toDataURL = HTMLCanvasElement.prototype.toDataURL; const getImageData = CanvasRenderingContext2D.prototype.getImageData; const shift = { x: 0.1, y: 0.05 }; HTMLCanvasElement.prototype.toDataURL = function(...args) { const ctx = this.getContext('2d'); if (ctx) { const imageData = ctx.getImageData(0, 0, this.width, this.height); for (let i = 0; i < imageData.data.length; i += 4) { imageData.data[i] += Math.floor(Math.random() * 2); } ctx.putImageData(imageData, 0, 0); } return toDataURL.apply(this, args); }; """

WebGL 类似,可以通过覆盖WebGLRenderingContext.prototype.getParameter来返回常见的显卡型号字符串,比如"Intel Inc."和"Intel Iris OpenGL Engine"。

注意:这些脚本要放在add_init_script里,确保在页面任何 JS 执行前生效。如果放在page.evaluate里,页面自己的脚本可能已经跑完了,来不及。

4. 混合模式实战:Playwright 取 token + requests 发请求

4.1 完整流程拆解

混合模式的核心思路是:浏览器负责"过验证",requests 负责"干重活"。具体流程:

  1. Playwright 启动浏览器,注入伪装脚本,访问目标站点首页。
  2. 模拟真实用户行为:随机等待、鼠标移动、滚动页面。
  3. 导航到目标页面,等待 v3 脚本执行完毕。
  4. 从页面中提取 v3 token(通常在隐藏 input 或 JS 变量里)和 Cookie。
  5. 把 token 和 Cookie 交给 requests 会话。
  6. requests 高频请求数据接口,token 过期后回到步骤 3 重新获取。

这个流程的关键在于会话复用。一个浏览器会话可以支撑几十甚至上百次 requests 请求,只要 token 没过期、Cookie 没失效。

4.2 提取 token 的几种方式

v3 token 的存放位置因站点而异,常见的有:

  • 隐藏的<input name="g-recaptcha-response">,值就是 token。
  • window.___grecaptcha_cfg全局对象里。
  • 通过grecaptcha.execute()动态获取。

最通用的是第一种,直接读 input 的值:

def get_recaptcha_token(page): page.wait_for_function( "() => document.querySelector('input[name=\"g-recaptcha-response\"]')?.value.length > 0", timeout=15000 ) token = page.eval_on_selector( 'input[name="g-recaptcha-response"]', 'el => el.value' ) return token

如果站点用的是动态 execute,可以这样拿:

token = page.evaluate(""" () => new Promise((resolve) => { grecaptcha.ready(() => { grecaptcha.execute('SITE_KEY', {action: 'submit'}).then(resolve); }); }) """)

SITE_KEY需要从页面源码里找,通常在引入recaptcha/api.js?render=xxx的 URL 里。

4.3 把 token 和 Cookie 交给 requests

拿到 token 和 Cookie 后,构造 requests 会话:

import requests def build_session(cookies, user_agent): session = requests.Session() session.headers.update({ "User-Agent": user_agent, "Accept": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Referer": "https://target-site.com/", }) for cookie in cookies: session.cookies.set(cookie["name"], cookie["value"], domain=cookie["domain"]) return session # 从 Playwright context 拿 cookies cookies = context.cookies() session = build_session(cookies, user_agent) # 发请求时带上 token payload = { "g-recaptcha-response": token, "other_param": "value" } resp = session.post("https://target-site.com/api/data", data=payload)

这里有个细节:请求头的顺序也要注意。真实浏览器的请求头顺序是固定的,requests 默认的顺序可能和浏览器不一致。虽然大多数站点不检查这个,但严格的 v3 实现会看。可以用OrderedDict或者自定义PreparedRequest来控制顺序。

4.4 控制请求频率,避免 429

too many requests you have exceeded a secondary rate limit这个报错,做采集的都见过。v3 本身不直接返回 429,但站点会在 v3 分数低的时候限流,表现就是 429 或 403。

控制频率的策略:

  • 令牌桶限流:设定一个速率上限,比如每秒 2 个请求,用令牌桶平滑控制。
  • 随机间隔:不要用固定time.sleep(1),改成random.uniform(0.5, 1.5),模拟人类的不规律操作。
  • 并发控制:用asyncio.Semaphore限制并发数,别一次性开几百个协程。
  • 退避重试:遇到 429 时指数退避,第一次等 2 秒,第二次 4 秒,第三次 8 秒,最多重试 3 次。
import time import random def safe_request(session, url, payload, max_retries=3): for attempt in range(max_retries): resp = session.post(url, data=payload) if resp.status_code == 429: wait = (2 ** attempt) + random.uniform(0, 1) print(f"429 触发,等待 {wait:.2f} 秒后重试") time.sleep(wait) continue return resp raise Exception("重试次数耗尽,仍然 429")

实测下来,把速率控制在每秒 1-2 个请求,配合随机间隔,基本不会触发限流。想再快就得加机器、加 IP 池,单机硬扛是不行的。

5. 常见问题与排查技巧实录

5.1 分数上不去怎么办:逐项排查清单

分数低是最常见的问题。我整理了一个排查清单,按优先级从高到低:

排查项检查方法修复方式
webdriver 标志page.evaluate("navigator.webdriver")注入脚本覆盖为 undefined
无头模式看 launch 参数改用 headless=False 或 xvfb
IP 信誉查 IP 是否被标记换住宅 IP 或降低频率
Cookie 缺失看 context.cookies()先访问首页建立会话
行为太机械看操作间隔加随机等待和鼠标移动
时区语言不匹配看 context 配置与 IP 地理位置一致

我遇到过一次分数死活上不去,最后发现是时区和 IP 不匹配——IP 在美国,时区设的 Asia/Shanghai,v3 直接判定异常。改成 America/New_York 后分数立刻正常。这种细节很容易忽略。

5.2 token 过期与刷新策略

v3 token 的有效期是2 分钟,而且是一次性的——同一个 token 用第二次就会失败。所以混合模式里必须处理 token 刷新。

我的做法是维护一个 token 池:后台起一个 Playwright 协程,每隔 90 秒生成一批新 token 放进队列,requests 请求时从队列取。队列空了就阻塞等待。这样既保证了 token 新鲜,又不会因为频繁启动浏览器拖慢速度。

import asyncio from collections import deque token_pool = deque(maxlen=50) async def token_producer(context, site_key, action): while True: page = await context.new_page() await page.goto("https://target-site.com/") token = await page.evaluate(f""" () => new Promise((resolve) => {{ grecaptcha.ready(() => {{ grecaptcha.execute('{site_key}', {{action: '{action}'}}).then(resolve); }}); }}) """) token_pool.append(token) await page.close() await asyncio.sleep(90)

注意:token 池不要开太大,v3 会检测同一会话内生成 token 的频率。生成太快反而异常。

5.3 Playwright 同步与异步混用的坑

playwright._impl._errors.error: it looks like you are using playwright sync这个报错,是因为在异步环境里用了同步 API。Playwright 的 sync 和 async 两套 API 不能混用。

如果你用asyncio做并发,就必须用async_playwright:

from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: browser = await p.chromium.launch(headless=False) page = await browser.new_page() await page.goto("https://example.com") await browser.close() asyncio.run(main())

反过来,如果你用同步 API,就别在async def里调用。我见过有人把 sync 的page.goto放在协程里,结果整个事件循环卡死。

5.4 动态 iframe 里的 v3 处理

有些站点把 reCAPTCHA 放在 iframe 里,scrapy playwright 动态 iframe这类搜索词热度很高,说明踩坑的人不少。处理方式是先定位 iframe,再在 iframe 上下文里操作:

frame = page.frame_locator('iframe[src*="recaptcha"]') token_input = frame.locator('input[name="g-recaptcha-response"]') token = token_input.input_value()

如果 iframe 是动态加载的,要先用page.wait_for_selector等 iframe 出现。注意 iframe 的src可能包含recaptcha/api2/anchor或recaptcha/api2/bframe,前者是徽章,后者是挑战框。v3 通常没有可见的挑战框,token 在主页面的隐藏 input 里。

6. 规模化部署与长期维护的几点体会

单机跑通只是第一步,真正上规模还有一堆事要处理。我现在的项目每天要处理几十万次请求,分享几个关键点。

浏览器实例复用。不要每次请求都启动新浏览器,开销太大。维护一个浏览器实例池,每个实例开多个 context,context 之间 Cookie 隔离。一个 Chromium 实例开 5-10 个 context 是合理的,再多内存吃不消。

IP 池与指纹一致性。IP 换了,浏览器指纹也要跟着换。同一个指纹配不同 IP,v3 很容易关联到同一实体。我的做法是把 IP、User-Agent、时区、语言打包成一个"身份包",一个身份包对应一个 context,用完就销毁。

监控与告警。v3 的阈值可能随时调整,今天能过的策略明天可能就失效。必须做监控:记录每次请求的响应状态、数据量、token 获取成功率。一旦成功率跌破 80%,立刻告警。我吃过一次亏,站点半夜调了阈值,第二天早上发现数据全是空的,白白浪费了一晚上的采集窗口。

合规边界。这一点必须说清楚:自动化采集要遵守目标站点的robots.txt和服务条款,控制频率不要给对方服务器造成压力。技术能力是一回事,用在哪里是另一回事。我现在的项目只采集公开的商品价格数据,不碰用户隐私,频率也控制在对方能接受的范围内。

最后分享一个我踩过的坑:有段时间 token 获取成功率突然从 95% 掉到 30%,排查了两天才发现是 Google 更新了 v3 的检测脚本,新增了对navigator.connection的检查。补上这个属性的伪装后恢复正常。所以保持对上游变化的敏感度很重要,别指望一套脚本用一年。定期回归测试,关注社区动态,比什么都强。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询