news 2026/10/21 17:44:00

Playwright替代Selenium:动态页面爬虫实战与反爬避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Playwright替代Selenium:动态页面爬虫实战与反爬避坑指南

这两年做动态页面采集,Python 爬虫实战里最让我纠结的就是选型:Selenium 用了三年,写起来顺手,但遇到反爬和复杂等待场景越来越吃力。后来我把核心采集流程全部切到 Playwright,用 Playwright 替代 Selenium 之后,采集稳定性肉眼可见地提升了一个档次。这篇博文就讲讲我为什么换、怎么换、以及换完之后踩过哪些坑,全程只聊实操,适合正在做动态页面爬虫、想迁移工具链的朋友。

1. 为什么最终决定用 Playwright 替代 Selenium

1.1 动态页面的真实麻烦

现在的网页早就不是“请求 HTML 就能拿数据”的时代了。大量电商、社区、数据平台的前端都是前后端分离,列表、详情、翻页全靠 JavaScript 异步请求渲染。你用 requests 拿到的 HTML 里可能只有框架标签,数据都在一堆 script 里。这种页面必须用一个真正的浏览器去执行脚本,等 DOM 更新后再提取内容,这就是动态页面爬虫的核心诉求。

拿我之前做的某跨平台系统数据采集项目来说,目标页面加载后要先发三个异步接口拉数据,再经过前端模板渲染,最后出现在 DOM 中。Selenium 也能做,但每次等数据渲染都必须靠WebDriverWait配合各种条件,写复杂的等待逻辑不说,还经常因为网络波动导致加载超时。Playwright 的自动等待机制把这个问题简化了不少,这是最初吸引我的点。

1.2 Selenium 让我抓狂的几个场景

先说清楚,我不是为了赶时髦才换工具,而是 Selenium 这几个问题直接影响到了项目质量和交付时间。

第一是启动速度。Selenium 配合 ChromeDriver 每次启动浏览器都要初始化一个 WebDriver 会话,冷启动经常要两三秒,在批量任务里这个开销会被放大很多倍。Playwright 启动浏览器同样不慢,但它能复用持久化上下文,多个页面任务可以共享同一个浏览器实例,整体开销明显更低。

第二是等待机制。Selenium 4 之前主要靠显式等待 + 隐式等待,两者混用时经常出奇奇怪怪的时序问题。比如隐式等待设了 10 秒,元素定位会卡住所有后续操作。Playwright 则内置了 actionability 检查,点击、填写、截屏之前都会自动等到元素可操作,基本不用手写time.sleep。

第三是反爬识别。Selenium 驱动的浏览器在navigator.webdriver、浏览器特征、CDP 行为上都容易被识别。我当时做一个模拟数据采集项目,Selenium 启动的 Chrome 在目标网站上一分钟就被拦了三次,换配置、换参数效果都不稳定。Playwright 对浏览器上下文的模拟更底层,配合启动参数和初始化脚本,看起来更像真实用户。

1.3 Playwright 到底强在哪里

Playwright 的核心是走 DevTools 协议,直接和浏览器底层通信,所以它不仅能控制浏览器,还能拦截网络请求、修改响应、模拟设备、处理多页面。这些能力在爬虫场景里都非常实用。

比如page.route()可以拦截请求并改写返回内容,有些页面会通过接口返回加密数据,你可以在浏览器层面直接替换响应体,再让前端用假数据渲染。这在 Selenium 里几乎没法优雅实现。再比如browser.new_context()可以把每个用户会话隔离成独立上下文,模拟多账号时的 cookie、localStorage 互不污染,做起并发来方便很多。

2. 核心概念与环境准备

2.1 浏览器上下文这个概念

开始写代码之前,必须先弄明白 Playwright 里的一个关键抽象:Browser Context(浏览器上下文)。它相当于一个独立的“隐身窗口副本”,同一浏览器实例下可以开多个 context,每个 context 的 cookie、缓存、存储都是独立的。

这个设计对爬虫太重要了。之前用 Selenium 做多账号采集,要么反复切换 profile,要么自己维护 cookie 字典,一不小心就串号。用 Playwright 只需要:

browser = p.chromium.launch(headless=False) ctx_a = browser.new_context(user_agent="Mozilla/5.0 ...") ctx_b = browser.new_context(user_agent="Mozilla/5.0 ...") page_a = ctx_a.new_page() page_b = ctx_b.new_page()

两个页面之间完全隔离,数据不会互相干扰。我现在的并发采集脚本都用这个模式,每个账号一个 context,跑完关闭即可。

