ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Playwright智能表单自动化实战:从登录到提交的完整指南

Playwright智能表单自动化实战:从登录到提交的完整指南

做爬虫的人都会遇到这么一件事:目标站点的表单越来越复杂,页面交互越来越重,过去用 requests 直接模拟 POST 提交的老套路根本走不通。尤其是碰到登录以后才能操作的页面、需要逐层联动的下拉框、上传文件后才生成的提交信息、用 JS 渲染且带动态 token 的隐藏字段,传统的静态请求方案基本处于“被按在地上摩擦”的状态。这几年我在实际项目里把重心从 requests 转向 Playwright,专门处理这类“浏览器表单自动化”任务,全程经历了不少坑,也积累了一整套可以直接复用的思路。

这篇文章先讲清楚为什么表单自动化非用浏览器不可,再给出环境准备、等待策略和定位器的核心用法,接着拆解一个登录到提交的完整流水线,然后深入联动下拉、文件上传、验证码、分步表单这些复杂场景,最后补上反检测与稳定性调优的经验,以及一份常见问题排查速查表。适合正在做爬虫进阶的开发者、想用自动化代替手工填表的测试工程师,以及做 RPA 流程的伙伴参考。这里先表个态:所有示例都用通用占位符,实操时请只在你拥有权限或明确允许测试的环境里运行,别拿别人的生产系统练手。

1. 为什么表单自动化要交给浏览器,而不是 requests

1.1 从“请求-响应”到“人机交互”

静态爬虫的核心逻辑是“请求-响应”:构造好 URL 和参数,发一个 HTTP 请求,然后从返回的 HTML 里提取数据。可表单类场景完全是另一种玩法,它的核心是“人机交互”:用户输入内容、选择选项、点击按钮,页面上的 JS 会实时校验、重组数据、生成提交参数,最后才触发真正的网络请求。

这里有个最典型的坑:很多表单提交时需要一个动态生成的隐藏字段,这个字段的取值依赖页面初始化时的 JS 计算,如果你用 requests 直接抓首页,得到的只是原始 HTML 里的空壳,根本没有那个字段的最终值。早几年我为了抠出这类字段,要反复看 JS 源码、复现加密逻辑,动不动就花掉一整天。换个思路,直接让浏览器去执行页面里的全部 JS,交互后该生成的参数自然会生成,这比逆向 JS 划算太多。

再就是对用户输入的即时校验。真实表单里,用户一旦改动了某个下拉框,页面立刻异步请求后台,把下一级选项刷新出来;用户一旦让某个字段失焦,页面马上校验格式并在旁边标红。这类行为用 requests 模拟的话,你得手动补齐一次一次异步请求的顺序和依赖关系,而浏览器自动化天然就在这个环境里,它看到的页面就是真实用户看到的页面。

1.2 表单自动化最典型的几类场景

根据我自己的项目经验,适合用 Playwright 做智能表单自动化的场景大概有这几类:

  • 账号注册与登录类:要做多次输入、勾选协议、点击登录,还要容忍偶尔出现的验证码或者二次确认。
  • 后台数据录入类:在内部管理系统里批量填写工单、提交报表、更新配置,通常是一个页面接一个页面连续操作。
  • 联动搜索与筛选类:先选城市再选区县,或者先选分类再选子分类,最后点击查询并翻页抓取结果。
  • 分步向导类:一个表单拆成三四步,每步都要求填写不同的字段,最后一次性提交,中间还有“上一步/下一步”的状态切换。
  • 文件上传与批量导入类:页面需要先传一个压缩包或表格文件,然后等待后台解析结果,再根据提示修正错误数据并再次提交。

这几种场景有个共同难点:页面上出现了什么内容,用户下一步能做什么,完全取决于前一步的操作结果,这正好是静态请求的盲区。Playwright 这类浏览器自动化工具,能力本质不是“发起请求”,而是“操作系统里的一个真实浏览器”,所以它对状态变化、页面刷新、异步加载这类事情反应非常自然。

