用接口而不是 DOM 做浏览器自动化:一次真实的重写记录
问题:脚本为什么总是坏
我给浏览器做了一套任务模块,最初所有操作都是「找元素、点按钮」:
document.querySelector('.task-center-view .cardItem')
.querySelector('button').click()
能用。但页面一升级就断——现在是 data-v-08ee82f2 这种编译期哈希,类名全变。
更麻烦的是拿不到数据。页面上只显示「已完成」「+30 积分」,任务周期、跳转地址、完成判据全在 DOM 里没有。想看「这个任务是每日还是每周」,只能靠标题里有没有「每日」两个字猜。
方向:接口比 DOM 稳
打开开发者工具看请求,页面自己调的是这么一组接口:
GET .../growth/bonus-beans
GET .../growth/sign
POST .../growth/sign {account_name}
POST .../growth/mission/query-mission-list-by-condition
POST .../growth/prize
关键发现:query-mission-list-by-condition 传 category_key: "DAILY_MISSIONS",返回的却是全部任务,不只是每日的。每项都带 rule_list[].mission_cycle_type(DAILY / WEEKLY)、is_finish、url、guide。
这比在 DOM 里凑信息强一个量级。原来是「标题里有每日就算每日」,现在是接口直接告诉你。
坑一:CSRF 会过期
写操作要一个请求头 csrf。我第一版从 cookie 里读 csrf-eco:
const csrf = document.cookie
.split(';')
.map(c => c.trim())
.find(c => c.startsWith('csrf-eco='))
.split('=').slice(1).join('=');
第一次跑通了。过了一会儿再跑,全部 403 CSRF unequal。
重开页面也不管用。查下来:cookie 里那份是旧的,新值要从 GET /api/get-ainfo 的响应头里取。
const res = await fetch('/api/get-ainfo');
const csrf = res.headers.get('csrf');
这个坑很阴:第一次能跑通,所以不会怀疑它;报错信息(CSRF unequal)指向「值不对」,真正的原因是「值过期」。
坑二:字段名报错和实际不一致
签到接口要一个账号名。我先按常见的驼峰写法:
{ accountName: 'xxx' }
返回:
400 parameter is error; accountName不能为空
报错说 accountName 不能为空,那就再传一遍 accountName——还是同样的错。
实际字段是 account_name,下划线。报错文案写的是驼峰,接口要的是下划线。照着报错改会一直绕不出来。
教训:报错文案不能当接口契约。抓一次真实的请求体比猜十次有用。
坑三:页面卡死,加超时没用
页面开久了之后,读状态从 1 秒变成 30 秒超时。不是某个接口慢,是fetch整体不响应。
第一反应是把超时从 30 秒调到 60 秒。没用,因为它不是慢,是不回。
真正有效的是重新导航一次:
try {
return await readState();
} catch {
await navigate(url);
await sleep(9000);
return await readState();
}
边界:哪些动作不该自动化
这套东西我划了两条线。
一、对外不可撤销的动作。比如给开源仓库点 Star。技术上能做,但点错了撤不回,而且这类动作往往还绑着另一套登录态。脚本改成先探登录态,能用就打开页面,不能用就如实报 auth-expired——而不是硬点然后猜结果。
二、需要人做判断的动作。比如「花积分兑换代金券」。弹窗里有实名认证要求、每日/每年封顶、违约条款。把这些替人点掉,等于替人同意条款。
所以脚本只做「算」:多少分、能换什么、每积分值多少钱、还差多少。至于换不换,留给人。
写法上的几条经验
- 默认只读。不带
--apply就只采集,不写。误操作的成本远高于多打一个参数。 - 失败要有名字。
auth-expired/no-csrf/cannot-reply这种 reason,比一个ok: false有用得多。 - 「0 条」是结论,不是失败。搜到 0 个结果、可抄的评论不足两条——这些是有效信息,不该和「请求挂了」混在一个布尔里。
- 别把超时时间当解药。慢和卡是两回事,后者要重连。
结果
重写之后,读一次状态从「点开页面、展开任务、逐个解析卡片」变成一次接口调用,拿到 14 项任务的完整信息(周期、积分、完成状态、跳转地址)。原来只能猜的东西现在都是字段。
脚本本身也短了:不依赖任何 class 名,页面改版不用动。
小结
浏览器自动化最容易犯的错,是把「会变的东西」当成契约。DOM 结构会变,class 名会变,报错文案和实际参数甚至可能不一致。相对稳的是接口——它有明确的入参出参,有错误码,能抓、能复现。
换成接口之后,脚本从一个「点击器」变成了一个「客户端」。这两者的维护成本差得很远。
- 点赞
- 收藏
- 关注作者
评论(0)