2.2 安装与初始化

安装过程比 Selenium 省心不少,因为 Playwright 把浏览器下载也集成到工具链里了:

pip install playwright playwright install chromium

playwright install会下载浏览器二进制,同时还会安装一些系统依赖,Linux 环境下可能需要额外装一些库。如果你用 Docker,建议直接用官方镜像,省去环境折腾。

初始化代码也很简单:

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

这里注意同步 API 和异步 API 的选择。同步 API 用起来直观,适合中小型任务;异步 API 基于 asyncio,适合并发量大的场景。第一次使用建议从同步 API 入手,逻辑清楚,调试方便。

2.3 三种启动模式怎么选

Playwright 启动浏览器时有三个关键参数:headless、channel和args。最常用的是headless=True,即无头模式,适合服务器环境,速度快、省资源。

但无头模式有个坑:有些站点会检测无头浏览器的特征。比如navigator.webdriver属性、浏览器窗口大小、字体渲染等,容易被识别。遇到这种情况,可以尝试新版的 headless 模式,或者干脆用headless=False在有显示器的环境跑,也可以在参数里加--headless=new使用 Chrome 的新无头模式。下面是我常用的一个启动配置:

browser = p.chromium.launch( headless=True, args=[ "--disable-blink-features=AutomationControlled", "--disable-dev-shm-usage", "--no-sandbox", ] )

--disable-blink-features=AutomationControlled可以去掉一部分自动化标记,--no-sandbox在容器内经常需要,--disable-dev-shm-usage则能避免共享内存不足导致的崩溃。这几个参数是我在 Linux 服务器上实测下来最稳定的组合。

3. 动手写第一个 Playwright 爬虫

3.1 目标页面分析与请求模拟

写任何爬虫,第一步都是分析页面。打开目标页面后,按 F12 看网络请求,重点关注哪些接口在滚动或翻页时才触发。很多时候你会发现,页面上的数据其实是某个 JSON 接口返回的,浏览器只是把 JSON 渲染成 DOM。这时候不一定非得模拟点击,直接请求那个 JSON 接口反而更高效。

但有些页面的参数是前端加密的,或者接口返回后还要经过前端 JS 额外处理才展示。这种情况我必须上浏览器自动化模拟。比如我之前做的某图像处理 Demo 的数据展示页,数据接口用一个自定义的签名参数,跑了半天也没逆向出来,最后直接让 Playwright 打开页面,等它渲染完再提取,反而省了大力气。

3.2 页面加载与元素定位

Playwright 定位元素的方式和 Selenium 很像,但语法更简洁:

page.goto("https://example.com/list") page.wait_for_load_state("networkidle") items = page.locator(".item-card") print(items.count())

wait_for_load_state("networkidle")表示等待网络空闲后再往下走,这是处理动态页面最常用的等待方式。要注意的是,networkidle在某些持续轮询的页面上会一直等不完,所以更稳妥的办法是配合超时控制:

try: page.wait_for_load_state("networkidle", timeout=10000) except Exception: page.wait_for_timeout(2000)

这样既兼顾了动态加载,又不会因为某个长连接卡死整个任务。

元素定位优先级上,我一般按这个顺序选:get_by_text、get_by_role、locator("css selector")。文本和角色定位更贴近用户视角,比一长串的 XPath 可读性强,也不容易因为层级变化就失效。

3.3 数据提取与翻页逻辑

提取数据我用locator加evaluate的组合。evaluate可以在浏览器上下文中执行任意 JavaScript,直接返回结构化数据,比 Selenium 一个元素一个元素地get_attribute高效得多。

举个例子,要提取列表页的卡片数据,我可以这样写:

cards = page.locator(".item-card") data = cards.evaluate_all( """els => els.map(el => ({ title: el.querySelector('.title')?.innerText, price: el.querySelector('.price')?.innerText, link: el.querySelector('a')?.href }))""" )

这样一次调用就把所有卡片信息拿回来了。翻页逻辑也不难,找到下一页按钮点击即可:

while True: # 提取当前页数据 ... next_btn = page.locator("a.next") if not next_btn.is_visible(): break next_btn.click() page.wait_for_load_state("networkidle")

但有个细节要注意:点击翻页后,列表数据是异步重新渲染的,直接提取可能拿到的是旧数据。我习惯在点击后先等待一个关键元素变化。比如记录当前页第一条数据的标题,等页面上出现不同标题时再开始提取,比固定sleep(2)可靠得多。

