ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Playwright绕过Webdriver检测:从原理到实战的反爬配置指南

Playwright绕过Webdriver检测:从原理到实战的反爬配置指南 写爬虫或者自动化脚本的同学十有八九都遇到过这种场景Selenium 跑得正欢页面突然弹个验证码或者干脆给你返回一堆乱码数据。F12 打开控制台一看navigator.webdriver这个属性明晃晃地显示为true。网站的反爬系统已经认出你是个机器人了。我早几年做数据采集的时候被这个检测卡了很久后来换了 Playwright情况才明显好转。这个项目标题说“使用 Playwright 绕过 Webdriver 检测”听起来有点玄乎但本质上就是在讲一件事——怎么让一个自动化浏览器看起来像个真人在用。这篇博文我打算从检测原理、工具差异、实战配置到常见坑点完整梳理一遍适合刚接触 Playwright 的爬虫新手也适合被反爬搞得头疼、想换个思路的测试开发同学。先给结论Playwright 确实比 Selenium 更难被常规手段识别因为两者的底层通信机制完全不同。但它不是免死金牌网站风控升级到行为级检测之后光靠某个属性已经不够了。下面我按自己的实操经验把整套方案拆开讲清楚。1. Webdriver检测到底在检测什么1.1 最常见的基础检测点很多早期的反爬脚本靠的就是几个简单的 JavaScript 特征。你打开任何一个页面浏览器里都会有一堆内置的全局对象、属性自动化工具驱动浏览器时会在这些地方留下“指纹”。最常见的几个检测点大概是这些navigator.webdriver在 Selenium 驱动的浏览器中这个属性通常返回true而正常浏览器返回undefined。window.chrome正常 Chrome 浏览器中这个对象存在但部分自动化场景下缺失或异常。navigator.plugins和navigator.languages自动化浏览器可能没有插件列表语言设置也异常。CDP 相关特征比如Runtime.enable之后某些全局对象会被修改或者 devtools 的调试端口暴露。浏览器环境一致性比如User-Agent和实际的浏览器版本、屏幕尺寸、时区信息不匹配。就拿最典型的navigator.webdriver来说很多教程会教你在 Selenium 里执行一段 JS 脚本把它改成undefined。但问题是网站的检测脚本不会只看这一个地方。它可能还会做这样的操作// 反爬脚本里常见的检测方式 const isWebdriver navigator.webdriver true || !window.chrome || navigator.plugins.length 0 || navigator.languages[0] en-US navigator.languages.length 1;它会把你设置过的属性、没设置过的属性、互相矛盾的信息组合起来判断。你只改一个属性其他检测点照样会暴露。这也是为什么“手动改navigator.webdriver”这种老办法经常失效。1.2 高级检测会怎么识别自动化基础特征检测只是入门级稍微正规一点的风控系统早就升级到行为检测和环境指纹比对了。它们看的是以下几个层面一是环境指纹。浏览器的 Canvas 渲染结果、WebGL 的 GPU 信息、字体列表、屏幕分辨率、时区、语言、硬件并发数这些合并起来可以算出一个非常稳定的fingerprint。同一个浏览器在自动化状态下手动改了几个 JS 属性指纹和正常浏览器会有细微差别后台一算就能发现异常。二是行为轨迹。真人打开网页鼠标移动是有加速度和弧度的不是一条直线从 A 点瞬间到 B 点。滚动页面也是一段一段来的有停顿、有回看。自动化脚本如果不加控制鼠标轨迹是直线的滚动是瞬时的这些都能被概率模型识别出来。三是请求规律。就算你的浏览器伪装得和真机一模一样如果你的请求频率是“每 5 秒一次连续 100 次不停”或者同一个 IP 同时有大量自动化浏览器开着风控系统照样会把你挑出来因为真人的操作不会这么规律。我之前测试过一个小网站表面上它只检查navigator.webdriver但如果你用--headless模式开启的浏览器去访问服务端还能拿到 WebRTC 的本地 IP 暴露信息对比一下 HTTP 请求的 IP发现不一致就直接拒绝。这就是环境指纹比对在起作用单纯改写 JS 属性已经覆盖不到这些层面了。2. Playwright和Selenium的底层差异决定了反检测的下限2.1 通信机制完全不一样Selenium 走的是 WebDriver 协议它通过 HTTP 接口把指令发给浏览器驱动比如 chromedriver驱动再转成浏览器能懂的命令。这个过程里浏览器会挂上一个“正在被自动化控制”的标记JS 环境里很容易被查询到比如navigator.webdriver就会变成true。Playwright 走的是 Chrome DevTools ProtocolCDP它直接和浏览器的调试接口通信。简单理解就像你在浏览器里按 F12 打开开发者工具手动操作里面的控制台一样。浏览器本身分不清楚这到底是用户打开的开发者工具还是自动化脚本在链接。所以在默认情况下Playwright 创建的浏览器navigator.webdriver不是true而是undefined。这也就是为什么很多人说 Playwright“天生”更隐蔽。但注意这里说的是“隐蔽”不是“不存在”。Playwright 还是会通过 CDP 创建 Target会注入一些内部的启动脚本来实现自动化 API。有经验的检测脚本可以通过查这些内部标记来定位自动化浏览器。比如判断window.cdc_开头的属性或者看某个内部函数是否存在。只是这些检测手段还不那么普及至少比改navigator.webdriver高端多了。2.2 Playwright的上下文隔离提供了额外的伪装能力Playwright 另外一个优势是它的 BrowserContext 隔离机制。你可以为不同的爬虫任务创建独立的上下文每个上下文可以单独设置 UA、语言、时区、地理位置、屏幕尺寸甚至权限策略。这个设计本来是为了测试多用户场景但用在反检测上非常顺手。你想想一个爬虫脚本如果每次请求的 UA 都不一样语言还时中时英系统时区换来换去任何一个正常的后端反爬系统都会把这些信息交叉比对很快判定为自动化。而 Playwright 可以在同一个浏览器实例里开多个上下文每个上下文指定一套完全一致的“真人配置”模拟一个真实的用户环境。配合代理池使用的时候基本上每个线程拿到的都是一个新的身份。另外Playwright 支持在页面加载任何资源之前先注入一段 JS 脚本这个能力叫做add_init_script。我用它来做全局的“清理”动作——在页面脚本还没运行的时候先把那些容易被检测的特征抹掉。比如设置navigator.webdriver为undefined伪造chrome对象修正plugins列表等等。因为这个注入发生在所有页面脚本之前所以检测脚本读到的属性就已经是“正常”的了。2.3 Playwright不是万能的这一点必须心里有数前面讲了很多 Playwright 的优势但我得泼个冷水它不是银弹。如果你要面对的是瑞数、盾之类的商业级风控产品光靠add_init_script改几个属性根本不够。这类风控会动态执行混淆后的 JS采集大量浏览器指纹并且通过机器学习模型判断操作是否像真人。我实测过几个带有商业风控的站点Playwright 默认启动之后即使所有基础检测点都清理过了依然会在某个步骤被拦下来。原因有很多可能是浏览器启动参数暴露了自动化痕迹比如--enable-automation这个参数也可能是 Canvas 指纹有问题也可能是 JS 执行时间异常。想要搞定这种级别的风控需要更深入的反检测方案比如自己编译修改过的 Chromium、屏蔽特定的 CDP 命令、对接 APK 代理实现指纹修改这就已经超出“绕 Webdriver 检测”的范畴了是一套完全独立的对抗技术体系。所以这篇博文里讲的内容目标定位是常规的 Webdriver 检测和中等强度的反爬策略。搞定了这一层你已经能应对市面上大部分网站的自动采集需求了。3. Playwright环境搭建与第一个能跑的脚本3.1 安装步骤别看简单但容易出错先说最基础的安装。Playwright 的 Python 版本是我用得最多的安装命令是pip install playwright playwright install chromium第一行是装 Python 库第二行是下载 Chromium 浏览器内核。这里有个很多人踩过的坑如果你只执行了pip install playwright没有执行playwright install那运行时会报错说找不到浏览器可执行文件。顺带提一下如果你在公司内网环境或者服务器访问外网受限这个浏览器下载可能会失败。这时候可以用playwright install --with-deps chromium来安装系统依赖或者手动下载浏览器包设置环境变量PLAYWRIGHT_BROWSERS_PATH指向浏览器存放目录。另外现在搜“chrome webdriver下载”还会搜到一堆老教程那都是 Selenium 时代的配套操作。用 Playwright 根本不需要单独下载 webdriver它自带驱动管理这也是它比 Selenium 方便的地方之一。刚转向 Playwright 的同学别再把 Selenium 那套思维带过来了不用匹配 chromedriver 版本不用在 PATH 里加驱动路径简单省事太多。3.2 常见的Playwright API报错先排掉这个坑安装完毕写第一个脚本的时候很多人会碰到这个报错playwright._impl._errors.Error: It looks like you are using Playwright Sync API inside the asyncio loop.这个报错的意思是你在一个异步asyncio环境中使用了同步 API。这类问题常见于 Jupyter Notebook、某些 Web 框架的请求处理函数里因为这些环境本身已经有一个事件循环在运行如果再用sync_playwright()就会冲突。解决办法有两个用异步 API把代码改成async def main()然后用await async_playwright().start()启动。确认自己真的是在普通 Python 脚本里运行没有开任何事件循环那大概率是你把sync_playwright和async_playwright混着用了检查一下 import 部分。我自己的习惯是写独立爬虫只用同步 API因为大部分爬虫是串行的同步代码读起来更直接。如果要用 Scrapy 或者 FastAPI 这种异步框架就统一走异步 API绝不在一个进程里混用两套 API。3.3 用同步API跑通一个最小脚本先不看反检测我们写一个最基础的页面访问脚本确认整个链路是通的from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) page browser.new_page() page.goto(https://example.com) print(page.title()) browser.close()这段代码的作用是启动一个可见的 Chromium 窗口访问 example.com打印页面标题然后关闭。headlessFalse表示有头模式测试阶段务必用有头模式因为你能直观看到页面加载过程中有没有弹验证码、有没有被跳转拦截。等全部调通之后再改成headlessTrue放到服务器上跑。有头模式和无头模式的区别不仅仅是能不能看到窗口。很多反爬脚本会检测navigator.webdriver之外的一些特征无头模式下的navigator.userAgent会少掉一些字段或者某些 WebGL 渲染结果和真机不同。早期 Playwright 的无头模式用的是老的无头方案特征明显现在新版本的无头模式已经和真正无头保持一致很多了但为了稳妥本地调试先用有头模式是明智的。4. 实战反检测配置与完整脚本4.1 最小有效配置启动参数和上下文要做反检测核心就是从启动参数、上下文配置、初始化脚本三个层面把自动化痕迹降到最低。下面这套配置是我实测过很多次、兼容性最好的一套from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch( headlessFalse, args[ --disable-blink-featuresAutomationControlled, --disable-automation, --no-sandbox, --disable-infobars, --start-maximized, ] ) context browser.new_context( viewport{width: 1920, height: 1080}, screen{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, localezh-CN, timezone_idAsia/Shanghai, permissions[geolocation], ) page context.new_page() page.goto(https://example.com) browser.close()几个参数分别解释一下--disable-blink-featuresAutomationControlled这是最核心的一个启动参数它会告诉 Chromium 不要启用“自动化控制”相关的 Blink 特性。加了它之后navigator.webdriver就不再是true也去掉了一些内部自动化标记。--disable-automation禁止了部分自动化相关的 UI 提示和特征。--start-maximized让窗口最大化避免窗口尺寸异常导致视口检测出问题。viewport和screen保持一致否则网站可以通过window.innerWidth和screen.width的差异判断出浏览器被缩放或驱动过。user_agent单独指定防止无头模式下的 UA 和真实浏览器不一致。locale和timezone_id让语言环境和时区保持为中文不然某些站点会根据语言环境判断访问者来源进而触发风控。4.2 初始化脚本主动清理所有JS特征除了启动参数我们还需要在页面任何脚本执行之前注入一段“清理脚本”。Playwright 给的能力是context.add_init_script()这个脚本会在每次页面导航前运行非常关键。下面是我目前一直在用的完整清理脚本覆盖了大部分常规的检测点stealth_js Object.defineProperty(navigator, webdriver, { get: () undefined }); Object.defineProperty(navigator, languages, { get: () [zh-CN, zh, en] }); Object.defineProperty(navigator, plugins, { get: () [1, 2, 3, 4, 5] }); // 伪造 chrome 对象 window.chrome window.chrome || { runtime: {}, loadTimes: function() {}, csi: function() {}, app: {} }; // 修正 permissions 描述 const originalQuery window.navigator.permissions.query; window.navigator.permissions.query (parameters) ( parameters.name notifications ? Promise.resolve({ state: Notification.permission }) : originalQuery(parameters) ); // 隐藏 CDP 相关特征 const originalGetElementsByTagName Document.prototype.getElementsByTagName; Document.prototype.getElementsByTagName function(tagName) { const elements originalGetElementsByTagName.call(this, tagName); for (let i 0; i elements.length; i) { if (elements[i].id webdriver-error || elements[i].id webdriver) { elements[i].remove(); } } return elements; }; context.add_init_script(stealth_js)这段脚本的核心思路是把navigator.webdriver定义为undefined这是最基础的。把navigator.languages定义成中文语言列表避免默认的英文语言特征。造一个navigator.plugins对象模拟装了插件的样子。很多检测脚本只看plugins.length如果是 0 就直接判定为自动化。伪造window.chrome对象保证检测脚本不会因为缺少这个对象而报异常。修正navigator.permissions.query方法避免权限查询结果异常触发检测。这里我要提醒一个非常容易踩的坑add_init_script必须在创建页面之前加到context上。如果你先new_page()再调用add_init_script那已经打开的页面上不会执行这段脚本检测照样会失败。正确顺序是create context - add_init_script - create page。4.3 行为层面的伪装降低被AI风控识别的概率前面两种手段解决的是“静态特征”也就是检测脚本“看一眼”就能发现的问题。但就像我在第 2 部分说的高级风控已经会用行为数据来识别自动化了。所以如果你的爬虫需要长时间运行或者目标网站风控等级较高我建议做一层行为伪装。最简单的行为伪装包括鼠标移动不是瞬间完成而是给一个随机的移动时间。页面滚动不要一次性滚到底分几次滚动每次间隔几百毫秒。填写表单时模拟逐字输入而不是一次fill进去。每个操作之间加上随机延迟延迟范围不要是固定值。在 Playwright 里实现这些非常容易比如import random import time # 模拟滚动每次滚动一段距离 for i in range(5): page.mouse.wheel(0, random.randint(300, 700)) time.sleep(random.uniform(0.5, 1.5))如果目标站点对行为要求更高还可以引入更高级的轨迹生成算法比如基于贝塞尔曲线的鼠标移动。不过这个在大多数场景下用不到不多展开。你要记住的核心原则是行为数据要让统计规律接近真人而不是看起来像代码指令。4.4 与Scrapy等框架集成时节省内存的配置技巧不少人是做爬虫框架集成的比如在 Scrapy 里用 Playwright 渲染动态页面。互联网上搜 “scrapy playwright 动态 iframe” 的同学基本都遇到过一个问题用 Scrapy 默认的下载中间件请求页面拿到的 HTML 没有内容因为数据是 JS 动态渲染的。用scrapy-playwright这个库可以比较方便地接入 Playwright。核心配置大概是# settings.py DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } PLAYWRIGHT_BROWSER_TYPE chromium PLAYWRIGHT_LAUNCH_OPTIONS { headless: True, args: [ --disable-blink-featuresAutomationControlled, --disable-automation, ] }在爬虫里指定某个请求需要渲染时通过meta传参yield scrapy.Request( urlhttps://example.com, meta{ playwright: True, playwright_page_methods: [ scrapy_playwright.page.PageMethod(wait_for_selector, .content div), ], }, )这里wait_for_selector是专门用来等动态内容的比固定time.sleep(3)可靠得多。你只要元素一出现就继续执行网页慢的时候不会浪费等待时间快的时候也不会因为超时失败。5. 动态iframe、请求监听和网络层技巧5.1 动态iframe的取数思路回到“scrapy playwright 动态 iframe”这个热搜词。很多登录后的数据、内嵌页面都会用 iframe 来加载而且这个 iframe 是 JS 动态创建的。你在主页面里抓 HTML 是看不到 iframe 内容的必须切换到 iframe 的 document 里去取。Playwright 处理 iframe 的方式比 Selenium 顺手很多核心就是frame_locator。等 iframe 出现之后直接在 iframe 内部再定位元素# 等待 iframe 加载完成 page.wait_for_selector(iframe[src*data]) # 使用 frame_locator 进入 iframe frame page.frame_locator(iframe[src*data]) frame.locator(input[nameusername]).fill(testuser) frame.locator(button[typesubmit]).click()注意frame_locator是“懒”定位它不会立即锁定帧而是等你真正操作里面元素时才去查找。如果你的 iframe 是嵌套多层可以连续调用.frame_locator(...)逐层进入。这个能力在处理复杂嵌套页面时简直就是救命稻草Selenium 里切换 frame 要写switch_to.frame还得担心 frame 有没有加载完Playwright 这套接口要舒服太多。另外如果你遇到的是“同源 iframe 中通过 JS 修改父页面数据”的情况可以用page.frames拿到所有已经加载的 frame 列表遍历查找目标 URL然后在对应 frame 上执行evaluate。5.2 监听页面请求拿到接口JSON比解析HTML高效得多很多爬虫同学习惯等页面加载完去解析 HTML 里的文本和数据。但如果目标页面里的数据是异步接口返回的 JSON那你就不用费劲跟 DOM 斗智斗勇了直接用 Playwright 监听请求把响应里的 JSON 截下来。监听请求的核心 API 是page.expect_response和page.on(request)。下面这段代码演示了怎么拦截一个 API 请求并解析 JSON# 提前定义一个响应对象数组 captured_responses [] def handle_response(response): if api/data in response.url and response.status 200: try: data response.json() captured_responses.append(data) except Exception: pass page.on(response, handle_response) page.goto(https://example.com) page.wait_for_timeout(3000) # 拿到所有捕获的数据 for data in captured_responses: print(data)这种方法的优势很明显JSON 数据结构化更好不依赖页面 DOM 结构页面改版也不会影响你的解析逻辑。我自己的经验是一个复杂的数据采集任务能用接口请求拿到 JSON 的绝不去解析 HTML。Playwright 的请求监听机制让我能精准地在正确的时机触发、捕获、提取请求和响应数据真正做到“眼见为实”的数据获取。还有一个使用场景你想知道某个按钮点击之后到底发了什么请求可以在点击之前加上监听点击之后打印所有捕获到的请求 URL、请求头和 POST 数据。这样做调试效率极高比我以前在 Selenium 里用 BrowserMob 之类的代理工具要轻量太多。5.3 处理弹窗、验证码和登录态真实爬虫里你还会遇到几个常见场景登录后才能看的页面、验证码弹窗、和维护会话的 Cookie。Playwright 在这块的解决方案是storage_state。第一次手动登录成功后把上下文状态保存下来context.storage_state(pathstate.json)后续脚本启动时直接加载context browser.new_context(storage_statestate.json)这样就能免登录访问需要认证的页面。storage_state保存的是 localStorage、sessionStorage、Cookie 等信息对大多数网站来说足够维持登录态。验证码是个大话题。简单验证码可以靠打码平台复杂的行为验证码比如滑动、点选需要配合图像识别和轨迹模拟。Playwright 在这类场景的优势是它本身就能模拟精确的鼠标操作配合打码平台返回的坐标可以比较稳定地完成滑块验证。但每家平台的验证码逻辑不同方案也要跟着调整这不是一篇博文能覆盖完的这里先提个思路。6. 常见问题与排查技巧实录6.1 问题速查表我在写爬虫过程中几乎把网上能踩的坑都踩了一遍。这里整理了一个高频问题速查表全部基于自己的实测经验问题现象可能原因解决思路navigator.webdriver仍为trueadd_init_script执行时机不对或在new_page之后才注册确保在new_context之后、new_page之前调用add_init_script网站总是返回“访问频繁”请求频率过高或 IP 被限制降低 QPS分批采集换代理 IP页面能打开但数据不渲染JS 加载慢可能是字体、CSS 阻塞用wait_for_selector等待具体元素不要用固定 sleepiframe 里拿不到元素iframe 本身是动态创建的加载耗时不确定用page.wait_for_selector(iframe)等 iframe 出现后再操作无头模式下行为异常老版无头模式的浏览器特征较明显升级 Playwright 版本使用新无头模式或有头模式启动时浏览器白屏、闪退缺少系统依赖库运行playwright install-deps chromium同步 API 报错 asyncio 冲突在异步环境中混用了sync_playwright统一换成async_playwright异步 API网站偶尔能过偶尔被拦环境指纹不稳定或风控模型有随机性保持 UA、视口等配置稳定不要频繁变化加上自动化重试机制6.2 高级风控场景的应对思路如果你发现目标网站用了瑞数这类商业级风控常规手段大概率会发现“这脚本根本进不了数据”。这不是你代码写得不对而是对抗的级别升级了。这类风控的特征是页面里加载了一段极其混淆的 JS生成动态 Cookie而且每个请求的 Cookie 有效期很短必须由浏览器实时执行 JS 才能生成。你要应对就得学会 JS 逆向找出那段混淆代码里 Cookie 的生成逻辑在真正发请求之前算好 Cookie或者完整地模拟浏览器执行环境。Playwright 在这种场景下更多是被拿来“验证”你的逆向结果而不是直接硬闯风控。这个方向牵扯到很多细节一步到位很难。我的建议是先把基础的反检测配置做扎实理解浏览器自动化每一层会暴露什么特征然后再根据目标网站的具体风控策略逐步升级手段。6.3 几个使用时的实用心得最后分享几条实操心得这些很难在官方文档里找到但能帮你少走很多弯路。第一慎用并发。Playwright 开多个 Context 确实很方便但每个 Context 都是一个完整的浏览器环境内存消耗很大。我之前在 8G 内存的服务器上开了 10 个并发 Context直接把机器跑崩了。合理规划并发数一般 3 到 5 个是最稳妥的资源允许再加。第二采集任务必须做断点续跑。不管你的脚本多稳定长时间运行一定会遇到网络抖动、页面超时、目标网站升级反爬这些意外。把已采集的数据先落盘用任务队列记录每个 URL 的状态失败的任务自动重试才能保证整个采集流程稳定可靠。第三日志一定要详细。Playwright 脚本跑起来是黑盒出了问题如果没有详细的日志排查起来非常痛苦。我的习惯是每次页面跳转、每个关键步骤都打印当前 URL 和状态码方便后期回溯是哪个环节被拦截了。第四反检测配置最好封装成公共类。不要每个爬虫脚本都重新写一遍启动参数和清理脚本把这些封装成一个工具函数比如create_stealth_context()所有任务都调用它。这样既统一了行为也方便后续升级反检测策略一处改动全部生效。这些经验都是我在一次次踩坑之后总结出来的。自动化采集这个领域本质上就是“你的脚本能不能更像真人”的对抗游戏你能做的就是从环境、行为、频率多个维度去逼近真实用户。Playwright 是个好工具但你最终能不能稳定拿到数据还是要看你对目标网站检测逻辑的理解深度以及对自己代码的细节把控有多严格。
返回列表