固定 sleep 的代价:一次「停留 110 秒、任务一步没走」的复盘

举报
yd_276135645 发表于 2026/11/07 06:10:05 2026/11/07
【摘要】 固定 wait_for_timeout 不是停留,而是什么都不看地等。实测一段自动化脚本 exit 0、日志写着 STAY DONE,页面却从第 1 秒到第 110 秒停在协议更新弹窗上一动不动——因为平台把按钮文案从「同意」改成「进入」、元素从 button 换成 a.eco-por-btn,选择器静默返回 None。补点后 20 秒就到位。结论:dwell 要由状态驱动(倒计时出现+准备态消失

先说结论

固定 wait_for_timeout(110000) 不是"停留 110 秒",而是"什么都不看地等 110 秒"。这两件事在成功路径上等价,在失败路径上差得很远:今天这段脚本跑完退出码 0,日志里写着 STAY DONE,而浏览器里的页面从第一秒到第 110 秒完全没变——它还卡在一个协议更新弹窗上,任务一步都没走。真正进到环境,是事后手工补点之后,20 秒内就完成了。

结论三条:

  1. dwell 必须由状态驱动,不能用固定 sleep。写"停留 N 秒"的地方,改成"轮询某个就绪信号,直到它出现,再加一个最短停留"。
  2. 失败必须能被判出来。脚本最后一定要输出一个"我做成了什么"的客观指纹(URL、关键文本、计数),没有指纹的脚本不算跑过。
  3. 弹窗会改版。按按钮文案匹配(同意)一旦平台把文案改成(进入),选择器就静默返回 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 只剩顶栏)

于是"进入环境"这件事有了三个可判定的信号:

  1. 倒计时出现:剩余时长 + \d{2}:\d{2}:\d{2} —— 说明容器已分配;
  2. 准备态出现又消失:环境准备中 从有到无 —— 说明环境真的起来了;
  3. 倒计时在走:两次采样数字不同(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 都删掉。它们在这两种情况下仍然是对的:

  1. 等待平台侧异步结算:积分、审核、队列消费这类"我在等别人"的环节,没有可观测的就绪信号,只能隔一段时间看结果(今天的积分就是分批到账,20 分钟内一条不增,之后一次性补齐)。
  2. 防触发限流的最短间隔:两次写请求之间必须有间隔,这时 sleep 是限流器,不是等待器。

但"停留一会儿让它算我完成"这类 dwell,属于第三条路——它是等待器,就必须有观测。今天我这段脚本的 110 秒,本质上是用 110 秒的沉默买了一个"看起来成功"的结论;而真实情况是:勾选 + 点进入之后 20 秒就到位了。

固定 sleep 最贵的地方不是慢,是它让失败和成功长得一模一样。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。