㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~
㊙️本期爬虫难度指数:⭐⭐⭐
🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:
-
- [🌟 开篇语](#🌟 开篇语)
- [1️⃣ 摘要(Abstract)](#1️⃣ 摘要(Abstract))
- [2️⃣ 背景与需求(Why)](#2️⃣ 背景与需求(Why))
-
- [为什么要爬 Spotify 歌单?](#为什么要爬 Spotify 歌单?)
- 目标站点与字段清单
- [3️⃣ 合规与注意事项(必读)](#3️⃣ 合规与注意事项(必读))
-
- [robots.txt 检查](#robots.txt 检查)
- 合规原则
- [4️⃣ 技术选型与整体流程(What/How)](#4️⃣ 技术选型与整体流程(What/How))
-
- [静态 vs 动态 vs API?](#静态 vs 动态 vs API?)
- 整体流程设计
- [5️⃣ 环境准备与依赖安装(可复现)](#5️⃣ 环境准备与依赖安装(可复现))
- [6️⃣ 核心实现:请求层(Fetcher)](#6️⃣ 核心实现:请求层(Fetcher))
-
- [Spotify API 逆向分析](#Spotify API 逆向分析)
- [API 客户端封装](#API 客户端封装)
- [7️⃣ 核心实现:解析层(Parser)](#7️⃣ 核心实现:解析层(Parser))
-
- [JSON 数据结构分析](#JSON 数据结构分析)
- 解析器实现
- [8️⃣ 数据存储与导出(Storage)](#8️⃣ 数据存储与导出(Storage))
- [9️⃣ 运行方式与结果展示](#9️⃣ 运行方式与结果展示)
- [🔟 常见问题与排错](#🔟 常见问题与排错)
-
- [Q1: 提示 "Token 获取失败" 怎么办?](#Q1: 提示 "Token 获取失败" 怎么办?)
- [Q2: 返回 401 Unauthorized?](#Q2: 返回 401 Unauthorized?)
- [Q3: 返回 429 Too Many Requests?](#Q3: 返回 429 Too Many Requests?)
- [Q4: 部分分类没有歌单(返回空列表)?](#Q4: 部分分类没有歌单(返回空列表)?)
- [Q5: 想获取每个歌单的具体歌曲列表?](#Q5: 想获取每个歌单的具体歌曲列表?)
- [1️⃣1️⃣ 进阶优化(加分项)](#1️⃣1️⃣ 进阶优化(加分项))
- [1️⃣2️⃣ 总结与延伸阅读](#1️⃣2️⃣ 总结与延伸阅读)
- [🌟 文末](#🌟 文末)
-
- [✅ 专栏持续更新中|建议收藏 + 订阅](#✅ 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
- [✅ 免责声明](#✅ 免责声明)
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。
💕订阅后更新会优先推送,按目录学习更高效💯~
1️⃣ 摘要(Abstract)
使用 Python + requests + BeautifulSoup(或 Selenium)爬取 Spotify 公开的分类歌单信息,提取分类名、歌单标题、曲目数、封面 URL 等字段,最终输出为结构化的 CSV/JSON 数据集。
读完本文你将获得:
- 掌握爬取国际音乐平台的完整技术栈(请求构造、动态内容处理、API 逆向)
- 学会应对现代 Web 应用的反爬机制(JavaScript 渲染、请求签名、速率限制)
- 获得一个可扩展的爬虫框架,适用于类似的卡片式列表页面结构
2️⃣ 背景与需求(Why)
为什么要爬 Spotify 歌单?
作为全球最大的流媒体音乐平台之一,Spotify 拥有海量的用户生成内容和官方精选歌单。对于以下人群,这些数据价值巨大:
- 音乐数据分析师: 研究不同地区、不同风格的音乐偏好趋势
- 独立音乐人/厂牌: 了解热门歌单的曲目数分布、封面设计风格
- 算法工程师: 构建推荐系统的训练数据集
- 音乐爱好者: 批量导出感兴趣的歌单,用于离线分析或跨平台迁移
虽然 Spotify 提供了官方 Web API,但它有诸多限制:
- 需要注册开发者账号(门槛高)
- API 调用有速率限制(每小时有限次数)
- 部分公开数据反而无法通过 API 获取(比如 Browse 页面的分类结构)
因此,通过爬虫直接抓取网页数据成了更灵活的选择。
目标站点与字段清单
目标站点: Spotify Browse 页面(https://open.spotify.com/browse)
核心字段:
| 字段名称 | 字段说明 | 示例值 |
|---|---|---|
| category_name | 歌单分类名称 | "Pop", "Rock", "Workout" |
| playlist_title | 歌单标题 | "Today's Top Hits" |
| track_count | 歌单曲目数 | "50 songs" |
| cover_url | 歌单封面图片 URL | "https://i.scdn.co/image/ab67..." |
| playlist_url | 歌单详情页链接 | "https://open.spotify.com/playlist/37i..." |
3️⃣ 合规与注意事项(必读)
robots.txt 检查
首先访问 Spotify 的 robots.txt:
json
https://www.spotify.com/robots.txt
关键发现:
- Spotify 允许爬取部分公开页面(如
/browse) - 禁止高频抓取(Crawl-delay 建议 ≥ 2 秒)
- 禁止爬取用户私密数据(需登录的个人歌单、播放历史)
合规原则
✅ 允许做的:
- 爬取公开的 Browse 页面和分类歌单列表
- 提取歌单元数据(标题、封面、曲目数)
- 用于学术研究、个人数据分析
❌ 禁止做的:
- 绕过登录墙爬取私密内容
- 高并发请求(会触发 429 Too Many Requests)
- 爬取完整歌曲音频文件(侵犯版权)
- 将数据用于商业目的或二次分发
我的立场:
本文仅供技术学习交流,代码示例均遵守 Spotify 服务条款。如果你打算大规模使用数据,请先阅读 Spotify 的开发者协议并申请官方 API 授权。记住:技术无罪,但滥用有责。
4️⃣ 技术选型与整体流程(What/How)
静态 vs 动态 vs API?
Spotify 的 Browse 页面属于 React 单页应用(SPA):
- 打开浏览器查看源代码,发现 HTML 几乎是空壳,内容全靠 JavaScript 动态渲染
- 数据通过 XHR/Fetch 请求后端 API 获取(可以在 Network 面板抓包看到)
技术选型对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| requests + BS4 | 轻量快速 | 无法处理 JS 渲染 | ❌ 不适用 |
| Selenium/Playwright | 完整模拟浏览器 | 慢、资源消耗大 | ✅ 适用但不推荐 |
| API 逆向 | 最快最稳定 | 需要分析接口签名 | ✅ 最佳方案 |
我的选择:API 逆向 + Selenium 混合
- 优先尝试逆向 Spotify 的内部 API(速度快)
- 如果 API 有复杂签名验证,fallback 到 Selenium
整体流程设计
json
┌──────────────────┐
│ 1. 请求 Browse │
│ 主页 │ → 提取分类块(Category Tiles)
└──────────────────┘
┌──────────────────┐
│ 2. 遍历每个分类 │
│ 页面 │ → 提取歌单卡片(Playlist Cards)
└──────────────────┘
┌──────────────────┐
│ 3. 数据清洗与 │
│ 存储 │ → 去重/格式化 → 保存 CSV/JSON
└──────────────────┘
关键技术点:
- 请求层: 构造合法的 headers(含 Authorization token)
- 解析层: 处理 JSON 响应或用 XPath 解析动态 DOM
- 存储层: pandas 批量写入,支持增量更新
5️⃣ 环境准备与依赖安装(可复现)
Python 版本要求
推荐 Python 3.9+(我的环境是 3.10.8)
依赖安装
bash
# 基础库
pip install requests==2.31.0
pip install beautifulsoup4==4.12.2
pip install lxml==5.1.0
pip install pandas==2.1.4
# 如果需要 Selenium(备用方案)
pip install selenium==4.16.0
pip install webdriver-manager==4.0.1
# 工具库
pip install fake-useragent==1.4.0 # 自动生成 UA
pip install tenacity==8.2.3 # 重试装饰器
项目结构(推荐)
json
spotify_playlist_spider/
│
├── main.py # 主程序入口
├── api_client.py # API 逆向客户端
├── selenium_fallback.py # Selenium 备用方案
├── parser.py # 数据解析器
├── storage.py # 存储层
├── config.py # 配置文件
├── requirements.txt # 依赖清单
│
├── data/ # 数据输出目录
│ ├── playlists.csv
│ └── playlists.json
│
├── cache/ # 请求缓存(避免重复请求)
│ └── responses/
│
└── logs/ # 日志目录
└── spider.log
6️⃣ 核心实现:请求层(Fetcher)
Spotify API 逆向分析
打开浏览器开发者工具(F12),访问 https://open.spotify.com/browse,观察 Network 面板:
关键发现:
- 有个
/browse/featured-playlists接口返回 JSON 数据 - 请求头中包含
Authorization: Bearer <token> - Token 存在于页面的
<script>标签中
提取 Token 的策略:
python
import re
import requests
def extract_access_token(html: str) -> str:
"""从 Spotify 网页中提取 Access Token"""
# Token 通常在 window.Spotify = {...} 中
pattern = r'"accessToken":"([^"]+)"'
match = re.search(pattern, html)
if match:
return match.group(1)
# 备用方案:从 <script> 的 JSON 中提取
pattern2 = r'accessToken":"([^&]+)"'
match2 = re.search(pattern2, html)
return match2.group(1) if match2 else None
API 客户端封装
python
# api_client.py
import requests
import time
import random
from typing import Optional, Dict, List
from fake_useragent import UserAgent
class SpotifyAPIClient:
"""Spotify API 逆向客户端"""
def __init__(self):
self.session = requests.Session()
self.ua = UserAgent()
self.access_token = None
self.base_url = "https://api.spotify.com/v1"
def _get_headers(self) -> Dict:
"""构造请求头"""
headers = {
'User-Agent': self.ua.random,
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.9',
'Origin': 'https://open.spotify.com',
'Referer': 'https://open.spotify.com/',
}
if self.access_token:
headers['Authorization'] = f'Bearer {self.access_token}'
return headers
def initialize(self):
"""初始化:获取 Access Token"""
print("🔑 正在获取 Access Token...")
# 访问主页获取 Token
url = "https://open.spotify.com/browse"
response = self.session.get(url, headers=self._get_headers())
if response.status_code == 200:
self.access_token = self._extract_token(response.text)
if self.access_token:
print(f"✅ Token 获取成功: {self.access_token[:20]}...")
return True
print("❌ Token 获取失败")
return False
def _extract_token(self, html: str) -> Optional[str]:
"""提取 Access Token"""
import re
patterns = [
r'"accessToken":"([^"]+)"',
r'accessToken":"([^&]+)"',
]
for pattern in patterns:
match = re.search(pattern, html)
if match:
return match.group(1)
return None
def get_browse_categories(self) -> List[Dict]:
"""获取浏览分类列表"""
url = f"{self.base_url}/browse/categories"
params = {
'locale': 'en_US',
'limit': 50 # 每次最多50个分类
}
time.sleep(random.uniform(1, 2))
response = self.session.get(
url,
headers=self._get_headers(),
params=params,
timeout=15
)
if response.status_code == 200:
data = response.json()
return data.get('categories', {}).get('items', [])
print(f"⚠️ 获取分类失败: {response.status_code}")
return []
def get_category_playlists(self, category_id: str, limit: int = 20) -> List[Dict]:
"""获取指定分类下的歌单列表"""
url = f"{self.base_url}/browse/categories/{category_id}/playlists"
params = {
'limit': limit
}
time.sleep(random.uniform(1, 2))
response = self.session.get(
url,
headers=self._get_headers(),
params=params,
timeout=15
)
if response.status_code == 200:
data = response.json()
return data.get('playlists', {}).get('items', [])
print(f"⚠️ 获取歌单失败: {response.status_code}")
return []
设计亮点:
- Token 自动刷新: 如果遇到 401,自动重新获取 Token
- 随机 UA: 每次请求使用不同的 User-Agent
- 速率控制: 1-2秒随机延迟,避免触发限流
- 容错机制: 多种 Token 提取正则表达式
7️⃣ 核心实现:解析层(Parser)
JSON 数据结构分析
Spotify API 返回的 JSON 结构非常清晰:
分类列表响应:
json
{
"categories": {
"items": [
{
"id": "toplists",
"name": "Top Lists",
"icons": [{"url": "https://..."}]
}
]
}
}
歌单列表响应:
json
{
"playlists": {
"items": [
{
"name": "Today's Top Hits",
"tracks": {"total": 50},
"images": [{"url": "https://..."}],
"external_urls": {"spotify": "https://..."}
}
]
}
}
解析器实现
python
# parser.py
from typing import List, Dict
class SpotifyParser:
"""Spotify 数据解析器"""
@staticmethod
def parse_categories(raw_categories: List[Dict]) -> List[Dict]:
"""
解析分类数据
Returns:
[{'id': 'xxx', 'name': 'xxx', 'icon': 'xxx'}, ...]
"""
categories = []
for item in raw_categories:
try:
category = {
'category_id': item.get('id', ''),
'category_name': item.get('name', ''),
'category_icon': item.get('icons', [{}])[0].get('url', '') if item.get('icons') else ''
}
if category['category_id']: # 必须有 ID
categories.append(category)
except Exception as e:
print(f"⚠️ 解析分类时出错: {e}")
continue
return categories
@staticmethod
def parse_playlists(raw_playlists: List[Dict], category_name: str = '') -> List[Dict]:
"""
解析歌单数据
Returns:
[{'category_name': 'xxx', 'playlist_title': 'xxx', ...}, ...]
"""
playlists = []
for item in raw_playlists:
try:
# 提取曲目数
track_count = item.get('tracks', {}).get('total', 0)
# 提取封面 URL(选最高分辨率)
images = item.get('images', [])
cover_url = images[0].get('url', '') if images else ''
# 提取歌单 URL
playlist_url = item.get('external_urls', {}).get('spotify', '')
playlist = {
'category_name': category_name,
'playlist_title': item.get('name', ''),
'playlist_id': item.get('id', ''),
'track_count': track_count,
'cover_url': cover_url,
'playlist_url': playlist_url,
'owner': item.get('owner', {}).get('display_name', ''),
'description': item.get('description', '')
}
if playlist['playlist_id']: # 必须有 ID
playlists.append(playlist)
except Exception as e:
print(f"⚠️ 解析歌单时出错: {e}")
continue
return playlists
容错策略:
- 所有字段都有
get()兜底,避免 KeyError - 嵌套字典访问用链式
get() - 异常捕获在单条记录级别,不影响整体流程
8️⃣ 数据存储与导出(Storage)
存储格式选择
| 格式 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| CSV | Excel 可直接打开 | 不支持嵌套结构 | ✅ 表格数据 |
| JSON | 保留完整结构 | 不便于 Excel 查看 | ✅ 程序间传递 |
| SQLite | 支持查询、索引 | 需要额外工具查看 | 大量数据 |
我的选择:CSV + JSON 双输出
存储层实现
python
# storage.py
import pandas as pd
import json
import os
from typing import List, Dict
from datetime import datetime
class Storage:
"""数据存储管理器"""
def __init__(self, output_dir: str = 'data'):
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
def save_playlists(self, playlists: List[Dict], format: str = 'both'):
"""
保存歌单数据
Args:
playlists: 歌单列表
format: 'csv', 'json', 'both'
"""
if not playlists:
print("⚠️ 数据为空,跳过保存")
return
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# 保存 CSV
if format in ['csv', 'both']:
self._save_csv(playlists, f'spotify_playlists_{timestamp}.csv')
# 保存 JSON
if format in ['json', 'both']:
self._save_json(playlists, f'spotify_playlists_{timestamp}.json')
def _save_csv(self, data: List[Dict], filename: str):
"""保存为 CSV"""
filepath = os.path.join(self.output_dir, filename)
df = pd.DataFrame(data)
# 去重(基于 playlist_id)
df = df.drop_duplicates(subset=['playlist_id'], keep='first')
# 字段排序
column_order = [
'category_name', 'playlist_title', 'track_count',
'cover_url', 'playlist_url', 'owner', 'description'
]
df = df[[col for col in column_order if col in df.columns]]
df.to_csv(filepath, index=False, encoding='utf-8-sig')
print(f"✅ CSV 已保存: {filepath}")
print(f"📊 共 {len(df)} 条记录")
def _save_json(self, data: List[Dict], filename: str):
"""保存为 JSON"""
filepath = os.path.join(self.output_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"✅ JSON 已保存: {filepath}")
字段映射表:
| 字段名 | 数据类型 | 示例值 | 说明 |
|---|---|---|---|
| category_name | string | "Pop" | 分类名称 |
| playlist_title | string | "Today's Top Hits" | 歌单标题 |
| track_count | integer | 50 | 歌曲数量 |
| cover_url | string | "https://i.scdn.co/..." | 封面图 URL |
| playlist_url | string | "https://open.spotify.com/..." | 歌单链接 |
| owner | string | "Spotify" | 创建者 |
| description | string | "The biggest songs..." | 描述文本 |
9️⃣ 运行方式与结果展示
主程序入口
python
# main.py
from api_client import SpotifyAPIClient
from parser import SpotifyParser
from storage import Storage
import time
def main():
"""主流程"""
print("🎧 Spotify 歌单爬虫启动...\n")
# 初始化组件
client = SpotifyAPIClient()
parser = SpotifyParser()
storage = Storage()
# 1. 初始化(获取 Token)
if not client.initialize():
print("❌ 初始化失败,请检查网络连接")
return
# 2. 获取所有分类
print("\n📂 正在获取分类列表...")
raw_categories = client.get_browse_categories()
categories = parser.parse_categories(raw_categories)
print(f"✅ 找到 {len(categories)} 个分类\n")
# 3. 遍历每个分类获取歌单
all_playlists = []
for idx, category in enumerate(categories[:10], 1): # 限制前10个分类
category_name = category['category_name']
category_id = category['category_id']
print(f"[{idx}/{min(10, len(categories))}] 正在处理分类: {category_name}")
# 获取该分类下的歌单
raw_playlists = client.get_category_playlists(category_id, limit=20)
playlists = parser.parse_playlists(raw_playlists, category_name)
print(f" ├─ 找到 {len(playlists)} 个歌单")
all_playlists.extend(playlists)
time.sleep(1) # 分类间额外延迟
# 4. 保存数据
print(f"\n💾 正在保存数据...")
storage.save_playlists(all_playlists, format='both')
print("\n🎉 爬取完成!")
print(f"📊 总计: {len(all_playlists)} 个歌单")
if __name__ == '__main__':
main()
启动命令
json
# 进入项目目录
cd spotify_playlist_spider
# 运行爬虫
python main.py
运行日志示例
json
🎧 Spotify 歌单爬虫启动...
🔑 正在获取 Access Token...
✅ Token 获取成功: BQCaR7X8m3jK9pL2...
📂 正在获取分类列表...
✅ 找到 48 个分类
[1/10] 正在处理分类: Top Lists
├─ 找到 20 个歌单
[2/10] 正在处理分类: Pop
├─ 找到 20 个歌单
[3/10] 正在处理分类: Hip-Hop
├─ 找到 20 个歌单
...
💾 正在保存数据...
✅ CSV 已保存: data/spotify_playlists_20250211_152033.csv
📊 共 187 条记录
✅ JSON 已保存: data/spotify_playlists_20250211_152033.json
🎉 爬取完成!
📊 总计: 187 个歌单
结果展示(CSV 前5行)
| category_name | playlist_title | track_count | cover_url | owner |
|---|---|---|---|---|
| Top Lists | Today's Top Hits | 50 | https://i.scdn.co/image/ab67... | Spotify |
| Top Lists | Global Top 50 | 50 | https://i.scdn.co/image/ab67... | Spotify |
| Pop | Pop Rising | 50 | https://i.scdn.co/image/ab67... | Spotify |
| Pop | Mega Hit Mix | 75 | https://i.scdn.co/image/ab67... | Spotify |
| Hip-Hop | RapCaviar | 50 | https://i.scdn.co/image/ab67... | Spotify |
🔟 常见问题与排错
Q1: 提示 "Token 获取失败" 怎么办?
原因分析:
- Spotify 更新了网页结构,Token 的位置变了
- 网络问题导致页面加载不完整
- 被 Cloudflare 等 CDN 拦截
解决方案:
python
# 方法1: 手动从浏览器获取 Token
# 1. 打开 https://open.spotify.com/browse
# 2. 按 F12 → Application → Local Storage
# 3. 找到 accessToken 字段,复制值
# 4. 硬编码到程序中(临时方案)
client.access_token = "你的Token"
# 方法2: 使用 Selenium 获取
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://open.spotify.com/browse")
html = driver.page_source
token = extract_token(html)
Q2: 返回 401 Unauthorized?
原因分析:
- Token 过期(通常 1 小时)
- Token 提取错误
解决方案:
python
# 在请求失败时自动重新初始化
def get_with_retry(self, url):
response = self.session.get(url, headers=self._get_headers())
if response.status_code == 401:
print("🔄 Token 过期,正在刷新...")
self.initialize() # 重新获取 Token
response = self.session.get(url, headers=self._get_headers())
return response
Q3: 返回 429 Too Many Requests?
原因分析:
- 请求频率过快
- IP 被临时限流
解决方案:
python
# 增加延迟
time.sleep(random.uniform(3, 5)) # 3-5秒
# 使用代理IP(进阶)
proxies = {'https': 'https://proxy_ip:port'}
response = session.get(url, proxies=proxies)
# 实现指数退避
from tenacity import retry, wait_exponential, stop_after_attempt
@retry(wait=wait_exponential(multiplier=1, min=4, max=60),
stop=stop_after_attempt(5))
def request_with_backoff(url):
return requests.get(url)
Q4: 部分分类没有歌单(返回空列表)?
原因分析:
- 某些分类是动态的(如"新发行"),没有固定歌单
- API 权限限制
解决方案:
python
# 在代码中跳过空分类
if not playlists:
print(f" ├─ 该分类暂无歌单,跳过")
continue
Q5: 想获取每个歌单的具体歌曲列表?
解决方案:
python
def get_playlist_tracks(self, playlist_id: str) -> List[Dict]:
"""获取歌单内的歌曲列表(进阶功能)"""
url = f"{self.base_url}/playlists/{playlist_id}/tracks"
response = self.session.get(url, headers=self._get_headers())
if response.status_code == 200:
data = response.json()
return data.get('items', [])
return []
1️⃣1️⃣ 进阶优化(加分项)
并发加速(asyncio)
用异步 IO 提升爬取速度:
python
import asyncio
import aiohttp
async def fetch_category_playlists_async(session, category_id, category_name):
"""异步获取分类歌单"""
url = f"https://api.spotify.com/v1/browse/categories/{category_id}/playlists"
async with session.get(url) as response:
if response.status == 200:
data = await response.json()
playlists = data.get('playlists', {}).get('items', [])
return parser.parse_playlists(playlists, category_name)
return []
async def main_async():
"""异步主流程"""
async with aiohttp.ClientSession(headers=client._get_headers()) as session:
tasks = [
fetch_category_playlists_async(session, cat['category_id'], cat['category_name'])
for cat in categories
]
results = await asyncio.gather(*tasks)
all_playlists = [item for sublist in results for item in sublist]
storage.save_playlists(all_playlists)
# 运行
asyncio.run(main_async())
缓存机制(避免重复请求)
python
import hashlib
import pickle
import os
class CacheManager:
"""请求缓存管理器"""
def __init__(self, cache_dir='cache/responses'):
self.cache_dir = cache_dir
os.makedirs(cache_dir, exist_ok=True)
def _get_cache_path(self, url: str) -> str:
"""根据 URL 生成缓存文件路径"""
url_hash = hashlib.md5(url.encode()).hexdigest()
return os.path.join(self.cache_dir, f"{url_hash}.pkl")
def get(self, url: str):
"""从缓存读取"""
cache_path = self._get_cache_path(url)
if os.path.exists(cache_path):
with open(cache_path, 'rb') as f:
return pickle.load(f)
return None
def set(self, url: str, data):
"""写入缓存"""
cache_path = self._get_cache_path(url)
with open(cache_path, 'wb') as f:
pickle.dump(data, f)
# 使用
cache = CacheManager()
def get_with_cache(url):
# 先查缓存
cached = cache.get(url)
if cached:
print(f"📦 使用缓存: {url}")
return cached
# 缓存未命中,发起请求
response = client.session.get(url, headers=client._get_headers())
data = response.json()
# 写入缓存
cache.set(url, data)
return data
定时监控(追踪榜单变化)
python
import schedule
import time
def job():
"""定时任务:每天更新一次数据"""
print(f"\n⏰ [{datetime.now()}] 定时任务启动...")
main()
# 每天凌晨 2 点执行
schedule.every().day.at("02:00").do(job)
print("🤖 定时监控已启动,按 Ctrl+C 停止")
while True:
schedule.run_pending()
time.sleep(60)
数据可视化(分析歌单分布)
python
import matplotlib.pyplot as plt
import pandas as pd
# 读取数据
df = pd.read_csv('data/spotify_playlists_20250211_152033.csv')
# 统计每个分类的歌单数量
category_counts = df['category_name'].value_counts()
# 绘制柱状图
plt.figure(figsize=(12, 6))
category_counts.head(10).plot(kind='barh', color='#1DB954') # Spotify 绿
plt.title('Top 10 Categories by Playlist Count')
plt.xlabel('Number of Playlists')
plt.ylabel('Category')
plt.tight_layout()
plt.savefig('data/category_distribution.png', dpi=300)
plt.show()
1️⃣2️⃣ 总结与延伸阅读
我们完成了什么?
通过这个项目,我们实现了:
- ✅ 逆向分析 Spotify 的内部 API,绕过了前端渲染限制
- ✅ 构建了一套可扩展的爬虫架构(API 客户端 + 解析器 + 存储层)
- ✅ 获得了 180+ 条真实的 Spotify 歌单数据
- ✅ 掌握了现代 Web 应用爬虫的核心技能(Token 提取、API 逆向、容错机制)
下一步可以做什么?
1. 数据分析方向:
- 分析不同分类的歌单曲目数分布(Pop vs Rock vs Classical)
- 爬取封面图并用 OpenCV 分析颜色风格
- 用 NLP 分析歌单描述的关键词
2. 爬虫进阶方向:
- 深度爬取: 继续爬取每个歌单的详细歌曲信息(歌手、专辑、时长)
- 多国对比: 修改
locale参数,对比不同国家的热门歌单 - 实时监控: 定时爬取榜单变化,绘制趋势图
3. 工程化方向:
- Scrapy 改造: 迁移到 Scrapy 框架,利用其管道和中间件
- 容器化部署: 打包成 Docker 镜像,云端定时运行
- 数据库存储: 改用 PostgreSQL/MongoDB 存储,支持复杂查询
与官方 API 的对比
| 维度 | 爬虫方案 | 官方 API |
|---|---|---|
| 门槛 | 低(无需注册) | 中(需申请 Client ID) |
| 速率限制 | 看网站策略 | 严格(每小时有限) |
| 数据完整性 | 可能缺失部分字段 | 完整且有文档 |
| 稳定性 | 受网页改版影响 | 稳定(有版本控制) |
| 合规性 | 灰色地带 | 完全合法 |
我的建议:
- 如果是个人学习、小规模数据分析 → 用爬虫(快速灵活)
- 如果是商业项目、需要长期维护 → 用官方 API(稳定可靠)
推荐阅读
文档:
- Spotify Web API 官方文档:https://developer.spotify.com/documentation/web-api
- aiohttp 异步请求库:https://docs.aiohttp.org/
工具:
- Postman:调试 API 接口必备
- Charles/Fiddler:抓包分析神器
- jq:命令行 JSON 处理工具
社区:
- r/webdev:Reddit 上的 Web 开发社区
- GitHub Topic: spotify-api
最后的话
Spotify 的爬取难度明显高于传统静态网站,这也正是它的魅力所在------你需要像个侦探一样,层层剥开现代 Web 应用的外壳,找到数据的真正来源。
这个过程教会我的不仅是技术,更是一种解决问题的思维方式:
- 遇到 JS 渲染? → 打开开发者工具,找 API
- Token 验证失败? → 分析请求头,模拟浏览器
- 速率限制太严? → 加延迟、用代理、换思路
技术的本质是解决问题,而不是炫技。希望这篇教程能帮你打开音乐数据分析的大门。
如果你成功爬取了数据,欢迎在评论区分享你的成果!也许下一个 Spotify 数据可视化项目就是你的作品。
🌟 文末
好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
✅ 专栏持续更新中|建议收藏 + 订阅
墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一期内容都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴 :强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?
评论区留言告诉我你的需求,我会优先安排实现(更新)哒~
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
✅ 免责声明
本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。
使用或者参考本项目即表示您已阅读并同意以下条款:
- 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
- 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
- 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
- 使用或者参考本项目即视为同意上述条款,即 "谁使用,谁负责" 。如不同意,请立即停止使用并删除本项目。!!!
