廷平律助的网页表单自动填写架构采用了哪些技术
项目背景
我们在做一个辅助律师助理网上立案的桌面工具(廷平律助)。助理把起诉状、身份证明、证据目录这些材料放进去,程序识别出当事人、诉讼请求等信息,再打开法院网上立案页面逐项填好,停在提交前,由律师自己核对后再决定是否提交。批量案件时,同一套流程可以一件件重复跑。
这类程序的核心是浏览器自动化。相比 Selenium,我们选了更底层的 Chrome DevTools Protocol(CDP):Python 通过 WebSocket 直接给浏览器发命令,能控制输入、文件、视口和事件这些细节。这篇按「问题现象 → 原因 → 做法」整理了处理复杂表单时最常遇到的 5 类问题,代码做了简化,换成 Puppeteer / Playwright 思路也一样。
坑 1:给输入框赋值了,页面却「没收到」
最直接的写法是:
document.querySelector('#name').value = '张三';
在 React / Vue 写的页面里,这样改完,框里能看到字,但组件内部的状态没变,一提交还是空值,校验提示「必填」。原因是这类框架接管了 value 属性,只认它自己监听的 input 事件。
解决办法: 用原型上的原生 setter 赋值,再手动派发事件。
const el = document.querySelector('#name');
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
setter.call(el, '张三');
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
textarea 要换成 HTMLTextAreaElement.prototype。更稳的办法是先让元素获得焦点,再用 CDP 的 Input.insertText,它走的是真实的输入链路,框架一定能收到:
tab.call("Runtime.evaluate", {"expression": "document.querySelector('#name').focus()"})
tab.call("Input.insertText", {"text": "张三"})
富文本编辑器(contenteditable)也一样,优先模拟粘贴或 insertText,不要直接改 innerHTML。
坑 2:后台标签页「卡住」,命令一直不返回
程序跑着跑着,Runtime.evaluate 不返回了;切到那个标签看一眼,又马上好了。原因是 Chrome 会对被遮挡的窗口和后台标签页节流:暂停渲染、拉长定时器间隔。远程桌面窗口最小化时,整个浏览器窗口都可能被当成「不可见」。
解决办法:
- 启动 Chrome 时加上这几个参数,关掉后台节流:
--disable-backgrounding-occluded-windows
--disable-renderer-backgrounding
--disable-background-timer-throttling
- 操作某个标签前,先把它切到前台:调用调试端口上的
/json/activate/<targetId>接口,等一两秒再发命令。 - 每条命令都要有超时。读不到结果时重连 WebSocket 再试,而不是无限等待。
坑 3:文件上传框点不出来,或者被隐藏了
很多页面的「上传」按钮是一个样式化的 div,真正的 <input type=file> 藏在别处,甚至在 iframe 或 Shadow DOM 里。用 JS 没法给文件框赋值(浏览器出于安全不允许)。
解决办法: 用 CDP 的 DOM.setFileInputFiles,它可以直接把本地路径交给文件框。先用 DOM.getDocument 加 pierce: true 把 iframe 和 Shadow DOM 一起展开,找到 type=file 的节点:
root = tab.call("DOM.getDocument", {"depth": -1, "pierce": True})["result"]["root"]
def find_file_inputs(node, out):
if node.get("nodeName") == "INPUT":
attrs = dict(zip(node.get("attributes", [])[::2], node.get("attributes", [])[1::2]))
if attrs.get("type") == "file":
out.append(node["backendNodeId"])
for child in node.get("children", []) + node.get("shadowRoots", []):
find_file_inputs(child, out)
if "contentDocument" in node:
find_file_inputs(node["contentDocument"], out)
ids = []
find_file_inputs(root, ids)
tab.call("DOM.setFileInputFiles", {"backendNodeId": ids[0], "files": [r"D:\材料\起诉状.pdf"]})
如果文件框是点击按钮后才动态创建的,就用 Page.setInterceptFileChooserDialog 拦截系统文件对话框,在 Page.fileChooserOpened 事件里拿到 backendNodeId 再赋值,不会弹出真正的对话框。
坑 4:下拉框弹不出来,或者选项不在原位置
组件库的下拉框(Select、Cascader、DatePicker)有两个特点:选项列表通常挂在 body 末尾,不在输入框旁边;而且只有在视口里放得下时才会正常展开。浏览器窗口很小时,弹层位置会算错,甚至根本不渲染。
解决办法:
- 不改真实窗口大小,只给当前连接模拟一个足够大的视口:
tab.call("Emulation.setDeviceMetricsOverride",
{"width": 1440, "height": 900, "deviceScaleFactor": 0, "mobile": False})
- 点开下拉框后,到「当前可见的弹层」里找选项,而不是在输入框附近找:
const pop = [...document.querySelectorAll('.el-select-dropdown, .ant-select-dropdown')]
.filter(p => p.offsetParent !== null).pop();
const opt = [...pop.querySelectorAll('li')].find(li => li.innerText.trim() === '民间借贷纠纷');
opt && opt.click();
- 级联选择(省 → 市 → 区)要一级一级来:选完上一级,等下一级选项真正出现再点,不能连着发。
坑 5:用 sleep 等页面,快的时候浪费时间,慢的时候直接出错
time.sleep(3) 是最常见的写法,也是最不可靠的:网络快时白等,网络慢时 3 秒不够,后面的操作全错位。
解决办法: 把「等」写成「轮询一个条件」,条件满足就继续,超时就明确报错:
def wait(tab, js, timeout=30, step=0.5):
end = time.time() + timeout
while time.time() < end:
try:
if tab.ev(js):
return True
except Exception:
tab.reconnect()
time.sleep(step)
raise TimeoutError("等待超时: " + js[:60])
wait(tab, "!!document.querySelector('.upload-list .success')")
条件要写「业务上真正完成」的标志,比如「上传列表里出现了文件名」「保存后出现了成功提示」,而不是「按钮出现了」。另外,Runtime.evaluate 记得加 awaitPromise: True,这样可以直接在页面里 fetch 接口、等 Promise 返回结果。
额外一条:把「不能自动做的事」写进代码里
表单自动化最怕的不是填错,而是程序替人做了不该做的决定。我们的做法是在代码层面加一道守卫:所有点击操作都经过一个统一函数,遇到「提交」「确认提交」一类按钮直接拒绝,流程只能停在提交前,由人看过之后自己点。配合每一步的截图和日志,出了问题也能回放定位。
小结
| 坑 | 关键做法 |
|---|---|
| 赋值无效 | 原生 setter + input 事件,或 Input.insertText |
| 后台卡住 | 关闭后台节流参数 + 操作前激活标签 + 超时重连 |
| 文件上传 | DOM.setFileInputFiles,必要时拦截文件对话框 |
| 下拉框 | 模拟大视口 + 在可见弹层里找选项 + 级联逐级等待 |
| 等待 | 轮询业务条件,超时明确报错 |
这些问题和具体网站无关,只要是用 CDP 或基于 CDP 的工具(Puppeteer、Playwright)处理复杂表单,大概率都会遇到。如果你也在做表单自动化,欢迎在评论区补充你遇到的其他情况。

浙公网安备 33010602011771号