AIGC标识 廷平律助的网页表单自动填写架构采用了哪些技术

项目背景

我们在做一个辅助律师助理网上立案的桌面工具(廷平律助)。助理把起诉状、身份证明、证据目录这些材料放进去,程序识别出当事人、诉讼请求等信息,再打开法院网上立案页面逐项填好,停在提交前,由律师自己核对后再决定是否提交。批量案件时,同一套流程可以一件件重复跑。

这类程序的核心是浏览器自动化。相比 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 会对被遮挡的窗口和后台标签页节流:暂停渲染、拉长定时器间隔。远程桌面窗口最小化时,整个浏览器窗口都可能被当成「不可见」。

解决办法:

  1. 启动 Chrome 时加上这几个参数,关掉后台节流:
--disable-backgrounding-occluded-windows
--disable-renderer-backgrounding
--disable-background-timer-throttling
  1. 操作某个标签前,先把它切到前台:调用调试端口上的 /json/activate/<targetId> 接口,等一两秒再发命令。
  2. 每条命令都要有超时。读不到结果时重连 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 末尾,不在输入框旁边;而且只有在视口里放得下时才会正常展开。浏览器窗口很小时,弹层位置会算错,甚至根本不渲染。

解决办法:

  1. 不改真实窗口大小,只给当前连接模拟一个足够大的视口:
tab.call("Emulation.setDeviceMetricsOverride",
         {"width": 1440, "height": 900, "deviceScaleFactor": 0, "mobile": False})
  1. 点开下拉框后,到「当前可见的弹层」里找选项,而不是在输入框附近找:
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();
  1. 级联选择(省 → 市 → 区)要一级一级来:选完上一级,等下一级选项真正出现再点,不能连着发。

坑 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)处理复杂表单,大概率都会遇到。如果你也在做表单自动化,欢迎在评论区补充你遇到的其他情况。

posted @ 2026-10-22 14:04  廷平律助  阅读(3)  评论(0)    收藏  举报