按坐标点按钮,换了块屏幕就全错:自动化判据该怎么建
按坐标点按钮,换了块屏幕就全错:自动化判据该怎么建
凌晨,我往掘金发一篇稿子。
脚本跑完,终端打出一行 PUB_CLICK clicked,然后就没了。没有报错,没有异常,退出码 0。我打开草稿箱,那篇稿子安安静静地躺在里面,状态是「草稿」。
发布按钮点了,「确定并发布」也点了,页面没有任何红字,接口没有报错,但文章就是没发出去。
这种失败最折磨人——它不告诉你它失败了。
后来我花了一整轮排查,才发现问题出在两个地方:一个是坐标,一个是判据。而这两个问题,在我做自动化的这半个月里反复出现,只是每次换一张脸。
一、第一次失败:坐标是对的,屏幕不是
我的掘金发布脚本里,有一张这样的表:
const CATXY = {
'后端': [890, 169],
'前端': [983, 169],
'人工智能': [890, 210],
'开发工具': [983, 210],
'代码人生': [1076, 210],
'阅读': [1169, 210]
};
写这张表的时候,我是这么干的:手动点开「发布文章」面板,用 getBoundingClientRect() 把「人工智能」那一项的中心点读出来,抄进代码。然后发布,成功。于是它就成了"标准答案"。
直到这次,脚本点了 [890, 210],什么都没发生。
我把当前环境读出来:
VIEWPORT {"w":1433,"h":1363,"sw":2560,"sh":1440,"dpr":1.5}
而那张表诞生时的环境,视口宽接近 1600。发布面板是一个定宽 560px 的浮层,它贴着编辑器右侧排布,编辑器宽度一变,整个面板就往左挪。分类项是横向排列的网格,每一项宽 93px 左右——面板左移 160px,差不多就是平移了一格半。
也就是说,我那一次点击,落在了「后端」和「前端」中间的缝隙里。
这就是硬编码坐标的本质问题:它不是在描述"点哪里",它是在描述"在我当时那块屏幕上,那个东西恰好在哪"。 前者是意图,后者是快照。快照只在它被拍下的那一次有效。
更麻烦的是,同一台机器上的环境也会漂移。我的记忆里记着上一次读到的屏幕是 3840×2160、DPR 1.0;这次读到的是 2560×1440、DPR 1.5。中间隔着一次显卡驱动更新和一次显示器切换,我自己都不知道。
所以第一条教训很朴素:凡是你在代码里写死的像素坐标,都是一笔债。 它现在能跑,不代表明天能跑;它能跑,也不代表它是对的做法。
二、第二次失败:界面上有字,服务端不认
修好坐标之后,我又撞上了第二次失败。
这次分类点中了(我后来加了一行校验,读 .category-list .item.active 的文本,确认是「人工智能」)。发布按钮也点中了。但点完「确定并发布」,文章依然没出去,接口依然静默。
问题出在标签。
发布面板里,「添加标签」是必填项。我原来的脚本是先按 placeholder 找输入框:
var e = [...document.querySelectorAll('input,textarea')]
.find(x => /搜索添加标签/.test(x.placeholder || ''));
返回 {}——一个空对象,什么都找不到。
我把面板的 DOM 扒出来看,才明白为什么:
<div class="form-item-content">
<div class="tag-input select">
<div class="byte-select">
<div class="byte-select__content-wrap">
<div class="byte-select__placeholder">请搜索添加标签</div>
<input class="byte-select__input">
</div>
</div>
</div>
</div>
那句「请搜索添加标签」根本不是 input 的 placeholder 属性,它是一个兄弟节点的文本。真正的 input 干干净净,一个属性都没有。
这是一个很典型的组件库做法:占位文案要跟着主题走、要能做动画、要能响应聚焦状态,塞进 placeholder 属性里就不好控制了,于是拿一个 div 盖在上面。肉眼看上去,它就是一个带占位符的输入框;对代码来说,它是一个空输入框加一块贴纸。
改成按结构定位之后,输入框找到了:
var e = document.querySelector('.tag-input input.byte-select__input');
我往里面灌了「AI编程」,下拉项出来了,我去点它。然后界面上确实出现了「AI编程」四个字。
我一度以为这就成了。但发布还是失败。
真正的判据在 DOM 里:
<!-- 没选中时 -->
<input class="byte-select__input"> <!-- 里面是文本,不是一个标签 -->
<!-- 选中之后 -->
<span class="byte-select__tag byte-tag byte-tag--checked">
<span> AI编程 </span>
<i class="byte-icon byte-icon--close-circle"></i>
</span>
在输入框里敲了字,和标签被选中,是两件完全不同的事。 前者只是输入框的值,你一点别的地方它就散了;后者才是一个真实的、会被提交的数据对象,它有一个 × 可以删掉。
界面上两种状态长得几乎一模一样——都显示「AI编程」四个字。唯一的区别是:一个旁边有个小叉,一个没有。
这就是我想说的第二个问题:自动化里最危险的不是报错,是"看起来对了"。
三、判据是有等级的
这两次失败其实指向同一件事:我需要一个不会骗我的判断依据。
复盘下来,我把用过的判据分成三档。
弱判据:文字。
"页面上出现了这个词,说明成功了。" 这次我踩了个特别蠢的坑——我写了个 toast 抓取逻辑,把页面上所有包含「请 / 失败 / 成功 / 重试」的短文本收集起来当提示看。结果抓出来这么一串:
["保存成功", "请搜索添加标签", "请搜索添加专栏...", "请搜索添加话题...",
"我在知乎补发一篇稿子,连着失败了四次。", "近期发布频率过高,请24小时后重试~",
"### 成功判据不要看按钮文案"]
这里面只有第一个「保存成功」是真的提示。剩下的全是正文——因为我这篇稿子写的就是知乎的限流提示,原文里引用了那句「近期发布频率过高,请24小时后重试~」。我的抓取逻辑不区分正文和提示层,于是一股脑全捞了回来。
弱判据的问题不是它经常错,是它错了你不知道。你会拿着一堆噪音去推理,越推越偏。
中判据:DOM 状态和接口响应。
"页面上出现了 span.byte-select__tag,说明标签选上了。"
"接口返回了 err_no: 0,说明发布成功了。"
这两个比文字可靠得多,但都有前提。DOM 状态要求你真的知道成功态长什么样——我如果不知道有个 byte-select__tag,就只能靠眼睛看有没有「AI编程」四个字。接口响应要求你抓得到——这次我信心满满地开了 Network.enable,监听 loadingFinished 之后调 getResponseBody,想直接把服务端的 err_msg 打出来。结果 API_BODIES 是空的,一个都没抓着。
原因有两个:一是我的 URL 正则没覆盖到实际请求路径;二是 loadingFinished 之后响应体不保证还留在缓存里,尤其请求很快的时候。想抓响应体,更稳的做法是 Fetch.enable 在请求阶段就拦下来,或者干脆别抓,换个判据。
强判据:结果的对外可见状态。
"URL 跳到了 juejin.cn/published。"
"用另一个标签页打开文章页,标题在,正文 4762 字。"
这是最高一档。它不看过程,只看终态;它不看页面上写了什么,只看外部世界变成什么样了。这次真正让我确认发布成功的,就是最后读到的那一行:
URL_AFTER https://juejin.cn/published
前面所有的 PUB_CLICK clicked、所有的「界面上有 AI 编程四个字」,加起来都不如这一行有说服力。
四、把坐标换成结构
回到坐标的问题。既然写死像素是债,那该怎么写?
答案是:描述你想点的东西,而不是它在哪。
具体有三层,从糙到细。
第一层:按可见文本找。
function rectOfText(txt) {
var els = [...document.querySelectorAll('button,span,div,li,a,label')];
for (var e of els) {
var r = e.getBoundingClientRect();
if (r.width < 2 || r.height < 2) continue; // 隐藏的
if (r.y < 0 || r.y > innerHeight) continue; // 视口外的
if ((e.textContent || '').trim() !== txt) continue; // 文本不匹配
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
}
return null;
}
这一层最通用。它不需要你知道组件库的内部类名,只要按钮上写着「确定并发布」,你就能找到它。过滤掉 width < 2 的隐藏副本是关键——React 组件经常在 DOM 里留一份不在屏幕上的镜像,不过滤就会点到空气。
第二层:按结构类名找。
当你需要的元素没有文字(比如一个空输入框),就得靠结构:
var e = document.querySelector('.tag-input input.byte-select__input');
这里的 .tag-input 是业务语义("标签输入区"),.byte-select__input 是组件库约定("输入框")。两个拼起来,比任何一个单独用都稳。
第三层:验证状态类。
找到还不够,点完之后要看状态有没有变:
// 分类:看 active 类落在谁身上
var cat = document.querySelector('.category-list .item.active');
// 标签:看 tag 元素有没有生成
var chip = document.querySelector('.tag-input .byte-select__tag');
组件库一定会把状态暴露成类名,因为 CSS 要用它来画样式。这是免费的可观测性,不用就亏了。
三层加起来,逻辑就从「点 (890, 210)」变成了「找到写着『人工智能』的分类项 → 点它 → 确认 .item.active 现在是它」。屏幕怎么变,这段代码都不用改。
五、一个能复用的动作模板
把上面这些串起来,我现在每个关键动作都写成同一个形状:做之前先找,做之后先验。
async function act(name, findJs, verifyJs, maxRetry) {
for (var i = 0; i < (maxRetry || 3); i++) {
var p = JSON.parse(await ev(findJs));
if (!p.x) { console.log(name, 'NOT_FOUND'); return false; }
await click(p.x, p.y);
await sleep(1200);
var ok = await ev(verifyJs);
console.log(name, 'TRY' + i, 'OK=' + ok);
if (ok === true || ok === 'true') return true;
}
console.log(name, 'FAILED_AFTER_RETRY');
return false;
}
用起来是这样:
await act('分类',
findByText('人工智能'),
`(document.querySelector('.category-list .item.active')||{}).textContent.trim()==='人工智能'`);
await act('标签',
findBySel('.tag-input input.byte-select__input'),
`!!document.querySelector('.tag-input .byte-select__tag')`);
这个模板的价值不在它多聪明,在于它把失败提前了。
原来的写法里,坐标错了 → 点了个空 → 脚本继续往下走 → 点「确定并发布」→ 服务端拒绝 → 脚本退出码 0。你拿到的是一个"跑完了但没生效"的结果,得自己去猜是哪一步出问题。
换成这个模板之后,坐标错了 → 第一行的 TRY0 OK=false 就告诉你了。失败发生在离原因最近的地方。
判据的另一个作用,是把"事后猜"变成"当场报"。
六、为什么自动化特别容易栽在这上面
写到这里我想多说一句:这两个坑,人操作都不会踩。
人不会按坐标点按钮。人点「人工智能」,是因为他看见了那三个字在那儿。屏幕分辨率变了,他照样点得中——他找的是字,不是位置。
人也不会把"输入框里有字"当成"标签已选中"。人会看见那个小叉,知道自己选上了;没看见,就知道还差一步。
自动化难的从来不是"让它做",而是"让它知道自己做得对不对"。 人手上的每一个动作,都伴随着一次无意识的确认——眼睛扫一下,状态就知道变了没有。而脚本没有这双眼睛,你得亲手给它装上。
所以我现在写脚本,花在"怎么验证"上的时间,比花在"怎么点"上的多。
七、一份自查清单
最后一轮下来,我给自己留了这份清单,每次写新平台的脚本都过一遍:
关于定位
- 代码里有没有写死的像素坐标?有就换成按文本或类名找
- 找元素时有没有过滤
width < 2的隐藏副本? - 有没有过滤视口外的元素(
r.y > innerHeight)? - 空输入框找不到时,占位文案是不是藏在兄弟节点里?
关于验证
- 每个关键动作之后,有没有一个不依赖文字的状态判据?
- 这个判据能不能区分"我做了一半"和"我做完了"?(比如输入框有字 vs 生成了 tag 元素)
- 兜底判据是什么?(这次是 URL 跳
published)
关于误报
- 抓提示信息时,有没有限定
position: fixed/absolute? - 有没有排除正文区域?(我那篇讲限流的稿子,正文里全是"请24小时后重试")
- 抓接口响应时,正则是不是覆盖得太窄?
关于环境
- 脚本里的坐标是在什么视口下取的?现在还是那个视口吗?
那天凌晨最后跑成功的输出是这样的:
CAT_ACTIVE 人工智能
TAGOPT {"t":"AI编程","x":1063,"y":360}
CHIP1 AI编程 AI编程
CONFIRM {"x":1285,"y":830}
URL_AFTER https://juejin.cn/published
URL_AFTER 那一行之前的所有输出,都只是过程。真正说明问题的,只有最后那一行。
判断一件事成没成,永远去看它对外部世界造成了什么改变,而不是去看它自己说它做了什么。
这句话放在自动化里成立,放在很多别的地方,好像也成立。

浙公网安备 33010602011771号