1.3 Playwright 相比 Selenium 和 Puppeteer 怎么选

做表单自动化,现在市场上有三套主流方案:Selenium、Puppeteer 和 Playwright。我最终把主力放在 Playwright 上,不是因为它没有缺点,而是它在几个关键点上最贴合爬虫和自动化提交的需求。

对比项SeleniumPuppeteerPlaywright
语言支持Java、Python、C# 等,官方支持广主要为 Node.js,Python 只能借第三方Python、Node.js、Java、.NET 都有官方支持
等待策略老版本经常要自己写显式等待,WebDriverWait 写法繁琐有内置等待,但部分场景还是要手动每个操作前会自动等待元素可交互,心智负担小
浏览器支持Chrome、Firefox、Safari、Edge 都行,但驱动配置麻烦基本 Chromium 系,Firefox 支持有限Chromium、Firefox、WebKit 全支持,自带驱动管理
多页面/多标签处理窗口句柄切换逻辑绕支持 BrowserContext 但生态弱一些BrowserContext 隔离机制强,多标签处理非常自然
移动端与地理/权限模拟要借助额外工具部分支持内置设备描述符,可一键模拟移动设备
截图与调试可以但不好用很好非常好,能自动保存 trace 文件

从爬虫角度说,Selenium 最大的问题是安装和调试成本高,尤其驱动版本和浏览器版本匹配错了,一早上就搭进去了。Puppeteer 很好,但生态更偏向 Node.js,Python 用的多的团队引入它有割裂感。Playwright 把浏览器实例、上下文、页面三个层级分得很清楚,尤其是 BrowserContext 隔离机制,让爬虫可以同时开多个相对独立的“会话”,每个上下文有自己的 cookie、localStorage 和浏览器指纹,这对批量处理表单非常有利。

我选 Playwright 还有一个很实在的理由:它有官方内置的自动等待逻辑,操作元素前会自动等待元素附着、可见、稳定、可接收事件。这套机制,写代码的时候能少掉一半的显式等待逻辑,出错率也低很多。

2. 环境准备与核心 API 基础

2.1 安装和浏览器驱动

Playwright 装起来很简单,核心就是两步。第一步用 pip 安装 Python 包,第二步安装浏览器内核。这边建议直接用虚拟环境,避免污染全局 Python。

pip install playwright playwright install chromium

playwright install会下载配套的 Chromium 浏览器,也可以加装 Firefox 和 WebKit。对爬虫来说,Chromium 足够覆盖绝大多数目标站点。如果部署到无图形界面的服务器上,还需要检查系统依赖库,直接运行playwright install-deps chromium就能把缺的库自动补上。

安装完快速验证一下:

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()

能打印出页面的 title,说明整套环境已经没问题。刚才这个例子是同步 API,Playwright 还提供异步 API(asyncio 版本),实际项目里我更推荐异步写法,因为并发效率更高,但这篇先以同步为主降低理解成本。

2.2 等待策略:自动等待的“三重奏”

刚开始用 Playwright 的人,最容易犯的错误是沿用 Selenium 时代的思维,到处写time.sleep(3)。其实 Playwright 自带一套等待机制,绝大多数内置操作都会先等元素可见、稳定,再执行点击或填写,所以代码里 90% 的 sleep 都可以删掉。

但有几种情况仍然需要“手动等待”:

  • 页面跳转后,要等 URL 变成预期地址。
  • 点击提交后,要等某个提示文本出现。
  • 连续异步刷新后,要等一个元素出现在 DOM 里。

常用写法是这样:

page.wait_for_url("**/home") page.wait_for_selector("#result_text", state="visible") page.wait_for_load_state("networkidle")

wait_for_url适合登录或提交后的跳转判断,它的参数支持简单通配符,**/home表示匹配任意域名下的 home 路径。wait_for_selector是最通用的等待方式,可以指定元素可见、隐藏、附加、分离等状态。wait_for_load_state("networkidle")是等网络空闲,这个要慎用,因为页面如果一直有轮询请求,它会一直等到超时。

