恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Selenium实战:破解JavaScript动态渲染页面的爬虫采集全攻略
首页
资讯中心
/
Selenium实战:破解JavaScript动态渲染页面的爬虫采集全攻略
Selenium实战:破解JavaScript动态渲染页面的爬虫采集全攻略
发布时间:2026/10/9 19:49:20
1. 从Requests到Selenium为什么静态采集会在这类页面上失灵先讲一个几乎每个爬虫初学者都撞过的场景你用Requests把目标页面源代码抓下来了打开一看HTML里干干净净——该有的数据一条都没有取而代之的是一堆script标签、空的div容器或者打包好的JSON配置。这不是目标网站坏了也不是你代码有问题而是页面本身压根就不打算通过HTML把数据交给你。这类页面的数据是靠JavaScript动态渲染出来的。浏览器加载页面后脚本会向接口发起请求拿到数据后以DOM操作或框架Vue、React这一卦的VDOM更新机制把内容画到页面上。你用Requests拿到的只是种子HTML真正有价值的数据发生在你的请求之后、由浏览器在本地执行的逻辑链路上。从技术原理上说这里区分了两种核心链路服务端渲染SSR数据在服务端拼进HTML再返回Requests直接能拿到。客户端渲染CSR服务端只返回空壳数据由浏览器里的JS异步获取和填充。Selenium这类自动化工具登上舞台正是为了处理CSR页面。这个场景在如今的企业级应用里非常普遍。换了框架的前端页面、带用户交互的数据面板、滚动加载的瀑布流列表、基于Canvas或WebGL图表生成的报表——都会让静态请求直接失效。你写了一堆XPath从text()函数提取数据结果节点都不存在XPath写得再漂亮也无从下手。于是问题变成怎么让爬虫拥有一个真正的浏览器的能力答案有几种Selenium、Playwright、Puppeteer甚至Pyppeteer。从市场存量、生态完整性和师承关系来看Selenium依然是最典型的切入点。它驱动的不是模拟请求而是完整的Chromium浏览器能执行完整JS、加载完整渲染管线、模拟真实用户的操作事件这本质上和你在电脑前手动开网页没有区别。我见过很多新手问能不能用Requests加上某个接口直接代替Selenium——如果页面数据来自明确的XHR接口那直接用接口当然更快更稳这一点Selenium反而显得笨重。但问题在于很多页面会把数据混淆在JS逻辑中加密、拼接、动态生成接口地址逆向成本无限膨胀。这时候用Selenium直接搞定渲染结果用时间换稳定往往是性价比最高的方案。什么时候该选Selenium而不是别的方案我自己的判断标准是这样的页面特征推荐方案数据来自直接可见的XHR接口Requests 接口直调数据隐藏在JS逻辑里接口经过加密或动态拼接Selenium或Playwright需要模拟登录、验证码等完整用户链路Selenium配合手动辅助高频、大数据量采集Requests方案为主Selenium只处理关键环节Selenium不是万金油但碰到JS渲染的硬骨头它是你工具箱里最不该缺席的一把锤子。2. 环境搭建与核心API用最小成本跑通第一个动态页面很多人在Selenium的第一步就卡住了——不是不会写代码而是环境始终跑不起来。Driver没配上、版本不匹配、浏览器自动更新后脚本全部报废这些问题几乎磨掉了所有人的耐心。这里我直接给出一套实测稳定的搭建路径。2.1 用Selenium Manager解决Driver匹配这个最大的坑Selenium从4.6版本开始内置了Selenium Manager它最大的价值是你不再需要手动下载ChromeDriver也不需要在代码里指定executable_path。Selenium Manager会在首次调用时自动检测本机Chrome版本下载匹配的Driver并缓存到本地。安装Selenium本身也简单到一句话pip install selenium然后这是第一个能跑通的完整脚本目标就选一个存在JS渲染的普通页面from selenium import webdriver from selenium.webdriver.common.by import By import time driver webdriver.Chrome() # Selenium Manager自动处理Driver driver.get(https://example.com) time.sleep(3) # 先粗暴等一下后面会换成更优雅的等待方式 title driver.find_element(By.TAG_NAME, h1).text print(title) driver.quit()如果你还在用webdriver.Chrome(executable_path/path/to/chromedriver)这种老式写法赶紧改了。4.x之后executable_path参数已被废弃Selenium Manager的自动处理才是正路。但我还需要提醒一件事自动下载Driver依赖网络环境。国内的网络环境偶尔会卡在这个环节如果你发现Selenium Manager下载失败或极慢备选方案是用淘宝或国内镜像源手动下载ChromeDriver然后通过service参数指定位置from selenium.webdriver.chrome.service import Service service Service(/your/path/to/chromedriver) driver webdriver.Chrome(serviceservice)这一手属于老鸟的常规操作了。2.2 看得见总比看不见好无头模式与调试模式的选择企业级采集通常跑在服务器上没有显示器所以无头模式几乎是必选项。但你上来就开无头模式写代码绝对是个反模式——页面长什么样、有没有弹窗、元素有没有加载出来看不到这些信息你只能靠猜。我的习惯是这样新页面开发阶段用有头模式调试调试稳定后再切无头模式部署。无头模式的写法很简单但请把参数写完整from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headlessnew) # Chrome 109用new模式 options.add_argument(--disable-gpu) options.add_argument(--window-size1920,1080) options.add_argument(--no-sandbox) # Linux服务器跑root用户时必加 options.add_argument(--disable-dev-shm-usage) # Docker容器内必加 driver webdriver.Chrome(optionsoptions)--disable-gpu在无头模式下是为了避开GPU相关崩溃--no-sandbox和--disable-dev-shm-usage是容器场景下的救星——不加这两个参数Docker里的Chrome经常秒崩报错信息还特别误导人让你以为是Driver问题。2.3 用JavaScriptExecutor补上逆天改命的能力Selenium最容易被低估的组件是execute_script()。它能在当前浏览器上下文里执行任意JavaScript代码这意味着你能做到很多常规API做不到的事情# 直接修改元素属性比如把只读input变成可编辑 driver.execute_script(arguments[0].removeAttribute(readonly), element) # 精确滚动到页面底部触发懒加载 driver.execute_script(window.scrollTo(0, document.body.scrollHeight)) # 获取渲染后整个页面的高度 height driver.execute_script(return document.body.scrollHeight) # 强制去除懒加载图片的src延迟 driver.execute_script(document.querySelectorAll(img[data-src]).forEach(img { img.src img.dataset.src; }))我看到很多教程把execute_script一笔带过但实际项目中它解决了我至少三成的问题。记住一点Selenium不是只能模拟点击和输入它控制的本来就是一个浏览器浏览器能执行的JS你都能执行。这个认知一旦打通很多看似无解的问题就有了突破口。还有判断页面数据类型的活也可以交给JS处理。热搜词里那类javascript判断数据类型的问题在爬虫场景下同样实用——比如你拿到的元素值到底是字符串、整型还是数组可以用typeof在浏览器里直接探明比在Python里猜半天要可靠得多。3. 等待与定位解决元素定位不到这个最大的拦路虎如果做一个Selenium使用痛点排行榜元素定位不到绝对断层第一。随便搜一下抱怨最多的都是NoSuchElementException。但根因往往不是XPath写错了而是你的代码跑得比浏览器渲染快。你在页面还在加载时就去找元素它自然不在——这个逻辑对新手来说明明是常识但真到写代码的时候十个人有八个会栽进去。3.1time.sleep是最差的等待方式没有之一初学者的第一反应就是time.sleep(5)固定等5秒。这个方案有两个致命缺陷一是不稳定页面快的时候浪费了时间页面慢的时候依然找不到二是难以维护网络状况变差时你只能把秒数越调越大直到脚本慢得没法用。正确的等待思路是轮询每0.5秒检查一次目标元素是否存在直到超时为止。这恰好就是显式等待的原理。Selenium官方的WebDriverWait封装了完整的重试逻辑和超时异常from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 15, poll_frequency0.5) element wait.until( EC.presence_of_element_located((By.XPATH, //div[classdata]/span)) )这段代码表达的语义是最多等15秒页面每隔0.5秒检查一次目标元素是否出现。这比time.sleep灵活一个量级——页面数据提前加载完了就能提前执行不用干等。我注意到很多教程只提到了presence_of_element_located但这个条件只判断元素出现在DOM里不保证它可见。JS渲染的页面经常出现这种情况元素在DOM里了但隐藏着没显示或者还在等某个异步数据填充。所以更严谨的写法是把条件换成visibility_of_element_locatedwait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, .table-row)))两者在实际语义上的差异往往是能定位但拿不到文本和能定位就能直接提取内容之间的差距。3.2 XPath与CSS Selector的选用智慧定位策略选得好代码能少写一半。Selenium里最常用的是XPath和CSS Selector。我的经验总结如下XPath适合基于文本内容定位、层级复杂、需要倒查父级兄弟节点的场景支持text()函数能直接按可见文本抓。CSS Selector语法更简洁执行速度略快适合ID、class、属性等结构化定位。热度词里有一条python xpath爬虫 text函数说明很多人都在为文本提取发愁。这里我掏一个实际写法差异给你看# XPath按文本精确定位适合动态列表 el driver.find_element(By.XPATH, //div[contains(text(), 目标关键词)]/../span) # CSS Selector按层级定位简洁直观 el driver.find_element(By.CSS_SELECTOR, div.item span.price)实际项目里我通常优先用XPath因为CSS Selector不支持按文本内容定位而JS渲染的表格中文本常常是唯一的可靠锚点。3.3 iframe与Shadow DOM查漏补缺的两个特殊场景页面结构再复杂一点你还会撞上iframe和Shadow DOM。iframe是一个独立文档嵌入到宿主文档里的结构。典型的坑是find_element默认只在主文档DOM里找iframe里的元素直接定位会报找不到。必须先切换上下文iframe driver.find_element(By.TAG_NAME, iframe) driver.switch_to.frame(iframe) # 现在可以定位iframe内部的元素了 inner_element driver.find_element(By.CSS_SELECTOR, .iframe-content) # 操作完成后记得切回主文档 driver.switch_to.default_content()Shadow DOM的情况更麻烦。它是Web Components的封装机制内部节点对普通CSS选择器不可见。Selenium 4支持通过shadowRoot属性访问host driver.find_element(By.CSS_SELECTOR, my-widget) shadow_root host.shadow_root inner_element shadow_root.find_element(By.CSS_SELECTOR, .inner-button)这两个场景都属于平时遇不到遇到卡半天的类型。每当你确信元素存在但就是定位不到先排查这两个方向往往能省下两小时的排查时间。4. 页面交互与数据抽取模拟点击、滚动加载和结构化输出等得稳、定位得准之后就能真正开始和页面互动了。JS渲染页面的数据往往藏在交互中——滚动加载、点击翻页、输入搜索、切换Tab。这样设计的原因一般是为了提升用户体验、分散请求压力但对爬虫就是多了一道工序。好消息是这些交互本质上都是标准事件Selenium能完整模拟。4.1 处理无限滚动滚动、判定、循环的完整闭环无限滚动Infinite Scroll是JS渲染页面的一个经典交互。瀑布流列表、信息流推荐、动态评论加载全是这种模式。核心逻辑并不复杂滚动到底部触发加载等新内容出现再滚动。但写成一个健壮的循环就有很多细节需要注意from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver.execute_script(window.scrollTo(0, document.body.scrollHeight)) try: WebDriverWait(driver, 10).until( lambda d: d.execute_script( return document.body.scrollHeight ) last_height ) except: break # 高度没再变化说明到底了 # 更新高度继续下一轮 new_height driver.execute_script(return document.body.scrollHeight)这个实现里最值钱的是通过页面高度是否增长来判断是否还有新数据这个思路。它比数列表项个数更通用——只要页面在加载新内容scrollHeight就一定会变化和列表项长什么样、多少个没有任何关系。也有一种情况是滚动加载不改变整个页面的高度而是弹出一个独立滚动区域如聊天窗口。这时要把滚动目标换成那个内部容器driver.execute_script( arguments[0].scrollTop arguments[0].scrollHeight, scroll_container )这类容器元素需要通过JS或定位拿到然后在execute_script里用arguments[0]传给页面。4.2 点击与翻页模拟用户操作的正确姿势分页点击是另一类高频场景。细节坑在于链接点击或者翻页按钮的触发条件有时候极为苛刻比如需要按钮可见、可点、未被遮挡。最稳的一招是这样的from selenium.webdriver.common.action_chains import ActionChains next_button wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, .pagination .next))) ActionChains(driver).move_to_element(next_button).click().perform()ActionChains的优势在于它会模拟真实鼠标移动和点击事件——对于绑定了mouseover、mousedown等复杂事件的按钮它比element.click()可靠得多。还有少数情况页面元素在点击时被透明遮罩层挡住了element.click()报ElementClickInterceptedError用execute_script直接触发点击反而更省事driver.execute_script(arguments[0].click();, next_button)JS渲染的页面通常有大量事件监听直接调用.click()方法在某些框架下可能不会触发事件监听器。这时候JS直击往往是最可靠的后手。4.3 数据落地的两种思路就地提取与整体快照拿到页面内容后怎么把数据抽出来这个选择直接决定后续代码的复杂程度。方案A就地提取。用element.text或get_attribute()把目标内容直接取成Python数据。优点是对目标数据结构高度可控缺点是选择器写得多页面结构变了就得跟着改。方案B整体快照。用driver.page_source拿到整个渲染后的HTML交给lxml或BeautifulSoup解析。优点是分离了取页面和解析页面两件事你可以把页面源码存盘、断点续跑、回头再解析。实际项目里我更喜欢方案B作为主链路、方案A作为补丁先整体拿到page_source用XPath批量解析如果发现个别字段在渲染后的HTML中还是拿不到比如Canvas图表里的数值再针对该类元素用Selenium精准提取。这里补充一个和python xpath爬虫 text函数直接相关的细节用lxml解析page_source时xpath(//span/text())只能取到直接子文本节点。如果目标节点的文本是嵌套在多个子元素里的//span//text()的效果会更好。这个函数在XPath里的写法差异对解析结果的完整性有决定性影响。from lxml import etree html driver.page_source tree etree.HTML(html) # 直接子文本 vs 所有后代文本差异很大 all_texts tree.xpath(//div[classinfo]//text()) desc_texts tree.xpath(//div[classinfo]/text()) # 可能只有空串至于获取更细的属性值get_attribute(href)、get_attribute(data-id)这类操作属于基本功。我的建议是所有能在渲染后的DOM里稳定拿到的值优先从page_source批量解析性能远高于一个个定位。5. 性能优化与反检测任务量从几十条到几万条的分水岭处理JS渲染的爬虫最大的痛点不是写不出来而是写出来后跑不快、跑不稳。这里涉及两个关键方向一类是效率问题——单机并发、任务分发另一类则是安全问题——如何不被目标站点识别为自动化脚本。这两个方向几乎是所有Selenium类爬虫开发者从入门到高级的分水岭。5.1 Selenium为何天生贪吃性能开销的几个真相Selenium最大的消耗来自启动浏览器实例。一个裸的Chrome实例大约吃掉200~400MB内存而实际业务往往还要同时开多个。笔者实测过在一个16GB内存的服务器上不控制并发的话五个实例就能让磁盘持续读写、内存吃紧。优化思路大致分为三档静态资源阻断图片、字体、CSS对数据采集毫无意义但会占用大量宽带和渲染时间。可以在启动参数中统一禁用图片加载或通过Preferences控制下载行为。按需加载页面先driver.get()打开页面再用driver.execute_script()动态控制加载内容比如对懒加载图片做scrollIntoView触发渲染。复用浏览器Session对于需要登录态的采集起一个driver完成登录将它的Cookie序列化保存后续任务全部复用该会话。这一招能把每条数据从几十秒降到几秒。实际上超过90%的场景用单实例串行配合好等待条件就够了。Selenium适合承担低频、高价值的数据渲染不适合去和纯Requests方案拼吞吐这是它的技术边界决定的。需要高频时正确思路是把Selenium当作技术栈中的一环而不是全程依赖它。5.2 分布式扩展从单机到Selenium Grid复杂度上升之后单实例撑不住时第一反应不该是写多进程或者加机器而是把任务转成分布式可调度的模式。Selenium Grid是官方提供的分布式组件思路很简单主节点Hub 多个工作节点Node各自跑着不同浏览器主节点负责任务分发工作节点作为中央调度的可伸缩资源池。用Docker部署Selenium Grid是目前最省心的方式。下面这套命令可以一键拉起标配集群docker network create selenium-grid docker run -d -p 4442-4444:4442-4444 --name selenium-hub --network selenium-grid selenium/hub:4.15.0 docker run -d --name chrome-node --network selenium-grid -e SE_EVENT_BUS_HOSTselenium-hub -e SE_EVENT_BUS_PUBLISH_PORT4442 -e SE_EVENT_BUS_SUBSCRIBE_PORT4443 selenium/node-chrome:4.15.0连接远程Hub时代码里的写法会略有不同from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() driver webdriver.Remote( command_executorhttp://localhost:4444/wd/hub, optionsoptions ) driver.get(https://example.com)Selenium Grid的真正价值在于你可以横向扩展Node数量用几台机器做的事情集中调度。配合Python的concurrent.futures做线程池单一节点内部再开3~5个会话并行任务吞吐量能轻松提升一个数量级。这才是高级爬虫技巧应有的进阶路线。5.3 反检测的核心逻辑让你的浏览器看起来像真人先声明一个基本原则反检测是一个不断攻防升级的过程不存在永久的方案但在常规场景下做足基础伪装已经能覆盖95%的目标站点。Selenium驱动的Chrome会有几个明显的WebDriver特征。最简单直接的暴露点是navigator.webdriver属性——正常浏览器里它是undefined或falseSelenium驱动下它会是true。很多站点的前端反爬就是靠这一行JS检测检测到就直接拒绝访问或返回假数据。推荐的处理方式是通过CDPChrome DevTools Protocol在页面加载前注入JS预先覆盖这些特征options Options() driver webdriver.Chrome(optionsoptions) driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, { get: () undefined }); window.chrome window.chrome || { runtime: {} }; }) driver.get(https://example.com)这段代码的含义是在每个新文档加载前先用JS把navigator.webdriver属性的返回值改成undefined并伪装一个window.chrome对象。对大多数做前端检测的页面来说这套首次注入就够用了。另一个比我看到多数教程更进阶的维度是行为层面真人访问页面会有鼠标轨迹、停留时间、视口变化。驱动Selenium时要刻意引入随机延迟、滚动停顿、有时点开链接看看再返回。这属于高成本反检测只在目标站点风控严格时使用。在明确写了用户协议禁止自动化访问的平台上我不建议使用本文介绍的技术去采集数据。技术要用于正经场景——比如自己网站的压力测试、公开数据的研究分析、内部系统的数据同步。越界使用反检测技术轻则账号被封重则面临法律风险这条红线各位务必守住。6. 疑难杂症排错指南JS渲染爬虫的常见坑与自查路径做了这么久的Selenium项目几乎所有疑难杂症都能归因到少数几个模块。遇到问题不要慌按下面的路径逐层排查比满网乱搜答案高效得多。6.1 元素定位不到先做这两件事再说这是出现频率最高的问题。处理路径如下确认元素是否在iframe或Shadow DOM里。先用driver.page_source搜关键字看它到底在不在页面源码中在的话用开发者工具查看它外层有没有iframe或自定义组件标签。确认是没加载还是不可见。用WebDriverWait配合visibility_of_element_located等待如果等到超时还是不行大概率是数据接口挂掉或页面改成异步加载结构了。用execute_script(return document.body.innerHTML)打印渲染后的完整HTML肉眼比对目标元素是否存在。有一类很隐蔽的情况报错信息里提示找不到元素但实际原因是页面崩溃了——Chrome在长时间运行后偶尔会白屏或JS报错此时所有等待都会超时。这种问题一般通过定期重启driver方式解决。热搜词里提到的javascript运行时报错就属于这类处理思路是分裂页面JS的错误日志收集到本地出现指定错误时主动重启。6.2 页面卡死与超时浏览器性能排查Selenium踩坑到后期最常见的不是找不到元素而是驱动实例本身变慢、变卡、甚至无响应。原因通常是长时间运行导致的内存泄漏或者页面复杂JS的持续运行耗光了CPU。推荐实用的自查流程排查项检查方式处理手段内存占用执行tasklistWindows/ps查看Chrome进程定时重启driver控制并发数JS主动阻塞页面内轮询document.readyState启动浏览器时禁用多余插件网络请求堆积使用driver.execute_script查看performance.getEntries()数量关闭自动下载图片、字体页面视觉闪烁无头模式下禁用GPU加速--disable-gpu参数前端动画优先级部分重渲染页图表闪烁在等待中增加固定停顿还有一点在热搜词echart 闪烁怎么解决前端里能对应上有些页面报表里的ECharts图表在自动刷新时会产生闪烁。在爬虫视角下这个不算问题但如果你的爬虫需要截图留证或OCR数据闪烁可能导致截到空白。解决方式是设置浏览器窗口的widthdevice-width并让wait条件包含图表canvas渲染完成标记。6.3 登录态维护和验证码绕不开的持久化话题需要登录的站点每次都走一遍账号密码流程不现实。Selenium项目里的常规做法是手动登录一次然后把登录态持久化保存下来import pickle from selenium import webdriver driver webdriver.Chrome() driver.get(https://example.com/login) # 手动输入账号密码登录或者用脚本自动填表完成后 # 保存Cookie pickle.dump(driver.get_cookies(), open(cookies.pkl, wb)) # 下次启动直接加载Cookie跳过登录 driver.get(https://example.com) for cookie in pickle.load(open(cookies.pkl, rb)): driver.add_cookie(cookie) driver.get(https://example.com/dashboard) # 直接进带登录态页面需要提醒几个细节Cookie有时效性长时间任务要定期检查登录态是否失效登录时使用的同域域名与后续脚本要完全一致否则Cookie无法正常注入。至于验证码——不要绕过它那是风控的核心环节想尽办法绕过验证码本身已经接近违规操作。正常做法是用打码平台或人工介入每次任务启动时由人工过一次验证码然后立即把Cookie保存好。只要任务间隔不是特别长一次人工验证就能维持很久的会话。7. 案例复盘一个JS渲染列表页的完整采集过程理论说了这么多最后用一个完整的实例把前面所有知识串起来。假设目标是一个动态排行榜页面数据通过AJAX加载列表无限滚动每条数据包含标题、链接和一个数字指标。目标采集全部记录输出为结构化数据。7.1 步骤一初始化Driver并配置反检测from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from lxml import etree import json, time options Options() options.add_argument(--headlessnew) options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) options.add_argument(--window-size1920,1080) driver webdriver.Chrome(optionsoptions) driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, {get: () undefined}) }) driver.get(https://example.com/ranking)7.2 步骤二等待首屏数据渲染完成wait WebDriverWait(driver, 20) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, div.ranking-row)))这一步是等待的实战验证如果首屏数据没渲染完找元素一定失败。用显式等待比任何time.sleep都稳妥。7.3 步骤三无限滚动循环results [] last_height driver.execute_script(return document.body.scrollHeight) for _ in range(30): # 最多滚动30轮防止永不停止 driver.execute_script(window.scrollTo(0, document.body.scrollHeight)) try: wait.until(lambda d: d.execute_script(return document.body.scrollHeight) last_height) except: break # 不再有新数据停止循环 last_height driver.execute_script(return document.body.scrollHeight) page_source driver.page_source tree etree.HTML(page_source) rows tree.xpath(//div[contains(class, ranking-row)]) # 每滚动一轮就提取一轮避免最后一次性处理时数据量过大 for row in rows: title row.xpath(.//div[classtitle]/text()) url row.xpath(.//a/href) score row.xpath(.//span[classscore]/text()) if title and url: results.append({ title: title[0].strip(), url: url[0].strip(), score: float(score[0].strip()) if score else None, })这里有一个我特别想强调的实践细节每滚动一轮就做一次增量提取而不是全部滚动完了再统一提取。好处是页面源码不用积累到超大体积才解析内存占用更稳定而且中途崩了也能保留已完成的数据。7.4 步骤四清理与持久化driver.quit() # 去重并写文件 seen set() clean [] for item in results: if item[url] not in seen: seen.add(item[url]) clean.append(item) with open(ranking.json, w, encodingutf-8) as f: json.dump(clean, f, ensure_asciiFalse, indent2) print(f采集完成共 {len(clean)} 条记录)7.5 这个案例里踩过的三个隐藏坑第一滚动速度不是越快越好。有的页面滚动触发接口有频率限制滚太快直接触发风控回调返回的数据里混着假数据。我在实际项目里会把每次滚动后加一个0.5到1.5秒的随机等待。第二xpath里的text()不一定只有一层。标题如果被包在多个标签里text()拿到的可能是空串用string(.)或者//text()更稳title row.xpath(string(.//div[classtitle])).strip()第三动态内容偶尔会和静态内容混杂。有的列表一次滚动加载的数据量很大但其中一部分条目是静态的另一部分是动态的——不要在提取时假设所有条目结构完全一致任何字段都可能缺失所以每一行提取都要做空值判断否则一个None让整个流程报错。这个案例完整跑下来大约两三百行代码但它覆盖了Driver初始化、反检测注入、显式等待、无限滚动、XPath解析、异常兜底、数据持久化恰好构成了一个Selenium处理JS渲染页面的标准流水线。8. 我个人的最后几点忠告回头看处理JavaScript渲染这件事停留在技术层面的讨论其实只占一半。真正决定项目成败的往往是另外几个不那么技术化的维度。第一先确认静态接口再上Selenium。不少JS渲染页面背后其实有一个返回JSON的接口只是前端代码里做了混淆。如果你在浏览器开发者工具的Network面板里能找到这个接口直接用Requests调接口比Selenium快十倍不止。Selenium是兜底方案不是首选方案。第二能复用的会话就别重新登录。参照上文Cookie持久化的做法把登录态和会话状态保持好这能帮你省下大量时间和验证码成本。每次任务都必须重新登录的Selenium项目基本都意味着没做Cookie持久化。第三流程里加上监控和告警。长跑的任务一定会出错这不取决于代码写得多好只是时间问题。给脚本加上任务结束是否产出数据的检查如果是零点产出就告警出来。这比盯着控制台高效得多。关于工具选型Selenium不会一直是你的最优答案。如果你的项目长期依赖浏览器自动化采集数据我会建议你也花时间看看Playwright和Puppeteer。Playwright在API设计、自动等待机制、多浏览器支持上确实比Selenium现代甚至在很多场景里更稳。但Selenium作为入门和理解浏览器自动化原理的路径依然没有过时——它生态成熟、资料多、坑基本都能搜到解法而且Selenium Manager的出现也让环境配置变得前所未有的顺畅。最后想跟所有刚开始接触这个领域的朋友说一句爬虫的本质是信息获取的技术实践请务必注意目标站点的服务条款和数据使用授权。把技术用在正经人、正经事上你的能力边界才会真正变成职业价值而不是麻烦本身。