☰
Selenium实战:从零破解JavaScript渲染页面的爬虫核心思路
2026/10/22 23:45:42 网站建设 项目流程

做了几年爬虫开发,我越来越觉得,高级爬虫技巧里最容易被低估的一项,就是处理JavaScript渲染页面。很多刚入门的朋友习惯用 requests 一把梭,正则、XPath 都写好了,结果换到一个前后端分离的站点,HTML 源码里干干净净,数据全得靠浏览器现场执行脚本才能出现。这时候懂不懂Selenium,就成了能不能继续往下走的分水岭。

这篇文章我不打算讲那些花里胡哨的框架,就围绕 Selenium 这一个老牌工具,把 JavaScript 渲染页面的处理思路、底层原理、完整实操和避坑经验一次说透。不管你是刚接触爬虫的新手,还是已经被动态页面折磨过一阵子的开发者,按这篇的思路去走,应该能少踩七八成的坑。

1. 为什么JavaScript渲染是爬虫路上的一堵墙

1.1 浏览器里看到的数据,HTML源码里却没有

你打开一个页面,看到商品列表、价格、评论、翻页数据都整整齐齐地显示在浏览器里。但如果你右键查看网页源代码,会发现那堆 HTML 里只有空壳的<div>、<script>标签和一些初始化的配置项。数据在哪?根本不在原始响应里。

这是因为现在的 Web 开发早就从“服务端渲染”转向了“客户端渲染”。后端提供的只是一个静态骨架和一堆 JavaScript 脚本,浏览器拿到这些脚本后,再去请求若干个接口,拿到 JSON 数据,然后把数据动态地插入页面 DOM 里。这个过程发生在浏览器内部,requests 这种只发 HTTP 请求的库,自然拿不到渲染后的结果。

这种情况在 Vue、React、Angular 这类框架的站点上特别常见。我处理过的某数据平台、某电商活动页,几乎全都是这种模式。你直接抓接口有时候能拿到一部分数据,但遇到参数加密、接口鉴权、数据结构不稳定的情况,还不如老老实实模拟浏览器渲染。

1.2 处理动态渲染的3条主流路线

面对这种页面,行业内通常有三条路线:

  1. 直接分析接口,绕过渲染。用抓包工具看浏览器发了哪些 XHR 请求,找到真正的数据接口,直接调。这条路效率最高、性能最好,也是我最推荐优先尝试的。
  2. 使用无头浏览器自动化工具。代表就是 Selenium、Playwright、Puppeteer。让真实的浏览器引擎去加载页面、执行 JS、渲染 DOM,脚本等数据出现后再抓取。这条路稳定、通用,适合接口分析困难、有复杂交互的场景。
  3. 使用渲染代理服务。把 URL 丢给云端渲染服务,服务端用浏览器渲染完再返回 HTML。适合临时救急,但一般要收费,而且数据实时性不如自己本地控制。

三条路线各有优劣,我通常的建议是:能走第一条,绝不走第二条;第二条主要用于破解“非走浏览器不可”的场景。

1.3 Selenium适合哪些场景,又不适合哪些场景

Selenium 本质上是浏览器自动化测试工具,后来被爬虫圈大量借用。它最大的优点是“所见即所得”,你在浏览器里能做的操作,它都能模拟,而且不用关心目标页面到底用了什么前端框架。登录态、cookie、iframe、弹窗、滚动加载这些场景,它天然就支持。

但它也有明显的短板:速度慢、资源占用高。每创建一个浏览器实例,内存轻轻松松几百 MB。你要是拿它去抓几千万条纯列表数据,别想了,性能和成本都扛不住。所以我在项目里通常把它定位成“攻坚型工具”——页面交互复杂、接口拿不到、又必须要数据的时候,用它顶上;平时优先用接口方案。

2. 搞懂Selenium的工作原理,才能用得心里有底

2.1 WebDriver协议:自动化脚本和浏览器之间的翻译官