我自己的等待策略,简单说是三层:第一层,完全信任 Playwright 内置的自动等待,只要操作元素前它能把元素稳定住,就不要画蛇添足。第二层,在关键状态切换点显式等待,比如登录后等 URL 变化、提交后等成功提示。第三层,只有极少数第三方组件渲染异常时,才会用固定 sleep 兜底,而且时间不超过两三秒。

2.3 定位器与表单组件操作

定位元素是表单自动化里最容易翻车的一环。以前 Selenium 流行的find_element_by_xpath写法又长又脆,页面稍微改个 class 就挂,而在 Playwright 里可以直接用面向用户的方式定位。

最推荐四种定位方式:

page.get_by_label("用户名") # 用 label 文本定位输入框 page.get_by_role("button", name="提交") # 用按钮角色和名称定位按钮 page.get_by_placeholder("请输入邮箱") # 用 placeholder 定位输入框 page.locator("#phone") # CSS 选择器,适合没有好属性的元素

get_by_label是最爽的,因为大多数表单里输入框都配了<label>标签,这种定位方式不关心输入框 id 和 name 是否变化,只要页面语义不变就能稳定命中。get_by_role适合定位按钮和链接,一看就知道你要“点击那个叫提交的按钮”。

表单组件的操作也有一些细节。输入框除了fill,有时候遇到“每输入一个字符触发一次校验”的组件,可以用press_sequential模拟逐字输入。下拉框必须用select_option,通过文本、值或索引选择都行。文件上传用set_input_files,它接受文件路径,还能传多个文件。复选框和单选框用check和uncheck。日期控件如果是原生input[type=date],直接fill("2025-06-01")就行。

在这一步要特别叮嘱的是:定位器表达式越贴近“用户在页面上看到的东西”越好。用get_by_label("用户名")而不是locator("input:nth-child(2)"),前者在页面结构变化时依然能扛住,后者可能一次改版就全线崩溃。

3. 智能表单自动化流水线:登录、动态等待、提交

3.1 拆解业务,画状态转换

真正做自动化之前,先别急着写代码。我的习惯是把整个流程画成一张状态表:每个阶段做什么操作、操作完成后等到什么信号、等不到信号时怎么处理。用一个“登录后提交申请单”的例子来看:

阶段关键动作成功信号失败处理
打开登录页访问登录 URL登录框出现在页面上重试访问并加一次等待
填写账号密码填 input 并点击登录URL 变成后台首页检查错误提示文本并截图
进入申请页面点击菜单入口申请表单可见回到首页重新定位入口
填写表单填文本、选下拉、传文件所有必填项无红框定位到第一个红框字段
提交申请点击提交按钮出现“提交成功”提示捕获提示信息并记录

这张表就是脚本的蓝图,每个“成功信号”都对应一个等待条件,每个“失败处理”都对应一个异常分支。不要觉得这是多此一举,真正写代码的时候你会发现,没有这张表的话,你会在“应该等什么”这个问题上反复犹豫。

3.2 可复用脚本骨架

下面给一个可直接套用的 Python 脚本骨架,覆盖了打开浏览器、登录、操作表单、提交、失败重试这些环节。代码里保留了关键注释,方便按实际场景替换选择器和页面信号。

import time from playwright.sync_api import sync_playwright def fill_login_form(page, username: str, password: str): page.goto("https://example.com/login", wait_until="domcontentloaded") page.get_by_label("用户名").fill(username) page.get_by_label("密码").fill(password) page.get_by_role("button", name="登 录").click() # 等待跳转回首页,超时则说明账号密码或验证环节出问题 page.wait_for_url("**/home", timeout=15000) def submit_application(page, form_data: dict): page.get_by_role("link", name="新建申请").click() page.wait_for_selector("#application_form", state="visible") page.get_by_label("标题").fill(form_data["title"]) page.get_by_label("描述").fill(form_data["description"]) page.get_by_label("类型").select_option(label=form_data["category"]) if form_data.get("attachment"): page.set_input_files("input[type=file]", form_data["attachment"]) page.get_by_role("button", name="提交申请").click() # 成功信号:出现成功提示文案 try: page.wait_for_selector(".success-message", state="visible", timeout=10000) return True except Exception: page.screenshot(path=f"failure_{int(time.time())}.png") return False with sync_playwright() as p: browser = p.chromium.launch(headless=False, slow_mo=100) context = browser.new_context() page = context.new_page() fill_login_form(page, "demo_user", "password123") if not submit_application(page, { "title": "测试申请单", "description": "这是自动化提交的示例申请内容", "category": "类型A", "attachment": "/tmp/report.csv", }): print("提交失败,请查看截图定位原因") browser.close()

