资讯动态

Selenium与Python爬虫实战:动态网页数据采集从入门到落地

发布时间:2026/10/22 11:02:41 来源:尧图企业网站定制
做爬虫时间久了你会对一件事越来越有感触现在真正难爬的页面早就不在“能不能请求到”上卡脖子了而是卡在“数据藏在一堆加载动画后面”。尤其是那些数据看板、资讯流、商品列表你用requests把网页源码拉下来翻半天找不到一点有用的数据标签里全是空的。问题的根源就是动态渲染——数据压根不在初始 HTML 里而是浏览器执行 JavaScript 之后才从接口取回来填充进去的。这时候Selenium Python就成了很实际的一套方案让真实浏览器去加载、执行、渲染我们再从渲染完的 DOM 里抽取数据。这篇文章就把这套玩法从头到尾拆开讲包括为什么用它、环境怎么搭、等待和定位怎么不出岔子、完整流程怎么落地以及我踩过的几个坑。适合刚开始接触动态页面爬取、或者用 requests 已经碰了一鼻子灰的朋友看完能直接上手照着写。1. 动态加载网页爬取的前置认知与环境准备1.1 为什么有些网页用 requests 抓不到数据先不急着写代码想清楚原理能少走很多弯路。你用requests拿到的是一个服务器返回的 HTML 文档这串 HTML 里有正文、有样式链接、有脚本引用但具体数据不一定在里面。比如一个商品列表页它的首屏静态结构是给定好的骨架而每个商品项的价格、销量、库存是页面启动后才通过异步请求去后台取然后由 JavaScript 把数据渲染进 DOM 的。整个过程发生在你打开页面之后的几毫秒到几秒钟里requests并不会去执行这些 JS所以你捞回去的源码自然就是一堆空壳。生活里打个比方你去餐厅点菜菜是后厨现炒的你拿着菜单HTML不能当饭吃。requests干的事就是“把菜单拍个照”而浏览器才是那个“等菜端上桌的人”。Selenium 扮演的就是真实浏览器它能把 JS 跑完、把菜端上来然后你再从盘子里夹菜。动态加载页面常见的技术有 Ajax 局部刷新、滚动懒加载、前端路由渲染、图表库动态绘制等等。如果后端接口找得到用requests模拟接口请求确实更轻量但难点在于接口参数加密、签名校验、反爬验证。很多初学者在接口逆向这条路上卡到怀疑人生这时候直接用 Selenium 控制真实浏览器绕开这些 JS 层面的弯弯绕绕是一种降维打击式的操作。1.2 开发环境搭建与浏览器驱动配置Selenium 本身不是浏览器它是一套自动化协议和工具库需要搭配一个真实的浏览器来干活最常见的组合是 Chrome ChromeDriver。就我自己的习惯我一般这么搭环境# 建议用 Python 3.9 以上版本Selenium 4.x pip install selenium然后需要下载对应版本的 ChromeDriver或者使用webdriver-manager来自动管理pip install webdriver-managerwebdriver-manager会让日子好过很多因为它会自动检测你本机 Chrome 的版本号再自动下载匹配的驱动文件省得每次浏览器升级之后驱动不匹配到处找包。你可以这样初始化from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver webdriver.Chrome(serviceService(ChromeDriverManager().install()))如果你是第一次跑我更推荐先不要开无头模式让窗体弹出来亲眼看看自动化浏览器到底把你带到了哪里。等代码稳定了再考虑无头。这里有个必须要留意的点驱动版本和浏览器主版本号必须一致比如 Chrome 126 就要配 ChromeDriver 126.x否则会报session not created错误。用webdriver-manager的话这个坑基本被自动抹平了但你要是手动下载得先确认chrome://version里的版本号再去对应路径下文件。2. 核心细节解析与关键参数剖析2.1 浏览器无头模式与性能取舍无头模式headless指的是浏览器不弹出窗口在后台默默运行。它带来的好处很明显不干扰你做别的事情在服务器上也能跑消耗的资源相对少一点。但坏处也很直接——你无法实时看到页面发生了什么出了问题更难排查。我的经验是先有头调试再无头上线。代码调通之后把 headless 打开加一行就行options webdriver.ChromeOptions() options.add_argument(--headlessnew) options.add_argument(--disable-gpu) options.add_argument(--window-size1920,1080) driver webdriver.Chrome(optionsoptions)注意--window-size一定要设置很多人因为忽略了这步导致无头模式下页面视口宽度默认只有 800响应式布局一变化某些元素就找不到了。这个坑特别隐蔽我当初排查了大半天最后发现不过是窗口尺寸问题。除了无头还有一些常用启动参数比如--disable-blink-featuresAutomationControlled可以减轻自动化特征被网站检测到的概率--no-sandbox是 Linux 环境下跑经常会用到的。但我不建议一股脑全堆上启动参数也不是越多越好每个参数背后都有副作用。举例来说关了 JavaScript 虽然能降低被检测风险但动态页面直接废了加了代理代理配置反而可能让请求变慢。因地制宜别照抄。2.2 选择页面元素定位策略的选择用 Selenium 抓数据最频繁的操作就是找元素。Selenium 4 提供了常见定位方式比如ID、NAME、CLASS_NAME、CSS_SELECTOR、XPATH。实际项目中我强烈建议把CSS_SELECTOR和XPATH作为默认武器因为很多页面没给咱干净的 idclass 还动不动带空格动态生成用单一属性经常翻车。以 CSS 选择器为例items driver.find_elements(By.CSS_SELECTOR, div.product-item[data-index])XPath 则更灵活items driver.find_elements(By.XPATH, //div[contains(class,product)]//span[classprice])这里要注意层级关系。用 XPath 时//开头表示从任意位置查找容易误伤。最好带上具体的父级范围比如先定位一个父容器再在里面find_elementscontainer driver.find_element(By.CSS_SELECTOR, div#box) prices container.find_elements(By.XPATH, .//span[classprice])注意 XPath 子查询前要加.//如果直接写//span会从整个文档根开始重新搜结果不一定是你想要的。这是新手最容易犯的错之一。2.3 动态内容加载的等待策略详解动态页面最核心的一个字等。但怎么等这里面门道深了去了。最常见也最笨的办法是time.sleep(3)让人先睡个三秒再说。你不能说它没用但问题在于网快的时候三秒浪费网慢的时候三秒不够而且等的时间一旦拉到十秒整个爬取效率会变得惨不忍睹。更好的方式是用显式等待让程序主动去轮询某个条件等到了继续、等不到就超时from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10) item wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, div.item)))expected_conditions里最常用的几个条件分别是presence_of_element_located元素出现在 DOM 里但未必可见、visibility_of_element_located元素可见、element_to_be_clickable元素可点击。用哪个取决于你的下一步操作。如果只是为了读取文本presence就够了如果要点击按钮或者输入内容得用visibility或clickable否则元素刚挂载到 DOM 里还没渲染完成就操作会抛ElementNotInteractableException。还有种情况是元素本身一直在但是内容是被异步刷新出来的。比如翻页之后列表数据变了你如果光等待列表存在可能拿到的是旧数据。这时候需要等待一个跟数据变化强相关的条件比如某个页码高亮、某个空状态消失、某个特定文本出现。抓数据时的等待条件要尽量和数据绑定而不是和页面壳子绑定。2.4 滚动加载机制及模拟方案电商网站、资讯流、社交平台特别喜欢用无限滚动。页面底部一旦接近就自动加载下一批数据。这种情况你光定位元素是不行的因为前几批数据确实渲染了但后面的还在接口里躺着。模拟滚动最直接的方式是让浏览器执行一段 JSdriver.execute_script(window.scrollTo(0, document.body.scrollHeight))调用一次只触发一次加载。想要把所有数据榨干就得循环多次“滚动到底部 → 等待新数据 → 再滚动到底部 → 再等待”。循环终止条件很关键不能无脑滚。你可以记录当前总条目数在下一轮之后比较条目数是否还在增长如果连续两三轮没有新增就认为见底了。也可以设置一个最大滚动次数防止接口抽风导致死循环。还有一种情况是页面里有“加载更多”按钮这就要先滚动到按钮可见再点击button wait.until(EC.element_to_be_clickable((By.XPATH, //button[contains(text(),加载更多)]))) driver.execute_script(arguments[0].click();, button)用execute_script点击的好处是能绕过元素被遮挡、被移动导致的click intercepted问题。我个人在实战里几乎不直接.click()都是顺手用 JS 点省掉很多定位飘移的麻烦。3. 实测过程以某模拟数据页为案例的完整爬取流程3.1 场景设定与需求描述理论说完了直接上一个压箱底的案例。假设我们要爬取一个“某资讯平台的热文推荐列表”页面它的加载方式是典型的无限滚动最初只有 20 条数据往下滚动会分批追加每次追加 20 条直到没有更多数据为止。每条数据包含标题、发布时间、热度值、摘要。同时页面里有一层弹窗广告偶尔会遮挡页面底部导致滚动不触发。目标是把所有数据保存在 CSV 和 JSON 里方便后续分析。这里特意模拟了几个真实场景里会遇到的干扰项动态加载、滚动触发、弹窗遮挡、字段缺失。你可以把这个案例当成模板后续换成任何类似结构的页面都适用。3.2 完整代码实现与步骤拆解先给出可以跑的基本框架我再逐步解释每一步在干什么import json import time import csv from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.chrome.service import Service from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from webdriver_manager.chrome import ChromeDriverManager options webdriver.ChromeOptions() options.add_argument(--headlessnew) options.add_argument(--disable-gpu) options.add_argument(--window-size1920,1080) options.add_argument(--disable-blink-featuresAutomationControlled) driver webdriver.Chrome(serviceService(ChromeDriverManager().install()), optionsoptions) wait WebDriverWait(driver, 10) url https://example.com/hot-news driver.get(url) # 1. 先关闭可能出现的弹窗 try: close_btn wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, .popup-close))) close_btn.click() except Exception: pass # 2. 收集所有数据条目 all_data [] last_count 0 no_growth_rounds 0 for i in range(30): # 最多滚动 30 次防止极端情况 # 滚动到底部触发新数据加载 driver.execute_script(window.scrollTo(0, document.body.scrollHeight)) time.sleep(1.5) # 给网络请求和渲染留时间 # 等待页面上卡片数量有变化 cards driver.find_elements(By.CSS_SELECTOR, div.news-card) current_count len(cards) if current_count last_count: no_growth_rounds 1 if no_growth_rounds 2: break # 连续两轮没新增数据认为已到底 else: no_growth_rounds 0 last_count current_count # 本轮抓取卡片数据 for card in cards: try: title card.find_element(By.CSS_SELECTOR, h2.title).text.strip() time_text card.find_element(By.CSS_SELECTOR, span.date).text.strip() hot card.find_element(By.CSS_SELECTOR, span.hot).text.strip() summary card.find_element(By.CSS_SELECTOR, p.summary).text.strip() all_data.append({ title: title, time: time_text, hot: hot, summary: summary }) except Exception: # 部分卡片字段缺失属于正常情况跳过 continue # 去重用 title 作为唯一键 seen set() unique_data [] for item in all_data: if item[title] not in seen: seen.add(item[title]) unique_data.append(item) all_data unique_data driver.quit() # 3. 保存数据 with open(news_data.json, w, encodingutf-8) as f: json.dump(all_data, f, ensure_asciiFalse, indent2) with open(news_data.csv, w, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnames[title, time, hot, summary]) writer.writeheader() writer.writerows(all_data) print(f共抓取 {len(all_data)} 条数据)几个细节逐个说明第一弹窗处理。很多动态页会延迟弹广告直接等元素点击可能浪费时间所以我这里用try wait.clickable等待十秒内如果弹窗没出现异常捕获就跳过了。不会影响主流程。有些弹窗不是一个按钮而是两三个广告 banner更稳妥的方式是定位可见的弹窗容器再在里面找关闭按钮。如果时间紧直接execute_script(window.scrollTo)时弹窗遮挡高度也会干扰加载。第二滚动的终止条件。我用no_growth_rounds 2作为停手标志。为什么不只判断一次因为页面在完成前一轮请求后可能数据渲染有一点延迟连续两轮没有增长基本可以确认没有更多内容了。如果你抓的是本来就有限的列表这个策略非常省时间。第三去重时机。无限滚动偶尔会把之前的数据重新渲染一遍或者接口返回重复内容。如果不做去重数据量会虚标。上面的代码在每轮滚动之后都会去重一次虽然稍微多耗一点内存但能保证准确。你也可以在全部抓完之后统一去重效果一样但前期带着重复数据继续查“是否还有新增”时容易误判——因为数量可能是虚增的。3.3 数据清洗与落地保存上面代码已经很完整了但现实中的数据往往乱得离谱。这里再讲几个清洗经验.text拿到的文本经常带换行、多余空格、甚至 “\xa0” 这种不可见字符建议统一做\s正则清理。时间字段可能是相对时间比如“3 小时前”也有可能是标准时间。保存前最好解析成统一格式或者额外保留一个原始字段避免后期想溯源时丢掉信息。数字字段如果带单位比如“1.2k 热度”建议把单位拆掉转成纯数字后续排序就方便了。有的卡片摘要字段可能为空不要直接丢弃整条数据可以留空字符串保持记录完整。保存格式上JSON 适合结构复杂的数据CSV 适合喂给 Excel 和数据分析工具。我一般两个都会导出CSV 用utf-8-sig编码免得 Excel 打开中文乱码这个坑我自己踩过好多次推荐直接照抄。3.4 让程序更健壮异常捕获与日志上面的基础代码里try包住了卡片字段提取但这还远远不够。实际运行中你遇到的异常远远比想象中多比如页面突然超时、元素瞬间消失、浏览器被系统 kill。我建议在关键节点加上日志输出把出错信息打出来方便排查import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(crawler)然后在每一轮滚动后打一条日志已抓取第 {i} 轮当前记录数{len(all_data)}。在最后也打一条汇总日志。有了日志你才知道程序是哪一步挂掉的而不是蒙头 debug。特别是滚动 20 多次以后如果某个元素反复定位失败有日志能立刻定位到是哪一轮、哪个条件没满足。4. 常见问题与排查技巧实录4.1 元素找不到两个最常见的翻车原因使用 Selenium 的初期NoSuchElementException是所有人绕不开的坎。据我观察八成情况是这两种。第一元素在 iframe 里。很多页面把广告、对话框、地图嵌在 iframe 中直接按 layer 去定位会扑空。解决方法是先切换到对应 framedriver.switch_to.frame(frame_name_or_id) # 操作完之后再切回主文档 driver.switch_to.default_content()可气的是iframe 的 id 可能是动态生成的那就要用frame_index或者 XPath 去匹配。如果不确定当前页面有几个 frame可以用driver.find_elements(By.TAG_NAME, iframe)打印出全部框架的 id 和索引。第二元素在 Shadow DOM 里。现代前端框架越来越爱用 Shadow DOM 封装组件常规定位根本摸不到里面的元素。这种情况需要先获取 shadow host再进入 shadow roothost driver.find_element(By.CSS_SELECTOR, my-component) shadow_root driver.execute_script(return arguments[0].shadowRoot, host) inner shadow_root.find_element(By.CSS_SELECTOR, .inner-element)Shadow DOM 的定位逻辑和普通页面完全不同初次遇到会懵但处理过一次就通了。简单说shadow root 相当于一个小型文档树你必须通过宿主元素进到里面去操作。4.2 浏览器更新后驱动失效的应对手段这可能是最破坏心情的问题。明明上周写好的脚本跑得好好的这周莫名其妙报session not created: This version of ChromeDriver only supports Chrome version xxx原因基本只有一个你的浏览器自动更新了而 ChromeDriver 还停在旧版本。应对就两条路。一关掉 Chrome 的自动更新锁定开发版本二使用webdriver-manager自动匹配。我强烈推荐第二种因为省心。但自动下载也有个前提你运行脚本的机器能访问外网否则驱动下载不下来就白搭。离线环境下你得手动下载驱动然后通过Service指定路径这里注意路径里别带空格免得个别系统解析出错。还有一种比较偏门的招装一个不带自动更新的 Chrome for Testing版本号固定配对应版本的 ChromeDriver。适合那些需要长期稳定跑任务的服务器环境。我的团队就维护了一台专门的采集机器用的就是 Chrome for Testing再配合 Docker 封装环境完全隔离再也不怕半夜被版本更新坑到。4.3 反爬拦截与屏蔽策略的合理应对动态页面爬多了你会遇到各种检测无头浏览器特征检测、WebDriver 特征检测、行为数据分析、请求频率限制。坦白说Selenium 本质上是浏览器自动化再怎么伪装也会有痕迹如果你的目标是高仿真模拟不如去研究请求加密或者纯浏览器协议。但对付中低强度的反爬下面这些够用。首先避免固定频率请求。不管你是滚动页面还是点击下一页每两次操作之间随机间隔 1.5 到 4 秒模拟人类阅读节奏。随机间隔不只是为了反爬也是给你的脚本留出页面渲染时间一举两得。其次记得传真实的 User-Agent 和 Headers。用options.add_argument(--user-agent...)替换默认 UA。注意别用太老的 UA现在很多网站对头部信息版本有校验。然后考虑使用指纹指纹伪装库或者stealth.js来抹掉自动化特征。Selenium 4 里可以注入脚本掩盖navigator.webdriver属性等漏洞。但这类方案要小心使用只应针对自己拥有或授权抓取的数据别拿去碰别人的核心防线那属于对抗性操作专业上不支持也不该去教。最后也是最重要的合规提醒抓取前一定看目标站点的robots.txt和用户协议尊重站点的公开数据声明不要对登录后可见的私有数据、受版权保护内容做批量采集。爬虫是工具用得好是数据助手用歪了就是侵权利器分寸全在你自己手上。4.4 页面加载超时与崩溃的处理跑久了你会发现浏览器偶尔会僵死或者PageLoadTimeout异常。我建议启动时设置页面加载超时driver.set_page_load_timeout(30)如果某个页面长时间卡住用driver.refresh()有时候能解决但更彻底的是增加一个“看门狗”机制每隔一定时间检查一下当前页面 title 和 url如果超过阈值没变化就driver.quit()重启浏览器再重新进入目标页面。在长任务里这种自愈机制比一切等待策略都管用。毕竟浏览器这种东西跑个几百次就是容易抽风不是代码写得不够好是 Chromium 本身复杂得吓人。4.5 使用框架实现断点续跑如果你要攒几万条数据单次脚本跑完不现实。我现在的习惯是每跑完一批数据就写入磁盘程序启动时先检查已有数据跳过已经抓取的标题。这样即使中途崩了重启脚本就能接着跑不会从头再来。具体实现很简单读取指定 JSON 文件把已有 title 放到一个set()里抓取时遇到重复的标题就跳过处理。这样既兼顾去重又实现断点续跑。我亲眼见过同事的采集脚本跑了一晚上第二天发现跑了三个小时就崩了因为没做断点前半夜的数据全白费。从那以后我的所有采集脚本都会同步落盘。5. 扩展玩法与性能优化建议5.1 提升抓取速度并发窗口的取舍Selenium 本身是同步操作单窗口抓取多页面的确慢。想提速你可以同时开多个浏览器窗口或者多开几个 WebDriver 实例各自跑不同的页面片段。但并发数不是越大越好——每开一个浏览器实例内存开销能有几百 MB8 个窗口同时开着8GB 内存的机器直接报警。我实测下来家用机器跑 3 到 4 个实例最舒服内存 16GB 以上可以跑到 8 个。另外并发抓取的频率控制也要注意同一时间多个窗口对同一站点发大量请求很容易触发站点限流反而适得其反。这里有个折中方案单窗口配合driver.switch_to.new_window()或者标签页管理也可以实现轻量级并行。但个人觉得如果不是千万级数据量的刚性需求老老实实单窗口顺序跑更省心。抓数据这件事稳定性永远比瞬时速度重要。5.2 从 Selenium 到半自动化请求的进阶路线等你把 Selenium 玩熟了会发现很多页面的数据其实来自一个相对固定的 JSON 接口。这时候有一个“中间派”策略用 Selenium 只做两件事——登录获取 Cookie、分析页面发起的 XHR 请求 URL然后退出浏览器再用requests直接带 Cookie 请求那些接口速度直接起飞。拿无限滚动来说你甚至不用真的滚动只需要照着浏览器发出的分页参数去翻页请求。我当初就是这么过渡的。先用开发者工具的 Network 面板找接口再用 Selenium 模拟一次滚动拿到带数据的响应体你看一眼 URL 和 payload基本就能写下纯接口脚本。这一招用来抓数据列表、图表配置、搜索建议这类“半动态”页面特别高效。Selenium 变成调试工具和 Cookie 提供器而不是保姆式地等在旁边慢慢渲染。5.3 把抓取流程封装成通用函数写多了你会上瘾的。我后来把所有常见的动作都封装了一遍打开页面、关闭弹窗、滚动到底、等待出现、滚动加载直到结束、页面 Down 掉后重启 driver。这些函数完全跟业务无关却能在所有动态页面爬取项目里复用。哪天你要写新爬虫直接把函数库拖进来改成对应的定位器就行开发效率翻倍。建议你一开始写代码就注意“分层”底层是浏览器操作封装中间层是页面元素抽象顶层才是业务数据抽取。别把定位器写死在内层循环里不然后面改动选器要改几十处能把你改到怀疑人生。6. 最后的几句真心话做动态网页抓取这一块我踩过的坑大概能装满一箩筐。本想只讲个入门用法结果越写越长。如果你只记住三件事那就是一是别信time.sleep万能论显式等待才是稳定性的基石二是元素定位失败时先考虑 iframe 和 Shadow DOM 这两个拦路虎三是收集数据一定要落盘和断点续跑否则你会在凌晨三点因为崩溃而对着屏幕叹息。另外自动化浏览器的使用边界问题我再次啰嗦一遍谨记目标网站的条款和 robots 协议只抓公开数据、不做恶意刷量、不绕过付费墙和账号体系这是这一行最基本的职业素养。技术本身没有对错使用它的人心里要有根弦。从我个人的实际操作体会来讲动态页面爬取的核心其实不是代码量而是你对浏览器渲染流程的理解深度。当你真的弄懂了页面从 HTML 到 DOM 再到交互渲染之间发生了什么写 Selenium 脚本就从“碰运气般的调试”变成了“有依据的工程实现”。希望这篇指南能让你少走几段弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