1. 从零开始为什么选择Playwright如果你之前用过Selenium或者Puppeteer来做网页自动化那你肯定遇到过不少头疼的事儿。比如页面元素加载太慢脚本等半天没反应或者动态网页内容用传统选择器死活定位不到再或者想模拟多用户同时操作结果会话全混在一起了。这些问题我在做自动化测试和爬虫项目时几乎天天碰到。直到我开始用Playwright才感觉真正找到了一个“现代”的解决方案。Playwright是微软开源的一个浏览器自动化库它和Selenium最大的不同在于它是“原生”为现代Web设计的。它不像Selenium那样通过WebDriver协议去“遥控”浏览器而是直接通过DevTools协议与Chromium、Firefox、WebKit这些浏览器内核对话。这意味着它的速度更快能做的事情也更底层、更强大。举个最简单的例子Playwright可以原生地等待网络请求完成、拦截和修改请求/响应、甚至模拟移动设备、地理位置和权限这些在Selenium里要么很麻烦要么根本做不到。对于Python开发者来说Playwright的Python绑定做得非常友好。它的API设计非常直观读起来就像在描述你要做的事情。比如page.goto()去访问页面page.fill()去填表单page.wait_for_selector()去等待元素几乎不需要额外解释。而且它默认就是异步友好的你可以用async/await来写高性能的并发脚本也可以用同步API来写简单的脚本上手门槛很低。我刚开始用的时候最让我惊喜的是它的“智能等待”。以前写Selenium脚本到处是time.sleep(10)这种硬编码的等待既不可靠又低效。Playwright内置了自动等待机制大部分操作比如clickfill在执行前都会自动等待元素变得可操作可见、可点击、可输入。这大大减少了脚本的脆弱性写出来的代码稳定多了。所以无论你是想写自动化测试脚本还是做数据抓取、网页监控或者只是简单地想自动化一些重复的网页操作Playwright都值得你花时间了解一下。它不仅仅是另一个自动化工具它更像是一个完整的浏览器操控框架。2. 环境搭建与第一个脚本5分钟跑起来说再多不如动手试一下。我们先来看看怎么把Playwright的环境搭起来并写出第一个能跑的脚本。整个过程非常快我保证你5分钟内就能看到浏览器自动打开并操作。2.1 安装Playwright安装Playwright主要分两步安装Python包然后安装浏览器驱动。别担心Playwright把第二步也简化成了一条命令。首先用pip安装Playwright的Python包。我建议创建一个新的虚拟环境来做这件事避免和你现有的项目环境冲突。pip install playwright安装完Python包后我们需要安装它支持的浏览器内核Chromium, Firefox, WebKit。Playwright提供了一个命令行工具来完成这个playwright install这条命令会下载所有三个浏览器Chromium、Firefox、WebKit的最新兼容版本。如果你只想安装其中一个比如Chromium最常用可以运行playwright install chromium。下载可能需要一点时间取决于你的网速。这些浏览器会被安装到用户目录下的一个缓存文件夹里不会干扰到你系统里已经安装的Chrome或Firefox。2.2 编写并运行第一个脚本环境准备好了我们来写一个最简单的脚本打开浏览器访问百度搜索一个关键词然后截个图保存。我们先用同步API来写这样逻辑更直观。from playwright.sync_api import sync_playwright def run(): # 1. 启动Playwright管理器 with sync_playwright() as p: # 2. 启动Chromium浏览器headlessFalse表示显示浏览器窗口 browser p.chromium.launch(headlessFalse) # 3. 创建一个新的浏览器页面标签页 page browser.new_page() # 4. 导航到百度 page.goto(https://www.baidu.com) # 打印当前页面标题 print(f页面标题: {page.title()}) # 5. 定位搜索框输入关键词 # Playwright的定位器Locator非常强大这里用CSS选择器 search_box page.locator(input#kw) search_box.fill(Playwright 自动化测试) # 6. 定位搜索按钮并点击 search_button page.locator(input#su) search_button.click() # 7. 等待搜索结果页面加载这里等待一个结果元素出现 page.wait_for_selector(div.result.c-container, timeout10000) # 8. 对搜索结果页进行截图 page.screenshot(pathbaidu_search_result.png, full_pageTrue) print(截图已保存为 baidu_search_result.png) # 9. 等待几秒方便观察然后关闭浏览器 page.wait_for_timeout(3000) browser.close() if __name__ __main__: run()把这段代码保存为first_script.py然后直接运行python first_script.py。你会看到一个Chromium浏览器窗口自动打开访问百度输入文字点击搜索最后把长长的搜索结果页完整地截图保存下来。这里有几个关键点我想强调一下sync_playwright()上下文管理器这是使用同步API的标准方式它能确保在代码块结束时正确地关闭所有浏览器和上下文资源避免资源泄漏。这就像我们用with open()来操作文件一样是个好习惯。launch(headlessFalse)headless是无头模式。设为False时你会看到浏览器界面适合调试和观察。当你脚本稳定后在生产环境可以设为True让它在后台静默运行效率更高。page.locator()这是Playwright最核心的元素定位方式。它返回一个Locator对象这个对象代表一个随时可以用于操作的页面元素。它的选择器语法非常丰富支持CSS、XPath甚至还可以根据文本内容定位我们后面会详细讲。wait_for_selector这是显式等待。虽然Playwright的操作有自动等待但在页面跳转后比如点击搜索等待新页面的特定元素出现是一个更稳妥的做法。timeout参数设置了最大等待时间毫秒。第一次运行成功后你可以试着把headless改成True再跑一次看看是不是没有界面弹出但截图依然生成了。这就是无头模式的魅力。3. 核心交互操作像真人一样操作网页现在我们已经能让浏览器动起来了接下来要学习如何精确地控制它模拟真人用户的各种操作。这部分是Playwright最实用、最体现其设计精妙的地方。3.1 精准导航与等待告别time.sleep访问页面用page.goto()这个很简单。但网页加载不是一瞬间的事我们需要等待页面到达某种“就绪”状态。Playwright的goto()方法内置了强大的等待控制。# 基础导航 page.goto(https://example.com) # 高级导航等待更严格的加载条件 page.goto( https://example.com, wait_untilnetworkidle, # 等待到网络空闲500ms内没有超过2个网络请求 timeout30000 # 超时时间设为30秒 )wait_until参数是关键它有以下几个选项load等待load事件触发即页面完全加载包括图片、样式等。这是默认值但对于单页应用SPA可能过早。domcontentloaded等待DOMContentLoaded事件触发即HTML文档被完全加载和解析不等待样式表、图片等。速度更快。networkidle我强烈推荐在大多数情况下使用这个。它等待到网络基本空闲通常指至少500毫秒内没有超过2个网络请求。这对于加载了大量异步资源的现代网页如React, Vue应用非常有效能确保动态内容已经加载完毕。除了等待页面加载更多时候我们需要等待页面上的特定元素。这就是page.wait_for_selector()和Locator的等待方法大显身手的时候。# 等待一个元素出现在DOM中默认状态 page.wait_for_selector(.modal, timeout5000) # 等待元素变为可见状态display: block 且 visibility: visible page.wait_for_selector(.modal, statevisible) # 等待元素从DOM中消失 page.wait_for_selector(.loading-spinner, statehidden) # 更优雅的方式使用Locator的等待方法 submit_button page.locator(button:has-text(提交)) # 等待这个按钮变为可点击状态enabled且可见 submit_button.wait_for(stateattached) # 或者直接进行操作操作本身会自带等待 submit_button.click() # click()内部会先等待元素可点击我的经验是尽量使用Locator对象的方法如click(),fill()因为它们内部已经包含了智能等待。只有在页面状态发生重大变化如导航、弹窗出现后才使用page.wait_for_selector进行显式等待。完全避免使用time.sleep它是脚本脆弱的主要根源。3.2 元素定位与操作不止是CSS选择器定位元素是自动化的基石。Playwright的定位器Locator支持多种策略非常灵活。# 1. CSS选择器 (最常用) page.locator(button.submit) page.locator(div#main ul.list li:first-child) # 2. XPath (处理复杂结构时有用) page.locator(//button[idsubmit]) page.locator(//li[contains(text(), 项目)]) # 3. 按文本内容定位 (非常实用) page.locator(text登录) # 精确文本匹配 page.locator(text/Log\s*in/i) # 使用正则表达式不区分大小写匹配“Log in” page.locator(:has-text(欢迎)) # 选择包含“欢迎”文本的任何元素 # 4. 按属性定位 page.locator([data-testidlogin-button]) # 常用于测试 page.locator(input[placeholder请输入用户名]) # 5. 组合定位 # 找到包含“购物车”文本的div再找它里面的span page.locator(div:has-text(购物车) span.count)定位到元素后就可以进行各种操作了# 点击 page.locator(button).click() # 双击 page.locator(item).dblclick() # 右键点击 page.locator(menu).click(buttonright) # 输入文本会先清空输入框 page.locator(input#username).fill(my_user) # 模拟逐个字符输入更真实会触发键盘事件 page.locator(input#username).type(my_user, delay100) # 每个字符间隔100毫秒 # 获取元素属性、文本、内部HTML value page.locator(input#username).input_value() text page.locator(h1.title).text_content() inner_html page.locator(div.content).inner_html() # 勾选复选框、单选框 page.locator(input[typecheckbox]).check() page.locator(input[typeradio]).check() # 选择下拉框选项 page.locator(select#city).select_option(label北京) # 按显示文本 page.locator(select#city).select_option(valuebeijing) # 按value值 page.locator(select#city).select_option(index2) # 按索引一个高级技巧处理动态元素和列表。现代网页很多内容是JS动态生成的列表项可能没有固定的ID。这时可以用locator配合nth、filter等方法。# 获取所有商品列表项 product_items page.locator(ul.products li) # 获取列表数量 count product_items.count() print(f找到 {count} 个商品) # 点击第三个商品 product_items.nth(2).click() # 找到文本包含“特价”的商品并点击 product_items.filter(has_text特价).click()3.3 处理表单与复杂输入填写表单是自动化的常见任务。除了简单的fill()我们还需要处理文件上传、富文本编辑器等复杂场景。基础表单填写我们已经见过了就是一连串的fill()和click()。对于富文本编辑器如TinyMCE, CKEditor它们通常嵌套在一个iframe里并且内容是可编辑的div而不是input。# 1. 先定位到富文本编辑器的iframe frame page.frame_locator(iframe.cke_wysiwyg_frame) # 2. 在iframe内部定位可编辑区域并填充内容 editable_div frame.locator(body[contenteditabletrue]) editable_div.fill(这里是富文本内容) # 或者如果你想保留原有格式使用innerHTML # editable_div.evaluate(el el.innerHTML p新内容/p)文件上传是另一个难点。Playwright处理起来非常优雅它允许你直接设置文件输入框的值而无需模拟复杂的点击操作。# 假设有一个 input typefile idavatar file_input page.locator(input#avatar) # 设置文件路径可以是单个文件也可以是文件列表数组 file_input.set_input_files(path/to/my/avatar.png) # 上传多个文件 file_input.set_input_files([file1.png, file2.jpg]) # 如果要清空已选择的文件 file_input.set_input_files([])处理弹窗和对话框。网页上的alert,confirm,prompt对话框会阻塞脚本执行。Playwright允许你在它们弹出前就进行监听和处理。# 监听并接受一个confirm对话框 page.on(dialog, lambda dialog: dialog.accept()) # 然后触发会弹出confirm的操作 page.locator(button.delete).click() # 对话框会自动被接受脚本继续执行 # 更精细的控制监听并获取对话框消息 def handle_dialog(dialog): print(f对话框消息: {dialog.message}) if 确认删除 in dialog.message: dialog.accept() else: dialog.dismiss() page.on(dialog, handle_dialog)3.4 截图与录屏不仅仅是截图截图功能在测试中用于视觉对比在爬虫中用于保存证据非常有用。Playwright的截图功能很强大。# 1. 全页面截图自动滚动拼接 page.screenshot(pathfull_page.png, full_pageTrue) # 2. 对特定元素截图 page.locator(div.chart).screenshot(pathchart.png) # 3. 截图时隐藏某些元素比如浮动广告 page.screenshot( pathpage_without_ad.png, mask[page.locator(div.floating-ad)] # 被mask的元素在截图中会显示为粉红色方块默认 ) # 4. 指定截图区域像素坐标 page.screenshot(pathregion.png, clip{x: 100, y: 100, width: 400, height: 300}) # 5. 设置截图质量仅对jpeg有效 page.screenshot(pathscreenshot.jpg, typejpeg, quality80)更酷的是录屏功能。你可以录制整个自动化操作过程生成视频这对于调试复杂流程或者生成演示材料太有帮助了。# 启动浏览器时开启录屏 browser p.chromium.launch(headlessFalse) context browser.new_context(record_video_dirvideos/) # 指定视频存放目录 page context.new_page() # ... 执行你的所有操作 ... # 关闭上下文时视频会自动保存 context.close() # 视频文件会保存在 videos/ 目录下文件名是随机的4. 高级实战浏览器上下文与多会话管理当你需要模拟多个用户同时操作或者需要隔离不同的测试会话时Browser Context浏览器上下文就是你的王牌。你可以把它理解为一个独立的隐身浏览器会话。每个上下文拥有独立的cookie、localStorage、sessionStorage和缓存但它们共享同一个浏览器进程非常高效。4.1 创建与使用独立上下文from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) # 创建第一个用户的上下文如普通用户 user1_context browser.new_context() user1_page user1_context.new_page() user1_page.goto(https://example.com) user1_page.fill(#username, user1) # user1的登录状态只存在于user1_context中 # 创建第二个用户的上下文如管理员 user2_context browser.new_context() user2_page user2_context.new_page() user2_page.goto(https://example.com) user2_page.fill(#username, admin) # user2的会话与user1完全隔离 # 关闭上下文 user1_context.close() user2_context.close() browser.close()这个特性对于测试多用户交互的场景如聊天应用、协作编辑或者需要并行执行多个独立任务时极其有用。4.2 会话持久化跳过重复登录在自动化测试中我们经常需要在多个测试用例中保持登录状态。每次都走一遍登录流程既慢又没必要。Playwright的storage_state功能可以完美解决这个问题。第一步保存登录状态。from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context() page context.new_page() # 执行登录操作 page.goto(https://your-app.com/login) page.fill(input[nameemail], testexample.com) page.fill(input[namepassword], password123) page.click(button[typesubmit]) # 等待登录成功跳转到首页或仪表盘 page.wait_for_url(**/dashboard) # 关键步骤将当前上下文的存储状态cookies, localStorage保存到文件 context.storage_state(pathauth_state.json) browser.close()运行上面的脚本后你会得到一个auth_state.json文件里面包含了登录后的所有会话数据。第二步加载登录状态直接访问受保护页面。with sync_playwright() as p: browser p.chromium.launch(headlessTrue) # 创建新上下文时直接加载之前保存的状态文件 context browser.new_context(storage_stateauth_state.json) page context.new_page() # 无需再次登录直接访问需要认证的页面 page.goto(https://your-app.com/dashboard) # 此时页面应该已经是登录状态 print(page.title()) # 输出仪表盘标题 # 可以继续执行其他需要登录状态的操作... page.click(text我的订单) browser.close()这个技巧能极大提升测试套件的执行速度。你可以把登录状态保存为不同的文件如admin_state.json,user_state.json来快速切换不同角色的会话。4.3 模拟设备与地理位置Playwright还能轻松模拟不同设备手机、平板和地理位置这对测试响应式网页和基于位置的服务非常关键。from playwright.sync_api import sync_playwright # 方法1使用Playwright预定义的设备描述符 iphone_12 p.devices[iPhone 12 Pro] with sync_playwright() as p: browser p.chromium.launch(headlessFalse) # 创建模拟iPhone 12的上下文 context browser.new_context(**iphone_12) page context.new_page() page.goto(https://example.com) # 此时视口大小、User-Agent等都是iPhone 12的 page.screenshot(pathiphone_view.png) # 方法2自定义上下文属性 with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context( viewport{width: 375, height: 812}, # 手机竖屏尺寸 user_agentMozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) ..., localezh-CN, # 设置语言区域 timezone_idAsia/Shanghai, # 设置时区 geolocation{longitude: 116.397128, latitude: 39.916527}, # 设置北京地理位置 permissions[geolocation] # 授予地理位置权限 ) page context.new_page() page.goto(https://maps.example.com) # 网站会认为你是一台位于北京的中文iPhone5. 集成自动化测试从脚本到测试用例Playwright不仅仅是一个自动化工具它本身就是一个强大的测试框架。它提供了断言、测试报告、并行执行等全套测试功能。让我们看看如何把刚才写的脚本改造成一个结构化的测试用例。5.1 使用Playwright Test RunnerPlaywright为Python提供了专门的pytest插件让编写测试变得非常规范。首先安装测试插件pip install pytest-playwright。然后Playwright提供了一个命令行工具来初始化测试环境playwright install如果之前没装过浏览器 和playwright codegen可以录制生成测试脚本但这里我们手写。创建一个测试文件test_login.pyimport re from playwright.sync_api import Page, expect def test_successful_login(page: Page): 测试用户成功登录 # 1. 导航到登录页 page.goto(https://your-app.com/login) # 2. 填写登录表单 page.fill(input[nameemail], valid_userexample.com) page.fill(input[namepassword], correct_password) page.click(button[typesubmit]) # 3. 断言登录后应跳转到仪表盘且页面包含欢迎语 expect(page).to_have_url(re.compile(r.*/dashboard)) welcome_msg page.locator(h1.welcome) expect(welcome_msg).to_be_visible() expect(welcome_msg).to_have_text(欢迎回来valid_user!) def test_login_with_invalid_password(page: Page): 测试使用错误密码登录失败 page.goto(https://your-app.com/login) page.fill(input[nameemail], valid_userexample.com) page.fill(input[namepassword], wrong_password) page.click(button[typesubmit]) # 断言应显示错误提示信息 error_toast page.locator(.toast.error) expect(error_toast).to_be_visible() expect(error_toast).to_contain_text(密码错误) # 断言URL不应改变仍然在登录页 expect(page).to_have_url(https://your-app.com/login)然后在命令行使用pytest运行测试# 运行所有测试 pytest # 运行特定文件 pytest test_login.py # 运行并显示详细结果 pytest -v # 在无头模式下运行默认 # 在有头模式下运行用于调试 pytest --headed # 在指定浏览器上运行如 Firefox pytest --browser firefoxPlaywright Test Runner会自动管理浏览器的启动和关闭并为每个测试用例提供一个干净的page对象。expect断言库是专门为Web内容设计的提供了to_be_visible,to_have_text,to_have_value等非常语义化的断言方法比传统的assert语句更强大、错误信息更清晰。5.2 配置与最佳实践为了高效管理测试我们通常需要一个配置文件pytest.ini或playwright.config.py。这里我推荐使用playwright.config.py因为它功能更全。# playwright.config.py import pytest from playwright.sync_api import Playwright pytest.fixture(scopesession) def browser_context_args(browser_context_args, playwright: Playwright): # 为所有测试上下文设置默认选项 iphone playwright.devices[iPhone 12 Pro] return { **browser_context_args, **iphone, # 模拟iPhone record_video_dir: test-results/videos/, # 录制失败测试的视频 viewport: {width: 1920, height: 1080}, } pytest.fixture(scopefunction) def context(browser, browser_context_args): # 每个测试函数一个独立的上下文 context browser.new_context(**browser_context_args) yield context context.close() pytest.fixture(scopefunction) def page(context): # 每个测试函数一个独立的页面 page context.new_page() yield page page.close()在测试中你可以直接使用page这个fixture。配置里我们做了几件事为所有测试模拟了iPhone设备。设置了录制视频的目录通常配合--video on参数使用只对失败测试录制。通过fixture确保了每个测试之间的隔离性。一些我踩过坑后总结的最佳实践选择器策略优先使用data-testid属性。让前端开发在元素上加上data-testidlogin-button这样你的选择器page.locator([data-testidlogin-button])既稳定又语义化不受CSS类名或结构变化的影响。等待策略多用expect断言进行等待如expect(locator).to_be_visible()。它比单纯的wait_for_selector更符合测试语义。并行执行使用pytest-xdist插件可以并行运行测试大幅缩短执行时间。命令是pytest -n auto。失败重试与截图在配置中或命令行添加--screenshot on和--video on可以在测试失败时自动截图和录屏对调试帮助巨大。全局前置登录利用storage_state和pytest的session作用域fixture实现一次登录所有测试复用。把登录逻辑写在一个conftest.py文件的fixture里。# conftest.py import pytest from playwright.sync_api import Page, BrowserContext pytest.fixture(scopesession) def auth_context(browser) - BrowserContext: 创建一个已登录的上下文供所有测试复用 context browser.new_context() page context.new_page() # ... 执行登录步骤 ... page.goto(/login) page.fill(#username, testuser) page.fill(#password, testpass) page.click(button[typesubmit]) page.wait_for_url(**/dashboard) # 保存状态到上下文后续从这个上下文创建的页面都自带登录状态 # 注意这里我们不需要保存到文件直接使用这个context即可 yield context context.close() pytest.fixture def authed_page(auth_context) - Page: 每个测试获取一个已登录的新页面 page auth_context.new_page() yield page page.close()这样在你的测试函数中只需要使用authed_page这个fixture拿到的就是已经登录好的页面对象可以直接测试登录后的功能避免了每个测试都重复登录。这种架构能让你的测试套件既健壮又高效。