You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python Selenium如何等待元素消失或变为stale状态 解决自动化脚本报错

Selenium 动态闪烁元素等待方案(记忆答题游戏实现)

场景说明

我正尝试通过代码实现地址 大脑记忆游戏 的答题逻辑,该游戏会连续展示多个闪烁数字,要求用户按倒序输入看到的数字。

游戏规则

  • 进入网站后有一个启动按钮,蓝色进度条会从左到右填满按钮,进度条填满前点击按钮不会触发逻辑,启动按钮ID为start,实测time.sleep(3)足够等待进度条加载完成
  • 点击启动按钮后页面刷新,首先展示提示:

The numbers are coming soon. Please pay attention...

  • 之后会出现多个闪烁数字,数字通过调整透明度实现闪烁效果,从完全可见过渡到完全不可见,一个数字消失后下一个数字才会出现,即使数值相同也是不同的DOM元素
  • 所有数字展示完成后会出现提示,同时展示0-9的数字键盘:

Please click on the number you just saw IN REVERSE ORDER.

  • 按倒序点击对应数字后,会出现展示答题结果的按钮,点击后进入下一题,总共有10道题。

元素结构

<div class="questionWrapper">
    <div class="question">
        <div class="gnumber_title">
            <div class="gend-tip">Please click on the number you just saw IN REVERSE ORDER.</div>
        </div>
        <div class="gnumber_btns" id="gbtn0" style=""><button data-n="9">9</button><button data-n="8">8</button><button data-n="7">7</button><button data-n="6">6</button><button data-n="5">5</button><button data-n="4">4</button><button data-n="3">3</button><button data-n="2">2</button><button data-n="1">1</button><button data-n="0">0</button></div>
    </div>
    <div class="answer" value="10">1</div>
</div>

元素交互逻辑

  • 所有题目都被questionWrapper类的div包裹,每个题目块内仅有一个gnumber_title类的元素,用于承载提示文本和闪烁数字
  • 该元素内任何时候都仅存在一个子元素,子元素的内容随时间变化,但相对于gnumber_title的XPath统一为./div
  • 答题阶段开始时,第一个提示文本对应的子元素类名为gstart-tip blink_me
  • 该元素消失后会替换为类名gflash-num的元素,其内容可通过.text属性获取
  • 该元素通过修改样式实现闪烁:样式从opacity: 1.000000;逐步变为opacity: 0.000000;,之后变为display: none;,最后被删除并替换为下一个gflash-num实例
  • 所有gflash-num元素展示完成后,会出现第二个提示文本,类名为gend-tip,和之前的提示、数字位于同一个XPath位置,同时类名为gnumber_btns的数字键盘变为可见
  • 点击和展示数字数量相同的按钮后,类名为answer的结果按钮会出现,点击进入下一题

原有实现&报错

原有代码

import time
from selenium import webdriver

Firefox = webdriver.Firefox()
Firefox.get('https://www.arealme.com/brain-memory-game/en/')

time.sleep(3)
Firefox.find_element_by_id('start').click()

questions = Firefox.find_elements_by_class_name("questionWrapper")
for q in questions:
    numbers = []
    title = q.find_element_by_class_name('gnumber_title')
    while True:
        if title.find_element_by_xpath('./div').get_attribute('class') != 'gstart-tip blink_me':
            break
    while True:
        if title.find_element_by_xpath('./div').get_attribute('class') == "gend-tip":
            break
        numbers.append(title.find_element_by_class_name('gflash-num').text)
        while True:
            if not title.find_element_by_class_name('gflash-num').get_attribute('style').startswith('opacity'):
                break
        while True:
            if not title.find_element_by_class_name('gflash-num').get_attribute('style').startswith('display'):
                break
    buttons = q.find_element_by_class_name("gnumber_btns")
    for n in reversed(numbers):
        buttons.find_element_by_xpath(f'.//*[text() = "{n}"]').click()
    time.sleep(0.5)
    q.find_element_by_class_name("answer").click()

报错信息