先说一个很多人忽略的点:Selenium 本身并不直接操作浏览器,它是通过 WebDriver 协议来驱动的。你把脚本交给 Selenium,Selenium 把操作翻译成标准化的 HTTP 指令,发给浏览器自带的驱动组件(比如 ChromeDriver),驱动组件再把这些指令转换成浏览器内部的原生操作。

也就是说,整个链路是:你的 Python 代码 -> Selenium 客户端 -> WebDriver 协议指令 -> 浏览器驱动 -> 浏览器内核。这跟 requests 直接发 HTTP 请求是完全两码事。理解这一点很重要,因为你会发现很多“奇怪的问题”都出在这一层通信上:驱动版本不对,浏览器压根打不开;页面加载慢,等待超时;元素还没渲染出来,指令就发下去了。

WebDriver 协议最初是各个浏览器自己定义的,后来统一成了 W3C 标准。所以现在用 Selenium 4 操作 Chrome、Firefox、Edge,API 基本上是一致的,你只需要换不同的驱动组件就行。

2.2 环境搭建容易被坑的3个细节

Selenium 的安装本身不复杂:pip install selenium就搞定了。真正容易出问题的是驱动。

第一个坑是版本匹配。Chrome 和 ChromeDriver 的版本必须大版本号一致,否则会报session not created或者压根连不上。我自己的习惯是先在浏览器地址栏输入chrome://version看版本号,然后去对应的驱动下载页下载同版本驱动。Selenium 4.6 之后的版本有 Selenium Manager,会自动帮你处理驱动下载和匹配,省了很大功夫,但还是建议手动确认一下。

第二个坑是 PATH 环境变量。如果驱动没有放进系统 PATH,Selenium 会直接报WebDriverException: 'chromedriver' executable needs to be in PATH。最简单的办法是把驱动放到 Python 解释器同一个目录,或者在初始化时用service=Service('/绝对/path/to/chromedriver')显式指定路径。

第三个坑是浏览器沙箱权限。在 Linux 服务器上跑 Selenium,经常遇到以 root 身份运行时需要加参数--no-sandbox的情况。这属于老生常谈,但每次换环境都会碰到,索性在这里提一嘴。

2.3 初始化Driver时的推荐参数配置

这里直接给一份我在项目中常用的初始化模板,省得你从零摸索:

from selenium import webdriver from selenium.webdriver.chrome.service import Service options = webdriver.ChromeOptions() # 无头模式,不弹出浏览器窗口,适合部署在服务器上 options.add_argument('--headless=new') # 屏蔽“正在被自动化软件控制”的提示条 options.add_argument('--disable-blink-features=AutomationControlled') # 去掉自动化相关标记 options.add_argument('--disable-infobars') # 指定窗口大小,避免某些响应式布局把元素隐藏掉 options.add_argument('--window-size=1920,1080') # 禁用 GPU,服务器上一般没有独立显卡,不加容易报错 options.add_argument('--disable-gpu') # 关闭沙箱,容器环境里不生效 options.add_argument('--no-sandbox') # 禁用 /dev/shm 使用,防止 Docker 容器里内存不够 options.add_argument('--disable-dev-shm-usage') # 自定义 User-Agent,降低部分网站默认检测风险 options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36') service = Service('/path/to/chromedriver') driver = webdriver.Chrome(service=service, options=options)

这些参数不是凭空堆上去的,每一个背后都有真实踩坑经历。比如不设置窗口大小,有些页面默认手机视口,元素布局完全不一样,定位就全乱了;不屏蔽自动化提示条,截图的时候也会看到黄条。你在自己的环境里不一定全都要,但先照抄一份,出问题概率会小很多。

3. 实战:用Selenium啃下一个JavaScript渲染页面的完整流程

3.1 等待策略:让页面把数据“吐”出来再动手

