news 2026/10/11 6:55:23

Selenium实战:破解JavaScript渲染难题,搞定动态页面爬取

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Selenium实战:破解JavaScript渲染难题,搞定动态页面爬取

做爬虫的朋友大概率都有过这种体验:目标页面的数据明明写在浏览器里,可你用requests把网页源码抓回来,翻遍整个 HTML 却只看到框架、脚本和空壳。问题就出在 JavaScript 渲染上——页面里的数据是浏览器执行完脚本之后才动态生成的,Python 的静态请求根本没这个机会。这种时候,Selenium 就成了最顺手的工具:让它启动一个真实浏览器,加载页面、执行 JS、模拟点击和滚动,最后再把渲染之后的 DOM 数据捞出来。

这篇文章不是基础教程,而是围绕“处理 JavaScript 渲染”这个高难度场景,把 Selenium 的核心打法和盘托出:什么情况下必须上 Selenium、等待和交互怎么写才稳、取数和调试有哪些独家经验。我会尽量用实际操作中积累的例子来讲,读完你至少能少踩几十个常见的坑。

1. 为什么静态请求拿不到动态页面:认识 JavaScript 渲染

1.1 一次完整的浏览器渲染流程拆解

很多初学者会把“爬虫抓 HTML”和“浏览器看页面”理解成同一件事,其实差别很大。你可以把服务器返回的 HTML 看成一份“装修图纸”,上面写的不是直接能用的家具,而是“这里要放一个柜子”“那里要并排放三把椅子”。浏览器拿到图纸后,会先解析 HTML,构建出 DOM 树,然后遇到<script>标签就停下来,把 JavaScript 代码交给引擎执行。代码里可能又有fetch()、XMLHttpRequest或者 WebSocket 请求,浏览器去后端接口拿到数据后,再把结果写进 DOM,最终呈现给你看到的完整页面。

这个流程如果细拆,大概是:请求 HTML → 解析 DOM / CSS → 执行 JavaScript → 发起异步请求 → 动态修改 DOM → 最终渲染完成。requests这类工具只能走到第一步,后面的执行、异步请求、修改 DOM 它都没办法参与。所以哪怕页面上已经显示了 50 条商品信息,你抓回来的源码里也只有一个空空的<div id="app"></div>。

打个生活化的比方:你让 requests 去饭店后厨拿菜谱,它能拿到的只是“菜单第 3 页有红烧肉”这样的静态文本;但真正端到你面前的菜,是厨师(浏览器引擎)照着菜谱现炒的,锅里的香气、颜色、分量,requests 根本看不见。Selenium 干的事情,就是派一个真人(浏览器)进厨房,把最终做好的菜端出来给你挑。

1.2 请求到的页面和最终渲染后的页面差距有多大

举个我曾经实际处理的例子。某个数据平台的列表页,浏览器里显示着完整的卡片信息、评分、跳转链接,我用requests请求后,发现返回的 HTML 里不仅数据是空的,连负责填充数据的接口地址都找不到。页面里只有一个window.__INITIAL_STATE__变量,里面是空的。再翻网络面板才发现,页面加载后会调用一个带签名参数的接口,参数是用 JS 动态生成的,每次刷新都不一样。

这种场景下,光靠分析和拼装请求参数,成本会非常高。更麻烦的是,requests还处理不了下面几类情况:

  • 页面是 Vue 或 React 写的单页应用,路由切换后 DOM 完全由 JS 生成;
  • 数据通过 WebSocket 实时推送,静态请求抓到源码时数据还没到;
  • 需要先点击某个按钮、打开某个弹窗,才会发起新的请求;
  • 页面数据绘制在 Canvas 上,文字只是渲染后的像素点,DOM 里根本捞不到原文。

我把常见的页面类型做了一个对比,你就知道什么时候该上 Selenium 了:

页面类型数据结构静态 requests 可行性是否建议 Selenium
纯静态 HTML数据直接写在源码里完全可行不需要
服务端模板渲染数据由模板填充,源码中直接可见可行,但要注意编码和分页可选
前端框架 + 普通接口数据由接口返回,DOM 由 JS 构建可尝试直接抓接口接口复杂时建议
前端框架 + 动态签名 / 复杂交互数据依赖执行 JS、点击、滚动、登录态基本不可行强烈建议