4. 动态内容处理:滚动、等待与交互

4.1 智能等待策略

Playwright 自动等待虽然省心,但不要盲目依赖。自动等待解决的是“元素可见可点”的问题,而动态页面最大的变量是“数据什么时候出现在 DOM 里”。我总结了一套等待策略,按优先级排列:

  • 优先等数据特征:比如某个关键文本出现、列表条数大于 N
  • 其次等网络空闲:networkidle或domcontentloaded
  • 最后才用固定wait_for_timeout,能不用就不用

比如等待某个指标数字出现:

page.wait_for_selector(".total-count:has-text('共 100 条')")

或者用wait_for_function等待自定义条件:

page.wait_for_function( """() => document.querySelectorAll('.item-card').length >= 20""" )

这种写法的好处是精确、不浪费时间。页面加载快了,爬虫也跟着快;页面慢了,也不会过早提取空数据。

4.2 滚动加载与懒加载

很多无限滚动页面,只有滚动到接近底部时才触发下一页数据加载。处理这种场景,我常用的方式是通过 JavaScript 滚动容器:

page.mouse.wheel(0, 5000) page.wait_for_timeout(1000)

但更可控的方法是用evaluate直接操作滚动:

page.evaluate("window.scrollTo(0, document.body.scrollHeight)") page.wait_for_timeout(800)

滚动之后要判断是否真的加载了新数据。我一般记录当前列表项数量,滚动后重新计数,如果数量没变,说明已经触底了:

count = 0 while True: page.evaluate("window.scrollTo(0, document.body.scrollHeight)") page.wait_for_timeout(1000) new_count = page.locator(".item-card").count() if new_count <= count: break count = new_count

需要注意,有些页面的滚动容器不是document.body,而是某个内部 div,这时候要换成对应元素的scrollTop操作。判断触底的条件也可以看是否出现了“没有更多了”这类标记。

4.3 复杂交互的模拟

动态页面不只是展示,还有很多交互式内容,比如下拉框筛选、日期范围选择、悬停菜单。Playwright 模拟这些操作比 Selenium 细腻一些,因为它的动作会触发完整的事件序列,而不只是设置一下 value。

我做过一个数据报表采集任务,页面有个日期范围选择器,是一个自定义组件。用 Selenium 的时候要先把输入框清空、再逐字输入,失败率很高。用 Playwright 我就老老实实走点击交互:

page.locator(".date-picker").click() page.locator(".calendar .day-cell:has-text('15')").first().click() page.locator(".calendar .day-cell:has-text('20')").first().click() page.keyboard.press("Enter")

还有搜索框的自动补全下拉,可以用fill输入关键词,再等下拉项出现后点击:

page.locator("#search").fill("天气预报") page.locator(".suggestion-item:has-text('天气预报')").first().click()

这里特别提醒一个坑:很多组件库的点击事件绑定在冒泡阶段,直接fill可能不会触发前端的响应。如果发现填了值但页面没有任何反应,可以先click再fill,保证焦点事件和输入事件正常触发。

5. 反爬与浏览器指纹规避

5.1 常见反爬手段

动态页面爬虫遇到最多的反爬手段有以下几类:检测自动化特征(navigator.webdriver)、检测浏览器指纹(Canvas、WebGL、字体)、IP 频率控制、接口签名校验。前两类正好是 Playwright 比较容易应对的,后两类需要配合代理和逆向,不在今天的主题范围内。

navigator.webdriver是最基础的检测点。Playwright 默认情况下这个属性也是true,所以需要用初始化脚本把它隐藏掉:

page.add_init_script( "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" )

这段代码会在页面任何脚本运行之前执行,把webdriver属性改掉。再配合启动参数里的--disable-blink-features=AutomationControlled,能绕过大部分初级检测。

5.2 像真人一样的操作

玩过采集的人都知道,频率和操作模式比浏览器特征更容易暴露。控制频率属于调度层面的问题,这里不细聊。操作模式上,有几个细节值得注意。

真实用户的行为是存在随机性的:鼠标移动会留有轨迹,点击位置会略有偏差,阅读时间有长有短。如果你的爬虫每次点击都在同一坐标、每次间隔都精确到毫秒,反爬系统很容易把你挑出来。我在脚本里加入随机延迟和动作变化:

import random import time time.sleep(random.uniform(0.5, 2.0)) page.mouse.move(random.randint(100, 400), random.randint(200, 500)) page.mouse.click(300, 350)

