Python Selenium爬虫代码:怎么获取JavaScript动态渲染页面数据
详细讲解使用Python与Selenium获取JavaScript动态渲染页面数据的完整代码实现、显式等待机制、动态滚动加载处理与生产避坑指南。
一、为什么传统爬虫抓不到:静态请求与 JS 动态渲染的区别
在很多初学者的认知中,爬虫获取数据的逻辑等同于“向目标网址发送 GET 请求,并用正则或选择器提取 HTML”。对于早期的纯服务端渲染网页,这种方式确实行之有效。然而随着单页应用(SPA)与现代前端框架(Vue、React 等)的普及,网页的数据加载机制发生了根本改变。
传统的 requests 仅完成了一次单纯的网络 I/O 操作。服务器返回给 requests 的,只是未经浏览器引擎解析的初始 HTML 文本。在这个初始文本中,往往只存在诸如 <div id="app"></div> 的空容器和若干外部 JavaScript 链接。真正的业务数据,需要浏览器下载并运行这些 JS 脚本,由脚本向后端接口发起异步请求(AJAX/Fetch),再将拿到的 JSON 数据动态转换为 DOM 节点插入页面中。
因为 requests 库不具备任何 JavaScript 执行能力,所以它永远无法触发后续的数据挂载阶段。而 Selenium 是一个浏览器自动化框架,它通过驱动真实的浏览器(如 Google Chrome),拥有完整的 V8 引擎、网络栈与 DOM 渲染树。浏览器能看到什么内容,Selenium 就能抓取到什么内容。
二、环境准备与免配置 WebDriver
在编写代码前,首先需要安装 Python 的 Selenium 库。从 Selenium 4.6.0 开始,官方内置了 Selenium Manager 工具。这意味着读者不再需要像旧版本那样手动查询 Chrome 浏览器版本,再去繁琐地下载对应版本的 chromedriver 并配置环境变量。只要机器上安装有官方 Chrome 浏览器,Selenium 会自动检测并匹配下载驱动。
# 安装 Selenium 核心库(建议 4.6.0 及以上版本) pip install selenium
在正式生产运行中,我们通常无需弹出一个可见的浏览器窗口,而是开启无头模式(Headless Mode)。无头模式让浏览器在后台静默运行,不仅节省了操作系统的图形渲染资源,还能显著提升抓取效率与稳定性。
三、核心步骤与完整代码实现
利用 Selenium 获取动态渲染页面的标准操作流程包含五个关键步骤:配置浏览器参数、发起页面导航、设置显式等待、定位并提取 DOM 节点,以及最后安全关闭驱动。
下面是一份结构完整、带有健壮异常处理和精准显式等待的可运行 Python 示例代码。代码以常见的商品动态列表或新闻列表为场景,展示从页面加载到提取所有动态节点的完整过程:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def scrape_dynamic_page(url: str):
# 1. 配置 Chrome 运行选项
options = Options()
# 启用现代无头模式(Chrome 109+ 推荐新语法)
options.add_argument("--headless=new")
# 禁用 GPU 硬件加速,提升容器及服务器环境运行兼容性
options.add_argument("--disable-gpu")
# 解决部分 Linux 环境下共享内存不足导致崩溃的问题
options.add_argument("--no-sandbox")
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 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
# 2. 初始化 WebDriver 实例(Selenium 4 会自动托管驱动匹配)
driver = webdriver.Chrome(options=options)
try:
print(f"正在导航至目标页面: {url}")
driver.get(url)
# 3. 显式等待:核心防翻车关键
# 设置最大等待超时时间为 10 秒,每 0.5 秒轮询一次 DOM 树
# 等待目标动态列表容器(此处以 class="product-item" 为例)渲染并挂载到 DOM 中
wait = WebDriverWait(driver, timeout=10, poll_frequency=0.5)
target_locator = (By.CSS_SELECTOR, ".product-item")
# 确认至少有一个目标元素已经出现在页面 DOM 中
wait.until(EC.presence_of_element_located(target_locator))
print("JavaScript 渲染完成,目标 DOM 节点已成功挂载!")
# 4. 获取所有动态渲染完毕的节点
elements = driver.find_elements(*target_locator)
results = []
for index, el in enumerate(elements, start=1):
# 在父节点内继续查询子节点信息
try:
title = el.find_element(By.CSS_SELECTOR, ".title").text.strip()
except Exception:
title = "暂无标题"
try:
price = el.find_element(By.CSS_SELECTOR, ".price").text.strip()
except Exception:
price = "暂无价格"
results.append({"index": index, "title": title, "price": price})
print(f"共成功抓取到 {len(results)} 条动态渲染数据。")
return results
except Exception as e:
print(f"抓取过程中发生异常: {e}")
return []
finally:
# 5. 安全销毁驱动实例,杜绝后台僵尸 Chrome 进程
driver.quit()
print("WebDriver 已安全退出。")
if __name__ == "__main__":
# 执行测试示例
target_url = "https://quotes.toscrape.com/js/"
data = scrape_dynamic_page(target_url)
for item in data[:3]:
print(item)
四、核心关键:为什么不能直接用 time.sleep?
在抓取动态页面时,最常见的误区是在 driver.get(url) 之后直接写一句 time.sleep(5)。这种“强制休眠”有两大致命缺陷:
- 极其浪费时间:如果服务器和本地网络状况良好,JavaScript 渲染往往在 0.5 到 1.5 秒内即已完成,剩下几秒完全处于无效停滞状态;在批量抓取成千上万个页面时,累积耗时将十分惊人。
- 极容易偶发报错:如果遇到网络瞬时波动或接口延迟,渲染花了 5.1 秒,强制休眠结束后代码仍会因为找不到元素直接抛出 NoSuchElementException 崩溃。
Selenium 提供了两种内置等待策略:隐式等待(implicitly_wait)与显式等待(WebDriverWait):
- 隐式等待:通过 driver.implicitly_wait(10) 设定全局超时。在查找任何元素时,只要该 HTML 标签出现在 DOM 树中就会立即放行。它的局限在于:现代前端框架很多时候会先生成一个空的占位标签(或骨架屏),数据内容尚未填充进去,隐式等待就会提前放行,导致取出的文本为空。
- 显式等待:通过 WebDriverWait(driver, 10).until(EC.xxx) 针对具体的目标元素定义更严密的预期条件(Expected Conditions)。例如等待元素不仅存在,而且必须满足“可见”、“包含特定文本”或“可点击”。一旦条件满足便在毫秒级内放行,若超时则抛出清晰的 TimeoutException。
| 常用预期条件 (Expected Conditions) | 判定标准 | 适用场景 |
|---|---|---|
| presence_of_element_located | 元素已存在于页面 DOM 树中(不要求可见) | 仅提取隐藏字段、meta 属性或后台挂载节点 |
| visibility_of_element_located | 元素在 DOM 树中存在,且宽和高均大于 0(人眼可见) | 提取正文文字、价格、商品列表等常规界面内容 |
| element_to_be_clickable | 元素可见且处于可交互、未被禁用的状态 | 需要点击“下一页”或“展开更多”按钮时 |
| text_to_be_present_in_element | 目标元素的 innerText 包含特定字符串 | 等待异步计算结果或动态加载百分比完成 |
五、复杂交互场景:滚动懒加载与分页点击
在很多实际网站中,数据并不会在页面初次载入时一次性全部渲染,而是采用了滚动懒加载(Infinite Scroll)机制:只有当用户将滚动条拖动到底部时,浏览器才会触发新的异步请求加载下一批数据卡片。
面对这种机制,可以通过调用 Selenium 的 driver.execute_script() 注入 JavaScript 代码执行窗口滚动,并监测页面总高度的变化来判定是否已经加载全部内容:
import time
def scroll_to_load_all(driver, max_scrolls=10, pause_time=1.5):
"""
通过注入 JavaScript 滚动页面,动态触发懒加载数据
"""
# 获取初始页面滚动总高度
last_height = driver.execute_script("return document.body.scrollHeight")
for scroll_idx in range(max_scrolls):
# 执行滚屏至当前最底部
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# 适当等待新数据包返回并完成 DOM 挂载
time.sleep(pause_time)
# 获取滚屏后的新总高度
new_height = driver.execute_script("return document.body.scrollHeight")
print(f"第 {scroll_idx + 1} 次滚动:旧高度={last_height}, 新高度={new_height}")
# 如果高度没有发生变化,说明已经到达真正的数据底部
if new_height == last_height:
print("已触达页面最底部,懒加载全部结束。")
break
last_height = new_height
如果是通过点击“点击加载更多”按钮加载数据,则应定位该按钮,并在循环中结合 EC.element_to_be_clickable 进行点击,直至按钮消失或变为禁用状态。
六、生产环境避坑与架构选型建议
在实际工程中使用 Selenium 抓取动态数据时,以下几个细节决定了爬虫能否长期稳定运行:
- 防范 StaleElementReferenceException(陈旧元素引用异常): 在动态渲染页面中,如果某个 DOM 节点在 JavaScript 重新渲染时被替换,之前持有的 WebElement 对象就会失效。解决方法是:不要长久持有元素引用;每次循环提取数据时重新查找定位,或者在页面渲染完成后,直接调用 html = driver.page_source 获取包含完整渲染节点的 HTML 字符串,交由 BeautifulSoup 或 lxml 进行快速静态解析。
-
规避 navigator.webdriver 自动化特征检测:
许多现代反爬系统会检查前端对象的 navigator.webdriver 属性。在 ChromeOptions 中可加入参数去除提示:
options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option('useAutomationExtension', False) # 初始化后通过 CDP 隐藏标志 driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": """ Object.defineProperty(navigator, 'webdriver', { get: () => undefined }) """ }) - 性能与架构选型边界: Selenium 的优势是功能完整、真实度高、能轻松应对复杂的客户端加密和状态计算;但由于调起了完整的浏览器内核,其内存消耗与单页耗时均高于轻量级爬虫。在面对百万级的大规模采集需求时,推荐的工程路径是:先打开浏览器的 F12 开发者工具,在 Network(网络)面板中过滤 Fetch/XHR,观察能否直接找到返回 JSON 的真实后端 API 接口。如果接口有难以逆向的复杂加密或动态滑块验证,再果断采用 Selenium 进行稳健的端到端数据采集。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