很多爬虫项目一上来就选 Selenium,其实不是最优解。正确做法是先抓源码,人工或脚本快速判断数据是否直接存在。只有确认数据确实依赖 JS 渲染,并且没有更简单的接口方案时,才把 Selenium 搬出来。这个判断做对了,能帮你省下大量不必要的资源和时间。

1.3 哪些场景必须用 Selenium 接管

结合我自己的经验,下面这些场景基本可以无脑选择 Selenium:

第一,页面需要真实用户行为才能触发数据加载。比如电商页面要往下滑动才会请求“猜你喜欢”,新闻页面要点击“加载更多”按钮才会追加新内容。Selenium 最大的优势就是能模拟真实交互,而不是干巴巴地发请求。

第二,数据依赖登录后的身份状态。很多站点的核心数据必须登录后才能看到,登录过程本身又有 JS 加密、识别码等逻辑。用 Selenium 可以完整走一遍登录流程,让浏览器持有真实的会话状态,后续访问自然会带上 Cookie。

第三,页面经过多层跳转和重定向。你用 requests 跟踪 302 可能没问题,但若页面里有meta refresh、window.location跳转、iframe 嵌套,静态请求根本吃不消,Selenium 却可以直接等到最终页面出现。

第四,你需要验证脚本执行后的页面效果。这其实是自动化测试思维:有时候爬出来的数据不是重点,页面在某次操作后是否出现报错、图表是否闪烁、渲染是否完整,这些交互后的状态只有真实浏览器能反馈。

2. 用 Selenium 接管浏览器:环境准备与方案选型

2.1 为什么选 Selenium 而不是其他工具

Selenium 的全名叫 Selenium WebDriver,它本质上是一个操作浏览器的标准化协议。Chrome、Firefox、Edge 等浏览器都实现了对应的驱动,你写的 Python 代码通过 WebDriver 协议把指令发给浏览器驱动,驱动再转发给浏览器内核,从而实现“打开网页 → 点击 → 输入 → 滑动 → 取值”的一系列操作。

选择 Selenium 的理由有几个:一是它足够成熟,网上资料多,遇到问题基本都能搜到解决方案;二是跨浏览器、跨语言支持非常好,Java、Python、C#、Ruby 都可以;三是它不需要修改被访问网站的代码,原生模拟浏览器环境,对 JavaScript 渲染的兼容性最好。很多大厂的自动化测试框架也基于它,所以遇到复杂页面时,它的上限很高。

当然,Selenium 不是完全没有替代品。后端的 Playwright、Pyppeteer 也都能处理渲染问题,我在第 5 章会专门做一个对比。但如果你现在已经开始用 Selenium 了,先把这套体系玩透,再谈迁移也来得及。

2.2 环境搭建与最容易踩的版本坑

Python 环境下的安装很简单,两条命令就能完成基本搭建:

pip install selenium pip install webdriver-manager

第二项是 WebDriver Manager,强烈建议装上。它最大的用处是自动下载和你本地 Chrome 版本匹配的驱动文件,省去手动去chromedriver.chromium.org找驱动、解压、放路径的麻烦。基础启动代码长这样:

from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager options = webdriver.ChromeOptions() driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options) driver.get("https://example.com") print(driver.title) driver.quit()

这套流程最难受的坑就是版本匹配。Chrome 和 ChromeDriver 之间存在严格的对应关系,比如你本机 Chrome 升到了某个新版本,而 Chromedriver 还停在旧版本,启动时会直接报session not created或This version of ChromeDriver only supports Chrome version xx。刚开始做爬虫的人遇到这个报错会一脸懵,其实不用慌,把 WebDriver Manager 换成最新版,让它重新拉取匹配的驱动就行。