这段代码里slow_mo=100很实用,它让每个操作之间插入 100 毫秒延迟,方便观察脚本运行时浏览器的动作。调试阶段建议开着它,上线跑批量任务时再设成 0 或者直接使用 headless 模式。

3.3 “智能”体现在哪儿

很多人觉得“智能”是个玄学词,其实落到表单自动化上,就是三个能力:根据页面状态决定做什么、操作失败后能自己恢复、提交前能做内容校验。

根据页面状态决定做什么,可以用条件判断实现。比如登录页可能出现两种布局,一种需要填账号密码,另一种还要求填一个额外的邀请码,那就先判断邀请码输入框是否存在,存在再填:

invite_input = page.get_by_label("邀请码").first if invite_input.is_visible(): invite_input.fill("code123")

失败恢复是最能省心的地方。我之前处理过一个后台列表页,点击“新建”按钮偶尔会因页面数据没加载完全而弹不出表单。解决办法不是盲目加 sleep,而是写一个带重试的点击函数:先尝试点击,如果超时就刷新页面再来一次,最多重试三次。

内容校验也不难做,提交之前遍历一遍必填字段,检查每个 input 的 value 是否为空,或者检查页面上是否存在红色错误提示的 class。这个习惯能避免你把一份明显缺数据的表单提交上去,然后还要等后台返回错误再返工。

3.4 登录态跨步骤复用

一个实际的自动化流程,很少是单次打开浏览器完成所有事。更多时候是登录一次,然后把登录态保存下来,后续脚本启动时直接恢复,跳过登录环节。Playwright 提供了非常干净的方案:storage_state。

# 第一次运行:登录后保存状态 with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context() page = context.new_page() # 执行登录操作 page.get_by_label("用户名").fill("demo_user") page.get_by_label("密码").fill("password123") page.get_by_role("button", name="登 录").click() page.wait_for_url("**/home", timeout=15000) context.storage_state(path="auth.json") browser.close() # 后续运行:直接恢复登录态 with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context(storage_state="auth.json") page = context.new_page() page.goto("https://example.com/home") # 这里已经是登录状态,直接继续后续表单操作

storage_state里包含 cookies、localStorage 等关键信息,恢复之后页面会直接认为你是已登录用户。要注意的是,登录态有有效期,一旦失效就要重新走一遍登录流程,所以在外面套一层异常捕获,识别到跳转回登录页时自动重新登录,这是企业级脚本的常规操作。

4. 复杂表单实战:联动下拉、上传、验证码、分步提交

4.1 联动下拉与动态选项

联动下拉是表单系统里很常见的设计:先选大类,然后小类的选项通过异步请求加载出来。用 Playwright 处理这类组件,关键点在于“每改动一个上级选项,就要等下级选项真正渲染出来,再继续操作”。

用伪代码拆解一下:

# 选择大类 page.get_by_label("所属大类").select_option(label="集团型客户") # 等待小类第一个选项出现 page.wait_for_selector("#sub_category option[value]") # 再选择小类 page.get_by_label("所属小类").select_option(label="战略客户")

不要小看中间那个wait_for_selector。实际项目中我遇到过很多次“大类选完,小类列表刷新需要一秒多”,如果不等待,脚本会拿旧的小类选项去select_option,然后直接抛异常。如果你想做得更稳,还可以写一个轮询函数,循环检查目标 option 是否存在,存在才执行选择。

