固定 sleep 的代价:一次「停留 110 秒、任务一步没走」的复盘
先说结论
固定 wait_for_timeout(110000) 不是"停留 110 秒",而是"什么都不看地等 110 秒"。这两件事在成功路径上等价,在失败路径上差得很远:今天这段脚本跑完退出码 0,日志里写着 STAY DONE,而浏览器里的页面从第一秒到第 110 秒完全没变——它还卡在一个协议更新弹窗上,任务一步都没走。真正进到环境,是事后手工补点之后,20 秒内就完成了。
结论三条:
- dwell 必须由状态驱动,不能用固定 sleep。写"停留 N 秒"的地方,改成"轮询某个就绪信号,直到它出现,再加一个最短停留"。
- 失败必须能被判出来。脚本最后一定要输出一个"我做成了什么"的客观指纹(URL、关键文本、计数),没有指纹的脚本不算跑过。
- 弹窗会改版。按按钮文案匹配(
同意)一旦平台把文案改成(进入),选择器就静默返回None,链路断在这里,而日志里只有一句同意: None——看起来像"这次没有弹窗"。
一、事故现场:exit 0,任务一步没走
下面是打出来的关键几行(为省篇幅删了中间链路):
LINK: 前往AI DevSpace实践 ... task-action ... y=583
new2: ['https://developer.huaweicloud.com/devspace?utm_source=officeagent']
立即进入: {'x': 729.5, 'y': 622, 'tag': 'BUTTON'}
URL2: https://devstation.developer.huaweicloud.com/aidevspace?utm_source=officeagent
BODY2: AI 开发者空间 | 华为云开发者服务协议、隐私声明及开发平台协议更新提醒 |
本次更新范围及内容:| 1、开发者空间开发平台服务协议:新增发布作业环境时协议条款。
| 我已阅读并同意上述 《华为云开发者服务协议》...
同意: None
AFTER: (同一段文案,一个字没变)
stay 110s ...
STAY DONE https://devstation.developer.huaweicloud.com/aidevspace?utm_source=officeagent
URL2 和 STAY DONE 后面的 URL 一模一样,AFTER 和 BODY2 一模一样。这就是固定 sleep 的经典输出:它把"没动"渲染成了"做完了"。
二、根因不是选择器写错,而是状态不可知
脚本里找弹窗按钮的写法是:
bs = [...document.querySelectorAll('button')]
.filter(e => e.offsetParent!==null && (e.innerText||'').trim()==='同意')
页面这次弹的是一份协议更新提示,主按钮文案是「进入」,勾选框是 input.por-native(原生 checkbox 套在 div.eco-checkbox 里),外层容器 class 是 eco-protocol-notice。也就是说:
- 文案维度:
同意→进入,匹配 0 个; - 元素维度:这次不是普通
button,是a.eco-por-btn; - 交互维度:它不是"点一下就过",而是"先勾选、按钮才可点"的两段式。
三种变化叠在一起,任何"按文案找按钮"的脚本都会静默失败。而 None 又被当成"没有弹窗要处理"顺手跳过,于是错误被彻底吸收。
三、把 dwell 换成观测:必须先打印的三个信号
手工补点的时候我刻意把过程拆成可观测的几步,数据如下:
| 时刻 | 页面文本指纹 |
|---|---|
| 勾选前 | 华为云开发者服务协议...更新提醒 |
勾选 input.por-native 后 |
checked: False → True |
点 a.eco-por-btn(进入)后 8s |
剩余时长 02:59:59 / 环境准备中,请勿关闭窗口... |
| 再 20s | 剩余时长 02:59:34,环境准备中 消失(IDE 在 iframe 里,body 只剩顶栏) |
于是"进入环境"这件事有了三个可判定的信号:
- 倒计时出现:
剩余时长+\d{2}:\d{2}:\d{2}—— 说明容器已分配; - 准备态出现又消失:
环境准备中从有到无 —— 说明环境真的起来了; - 倒计时在走:两次采样数字不同(
02:59:59→02:59:34)—— 说明页面还活着,不是渲染卡住。
第 3 条最容易被忽略。只判一次 环境准备中 不在字符串里,可能撞上"页面还没渲染"的空白态;判两次数字变化,才能区分"起来了"和"白屏"。
四、弹窗改版的容错:按语义锚点找,别按按钮文案找
改版挡不住,但可以让脚本"知道发生了什么"。我的做法是把弹窗识别从"找按钮"换成"找容器 + 找它内部所有可点项":
def find_gate(page):
return page.evaluate("""() => {
const ks = ['protocol','agreement','notice','consent'];
const box = [...document.querySelectorAll('div,section')].find(e =>
e.offsetParent !== null && ks.some(k => (e.className||'').toString().includes(k))
&& /同意|协议|隐私/.test(e.innerText||''));
if (!box) return null;
const clickable = [...box.querySelectorAll('input[type=checkbox],a,button,label')]
.filter(e => e.offsetParent !== null)
.map(e => { const r = e.getBoundingClientRect();
return {tag: e.tagName, cls: (e.className||'').toString().slice(0,60),
t: (e.innerText||'').trim().slice(0,30),
x: r.x + r.width/2, y: r.y + r.height/2}; });
return {text: (box.innerText||'').slice(0,120), clickable};
})()""")
拿到 clickable 之后按类型而不是文案行动:先点唯一的 input[type=checkbox],再点容器里最后一个 a/button(在这种"同意并进入"的弹窗里,主按钮几乎总在末尾)。文案只用来写日志和判断"这是不是协议弹窗",不用来定位。
再配一条兜底:只要页面文本里出现 协议 / 更新提醒 且 URL 没变超过 30 秒,就认定被门挡住了,直接判失败并 dump 文本,而不是继续 sleep。
五、可以直接抄的骨架
import time
def observe(page, seconds=120, step=3):
"""轮询直到就绪;返回 (ok, 最后一次指纹)"""
seen, last, ok = [], None, False
deadline = time.time() + seconds
while time.time() < deadline:
snap = page.evaluate("document.body ? document.body.innerText.replace(/\\s+/g,' ') : ''")
timer = re.search(r"剩余时长\s*(\d{2}:\d{2}:\d{2})", snap)
ready = "环境准备中" not in snap and timer is not None
fingerprint = (bool(timer), timer.group(1) if timer else None,
"环境准备中" in snap, len(snap))
if fingerprint != last:
seen.append((round(time.time()), fingerprint))
last = fingerprint
if timer and seen and len({s[1][1] for s in seen}) > 1:
ok = True # 倒计时在走
break
page.wait_for_timeout(step * 1000)
return ok, seen
要点:
- 判据是"信号变化",不是"sleep 结束"。这里只要倒计时出现过两个不同值就收工,通常 10~20 秒,而不是硬等 110 秒。
seen一定打印。它同时是日志和证据:[(t, (True,'02:59:59',True,312)), (t, (True,'02:59:34',False,268))]一眼就能看出环境起来了。- 失败路径要有明确出口:
ok=False时把snap[:300]和find_gate()的结果一起打出来,下次改版能直接看出是哪个容器。
六、什么时候固定 sleep 仍然合理
我不主张把所有 sleep 都删掉。它们在这两种情况下仍然是对的:
- 等待平台侧异步结算:积分、审核、队列消费这类"我在等别人"的环节,没有可观测的就绪信号,只能隔一段时间看结果(今天的积分就是分批到账,20 分钟内一条不增,之后一次性补齐)。
- 防触发限流的最短间隔:两次写请求之间必须有间隔,这时 sleep 是限流器,不是等待器。
但"停留一会儿让它算我完成"这类 dwell,属于第三条路——它是等待器,就必须有观测。今天我这段脚本的 110 秒,本质上是用 110 秒的沉默买了一个"看起来成功"的结论;而真实情况是:勾选 + 点进入之后 20 秒就到位了。
固定 sleep 最贵的地方不是慢,是它让失败和成功长得一模一样。
- 点赞
- 收藏
- 关注作者
评论(0)