Playwright 这套东西,在我刚开始接触 Python 浏览器自动化的时候,确实帮了大忙。以前用 Selenium 写脚本,最头疼的就是各种等待条件、元素定位不稳定,还经常因为浏览器版本不匹配而莫名其妙报错。后来换到 Playwright,发现这些麻烦事儿至少少了一半。有人把它叫作“新一代浏览器自动化工具”,这话不算夸张。它自己带了一套智能等待机制,操作元素之前会自动等元素稳定、可交互,脚本写起来比传统方式要顺手很多,代码量也肉眼可见地变少了。
这篇内容面向的是想用 Python 做网页自动化、数据采集、定时截图或者简单回归测试的朋友。哪怕你是从来没接触过浏览器自动化的纯新手,只要按着下面的步骤走一遍,也基本能完成从环境搭建到跑通第一个实战脚本的完整过程。我会把环境安装、核心选择器逻辑、实战代码和常见疑难问题全部串起来讲,有些细节是文档里不会明说但实际开发中很容易踩的坑,照着这份内容操作会顺很多。
1. 环境搭建与安装:把基础打牢
1.1 Python 环境准备与虚拟环境的选择
先说 Python 本身。Playwright 的 Python 版本要求是 3.7 及以上,实际开发中我用 Python 3.9、3.10、3.11 都跑过,稳定性没什么区别。如果你机器上还没有 Python,直接去官网下载安装包,安装时记得把“Add Python to PATH”勾上,这样命令行里才能直接调用 python 和 pip,省得后面到处找路径。
然后是虚拟环境。这一步看起来多此一举,实际非常有价值。不同项目依赖的库版本经常冲突,比如 A 项目需要 requests 2.28,B 项目又锁定了 requests 2.31,如果不隔离,光是装依赖就能把你折腾疯。命令行里执行:
python -m venv venv创建完虚拟环境后,Windows 用户激活方式是在命令行执行 “venv\Scripts\activate”,Mac 和 Linux 用户则是用 “source venv/bin/activate”。激活成功后,命令行提示符前面会多出一个 “(venv)” 前缀,看到这个就说明已经进入独立环境了。后续所有 pip 安装的包都会装到这个环境里,跟系统全局环境互不干扰。
1.2 安装 Playwright 库与浏览器内核
虚拟环境激活后,安装 Playwright 库本身很简单:
pip install playwright这里必须提醒一个常见误解:安装这个包只代表 Python 代码层面有了 Playwright 的库,但真正运行脚本时需要的浏览器内核还没有。Playwright 不像 Selenium 那样直接调用你已经安装的 Chrome 或 Edge,它更推荐使用自己打包好的浏览器版本,这样做的好处是内核版本和驱动代码完全匹配,彻底告别“ driver 和浏览器版本对不上 ”这种经典报错。
所以安装完库之后,还必须执行一条安装浏览器的命令:
playwright install chromium执行这条命令会下载一个专用的 Chromium 内核,下载完成后会保存在本机用户目录下的固定位置。如果后续执行脚本时报错说找不到浏览器可执行文件,九成就是这一步没做或者下载被中断了。国内网络环境下有时下载较慢,耐心等一等。如果公司网络有限制导致下载失败,可以考虑在命令行配置镜像或临时更改下载源,这个属于常见问题,后面我会再详细说。
1.3 验证环境是否真正可用
环境装好以后,强烈建议先跑一个最小化脚本验证一切正常,不要一上来就写复杂功能。新建一个 test_playwright.py 文件,里面放下面这段代码:
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()运行 “python test_playwright.py” 后,如果控制台输出了 “Example Domain” 这个页面标题,就说明从库到浏览器内核到基础调用链路全部没问题。看到这个结果,后面就可以放心进行实操了。
这里我用的是一个比较安全的测试站点,实际项目中你可以替换成任何你允许和有权访问的网站。但请记住,上线到生产环境的自动化脚本一定要评估好目标网站的服务条款和访问频率,做一个负责任的开发者。
2. 核心机制与定位方式:为什么 Playwright 更顺手
2.1 自动等待机制是最核心的体验区别
很多用过 Selenium 的朋友最开始不习惯 Playwright,是因为它“看起来”没有显式等待的代码。以前我们写脚本要到处塞 “WebDriverWait”,条件判断“等待某个元素可见、可点击”,代码又长又容易出错。Playwright 的做法完全不一样,它的几乎所有定位和操作动作都内置了自动等待逻辑。
比如你执行 “page.click(selector)”,它内部会自动等待直到元素满足以下条件:元素被附加到 DOM、元素可见、元素是稳定的(即不处于动画或移动中)、元素能接收事件、元素没有被其他元素遮挡。这些条件任何一条不满足,它会继续等待直到超时,默认超时时间是 30 秒,用时也可以按需修改。
这个机制带来的好处是惊人的:脚本整体稳定性大幅提高,网络慢、页面加载慢导致的失败率明显下降。我做过一个数据采集任务,以前用传统方式写,每次跑大约有 10% 的概率会因为某个弹窗或者元素加载慢而挂掉;换到 Playwright 之后,同样的场景,连续跑一周都很少出问题。
2.2 选择器不再是简单传字符串
很多人刚接触 Playwright 时有一个思维惯性:选择器就是 CSS 或者 XPath,比如 “#submit” 或者 “//button[@type='submit']”。Playwright 支持这两种,但它还提供更贴近用户视角的定位方式,比如通过文本定位:
page.click("text=提交")这个操作会智能匹配页面上可见文本为“提交”的元素。再比如通过 ARIA 角色定位,用 “get_by_role” 方法来找按钮、输入框,这对无障碍语义比较规范的页面非常有效:
page.get_by_role("button", name="登录").click()这类定位方式的好处是选择器跟页面结构耦合更低。举个例子,如果开发同学改了某个按钮的 CSS 类名,用传统 CSS 选择器的脚本可能就失效了;但用文本或角色定位的脚本则完全不受影响。日常实战中,建议优先考虑文本、角色、标签这几种“语义化”定位方式,CSS 和 XPath 作为备选或补充。
2.3 强大的 Locator 对象体系
Playwright 里有一个重量级概念叫 Locator,它是定位页面元素的核心对象,几乎所有操作都是围绕它展开的。它跟“直接传 selector 字符串”的区别在于,Locator 是懒加载的,你拿到一个 locator 并不代表它马上在你页面上查找,而是等到具体操作时才去匹配。这意味着你可以在一个函数里定义好 locator,反复使用,性能上更高效。
Locator 还有非常实用的链式操作和范围限定能力,比如:
dialog_box = page.locator(".dialog-box") confirm_button = dialog_box.get_by_role("button", name="确定")这样做的好处是不需要写一长串完整的选择器,逻辑结构也更清晰。而且,Locator 对象还内置了 “all_text_contents()” 这类用于批量提取文本的方法,处理列表类页面时特别方便。举例来说,想提取页面上某个列表的全部标题文字,可以这样写:
titles = page.locator("ul.news-list li .title").all_text_contents()执行完之后 titles 就是一个 Python 列表,后面想写入文件、入库或者继续处理都非常顺手。
3. 实战项目一:自动化登录并抓取清单数据
3.1 确定目标场景与准备工作
下面用一套比较有代表性的实战场景来说明完整流程:一个内部管理系统的登录页,登录后有一个“任务清单”列表,需要把列表里的全部任务标题和状态抓取下来。这类场景在工作中很常见,比如定时巡检数据、同步内部系统信息、生成日报素材等等。
说清楚一点,做任何自动化采集之前,先确认目标系统是否允许自动化访问,以及你是否有合法的权限。这里只用虚构的内部系统作为示例,重点讲实现过程和代码逻辑。
在写代码之前要先弄清两个基础信息:登录页的地址,以及用户名密码输入框和登录按钮的定位方式。如果是有浏览器调试工具的浏览器,可以在开发者工具里直接检查元素,也可以右键选择“检查”。实际操作中我习惯先用 playwright 自带的 “codegen” 工具辅助定位。在命令行执行:
playwright codegen https://your-target-login-url它会打开一个浏览器窗口,同时生成一个 Playwright 脚本面板。你在浏览器里手动操作一遍,比如点击输入框、输入账号密码、点击登录,旁边的脚本面板会自动生成对应的 Python 代码,而且选择器基本都是可靠的。这个功能非常适合快速确定初始版本脚本,拿到生成结果后再手动优化成可复用的完整版本。
3.2 初始化浏览器与页面对象
用同步 API 来写第一个完整脚本,因为同步模式更直观,也更贴合大多数新手的编程习惯。脚本开始前先创建浏览器上下文,这个概念很多人容易忽略:浏览器上下文相当于一个“隔离的浏览器会话”,不同上下文之间 cookies、localStorage 完全隔离,互不干扰。
之所以推荐使用上下文而不是直接用默认页面,是为了避免脚本之间互相污染登录态。比如你写了一个脚本 A 和脚本 B,如果共用同一个浏览器实例的默认页面,A 登录了某系统,留下的 Cookie 可能会影响到 B 的运行结果。每次创建独立上下文,相当于每次都是“全新访客”,状态隔离干净。
代码框架如下:
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False, slow_mo=500) context = browser.new_context() page = context.new_page() # 后续操作都基于 page 来进行这里我特意在 launch 时设置了 headless=False,并且加上了 slow_mo=500,目的是让浏览器以真实窗口模式运行并放慢操作速度。第一次调试脚本时这样设置很有帮助,因为你可以亲眼看到每一步操作的效果,发现问题也能立刻察觉。等脚本彻底稳定了,再改回 headless=True 提升运行效率。
3.3 编写登录与页面交互逻辑
假设登录页的用户名输入框可以通过 “#username” 定位,密码输入框是 “#password”,登录按钮是 “text=登录”,那么核心操作如下:
page.goto("https://your-target-login-url") page.fill("#username", "my_username") page.fill("#password", "my_password") page.click("text=登录") page.wait_for_load_state("networkidle")这里有三个细节要重点说明。第一,“fill” 方法填值之前会自动等待输入框可交互,如果页面加载慢了也不会报错,等等就好。第二,点击登录按钮后,页面可能发生跳转,后续如果马上要定位新页面元素,最好先加一个 “wait_for_load_state” 等待页面加载完成,这样能避免页面还在跳转时就去做后续操作。第三,登录提交后,如果系统有验证码或者二次验证流程,自动化会麻烦许多,这个我在常见问题部分再详谈。
登录完成后,进入任务清单页面。清单数据项可能在一个表格或者列表里,每一项包含任务名称和状态。拿到页面上这些内容可以采用 Locator 对象:
task_names = page.locator(".task-list .task-name").all_text_contents() task_states = page.locator(".task-list .task-state").all_text_contents()如果页面结构复杂,比如任务名称和状态不在同一个列表容器里,也可以先用一个整体定位拿到所有条目,再逐条提取。基本思路没变,就是先定位容器,再通过范围限定逐项取数据。
3.4 处理分页与批量采集的完整方案
实际情况中,清单数据往往不止一页,这就涉及到翻页处理。常见翻页按钮是一个“下一页”链接,或者带有分页列表。最简单的翻页逻辑是循环点击“下一页”,直到按钮不可用为止。
while True: page.locator(".task-list .task-name").all_text_contents() # 这里把数据累积到列表中 next_button = page.locator("text=下一页") if next_button.is_disabled(): break next_button.click() page.wait_for_load_state("networkidle")这里有一个非常重要的实战注意点:“is_disabled()” 这种判断必须在按钮可见可交互的状态下才有效,如果按钮不存在时不能直接报错。更稳妥的做法是每次点击前先判断按钮是否存在,例如用 “count()” 来检查。我第一次写类似循环时就是没做这个判断,跑到最后一页时点击了一个已经不存在的“下一页”按钮,结果直接抛异常。所以循环翻页时一定要设计好退出条件,别指望每次都靠“兜底”方式退出。
采集完所有数据后,写入 CSV 文件即可。如果数据量更大,也可以直接写入数据库。写 CSV 我建议用 Python 标准库 csv,不折腾额外依赖:
import csv with open("tasks.csv", "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["任务名称", "状态"]) writer.writerows(zip(task_names, task_states))完成后打开 CSV 文件,检查一下数据是否有乱码、字段是否对齐。编码方面建议统一使用 utf-8,如果遇到 Excel 打开乱码的问题,可以尝试把编码换成 “utf-8-sig”,这是中文环境下一个很常见的小坑。
4. 实战项目二:截图、PDF 生成与 headless 批量任务
4.1 headless 模式与页面截图
自动化任务跑到正式环境时,大多数人不会希望每次都弹出一个浏览器窗口,这时“headless 模式”就派上用场了。所谓 headless,就是浏览器在后台运行,不显示任何界面。好处很明显:性能开销更低,适合服务器定时执行;不会干扰其他正在运行的桌面程序;在无图形界面的机器上也能跑。
截图是 Playwright 最实用的功能之一。它可以对整页截图、对特定元素截图,也可以对指定区域截图。整页截图在页面内容高度超过一屏时特别有用,因为默认截图只截视口区域,长页面截不全。执行整页截图只需要一个参数:
page.screenshot(path="full_page.png", full_page=True)这个“full_page=True”会自动滚动整个页面再拼接图片,效果等同于浏览器开发者工具里的整页截图功能。我在工作中经常用它来对数据面板做每日定时存档,比如每天早上上班前自动把某个统计页面的全部图表截个图发到工作群或者存档到网盘,方便后续对比数据变化。
元素级截图则适合只截取页面某个部分,比如只截取某个报表表格:
table = page.locator(".report-table") table.screenshot(path="report_table.png")4.2 生成 PDF 文件的注意点
Playwright 在 Chromium 内核下可以调用 “page.pdf()” 方法,不用额外第三方库就能把页面直接保存为 PDF。但有一个重要前提:这个方法只在 headless 模式下生效,在 headful 模式下运行会直接报错。很多新手第一次调 PDF 时刚好处于默认无头模式,所以没发现问题;但如果改成了窗口模式,就要特别注意。
生成 PDF 时可以指定纸张尺寸、打印边距、页眉页脚等参数,基本能满足日常存档需求:
page.pdf(path="report.pdf", format="A4", print_background=True)这里 “print_background=True” 非常关键。页面上很多元素的背景色默认在打印时会被浏览器忽略,不加这个参数生成的 PDF 会白花花一片,表格样式全部丢失。实测中我踩过这个坑,第一次生成 PDF 时整个页面跟纯文本一样,后来查文档才发现就是这个参数的问题。
4.3 编写批量化定时任务脚本
把截图和 PDF 结合起来,就可以做一个非常典型的批量定时任务:每天固定时间打开若干页面,截图或生成 PDF,保存到指定目录。比如我有一个内部数据面板,每天要生成三张页面截图,分别对应销售、库存、客户三个模块。
这种批量任务适合把所有目标 URL 和相关配置放在一个字典或列表里,循环处理。脚本主体大概这样:
targets = [ {"name": "sales", "url": "https://your-target/sales"}, {"name": "inventory", "url": "https://your-target/inventory"}, {"name": "customers", "url": "https://your-target/customers"} ] with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context(viewport={"width": 1920, "height": 1080}) page = context.new_page() for item in targets: page.goto(item["url"]) page.wait_for_load_state("networkidle") page.screenshot(path=f"{item['name']}_daily.png", full_page=True) browser.close()这里设置了 viewport 尺寸为 1920x1080,因为很多数据面板页面依赖宽屏布局,如果默认视口太小,页面上的图表可能以响应式布局展示,截出来的效果跟你在电脑上看到的完全不一样。碰到这种问题,调整视口尺寸是一个很有效的解决手段。
定时执行的部分,在 Linux 或 Mac 上可以直接用系统自带的 crontab;在 Windows 上可以用“任务计划程序”;如果不想依赖系统级配置,也可以在 Python 脚本里用循环加 sleep 的方式做轮询定时,但这种方式比较耗费资源,不适合长期运行。我个人的建议是:能交给系统调度就交给系统,Python 本身更适合做执行者,而不是调度者。
4.4 多页面并发时的上下文管理
如果一次要打开很多个页面,比如要批量对几十个 URL 做截图,逐个页面顺序执行虽然简单,但速度会偏慢。这时可以用多上下文加多页面并发来提升吞吐量。不过并发会带来更大的内存开销和更高的出错概率,像某些页面会在短时间内限制连接数,访问过频反而更容易被限制。
一个相对稳妥的方案是用 Python 的 “concurrent.futures” 或者异步 API 来实现并发,但并发热度级别要控制好,建议每个上下文只负责少量页面,并且给每个上下文设置独立视角。异步模式写并发任务看着更优雅,但对新手理解成本也更高。我的建议是,初期先跑通同步模式、串行任务,等确认业务逻辑完全没问题之后,再考虑并发优化。不要一上来就搞并发,否则问题排查难度会成倍上升。
5. 常见问题排查与经验心得
5.1 常见异常速查表
在实际使用 Playwright 的过程中,有几个异常几乎是每个人都会遇到的。我把它们整理成了一张速查表,方便大家遇到问题时快速对照。
| 异常现象 | 常见原因 | 解决思路 |
|---|---|---|
| 提示找不到浏览器可执行文件 | 没有执行 “playwright install” 或下载失败 | 重新执行安装命令;检查网络;必要时配置镜像源 |
| 页面元素定位超时 | 元素确实不存在、页面加载慢、元素被遮罩层覆盖 | 先确认选择器是否正确;适当调大超时时间;排查页面是否有弹窗遮挡 |
| 点击元素报 intercepted | 元素被其他元素遮挡或不可点击 | 改用 “force=True” 强制点击前先确认是否影响业务逻辑;等待遮挡消失 |
| 页面跳转后定位失败 | 跳转未完成就开始操作 | 添加 “wait_for_load_state” 或等待特定元素出现 |
| 生成的 PDF 样式丢失 | 没开启打印背景参数 | 添加 “print_background=True” |
| headless 模式下非常快但页面元素没有渲染 | 部分数据加载依赖延时或懒加载 | 先等待网络空闲,必要时手动滚动页面触发懒加载 |
这里要额外说明一下 “force=True” 的边界。元素被其他层遮挡时,Playwright 默认会拒绝点击并报错,这样设计是合理的,因为强制点击很可能点不到真正想点的位置,甚至误触别的东西。使用 “force=True” 前要仔细确认遮挡原因是临时的还是持续性的。比如页面有为期几秒的 loading 遮罩,等几秒就好,不建议直接强制点击;如果遮挡是固定的广告浮层,不影响目标元素位置,则可以强制点击。
5.2 关于 headless 与有头模式的选择
很多新手分不清什么时候用有头模式、什么时候用无头模式。我的习惯是:开发调试阶段一律用有头模式并且开启 slow_mo 参数,让每一步操作都能被肉眼看到;等到脚本稳定后,再切换到无头模式跑正式任务。切换的时候记得注意两点:一是截图和 PDF 行为在无头模式下更容易保持一致;二是有头模式下有些页面可能出现验证码或人机验证,这类问题在无头模式下反而可能更容易触发,需要具体场景具体分析。
如果你需要模拟真实用户浏览器的更多特征,可以设置自定义的 user agent、时区、语言、屏幕尺寸等参数。这些参数可以通过 “new_context()” 传入,比如:
context = browser.new_context( locale="zh-CN", timezone_id="Asia/Shanghai", viewport={"width": 1920, "height": 1080}, )这样做的好处是页面更倾向于按符合本地用户习惯的方式渲染,尤其是一些多语言网站,通过设置 locale 可以确保页面以中文展示,避免抓取到的数据可能是英文文案导致后续匹配出错。
5.3 关于“等待”的两种不同用法
Playwright 的自动等待机制虽然省心,但也不是万能的。有一种情况需要手动处理:页面显示的是骨架屏或 Loading 状态,此时元素已经存在但内容还没填充完毕。自动等待只能负责“元素可见可交互”,不负责“数据加载完成”。所以,如果需要确保某个数据渲染完成后再做截图或采集,建议使用 “page.wait_for_function” 配合一个 JavaScript 判断条件。
比如你要等待表格里出现至少 10 行数据:
page.wait_for_function("document.querySelectorAll('table tbody tr').length >= 10")这个操作的意思是在页面环境里实时执行 JavaScript 表达式,直到表达式返回 True 或超时才继续。这个用法在实际项目中出现的频率相当高,因为它能精确表达“我关心的数据到底有没有加载备好”这个条件,而不是笼统地去等一个元素存在。
5.4 多个 tab 与弹窗的处理方式
自动化过程中经常会遇到点击后弹出新标签页的情况,比如点击一个“在新窗口查看”的链接。很多人第一次遇到时一脸懵,因为 Playwright 的默认逻辑里,新打开的页面不会自动成为当前操作页面。处理方式是需要监听新页面事件。
以下是一个处理新标签页的经典写法:
with context.expect_page() as new_page_info: page.click("text=在新窗口打开") new_page = new_page_info.value new_page.wait_for_load_state() print(new_page.title()) new_page.close()注意这里用的上下文是 “context.expect_page()” 而不是 “page.expect_page()”,因为新页面是属于整个浏览器上下文的。如果用了前者,偶尔会漏掉事件,新手排查时容易卡住。还有 JS 弹窗,比如 alert、confirm 这类对话框,Playwright 默认会自动把弹窗关掉,不会像人一样去点击确定或取消。要自定义处理弹窗逻辑,需要监听 “dialog” 事件,并调用 “dialog.accept()” 或 “dialog.dismiss()”。
5.5 脚本稳定性的综合调优经验
到这里,整个入门流程已经走得差不多了。最后分享几个我在实际使用过程中总结的稳定性调优经验,希望对大家后续做更复杂的项目有帮助。
第一,尽量把选择器定义成可复用的变量或函数,而不是每个动作都写一个完整选择器。这样当页面结构发生变化时,只需要改一处而不是到处改。尤其是一个选择器在多处用到时,这个习惯的价值会体现得非常明显。
第二,凡是涉及文件下载的任务,一定要配置下载路径,并且等下载完成。Playwright 里下载事件是一个异步过程,点击下载后不要立刻去访问下载目录,而是通过 “expect_download” 来捕获下载对象并等待完成,否则脚本可能已经跑完了,文件却还没落盘。
第三,日志和截图是排查问题的两大法宝。正式跑批任务时,建议在关键节点添加日志输出;在异常分支里保留页面截图和 DOM 快照,这样脚本一旦出问题,至少能快速定位是到达了哪一步、页面状态是什么样的。
第四,也是最重要的,自动化脚本再稳定也难免受外部环境影响,比如目标网站改版、网络波动、第三方登录策略变化等。所以千万不要让脚本“裸奔”,要设计好失败后的重试机制和告警通知。重试次数不需要多,两三轮即可,超过重试次数后发一封告警邮件或者推送一条消息到群里,让相关人员及时介入,比让脚本一直默默失败要强得多。