一句话总结:现代 Web 应用的多层弹窗(内容检测弹窗 → 发布设置弹窗 → 确认对话框)必须按顺序逐层关闭,上层弹窗会阻止下层交互。用轮询检测而非固定等待,用 offsetParent 检测可见性,确保弹窗真正消失后再操作下一层。
目录
- 问题场景
- 为什么不能直接点
- 正确的策略:逐层关闭 + 轮询检测
- 弹窗检测函数集的封装
- 完整流程示例
- 总结
问题场景
在某平台发布内容时,点击”下一步”后会依次出现多个弹窗:
- 错别字提示弹窗:检测到可能的错别字,提示确认或修改
- 内容检测弹窗:检查内容合规性,提供”仅基础检测”或”全面检测”
- 发布设置弹窗:包含 radio button(AI 声明)和”确认发布”按钮
这些弹窗不是浏览器原生的 alert/confirm,而是页面自定义的 div 弹窗(通过 CSS overlay 实现)。
更麻烦的是:检测过程需要 5~30 秒不等,无法用固定 sleep 解决。
为什么不能直接点
问题一:检测时间不确定
检测过程调用的是第三方 API,响应时间波动很大。
问题二:弹窗叠加阻挡
当检测弹窗(overlay)覆盖在页面上时,”确认发布”按钮虽然存在于 DOM 中,但:
1 2 3
| js(ws, "document.querySelector('.confirm-btn').click()")
|
因为 overlay 的 z-index 高于按钮,点击事件被遮罩层捕获了。
问题三:必须按顺序
1 2 3
| ❌ 跳过第1层直接点第3层 → 被遮罩挡住 ❌ 先关第3层再关第1层 → 第3层按钮被第1层遮住 ✅ 第1层 → 第2层 → 第3层 → 顺序执行
|
正确的策略:逐层关闭 + 轮询检测
核心思路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| def wait_for_dialog(ws, keyword, timeout=30, interval=0.5): """等待包含关键词的弹窗出现""" t0 = time.time() while time.time() - t0 < timeout: text = dialog_text(ws) if keyword in text: return text time.sleep(interval) return None
def wait_dialog_gone(ws, timeout=10, interval=0.3): """等待当前弹窗消失""" t0 = time.time() while time.time() - t0 < timeout: if not dialog_text(ws): return True time.sleep(interval) return False
|
弹窗检测函数
识别弹窗的核心是找到可见的 overlay/dialog 元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| def dialog_text(ws): """获取当前可见弹窗的文字内容""" return js(ws, """ (() => { const dialogs = document.querySelectorAll( '[class*="dialog"],[class*="modal"],[class*="overlay"],' + '[class*="mask"],[role="dialog"],[class*="popup"]' ); for (const el of dialogs) { if (el.offsetParent !== null) { return (el.innerText || '').trim().substring(0, 600); } } return ''; })() """) or ''
|
关键技巧:el.offsetParent !== null 用于判断元素是否可见。不可见的弹窗(如已关闭但 DOM 未清除)会被过滤掉。
弹窗检测函数集的封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| class DialogHandler: """弹窗处理器:自动检测和操作弹窗""" def __init__(self, ws): self.ws = ws def get_text(self): """获取当前可见弹窗的文字""" return dialog_text(self.ws) def has_text(self, keyword): """当前弹窗是否包含指定文字""" return keyword in self.get_text() def wait_for(self, keyword, timeout=30): """等待包含关键词的弹窗出现""" t0 = time.time() while time.time() - t0 < timeout: text = self.get_text() if keyword in text: return text time.sleep(0.5) return None def wait_gone(self, timeout=10): """等待当前弹窗消失""" t0 = time.time() while time.time() - t0 < timeout: if not self.get_text(): return True time.sleep(0.3) return False def click_button_in_dialog(self, button_text): """在当前弹窗中点击指定按钮""" return js(self.ws, f""" (() => {{ const dialogs = document.querySelectorAll( '[class*="dialog"],[role="dialog"]' ); for (const d of dialogs) {{ if (d.offsetParent === null) continue; const btns = d.querySelectorAll('button, a, span, div'); for (const b of btns) {{ if (b.innerText.trim() === '{button_text}' && b.offsetParent !== null) {{ b.click(); return true; }} }} }} return false; }})() """) def click_radio_in_dialog(self, label_text): """在当前弹窗中点击 radio label""" return js(self.ws, f""" (() => {{ const dialogs = document.querySelectorAll( '[class*="dialog"],[role="dialog"]' ); for (const d of dialogs) {{ if (d.offsetParent === null) continue; const labels = d.querySelectorAll('label'); for (const lb of labels) {{ if (lb.innerText.trim() === '{label_text}') {{ lb.click(); return true; }} }} }} return false; }})() """)
|
完整流程示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| def handle_publish_flow(ws): """处理发布流程中的多层弹窗""" handler = DialogHandler(ws) click_btn(ws, '下一步') text = handler.wait_for('错别字', timeout=5) if text: handler.click_button_in_dialog('提交') handler.wait_gone() text = handler.wait_for('内容检测', timeout=30) if text: handler.click_button_in_dialog('仅基础检测') handler.wait_gone() text = handler.wait_for('确认发布', timeout=30) if text: handler.click_radio_in_dialog('否') time.sleep(0.5) handler.click_button_in_dialog('确认发布') handler.wait_gone() return verify_published(ws)
|
弹窗消失的验证方法
不要只依赖 wait_gone(),有时弹窗 DOM 还在但不可见了。多重验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| def verify_no_overlay(ws): """确认页面上没有遮挡弹窗""" return js(ws, """ (() => { const dialogs = document.querySelectorAll( '[class*="overlay"],[class*="mask"]' ); for (const d of dialogs) { // 跳过完全透明的 overlay if (d.offsetParent === null) continue; const style = window.getComputedStyle(d); if (style.opacity === '0' || style.display === 'none') continue; return false; // 仍有可见遮罩 } return true; // 没有遮罩了 })() """)
|
注意
浏览器原生对话框 vs 自定义弹窗
- 浏览器原生(alert/confirm/prompt):用 CDP 的
Page.javascriptDialogOpening 事件
- 页面自定义弹窗(div + overlay):用本文的
offsetParent 检测 + 轮询
轮询间隔不要太小
- 0.5 秒比较合适——太频繁的检测会干扰页面性能
- 总超时时间根据场景设置(检测弹窗 30 秒,确认弹窗 10 秒)
弹窗内容可能动态更新
- 检测中的”加载中…”文字会变成结果文字
- 不要只判断一次文字内容,要轮询直到出现最终状态
善用 offsetParent
el.offsetParent === null 表示元素或它的父元素 display: none
- 这是区分”弹窗已关闭”和”弹窗还没出现”的好方法
总结:多层弹窗的自动化核心是”逐层关闭”——先出现的弹窗先处理,用 offsetParent !== null 检测可见性,用轮询替代固定等待。绝对不要跳过顺序或尝试同时操作多层弹窗。