☰
Selenium无限下拉页面自动化:滚动加载原理与稳定采集方案
2026/9/29 15:33:58 网站建设 项目流程

接手无限下拉页面自动化,最常见的一个场景就是:打开一个列表页,数据一屏一屏地加载,鼠标怎么滚都滚不到头。手动操作很容易,但想用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/chromedriver

2.2 浏览器启动参数的最小配置集

加载驱动完成后,接下来配置启动参数。我平时处理无限下拉页面的最小配置集是这样的:

from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--start-maximized") options.add_argument("--lang=zh-CN") options.add_experimental_option("excludeSwitches", ["enable-automation"]) driver = webdriver.Chrome(options=options)

--start-maximized是为了让浏览器窗口最大化打开。很多无限下拉页面在窗口较小时,单屏幕能展示的条目数量变少,滚动频率就会变高,可能导致页面触发频率控制的频控逻辑,反而更容易出事。

--lang=zh-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", encoding="utf-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("--lang=zh-CN") driver = webdriver.Chrome(options=options) 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", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2) print("采集完成,共", len(results), "条") finally: driver.quit() if __name__ == "__main__": main()

这里的关键点在于:滚动使用全局window.scrollTo;数据读取用CSS选择器定位列表项;去重用>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询