还有一点很多人容易忽略:如果你的生产服务器上没有 Chrome,只有 Chromium 或 Edge,驱动也得跟着换。比如用 Edge 浏览器,就要用EdgeChromiumDriverManager,代码里改成webdriver.Edge(...)。驱动、浏览器类型、浏览器版本三者必须对齐,这是使用 Selenium 的底层逻辑。

2.3 浏览器参数配置:每个参数都对应一个坑

Selenium 启动浏览器时,可以加载一堆选项,最常见的参数模板我整理出来供你参考:

from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--headless=new") # 无头模式,不弹窗口 options.add_argument("--disable-gpu") # 关闭 GPU 加速,兼容性更强 options.add_argument("--no-sandbox") # 某些 Linux 服务器必须加 options.add_argument("--ignore-certificate-errors") # 忽略证书错误 options.add_argument("--window-size=1920,1080") # 设置窗口大小 options.add_argument("--user-agent=Mozilla/5.0 ...") # 自定义 UA options.add_argument("--disable-blink-features=AutomationControlled") options.page_load_strategy = "eager" # 页面加载策略 prefs = {"profile.managed_default_content_settings.images": 2} options.add_experimental_option("prefs", prefs) # 关闭图片加载

逐个细讲一下:

--headless=new是 Chrome 新版的无头模式。老版本用--headless,换成 Chrome 109 之后建议用--headless=new,因为新版在隐藏模式下也能保留更多浏览器行为,对检测的抵抗性更强,但代价是有些页面在无头模式下渲染出来的结构会不一样。明明手动打开浏览器数据正常的页面,换成无头模式就找不到元素,这种问题我也遇过多次。遇到这种情况,别急着改代码,先去掉 headless 参数,用有头模式跑一遍,确认问题是否只出现在无头模式下。

--ignore-certificate-errors不是必须的,但很多测试环境的 https 证书不完整,不加这个参数页面直接白屏。

page_load_strategy = "eager"会让driver.get()在 DOM 加载完成后就立刻返回,不用等图片、脚本全部加载完。这个参数对性能优化极其重要,后面我会单独展开。

prefs里的profile.managed_default_content_settings.images设成 2,意思是禁用图片资源,这对抓取文字类数据能节省大量带宽和加载时间。

3. 核心实操:等待、点击、滑动、取值

3.1 等待策略:向 sleep 硬等说再见

用 Selenium 处理 JavaScript 渲染页面时,最多人犯的错误就是一上来写time.sleep(3)。这种做法非常不稳定:网络快的时候页面 1 秒就渲染完了,你还在傻等 2 秒;网络慢的时候 10 秒还没出来,你只等 3 秒,元素根本找不到。正确做法是显式等待和隐式等待配合,让浏览器根据实际情况动态决定等待时间。

隐式等待的意思是告诉 WebDriver:在查到某个元素之前,最多等这么久。设置一次,全局生效:

driver.implicitly_wait(10)

隐式等待有个局限性:它只在查找元素的时候生效,无法判断元素是否可见、可点击、还是已经消失。所以更可靠的是显式等待,配合expected_conditions使用:

from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, timeout=10, poll_frequency=0.5) element = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".list-item")))

这段代码的意思是:最多等 10 秒,每 0.5 秒检查一次,直到页面上的.list-item元素可见,才继续执行。如果超时还没出现,就抛出TimeoutException。

比较常用的等待条件我整理成了速查表:

等待条件判断内容典型场景
presence_of_element_located元素是否出现在 DOM 中页面刚加载完,元素还没完全可见时
visibility_of_element_located元素是否可见需要点击或读取文本时
element_to_be_clickable元素是否可见且可点击点击按钮前
frame_to_be_available_and_switch_to_itiframe 是否可用页面数据封装在 iframe 中
staleness_of元素是否已经不在 DOM 中判断旧元素是否被刷新替换
text_to_be_present_in_element元素内是否包含指定文本等待异步数据填充完成

实战中我更推荐优先写显式等待,不要依赖sleep。只有在确实无法用条件判断、必须等某个计时器走完的情况下,才用sleep兜底,而且要尽量把等待时间缩短。

3.2 模拟用户交互:点击、输入、滚动加载

