爬虫开发者经常会遇到这样一个现象:使用Python的Requests库请求一个网页,返回的HTML源码中就是找不到页面上实际显示的内容。打开浏览器开发者工具查看Elements面板,目标数据明明就在某个<div>节点里,但Requests拿到的响应体却只有一堆空壳标签和脚本引用。这种差异的根源在于前端渲染机制的不同,理解这一点是解决动态内容抓取问题的前提。

一、动态加载的本质:为什么Requests拿不到JS渲染后的数据
传统的Web应用采用服务端渲染,每次请求返回的HTML已经包含了完整的业务数据,浏览器只需要解析并显示。而现代前端框架例如Vue、React、Angular,往往采用客户端渲染模式:服务器只返回一个基础的HTML骨架,其中包含JavaScript文件引用和空的挂载节点。页面加载后,浏览器执行这些脚本,通过Ajax或Fetch向后端API发起异步请求,获取JSON数据,再动态生成DOM节点填充到页面中。整个过程发生在客户端,Requests只能看到第一步服务端返回的原始HTML,自然不会包含后续JavaScript生成的内容。
举个例子,假设目标页面有一个商品列表,数据来自接口/api/products。直接用Requests请求页面时,响应体中的列表容器可能是这样的:
<div id="product-list"></div> <script src="/static/app.js"></script>
而浏览器实际渲染后,<div id="product-list">内部会被填入大量商品节点。Requests因为没有执行JavaScript的能力,所以永远只能拿到那个空容器。要判断一个页面是否属于动态加载,可以对比查看网页源代码和检查元素的结果,如果两者内容差异很大,基本可以确定数据是通过JavaScript异步获取的。
理解了原理之后,解决的思路就很清晰了:要么模拟浏览器去执行JavaScript拿到渲染后的DOM,要么绕过JavaScript直接找到它调用的数据接口。下面分别介绍这两类策略的具体实现。
二、策略一:定位数据接口,用Requests模拟Ajax请求
既然页面数据是通过Ajax请求获取的,那么最直接高效的方式就是找到这些接口,用Requests直接请求接口地址。打开浏览器的开发者工具,切换到Network面板,刷新页面,筛选XHR或Fetch类型的请求。这些请求通常返回JSON或XML格式的数据,URL中往往带有api、ajax、query等关键词。点击某个请求可以查看完整的请求头、请求参数和响应内容。如果响应数据里包含了页面上的目标信息,就可以用Requests重放这个请求。
以下是一段模拟Ajax请求获取商品列表的示例代码:
import requests
url = "https://ippipp.com/api/products"
params = {
"page": 1,
"pageSize": 20,
"keyword": "laptop"
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"X-Requested-With": "XMLHttpRequest",
"Referer": "https://ippipp.com/products"
}
resp = requests.get(url, params=params, headers=headers)
data = resp.json()
for product in data["list"]:
print(product["name"], product["price"])
这种方式的最大优势是速度快、资源消耗极低,不用启动浏览器,也不涉及DOM解析,直接处理结构化数据即可。但它的难点在于接口可能带有签名、加密参数、时间戳、token等验证机制。很多网站会对接口请求做防护,比如要求携带特定的cookie、在请求头中加入动态生成的签名、或者对参数进行加密。此时就需要分析JavaScript源码,找到加密逻辑并用Python复现。虽然逆向分析有一定门槛,但一旦破解成功,抓取效率会非常高。
另外要注意请求频率和礼貌性。接口直接暴露数据,往往也有反爬限制,比如IP频率限制、验证码等。适当控制请求间隔、使用代理池、模拟真实请求头都是必要的辅助手段。对于需要登录才能访问的接口,还要先模拟登录获取会话cookie,再带着cookie去请求数据接口。
三、策略二:使用渲染引擎执行JavaScript
如果数据接口的逆向成本太高,或者页面逻辑极其复杂,用渲染引擎执行JavaScript是更省心的方案。Selenium、Playwright、Pyppeteer等工具可以驱动真实的浏览器或模拟浏览器环境,完整执行页面中的脚本,等待动态内容加载完成后再提取DOM。这类工具本质上是用浏览器来渲染页面,所以几乎能应对所有动态渲染场景,尤其适合处理需要复杂交互、登录态维持、滚动加载、点击翻页等操作的目标网站。
以Playwright为例,下面的代码展示了如何等待商品列表加载完成并提取文本:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://ippipp.com/products")
# 等待商品列表容器出现
page.wait_for_selector("#product-list .item", timeout=10000)
items = page.query_selector_all("#product-list .item")
for item in items:
name = item.query_selector(".name").inner_text()
price = item.query_selector(".price").inner_text()
print(name, price)
browser.close()
渲染引擎的优点是省去了逆向接口的麻烦,编写脚本的过程更接近人工操作浏览器的流程,适合快速验证和低频抓取。但缺点也很明显:启动浏览器会占用大量内存和CPU资源,抓取速度远慢于直接请求接口;同时浏览器特征容易被反爬系统识别,比如WebDriver属性、自动化控制标记等,可能触发验证码或封禁。使用这些工具时,通常需要配合隐藏自动化特征、设置随机UA、使用代理等方式降低被检测的概率。
在Selenium和Playwright之间,Playwright的API更现代,支持异步操作,等待条件更丰富,性能也更好一些。Selenium生态更成熟,文档和社区资源多,对旧项目兼容性好。Pyppeteer则是Puppeteer的Python移植版,功能完整但维护更新不如前两者活跃。如果只是偶尔抓取少量动态页面,三者差别不大;如果需要大规模采集,建议优先考虑Playwright。
四、策略三:轻量级渲染方案requests-html与混合策略
对于不想深入学习Selenium或Playwright的开发者,requests-html是一个折中选择。它是Requests作者Kenneth Reitz开发的一个扩展库,在Requests的基础上集成了HTML解析和JavaScript渲染能力。requests-html内部使用Pyppeteer驱动无头浏览器,因此可以执行页面脚本,同时保留了Requests简洁的API风格。使用示例如下:
from requests_html import HTMLSession
session = HTMLSession()
resp = session.get("https://ippipp.com/products")
# 触发JavaScript渲染,等待2秒,还可以传入scrolldown参数模拟滚动
resp.html.render(timeout=20, sleep=2)
items = resp.html.find("#product-list .item")
for item in items:
name = item.find(".name", first=True).text
price = item.find(".price", first=True).text
print(name, price)
requests-html的优势是上手简单,对于JavaScript渲染需求不复杂的页面非常高效。但它的渲染功能依赖本地安装的Chromium,首次使用需要下载,而且底层Pyppeteer的稳定性一般,遇到复杂的单页应用或大量异步请求时容易出现超时或渲染不完整的问题。因此它更适合作为轻量级场景的补充工具,而不是大规模采集的主力方案。
实际工程中,往往需要混合使用多种策略。例如先尝试分析Network面板,如果能找到干净的JSON接口就直接用Requests抓取;如果接口参数加密严重,再考虑Playwright渲染;对于只需要简单渲染就能出数据的页面,requests-html可以作为快速验证的手段。同时可以结合缓存机制、失败重试、请求调度等工程手段,提高整个抓取流程的健壮性。
五、实战建议与避坑指南
在处理JavaScript动态加载内容时,有几个常见的误区需要避免。一是盲目使用无头浏览器,忽略了更高效的接口方案。很多开发者一看到页面有动态数据就上Selenium,结果抓取速度极慢,资源消耗巨大。实际上很多动态页面的接口都是公开的,只要细心观察Network面板就能找到,用Requests直接请求可以节省几十倍的资源和时间。二是对渲染工具的超时设置不合理。无头浏览器执行JavaScript需要时间,页面加载慢时如果等待时间不够,就会拿到不完整的数据。建议使用显式等待条件,例如等待目标元素出现或网络空闲,而不是写死固定的sleep秒数。三是忽视了反爬检测。无头浏览器默认的WebDriver特征很容易被识别,如果不做指纹隐藏,很快就会被封IP或弹验证码。可以借助stealth.js等工具隐藏自动化痕迹,或者使用商业化的无头浏览器服务。
另外,无论采用哪种策略,都应该遵守目标网站的robots协议和服务条款,合理控制请求频率,避免对网站服务器造成压力。如果是大规模数据采集,建议优先寻找官方提供的API或数据接口,这样既稳定又合规。对于需要登录的站点,务必妥善保管账号和token信息,不要进行恶意抓取。
最后做一个简单总结:Requests适合抓取服务端渲染的静态页面;动态加载的页面优先尝试分析Ajax接口并用Requests模拟;接口逆向困难时使用Playwright等渲染引擎执行JavaScript;轻量需求可以考虑requests-html。技术选型没有绝对的优劣,取决于目标网站的技术栈、反爬强度以及你对抓取效率和开发成本的权衡。理解每种方案背后的原理,才能在遇到不同类型的动态页面时快速做出正确的选择。
Python RequestsJavaScript动态加载爬虫修改时间:2026-08-20 08:17:17