---------------------------------------------------------------------------
StaleElementReferenceException            Traceback (most recent call last)
<ipython-input-1-9d34e88c4046> in <module>
     20         numbers.append(title.find_element_by_class_name('gflash-num').text)
     21         while True:
---> 22             if not title.find_element_by_class_name('gflash-num').get_attribute('style').startswith('opacity'):
     23                 break
     24         while True:

c:\program files\python39\lib\site-packages\selenium\webdriver\remote\webelement.py in get_attribute(self, name)
    137         attributeValue = ''
    138         if self._w3c:
--> 139             attributeValue = self.parent.execute_script(
    140                 "return (%s).apply(null, arguments);" % getAttribute_js,
    141                 self, name)

c:\program files\python39\lib\site-packages\selenium\webdriver\remote\webdriver.py in execute_script(self, script, *args)
    632             command = Command.EXECUTE_SCRIPT
    633
--> 634         return self.execute(command, {
    635             'script': script,
    636             'args': converted_args})['value']

c:\program files\python39\lib\site-packages\selenium\webdriver\remote\webdriver.py in execute(self, driver_command, params)
    319         response = self.command_executor.execute(driver_command, params)
    320         if response:
--> 321             self.error_handler.check_response(response)
    322             response['value'] = self._unwrap_value(
    323                 response.get('value', None))

c:\program files\python39\lib\site-packages\selenium\webdriver\remote\errorhandler.py in check_response(self, response)
    240                 alert_text = value['alert'].get('text')
    241             raise exception_class(message, screen, stacktrace, alert_text)
---> 242         raise exception_class(message, screen, stacktrace)
    243
    244     def _value_or_default(self, obj, key, default):

StaleElementReferenceException: Message: The element reference of <div class="gflash-num"> is stale; either the element is no longer attached to the DOM, it is not in the current frame context, or the document has been refreshed

问题原因&解决方案

问题原因

原有代码采用死循环反复查询动态元素的属性,当元素刚好在查询瞬间被DOM移除时,就会触发StaleElementReferenceException。

解决方案

使用Selenium内置的显式等待工具,配合staleness_of预期条件,直接等待指定元素失效(从DOM中移除),该方法内部已经做了异常捕获,不会触发上述报错。
首先导入依赖:

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

修改后的核心逻辑:

import time
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

Firefox = webdriver.Firefox()
Firefox.get('https://www.arealme.com/brain-memory-game/en/')
# 全局显式等待实例,超时时间设为10秒
wait = WebDriverWait(Firefox, 10)

time.sleep(3)
wait.until(EC.element_to_be_clickable((By.ID, 'start'))).click()

questions = Firefox.find_elements(By.CLASS_NAME, "questionWrapper")
for q in questions:
    numbers = []
    title = q.find_element(By.CLASS_NAME, 'gnumber_title')
    # 1. 等待初始提示元素消失
    start_tip = title.find_element(By.XPATH, './div')
    wait.until(EC.staleness_of(start_tip))
    
    # 2. 收集闪烁数字
    while True:
        current_div = title.find_element(By.XPATH, './div')
        # 判断是否进入答题阶段
        if current_div.get_attribute('class') == 'gend-tip':
            break
        # 记录当前数字
        numbers.append(current_div.text)
        # 等待当前数字元素被移除
        wait.until(EC.staleness_of(current_div))
    
    # 3. 倒序点击数字
    buttons = q.find_element(By.CLASS_NAME, "gnumber_btns")
    for n in reversed(numbers):
        # 用data-n属性匹配比文本匹配更可靠
        buttons.find_element(By.XPATH, f'.//button[@data-n="{n}"]').click()
    
    # 4. 等待结果按钮可点击后进入下一题
    wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'answer'))).click()

如果追求更简单的实现,结合实测的固定时长,也可以直接用固定等待替换显式等待:初始提示后sleep(3),每收集一个数字后sleep(1),也能避开元素删除的时间点,不会触发报错。

内容的提问来源于stack exchange,提问作者Ξένη Γήινος

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 11:36:01