每年年底打开 Spotify 的年度报告,看见"你最喜欢的歌手"那一栏,我都觉得某项统计有点"雾里看花"——它只给我一张精心设计的图,却从不把完整数据交到我手上。音乐软件说"大数据比你更懂你",那我自己能不能把这笔数据要回来,用 Python 拆开看一遍?答案是可以的。这篇内容就把我完整跑过的流程拉出来讲讲:从向 Spotify 申请下载原始数据,到用 pandas 清洗和分析,最后画出几张能放进简历的作品集图表。如果你正在学 Python 数据分析,或者单纯想深挖自己的听歌口味,这篇可以直接当操作手册来用。
老实说,第一次接触 Spotify 数据档案时我以为会拿到一个可以直接导入 Excel 的完整 CSV 文件,结果打开文件夹才发现里面有十几个 JSON 和几十个子目录。数据虽乱,但好处是真实、细粒度、覆盖从注册以来的全部行为。下面我按自己实际操作过的顺序,从数据获取讲到最后的可视化输出,每一步都附上代码和排查思路,保证你在自己电脑上能完整复现。
1. 从 Spotify 拿数据的两种方式:官方导出与 API 抓取
1.1 官方数据包:最全量的离线快照
Spotify 官方账户设置里有一个"Privacy settings"区域,往下拉能找到"Request your data"按钮。点击之后官方会给你一个下载链接,通常几分钟到几小时不等就会通过邮件发过来。这个数据包覆盖的是你从注册第一天开始的所有数据,包括播放历史、收藏歌曲、创建的歌单、关注的艺术家、搜索记录等等,信息量非常大。
我第一次下载下来解压之后,发现里面的文件结构大概是这样的:
StreamingHistory0.json到StreamingHistoryN.json:逐文件记录听歌流水,包含歌手名、曲名、播放开始时间、播放时长。YourLibrary/:收藏列表,包括歌曲、专辑、艺术家三类。Playlist1.json/Playlist2.json:你创建的歌单和收藏的别人歌单。Shows/、Audiobooks/等目录:播客和有声音频记录。
StreamingHistory*.json是我这次分析的核心,因为只要有播放记录,就有了一切。这个导出包是官方提供的全量数据,最多不过几MB,处理起来完全不费劲,推荐所有个人分析都优先用这条路径。
1.2 Spotify Web API:适合做实时增量
如果你想要的是"最近 50 条播放记录"或者"我的 Top 歌手"这类实时数据,那就用 Web API。这个方案适合做持续观察、定时抓取,比如每天跑一遍脚本、存下当天的播放记录到本地数据库。
用 Web API 之前,需要先去 Spotify Developer Dashboard 注册一个应用,拿到Client ID和Client Secret。这不是什么难事,免费提交流程即可。然后根据你需求申请对应的 Scope,比如user-read-recently-played可以读取最近播放记录、user-top-read可以读取你的 Top 艺人和 Top 歌曲。
下面是一段用requests获取最近播放记录的极简示例:
import requests CLIENT_ID = "你的Client_ID" CLIENT_SECRET = "你的Client_Secret" auth_response = requests.post( "https://accounts.spotify.com/api/token", data={"grant_type": "client_credentials", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET} ) ACCESS_TOKEN = auth_response.json()["access_token"] headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"} response = requests.get( "https://api.spotify.com/v1/me/player/recently-played", headers=headers, params={"limit": 50} ) data = response.json()这段代码能跑通的前提是在 Spotify Developer Dashboard 里把你的应用设置成正确重定向地址,并且拿到带user-read-recently-played权限的 token。这里只用了client_credentials,但recently-played这类接口通常需要OAuth用户授权流程,所以更现实的用法是借助spotipy库里现成的授权方法:
import spotipy from spotipy.oauth2 import SpotifyOAuth sp = spotipy.Spotify(auth_manager=SpotifyOAuth( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, redirect_uri="http://localhost:8888/callback", scope="user-read-recently-played" )) results = sp.current_user_recently_played(limit=50)API 方便是方便,但它只能返回最近一段时间的记录,而且历史数据范围非常有限。想做完整分析的朋友,还是建议直接走官方导出包,一次性把全部家底拿回来。
1.3 两种获取方式对比
| 对比维度 | 官方数据包 | Web API |
|---|---|---|
| 数据范围 | 全量历史记录 | 仅最近记录(上限因 Scope 而异) |
| 获取难度 | 简单,网页勾选等待邮件 | 需要注册应用、配 OAuth 流程 |
| 数据形态 | JSON 文件批量下载 | JSON 接口实时返回 |
| 适合场景 | 一次性深度分析 | 定时增量采集 |
我的最终建议是:如果你要做长期趋势分析,直接选官方数据包;如果你只是想做个脚本每天备份当天的播放记录,再考虑 API。两套方案其实可以并行:拿官方数据包做历史全量底稿,API 做后续持续增量。
2. 拿到手的数据先别急着画图:清洗是第一个拦路虎
2.1 先搞清楚 JSON 里面到底存了什么
我第一次打开StreamingHistory0.json时差点以为下载坏了,因为这个文件不带任何表头,就是一个用中括号包起来的对象数组。每个播放记录的字段包括:
| 字段名 | 示例值 | 含义 |
|---|---|---|
endTime | "2024-06-18 08:34" | 这次播放结束的本地时间 |
artistName | "Radiohead" | 歌手名 |
trackName | "No Surprises" | 歌名 |
msPlayed | 216000 | 播放时长,单位毫秒 |
新版数据包里还会附带trackUri和episodeName等字段,核心逻辑不变。注意endTime是"播放结束时间"而不是"开始时间",后面做时段分析时要用播放时长倒推区间,别搞反了。
2.2 时间字段的时区陷阱
endTime字段字面上看起来只是一个普通字符串,比如"2024-06-18 08:34",但不同地区用户的导出样式可能不一样。Spotify 官方数据导出在做 UTC 转换时并不一致,有的人拿到的是纯粹 UTC,有的人拿到的是改过的本地时间。我的建议是不要相信直觉,统一把它当成 ISO 字符串解析,然后按你的目标时区做偏移。
我在清洗时写了一小段辅助函数来做时间转换:
from datetime import datetime import pytz def parse_time(time_str): """把 Spotify 导出的字符串解析成 UTC+8 的 datetime。""" dt = datetime.strptime(time_str, "%Y-%m-%d %H:%M") dt_utc = dt.replace(tzinfo=pytz.utc) return dt_utc.astimezone(pytz.timezone("Asia/Shanghai")) df["end_time"] = df["endTime"].apply(parse_time) df["play_date"] = df["end_time"].dt.date df["hour"] = df["end_time"].dt.hour这里有个非常容易踩的坑:直接pd.to_datetime(df["endTime"])解析后不会自动处理时区,结果就是你的月度统计会莫名其妙地产生几个小时的偏移。我的经验是:导出包数据的时间并不绝对可信,用pytz显式转成目标时区,至少保证同一套代码在换电脑、换环境之后跑出来的结果一致。
2.3 "秒断"与无效记录:先过滤再统计
流媒体平台有很多"半首歌"记录,也就是用户听了 3 秒就切歌、或者被 AI 自动播放误触了。这些记录如果不处理,会严重虚增艺术家的播放次数和播放总时长。我在分析前设置了一个 30 秒阈值:
df_valid = df[df["msPlayed"] >= 30000].copy()为什么是 30 秒而不是 10 秒或 60 秒?结合我自己的切歌习惯:只听了十几秒说明根本不感兴趣,听了 30 秒以上说明至少进入了重复循环。你也可以按自己的习惯调整阈值,但建议统一口径后再算 Top 榜单,否则谁在切歌榜上排第一完全没意义。
清洗完成之后,df_valid就是我们后续所有分析的核心 DataFrame。到这一步,数据管道基本打通了,后面可以放心交给 pandas 去聚合。
3. 听歌画像怎么算?四个维度还原你的真实口味
3.1 播放时长趋势:你的播放量为什么忽高忽低?
拿到干净数据之后,我做的第一件事是看时间维度上的总播放时长趋势。把play_date按月份聚合,能直观看到自己一年里什么时间段听得最多。这里有一段相对完整的代码:
import pandas as pd df_valid["month"] = df_valid["play_date"].astype(str).str[:7] monthly_play_ms = df_valid.groupby("month")["msPlayed"].sum() monthly_play_min = (monthly_play_ms / 60000).round() print(monthly_play_min)跑出来的数据会很有故事感。我自己在考项目那几个月播放量明显缩水,五六月因为露营和自驾猛涨一波。看趋势的意义不只是复盘,还能用来验证"我那段时间到底是怎么过来的"——数据不会撒谎,它比日记还诚实。
月度趋势分析还可以进一步拆成周几维度,比如周五和周日往往会形成两个不同的听觉高峰。这些在后续做报告的时候可以一并附上,让分析更丰满。
3.2 歌手集中度:你是"专一型"还是"杂食型"?
第二个我想搞清楚的维度是歌手分布。一般人对自己的判断常常是"我什么类型都听一点",但从统计上看,其实你可能被极少数歌手垄断了大部分播放时长。我写了这样一个统计逻辑:
artist_play_count = df_valid["artistName"].value_counts() top10_count = artist_play_count.head(10).sum() total_count = artist_play_count.sum() top10_ratio = top10_count / total_count * 100 print(f"Top10 歌手播放次数占比: {top10_ratio:.1f}%")同时也要算播放时长维度的 Top:
artist_play_ms = df_valid.groupby("artistName")["msPlayed"].sum().sort_values(ascending=False) top10_ms = artist_play_ms.head(10).sum() total_ms = artist_play_ms.sum() top10_ms_ratio = top10_ms / total_ms * 100 print(f"Top10 歌手播放时长占比: {top10_ms_ratio:.1f}%")播放次数占比和播放时长占比通常会给人完全不同的画像:有人很爱点某首歌的前奏,秒剪;有人单曲循环一晚上。看次数占比可能看不出谁是真爱,但看时长占比就能发现那些被反复循环播放的"深度歌手"。
我自己的结果是 Top10 歌手占了总时长的 43%,Top50 占了 78%。看到这个数字时我意识到自己其实比想象中更"恋旧",张口就是"什么都听",数据却说我只在十来位老熟人之间打转。这类集中度指标很适合用作文章的配图,直观又有冲击力。
3.3 时段分布:"深夜歌单"背后的情绪规律
时间段分析非常有意思,因为你会发现不同的时段对应不同的听歌场景。结合hour字段和artistName,可以看每个小时里被播放最多的歌手是谁:
hourly = df_valid.groupby(["hour", "artistName"]).size().reset_index(name="count") hourly_top = hourly.sort_values(["hour", "count"], ascending=[True, False]) hourly_top10 = hourly_top.groupby("hour").head(1) print(hourly_top10)跑完这个结果,我顺手做了个 24 小时柱状图。能明显看到早晨通勤时段有大量流行播客和轻音乐,下午工作时段偏电子和氛围乐,而凌晨两三点集中出现一堆低沉、慢节奏的歌曲。音乐 App 常说"深夜歌单",结果从个人数据里真的能挖出来一个"暗夜 B 面歌单"——那是我白天根本不会承认在听的曲风。
这类分析其实反映的是生活节奏:几点睡觉、几点通勤、几点emo,全部藏在数据里。做这种分析不需要高深的算法,只需要按小时分组再排序,就能拼出一幅个人生活作息图。
3.4 口味漂移:季度对比识别"变心时刻"
最后一个维度是看口味随时间的推移有没有变化、变化发生在什么时候。我按季度把数据切分,然后每个季度取播放次数 Top3 的歌手做对比:
df_valid["quarter"] = df_valid["month"] + "-Q" + ((df_valid["play_date"].dt.month - 1) // 3 + 1).astype(str) quarter_top = df_valid.groupby(["quarter", "artistName"]).size().reset_index(name="count") quarter_rank = quarter_top.sort_values(["quarter", "count"], ascending=[True, False]) quarter_top3 = quarter_rank.groupby("quarter").head(3)输出结果后你会看到一张很直观的"变心表"。比如某位歌手第一、二季度都在前列,第三季度突然消失,取而代之的是某个突然上位的后摇乐队。这个变化往往对应着现实生活中的环境切换:换工作、换城市、认识新朋友、或者某段关系开始/结束。数据在这里不再是冷冰冰的记录,而是个人史的索引。
四个维度算完之后,分析的主体工作量就完成了。剩下的是把结果摆上台面,给别人看、给自己回顾。
4. 可视化不炫技:三张图讲清你的听歌故事
4.1 时间趋势图:月度播放时长柱状图
时间趋势类数据用柱状图最合适,一眼能看出波动。我用 matplotlib 画图,先确保中文字体正常:
import matplotlib.pyplot as plt import numpy as np plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False months = monthly_play_min.index.tolist() values = monthly_play_min.values plt.figure(figsize=(14, 5)) plt.bar(months, values, color="#1DB954") plt.title("月度播放时长(分钟)") plt.xticks(rotation=45) plt.xlabel("月份") plt.ylabel("播放分钟") plt.tight_layout() plt.savefig("monthly_play_时长.png", dpi=150)需要说明的是,全年播放量很大的话,分钟数会到几千甚至上万,画图时不用担心数值太大,独占一张图效果反而好。若想更平滑地观察趋势,可以叠加一个滚动平均值曲线,让大起大落不那么扎眼。
4.2 歌手分布条形图:谁在霸占你的耳朵
Top 歌手分布用横向条形图最适合,因为歌手名可能比较长,横向排布可读性更高:
top_artists = df_valid["artistName"].value_counts().head(15) plt.figure(figsize=(10, 8)) plt.barh(top_artists.index[::-1], top_artists.values[::-1], color="#1DB954") plt.title("播放次数最多的 15 位歌手") plt.xlabel("播放次数") plt.tight_layout() plt.savefig("top_artists.png", dpi=150)我当初还试图用词云来做这张图,后来放弃了。词云看着"好看",但信息量很低,难以准确对比不同歌手的占比,视觉面积也会被单字长度误导。做个人数据分析,信息表达优先级永远比视觉炫技高。
如果觉得 Matplotlib 单调,可以把数据传给plotly画出互动图表,鼠标悬停能看到具体数值,适合放在博客或 HTML 报告里。但做静态 PDF 或截图时,Matplotlib 仍然是最省事的方案。
4.3 24 小时时段分布图:单日听歌节奏
时段分布使用柱状图一目了然。我把小时从 0-23 按顺序排列,数值用播放时长或播放次数均可:
hourly_play = df_valid.groupby("hour")["msPlayed"].sum() / 60000 hourly_play = hourly_play.reindex(range(24), fill_value=0) plt.figure(figsize=(14, 5)) plt.bar(hourly_play.index, hourly_play.values, color="#1DB954") plt.title("24 小时播放时长分布(分钟)") plt.xlabel("小时") plt.ylabel("播放分钟") plt.tight_layout() plt.savefig("hourly_distribution.png", dpi=150)这张图能非常明确地显示出你的一天里什么时候最"躁"。我的图上傍晚六点到九点有一个超高峰,夜里十一点到凌晨一点还有个小高峰,正午反而一片安静。看这张图时我第一反应是"原来我下班晚高峰路上听得这么多",可见数据分析真的能把生活轨迹画出来。
这三张图整体就可以组成一份小报告,配上一段文字就是可分享的作品。有兴趣的可以把图片用PIL拼成一张长图,或放到一个简单的 HTML 页面里,命名成 "我的 2024 听歌数据年报",效果不比官方给的年度总结差。
5. 我在分析过程中踩过的坑:排雷时间
5.1 时区问题差点让我的月度统计偏移整整一天
刚开始做清洗时我偷了个懒,直接pd.to_datetime(df["endTime"]),没有指定时区。结果出来之后,我看到某些天凌晨的播放记录被归到了前一天,月度统计也生生偏了几个小时。这个坑很隐蔽,因为数据本身看起来"没错"。排查方法很简单:取一条已知记录核对,比如我在某天凌晨 1 点听了一首歌,导出的endTime是"2024-01-05 01:00",但分析里它跑到了 1 月 4 日。核对时间和时区之后,我立刻改用显式转换方案。这个坑我强烈建议你在清洗阶段就提前处理,不要等分析结束了才发现整套图表日期错位。
5.2 三十秒以下的"切歌党"让艺术家榜单失了真
我一开始没有过滤短时播放,直接按歌手统计播放次数。结果排行榜第一名是一位我根本没有深度听过的流行歌手——仔细看数据,那 200 多次播放基本全是 10 到 20 秒的切歌,可能是某次随机歌单扫出来的。对我来说,这类数据只代表"我遇见过谁",不代表"我认可谁"。所以一定要先定一个能代表"完整播放"的阈值,再计算所有榜单。
5.3 导出包文件名很像,一不小心就把旧数据当新数据
Spotify 的数据导出文件名格式基本是固定的,但如果你重复申请了好几次下载,本地文件夹里可能会出现StreamingHistory0 (1).json这种重名文件。我因为误读旧文件导致分析结果里多出整整一年的历史,一度以为自己穿越了。后来养成了固定命名规范:下载后立刻重命名为streaming_history_2024.json,并在脚本开头打印文件解析行数,用来做 sanity check。这个习惯之后帮了我大忙。
5.4 API 拿不到历史数据的另一个隐蔽坑
如果只用 API 做分析,会发现recently-played接口最多只能返回最近 50 条记录,且通常有严格的速率限制。假如你想用 API 反向重建长期历史,基本上是做不到的,接口根本不会给你完整列表。所以我的结论还是那句话:全量分析请用官方数据包,不要和 API 较劲。
5.5 歌曲名和歌手名去重:数据包里的小动作
有些歌手会改艺名、发多个版本专辑,导致同一位歌手的名字在不同记录中写法不一致(比如与不同乐团合作、feat 别名等)。我跑完 Top 榜单后手动检查过几个歌手,发现"某人"和"某人 feat. 某某"是两条独立记录。严格的分析场景下需要做规则清洗,把同一人的所有别名归并。不过这类操作容易误伤不同歌手,所以我的处理方法是:做总榜排名时保留原名;做歌手画像时再手动归并 top 若干位。
6. 进阶扩展:除了听歌记录,还能玩点什么
数据包里除了StreamingHistory,还有歌单文件。把歌单数据按收藏时间排序,可以看到自己 "歌单建设史"。我被这个数据惊艳过,因为我收藏歌单的节奏能直接映射出不同阶段的音乐追求:一开始收藏流行榜单,中期疯狂搜集电影原声,后来越来越偏向某一两个固定音乐厂牌。
另一个方向是做歌曲去重分析。对比不同年份的播放记录,看"单曲循环冠军"是哪首歌、循环播放周期长达多久。《数据发现我每年冬天都会点开某一首圣诞主题歌》,这类奇妙信息用来写个人报告非常有趣。
如果还想更深一点,可以把trackUri与 Web API 里的音频特征字段做关联,比如采集每首歌的danceability、energy、valence、tempo等窗口值。这样就能画出一张"情绪分布图":你早上听的歌能量多高、深夜听的歌能量多低。这个分析我之前只跑过一到两遍,因为音频特征 API 对每个 track 都要单独请求一次,懂 LIMIT 的人应该秒懂这有多耗时。
features_list = [] for idx, row in df_aligned.iterrows(): feats = sp.audio_features(row["track_uri"])[0] features_list.append(feats)这段没有缩略,实际运行前请务必要控制样本量,最好只对播放次数 Top100 的歌曲做音频特征采集,否则几百个请求直接会让你等哭。
数据是死的,分析是活的。把 Spotify 导出的 JSON 当成一面镜子,你能看到一年里情绪的起伏、通勤的漫长、深夜的反复和无数个自我调整的瞬间。这份数据挖掘最迷人的地方,不在于图表本身多精致,而在于每个数字背后都有你自己真实存在过的痕迹。如果你也想看看自己音乐口味背后的真相,现在就登录 Spotify 账户申请一份数据包,等邮件送达的时间正好可以用来把 pandas 环境和绘图环境配好。