联动场景还有一个隐藏坑:部分下拉框被包在自定义组件里,看起来是下拉,实际上是点击后弹出一个列表,再点击列表项。这种就不能用select_option,要用click加get_by_text点击选项。怎么判断?先看元素标签,原生<select>用select_option,自定义组件就老老实实模拟点击。

4.2 文件上传与日期控件

文件上传分两种情况。原生上传框可以直接set_input_files,就像示例代码里写的那样简单。但还有一种“拖拽上传区域”,它接受的是 drop 事件,set_input_files不一定有效。我自己的处理方式是定位到这个上传区域的内部 input 元素,再调用set_input_files,大多数实现都隐藏着一个 input 负责接收文件。

upload_locator = page.locator(".upload-area input[type=file]") upload_locator.set_input_files(["/tmp/report.csv", "/tmp/attachment.pdf"])

日期控件同样要区分。原生input[type=date]是最省事的,fill("2025-06-01")直接生效。第三方日期组件,比如带日历弹窗的那种,通常不能直接填值,我的方案是先点击日期输入框,让日历弹出来,然后用get_by_role("button", name="确定")或直接点击对应日期文本完成选择。想省事的话,可以试试先focus再press键盘输入,但实测成功率没有点击日历高。

这里有个经验:碰到复杂组件,先别硬写代码,打开浏览器开发者工具,看看它到底是原生元素还是自定义组件,再决定用哪种方案。这个前期判断,能省下后面一半的调试时间。

4.3 验证码环节的处理思路

验证码确实是表单自动化里绕不开的坎,但处理方式要看验证码的类型和业务需求,没有万金油方案。

字符型图形验证码,可以先用 OCR 做轻度识别,或者把验证码图片截下来交给识别服务,识别结果填进去后页面上会告诉你对错,识别失败就刷新验证码再来一次。实际代码大概是这样:

# 定位验证码图片并截图交给 OCR 逻辑识别 captcha_img = page.locator("img.captcha") captcha_img.screenshot(path="captcha.png") captcha_text = ocr_recognize("captcha.png") # 替换成你的 OCR 函数 page.get_by_label("验证码").fill(captcha_text)

但我要泼一盆冷水:OCR 对简单验证码还能应付,碰到扭曲严重、带干扰线的图形,识别率会明显下降。生产级方案里我更推荐“人工介入”模式:脚本跑到验证码这一步,自动截图发送到某个审核队列,由真实用户看一眼填进去,脚本再继续跑。这种方式不会把流程卡死,而且比盲目依赖 OCR 稳定得多。

滑块验证码也不建议用复杂的曲线拟合去硬刚。我的思路是尽量保持行为自然:先移动鼠标到滑块附近,小范围抖动一下,再以一个先快后慢的速度拖过去。多数情况下能通过;实在通不过,就进入人工兜底流程。每家站点对异常行为的判定机制都不太一样,这个环节没有绝对的“一招鲜”。

4.4 分步多页表单的状态保持

分步表单的核心难点,不在于每一步怎么填,而在于“上一步/下一步”的状态切换和整体的状态保持。Playwright 的 page 对象天然维持着同一个浏览器上下文里的所有会话数据,只要不关闭页面,cookie、localStorage、以及页面内部的暂存数据都不会丢。

有种情况要特别小心:跳转到新页面时,目标站点会在新标签页里打开表单。这时候要用上下文级别的expect_page才能捕获新页面:

with context.expect_page() as new_page_info: page.get_by_role("button", name="去填写完整资料").click() new_page = new_page_info.value new_page.wait_for_load_state("domcontentloaded") # 此时 new_page 就是新标签页,后续操作都基于它

另外,分步表单里经常出现弹窗或嵌套 iframe,等会专门说。在状态保持上还有一个操作习惯值得推荐:每填完一步,顺手把当前页面的关键数据截个图,尤其是那种业务单据,截图能帮你快速定位到底哪一步填错了,而不必回溯日志。

