Python Selenium滚动ul提取所有li标签 解决到顶判断与去重问题
Selenium 虚拟滚动聊天记录全量提取方案
针对Electron内嵌聊天页的虚拟滚动机制(超长列表仅渲染可视区DOM,上下滚动时动态增删<li>节点),现有代码存在到顶判断不准、元素重复两个核心问题,以下是可直接落地的修复方案:
问题1:可靠判断滚动到聊天最顶部(无更多历史消息)
原有逻辑通过比对新旧列表首元素文本判断到顶存在明显缺陷:如果连续两条消息文本完全一致(比如用户连发相同内容、重复系统提示),会直接误判终止加载。
采用双重校验逻辑可100%准确判断到顶:
- 第一维度:通过JS获取
<ul>容器的scrollTop属性,当属性值为0时,说明容器已经滚动到最顶部位置 - 第二维度:校验顶部
<li>节点的唯一标识,连续2次滚动后顶部节点标识无变化,说明没有新的历史消息加载完成 - 补充容错:每次滚动后增加1-2秒等待时间,适配网络差时历史消息加载延迟的场景,避免误判
对应JS取值代码:
current_scroll_top = driver.execute_script("return arguments[0].scrollTop", the_ul)
问题2:列表元素去重,避免重复存储<li>
注意:Selenium的WebElement对象不能直接通过==或者set()去重,不同批次find_elements返回的WebElement是完全独立的Python实例,哪怕对应同一个DOM节点,对象比对结果也为False。
正确去重逻辑:
- 优先提取每个
<li>自带的唯一属性作为去重key,比如聊天应用普遍会给消息节点加data-id、message-id、id这类全局唯一属性,完全不会重复 - 如果找不到明确的唯一属性,组合多个特征生成唯一key:比如发送人字段+时间戳字段+消息文本的拼接值,重复概率可忽略
- 存储层用字典结构,key为上述唯一标识,value为提取到的消息内容,写入时自动去重,比列表
extend后再去重效率高很多 - 不要长期存储WebElement对象:虚拟滚动会移除不在可视区的DOM节点,长期持有元素引用会触发
StaleElementReferenceException失效异常,遍历节点时直接提取需要的文本、时间、发送人等内容存储即可
修复后完整代码
import time from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 初始化显式等待,超时时间10秒 wait = WebDriverWait(driver, 10) chat_frame = driver.find_element_by_xpath("//iframe[@class='embedded-electron-webview embedded-page-content']") driver.switch_to.frame(chat_frame) wait.until(EC.element_to_be_clickable((By.XPATH, '//ul[@aria-label="Chat content"]'))) the_ul = driver.find_element(By.XPATH,'//ul[@aria-label="Chat content"]') # 字典存储消息,key为消息唯一标识,自动去重 msg_dict = {} # 到顶判断计数器,连续2次判定到顶才终止,避免加载延迟误判 same_top_count = 0 print(f"Archiving Chat with {name} ...\n") while True: current_lis = the_ul.find_elements(By.TAG_NAME,'li') if not current_lis: break # 遍历当前渲染的所有消息,写入字典去重 for li in current_lis: # 优先取节点自带的唯一ID属性,可根据实际DOM结构调整属性名 msg_id = li.get_attribute("data-id") or li.get_attribute("id") # 无明确唯一ID时,组合多字段生成唯一key if not msg_id: sender = li.find_element(By.CSS_SELECTOR, ".sender-name").text if li.find_elements(By.CSS_SELECTOR, ".sender-name") else "" msg_time = li.find_element(By.CSS_SELECTOR, ".msg-time").text if li.find_elements(By.CSS_SELECTOR, ".msg-time") else "" msg_text = li.text msg_id = f"{sender}_{msg_time}_{msg_text}" # 新消息才写入,避免重复覆盖 if msg_id not in msg_dict: # 直接存需要的文本内容,不要存WebElement对象 msg_dict[msg_id] = li.text # 滚动到当前最顶部消息,触发更早历史消息加载 driver.execute_script("arguments[0].scrollIntoView({block: 'start'});", current_lis[0]) # 等待历史消息加载完成,可根据应用实际加载速度调整时长 time.sleep(1.5) # 到顶校验 current_scroll_top = driver.execute_script("return arguments[0].scrollTop", the_ul) new_top_li = the_ul.find_elements(By.TAG_NAME,'li')[0] # 取新、旧顶部节点的标识 new_top_id = new_top_li.get_attribute("data-id") or new_top_li.get_attribute("id") or new_top_li.text old_top_id = current_lis[0].get_attribute("data-id") or current_lis[0].get_attribute("id") or current_lis[0].text if current_scroll_top == 0 and new_top_id == old_top_id: same_top_count += 1 if same_top_count >=2: break else: same_top_count = 0 # 转成列表,反转后顺序为从最早消息到最新消息 all_msg_list = list(msg_dict.values()) all_msg_list.reverse() print(f"Total extracted messages: {len(all_msg_list)}")
调优提示
- 先通过开发者工具检查
<li>标签的属性,优先使用节点自带的唯一ID作为去重和判断依据,比拼接文本可靠性更高 - 如果应用加载速度不稳定,可以把固定
sleep替换成显式等待,检测到新的顶部节点出现后再继续执行,效率更高 - 如果聊天记录量级特别大,可以每提取100条消息就临时落盘存储,避免程序异常崩溃导致已爬内容丢失
内容的提问来源于stack exchange,提问作者A B
相关产品推荐
相关产品推荐

