
NerdWallet的CD利率页面采用前端动态渲染,HTML源码中不包含目标数据;直接使用BeautifulSoup解析无法获取APY信息,需调用其后台GraphQL/REST API接口并构造合法请求参数才能成功提取。
nerdwallet的cd利率页面采用前端动态渲染,html源码中不包含目标数据;直接使用beautifulsoup解析无法获取apy信息,需调用其后台graphql/rest api接口并构造合法请求参数才能成功提取。
现代金融比价类网站(如NerdWallet)普遍采用服务端渲染(SSR)+ 客户端数据懒加载架构:用户看到的APY、银行名称等核心内容并非静态写入HTML,而是由JavaScript在浏览器中发起异步请求(通常是POST到/api/marketplace/offers/v1/banking/search),再动态注入DOM。因此,当你用requests + BeautifulSoup仅抓取初始HTML时,这类用于展示APY的元素根本不存在于原始响应中——这正是你得到空列表的根本原因。
✅ 正确方案:直连后端API
NerdWallet提供了结构化、分页、可过滤的公开API接口。我们无需模拟浏览器或使用Selenium,只需:
- 构造符合要求的JSON请求体(含字段筛选、分页、多条件查询);
- 设置必要请求头(尤其是 X-Caller-Client-ID: offer-shopping-experience);
- 解析返回的JSON响应,精准提取银行名与APY字段。
以下为精简可靠的实现代码(已去除冗余字段,保留关键逻辑):
import requests
import json
api_url = "https://www.nerdwallet.com/api/marketplace/offers/v1/banking/search"
payload = {
"fields": [
"offer.product.institution.details.display_name",
"offer.details.apy",
"offer.details.cd_deposit_term"
],
"pagination": {"limit": 50, "offset": 0},
"query": [
{"field": "offer.details.display_status", "type": "eq", "value": "ACTIVE"},
{"field": "offer.product.details.display_status", "type": "eq", "value": "ACTIVE"},
{"field": "offer.product.institution.details.display_status", "type": "eq", "value": "ACTIVE"},
{"field": "offer.details.display_pages", "type": "in", "value": ["TOOLS"]},
{"field": "offer.product.details.sub_product_type", "type": "in", "value": ["CD"]},
{"field": "offer.product.institution.details.type", "type": "in", "value": ["BK", "IB", "CU", "NBFSP"]}
],
"sort": [{"field": "offer.details.apy", "order": "desc"}]
}
headers = {
"X-Caller-Client-ID": "offer-shopping-experience",
"Content-Type": "application/json"
}
response = requests.post(api_url, json=payload, headers=headers)
response.raise_for_status() # 自动抛出HTTP错误异常
data = response.json()
print(f"{'银行名称':<30} {'APY (%)':<10} {'存期(月)':<8}")
print("-" * 50)
for offer in data.get("data", {}).get("offers", []):
inst = offer.get("product", {}).get("institution", {}).get("details", {})
details = offer.get("details", {})
bank_name = inst.get("display_name", {}).get("value", "N/A")
apy = details.get("apy", {}).get("value", "N/A")
term_months = details.get("cd_deposit_term", {}).get("value", "N/A")
print(f"{bank_name:<30} {apy:<10} {term_months:<8}")⚠️ 注意事项与最佳实践
- 分页处理:pagination.offset 需配合循环调用以获取全部结果(示例中仅取前50条);
- 字段健壮性:API返回结构嵌套较深,务必使用 .get() 链式访问并提供默认值,避免KeyError;
- 反爬策略:当前接口未强制校验User-Agent或Cookie,但X-Caller-Client-ID为必需头,缺失将返回403;
- 数据时效性:API数据与前端一致,实时更新,无需额外缓存管理;
- 合规提醒:请遵守NerdWallet robots.txt及《服务条款》,建议添加合理请求间隔(如time.sleep(1)),避免高频调用。
通过直连API而非解析渲染后DOM,你不仅能稳定获取结构化CD利率数据,还能灵活扩展筛选维度(如按州、最低起存额、存期等),大幅提升抓取效率与维护性。

