JavaScript 渲染页面另一个常见特征是用户操作会触发新的内容。比如点“加载更多”按钮、输入关键词搜索、横向滑动切换 tab、滚动到页面底部触发懒加载。这些动作 Selenium 都能模拟,但很多细节不到位就会失败。

先看最常见的点击“加载更多”按钮。这类按钮往往在数据流末尾,需要先滚动到可见位置再点击,否则 WebDriver 会提示element is not interactable:

from selenium.webdriver.common.action_chains import ActionChains load_more = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), '加载更多')]"))) driver.execute_script("arguments[0].scrollIntoView();", load_more) load_more.click() # 点击后等待新的列表项出现 wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".card-item")))

再来看页面滚动。很多页面的数据是“无限滚动”模式,往下滑到一定位置才会加载一批新数据。滚到底部的通用写法是:

driver.execute_script("window.scrollTo(0, document.body.scrollHeight)")

如果页面是在某个容器内部滚动,不是整个窗口滚动,这个写法就没用了。你需要定位到那个容器,然后修改它的scrollTop:

container = driver.find_element(By.CSS_SELECTOR, ".scroll-container") driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight", container)

热搜词里提到的“selenium 网页左右滑动”,在横向轮播或者图表翻页场景中也很常见。横向滚动可以借助ActionChains,比如向右侧移动鼠标:

from selenium.webdriver.common.action_chains import ActionChains slider = driver.find_element(By.CSS_SELECTOR, ".horizontal-panel") ActionChains(driver).move_to_element(slider).move_by_offset(300, 0).perform() driver.execute_script("arguments[0].scrollLeft = 500", driver.find_element(By.CSS_SELECTOR, ".horizontal-panel"))

滚动式加载的数据有一个天然的不稳定性:你无法确认数据是不是已经全部加载完了。我在实战里会用一个“等到数据条数稳定”的策略:不断滚动到底部,连续两次检测到同样的列表长度和底部标记,才认为加载结束:

def wait_for_scroll_finish(driver, locator, max_rounds=10): last_count = 0 unchanged_rounds = 0 for _ in range(max_rounds): driver.execute_script("window.scrollTo(0, document.body.scrollHeight)") time.sleep(1) # 这里只能小幅度等待,给渲染留时间 elements = driver.find_elements(*locator) current_count = len(elements) if current_count == last_count: unchanged_rounds += 1 if unchanged_rounds >= 2: return elements else: unchanged_rounds = 0 last_count = current_count return elements

time.sleep(1)这种写法我前面刚批评过,但在这个场景里反而合理:因为滚动触发加载的行为没有统一的 DOM 标记可以用,很难用 expected_conditions 精确判断,只能给一个小缓冲时间,然后用轮询逻辑兜底。注意这里的关键是“连续两次长度不变就结束”,而不是盲目等待固定的 10 秒。

3.3 数据提取的几种姿势:文本、属性、执行 JS 取值

元素定位到了,数据提取就有多种方式。最常规的是取文本和取属性:

cards = driver.find_elements(By.CSS_SELECTOR, ".card-item") for card in cards: title = card.find_element(By.CSS_SELECTOR, ".title").text price = card.find_element(By.CSS_SELECTOR, ".price").text link = card.find_element(By.CSS_SELECTOR, "a").get_attribute("href") print(title, price, link)

这里有个性能细节:如果在循环里每次都调用driver.find_elements重新查整个页面,速度会非常慢。正确做法是先把一页的卡片列表一次性查出来,然后再逐个解析,减少浏览器和 Python 之间的通信次数。如果页面数据特别多,可以考虑先用 XPath 把关键字段全部提取出来,再配合execute_script一次性把多个值返回:

data = driver.execute_script(""" const items = []; document.querySelectorAll('.card-item').forEach(el => { items.push({ title: el.querySelector('.title')?.innerText, price: el.querySelector('.price')?.innerText, link: el.querySelector('a')?.href }); }); return items; """) print(data)

