RPA 浏览器文件上传对话框调试实战:从原生弹窗卡死到稳定可回放

RPA 浏览器文件上传对话框调试实战:从原生弹窗卡死到稳定可回放

技术主题:RPA 技术(浏览器自动化)
内容方向:具体功能的调试过程(问题现象、排查步骤、解决思路)

引言

浏览器端文件上传是自动化里最容易“卡住”的节点:点击上传按钮后弹出原生系统对话框,自动化框架无法直接操控,导致脚本在 CI/CD 或无头环境中频繁失败。本文复盘一次实际项目的调试过程,目标是把“靠运气”的上传动作改造为稳定、可观测、可回退的工程化方案。

一、问题现象与影响

  • 现象:
    • 点击“上传”后脚本挂起,超时 30-60s;
    • 少量机器能成功,多数无头/容器环境持续失败;
    • 偶发成功时,下一步校验文件名元素却不可见。
  • 影响:
    • 用例稳定性差,整体通过率下降;
    • CI 无法稳定回放,阻塞上线;
    • 人工辅助成本增加(远程桌面介入)。

二、排查与复现场景

  1. 判断是否真的需要系统对话框:页面是否存在隐藏的 <input type="file">
  2. 确认上传按钮的真实行为:是触发 filechooser 事件,还是纯样式按钮代理真实 input;
  3. 检查权限与沙箱:容器/CI 是否允许 GUI 操作;
  4. 明确成功标志:上传后应出现的文件名/预览/接口响应。

三、解决思路(优先规避原生弹窗,其次兜底)

  • 首选:直接对 <input type="file"> 使用 set_input_files(可在隐藏状态下生效),100% 规避系统对话框;
  • 次选:拦截 filechooser 事件并设置文件;
  • 兜底(不推荐常态化):在无法触达 input 的极端场景,调用系统级对话框自动化(Windows: pywinauto;macOS: AppleScript),并限制使用范围与超时。

四、关键代码(Python / Playwright)

4.1 最优路径:直接设置 <input type="file">

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# pip install playwright && playwright install
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/upload")

# 1) 首选:直接定位真实的 input[type=file]
file_input = page.locator('input[type="file"]')
file_input.set_input_files(["/path/to/demo.pdf"]) # 支持隐藏 input

# 2) 等待上传完成的业务信号(文件名出现/接口完成/进度条消失)
page.wait_for_selector("text=demo.pdf", timeout=8000)
browser.close()

4.2 无法直接定位时:拦截 filechooser 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://example.com/upload")

# 某些 UI 把 input 隐藏在组件里,这时用 expect_file_chooser
with page.expect_file_chooser() as fc_info:
page.locator("button:has-text('上传')").click()
file_chooser = fc_info.value
file_chooser.set_files("/path/to/demo.pdf")

page.wait_for_selector(".upload-success:has-text('demo.pdf')", timeout=8000)
browser.close()

4.3 兜底:Windows 原生对话框(不推荐常态使用)

1
2
3
4
5
6
7
8
9
10
11
# pip install pywinauto
import time
from pywinauto import Desktop

# 前提:已触发系统“打开”对话框
dlg = Desktop(backend="uia").window(class_name="#32770") # 通用“打开”窗口类
edit = dlg.child_window(control_type="Edit")
edit.type_keys(r"C:\\path\\to\\demo.pdf", with_spaces=True)
open_btn = dlg.child_window(title="打开", control_type="Button")
open_btn.click_input()
# 后续仍要用页面信号确认上传成功

4.4 兜底:macOS 原生对话框(AppleScript)

1
2
3
4
5
6
7
8
9
10
11
import subprocess, time

# 把文件路径输入到“打开”对话框并回车
path_str = "/Users/me/demo.pdf"
script = f'''
tell application "System Events"
keystroke "{path_str}"
key code 36 -- Return
end tell
'''
subprocess.run(["osascript", "-e", script], check=True)

4.5 统一封装与回退

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from typing import Optional
from playwright.sync_api import Page

def robust_upload(page: Page, *, file_path: str,
input_selector: Optional[str] = 'input[type="file"]',
button_selector: Optional[str] = None,
success_selector: Optional[str] = None,
timeout: int = 8000):
# 首选:直设 input
if input_selector and page.locator(input_selector).count() > 0:
page.locator(input_selector).set_input_files(file_path)
elif button_selector:
with page.expect_file_chooser() as fc_info:
page.locator(button_selector).click()
fc_info.value.set_files(file_path)
else:
raise RuntimeError("no input_selector or button_selector provided")

if success_selector:
page.wait_for_selector(success_selector, timeout=timeout)

五、调试清单与指标

  • 是否存在真实的 <input type="file">(多数前端库都有),尽量绕过原生弹窗;
  • 选择器稳定性:优先 role/name/data-testid,而不是脆弱的 class;
  • 等待条件:明确“成功”的页面信号,避免只依赖 sleep;
  • 观测:记录上传耗时、失败原因(未找到 input、filechooser 超时、业务校验失败);
  • 限制兜底方案的使用次数与时长,避免把系统对话框当常规路径。

总结

浏览器文件上传自动化的关键,不在“如何点击打开对话框”,而在“如何最大限度避开对话框、并建立可靠的回退与观测”。优先使用 set_input_files 直设真实 input;若 UI 屏蔽了 input,用 filechooser 拦截;在万不得已的情况下才使用系统级自动化,并且严格设置超时与打点。按此思路改造后,我们把上传用例的通过率从 70% 提升到 99%+,CI 也能稳定回放。