5. 反检测与稳定性调优

5.1 模拟真实环境参数

浏览器自动化跑多了,目标站点很容易察觉出异样。最常见的几个破绽包括:无头浏览器特征、自动化控制标记、客户端尺寸异常、时区和语言不匹配。

Playwright 里可以通过new_context参数直接模拟一批环境属性,让浏览器指纹看起来更真实:

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

关键是这几项必须互相匹配。如果你用的是 Windows 的 UA,但 viewport 却是 375 像素宽(手机尺寸),那一眼就会被识破。headless=False是另一个大杀器,很多检测脚本会通过navigator.webdriver和其他指纹来判断是不是自动化浏览器,Close headless 模式能规避掉相当一部分检测。

5.2 浏览器指纹一致性与自动化标记处理

做爬虫的人应该都知道一个概念:目标站点可以通过navigator.webdriver属性判断当前浏览器是否由自动化驱动。玩过 Playwright 的就会发现,默认情况下这个属性是 true。

处理办法之一是通过add_init_script在页面初始化之前修改这个属性:

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

这句脚本的作用是把navigator.webdriver改回 undefined,让页面觉得当前浏览器不是自动化控制的。类似的思路也可以处理window.chrome等特征项,但我劝你别在对抗层面陷太深。这些特征修改只是提高“自然度”,不能保证百分百不被发现,目标站点的风控系统也在不断升级,越往后越要依赖真实设备、真实行为。

5.3 用 route 拦截请求,加快脚本运行速度

表单页面经常会在加载时请求一堆资源,但很多对填表单毫无帮助,比如统计脚本、图片素材、字体文件。用 Playwright 的route可以把它们拦掉,页面加载速度能提升不少:

def block_unused(route, request): if request.resource_type in ["image", "font", "media"]: route.abort() else: route.continue_() page.route("**/*", block_unused)

注意,拦截图片和字体有个副作用:页面布局可能会因为图片缺失而变化,某些点击坐标会错位。所以我更推荐按域名拦截,比如只拦截目标站点的统计域名和第三方广告域名,而不是一刀切掉所有图片。你可以先跑一次看日志,统计哪个域名资源占资源比最高,再做精准拦截。

5.4 资源释放与并发控制

跑批量表单任务时,最怕的不是脚本逻辑错,而是浏览器进程把服务器内存吃光。Playwright 每个浏览器实例都很重,开几十个进程是灾难。

我的实践方法是:一次自动化任务只开一个浏览器进程,每个任务用一个独立 context 和一个独立 page。任务结束后,显式关闭 context 和 page,而不是依赖 Python 垃圾回收。如果用异步 API 的 asyncio 模式,可以用信号量控制并发数,比如同时只跑 5 个上下文。

import asyncio from playwright.async_api import async_playwright async def worker(sem, task): async with sem: # 每个任务创建独立 context 并执行表单操作 pass async def main(): sem = asyncio.Semaphore(5) await asyncio.gather(*[worker(sem, task) for task in tasks]) asyncio.run(main())

并发数不是越大越好,还要考虑目标站点的承受能力和自身的 IP 资源。我曾经一上来就开 20 个并发去跑某个后台系统的批量导入,结果对方的并发限制直接把我一部分请求 429,反倒拖慢了整体进度。后来把并发调到 8,脚本稳定性至少翻了一倍。

5.5 合规和边界意识

这部分我不会回避。表单自动化这件事,本质上是在模拟人的行为,如果用在授权环境里,它是提效利器;如果用在未经允许的站点上,就可能涉及越过访问控制、滥用接口等问题。我自己的底线是三条:

  • 只对你有账号、有业务关系的系统做自动化,优先用于内部系统或测试环境。
  • 不只盯着能不能跑通,还要控制频率,给目标站点留出正常的处理余量。
  • 遇到明确的验证码、风控拦截、用户协议禁止自动化的站点,主动停手,该人工就人工。

守住这些边界,你才能放心大胆地把这套技术用在真正有价值的地方。

6. 常见问题与排查实战速查