这种写法把查找逻辑直接交给浏览器原生 JS,速度几乎吊打 Python 层反复查找。因为 Selenium 每调一次find_element都要走 WebDriver 协议,跨进程通信很多次,而execute_script只通信一次,只是 JavaScript 返回的数据结构必须是 JSON 可序列化的。

取数过程中还有一个老生常谈但高频踩坑的点:iframe。很多第三方数据、登录弹窗都嵌在 iframe 里,直接用driver.find_element会报no such element。原因是 WebDriver 默认只在当前主文档的 DOM 中查找,不会自动进入 iframe。解决方案是显式切换:

# 先等待 iframe 可用并切进去 frame = wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, "mainFrame"))) # 切进去之后,查找操作都在 iframe 内生效 content = driver.find_element(By.CSS_SELECTOR, ".main-content").text # 处理完后切回主文档 driver.switch_to.default_content()

等你切完 iframe,再想找主文档里的元素,一定要记得切回来,不然就会一头雾水地报element not found。新窗口的切换逻辑类似:先记录当前窗口句柄,打开新窗口后driver.switch_to.window(driver.window_handles[-1]),操作完再切回原来的窗口句柄。

3.4 等页面“真正稳定”的做法

处理复杂页面时,我一般会额外封装一个“等待网络空闲”的思路。Selenium 本身没有直接提供“网络空闲”的 API,但可以通过判定 DOM 状态来间接实现。前文已经展示了滚动加载的稳定判断,这里再补充一个更通用的轮询方法:等待页面上某个标志元素出现,且连续两次查询结果一致。

比如你要抓一个图表,页面会先显示“加载中”,然后渲染出图表 SVG。你可以在代码里先轮询等待“加载中”消失,再等待图表容器的节点数稳定:

def wait_until_stable(driver, css_selector, timeout=20, stable_rounds=2): start = time.time() last_value = None stable_count = 0 while time.time() - start < timeout: try: elems = driver.find_elements(By.CSS_SELECTOR, css_selector) current_sign = (len(elems), driver.execute_script("return document.body.innerText.length")) except Exception: current_sign = None if current_sign == last_value: stable_count += 1 if stable_count >= stable_rounds: return True else: stable_count = 0 last_value = current_sign time.sleep(0.5) return False

这个函数把“元素数量”和“页面文本总长度”组合成稳定信号,只有连续两次采样一致才认为页面稳定。实际跑起来比固定等待可靠得多,尤其在网络波动大的环境下,能明显减少因页面未渲染完成导致的抓取失败。

4. 常见问题与排查技巧实录

4.1 元素定位失败:按这个顺序排查

我调试 Selenium 脚本时,遇到过太多NoSuchElementException。新手的反应往往是改选择器,但其实问题可能根本不在选择器上。我建议你按下面的顺序排查:

排查点现象验证方法
页面还没有渲染完元素报找不到,但手动打开能看到打印driver.page_source看有没有相关节点
元素在 iframe 中主文档找不到检查页面源码里有没有<iframe>
元素不是静态节点数据是 JS 异步填充用time.sleep(2)再查一次,确认是否动态出现
选择器写错报错,但页面源码有在浏览器 DevTools 里手动验证选择器
元素被遮罩层挡住元素存在,但提示不可交互检查页面是否有弹窗、浮层覆盖
页面跳转到了新 URL在旧页面找新页面的元素打印driver.current_url确认地址

我自己最常用的调试手段,是在except分支打印当前页面的关键快照,然后保存到本地:

try: element = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".target"))) except Exception as e: print("当前URL:", driver.current_url) print("页面源码长度:", len(driver.page_source)) with open("debug.html", "w", encoding="utf-8") as f: f.write(driver.page_source) raise e

这一步看起来简单,但能帮你快速定位:如果保存出来的 HTML 里压根没有你想找的节点,说明问题在“页面渲染没完成”或“选错了页面层级”;如果节点存在但代码找不到,那大概率是 iframe 或 new window 没切换。

4.2 页面加载超时与卡死的处理办法

