ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

浏览器自动化利器Playwright:动态渲染处理与pytest实践

浏览器自动化利器Playwright:动态渲染处理与pytest实践 入行做浏览器自动化这些年我一直有个很深的感触很多人一提到爬虫或者自动化第一反应还是“直接抓接口、解参数”觉得这才是高效的正道。但真到了生产环境你会发现页面上随便一个动态Token、一段JS加密、一层嵌套iframe就能让纯协议方案从“半天搞定”变成“维护到天荒地老”。这也是我把越来越多项目迁到Playwright上的原因——它不是万能银弹但在我接触过的同类型工具里它是把“稳定性”和“开发效率”平衡得最好的那一个。这篇内容我想从Playwright解决了哪些老问题讲起一路聊到环境安装、核心API、动态内容处理、Codegen调试再到pytest工程化集成和实际采集场景里的稳定性方案。不论你是想从Selenium迁移过来的测试工程师还是打算用浏览器自动化替代纯解码方案的爬虫开发又或者只是想把重复的网页操作脚本化这篇应该都能给你一条能直接走通的路。1. 为什么是Playwright它解决了自动化里的哪些老问题1.1 从Puppeteer到Playwright多浏览器驱动的演进Playwright是微软在2020年初开源的项目核心团队本身就是Puppeteer的原班人马。用过Puppeteer的朋友应该都知道它只有Chrome系内核这在小项目里没什么可真到了企业级自动化测试或者采集系统落地时“只支持Chrome”就是一个很尴尬的约束领导要求兼容Firefox测试环境里还有一批跑在WebKit内核的业务你就得额外维护两套代码。Playwright从设计之初就把多浏览器作为一等公民一套API统一操作Chromium、Firefox和WebKit。这意味着你在本地用Chromium调通的脚本换到Firefox上只需要改一行browser_type断言逻辑、等待机制、事件监听全部复用。我在一个实际的数据看板采集项目里生产环境Chromium出问题时切到Firefox几乎零成本这种冗余设计在关键时刻是能救命的。1.2 自动等待机制开发者“无感”但至关重要的设计老一代自动化框架里最让人头疼的就是各种time.sleep()和WebDriverWait。元素还没渲染出来就点击脚本就挂了等太久又浪费时间。Selenium里常见的EC.presence_of_element_located这类写法不仅啰嗦而且对“这个元素到底能不能点”的判断很弱。Playwright把所有交互操作都内置了自动等待底层是“可操作性检查actionability checks”。什么意思当你调用click()时框架会持续做四件事元素是否附着在DOM上、是否可见、是否稳定比如动画结束、是否能接收事件没有被遮挡。满足全部条件才真正点击超时才会抛异常。这套机制直接省掉了项目里80%的“等元素”代码。我自己测试过Playwright的自动等待最短是每100毫秒轮询一次默认超时30秒。开发时你基本可以无脑写操作不用关心异步渲染。这也是它“基于真实事件驱动”而不是“基于JS定时器”设计的优势——Selenium的click在某些版本里只是派发JS事件而Playwright是真真切切模拟了鼠标按下抬起的全过程触发的行为更接近真实用户。1.3 和Selenium/直接解码爬虫的对比顺着前面的思路我直接给一张自己在选型时常用的对比表方便大家看清Playwright的位置方案学习成本动态JS渲染维护成本多浏览器事件模拟真实度直接HTTP解码高无法处理极高不涉及无Selenium中可以处理高支持一般Puppeteer中可以处理中不支持高Playwright低天然支持低支持高很多人觉得“直接解码爬虫”才是高手的做法Playwright这种浏览器自动化太重。但我想说这个判断要看场景。如果你要抓的站是服务端渲染、接口干净、没有反爬那直接抓HTTP确实更轻量。可一旦遇到前端动态渲染、参数被JS加密、或者需要在页面里做复杂操作后才能触发请求协议层面的成本会指数级上升——你要维护逆向算法还要不断跟着对方前端改动去升级这是典型的“用战术勤奋掩盖战略懒惰”。2. 环境准备与第一个可用脚本绕开新手最常见的安装坑2.1 安装核心库与浏览器内核Playwright的安装分两部分Python库本身和浏览器内核。很多人只执行了第一步跑脚本时报“Executable doesnt exist”就懵了所以我把这两步都列出来pip install playwright playwright install chromium第一条命令装的是API库第二条才是真正下载Chromium浏览器内核。如果你后面要用Firefox或WebKit可以装全playwright install --with-deps chromium firefox webkit--with-deps这个参数在Linux环境下特别重要它会自动帮你装好系统级依赖库包括各种so库。我自己在Ubuntu 22.04上踩过坑没加这个参数时Chrome能启动但一打开带视频的页面就崩溃排查半天发现是缺少libnss3之类的系统库。有一点要说明Playwright的浏览器内核是独立下载的不会影响你电脑上日常使用的Chrome。你在脚本里看到的chromium是它自带的版本和系统浏览器互不干扰。如果你网速不好导致下载失败可以手动下载对应版本的浏览器压缩包解压后用executable_path参数指向本地路径。from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(executable_path/usr/bin/chromium-browser) page browser.new_page() page.goto(https://example.com) print(page.title()) browser.close()2.2 第一个脚本打开页面并获取标题装完之后建议不要一上来就写复杂的工程代码先把最小可用脚本跑通from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(https://example.com) print(页面标题:, page.title()) browser.close()这里有两个概念需要建立browser是浏览器实例page是这个浏览器里的一个标签页。new_page()会在一个全新的上下文里创建页面每个page是隔离的互相不共享cookie和localStorage。headlessTrue表示无头模式也就是没有可见的浏览器窗口。日常调试时我建议改成headlessFalse亲眼看看脚本在做什么避免“脚本报错但不知道页面当时是什么样”的盲调局面。2.3 初始化时常见的网络与路径问题如果你是在公司内网开发装浏览器内核时经常会遇到证书报错。Playwright提供了NODE_EXTRA_CA_CERTS这种环境变量来指企业根证书但Python环境下更通用的是把下载域名加入代理白名单。这个你可以按自己网络环境处理核心思路就是“保证安装命令能走到官方下载端点”。另外一个容易被忽略的问题Python的虚拟环境。pip install playwright装在了虚拟环境里但playwright install下载的浏览器内核默认放在用户目录下Linux和macOS是~/Library/Caches/ms-playwrightWindows是%USERPROFILE%\AppData\Local\ms-playwright它是全局共享的。所以就算你继续用虚拟环境跑代码浏览器内核也不需要重复下载。如果你用Docker部署记得把浏览器内核打进去否则容器一重建就没了。3. 核心交互能力拆解定位、操作、状态断言3.1 定位器Locator而不是选择器这是Playwright一个非常重要的设计理念。传统框架里我们写的是选择器Selector比如page.query_selector(#submit-btn)拿到的是一个DOM元素然后对这个元素做操作。Playwright里用的是定位器Locator它描述的是一个“我要找什么东西”的规则而不是一个已经找到的元素。这种设计带来的直接好处是定位器可以在页面上重试查找配合自动等待机制元素晚点出现也能被准确找到。比如login_btn page.locator(button:has-text(登录)) await login_btn.click()locator这只是一种描述click发生时Playwright会自动等待它满足可操作条件。如果你用的是传统写死元素的方式页面一变化脚本就废了。新手上路我不建议背一堆CSS选择器语法直接用最语义化的几个API就够page.get_by_role(button, name登录)按无障碍角色和名称定位最推荐page.get_by_label(用户名)按表单label定位page.get_by_placeholder(请输入手机号)按placeholder定位page.get_by_text(确认订单)按文本内容定位page.locator(.product-item)普通CSS选择器3.2 常见的用户操作与输入处理定位到元素之后常用的操作就这么几种click()、fill()填输入框、press()按键、select_option()下拉选择、check()勾选、hover()悬停。我举一个完整一点的业务操作场景比如登录页page.get_by_label(用户名).fill(test_user) page.get_by_label(密码).fill(passw0rd) page.get_by_role(button, name登 录).click() page.wait_for_url(**/dashboard)这里有几个细节说明一下fill()方法会先清空输入框再输入所以不需要手动ctrla删除旧内容。click()自动等待元素可点如果按钮被loading遮罩挡住会一直等到遮罩消失。wait_for_url是等待URL变成某个模式适合作为登录成功的校验点。还有一个小技巧处理上传文件时用set_input_files()它接受本地文件路径但这个API只能用于input typefile元素不能drag-drop到任意区域。如果你想模拟拖拽上传需要组合dispatch_event(drop)和文件负载比较绕大多数场景用set_input_files就够了。3.3 断言与重试机制断言是测试框架里的概念但在爬虫和自动化脚本里同样重要——你总得知道操作是否真的成功。Playwright对断言做了很好的封装推荐直接用expectfrom playwright.sync_api import expect expect(page.get_by_text(操作成功)).to_be_visible(timeout10000) expect(page).to_have_url(**/payment/success)这里的expect自带重试机制不像assert语句一旦失败立刻抛异常。它会持续轮询直到超时解决了“异步操作还没完成但断言已经执行”的经典问题。我在实际项目里的经验是凡是涉及异步渲染的断言一律用expect不要用裸assert。4. 动态内容的处理等待策略、iframe与滚动加载4.1 显式等待、自动等待与网络空闲上一章说了自动等待但自动等待只对“操作”生效比如点击、填写。如果你要等待一个数据接口返回后再做断言就需要显式等待。Playwright的page.wait_for_selector()、page.wait_for_response()、page.wait_for_load_state()是三个我最高频使用的等待方式。其中page.wait_for_load_state(networkidle)表示等待网络空闲即500毫秒内没有网络连接。很多人都喜欢在采集页面时用这个以为页面加载完就万事大吉。但我要提个醒networkidle在SPA应用里经常不靠谱因为页面会持续轮询接口永远没法真正空闲。更稳妥的做法是监听你关心的那个接口with page.expect_response(lambda r: api/order/list in r.url) as resp_info: page.get_by_role(button, name查询).click() response resp_info.value data response.json()这个写法是我认为Playwright最优雅的API之一先声明“我期待一个请求返回”再触发操作然后拿到那个响应对象。这样既等待又拿数据比wait_for_selector去等渲染结果要精准得多因为渲染出来的HTML可能被模板缓存不如直接读取接口JSON可靠。4.2 动态iframe场景的处理结合scrapy如果你在scrapy项目里接入了scrapy-playwright中间件会经常碰到一个页面里嵌套iframe、而目标内容在iframe里的情况。iframe是动态渲染的重灾区直接page.locator是拿不到跨域iframe内容的。Playwright提供了专门的frame_locator来处理frame page.frame_locator(#main-iframe) frame.get_by_text(确认授权).click() iframe_content frame.locator(.data-table).inner_text()frame_locator同样有自动等待能力它会在iframe加载完成后才去匹配内部元素省去了自己监听frameattached事件再获取frame对象的繁琐操作。在scrapy的parse回调里如果你拿到了Page对象思路完全一致——先定位iframe再从frame里提取数据。还有一个小技巧page.frames属性列出了页面当前所有frame你可以在调试时打印每个frame的URL快速确认目标内容到底在哪个iframe里。这点在遇到多层嵌套iframe时极其管用。4.3 滚动加载与无限流页面的采集思路“饿了吗”式的无限滚动列表是采集脚本的高频场景。直接抓接口往往面临分页参数加密问题而浏览器自动化可以走最朴素的路——不断滚动触发加载直到滚到底。我的做法是组合mouse.wheel和循环判断while True: before_count page.locator(.list-item).count() page.mouse.wheel(0, 3000) page.wait_for_timeout(1500) # 给渲染留时间 after_count page.locator(.list-item).count() if before_count after_count: breakmouse.wheel模拟的是真实的滚轮事件比直接执行window.scrollTo更不容易被前端监听逻辑识破。注意这里我用了wait_for_timeout因为滚动后渲染过程没有明确的事件可以监听只能适当等待。这个等待时间建议做个随机化比如1.2到1.8秒之间随机一次避免节奏过于规律。5. Codegen生成脚本与调试技巧让入门速度翻倍5.1 codegen的使用姿势Playwright自带的Codegen工具是我向所有新手首推的功能。它能把你在浏览器里的每一步操作自动录制为脚本省去手写定位器的时间。用法很简单playwright codegen https://example.com命令执行后会打开两个窗口左侧是操作页面右侧是实时生成的Python代码。你在页面上点击、输入、跳转右侧就会同步生成对应的page.get_by_role(...).click()这类代码。等操作完把代码复制到编辑器里就能跑。这个工具的价值不只是给新手用的。我在做复杂流程自动化时也经常会先用Codegen录制一遍底稿再手动改造成健壮的版本——比如把录出来的wait_for_timeout删掉换成等待特定接口响应把自动生成的CSS选择器改成get_by_role语义化定位。它相当于给你的自动化脚本打了一个草稿能省大量查阅文档的时间。5.2 Trace Viewer与本地调试如果只是看元素定位怎么写Codegen就够用了。但脚本出错时尤其是那种“偶尔失败”的疑难杂症我推荐用Trace Viewer看录制轨迹context browser.new_context(traceon) page context.new_page() # ... 执行逻辑 ... context.close() # 会生成trace文件或者用命令行方式录制playwright show-trace trace.zipTrace Viewer会展示完整的操作时间线包括每个步骤的DOM快照、网络请求、控制台日志、页面截图。排查问题时你直观地看到“页面在这一步长什么样子”“这一步发了什么请求”比自己盲猜要高效太多。我还习惯给关键操作加page.screenshot(pathdebug.png)脚本挂掉时截图能保留现场。配合try/except捕获异常时打印当前URL和相关元素数量基本能解决90%的偶发失败问题。5.3 监听页面请求与响应不只是调试更是数据抓手这里单独拿出来说因为监听网络请求对采集场景实在太重要了也是搜索热词里出现频率很高的点。Playwright提供两类网络监听方式page.on(request)和page.on(response)是只读的适合观察page.route()是可拦截修改的适合Mock。def handle_response(response): if api/order/detail in response.url: data response.json() print(data[order_no]) page.on(response, handle_response)有了page.on(response)你可以在用户操作页面的同时被动收集接口返回的JSON数据。这比解析页面HTML拿数据可靠得多因为接口JSON结构稳定而DOM结构可能被样式调整影响。实际采集时我经常同时开着多个监听一个负责订单接口一个负责列表接口各写各的解析函数实现“页面操作与数据采集解耦”。page.route()则更强大它可以在请求发出前改写URL或请求头也可以在响应返回时直接返回假数据def mock_route(route): if api/login in route.request.url: route.fulfill(status200, content_typeapplication/json, body{code:0}) else: route.continue_() page.route(**/*, mock_route)这个能力在测试场景里特别有用——你不想真的调用支付宝支付就可以直接mock掉支付接口的响应。6. 工程化实战pytest集成与自动化框架搭建6.1 pytest-playwright插件与fixture设计单独写脚本玩可以但要做回归测试或者持续定时任务就必须工程化。Playwright官方提供了pytest-playwright插件安装后会自动注入browser、context、page这几个fixturepip install pytest-playwrightdef test_order_flow(page): page.goto(https://example.com/login) page.get_by_label(用户名).fill(test) page.get_by_label(密码).fill(pass) page.get_by_role(button, name登录).click() page.get_by_role(button, name下单).click() expect(page.get_by_text(下单成功)).to_be_visible()这个插件的最佳实践是每个测试函数默认拿到一个全新的page测试之间天然隔离。背后是fixture的作用域机制——browser是session级所有测试共享context是function级page是function级。我在团队里推这套框架时发现有人直接把登录逻辑写死在每个测试里这样一旦登录页改版要改一堆地方。建议封装一个登录fixturepytest.fixture def logged_in_page(page): page.goto(https://example.com/login) # 执行登录 page.get_by_role(button, name登录).click() yield page def test_place_order(logged_in_page): logged_in_page.get_by_role(button, name下单).click()这样可以彻底隔离“前置条件”和“测试目标”测试用例更清晰。还能用storage_state保存登录态跳过每次重复登录的时间context browser.new_context(storage_stateauth.json)6.2 结合AI语义做不稳定元素定位搜索词里有人提到“playwright python AI语义 pytest”的组合这个方向我觉得值得展开讲讲。传统定位器依赖CSS、文本、角色但前端经常改文案比如按钮从“登录”改成“立即登录”测试就挂了。AI语义定位的思路就是让模型理解“我要点击登录按钮”这句话动态生成定位策略。我做过一个比较轻量的封装def ai_click(page, description: str): # 调用LLM接口返回推荐的定位策略表达式 locator_expr llm_generate_locator(description, page.content()) page.locator(locator_expr).click()这个方案本质是用LLM辅助生成定位表达式。它的好处是容错更强比如描述“页面右下角的悬浮客服按钮”LLM能结合页面文本和DOM结构给出一个合理的get_by_text(客服)或locator(.service-btn)。但它不适合高频执行因为每次调用都有成本更合理的落地方式是在测试失败重跑时用AI辅助分析而不是一开始就全链路AI定位。我的实际建议是把AI语义定位作为“兜底机制”而不是“主流程”。正常情况下用语义化角色定位元素改版导致定位失败时才把页面内容喂给模型让它给出修正后的定位器。这样兼顾稳定性和成本。6.3 测试报告、失败重跑与CI接入pytest生态成熟失败重跑用pytest-rerunfailures插件报告用pytest-html或Allure。我只说两个比较容易被忽略的点第一失败截图。用pytest的钩子函数自动抓截图pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: page item.funcargs.get(page) if page: page.screenshot(pathfscreenshots/{item.name}.png)这个钩子会在测试失败时自动保存一张页面截图配合Allure报告看失败现场特别直观。第二CI接入。Playwright支持在Jenkins/GitHub Actions里跑前提是CI环境里装有浏览器内核。Docker用户可以直接用官方镜像mcr.microsoft.com/playwright镜像里预装了浏览器和系统依赖省去安装步骤。7. 实际采集场景中的稳定性方案与合规边界7.1 面对网站风控机制的应对思路聊到采集就避不开“反爬”这个话题。搜索词里频繁出现“过瑞数”这类词说明很多朋友在真实项目里碰到了动态防护系统。不过我想先给一个大前提任何自动化技术都应该在合法授权范围内使用包括你自己拥有、或明确允许自动化访问的应用。在这个前提下理解风控机制仍然是有价值的因为做自动化测试也经常要模拟高防护环境下的用户路径。瑞数这类动态防护系统的核心逻辑是通过JS在浏览器里动态生成Cookie和Token并且在运行时做环境指纹和行为采集。最明显的特点就是同一个页面每次请求的Cookie都不是固定的纯协议层很难直接模拟协议破解需要还原JS加密逻辑维护成本极大。浏览器自动化的思路之所以有效是因为它让“真实的浏览器引擎”执行了这些JS服务器看到的是一个正常的执行环境。但我要泼一盆冷水浏览器自动化不等于“天衣无缝”。页面里可以通过检查navigator.webdriver标记、CDP连接特征、事件顺序等方式识别自动化环境。Playwright本身并没有内置反检测功能社区有playwright-stealth这类补丁方案不过它们的效果也是不断升级、不断被击穿你没有必要把这个当成长期稳定的救命稻草。7.2 浏览器指纹与伪装能做什么、不能做什么在合规前提下减少自动化痕迹的常见手段包括设置自定义UA、固定viewport、设置locale和timezone以及最重要的——放慢操作节奏。你不要一上来就抱着“绝对不被发现”的心态因为从法律和平台规则的角度绕过技术保护措施本身就有风险。站在工程角度合理的伪装是为了“不要触发风控误伤”而不是为了“恶意绕过”。我常用的初始化参数是这些context browser.new_context( user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, viewport{width: 1920, height: 1080}, localezh-CN, timezone_idAsia/Shanghai, )这些参数能让浏览器环境更贴近一个普通访客对校验不那么严格的站点来说足够。需要注意的“不能做”的部分不要试图伪造物理设备的GPU指纹、硬件并发数这类深层信息一方面效果有限另一方面确实越过了“合理测试”的界线。7.3 从“过瑞数”热搜说起自动化工程的合法边界搜索趋势里能很明显看到“playwright过瑞数”这类词的搜索量一直不低。这背后反映的是真实工程中的一条铁律当协议解析成本超过合理范围时用浏览器自动化替代反而更省力。但我想认真提醒一句评估技术方案时不要只看“能不能绕过去”还要看“允不允许绕过去”。如果你的目标是采集公开数据优先确认网站是否有开放API如果你的目标是测试自己公司的产品那造一些自动化流量完全没问题如果你面对的站点没有明确授权我建议不要用Playwright去做突破防护的采集这不只是道德问题也是实际的法律风险。在这个前提下研究动态防护的原理对你仍然是加分的。因为在做自己产品的高并发测试、安全巡检时你会需要理解“普通用户与自动化程序的区别”从而验证产品本身的风控能力是否合格。这是自动化工程师更健康的成长路径。说回Playwright本身我个人的体会是它最大的价值不在某个单点功能而在于把“真实浏览器模拟”这件事打磨到了足够工程化的水平。自动等待、Codegen、Trace Viewer、网络监听每一个设计都在降低自动化脚本的维护成本。如果你正准备入坑先跑通最小脚本再用Codegen录一个业务流程然后加上pytest把它变成可重复执行的用例——这一条链路走下来你就能感受到它和其他自动化工具的差距。最后提醒一点生产环境的脚本稳定性永远比炫技重要保持简单的结构、明确的等待方式比引入各种花哨的封装更能长久运行。
返回列表