虽然不能完全拟人,但可以让行为曲线没机器那么规律。如果你对反爬要求严格,可以引入行为轨迹库,模拟更自然的鼠标路径。

5.3 代理与请求头配置

配置代理在 Playwright 里非常直接,创建 context 时传入proxy参数即可:

browser.new_context( proxy={ "server": "http://proxy.example.com:8080", "username": "user", "password": "pass", } )

请求头也可以在 context 级别统一设置,比每个请求单独设置方便:

browser.new_context( user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", locale="zh-CN", viewport={"width": 1920, "height": 1080}, timezone_id="Asia/Shanghai", )

locale、timezone_id这些参数很多人忽略,但它们在指纹检测里很关键。一个浏览器同时声明zh-CN的地区设置和America/New_York的时区,本身就显得可疑。我的经验是尽量让所有上下文特征保持一致,浏览器才会显得自然。

6. 常见问题与排查实录

6.1 元素定位失败

现象:脚本报TimeoutError,模块找半天wait_for_selector都超时。

排查思路:先确认页面是不是加载到了目标状态。我在调试时习惯先截一张图看看当前页面长什么样:

page.screenshot(path="debug.png", full_page=True)

然后检查是否有多层 iframe。动态页面经常把内容放在 iframe 里,Playwright 需要先切换到对应 frame:

frame = page.frame_locator("iframe[name='main']") frame.locator(".item-card").count()

如果确认元素存在但定位不到,十有八九是选择器写死了某个动态类名,换成相对稳定的属性定位即可。

6.2 页面白屏或超时

现象:page.goto()偶尔卡住不动,或者页面加载出来但是白屏。

排查思路:白屏最常见的原因有两个。一个是前端脚本报错中断渲染,另一个是浏览器渲染进程崩溃。对于前者,可以在控制台监听异常事件:

page.on("console", lambda msg: print(msg.text)) page.on("pageerror", lambda e: print(f"Error: {e}"))

对于后者,检查启动参数里的--disable-dev-shm-usage是否加上。在 Docker 环境中/dev/shm默认只有 64MB,浏览器很容易崩溃,这个参数几乎是必加的。

6.3 资源占用过高

现象:跑着跑着内存飙升,服务器卡死。

排查思路:先看看是不是每个任务都重新启动了浏览器。正确的做法是复用浏览器实例,每个任务只创建独立 context。跑完一个 context 记得context.close()。

另外一个隐藏内存泄漏点是 event listener。如果你在 page 上挂了多个on("response")回调,任务结束后监听器没清理,会随着任务数增加越积越多。我习惯把事件回调统一封装在 context 级别,任务结束时统一移除。

6.4 反爬识别后页面返回假数据

现象:页面能正常打开,但提取到的数据全部是指定占位符,或前端故意渲染了伪造内容。

这种反爬手段越来越常见,服务器给你真实页面,但把数据替换成了干扰项。这时候别急着改代码,先手动用浏览器访问目标页面,对比真实数据和采集数据的差异。如果两者确实不同,说明是服务端根据你的指纹或行为做了差异化响应。

应对方式还是要回到上下文特征和操作行为上。实测中,把 viewport、user-agent、时区设置得更接近普通用户,并减少高频操作,通常能缓解。如果站点明确禁止采集,就没有必要强攻,换一个合规的数据源更实际。

7. 迁移成本与选型建议

7.1 从 Selenium 迁移过来要改多少代码

如果你手头已经有一套 Selenium 的爬虫,不用推翻重写。定位、等待、提取这些核心逻辑可以平替。Playwright 提供了类似的条件定位方式,大部分 CSS 选择器和 XPath 都能直接复用。

迁移时最容易栽跟头的地方是 iframe 切换和窗口管理。Selenium 用switch_to.frame,Playwright 用frame_locator;Selenium 的switch_to.window在 Playwright 里变成了监听page事件获取新页面。只要把这两个地方梳理清楚,其余代码基本都是机械替换。

我给一个简单的对照:

功能SeleniumPlaywright
刷新等待WebDriverWait + expected_conditionswait_for_selector / wait_for_load_state
执行 JSexecute_scriptevaluate / evaluate_all
切换 iframeswitch_to.frameframe_locator
新窗口switch_to.windowpage.expect_popup()
截图save_screenshotpage.screenshot
cookie 管理get_cookies / add_cookiecontext.cookies() / context.add_cookies()

7.2 什么场景值得换,什么场景不用换

如果只是简单地从静态 HTML 里提取数据,或者页面几乎不涉及 JavaScript 渲染,用 requests 加解析库就够了,没必要上任何浏览器自动化工具,成本反而更高。

如果已经在用 Selenium,且页面交互复杂、反爬压力大,换成 Playwright 的收益就非常明显。我这里给个更具体的判断标准:单个任务的超时重试率如果超过 10%,说明工具链的稳定性已不足以支撑任务,值得花时间迁移。

如果页面是纯 SPA(单页应用)且内部状态很多,那 Playwright 或同类的 CDP 方案基本是唯一选择。

7.3 一点点使用心得

用 Playwright 做了半年多采集,最深的感受是它的设计思路让爬虫代码变得更接近“用户在浏览器里操作”的自然流程。对比 Selenium 那种“通过 WebDriver 协议间接控制浏览器”,Playwright 的 CDP 直连方式减少了中间层损耗,稳定性和速度都更好。

但工具终究只是工具,数据采集的核心还是要遵循目标站点的规则,尊重双方权益。我现在的项目里,会把采集频率控制在合理范围,对目标站点限流时自动退避,而不是硬碰硬。毕竟爬虫不是只跑一次,能长期稳定运行才最有价值。如果你正被 Selenium 的等待、反爬问题折磨,完全可以把 Playwright 纳入备选方案,用一个实际项目试两手,再决定要不要切过去,我相信体验会让你眼前一亮。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 17:42:02

2026年CI/CD测试工具选型:从执行到决策的数据闭环

2026年&#xff0c;做CI/CD测试工具选型这件事&#xff0c;我最大的感触是&#xff1a;单点工具的比拼已经结束了&#xff0c;大部分项目的痛点和机会点&#xff0c;都集中在“工具链如何围绕测试数据闭环运转”这件事上。过去一年里找我聊CI/CD测试方案的朋友&#xff0c;问的…

作者头像 李华
网站建设 2026/10/21 17:41:44

AI绘图风格总飘?327种手绘风格编号化实现稳定输出

1. 当AI画图开始"飘"&#xff0c;问题到底出在哪做AI绘图相关工作的人大概都有过这种体验&#xff1a;同一段提示词&#xff0c;今天跑出来是干净利落的扁平插画&#xff0c;明天再跑就变成了厚涂油画风&#xff1b;明明想要的是"手绘感"&#xff0c;结果出…

作者头像 李华
网站建设 2026/10/21 17:41:11

三步把 AI PR 门禁接进 GitHub Actions

把 AI 接到 PR 审查上这件事&#xff0c;我去年就试过一轮。当时最大的感受不是“爽”&#xff0c;而是“乱”&#xff1a;要么是模型评论刷屏没人看&#xff0c;要么是 workflow 里一把梭跑完就散&#xff0c;完全没有起到把关的作用。后来我把整个逻辑重新拆成“拿 diff ->…

作者头像 李华
网站建设 2026/10/21 17:41:07

App渗透测试实战:SQL注入与人脸识别逻辑绕过登录任意账号

做App渗透测试这几年&#xff0c;我见过不少有意思的漏洞&#xff0c;但这个案例值得单独拿出来聊聊&#xff1a;一个带人脸识别登录的App&#xff0c;攻击链路竟然是SQL注入换数据&#xff0c;再抓住人脸识别接口的校验缺陷&#xff0c;最终成功登录任意账号。整个过程不算花哨…

作者头像 李华
网站建设 2026/10/21 17:39:24

基于LSTM-Adaboost的配电网短期负荷预测Matlab实现

最近在做一个配电网的短期负荷预测项目&#xff0c;数据预处理、特征工程折腾了一圈&#xff0c;最后在预测模型上卡住了。试过传统的ARIMA、支持向量回归&#xff0c;效果怎么说呢&#xff0c;能用&#xff0c;但总觉得差点意思。后来把LSTM和Adaboost结合起来搞了一套混合模型…

作者头像 李华
网站建设 2026/10/21 17:39:23

汇川H3U多轴混控模板设计:3步进3伺服CANlink总线与触摸屏配方实战

很多同行看到“汇川H3U 三个步进 三个伺服&#xff0c;伺服还走CANlink”这种组合&#xff0c;第一反应就是程序会不会很乱&#xff1a;步进要发脉冲&#xff0c;伺服要写总线&#xff0c;触摸屏还要来回切画面&#xff0c;一套下来光地址规划就够头疼的。这套配置我前后做过…

作者头像 李华