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

SPA网站Selenium点击触发StaleElementReferenceException问题求助

解决方案:规避SPA页面的StaleElementReferenceException问题

你的核心问题在于一次性获取所有用户名元素后,点击第一个用户触发SPA重新渲染DOM,导致之前获取的元素全部失效。以下是针对性的修复方案:

关键改进思路

  1. 不要预先批量获取元素列表,而是按索引逐个重新定位元素,避免旧引用失效
  2. 点击前先提取用户名文本,防止DOM刷新后无法获取
  3. 对单个元素操作做异常捕获,失败后重试当前元素
  4. 处理分页逻辑,确保能抓取全部1000个用户

修复后的代码

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import StaleElementReferenceException, TimeoutException
import json
import random
import time

def scrape_user_rosters(driver):
    parent_dict = {"data": []}
    # 核心元素定位(需根据页面实际结构微调)
    USERNAME_XPATH = "//span[@class='hidden lg:flex font-semibold']"
    ROSTER_XPATH = "//span[@class='text-white hidden lg:flex font-semibold']"
    TITLE_USER_XPATH = "//div[@class='flex flex-col space-y-2']/span"
    NEXT_PAGE_XPATH = "//button[contains(@class, 'pagination-next')]"  # 替换为实际翻页按钮的定位

    page_num = 1
    while True:
        try:
            # 等待当前页用户列表完全可见
            WebDriverWait(driver, 20).until(
                EC.visibility_of_all_elements_located((By.XPATH, USERNAME_XPATH))
            )
            current_page_user_count = len(driver.find_elements(By.XPATH, USERNAME_XPATH))
            
            # 逐个处理当前页的用户
            for index in range(current_page_user_count):
                retry_count = 0
                max_retries = 3
                while retry_count < max_retries:
                    try:
                        # 每次循环重新定位当前索引的用户元素,避免stale引用
                        target_user = WebDriverWait(driver, 15).until(
                            EC.element_to_be_clickable((By.XPATH, f"({USERNAME_XPATH})[{index+1}]"))
                        )
                        # 提前获取用户名文本,防止点击后DOM刷新丢失引用
                        username = target_user.text.strip()
                        if not username:
                            break

                        target_user.click()
                        # 等待阵容数据加载完成
                        WebDriverWait(driver, 15).until(
                            EC.visibility_of_all_elements_located((By.XPATH, ROSTER_XPATH))
                        )
                        # 提取阵容数据
                        roster = [elem.text.strip() for elem in driver.find_elements(By.XPATH, ROSTER_XPATH)]
                        # 保存数据
                        parent_dict["data"].append({
                            "username": username,
                            "roster": roster
                        })
                        print(f"已抓取: 第{page_num}页 - {username}")

                        # 返回用户列表页面(根据实际页面调整:如果是弹窗则关闭,SPA路由切换则等待列表重新加载)
                        # 示例:如果是弹窗,添加关闭按钮定位并点击
                        # close_btn = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.XPATH, "//button[@aria-label='Close']")))
                        # close_btn.click()
                        # 等待用户列表重新渲染完成
                        WebDriverWait(driver, 15).until(
                            EC.visibility_of_all_elements_located((By.XPATH, USERNAME_XPATH))
                        )
                        break

                    except StaleElementReferenceException:
                        retry_count += 1
                        print(f"用户{index+1}引用失效,重试第{retry_count}次...")
                        time.sleep(random.uniform(0.5, 1.5))
                        continue
                    except TimeoutException:
                        print(f"用户{index+1}加载超时,跳过")
                        break

            # 处理翻页
            try:
                next_btn = WebDriverWait(driver, 10).until(
                    EC.element_to_be_clickable((By.XPATH, NEXT_PAGE_XPATH))
                )
                # 检查是否为最后一页(如果按钮禁用则停止)
                if "disabled" in next_btn.get_attribute("class"):
                    print("已到最后一页,结束抓取")
                    break
                next_btn.click()
                page_num += 1
                print(f"切换到第{page_num}页")
                # 等待旧页面元素失效,新页面加载完成
                WebDriverWait(driver, 15).until(
                    EC.staleness_of(driver.find_element(By.XPATH, USERNAME_XPATH))
                )
                time.sleep(random.uniform(1, 2))
            except TimeoutException:
                print("没有更多页面,结束抓取")
                break

        except Exception as e:
            print(f"第{page_num}页处理出错: {str(e)}")
            break

    # 保存数据(每100条自动保存一次,防止崩溃丢失)
    with open('user_rosters.json', 'w', encoding='utf-8') as f:
        json.dump(parent_dict, f, indent=2, ensure_ascii=False)
    print(f"抓取完成,共获取{len(parent_dict['data'])}条数据")
    return parent_dict

额外注意事项

  • 需根据页面实际结构调整翻页按钮和弹窗关闭按钮的定位表达式
  • 添加随机延迟random.uniform(),避免触发网站反爬机制
  • 可添加数据增量保存逻辑,比如每抓取100条就写入一次文件
  • 确保登录状态持久有效,SPA可能会在会话过期后自动登出,需添加登录重试逻辑

内容的提问来源于stack exchange,提问作者neo5_50

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 12:55:36