
接到一个需求要把某个无限下拉页面里的数据完整扒下来。打开浏览器一看页面像个无底洞往下滚一屏加载一屏永远看不到尽头。用Selenium去自动化这个场景说来不复杂但真写起来坑不少——滚动太快触发不了加载、滚动太慢耗时翻倍、页面中途崩了要重新跑、有些站点还会识别你是自动化脚本直接给你返回空数据。这篇文章就把我从启动浏览器到稳定拿到全部数据的过程中积累的方案、代码和教训一次说清楚。1. 无限下拉页面的自动化需求从哪来1.1 哪些场景必须面对无限滚动无限滚动Infinite Scroll已经是Web开发的标配交互了最常见的应用场景包括电商商品流、社交媒体信息流、视频推荐列表、评论区和博客归档页。这类页面不会一次性把全部数据塞给浏览器而是通过监听滚动事件当用户接近页面底部时向后端接口发起请求拿到下一批数据渲染出来。做自动化的人遇到无限滚动页面一般就两种情况。第一种是做数据采集比如要分析某个电商平台某一类目下的全部商品信息要批量抓取用户评论做舆情分析要把某个内容平台的历史文章全部存档。第二种是做UI自动化测试验证滚动加载这个功能本身是否正常比如滚到第几屏触发了加载、加载出来的内容是否完整、用户快速滚动时页面有没有卡死报错。这两种需求对滚动策略的要求差别很大测试只需要模拟用户滚动几下断言结果采集则需要持续滚动直到把整棵数据树都摇下来。本文主要讲的是采集场景——因为它对稳定性的要求最高所有坑都会暴露出来。1.2 无限滚动的底层机制你不需要会前端开发也知道怎么模拟滚动但完全不了解底层机制的话遇到滚动无效加载不出来这类问题时会非常被动。常见的无限滚动实现方式有三种scroll事件监听最古老的方案。页面监听window或容器的scroll事件判断scrollTop clientHeight是否接近scrollHeight如果接近就触发加载。IntersectionObserver现代主流方案。在页面底部放一个哨兵元素当这个元素进入视口时触发加载回调。相比scroll事件性能更好监听更精准。固定分页的伪装版滚到某位置时前端直接加载下一批并且URL的hash或path跟着变比如某些论坛的下一页式滚动。这种其实可以当分页处理属于特例。不管用哪种方式Selenium模拟滚动的本质都一样让页面认为用户已经滚到相应位置了。理解这一点后面所有的代码逻辑就都能想通。1.3 为什么新手方案总是只抓到第一屏很多人第一次写无限滚动采集用的是最简单粗暴的逻辑循环查找页面里某个元素如果没有就滚动一下再找。代码大概是这样的items [] while True: page_items driver.find_elements(By.CSS_SELECTOR, .item) if len(page_items) 0: driver.execute_script(window.scrollTo(0, document.body.scrollHeight)) time.sleep(2) else: items.extend(page_items) break这代码会有几个很明显的问题第一个.item从第一屏就存在find_elements永远都能找到循环第一次就break了根本没触发滚动。第二个就算你把条件改成数量不再增加才滚动由于滚动后元素数量变化需要渲染时间瞬时判断往往不准确。第三个time.sleep(2)这种固定等待在网速好、加载快的机器上浪费大量时间网速差的时候又可能等不够。真正的难点在于无限滚动页面的加载是异步的采集逻辑必须和页面渲染状态同步。你既要模拟滚动又要感知页面加载完成的信号还要知道什么时候真的到底了。这三件事缺一不可。2. 滚动刷新的本质与Selenium的三种滚动姿势2.1 方案一execute_script直接操作DOM滚动最常用也是最稳定的方式是借助Selenium的execute_script方法执行JavaScript直接操作页面滚动。核心命令是window.scrollTo()或window.scrollBy()。# 瞬间滚到页面最底部 driver.execute_script(window.scrollTo(0, document.body.scrollHeight)) # 每次向下滚动一屏的高度 screen_height driver.execute_script(return window.innerHeight) driver.execute_script(fwindow.scrollBy(0, {screen_height}))scrollTo是直接跳到指定坐标scrollBy是在当前位置基础上增量滚动。模拟用户行为的话scrollBy一屏一屏往下滚更真实因为真实用户不会瞬间从顶部飞到最底部——那种瞬移式的滚动在某些站点会被判定为异常行为。很多前端里滚动的不是window而是某个固定高度的容器比如div classfeed-scroll。这种情况就不能用window.scrollTo了得找到那个容器再滚container driver.find_element(By.CSS_SELECTOR, .feed-scroll) driver.execute_script(arguments[0].scrollTop arguments[0].scrollHeight, container)判断标准很简单地址栏还在但是页面上有内部滚动条那就是容器滚动没有内部滚动条只有浏览器滚动条就是window滚动。动手之前先确认这一点能省不少时间。2.2 方案二ActionChains模拟键盘事件除了直接执行JSSelenium的ActionChains也可以模拟键盘的END键或方向键触发滚动。浏览器里按END键会直接跳到页面底部连续按PAGE_DOWN会一屏一屏往下翻。from selenium.webdriver.common.keys import Keys actions ActionChains(driver) actions.send_keys(Keys.END).perform()这个方式的价值在于某些极端的前端页面直接调scrollTo触发的滚动事件监听会被浏览器判定为非用户行为从而被过滤掉多见于带复杂埋点和行为分析的前端框架。键盘事件模拟的是真实的用户输入通道在触发滚动监听时兼容性更好。缺点是速度不可控连续快速按END键时页面滚动动画没结束可能就执行了下一次导致部分屏幕被跳过。我一般把键盘方案当辅助手段用。在直接JS滚动失效时先用send_keys(Keys.END)测试一下页面是否响应如果响应了再评估要不要彻底切换到键盘方案。2.3 方案三滚动到指定元素还有一种思路是scrollIntoView。它的作用是让指定的元素滚动到视口中类似手动把页面滚到某个元素的位置。target driver.find_element(By.XPATH, //div[classload-more]) driver.execute_script(arguments[0].scrollIntoView(true), target)这种方式特别适合页面里存在明确的加载更多按钮或底部哨兵元素的场景。滚动到哨兵元素触发它进入视口自然就触发了加载。相比scrollTo滚到页面最底部scrollIntoView更精准不会出现滚过头了哨兵元素一闪而过没触发加载的情况。2.4 三种方案的对比与选型建议方案实现方式适用场景优点缺点execute_script scrollTo/scrollByJS直接操作绝大多数标准无限滚动页面速度快、可控性强、可精确控制滚动距离个别反检测站点会过滤JS滚动事件ActionChains END/PAGE_DOWN键盘事件模拟标准方案失效时的替补更接近真实用户操作通道速度不可控、容易跳过部分屏幕scrollIntoView滚动到指定元素存在哨兵元素或加载更多按钮的页面触发精准、不易漏加载需要先找到目标元素找不到就白搭实际项目中我通常以execute_script为主力scrollIntoView作为局部辅助键盘方案作为兜底。没有绝对最优的方案只有针对当前页面最合适的组合。3. 从启动浏览器到稳收数据的完整实现3.1 环境准备与浏览器配置老生常谈的Selenium安装和WebDriver下载就不多啰嗦了假设你已经装好了selenium库并且浏览器驱动版本和浏览器版本对应得上。这里重点说的是浏览器启动参数它对无限滚动采集的稳定性影响非常大。from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() # 禁用自动化特征标记 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) # 去掉Chrome正在受到自动软件控制的提示条 options.add_argument(--disable-blink-featuresAutomationControlled) # 无头模式会节省大量资源但个别站点对无头环境识别严格 # options.add_argument(--headlessnew) # 确保中文等特殊字体能正常渲染 options.add_argument(--langzh-CN) # 禁用图片加载能显著降低带宽压力但可能影响懒加载逻辑判断 # options.add_argument(--blink-settingsimagesEnabledfalse) driver webdriver.Chrome(optionsoptions) # 覆盖navigator.webdriver属性降低被识别为自动化程序的风险 driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, {get: () undefined}) })需要特别提醒无头模式headless和禁用图片这两项会改变前端渲染行为。有些页面在无头模式下不加载任何数据有些页面因为有图片懒加载逻辑图片禁掉之后整个加载逻辑就卡住了。我的建议是第一轮调试先不开无头模式用有头模式确认逻辑跑通了再测试无头模式是否兼容。采集任务量大且稳定之后再切无头跑。3.2 核心滚动循环的设计滚动的核心循环目标是一直滚到不能再滚为止。但不能再滚的判定需要技巧后面第4节专门讲。这里给出一个基础循环结构import time from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By scroll_count 0 max_scroll 200 # 安全上限防止页面异常情况下无限循环 last_height driver.execute_script(return document.body.scrollHeight) for _ in range(max_scroll): # 1. 执行滚动 driver.execute_script(window.scrollBy(0, window.innerHeight)) scroll_count 1 # 2. 等待新内容出现隐性条件页面高度变大或元素数量增加 try: WebDriverWait(driver, 5).until( lambda d: d.execute_script(return document.body.scrollHeight) last_height ) except: # 高度没变化可能到底了也可能是加载失败 pass new_height driver.execute_script(return document.body.scrollHeight) if new_height last_height: # 连续N次高度无变化才判死 # 这里先break后面第4节再优化 break last_height new_height这个循环有几个注意点滚动用scrollBy(window.innerHeight)而不是scrollTo(scrollHeight)每步只滚一屏给页面留出触发加载的机会也更像真实用户。WebDriverWait里的lambda等待条件是在等待高度变化。如果5秒内高度没变就认为这一屏没加载出东西。立刻比较高度并break的做法太激进因为有些页面加载间隔比较长需要多次滚动才会触发下一次加载。更稳妥的做法是记录连续没有高度变化的次数达到3次再break。3.3 数据采集与去重滚动只是手段采集才是目的。常见的设计模式是边滚边采每次滚动后把当前页面上所有目标元素抓一遍跟已采集的数据做对比只保留新增的。def extract_items(driver): 从当前DOM中提取所有目标元素返回去重后的新数据 elements driver.find_elements(By.CSS_SELECTOR, .feed-item) new_items [] seen_keys set() # 用元素的一个唯一属性做去重键 for el in elements: key el.get_attribute(data-id) if key and key not in seen_keys and key not in collected_keys: new_items.append({ id: key, title: el.find_element(By.CSS_SELECTOR, .title).text, url: el.find_element(By.CSS_SELECTOR, a).get_attribute(href), }) seen_keys.add(key) return new_items为什么强调去重无限滚动页面非常容易重复渲染同一批数据而且由于页面滚动时DOM节点会被销毁重建有的框架用虚拟列表同一个元素前后两次抓取到的引用完全不同。如果不去重数据量会翻倍膨胀。用业务ID做去重键是最可靠的如果没有明显的ID退而求其次用标题URL拼一个键。3.4 完整代码示例把上面的思路串起来给一个可以直接改改就能用的完整脚本import csv import time from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.by import By def setup_driver(): options Options() options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_argument(--disable-blink-featuresAutomationControlled) options.add_argument(--langzh-CN) driver webdriver.Chrome(optionsoptions) driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, {get: () undefined}) }) return driver def collect_feed(driver, url, max_scroll300, export_filefeed.csv): driver.get(url) time.sleep(3) # 等待首屏渲染 collected {} # key - item dict stagnant_times 0 last_height driver.execute_script(return document.body.scrollHeight) for i in range(max_scroll): # 滚动一屏 driver.execute_script(window.scrollBy(0, window.innerHeight)) time.sleep(1.5) # 给加载请求留出时间 # 抓取新元素 elements driver.find_elements(By.CSS_SELECTOR, .feed-item) for el in elements: try: item_id el.get_attribute(data-id) title el.find_element(By.CSS_SELECTOR, .title).text link el.find_element(By.CSS_SELECTOR, a).get_attribute(href) except Exception: continue if item_id and item_id not in collected: collected[item_id] {id: item_id, title: title, link: link} # 判定是否还在加载 new_height driver.execute_script(return document.body.scrollHeight) if new_height last_height: stagnant_times 1 if stagnant_times 3: print(f已到底部共滚动{i1}次采集{len(collected)}条) break else: stagnant_times 0 last_height new_height # 导出CSV with open(export_file, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[id, title, link]) writer.writeheader() writer.writerows(collected.values()) print(f导出完成{export_file}) if __name__ __main__: driver setup_driver() try: collect_feed(driver, https://example.com/feed) finally: driver.quit()这个脚本追求的是清晰和稳执行效率还有优化空间。比如time.sleep(1.5)是固定等待更优的做法是用WebDriverWait动态等待加载完成的信号。但从工程角度说固定等待在绝大多数场景下够用而且代码更简单、更好排查问题。我自己的经验是先把固定等待的版本跑通拿到正确数据再去优化等待方式避免一上来就陷入性能调优的黑洞里。4. 判定滚到底了的几种方法与边界情况4.1 高度对比法scrollHeight的谎言前文代码里用的是document.body.scrollHeight前后对比来判断页面是否还有新内容。这个方法简单但有一个致命缺陷scrollHeight包含的是文档内容高度而无限滚动页面会根据当前已加载内容动态调整这个值。页面加载是异步的滚动脚本执行后scrollHeight可能不是立刻变化。更麻烦的是有些页面会把整个文档高度固定在一个值上内容全部放在一个容器里容器内部滚动。这时候document.body.scrollHeight根本就不变你滚动window一辈子也触发不了加载。对这种容器型无限滚动页面判断是否到底要看的是容器自己container driver.find_element(By.CSS_SELECTOR, .feed-scroll) scroll_top driver.execute_script(return arguments[0].scrollTop, container) client_height driver.execute_script(return arguments[0].clientHeight, container) scroll_height driver.execute_script(return arguments[0].scrollHeight, container) is_bottom scroll_top client_height scroll_height - 5滚动判断、是否到底的判断必须和实际滚动的对象保持一致。这是页面看起来滚了实际一点没动问题最常见的根源。4.2 干扰项处理懒加载图片、内嵌滚动容器、虚拟列表判定到底的准确性还受到三类干扰项的影响。**第一类是懒加载图片。**页面里图片高度可能在加载之前是0加载之后撑开容器导致scrollHeight变化进而干扰到底了的判定。你可能会遇到滚了很多次高度还在涨但其实数据已经很久没新增了的情况。解决思路是判断是否到底时不要只看scrollHeight可以同时观察目标元素数量是否还在增加。如果高度在涨但目标元素数量连续几次没变那就说明变化的只是图片之类的内容不算新数据。**第二类是内嵌滚动容器。**页面里可能有一个大容器套着好几个小滚动区域你的目标数据藏在其中一个内嵌容器里。怎么发现这种情况在浏览器控制台里先手动执行一下滚动看是哪个元素出现滚动条。Selenium定位到那个元素后把滚动动作从window切换成容器scrollTop。**第三类是虚拟列表。**这是最难搞的情况。虚拟列表Virtual List为了性能只渲染视口附近的数据你滚到哪它渲染到哪。这种页面用Selenium滚动抓取非常吃力因为DOM里的元素永远只有那么几十个之前抓到的元素会被销毁。应对方案有两个边滚边采并且每次滚动后用URL或ID去重只要在元素销毁前抓到数据就行。虚拟列表滚得快元素存在时间短所以滚动速度要放慢给采集逻辑留时间。绕过前端渲染直接抓接口。在Network面板里看加载下一页的XHR请求拿到接口地址和参数用requests直接请求。这种方法对虚拟列表而言效率高得多。4.3 超时与重试机制滚动采集跑久了页面难免偶发加载失败——请求超时、接口报500、前端渲染异常。此时如果不做处理脚本会误判到底了提前结束或者卡在某个等待里死循环。我的习惯是引入连续无新增次数和整体超时两个维度MAX_STAGNANT 5 # 连续5次滚动没有新元素才算到底 MAX_SCROLL 500 # 总滚动次数硬上限 SCROLL_TIMEOUT 8 # 单次等待加载的秒数动态等待用硬上限很重要。有些页面加载接口会间歇性失败你以为到底了其实是接口挂了有些页面加载逻辑有bug会无限重复加载同一批数据。设置滚动次数上限至少保证脚本不会永远跑下去。数据量级大时配合断点续跑每次滚动记录当前进度重启后从上次位置继续能省很多时间。另外滚动循环里如果某个WebDriverWait超时了先不要断言到底了而是做一个页面状态检查def page_healthy(driver): try: driver.execute_script(return document.readyState) return True except Exception: return False如果页面挂了比如崩溃白屏直接driver.refresh()重新加载然后根据已采集的数据量决定是从头重来还是跳到上次位置。5. 反爬虫防护下的滚动策略调整5.1 滚动速度与随机性无限滚动采集做多了你会发现一个规律**跑得快的数据质量差跑得慢的反而不容易出问题。**这个慢指的不是单次操作慢而是操作节奏要像一个真实用户。真实用户的滚动有什么特征滚动快慢不均有时快速翻几屏有时停留几秒慢慢看内容。程序员手动滚动时还经常有往上滚回来看之前的东西的操作。这些行为模式很难完全模拟但我们可以做到几点滚动距离随机化不要每步都固定滚一个window.innerHeight有时候滚0.8屏有时候滚1.3屏。等待时间随机化time.sleep在1到3秒之间随机取值而不是固定1.5秒。偶尔做一次回滚往下滚了几十屏后随机往上滚小半屏再往下滚。这种行为在真实用户里非常常见却是大多数自动化脚本不会做的。import random def human_like_scroll(driver): distance random.randint(500, 900) driver.execute_script(fwindow.scrollBy(0, {distance})) time.sleep(random.uniform(0.8, 2.5)) # 约10%的概率执行一次回滚 if random.random() 0.1: driver.execute_script(window.scrollBy(0, -300)) time.sleep(random.uniform(0.3, 0.8))不要小看这些随机化。很多站点的风控模型核心指标就是操作节奏的熵值。节奏太规律的操作流到达一定阈值就会被标记为脚本。5.2 浏览器指纹与特征规避除了滚动节奏浏览器指纹是更底层的检测维度。Selenium打开的浏览器在指纹上和普通Chrome有差异最明显的是navigator.webdriver属性为true。前面代码里已经通过CDP命令把这个属性改掉了那是第一步。在此基础上还可以做的设置真实User-Agent并且要和操作系统版本匹配。固定窗口尺寸避免默认的800x600被识别。去掉自动化扩展Chrome一定会加载的default_extensions可以被禁用。使用真实浏览器配置目录加载你自己平时用的--user-data-dir这样浏览器带上真实的cookie和本地存储。options.add_argument(--user-data-dir/path/to/your/chrome/profile)使用自己的配置目录有个风险浏览器可能处于登录态cookie带过去的同时也带着身份信息数据采集过程中如果触发了风控风险更高。是否用它取决于采集目标合规性自己评估。5.3 验证码与风控的应对思路滚动一段时间后弹出验证码或滑块验证是采集老手都见过的场景。我的原则是不硬刚设置检测机制遇到就暂停人工介入。在滚动循环里加入验证码检测CAPTCHA_SELECTORS [#captcha, .g-recaptcha, iframe[src*captcha]] def detect_captcha(driver): for selector in CAPTCHA_SELECTORS: try: if driver.find_element(By.CSS_SELECTOR, selector): return True except Exception: pass return False # 在滚动循环内 if detect_captcha(driver): print(检测到验证码暂停等待人工处理) input(处理完成后按回车继续...) # 手动处理完后刷新一下页面状态再继续人工介入处理验证码之后脚本能不能继续跑取决于页面状态是否被重置。常见的做法是人工验证完成后让脚本检测页面是否恢复到正常状态比如目标元素可以正常定位恢复不了就重启浏览器从断点继续。另外记住一点**风控是分级的。**第一次被验证明说明IP或浏览器指纹已经被标记接下来就算这次跑通了下次同样的方案可能直接就触发验证。采集量大的任务考虑轮换IP和浏览器指纹是躲不开的功课。6. 性能优化与稳定性保障6.1 边滚边采 vs 滚动完再采很多人纠结一个问题先全部滚动到底最后一并抓取还是边滚边抓我的答案是绝大多数情况下边滚边抓。原因有三。第一前面提到过虚拟列表会销毁DOM节点滚动完再采会导致大部分数据已经不在页面上了。第二滚动过程中页面元素数量膨胀每次find_elements全量查找的耗时是递增的滚动到后期单次查找可能要好几秒拖慢整个流程。第三边滚边抓可以在脚本崩溃时保留大部分已采集数据如果滚动完再采中途异常重来一次代价太大。边滚边抓的代价是采集代码和滚动代码耦合逻辑稍微复杂一点。但用去重键的方式管理代码量也就是多了十几行的事收益远大于成本。6.2 内存控制与日志记录长任务最怕跑了一半内存爆掉。Selenium跑的浏览器本身就吃内存加上滚动了成百上千屏每个屏幕的DOM都积累在内存里。如果页面框架没有做虚拟列表优化DOM节点数量会是几万甚至几十万的级别浏览器的内存占用会非常难看。内存控制不是脚本层面能完全解决的但可以在工程层面缓解每滚50屏左右执行一次driver.execute_script(window.scrollTo(0, 0))回到顶部或者强行触发一次垃圾回收window.gc()需要启动时加--js-flags--expose-gc参数。把采集结果及时写入文件而不是全部攒在内存里。每采集一部分就增量落盘即使中途崩溃也不至于全部丢失。记录滚动次数、采集量、时间戳到日志文件。排查问题时日志是最可靠的线索。增量落盘其实很简单import json def append_to_file(data, pathcollected.jsonl): with open(path, a, encodingutf-8) as f: f.write(json.dumps(data, ensure_asciiFalse) \n)每采集到一批新元素就调用一次不占用多少IO却能给任务加一层安全网。6.3 失败恢复与断点续跑长任务采集设计断点续跑机制是专业做法和业余做法的分水岭。断点续跑的第一个层次是数据层面的因为采集结果已经增量落盘重启后从文件里加载已采集的ID继续往下滚就不会重复采。第二个层次是位置层面的。有些页面支持通过URL参数跳到指定位置比如评论列表可以用?page50跳到第50页。这种页面完全可以直接按页数断点续跑不需要从头滚。不支持URL定位的页面可以记录上次采集到的最后一条数据的ID重启后先滚动查找这条数据找到它就从这里继续。def resume_from_last(driver, last_id): 滚动到包含last_id的位置返回是否成功找到 for _ in range(500): driver.execute_script(window.scrollBy(0, window.innerHeight)) time.sleep(0.5) try: driver.find_element(By.CSS_SELECTOR, f[data-id{last_id}]) return True except Exception: continue return False这个方法不完美但比从头跑一遍省时间。能用ID定位的页面有限实在不支持也不强求数据落盘机制已经保证了重启后的安全性最多是多滚一些重复页面而已。7. 踩坑实录与经验沉淀7.1 常见报错与解决方案把这些年在无限滚动采集里踩过的典型报错整理一下基本都是排查过很多次的问题。ElementNotInteractableException目标元素存在但不可交互。多半发生在元素还在渲染动画中或者被其他浮层盖住了。点击前用ActionChains移动鼠标到元素上方并等待一下能解决大部分情况。StaleElementReferenceException元素引用失效。滚动导致DOM重建后旧的元素引用指向了被销毁的节点。解决方式是尽量不用长期保存的元素引用每次滚动后重新find_elements。TimeoutException反复出现WebDriverWait等待超时。先确认等待的条件是否正确比如是否应该是等待元素消失而不是出现再确认目标元素是不是躲在iframe里。driver.quit()卡住不退出滚动采集跑久了浏览器进程可能处于半死状态。kill进程时要彻底driver.quit() # 如果卡住用os.kill强制结束 import os, signal os.kill(driver.service.process.pid, signal.SIGTERM)7.2 实战小技巧最后分享几个从项目里沉淀下来的小技巧都是文档里不写的东西。**第一滚动之前先记录初始数据量。**很多时候页面根本没有无限滚动只是看起来像。打开页面立即采集一次如果总量已经达到预期就不用滚了。这个检查看似多余实际上每天都在帮我省时间。**第二把常用代码模块化。**我会把滚动循环、元素采集、去重、落盘这些逻辑抽成函数放在一个工具包里。换一个新站点采集时只需要改setup_driver里的参数和extract_items里的选择器滚动核心逻辑几乎不用动。**第三注意页面的robots协议和服务条款。**这不是套话是真实的工程约束。有的站点明确禁止自动化采集硬碰硬不但拿不到数据还可能带来法律和账号风险。正确的姿态是优先看有没有官方API没有API再看是否通过合规渠道获取授权授权都没有就谨慎评估采集的必要性和频率。无限滚动页面的自动化采集说到底是模拟用户行为和理解页面状态两件事的结合。把滚动动作、加载等待、到底判定这三个环节想清楚了再难搞的页面也能慢慢啃下来。踩过的坑越多后面的路越顺希望这篇文章能帮你少走几段我走过的弯路。