Selenium 默认会等页面完全加载完成才执行下一步,这对图片多、广告多、埋点脚本多的页面非常不友好。我处理过很多次TimeoutException的崩溃,后来总结了三个方面的优化。

第一,设置页面加载策略。把page_load_strategy从默认的normal改成eager,代表 DOM readyState 变为interactive时就认为加载完成,不再等待所有资源。这样可以显著提升速度:

options.page_load_strategy = "eager"

第二,限制浏览器加载无关资源。很多站点真正需要的数据只是文本和结构,图片、字体、视频完全可以关掉。用前面提到的 prefs 配置关闭图片后,页面加载时间经常能缩短一半以上。

第三,给driver.get()包一层超时保护。有的页面会进入诡异的“一直加载中”状态,哪怕设置了页面加载策略也无济于事。这时候可以用driver.set_page_load_timeout(20)限制整体加载时间,超时就捕获异常重试。配合重试逻辑,代码稳定性会好很多:

from selenium.common.exceptions import TimeoutException for attempt in range(3): try: driver.set_page_load_timeout(20) driver.get(url) break except TimeoutException: driver.execute_script("window.stop()") print("第", attempt + 1, "次加载超时,强制停止")

driver.execute_script("window.stop()")是大杀器:页面还在加载时,强制命令浏览器停止后续资源的获取,此时已经渲染出来的 DOM 仍然可以照常操作。这个技巧非常实用,很多复杂页面就是靠它强行拉回节奏的。

另外,页面卡死还会表现为点击按钮无反应。此时先别急着点击,检查是不是有弹窗遮挡。如果遇到了 JavaScript 的原生弹窗(alert、confirm、prompt),需要先接受或关闭它:

from selenium.webdriver.common.alert import Alert Alert(driver).accept() # 接受弹窗,相当于点击确定 Alert(driver).dismiss() # 取消弹窗

原生弹窗会阻塞页面脚本执行,如果代码里没处理,后面的操作会一直等待超时。换个角度说,能用完成.until判断弹窗是否存在,是防止卡死的最稳手段。

4.3 如何快速定位 JavaScript 运行时报错

抓动态页面时,经常会有一种“潜水”的感觉:你看到的页面是 JS 执行完之后的样子,但 JS 在哪个环节报错了,浏览器控制台的信息你不一定看得到。好在 Selenium 提供了读取浏览器日志的能力,你可以在关键时刻把日志捞出来:

logs = driver.get_log("browser") for log_entry in logs: if log_entry["level"] == "SEVERE": print("JS报错:", log_entry["message"])

用这个方法,我抓过一个渲染不完全的图表页面。当时页面大部分数据都正常,只有最后几行是空的,前端控制台报了一个Cannot read property 'x' of undefined。看到这条报错后,我才明白页面内部异步组件的渲染顺序有问题,于是加了一个前置的等待条件,问题直接解决。

不过要注意,driver.get_log("browser")必须在 ChromeDriver 开启日志能力时才有效,如果没有任何输出,可以加上下面的参数再试:

options.set_capability("goog:loggingPrefs", {"browser": "ALL"})

另外,execute_script执行自定义 JS 报错时,异常信息是标准 JavaScript 错误文本,和浏览器控制台里的基本一致。遇到报错先看清是哪一行、哪个属性出了问题,比盲目改代码高效得多。

4.4 关于“反爬”和自动化的边界问题

热搜词里出现“python selenium反爬虫”,说明很多人都关心 Selenium 会不会被识别。这个问题我简单说清楚:网站完全可以通过 JS 检测window.navigator.webdriver属性、浏览器的插件对象、行为轨迹等指标,判断访问是否由自动化工具驱动。部分网站还会加上人机验证、请求频率限制和 IP 封禁策略。

我认为做自动化测试和合法爬虫,核心不是写一套“绝对不被识别”的伪装方案,而是要搞清楚目标站点允许什么、禁止什么。可以合理做的事情包括:遵循网站的robots.txt约定、控制抓取频率、不恶意占用服务器资源、不对登录后的私密信息做批量采集。我在实际项目中,更多时间花在优化等待策略和选对等待条件上,而不是想方设法绕过识别机制。即使要调整用户代理或浏览器参数,也应当保持在“模拟正常用户访问”的合理范围内,而不是积极规避安全机制。

