一句话总结:现代 Web 应用的多层弹窗(内容检测弹窗 → 发布设置弹窗 → 确认对话框)必须按顺序逐层关闭,上层弹窗会阻止下层交互。用轮询检测而非固定等待,用 offsetParent 检测可见性,确保弹窗真正消失后再操作下一层。


目录

  1. 问题场景
  2. 为什么不能直接点
  3. 正确的策略:逐层关闭 + 轮询检测
  4. 弹窗检测函数集的封装
  5. 完整流程示例
  6. 总结

问题场景

在某平台发布内容时,点击”下一步”后会依次出现多个弹窗:

  1. 错别字提示弹窗:检测到可能的错别字,提示确认或修改
  2. 内容检测弹窗:检查内容合规性,提供”仅基础检测”或”全面检测”
  3. 发布设置弹窗:包含 radio button(AI 声明)和”确认发布”按钮

这些弹窗不是浏览器原生的 alert/confirm,而是页面自定义的 div 弹窗(通过 CSS overlay 实现)。

更麻烦的是:检测过程需要 5~30 秒不等,无法用固定 sleep 解决。

为什么不能直接点

问题一:检测时间不确定

1
2
# ❌ 固定等待不可靠
time.sleep(10) # 10秒不够怎么办?30秒又太浪费

检测过程调用的是第三方 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)

# 第1步:点击"下一步"触发发布
click_btn(ws, '下一步')

# 第2层:错别字提示(可能没有)
text = handler.wait_for('错别字', timeout=5)
if text:
handler.click_button_in_dialog('提交') # 忽略错别字,直接提交
handler.wait_gone()

# 第3层:内容检测弹窗
text = handler.wait_for('内容检测', timeout=30)
if text:
handler.click_button_in_dialog('仅基础检测')
handler.wait_gone()

# 第4层:发布设置弹窗(含 radio + 确认按钮)
text = handler.wait_for('确认发布', timeout=30)
if text:
# 选择 AI 声明 = 否
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; // 没有遮罩了
})()
""")

注意

  1. 浏览器原生对话框 vs 自定义弹窗

    • 浏览器原生(alert/confirm/prompt):用 CDP 的 Page.javascriptDialogOpening 事件
    • 页面自定义弹窗(div + overlay):用本文的 offsetParent 检测 + 轮询
  2. 轮询间隔不要太小

    • 0.5 秒比较合适——太频繁的检测会干扰页面性能
    • 总超时时间根据场景设置(检测弹窗 30 秒,确认弹窗 10 秒)
  3. 弹窗内容可能动态更新

    • 检测中的”加载中…”文字会变成结果文字
    • 不要只判断一次文字内容,要轮询直到出现最终状态
  4. 善用 offsetParent

    • el.offsetParent === null 表示元素或它的父元素 display: none
    • 这是区分”弹窗已关闭”和”弹窗还没出现”的好方法

总结:多层弹窗的自动化核心是”逐层关闭”——先出现的弹窗先处理,用 offsetParent !== null 检测可见性,用轮询替代固定等待。绝对不要跳过顺序或尝试同时操作多层弹窗。