问题现象
脚本运行一段时间后突然报错:
selenium.common.exceptions.StaleElementReferenceException:
Message: stale element reference: element is not attached to the page document
明明 xpath 是对的,元素也看得见,就是点不了。
根本原因
Selenium 的 WebElement 对象本质上是一个指向 DOM 节点的远程引用。当你拿到一个元素对象之后,如果页面发生了任何变化——跳转、局部刷新、DOM 重排——这个引用就失效了,变成"过期引用"。
最常见的触发场景:
- 页面跳转后还拿着上一页的元素对象
- 循环里先
find_elements拿到列表,然后对列表里的元素逐个操作,操作过程中 DOM 更新了 - 等待条件满足后,又重新用旧的元素对象
解决方案
方案一:每次操作前重新查找(最稳)
# 错误写法:拿到引用后反复用
rows = driver.find_elements(By.XPATH, "//table//tr")
for row in rows:
row.click() # 第一次可能成功,第二次 Stale
# 正确写法:用索引,每次重新查找
rows = driver.find_elements(By.XPATH, "//table//tr")
for i in range(len(rows)):
driver.find_elements(By.XPATH, "//table//tr")[i].click()
# 每次都重新查找,引用永远是 fresh 的
方案二:封装重试逻辑
from selenium.common.exceptions import StaleElementReferenceException
def safe_click(driver, by, locator, max_retries=3):
for attempt in range(max_retries):
try:
el = driver.find_element(by, locator)
el.click()
return
except StaleElementReferenceException:
if attempt == max_retries - 1:
raise
time.sleep(0.5) # 等 DOM 稳定
# 使用
safe_click(driver, By.XPATH, "//button[@id='submit']")
方案三:用 WebDriverWait 配合 expected_conditions
from selenium.webdriver.support import expected_conditions as EC
# 不要先存元素对象,等真正要操作时再等它可点击
WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, "//button[@id='submit']"))
).click()
# EC.element_to_be_clickable 每次都会重新查找,不会持有过期引用
⚠️ 核心原则:不要在变量里"存" WebElement 对象超过一次操作周期。即拿即用,用完就丢。
我的最终方案
在 MSD 自动化项目里,我封装了一个 find_and_act 工具函数:
def find_and_act(driver, by, locator, action="click", text=None, timeout=10):
"""即找即做,绝不持有过期引用"""
wait = WebDriverWait(driver, timeout)
el = wait.until(EC.presence_of_element_located((by, locator)))
if action == "click":
wait.until(EC.element_to_be_clickable((by, locator))).click()
elif action == "send_keys":
el.clear()
el.send_keys(text)
elif action == "get_text":
return el.text
return None
所有操作都走这个函数,项目里 StaleElement 问题基本归零。