ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Selenium无限下拉页面自动化:滚动加载原理与稳定采集方案

Selenium无限下拉页面自动化:滚动加载原理与稳定采集方案 接手无限下拉页面自动化最常见的一个场景就是打开一个列表页数据一屏一屏地加载鼠标怎么滚都滚不到头。手动操作很容易但想用Selenium把这套流程自动化让脚本自己滚动、自己加载、自己判断结束第一步就卡住了——页面高度在变、数据在长、滚动条却不受控制。这篇文章就把我调试这类页面时沉淀下来的完整方案写清楚无限下拉的原理、Selenium模拟滚动的几种写法、怎么判断“真的滚到底了”、常见坑怎么排查以及最后拿得出手的一套稳定采集脚本。无论是做自动化测试、还是做数据整理这份实操路线都可以直接照搬。1. 无限下拉页面到底难在哪里1.1 无限下拉的实现原理无限下拉页面通常不是整页一次性返回数据而是靠前端JavaScript在用户滚动到接近底部时向后端接口发起请求把下一页数据追加到当前列表后面。常见触发方式有两种监听window.onscroll事件判断滚动位置是否接近文档底部或者用IntersectionObserver观察列表末尾的“哨兵元素”是否进入视口。不管哪种写法浏览器都要经过“滚动 - 触发事件 - 发起请求 - 渲染新节点”这一串流程。对纯手工操作来说这套流程是自然的看到内容出来了就继续往下滚。但对脚本而言最棘手的是时序。你滚动一下数据不一定马上就出现在DOM里接口可能还在路上渲染可能还有延迟。如果脚本不管三七二十一连续执行十次滚动那么前几次滚动触发的请求还没返回后几次滚动又发出去了结果就是列表中间出现大段空白或者脚本已经滚到了页面最底部后面的数据却还没渲染出来。还有一个隐蔽的点无限下拉页面的“底部”是一个动态概念。文档总高度会随着新内容的追加而不断变大所以你不能像静态页面那样只滚动一次就到终点。你必须循环执行“滚动 - 等待 - 检查 - 再滚动”这样的操作每一轮都要重新读取当前页面高度、重新确认当前页码的内容是否加载完成才有可能完整地拿到所有数据。1.2 常规点击脚本为什么拿不到完整数据很多人一开始会尝试绕过滚动直接用requests或者Selenium发起下一页请求。这个思路在某些分页页面可行但在无限下拉页面里往往会失败原因有三。第一数据接口的鉴权通常很复杂。有些页面在下拉加载时请求头里带了一串由前端JS动态生成的签名参数这些参数跟当前会话状态、时间戳甚至鼠标轨迹都有关系。你静态模拟请求签名校验就过不了返回的是一张验证码页面或者空数据。第二接口的翻页逻辑是乱序推流。很多信息流场景里数据并不是按页码顺序返回的而是基于推荐权重同一页的数据可能会在你滚动到第三屏之后再追加进来。你提前把所有页码请求一遍拿到的数据跟实际展示顺序不一致后面的解析逻辑全乱了。第三页面存在懒加载。有些列表项本身依赖滚动触发渲染图片、文本、按钮这些DOM节点都只有在进入可视区域附近时才会真正生成。你光发接口请求没有触发渲染DOM里依然拿不到对应的元素。这就是为什么我们最终还是要回到最基础的操作老老实实模拟用户滚动行为让页面自己去加载。所有校验逻辑、渲染逻辑、推荐排序都由页面自己处理脚本只负责驱动滚动和采集渲染好的结果。虽然看起来更“笨”但它是兼容性最高、最不容易被页面内部逻辑变化干扰的方案。2. 环境准备装好Selenium的三道工序在写滚动代码之前先把环境搭好。这一步看起来简单但版本匹配问题能卡住不少人。2.1 安装Selenium库与浏览器驱动的版本匹配首先是安装Selenium库本身。Python环境里直接执行pip install selenium如果你用的是国内网络环境可以考虑加一个镜像源执行pip install selenium -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后真正麻烦的是浏览器驱动。Selenium本身并不包含浏览器内核它需要通过浏览器驱动去控制本机已安装的浏览器。以Chrome为例你需要下载ChromeDriver而且ChromeDriver的版本必须和你本机Chrome浏览器的版本严格对应。查看Chrome版本的方法很简单在浏览器地址栏输入chrome://version/记下“Google Chrome”后面的版本号比如126.0.6478.127。然后去ChromeDriver的官方下载页面找同版本号的驱动。驱动版本和浏览器版本差一个小版本都可能启动时报“session not created”之类的错误。下载完成后Windows系统建议把chromedriver.exe放到Python解释器同一个目录或者单独建一个目录把它加进系统PATH环境变量。macOS和Linux系统一般喜欢把它放到/usr/local/bin目录并赋予可执行权限sudo mv chromedriver /usr/local/bin/ chmod x /usr/local/bin/chromedriver2.2 浏览器启动参数的最小配置集加载驱动完成后接下来配置启动参数。我平时处理无限下拉页面的最小配置集是这样的from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--start-maximized) options.add_argument(--langzh-CN) options.add_experimental_option(excludeSwitches, [enable-automation]) driver webdriver.Chrome(optionsoptions)--start-maximized是为了让浏览器窗口最大化打开。很多无限下拉页面在窗口较小时单屏幕能展示的条目数量变少滚动频率就会变高可能导致页面触发频率控制的频控逻辑反而更容易出事。--langzh-CN是设置语言避免一些页面因为浏览器语言环境返回不同的页面结构。excludeSwitches里的enable-automation是用来去掉浏览器顶部“Chrome正在受到自动软件的控制”的黄条提示。这个设置不影响页面正常加载但在某些对自动化敏感的页面里可以减少一些不必要的干扰因素。这里多说一句headless无头模式虽然启动快但在处理无限下拉页面时我不建议优先使用。因为很多前端框架在无头模式下会判定为低性能环境自动压缩图片、降低动画帧率甚至直接禁用懒加载效果。对于滚动加载这种强依赖渲染机制的页面无头模式比较容易出现“滚动没反应”、“高度不变化”这类奇怪现象。先用有头模式把逻辑跑通再考虑是否切换到无头模式优化部署顺序不能反。3. 模拟滚动的三种核心方法和代码3.1 方法一window.scrollTo按固定高度滚动最常用、最容易理解的做法是直接使用浏览器原生的滚动接口让页面滚动到文档底部。对应的JavaScript语句是driver.execute_script(window.scrollTo(0, document.body.scrollHeight);)每次执行这一句浏览器都会把滚动条一次性拉到底部。配合循环使用代码就是import time from selenium import webdriver driver webdriver.Chrome() driver.get(https://example.com/list) last_height driver.execute_script(return document.body.scrollHeight) while True: driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(2) new_height driver.execute_script(return document.body.scrollHeight) if new_height last_height: break last_height new_height需要注意一个兼容性问题有的页面高度不是存在document.body上而是存在document.documentElement上。稳妥的写法是同时读取两者取较大值def get_page_height(driver): return driver.execute_script(return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight);)这个方案适用于绝大多数整页滚动的场景比如博客文章列表、问答社区问题列表、电商搜索结果页。优点是代码简单、不需要额外定位元素、不受页面内部布局影响缺点是一次性滚到底中间不给你停下来的机会如果页面加载速度快还好加载速度慢就容易出现空白区域。3.2 方法二scrollIntoView滚动到目标元素第二种方法是scrollIntoView它允许你把某个指定元素滚动到可视区域。在无限下拉场景里通常配合“加载更多”按钮或列表末尾的哨兵元素使用。from selenium.webdriver.common.by import By element driver.find_element(By.CSS_SELECTOR, #load-more) driver.execute_script(arguments[0].scrollIntoView(true);, element) element.click()这种方式适合那些加了“加载更多”按钮的页面也适合页面里只有一个局部滚动容器的情况。它的核心优势是精准你说滚动到哪就滚动到哪不需要自己去计算滚动距离。但在无限下拉页面里使用场景往往不是点击按钮而是观察列表末尾的哨兵元素是否进入视口。如果你定位的哨兵元素本身还没被渲染出来find_element就会直接抛NoSuchElementException所以你必须做异常处理或者配合显式等待from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC try: sentinel WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, .list-sentinel)) ) driver.execute_script(arguments[0].scrollIntoView(true);, sentinel) except Exception: driver.execute_script(window.scrollTo(0, document.body.scrollHeight);)3.3 方法三模拟键盘End键触发加载第三种方法比较适合那些把滚动事件监听在局部容器节点上的页面。比如页面中间有一个固定高度的滚动区域区域内部才是可以滚动的列表这时候你用window.scrollTo去滚动整个页面发现根本不生效。这种情况下可以模拟键盘操作聚焦滚动区域后再按End键from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.by import By scroll_container driver.find_element(By.CSS_SELECTOR, .scroll-panel) scroll_container.click() scroll_container.send_keys(Keys.END) time.sleep(2)这个方法的思路是让浏览器认为用户正在通过键盘操作滚动条。很多页面监听的其实是整个文档的scroll事件但键盘操作同样能触发滚动效果和其他方法没有本质区别。唯一的坑是聚焦位置很重要焦点如果在页面顶部按End键可能只滚动了页面而不是局部容器。所以执行Send_keys之前一定要先click()一下目标容器确保焦点落在正确的滚动区域内部。3.4 三种方法的选择建议实际项目里我按下面的标准去选择场景特征推荐方法原因整个页面滚动列表撑满全屏window.scrollTo或window.scrollBy代码最少直接操作全局滚动条有“加载更多”按钮或哨兵元素scrollIntoView精准触发避免多次无效滚动局部滚动容器页面本身不滚动模拟键盘End键解决window.scrollTo无效的问题页面结构不明需要通用方案优先window.scrollTo失败时降级通用性最好可以作为默认方案你不需要把三种方法都背下来但最好都写在工具函数里遇到不同类型的页面时按需切换。我做自动化测试项目时通常会把滚动动作封装成单独的函数这样不论页面结构怎么变化调用方代码都不用改。4. 滚动加载的判定逻辑与完整案例4.1 怎么判断页面已经滚动到底部写滚动循环时最大的工程量不在“滚动”本身而在“什么时候停”。停错了数据少一半停不下来脚本就会陷入死循环。第一个判断指标是页面高度不再变化。我们每轮滚动后读取一次页面总高度和上一轮对比如果连续几轮高度都没变大概率就是加载完了。但要注意这个判断要设置一个连续次数阈值比如连续3次高度相同才判定结束防止因为网络抖动导致某一轮数据没加载出来而误判。第二个判断指标是可视窗口底部与页面底部的距离。深度的判断可以这样写y_offset driver.execute_script(return window.pageYOffset;) inner_height driver.execute_script(return window.innerHeight;) page_height driver.execute_script(return document.body.scrollHeight;) scroll_remaining page_height - (y_offset inner_height) if scroll_remaining 50: driver.execute_script(window.scrollTo(0, document.body.scrollHeight);)这个写法更适合那些会预留一段空白来加载下一页数据的页面。你判断出当前已经滚到底部附近了就再滚动一次触发加载。第三个判断指标是数据条数不再变化。在页面滚动的同时统计列表里已经渲染出的节点数量如果滚动多次后数量还是同一个数就说明没有新数据接上来。这个方法最适合那些列表项结构统一、底部存在固定内容的页面比如评论列表、搜索结果列表。4.2 滚动过程的数据去重与保存无限下拉有很多情况下会重复渲染同一条数据。比如页面自带滚动防抖把数据追加到列表头部再重新排序又比如断网重连后页面会自动重新加载一小段内容。如果脚本看到元素就采集最终结果里就会出现大量重复项。所以我在采集数据时强制给每条数据建一个唯一标识。标识可以是链接地址、商品编号、帖子ID或者标题加摘要的哈希值。每采集一条就往集合里塞一条。集合存在的意义就一句话重复数据不进结果。seen_ids set() data_list [] items driver.find_elements(By.CSS_SELECTOR, .list-item) for item in items: item_id item.get_attribute(data-id) if item_id and item_id not in seen_ids: seen_ids.add(item_id) data_list.append(item.text)>def append_to_file(path, record): with open(path, a, encodingutf-8) as f: f.write(record \n)每采到一条数据立刻追加一行。这样哪怕脚本中途停了已经采集到的数据也已经保存在本地。4.3 一个可复用的完整滚动采集脚本把前面所有环节整合起来一个我实际项目中直接使用过的完整脚本长这样import time import json from selenium import webdriver from selenium.webdriver.chrome.options import Options def get_height(driver): return driver.execute_script( return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight); ) def scroll_to_bottom(driver): driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) def main(): options Options() options.add_argument(--start-maximized) options.add_argument(--langzh-CN) driver webdriver.Chrome(optionsoptions) try: driver.get(https://example.com/list) time.sleep(3) last_height get_height(driver) same_height_count 0 seen_ids set() results [] while True: scroll_to_bottom(driver) time.sleep(2) items driver.find_elements( css selector, .list-item ) new_count 0 for item in items: item_id item.get_attribute(data-id) title item.find_element( css selector, .title ).text.strip() if item_id and item_id not in seen_ids: seen_ids.add(item_id) results.append({id: item_id, title: title}) new_count 1 new_height get_height(driver) if new_height last_height: same_height_count 1 if same_height_count 3: break else: same_height_count 0 last_height new_height with open(data.json, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2) print(采集完成共, len(results), 条) finally: driver.quit() if __name__ __main__: main()这里的关键点在于滚动使用全局window.scrollTo数据读取用CSS选择器定位列表项去重用>
返回列表