
最近我接到一个页面巡检的活儿目标是用脚本每天定时打开一个内部系统点几个菜单、导出几份报表。麻烦的地方在于这个系统只在微软 Edge 里表现正常换成其他内核浏览器要么样式错位要么按钮点了没反应。我一开始想用 Selenium 凑合后来把技术方案换成了 Python 版的 Playwright目标锁定微软 Edge。这篇《Playwright(python)微软浏览器自动化教程一》就记录一下从环境准备到元素定位的全过程适合写过一点 Python、想用真实浏览器做自动化的朋友参考。1. 为什么把自动化目标锁定在微软 Edge1.1 先搞清楚微软 Edge 和 Chromium 的关系可能有人会觉得“微软浏览器”不就是 Windows 自带的那个 Edge 嘛和程序员常用的 Chrome 有什么关系这里有一个关键背景现在的微软 Edge 已经全面转向 Chromium 内核你可以把它理解成 Chrome 的同源兄弟。这意味着很多针对 Chrome 的自动化方案换到 Edge 上同样成立只要驱动名和启动参数稍微调整一下。我选中 Edge 而不是 Chrome 的直接原因很简单我那个内部系统在 Edge 下最稳定用户平时都用 Edge 办公。自动化脚本出差错时会直接影响业务与其让脚本跑在一个用户根本不用的浏览器上不如老老实实对齐生产环境。这也是我在很多项目里养成的一个习惯——先看目标用户用什么浏览器再决定用哪个内核做自动化而不是为了技术噱头去凑一个酷炫方案。1.2 对比 Selenium 和 requestsPlaywright 赢在哪很多老读者第一反应是 Selenium毕竟它出现得早、学习资料多。但我这次没有选它主要是受够了几个问题Selenium WebDriver 需要额外下载一个和浏览器版本严格对应的驱动文件浏览器一旦自动升级驱动就废了。Edge 的驱动版本号匹配问题尤其容易踩我见过不止一次运行时提示“session not created”的报错排查半天才发现是驱动和浏览器版本差了一位。Playwright 的方案就好很多它内置了浏览器驱动管理当传入channelmsedge时会直接复用系统已安装的微软 Edge不需要手动下载任何驱动。哪怕 Edge 自动升级脚本一般也能继续跑省掉了版本匹配的整段痛苦。另一个优势是自动等待机制这个后面我会单独用一大节来讲简单说就是 Playwright 知道元素什么时候能点、什么时候能填不用在代码里到处塞time.sleep。那有人会问直接用requests模拟请求不是更轻量吗我确实这么干过但遇到单页应用就崩了。很多现代前端页面需要执行 JavaScript 才能渲染出按钮和表格接口返回的数据和你在浏览器里看到的 DOM 根本对不上。这种时候必须上真浏览器Playwright 出来就是干这个的。1.3 用 Python 而非 Node.js 的理由Playwright 官方有 Python、Node.js、Java 等多个语言版本语法基本一样。我选择 Python 是因为我后续要把抓取到的页面数据直接拿来加工Python 的 pandas、openpyxl 处理报表非常顺手。如果你是纯前端背景用 Node.js 版本也完全没问题教程里的核心概念都能通用。2. 环境准备装完 Playwright 不等于能跑浏览器2.1 虚拟环境是第一道护城河我讲自动化教程通常第一步都是强调虚拟环境。这里不是老生常谈而是因为 Playwright 的依赖涉及浏览器可执行文件和系统库直接装进系统 Python 环境过两个月换项目时容易产生版本冲突。我习惯这样操作python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate pip install playwright安装完成后在命令行输入playwright --version如果能看到版本号说明命令已经进入 PATH 了。Windows 下如果提示“不是内部或外部命令”大概率是当前终端没有重启或者没有激活虚拟环境跟 Python 本身没关系。2.2 install 浏览器那一步到底装了啥初次接触 Playwright 的人最容易在这个地方卡住明明pip install playwright成功了一运行脚本却报错说找不到浏览器。原因在于Playwright 库本身和浏览器可执行文件是两回事你还需要执行一次下载命令playwright install chromium不过这里有个很多人不知道的点如果你确定只用微软 Edgechannelmsedge会直接复用系统里已经装好的 Edge并不需要下载 Chromium。那为什么我仍然建议跑一次playwright install呢因为 Windows 下有时候缺运行库Linux 下更是经常需要额外补系统依赖。跑一下能提前把这些问题暴露出来免得写了大段代码才发现环境不过关。如果系统提示缺动态库一类的错误可以尝试playwright install-deps chromium这个命令会自动安装浏览器运行所需的系统依赖包在 Linux 服务器上尤其好用。Windows 基本用不上但知道这个命令存在遇到问题时心态会稳很多。2.3 同步 API 与异步 API 的选择Playwright 的 Python 版本提供了两套 API同步 APIsync_playwright和异步 APIasync_playwright。我建议初学者直接学同步 API理由很简单代码好读、调试直观、不容易在事件循环上翻车。from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(channelmsedge, headlessFalse) page browser.new_page() page.goto(https://example.com) print(page.title()) browser.close()异步 API 主要用于需要并发控制大量浏览器任务的场景比如同时开 20 个页面抓数据。这种玩法确实更吃内存和 CPU但不是入门阶段应该优先考虑的。我见过不少新人为了“显得高级”一上来就上async结果连asyncio.run的作用都没搞清楚最后被报错淹没。3. 启动 Edge 的完整脚本与 launch 参数拆解3.1 那个能跑的 Hello World 代码长什么样上一节的脚本虽然短却已经把第一次启动浏览器的所有关键部件都提到了。我再写一个更完整的版本注释也更细致一点from playwright.sync_api import sync_playwright with sync_playwright() as p: # 核心指定 channel 为 msedge复用系统已安装的微软 Edge browser p.chromium.launch( channelmsedge, headlessFalse # 有头模式方便看到浏览器窗口 ) # 新建一个标签页 page browser.new_page() # 打开目标地址 page.goto(https://example.com) # 获取页面标题 title page.title() print(f页面标题是: {title}) # 保存整页截图 page.screenshot(pathfirst_screenshot.png) # 关闭浏览器释放资源 browser.close()这里有一个习惯我想强调一下browser.close()要写在脚本结尾保证浏览器进程被正常回收。如果脚本中途报错with块能保证 Playwright 的驱动进程被自动清理但browser.close()仍然是最稳妥的收尾动作。新手经常为了看效果不关浏览器结果脚本反复跑时残留一堆进程把系统拖得越来越重。3.2 为什么启动的是 Edge 却感觉像 Chrome有读者反馈说明明在launch里指定了channelmsedge跑起来却好像还是 Chromium 的样子。这里需要理解 Playwright 的启动逻辑它内部的浏览器类型名是chromium这个名指的是 Chromium 内核家族你传入channelmsedge之后实际启动的才是“微软 Edge 加载了这个脚本”。所以代码里写p.chromium.launch(...)并没有错这是 API 层面的命名约定。如果你真的想确认启动的是 Edge 而不是开源 Chromium可以在页面执行一段 JavaScript 检查一下# 在页面里读取浏览器 userAgent ua page.evaluate(() navigator.userAgent) print(ua)正常会看到Edg/字样的版本信息。这个方法在排障时非常管用。3.3 无头模式 vs 有头模式headlessFalse表示有头模式也就是你亲眼能看到浏览器窗口弹出。这在开发调试阶段几乎必须因为你能直观看到点击有没有生效、页面有没有弹窗。等脚本稳定之后再改成headlessTrue把浏览器藏到后台去跑适合部署到 Linux 服务器做定时任务。早期的无头模式旧版 headless有一个著名的问题很多网站能检测到浏览器处于 headless 状态故意返回不同的页面形态。Playwright 的新无头模式已经改进很多但我在实际项目里仍然遇到过页面在无头模式下比有头模式少渲染一块表格的情况。所以我的建议是关键业务流程第一次上线时务必在有头模式下用肉眼确认一遍再切无头。3.4 用 persistent context 保留登录状态普通launch启动的浏览器是临时环境关掉之后所有 Cookie、登录状态都清空。如果你想自动化一个需要扫码登录的网站每次都重新扫码会崩溃。这时候可以用launch_persistent_context给浏览器指定一个用户数据目录登录一次之后状态会被保存在本地下次启动自动带上。from playwright.sync_api import sync_playwright with sync_playwright() as p: context p.chromium.launch_persistent_context( user_data_dir./edge_profile, channelmsedge, headlessFalse ) page context.new_page()这个模式用起来和普通launch差别不大但有个细节要提醒不要用你平时日常浏览的那个 Edge 用户目录当user_data_dir否则自动化脚本可以读取到你真实账号的 Cookies这既危险也容易在测试时误操作。建议单独建一个edge_profile文件夹专门给自动化脚本使用。4. 元素定位与交互locator 是 Playwright 的精华4.1 为什么不推荐一上来就写 XPath很多从 Selenium 转过来的同学习惯性就是find_element_by_xpath。在 Playwright 里最推荐的定位方式是 locator它不是马上返回一个具体元素而是一个“定位规则”等到你要点击或断言的时候它才会真正去页面里找元素。这种延迟求值配合自动等待让脚本稳定了很多。我举一个对比场景你点击一个按钮之后页面出现一个新的弹窗。在 Selenium 的老写法里你经常要手动写显式等待直到弹窗可见在 Playwright 里你只需在点击弹窗元素前定义好 locatorPlaywright 会自动等待这个元素满足可操作条件。4.2 几种常用定位方式的对比我用表格整理一下日常最常用的定位 API方便你对照查阅定位方式写法示例适用场景文本page.get_by_text(报表中心)链接、按钮、纯文本元素占位符page.get_by_placeholder(请输入用户名)输入框标签文本page.get_by_label(用户名)有 label 关联的表单项角色page.get_by_role(button, name登录)语义清晰的按钮或链接CSSpage.locator(#submit-btn)有稳定 id/class 的元素XPathpage.locator(xpath//div[classtip])结构复杂且无更优选择其中get_by_role是我现在的主力因为很多页面的按钮是可访问性语义良好的元素它的定位结果跟人类看页面的方式接近比 CSS 类名变化来得稳定。如果页面只改了样式类名角色定位往往不受影响。4.3 用一个完整用例串起 fill、click、select_option下面这段代码模仿一个内部后台系统的登录和查询流程我把常见的表单交互都放了进来。具体页面结构请你按实际情况调整重点是看交互方法的组合方式from playwright.sync_api import sync_playwright, expect with sync_playwright() as p: browser p.chromium.launch(channelmsedge, headlessFalse) page browser.new_page() # 打开登录页 page.goto(https://example.com/login) # 填写表单 page.get_by_placeholder(请输入用户名).fill(test_user) page.get_by_placeholder(请输入密码).fill(pass123) # 勾选“记住我” page.get_by_label(记住我).check() # 点击登录 page.get_by_role(button, name登录).click() # 断言进入了后台首页 expect(page).to_have_url(https://example.com/dashboard) # 打开报表查询页 page.get_by_text(报表中心).click() # 选择查询条件 page.locator(select[namereport_type]).select_option(daily) page.get_by_role(button, name查询).click() # 等待表格数据出现 table page.locator(table.report-table) expect(table).to_be_visible()需要注意fill()会先清空输入框再填入内容如果输入框本身带默认值这是最省事的写法。check()用于复选框和单选框。select_option()适用于原生下拉框自定义前端下拉框则大多需要先点击再选择选项。4.4 iframe 和 shadow DOM 再说几句现代后台系统免不了出现 iframe比如报表预览、第三方支付页面。Playwright 处理 iframe 的方式比较优雅用frame_locator在 iframe 内部继续做 locatoriframe page.frame_locator(iframe[title报表预览]) iframe.get_by_role(button, name导出).click()关于 shadow DOMPlaywright 的定位器默认就能穿透大多数开放模式的 shadow root这点比不少同类工具省心。不过如果你的页面是封闭模式 shadow DOM那定位起来依旧麻烦通常得依靠页面提供的调试接口或者改变测试策略。遇到时可以先用 Chrome 开发者工具确认一下元素到底是不是被 shadow root 包着别在定位器写法上做无谓的猜测。5. 自动等待机制与随机失败排查5.1 Playwright 的自动等待到底在等什么这是我认为 Playwright 最值钱的功能也最常被低估。每当你对一个 locator 执行.click()或.fill()时Playwright 并不会立刻去操作而是先检查这个元素是否满足“可操作性”条件这些条件包括元素是否可见、是否稳定位置和大小不再变化、是否处于启用状态、是否能够接收鼠标键盘事件。你可以把这一套检查理解成“真人操作流程”你看到按钮鼠标移过去按钮还在那里你才点下去。这套检查有默认超时时间通常是 30 秒超出就抛异常。正是因为这些自动等待的存在很多time.sleep(2)才能稳定的脚本在 Playwright 里可以直接删掉。5.2 自动等待、显式等待和强制等待的取舍虽然自动等待很强大有些场景仍然需要你手动介入。我把三种等待方式对比一下等待方式写法使用建议自动等待locator.click()自带默认首选显式等待page.wait_for_url(...)/locator.wait_for()页面跳转、AJAX 渲染后强制等待time.sleep(2)少用实在没有事件可监听时我的经验是页面跳转尽量用page.expect_navigation()或者wait_for_url因为跳转是浏览器级事件比傻等稳定得多。表格数据加载完则用expect(table).to_contain_text(某数据)让断言代替盲等。5.3 随机失败的排查链路脚本明明上午还在稳定运行下午突然报“元素不可见”或者“超时”这是自动化最磨人的问题。我梳理一下自己的排查顺序分享给你们参考第一步出问题后先截图看看页面当时是什么状态。是根本没加载出来还是弹了个遮罩挡住按钮。第二步检查页面是不是有接口很慢。打开开发者工具里的 Network 面板找到那个支配着按钮出现的请求看它的响应时间。第三步判断是否在 iframe 或遮罩层里。元素可能其实存在只是被别的图层盖住了所以 Playwright 认为“不可见”。第四步把page.set_default_timeout(10000)调大一点。默认 30 秒其实不算短了但如果你的业务接口要 40 秒才返回超时就不可避免不如直接调成和业务耗时匹配的数值。排查链路中最重要的是不要一上来就怀疑 Playwright 的定位器有问题。我见过太多人把get_by_role(button, name登录)换成了xpath换了一圈发现是页面弹窗遮罩没消失。定位方式背了很多锅真正的元凶是页面交互时序。6. 调试三板斧录制器、Trace、截图6.1 codegen 一键录制脚本刚开始写自动化时你很难凭肉眼判断一个按钮应该用什么定位方式。Playwright 自带录制器可以在图形界面里操作自动生成 Python 代码。它有两个用途一是快速生成脚本雏形二是帮你反推一个真实页面的元素定位写法。在命令行执行playwright codegen --channel msedge随后会打开一个微软 Edge 窗口和一个代码窗口你在浏览器里点按钮、填表单、跳页面代码窗口就会实时生成对应的 Python 脚本。生成的代码虽然不能直接用但它能把最难猜的定位器替你猜好剩下的就是整理和补充断言。6.2 Trace Viewer 回放脚本过程脚本运行失败时截一张图往往不够因为你看不到中间发生了什么。Playwright 的 Trace 功能可以记录完整的时间线包括每个操作前后的 DOM 快照、网络请求、控制台日志。启用方式是在 context 上开启 tracingcontext.tracing.start(screenshotsTrue, snapshotsTrue, sourcesTrue) # ... 执行业务操作 ... context.tracing.stop(pathtrace.zip)跑完后在命令行执行playwright show-trace trace.zip会打开一个可视化面板你能像看录像一样一步步检查脚本在哪个节点开始出错。这个方法在调试偶发性问题时价值极高。我有一次排查一个“十次里挂一次”的脚本就是靠 Trace 发现某次请求返回了 500页面根本没有渲染出表格而不是脚本本身写错了。6.3 关键步骤截图和小范围调试截图是最朴素的调试手段。我习惯在关键操作前后各留一个page.screenshot(pathfdebug/{step_name}.png)脚本跑完甚至不需要看日志直接按时间线看图就行。再加上代码里的日志输出就能拼出整个执行过程。如果你觉得浏览器一帧帧跳得太快可以把操作放慢。Playwright 有一个实用的方法page.slow_mo不过它位于 launch 参数里browser p.chromium.launch( channelmsedge, headlessFalse, slow_mo500 # 每个操作之间暂停 500 毫秒 )调试时把slow_mo设成 300 到 800 毫秒能非常直观地看到每一步操作非常适合边调边录屏讲解场景。7. 把常用启动和等待封装成函数7.1 封装思路当你开始跑多个自动化任务后会发现每个脚本开头都是一套重复动作启动 Edge、设定默认超时、准备截图目录。我建议把它封装成一个工具函数统一管理。一个比较简单的基础封装如下from playwright.sync_api import sync_playwright def open_edge(headlessTrue, user_data_dirNone): options { channel: msedge, headless: headless, } if user_data_dir: context playwright.chromium.launch_persistent_context( user_data_diruser_data_dir, **options ) else: browser playwright.chromium.launch(**options) context browser.new_context() page context.new_page() page.set_default_timeout(20000) return page使用时我通常在with sync_playwright() as p的上下文里调用这个函数把不必要的重复代码收敛到一起。需要登录状态的任务传入一个专用的user_data_dir一次性任务就用默认模式。7.2 个人使用中的两个习惯有一个习惯是我踩过坑之后养成的脚本里凡是读取本地静态资源的路径绝对不直接用绝对路径而是全部基于pathlib.Path(__file__).parent来拼。否则同一个脚本换一台机器就崩光找路径问题就能耗掉半天。另一个习惯是给每个任务单独建一个日志目录按日期命名。截图、Trace、生成的报表都扔进对应的日期目录里。排查问题是能以“今天跑了哪个脚本、哪一步成功、哪一步失败”为线索快速定位而不是在一堆无命名文件里翻找。自动化脚本在本地跑通只是第一步真正麻烦的是部署到服务器上排障。等到折腾过一次无头模式下的乱码字体、后台杀掉残余进程这些事之后你才会反过来觉得能把浏览器稳定地启动和关闭就已经算成功了一大半了。我目前的经验是入门阶段不用追求把代码写得多么框架化先把launch参数、locator 定位、自动等待这三样吃透再考虑工程化封装。下一篇我打算单独写登录态管理与多标签页处理这两块在实际业务里几乎无可避免。如果你也在用 Playwright 控制微软 Edge欢迎直接留言聊聊。