刚开始用 Selenium 的人最容易犯的一个错,就是打开页面后立刻找元素,然后报NoSuchElementException。为什么找不到?因为页面渲染是异步的,JS 还没跑完,数据还没插进 DOM,你的代码就已经去找了。

解决这个问题的核心是“等待”,而且要用对等待方式。常见的有三种:

  • 强制等待:time.sleep(5)。简单粗暴,但时间短了不稳定,时间长了浪费效率,而且页面网络波动时特别容易打脸。
  • 隐式等待:driver.implicitly_wait(10)。告诉浏览器在查找每个元素时最多等 10 秒。问题是它也等待页面加载完成,而对 Ajax 动态插入的元素不一定有效。
  • 显式等待:WebDriverWait+expected_conditions。专门等某个条件成立,比如某个元素可见、存在、可点击,精确又高效。

我个人的建议是:主用显式等待,放弃隐式等待。举个真实例子,某信息查询类站点的搜索结果是通过 JS 异步刷新出来的,用隐式等待经常 5 秒就报错,改成显式等待后稳定了很多。

from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 等待某个 id 为 result_list 的元素变得可见 wait = WebDriverWait(driver, 10) element = wait.until( EC.visibility_of_element_located((By.ID, 'result_list')) )

这里我特别想说明presence_of_element_located和visibility_of_element_located的差别。前者只要求元素存在于 DOM 里,哪怕还藏在页面底部或者display: none状态;后者要求元素真正可见。如果你后续要点击元素,最好用element_to_be_clickable,否则可能元素存在了,但按钮还不可点,依然会报错。这个细节是区分新手和老手的一个标志。

3.2 元素定位的取舍:从快准狠到兜底方案

页面渲染出来后,下一步是定位元素。Selenium 提供多种定位方式:By.ID、By.CLASS_NAME、By.CSS_SELECTOR、By.XPATH、By.NAME等。性能和使用偏好差别很大。

我的优先级习惯是:ID 优先,CSS 选择器次之,XPath 兜底。ID 是页面里唯一标识,定位最快最稳;CSS 选择器语法简洁,阅读性好,适合按层级关系找;XPath 功能最强,能支持各种复杂逻辑,比如“包含某个字符串”“找父节点后的兄弟节点”,但性能和可读性一般。

举个例子,你要抓取列表里所有商品标题,每个标题的 class 都叫list-title,但外层容器不同。用 CSS 可以这样:

elements = driver.find_elements(By.CSS_SELECTOR, 'div.goods-list a.item-title')

如果目标这个 class 是动态生成的,比如item-title-28173这种每次刷新都变的后缀,CSS 就不好使了。这时候 XPath 的contains就派上了用场:

elements = driver.find_elements(By.XPATH, '//a[contains(@class, "item-title")]')

还有种情况也经常遇到:同一个class名在页面里出现几十次,但父节点不同。你可以通过 XPath 的following、preceding、ancestor等轴来精确定位,不过这种写法复杂,建议先用浏览器开发者工具里的“Copy XPath/Copy Selector”生成一份再改,比自己凭空写快得多。

3.3 高频交互场景:iframe、弹窗、下拉加载

处理 JavaScript 渲染页面,你不光要“看”,还要“操作”,否则好多页面根本不会加载后续数据。

先说 iframe。有些第三方组件(比如登录框、支付面板、客服系统)是通过 iframe 嵌入的,里面的元素在 Selenium 里直接定位会报找不到元素。原因很简单:DOM 树里 iframe 是独立文档,你要先切换进去:

driver.switch_to.frame(driver.find_element(By.CSS_SELECTOR, 'iframe[src*="login"]')) # 操作 iframe 内部元素 driver.find_element(By.ID, 'username').send_keys('xxx') # 操作完切回主文档 driver.switch_to.default_content()

注意,switch_to.frame之后,所有定位都只针对 iframe 内部文档,除非你切回主文档,否则很容易遇到“明明在页面里看得见却找不到”的诡局。

