Selenium(Python):sleep与显式等待的差异及失效问题排查
爬取机场航班信息时,用Selenium显式等待处理元素,运行几条航班后抛出StaleElementReferenceException异常,即便把该异常加入忽略列表、延长超时时间都没用。换成time.sleep(1)替代循环内的显式等待后,代码就能正常运行,疑惑为什么显式等待没法避免崩溃,而手动等待1秒却可行,原本以为两者效果一致。
原代码
import time from selenium import webdriver from selenium.common import NoSuchElementException, StaleElementReferenceException from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.chrome.options import Options options = Options() options.add_experimental_option("detach", True); driver = webdriver.Chrome(options) driver.get("https://www.flightradar24.com/airport/tun/arrivals") # 爬取到达航班 ignored_exceptions = (NoSuchElementException, StaleElementReferenceException) wait = WebDriverWait(driver, 5,ignored_exceptions=ignored_exceptions) try: consent = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'button[id="onetrust-accept-btn-handler"]'))) # 处理弹窗,点击同意 consent.click() except: print('弹窗处理出错') info = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR,'li[class="airport__flight-list-item border-b-2 delay-300 border-white w-full"]'))) # 获取所有航班条目 for i in info: # 遍历航班,只处理TU开头的航班 wait_i = WebDriverWait(i, 5, ignored_exceptions=ignored_exceptions) Flightnumber = wait_i.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'span[class="truncate text-sm text-gray-900"]'))).text if Flightnumber[:2] == 'TU': drop_down_button = wait_i.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'button[class="px-1"]'))) drop_down_button.click() # 点击右侧箭头展开航班详情 details = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR,'div[class="mx-2 grid grid-cols-24"]'))).text # 等待详情加载后打印 print(details)
报错信息
StaleElementReferenceException: Message: stale element reference: stale element not found at info = driver.find_element(By.CSS_SELECTOR,'div[class="mx-2 grid grid-cols-24"]').text
修改后正常运行的代码片段
for i in info: Flightnumber = i.find_element(By.CSS_SELECTOR, 'span[class="truncate text-sm text-gray-900"]').text if Flightnumber[:2] == 'TU': drop_down_button = i.find_element(By.CSS_SELECTOR, 'button[class="px-1"]') drop_down_button.click() time.sleep(1) details = driver.find_element(By.CSS_SELECTOR,'div[class="mx-2 grid grid-cols-24"]').text print(details)
原因分析
DOM动态刷新导致元素引用过期:
原代码提前一次性获取了所有航班的info列表,之后循环遍历这些元素。但点击下拉按钮后,页面可能会重新渲染部分DOM(比如其他航班条目或详情区域),导致之前获取的info列表中的元素引用失效,触发StaleElementReferenceException。显式等待虽然配置了忽略该异常,但当循环到已过期的元素时,等待逻辑无法重新获取新的元素引用,最终超时抛出异常。显式等待的作用范围不匹配:
原代码对单个航班元素i使用局部等待WebDriverWait(i, ...),但点击下拉按钮后引发的是全局DOM变化,局部等待无法应对这种全局的元素失效问题。详情元素定位缺乏唯一性:
定位详情的CSS选择器div[class="mx-2 grid grid-cols-24"]可能不是唯一的,显式等待可能找到旧的过期元素;而time.sleep(1)给了足够时间让新的详情元素完全加载并替换旧元素,此时再查找就能拿到有效的元素引用。
修复方案
核心思路是避免持有过期的元素引用,不要提前一次性获取所有航班元素,而是每次循环都重新查找当前页面的航班列表,同时优化显式等待的定位条件,确保操作的是最新的DOM元素。
优化后的完整代码
import time from selenium import webdriver from selenium.common import NoSuchElementException, StaleElementReferenceException from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.chrome.options import Options options = Options() options.add_experimental_option("detach", True) driver = webdriver.Chrome(options) driver.get("https://www.flightradar24.com/airport/tun/arrivals") ignored_exceptions = (NoSuchElementException, StaleElementReferenceException) wait = WebDriverWait(driver, 5, ignored_exceptions=ignored_exceptions) try: consent = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'button[id="onetrust-accept-btn-handler"]'))) consent.click() except: print('弹窗处理出错') # 循环处理,每次重新获取航班列表避免元素过期 processed_count = 0 while True: try: # 每次循环都重新获取当前页面的航班条目 info = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR,'li[class="airport__flight-list-item border-b-2 delay-300 border-white w-full"]'))) found = False for i in info: try: flight_num = i.find_element(By.CSS_SELECTOR, 'span[class="truncate text-sm text-gray-900"]').text if flight_num[:2] == 'TU': # 确保下拉按钮可点击,且关联当前航班 drop_btn = wait.until(EC.element_to_be_clickable( (By.CSS_SELECTOR, f'li:has(span:text("{flight_num}")) button[class="px-1"]') )) drop_btn.click() # 等待详情元素加载完成并获取文本 details = wait.until(EC.presence_of_element_located( (By.CSS_SELECTOR,'div[class="mx-2 grid grid-cols-24"]') )).text print(f"航班 {flight_num} 详情:\n{details}\n") processed_count += 1 found = True # 处理完一个后退出内层循环,重新获取航班列表 break except StaleElementReferenceException: # 遇到过期元素直接跳过,进入下一次循环 continue if not found: # 没有找到未处理的TU开头航班,退出循环 print(f"共处理 {processed_count} 个TU开头航班") break except Exception as e: print(f"循环出错: {str(e)}") break driver.quit()
关键优化点
- 每次循环重新获取航班列表:确保每次操作的都是当前DOM中的有效元素,避免持有过期引用。
- 精准关联元素:通过航班号定位对应的下拉按钮,避免误操作其他航班的按钮。
- 异常捕获后重试:遇到
StaleElementReferenceException时直接跳过,进入下一次循环重新获取元素。 - 显式等待条件优化:使用
element_to_be_clickable替代单纯的presence_of_element_located,确保元素不仅存在还可交互。
内容的提问来源于stack exchange,提问作者Dimbo123