爬虫本身是个中性技术,能不能持续用,取决于你有没有尊重目标平台的数据边界。这个边界划定清楚,你后续写代码心里才有底。

5. 性能优化与备选方案

5.1 单实例多页面任务,少开浏览器

一个刚接触 Selenium 的人最容易写出这样的代码:在循环里反复driver = webdriver.Chrome(),处理完一个页面就driver.quit(),下一个页面再从头启动。这个写法在功能上没问题,但性能上非常差。每次启动 Chrome 都要加载插件、初始化上下文、建立 WebDriver 连接,耗时少则几秒,多则十几秒。如果一个任务要跑几千个 URL,总耗时会多出非常多。

更好的做法是让无头浏览器常驻。只要页面之间相互独立,就可以在一个 driver 实例上连续处理:

driver = webdriver.Chrome(options=options) try: urls = ["https://a.com", "https://b.com", "https://c.com"] for url in urls: driver.get(url) # 处理当前URL的逻辑 finally: driver.quit()

需要注意的是,连续访问不同站点时,上一个站点的 localStorage、Cookie 可能会保留。如果你要处理的数据不希望被“串味”,可以在每次driver.get()前清理:

driver.delete_all_cookies() driver.execute_script("window.localStorage.clear();")

这里也涉及一个取舍:如果你需要使用同一个登录态处理多个页面,那就不能删除 Cookie。最好根据实际业务决定清理策略。

5.2 并发爬虫为什么慎用 Selenium

谈到性能,很多人第一反应是“开多线程”。但在 Selenium 场景里,多开 10 个浏览器实例,你的内存和 CPU 就会爆炸。一个典型 Chrome 实例的内存占用大约在 200MB 到 500MB,取决于页面复杂度,这么一算,并发 5 个就可能吃掉 2GB 以上内存。还有一个更隐蔽的问题:每个 driver 实例都要占用端口和进程资源,数量一多,系统文件句柄不够用,到处是奇怪异常。

如果你确实需要并发,我的建议有两点:一是严格控制并发数,单机跑 2 到 4 个实例已经很多;二是优先考虑用多进程而不是多线程,因为浏览器进程本身就是高耗资源的,多线程在 Python 里还受 GIL 限制,收益不大。考虑到资源占用,我更推荐大家优先用第一节说过的思路:能请接口就直接请求接口,Selenium 只处理最棘手的渲染页面。

5.3 Selenium 之外的替代方案怎么选

Selenium 虽强,但它也有一些固有的麻烦,比如版本匹配、需要手动等待、异常处理繁琐。近几年 Playwright 和 Pyppeteer 也很流行,很多人问我该不该换。

Playwright 的核心优势是:内置了自动等待机制,page.locator操作元素时会自己等待元素可见,不需要你手写WebDriverWait;还支持网络请求拦截,可以直接 mock 接口数据;而且它的浏览器驱动是内置的,不需要额外下载。对于新项目,我会优先考虑 Playwright。

Pyppeteer 是 Puppeteer 的 Python 移植版,也能处理 JS 渲染,不过社区维护不如前两者活跃。Selenium 的优势则在于生态成熟、岗位需求大、老项目里很多代码都是基于它的,学了不容易浪费。

简单总结一下我的选型思路:

工具优点适合场景
Selenium成熟、资料全、兼容性好老项目、测试框架、跨语言需求
Playwright自动等待、网络拦截、驱动内置新项目、复杂交互、需要高性能
Pyppeteer简单易上手临时任务、轻量需求

说实话,工具本身差异并不大,真正影响开发效率的是你对页面渲染机制的熟悉程度。Selenium 你玩得转,转到 Playwright 也就是半天的事。

5.4 沉淀一套自己的页面渲染处理套路