再说弹窗。很多页面会弹出遮罩广告、引导浮层,直接挡住目标元素导致无法点击。常规做法是用 JavaScript 强制点击,或者等弹窗消失。如果是原生的alert弹窗,Selenium 里这样处理:

alert = driver.switch_to.alert print(alert.text) alert.accept() # 确定 # alert.dismiss() # 取消

下拉加载更常见。这类页面初始只给 20 条数据,你滚动到页面底部,它才去请求下一页数据。实现方式通常是执行 JS 滚动:

driver.execute_script('window.scrollTo(0, document.body.scrollHeight);') # 等待数据加载 wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div.list-item')))

如果页面不是整页滚动而是某个容器内部滚动,就要把滚动目标从document.body换成对应容器元素。这个细节不重,但实际项目里十个有八个是卡在这。

3.4 登录态保持:手动登录一次,后面全自动

很多动态页面的数据接口需要登录后才能请求。Selenium 处理登录的方式比 requests 舒服得多:你手动打开浏览器,自己输入账号密码,遇到验证码也能手动过,登录成功后再把 cookie 存下来,后面程序启动时直接注入 cookie,不用每次都走登录流程。

import json # 第一次手动登录后,保存 cookies cookies = driver.get_cookies() with open('cookies.json', 'w') as f: json.dump(cookies, f) # 下次启动时加载 cookies driver.get('https://example.com/login') # 先访问首页建立会话 for cookie in cookies: driver.add_cookie(cookie) driver.refresh()

这里有个常见的坑:add_cookie之前必须先访问一次目标域名的主页,否则浏览器不知道该 cookie 属于哪个域名。另外 cookie 里如果有expiry字段,直接传给add_cookie有时会报错,读取时最好过滤掉。

如果能拿到登录态的 cookie,你甚至可以把它导出给 requests 使用,这样既能有登录权限,又能走接口方案抓数据,速度和稳定性都比纯 Selenium 好不少。cookie 导出成字典格式,放进 requests 的headers或直接作为cookies参数传入即可。

4. 我踩过的坑:常见问题与排查清单

4.1 元素明明存在,却一直报NoSuchElementException

这是我被问得最多的问题,省流版排查顺序:

  1. 是否在 iframe 里?先switch_to.frame,再定位。
  2. 是否在 shadow DOM 里?部分前端组件会用 shadow DOM 封装内部结构,普通定位找不到。需要先拿到 shadow host,再用 JS 穿透。
  3. 是否在窗口外面?元素可能在页面底部,需要先滚动到可视区域。
  4. 是否有多个同名 class?find_element只返回第一个,可能第一个是隐藏的,真正的元素在后面,改成find_elements取索引。
  5. 是否页面还在加载?等待时间不够,换成显式等待至少等 10 秒。
  6. 是否在新标签页?点击链接后会开新页,旧页面里当然找不到。需要driver.switch_to.window(driver.window_handles[-1])。
  7. 浏览器是否弹出了异常页面?先看driver.current_url和driver.page_source,要么被验证拦截,要么跳到了 404。

排查时最有用的命令是:

print(driver.page_source) # 把当前页面 HTML 打出来分析 print(driver.current_url) # 看看页面是不是已经跳走

别嫌麻烦,这些命令能解决九成以上的“找不到元素”。

4.2 Selenium特征被识别,怎么办

有些网站会检测当前浏览器是否被自动化工具控制。如果你发现页面能正常打开,但数据一直加载不出来,或者直接被弹验证码,可能就是特征暴露了。

Selenium 常见的特征有:navigator.webdriver属性为true、浏览器窗口里有“Chrome 正在受到自动测试软件的控制”提示条、一些特定 API 被替换等。前面初始化代码里我加的--disable-blink-features=AutomationControlled就是针对其中一个特征做的处理。

更稳妥的办法是用专门的驱动分支版本,社区里有针对自动化检测做过改写的工具,能隐藏大部分特征。另外设置一个正常的 User-Agent、Window-size、语言环境,也能降低异常概率。

