想搞清楚自己这一年到底在听什么,别只用 Spotify 官方的 Wrapped 年终总结,那玩意儿好看是好看,但信息密度太低了。我去年整理自己的听歌数据时发现,用 Python 把 Spotify 的播放记录拉下来做一次完整分析,能看到的维度远比官方总结丰富——几点钟最沉迷、周末和通勤时段在听什么、口味在一年里怎么漂移、音频特征有没有变“丧”,这些都能量化。这篇文章就完整记录我是怎么用 Python 分析 Spotify 听歌数据的,从授权拿数、清洗处理到可视化出图,全程可复现,适合刚学 Python 想找真实项目练手的人,也适合想深度了解自己听歌习惯的老玩家。
1. 项目思路与整体设计:先想清楚要分析什么,再动手写代码
做个人数据分析项目最大的坑就是一上来就写代码。我一开始也犯过这个毛病,急着调通接口看到数据,结果拿到一堆 JSON 之后不知道拿字段干什么,又回头重新设计。所以先花几分钟把分析目标定下来,后面会省一大半时间。
1.1 拿到了数据,能回答哪些问题
Spotify 的听歌数据能分析的方向非常多,我这次主要锁定了五个问题:
- 每天什么时候听歌最集中,工作日和周末的模式有什么差异
- 播放时长最长的 Top 歌手、Top 歌曲分别是谁
- 一周七天里哪几天听歌最猛,哪个季节或月份听歌时长有明显变化
- 我的听歌时段分布是“通勤型”“工作背景音型”还是“深夜emo型”
- 如果拿到音频特征数据,还能看出我喜欢歌曲的平均能量、愉悦度、声学度倾向
这些问题拆开看就是数据聚合和可视化的事,难度不高,但能拼出一张完整的“听歌画像”。我建议你把想解决的问题写到纸上,后面每一步处理都对照这些问题,避免瞎逛。
1.2 为什么首选官方 API,而不是去做爬虫
很多新手第一反应是去爬网页版 Spotify,或者抓公共播放列表。我很不推荐这么干,原因有两个。第一,Spotify Web API 是官方提供的数据接口,播放记录、歌曲信息、音频特征都有结构化字段,做数据分析要的是干净数据,不是从 HTML 里辛苦抠出来的文本;第二,自己账号的数据涉及隐私,用官方 API 走正规授权流程,比抓包和模拟请求安全得多,也省心得多。
爬虫不是不能用,但在这个场景里属于“吃力不讨好”。API 一次请求返回的都是规整的 JSON,字段名稳定、文档齐全,数据量大一点还能分页拿到更多记录。所以我下面所有流程都基于 Spotify 官方 Web API。
1.3 两种数据获取路线,按需求选一种
我之前以为 Web API 能把开号以来的全部听歌记录都拉回来,跑完才发现不是这么回事。recently-played 这个端点一次最多返回 50 条,能查到的范围也锁死在最近 90 天内,所以想做“全年完整回顾”,单靠 API 不现实。
| 数据获取方式 | 覆盖范围 | 数据量 | 适合场景 |
|---|---|---|---|
| Web API(recently-played) | 最近 90 天 | 最多 50 条/页,可分页 | 了解近期听歌习惯、做实时监控 |
| Web API(user-top-read) | 长期聚合 | Top 50 歌曲/歌手 | 看长期口味,不需要时间维度 |
| 官方数据导出(Extended streaming history) | 全部历史记录 | 全部播放事件 | 长期趋势分析、完整回顾 |
我自己最后是两条路线都跑了:先用 API 拿近期数据做“近期画像”,然后去 Spotify 账号后台申请了完整历史数据导出,等到邮件通知后下载了 JSON 文件,这样才拿到了真正意义上从第一天用 Spotify 开始的完整播放记录。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与工具选型:Python 环境、虚拟环境和核心库
这个项目用到的库不算多,但有一个算一个都是数据分析和处理的主力。先把环境配好,后面才不会装依赖装到怀疑人生。
2.1 Python 版本和虚拟环境
我本机用的是 Python 3.10+,其实 3.9 以上都能跑。如果你是第一次装 Python,Windows 上记得安装时勾选 “Add Python to PATH”,不然命令行里敲 python 会提示找不到命令。macOS 用户系统自带的是 2.7 时代遗留下来的问题,我建议直接用 Homebrew 装一个新版,省得后面跟系统自带版本搞混。
装好 Python 之后,我强烈建议为这个项目单独建一个虚拟环境,别往全局环境里怼依赖。我有一次直接全局装了一大堆包,后来版本冲突把系统工具都搞出问题了,老老实实敲了这么几条命令:
bash复制python -m venv spotify_env
# Windows 激活
spotify_env\Scripts\activate
# macOS / Linux 激活
source spotify_env/bin/activate
虚拟环境能隔离项目依赖,之后不管装哪个版本的 pandas 或 spotipy,都不会污染系统 Python,出问题删掉重建就行,成本几乎为零。
2.2 核心库清单和安装命令
这个项目用到的核心库和它们各自的职责如下:
spotipy:Spotify Web API 的 Python 封装,处理授权、请求、分页都方便,不用自己写一堆请求头pandas:数据分析的地基,加载 JSON、清洗、聚合、统计全靠它matplotlib:基础的静态图表,画折线、柱状图、热力图都够用seaborn:基于 matplotlib 的高级绘图库,默认主题比裸 matplotlib 好看几个档次python-dotenv:管理 Client ID 和 Client Secret,避免把密钥硬编码在代码里plotly(可选):生成交互式图表,适合导出 HTML 自己慢慢玩
安装就一句话:
bash复制pip install spotipy pandas matplotlib seaborn python-dotenv plotly
如果 pip 下载速度很慢,可以临时切换为清华或阿里云镜像源,把 pip 的默认源改成 https://pypi.tuna.tsinghua.edu.cn/simple。装好之后你可以跑一句 python -c "import pandas; print(pandas.__version__)" 确认安装成功,有版本号输出就没问题。
2.3 为什么要用 spotipy 而不是手写 requests
Spotify 的授权流程是标准的 OAuth 2.0,听起来不复杂,但真正自己写的时候要处理授权链接生成、回调接收、token 存储、token 自动刷新等一系列琐碎逻辑。我第一次就有两个晚上耗在 token 刷新上,后来换来换去还是觉得 spotipy 省事。
spotipy 把最麻烦的 OAuth 流程封进了 SpotifyOAuth,你把 Client ID、Client Secret、回调地址和需要的权限 scope 传进去,它自己维护缓存和过期刷新,你要做的只是调用业务方法拿数据。所以我的建议是:想深入理解 OAuth 原理,可以自己去读文档;想快速做分析项目,直接上 spotipy。
3. 数据获取:申请开发者应用、授权拉取与官方数据导出
数据获取是整个项目最关键的一步,这里的授权配置搞错了,后面全白费。我会把两条路线都走一遍,你可以根据自己的需求选一条,或者像我省事一点两条都跑。
3.1 第一步:在 Spotify Developer Dashboard 创建应用
先打开 Spotify for Developers 的 Dashboard(开发者后台),用你的 Spotify 账号登录,然后点 “Create App” 创建应用。应用名称和描述随便填,关键是建完后在应用详情页能看到两个重要信息:
Client ID:应用的公开标识,类似身份证号Client Secret:应用的密钥,只能看一次,再次查看要验证账号
创建完应用后,还要点 “Edit Settings”,在 Redirect URIs 这一项填入 http://localhost:8888/callback。这个地址是授权完成后 Spotify 用来跳转的本地回调地址,很多新手漏了这一步,结果授权时一直报 “Redirect URI mismatch”。
我习惯把这两串密钥放到项目根目录下的 .env 文件里,不让它们出现在代码中:
plaintext复制SPOTIPY_CLIENT_ID=你的client_id
SPOTIPY_CLIENT_SECRET=你的client_secret
SPOTIPY_REDIRECT_URI=http://localhost:8888/callback
然后用 python-dotenv 在代码开头加载环境变量,这样即使项目传到公开仓库,也不会把密钥暴露出去。这也是我一直强调的习惯:密钥永远不要写死在代码里。
3.2 第二步:用 spotipy 完成 OAuth 授权
下面这段是我的授权代码,核心就是把 scope 参数设置好,然后交给 SpotifyOAuth 管理整个授权流程。
python复制import os
from dotenv import load_dotenv
import spotipy
from spotipy.oauth2 import SpotifyOAuth
load_dotenv()
CLIENT_ID = os.getenv("SPOTIPY_CLIENT_ID")
CLIENT_SECRET = os.getenv("SPOTIPY_CLIENT_SECRET")
REDIRECT_URI = os.getenv("SPOTIPY_REDIRECT_URI")
scope = "user-read-recently-played user-top-read"
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
redirect_uri=REDIRECT_URI,
scope=scope,
cache_path=".spotify_cache",
))
results = sp.current_user_recently_played(limit=50)
for item in results["items"]:
track = item["track"]
played_at = item["played_at"]
artist = track["artists"][0]["name"]
print(f"{played_at} | {artist} - {track['name']}")
第一次运行这段代码会输出一个授权链接,点击后用 Spotify 账号登录并确认授权,你会看到一个允许页面。授权完成后浏览器会跳转到回调地址,这时候可能显示一个本地服务无法访问的页面,别慌,把跳转后的 URL 复制回终端,spotipy 会自己完成 token 交换。之后 token 会缓存在项目目录下的 .spotify_cache 文件里,下次运行不再需要重新授权。token 过期后 spotipy 会自动刷新,所以平时使用完全无感。
3.3 第三步:拉取近期播放记录并解决分页限制
current_user_recently_played 一次最多 50 条,如果你想要更多的记录,需要利用 API 返回的 before 时间戳做循环拉取。下面是我写的分页拉取逻辑:
python复制import time
def fetch_recently_played(sp, total_limit=200):
items = []
before = None
while len(items) < total_limit:
if before:
results = sp.current_user_recently_played(limit=50, before=before)
else:
results = sp.current_user_recently_played(limit=50)
batch = results["items"]
if not batch:
break
items.extend(batch)
# 取这一批中最早的播放时间戳,继续往前翻
before = batch[-1]["played_at"]
time.sleep(1) # 加个延时,避免触发频率限制
return items[:total_limit]
注意一点,即便你写了 total_limit=1000,只要当前时间往前超过 90 天,这个端点就再也查不到更早的数据了。这是 Spotify 的限制,不是代码的问题。
3.4 完整历史数据:去账号后台申请导出
如果像我一样想分析开号以来的所有听歌记录,API 就不够用了,必须走官方数据导出。登录 Spotify 后进入账号页面的 “Privacy settings”,找到 “Download your data” 相关选项,点击申请导出数据,其中有一项是 “Extended streaming history”,也就是完整的播放历史记录。
提交申请后通常要等几天,Spotify 会往你的注册邮箱发一封包含下载链接的邮件。下载下来的压缩包里,主要的播放历史文件一般是包含所有年份的 JSON 文件,里面有每一次播放事件的完整记录,字段大概是这种感觉:
json复制{
"ts": "2024-11-20T14:23:45Z",
"ms_played": 245000,
"master_metadata_track_name": "Song Title",
"master_metadata_album_artist_name": "Artist Name",
"master_metadata_album_album_name": "Album Name",
"spotify_track_uri": "spotify:track:xxxxx",
"reason_start": "clickrow",
"reason_end": "trackdone"
}
拿到这个 JSON 之后,你就可以直接在本地做完整历史分析了,完全不依赖 API,也不受 90 天限制。我当时把这个文件和 API 拉到的近期数据做了交叉验证,发现两边数值对得上,说明数据可信度还是很高的。
4. 数据清洗与基础分析:从原始播放记录到可读的统计结果
数据拿到手只是第一步,接下来要做的是把不适合直接分析的数据清洗成整齐的表格,再做聚合统计。这一部分能用到 pandas 的不少核心操作,我边写边讲为什么要这么做。
4.1 用 pandas 读取并理解数据结构
如果把导出的 JSON 文件用 pandas 读进来,长这样:
python复制import pandas as pd
# 根据实际文件名修改
df = pd.read_json("Spotify Extended Streaming History/Streaming_History_Audio_2023_2024.json")
print(df.head())
print(df.info())
读进来后你会发现几个字段特别值得关注:ts 是播放时间,ms_played 是播放毫秒数,master_metadata_track_name 是歌曲名,master_metadata_album_artist_name 是歌手名,spotify_track_uri 是歌曲的唯一标识。
有个细节我最早没注意:ts 字段记录的是 UTC 时间,而我想看的是本地时区的收听时段。如果直接按 UTC 来统计“晚上八点”的播放量,数据会和实际感受完全对不上。所以第一步就要把时区转成自己的本地时区。
python复制df["ts"] = pd.to_datetime(df["ts"], utc=True)
df["ts_local"] = df["ts"].dt.tz_convert("Asia/Shanghai")
df["hour"] = df["ts_local"].dt.hour
df["weekday"] = df["ts_local"].dt.weekday
df["date"] = df["ts_local"].dt.date
4.2 过滤掉“无效播放”,否则统计全被污染
这个坑我印象太深。第一次画播放时长分布图时,数据特别奇怪——很多歌只播了三四秒就跳过了,还有一些是暂停在播放列表里自动续播的算进了时长。原始数据里有 ms_played 字段,单位是毫秒,我直接从原始数据聚合,结果 Time Top 榜全被那些只播开头就被切掉的歌霸占了。
最后我做的清洗规则是保留“播放超过 30 秒”的记录作为有效播放,因为正常听歌不太可能 30 秒内就结束,除非是刻意切歌。纯听歌场景下,30 秒以上基本能代表“真正听过”。
python复制df_valid = df[df["ms_played"] >= 30000].copy()
df_valid["play_minutes"] = df_valid["ms_played"] / 60000
这道过滤做完,统计结果马上变得合理了。如果你的目的是分析“切歌行为”本身,那就反过来只保留 30 秒以下的记录,看哪些歌最容易让人切掉,这个视角也很有意思。
4.3 核心指标的计算:总时长、每日趋势、歌手排名
清洗完之后就可以算一些基础指标了,代码短但信息量很大:
python复制total_minutes = df_valid["play_minutes"].sum()
print(f"有效播放总时长:{total_minutes:.1f} 分钟,约 {total_minutes / 60:.1f} 小时")
# 按天汇总播放时长
daily = df_valid.groupby("date")["play_minutes"].sum()
# 歌手 Top 10
artist_stats = (
df_valid.groupby("master_metadata_album_artist_name")["play_minutes"]
.sum()
.sort_values(ascending=False)
.head(10)
)
我在跑这段的时候发现一个特别有意思的现象:把总时长除以歌曲数量后,我的平均单曲播放时长只有 2 分半,说明我确实有“听了前几句就切歌”的毛病。而按小时维度分组后,工作日和周末的听歌高峰完全不一样——工作日是早上通勤时段和下午工作时段双峰,周末则是中午到下午一个长坡。这些现象如果只用官方 Wrapped,根本看不出来。
5. 可视化:把统计结果画成能一眼看懂的趋势
只看数字总感觉少了点什么,可视化才是我觉得这个项目最有成就感的环节。图表能把你前面算出来的那些分组数据变成真正让人“哇”出来的结果,Reveal 的瞬间很有满足感。
5.1 先解决中文显示问题,不然全是方块
用 matplotlib 画中文标签,如果不做设置,大概率出来全是□□□□。这不是代码问题,是 matplotlib 默认字体不支持中文字符。我通常会在代码开头做这样的设置:
python复制import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "PingFang SC"]
plt.rcParams["axes.unicode_minus"] = False
axes.unicode_minus 那行也得加上,否则负号会显示成奇怪的方块。macOS 上可以优先写 PingFang SC,Windows 上写 Microsoft YaHei 或 SimHei 效果都行。如果嫌麻烦,也可以直接用 seaborn 的默认主题,再配合 sns.set_theme() 能让整体风格干净不少。
5.2 三张最核心的图:时段热力图、月度趋势、歌手 Top 榜
我最终出图的三个主要方向如下,每个我都贴了代码思路。
第一张:一周 7 天 × 24 小时的播放时段热力图。 这张图能最直观地暴露“几点钟在听歌”的规律。做法是先把每个播放事件映射到 weekday 和 hour 两个维度,再统计每个组合的播放时长,然后画热力图:
python复制import seaborn as sns
df_valid["hour"] = df_valid["ts_local"].dt.hour
df_valid["weekday"] = df_valid["ts_local"].dt.weekday
heatmap_data = (
df_valid.pivot_table(index="weekday", columns="hour", values="play_minutes", aggfunc="sum")
.fillna(0)
)
plt.figure(figsize=(14, 6))
sns.heatmap(heatmap_data, cmap="YlOrRd", linewidths=0.3)
plt.yticks(
ticks=range(7),
labels=["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
)
plt.xlabel("小时")
plt.ylabel("星期")
plt.title("一周听歌时长分布热力图")
plt.tight_layout()
plt.savefig("output/weekday_hour_heatmap.png", dpi=150)
我自己出图后的发现是:工作日上午 8-9 点有一片亮红色,下午 14-17 点有小高峰;周末则集中在 10-15 点,典型“周末宅家放歌”型。
第二张:按月份聚合的播放时长趋势折线图。 用 pandas 的 resample 把数据按月份汇总,然后直接画线,看长期口味和播放量的升降:
python复制monthly = df_valid.set_index("ts_local")["play_minutes"].resample("ME").sum()
plt.figure(figsize=(12, 5))
monthly.plot(kind="line", marker="o")
plt.title("每月有效播放时长变化")
plt.xlabel("月份")
plt.ylabel("播放时长(分钟)")
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("output/monthly_trend.png", dpi=150)
比如我注意到每年年底的播放量明显上涨,估计是“年度回顾”氛围诱导我狂补歌单。这种结论虽然主观,但至少数字支撑。
第三张:Top 歌手的横向条形图。 横向条形图适合展示排名,歌手特别多的时候也能轻松读。
python复制top_artists = artist_stats.head(10)[::-1] # 反转一下,让最大的在上面
plt.figure(figsize=(10, 7))
top_artists.plot(kind="barh", color="#1DB954")
plt.xlabel("累计播放时长(分钟)")
plt.title("Top 10 歌手播放时长排行")
plt.tight_layout()
plt.savefig("output/top_artists.png", dpi=150)
绿色用的就是 Spotify 品牌色 #1DB954,整组图放在一起很有一体感。
5.3 想玩交互,就用 plotly 把图变成网页
如果觉得静态图不过瘾,我还会用 plotly 做一张可以缩放、悬停看数据的交互图,直接输出成一个 HTML 文件。代码大概是这样:
python复制import plotly.express as px
fig = px.line(
monthly.reset_index(),
x="ts_local",
y="play_minutes",
title="每月播放时长(交互版)",
labels={"ts_local": "月份", "play_minutes": "播放分钟数"},
)
fig.update_traces(line_color="#1DB954")
fig.write_html("output/monthly_trend_interactive.html")
生成出来的 HTML 可以直接用浏览器打开,鼠标滑过去能看到每月的具体数值。分享给自己看或者放到 Notion 里做数据报表都很爽。不过既然是工具项目,我建议先以 matplotlib 为主,交互图作为加分项就好。
6. 常见问题与排查技巧实录
我完整跑完这个项目之后踩了不少坑,这里整理成一张表和一些详细的排查思路,按频率从高到低排。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 授权跳转后报 Redirect URI mismatch | 开发者后台没填回调地址或填错 | 去 Dashboard 的 Edit Settings 里检查 Redir. URI 是否完整一致 |
| token 失效后总是重新授权 | .spotify_cache 被删了,或 cache_path 不一致 |
检查代码里的 cache_path 恒定,删除缓存文件后重新跑一次授权 |
| 请求频繁后报 429 | 访问频率超过 Spotify API 限制 | 在循环里加 time.sleep,适当减慢请求频率 |
| 图表中文乱码 | matplotlib 默认字体不支持中文 | 设置 plt.rcParams["font.sans-serif"] 和 axes.unicode_minus |
recently-played 查不到 90 天前的数据 |
Spotify API 本身限制 | 改用官方数据导出获取完整历史 |
| 播放时长异常偏短 | 没过滤短播放记录 | 加上 ms_played >= 30000 的过滤条件 |
6.1 授权和 token 相关的问题,照这个套路排查
如果你之前跑过一遍授权,后来不知道怎么回事重新跳出了授权链接,十有八九是 .spotify_cache 文件被删掉了或者代码里的 cache_path 设置不一致。这个缓存文件存的是 refresh_token 和 access_token,没有它就要重新走一遍浏览器授权。这不是 bug,是设计如此。
还有一种常见情况是代码部署到服务器上,没有浏览器环境,授权那一步就卡住了。我的建议是:本地跑通授权后,把生成的 .spotify_cache 文件一并处理到服务器对应路径,或者改用 PKCE 流在服务器端自己处理,不过这属于进阶话题,普通分析项目在本地跑就够了。
6.2 播放记录不全?先确认是 API 限制还是请求写错
很多人发现 current_user_recently_played 返回的数据总是最近的记录,就以为代码写错了。其实没写错,Spotify 官方的这个接口就是只保留最近 90 天内播放过的歌曲,并且单次请求最多 50 条。如果你想得到完整历史,别在 API 这里死磕,直接走我在 3.4 节讲的官方数据导出。
另外,导入 JSON 文件后你可能会发现数据里有不少 ms_played 非常小的记录,例如 4000 毫秒、2000 毫秒。这个在真实播放场景里大概率是误触、跳过或者没加载完就切歌,分析前一定要过滤。
6.3 大文件读取慢或内存占用高?给个小技巧
如果你的账号用了好几年,导出的完整历史 JSON 可能有几百 MB,pd.read_json 读取时可能会有点慢,内存也吃得比较紧。我一般会先分块读取或者只加载需要的列,具体做法是用参数限制读取字段,减少内存占用:
python复制df = pd.read_json(
"Streaming_History_Audio_2023_2024.json",
dtype={
"master_metadata_track_name": "string",
"master_metadata_album_artist_name": "string",
"ms_played": "int64",
},
)
如果文件实在太大了,建议按年份拆分成多个小文件分片处理,最后再合并。个人数据一般不会真的大到离谱,但养成好习惯总没错。
最后再分享一点我的个人体会
这个项目我跑完之后,最上瘾的不是图有多好看,而是后面每次听歌都在想着“过两周再拉一次数据,看看这段时间听歌习惯变了没有”。如果你也是这个心态,我强烈建议把拉数据和清洗这两段逻辑封装成可复用的模块,或者写成脚本定期执行,把每次的结果追加到同一个汇总表里,长期积累出来的趋势比单次看更有意思。我自己的做法是每个月手动导出一次完整数据,用同一套代码重新出一版图,放到同一个目录里对比,能看到一年里口味迁移的全过程。下一篇如果你们有兴趣,我可以再写怎么写音频特征字段,把“歌单酸化程度”这种指标量化出来。