6.1 定位超时:元素找不到怎么办

错误提示基本是TimeoutError: locator.click: Timeout exceeded while waiting for locator。这种问题 90% 是选择器不够准确或者页面动态渲染没完成。排查顺序:先看脚本运行时页面截图(我习惯在每个关键操作前加截图),确认元素是否真实存在;再检查元素是不是在 iframe 里,如果在 iframe 就必须用 frame_locator;最后再考虑是不是有多个元素命中了同一个 locator,加.first或收窄定位表达式。

6.2 点击按钮提示“元素被遮挡”

表单页面经常有固定定位的悬浮层、弹窗遮罩,或者按钮上方盖着一个透明元素,导致 Playwright 认为按钮虽然可见但不可稳定点击。解决办法是先滚动到目标位置:

button = page.get_by_role("button", name="提交申请") button.scroll_into_view_if_needed() button.click()

如果滚动也不管用,很可能是遮罩层盖住了按钮,那就得先关闭弹窗或隐藏遮罩层,再执行点击。

6.3 新标签页和弹窗处理

点击“下载模板”或“在新窗口打开”这类按钮时,会弹出新标签页。Playwright 不能用老的driver.switch_to_window思路,它的方式是用expect_page捕获新页面,这个前面已经写过。弹窗 alert 和 confirm 则需要用page.on("dialog")事件监听,默认情况下不处理会阻塞页面。

page.on("dialog", lambda dialog: dialog.accept())

6.4 浏览器进程残留

脚本异常退出后,Chromium 子进程经常赖在内存里,我被迫养成了写入异常清理的习惯:

try: with sync_playwright() as p: ... finally: if browser: browser.close()

批量任务里如果发现内存频繁爆涨,可以在启动前用系统命令把残留的 chrome 进程清理掉,再启动新一轮任务。

6.5 常用问题排查速查表

现象可能原因推荐排查方向
元素定位超时选择器过弱 / 元素在 iframe / 页面未加载完成截图确认,检查 iframe,改用 get_by_role 或 get_by_label
点击无反应元素被遮罩层阻挡 / 按钮 disabled检查是否有 modal,等待按钮变成 enabled
下拉选不到目标项次级选项异步加载未完成在 select_option 前增加 wait_for_selector
登录后脚本失效登录态过期 / cookie 被清重新登录并更新 storage_state
页面加载一直 networkidle 失败页面有轮询请求改用 domcontentloaded 和显式元素等待
脚本一跑就被风控拦截指纹不一致/频率过高检查 UA、viewport、时区,降低并发和速度
上传文件无效上传框是自定义组件定位内部 input[type=file] 并调用 set_input_files
内存持续增长context 和 page 未关闭显式 close,控制并发放到合理水平

这表里的大部分问题,我在真实项目里都踩过。定位超时是最频繁的,但也是最好解决的,只要养成“先截图、再查 DOM、最后调定位器”的习惯,基本都能快速定位。

我个人还有一个压箱底的小技巧:跑脚本之前,先在 Playwright 自带的 trace 模式下跑一遍,它会把每个操作和页面状态录制成一个可视化文件,后面看回放的时候,你一眼就能看出脚本是在哪一步歪掉的:

context = browser.new_context() context.tracing.start(screenshots=True, snapshots=True, sources=True) # 执行表单流程 context.tracing.stop(path="trace.zip")

这个 trace 文件可以用 Playwright 自带的查看器打开,非常直观。某种程度上,它比日志和截图加起来都好用,因为它是按时间轴完整还原了页面交互过程。

说了这么多,最后再补一句实在的:Playwright 表单自动化真正考验人的地方,不是某个 API 记不记得住,而是你能否把一个业务流程拆成可等待、可重试、可恢复的状态步骤。脚本只是把流程固化下来而已。如果每做一个新任务,都先画一遍状态表,把每个关键动作的成功信号和失败分支想清楚,写出来的代码会稳很多。把这一点养成习惯,后面不管遇到多绕的表单,你都不会慌。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进