不过这里我想多说一句:学这些技术是为了处理合规的数据采集需求,不是鼓励突破别人的安全防线。在实际项目里,最好先确认目标网站的服务协议允许自动化采集,不要对在线业务系统做超出必要范围的请求。技术能力用在对的方向,才能真正创造价值。

4.3 运行越来越慢,怎么优化

Selenium 慢是通病,但很多“慢”是自己写的代码造成的。我优化过的几个方向,效果都挺明显:

  • 减少无谓等待:把time.sleep全部替换成显式等待,能省一半时间。
  • 关了不必要的动画、图片加载:在 Chrome options 里禁用图片,通过--blink-settings=imagesEnabled=false参数,数据抓取完全不受影响。
  • 尽量用接口请求替代页面渲染:能用 requests 直接拿的数据,就不要开浏览器。
  • 复用浏览器实例:不要每个 URL 都新建一个driver,长时间批量采集时,保持一个实例反复get(),效率远高于频繁启动销毁。
  • 及时清理无用元素:如果页面 DOM 越来越大,操作越来越卡,可以考虑定期刷新页面,而不是在同一个页面里无限滚动。

性能优化的终极原则是:Selenium 只解决“必须用浏览器”的那部分,其他事情都交给 requests 和接口。

4.4 高频异常速查表

最后整理一份速查表,方便你遇到问题时快速对照。这些错误信息是我在实际项目中高频遇到的,每一项后面都给出了可直接操作的解决路径。

异常信息可能原因解决思路
session not createdChromeDriver 与浏览器版本不匹配下载同版本驱动,或升级 Selenium 使用 Selenium Manager
WebDriverException: chrome not reachable浏览器崩溃、路径错误或权限不足检查浏览器安装路径,确认无头模式参数,必要时加--no-sandbox
NoSuchElementException元素未渲染、在 iframe/shadow DOM 中、等待不足按 4.1 的七步排查依次确认
StaleElementReferenceException页面结构变化,之前拿到的元素引用已失效重新定位元素,不要复用旧引用
ElementClickInterceptedException元素被遮罩、弹窗挡住先关闭弹窗,或 JS 强制点击driver.execute_script('arguments[0].click()', element)
TimeoutException等待条件未满足,页面加载过慢适当调长超时时间,同时检查网络和代理设置
InvalidCookieDomainExceptioncookie 域名与当前页面不匹配先访问目标站点,再加对应 cookie
Message: no such element: Unable to locate element定位表达式写错或页面是动态渲染先用 page_source 确认元素真实存在,再修正定位方式

这张表不建议背,建议收藏,等踩到对应坑的时候回来对照。

4.5 长期项目中的取舍经验

这篇文章写到这,核心内容基本都讲透了。最后再分享一点我在长期项目里的体会。

用 Selenium 处理 JavaScript 渲染,最大的价值不是“能爬到数据”,而是“能帮你确认数据到底从哪里来”。很多情况下,我开一次 Selenium,看着它加载页面、发请求、渲染数据,配合抓包工具看看它到底请求了哪些接口、带了哪些参数,然后就把 Selenium 关了,改用 requests 直接调接口。Selenium 在这里更像是侦察兵,而不是主力部队。

如果你真的需要长期跑 Selenium 采集,一定要在脚本里加上异常重试、日志记录和资源清理逻辑。driver.quit()一定要用,不然后台留一堆僵尸进程,内存迟早被吃光。日志方面,至少记录每次请求的 URL、状态、耗时,方便定位问题。

处理 JavaScript 渲染这条路,工具会一直变,今天流行 Selenium,明天可能就换 Playwright 了,但底层的思路是不变的:先搞清楚数据从哪来,再决定用哪一层工具去拿。把这一点想通了,不管你以后换什么工具,都不会慌。

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

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

立即咨询