表单自动化的鲁棒性:为什么 fill 填进去了,提交却没反应
自动化脚本最让人抓狂的一类问题:所有字段都填对了,值也读得回来,点提交却什么都没发生——不报错、不跳转、不发请求。
今天在一个后台表单上又遇到,排查了两小时。把诊断路径记下来,下次能少走很多弯路。
一、先确认一个信号:请求发出去了吗
这是唯一不会骗人的判断依据。
requests = []
page.on("response", lambda r: requests.append(r) if r.request.method == "POST" else None)
# ... 填表、点提交 ...
print([r.url for r in requests])
如果列表是空的,说明提交处理器根本没触发,问题在前端,不在接口。后面所有的排查都应该围绕「为什么没触发」,而不是去猜参数对不对。
如果请求发出去了但被拦,那才去看状态码和响应体。这两种情况的排查方向完全相反,先分清能省一半时间。
二、填进去 ≠ 模型更新
这是最常见的一层。page.fill() 在 DOM 层面是可靠的:它会设置 value 并派发 input 和 change 事件。但它更新的是原生元素,不是框架的响应式状态。
几种典型情况:
受控组件。React 的受控 input,值由 state 决定。直接改 value 可能被下一次渲染覆盖,或者只改了 DOM 没改 state。正确做法是通过真实键盘输入:
page.locator("#title").click()
page.keyboard.type("文本", delay=10)
富文本编辑器。TinyMCE、ProseMirror 这类编辑器,真正的内容在 iframe 或 contenteditable 里。直接改 iframe 的 DOM,编辑器内部的 model 不会变,提交时读到的是空的。
两条路:一是用编辑器自己的 API,二是模拟真实输入。
# 路子一:编辑器 API
page.evaluate("html => tinymce.get('editor').setContent(html)", html)
# 路子二:真实输入
frame = page.frame_locator("#editor_ifr")
frame.locator("body").click()
page.keyboard.type("正文", delay=10)
自定义组件。自己封装的标签选择器、下拉树、级联选择,通常有一个隐藏的原生 input 做实际存储。点击选项时,组件把值写进那个隐藏 input;如果你用 JS 直接 li.click(),可能只触发了视觉层的更新。
判断方法很简单:选完之后去读那个隐藏 input 的值。
page.evaluate("() => document.querySelector('#input-tag').value")
读到值说明模型更新了;读不到,说明你点的不是组件认的那个元素。
三、提交按钮为什么点不动
按钮看着能点,点了没反应,常见三种原因:
有遮罩层。弹窗、引导、悬浮广告盖在按钮上。elementFromPoint 能直接告诉你点到的到底是谁:
page.evaluate("""() => {
const el = document.elementFromPoint(x, y);
return el ? el.tagName + ' ' + el.className : 'none';
}""")
按钮在视口外。mouse.click(x, y) 用的是视口坐标,元素滚出屏幕后,这个坐标点到的就是别的东西。先用 scroll_into_view_if_needed(),再取 bounding_box() 点。这一点很容易忘,因为 locator.click() 会自动滚动,而 mouse.click() 不会。
事件绑定不在你以为的元素上。有些老系统把提交绑在 document 上做委托,或者绑在父容器。可以查一下:
page.evaluate("""() => {
const el = document.querySelector('#submit-btn');
const ev = window.jQuery ? jQuery._data(el, 'events') : null;
return { onclick: el.getAttribute('onclick'), events: ev ? Object.keys(ev) : null };
}""")
如果两个都是空的,说明处理器是原生 addEventListener 挂的,或者压根不在这个元素上。这时候换父容器点、用真实鼠标点,都是值得试的。
四、还有一种情况:点了,弹了个说明
有些表单第一次提交会弹一个「填写须知」之类的说明层,必须先关掉,再提交一次。
关掉之后要注意:这类浮层常常会重置页面的某些状态,比如把同意勾选取消掉。我这次就踩了这个坑——关完弹窗直接再点提交,一直失败,因为同意框被重置成未勾选,按钮其实是 disabled 的。
所以每次点提交前都该重新确认一遍前置条件:
page.evaluate("""() => ({
checked: document.querySelector('#agreement').classList.contains('checked'),
disabled: document.querySelector('#submit-btn').hasAttribute('disabled'),
})""")
五、绕不过去的时候,把成本降到最低
这次最后没绕过去:按钮能点、状态都对、就是不发请求。我判断是站点在某个我没找到的地方做了拦截。
继续硬啃的收益已经很低了。这时候正确的做法不是死磕,而是把人工成本压缩到最小:
- 把要填的内容整理成结构化的可复制文本
- 写清楚每个字段填什么、选哪个选项
- 让人只需要复制粘贴 + 点一下提交
三分钟的人工,比再花两小时猜实现划算得多。
六、几条经验
先用网络请求判断方向,不要一上来就换选择器。
填完之后读一遍,读的是组件实际使用的那个字段,不是你以为的那个。
点击之前看一眼 elementFromPoint,遮罩层是隐形杀手。
每次提交前重新校验前置状态,浮层会重置东西。
给自己留退路:脚本跑不通时,输出一份人能直接用的东西。自动化的目标不是 100% 无人值守,而是把人的介入成本压到最低。
能跑通是好事,跑不通的时候不浪费别人的时间,也是。
- 点赞
- 收藏
- 关注作者
评论(0)