最后想分享一个我在多个项目里验证过的个人习惯。处理任何 JavaScript 渲染页面,我不会马上写抓取逻辑,而是先打开浏览器 DevTools 的 Network 面板,把页面从请求开始到数据完整的整个时间线过一遍。看哪些请求是数据接口,哪些请求是追踪脚本,页面在什么时机触发第二次加载,数据是直接在 DOM 里还是在 iframe 里。这个过程可能只花 10 分钟,但能帮你省下后面写代码、调试的半天时间。

等代码写完,我还会刻意观察跑动过程的浏览器日志,看有没有报错、有没有因为等待不足而白跑的情况。久而久之,你会形成一套非常稳定的处理流程:先判断是否需要 Selenium,再配置浏览器参数,然后用显式等待和稳定轮询处理动态加载,最后提取数据并做好异常兜底。

另外一个我私藏的技巧是:如果页面数据量很大,尽量在execute_script里用原生 JS 把列表数据一次性取出,而不是用 Python 层反复遍历。这种做法不仅能降低 WebDriver 的通信次数,还能让代码的整体耗时下降一个数量级。遇到特别复杂的页面,我甚至会先写一个纯 JS 的数据提取函数,在 DevTools 里调试好,再粘贴到 Selenium 脚本里跑,效率高得多。

爬虫做到后面,拼的其实不是用了多聪明的工具,而是你对浏览器渲染过程的理解深不深。Selenium 只是把你从“只能看到 HTML 源码”的局限里解放出来的第一步,真正的进阶,是你懂得在合适的位置等待、合适的方式取数,以及在各种异常出现时,快速定位问题所在的那种判断力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 6:54:15

零碳园区商业模式怎么赚钱?政策支持体系全拆解

搞零碳园区这几年&#xff0c;我被客户问得最多的一句话是&#xff1a;“零碳模式到底怎么赚钱&#xff1f;”预算批了、减碳意愿也有&#xff0c;但一到测算模型那里就卡住。其实这个问题的背后&#xff0c;藏着一个更现实的问题——政策给的钱、给的收益工具、给的交易空间&a…

作者头像 李华
网站建设 2026/10/11 6:53:42

护眼灯品牌排行第一名是哪家?护眼台灯真实口碑测评,选灯有参考

​护眼灯品牌排行第一名是哪家&#xff1f;不少家长挑选台灯时都会有这个疑问&#xff0c;想要找到口碑靠谱的款式&#xff0c;这份真实测评可以作为选灯参考。孩子写作业总爱歪头&#xff0c;很多时候不是习惯不好&#xff0c;而是台灯存在照明死角。普通台灯仅能照亮正下方&a…

作者头像 李华
网站建设 2026/10/11 6:51:14

6GB显存跑通Qwen-Image-2.1:量化、显存卸载与注意力切片实战指南

我折腾了两天&#xff0c;总算把 Qwen-Image-2.1 压到一块只有 6GB 显存的 GTX 1660Ti 上跑通了。说实话&#xff0c;这个目标一开始听起来有点离谱——现代图像生成大模型的权重动辄三四十 GB&#xff0c;哪怕量化后也要十几 GB&#xff0c;6GB 显存连模型本体都塞不下。但真做…

作者头像 李华
网站建设 2026/10/11 6:49:56

C盘AppData占87.81GB?用Codex精准定位与安全清理

C盘爆红的时候&#xff0c;人最容易上头。看着剩余空间从几 GB 掉到 0&#xff0c;很多人第一反应就是到处找文件夹删。但C盘真不是“删得越多越好”&#xff0c;特别是那个叫 AppData 的隐藏目录——它经常占着几十 GB&#xff0c;可你根本不敢动。我这次没有乱试&#xff0c;…

作者头像 李华
网站建设 2026/10/11 6:49:45

kaggle notebook下载方法

今天下载 Kaggle Notebook 文件时&#xff0c;两个文件进入不同页面&#xff0c;页面A和页面B页面A遍寻找不到下载按钮&#xff0c;页面B下载按钮则轻松可见点击页面B下载后成功下载.ipynb文件&#xff0c;ai可解析页面A则怎么找也找不到变成页面B的按钮&#xff0c;另寻方法&a…

作者头像 李华