尧图精选

用Python分析Spotify听歌数据:从JSON清洗到可视化完整指南

🕒 发布时间:2026/10/2 13:41:44 📁 来源:尧图网络
1. 为什么折腾自己的 Spotify 数据我花了一个周末把自己过去两年的 Spotify 听歌记录拉下来写了个 Python 脚本跑了十几分钟得到了一堆让我自己都意外的结论我的情绪低谷期其实有明显的主旋律周五晚八点的播放列表风格和周一早上完全不是一回事还有那个我嘴上说“一般般”的乐队数据告诉我它其实被我循环了 47 次。很多人听到“分析 Spotify 数据”第一反应是官方不就有年度总结吗是Spotify Wrapped 每年给你一张漂亮的卡片但那只是平台想让你看到的东西。它不会告诉你你真正沉迷某首歌的准确次数不会告诉你某个时间段你的播放习惯有多反常更不会把原始数据交给你自己玩。而通过 Python 分析自己的 Spotify 听歌数据你能拿到的是一份完全属于自己的、可复现的、甚至可以长期追踪的个人音乐行为报告。这篇博文不假设你有任何数据分析基础只要求你装好了 Python 3.8 以上版本。我会从怎么把数据搞出来讲起再到用 pandas 清洗、用 matplotlib 做可视化每条代码都给出可以直接跑起来的完整版本同时把我踩过的坑一并交代清楚。适合想入门数据分析的 Python 新手也适合对“个人数据到底能挖出什么”这件事有好奇心的任何 Spotify 用户。2. 整体设计思路拿到数据只是开始2.1 你想从听歌数据里得到什么在动笔写代码之前想清楚“分析什么”比“怎么分析”重要得多。我见过不少人拿到数据后一股脑把所有字段打出来然后面对几万行 JSON 发懵最后不了了之。这个项目的核心思路是先明确你关心的维度再决定处理哪些字段、怎么聚合。我在这个项目里锁定了四个方向时间维度的播放偏好变化一天中不同时段的播放量分布、一周中各天的活跃度差异、一年中每月的总体播放时长走势。艺术家与曲目的集中度哪些歌手占据了我的大多数播放次数是否存在明显的“头部效应”。播放场景的间接推断结合播放时长和跳过行为能大概判断哪些歌是“真听”还是“当背景音”。个人口味的自我验证自己嘴上说喜欢的风格在实际消费数据里到底占多大比例。这套思路不只适用于个人娱乐换个数据源比如放到电商订单、阅读记录、观影历史上方法论完全通用——先定问题再取数据最后用可视化把答案“画”出来。2.2 数据源选型Spotify 官方导出 vs 第三方爬虫分析 Spotify 数据的第一步是拿到数据这一步我强烈建议走官方渠道Spotify 账号设置里可以申请导出完整的隐私数据包括你的播放历史、搜索历史、曲库、收藏列表等。平台会在几天内打包好发到你的注册邮箱以 JSON 文件形式交付。整个过程不需要写一行爬虫代码也不需要担心封号数据完整度远超任何第三方接口能拿到的范围。为什么不建议自己扒接口第一Spotify 的 Web API 虽然开放但授权的 scope 有限个人拿不到完整的完整听歌流水第二用爬虫抓自己的私有数据属于灰色操作既有合规风险也浪费精力。官方导出虽然等待时间长一点但胜在干净和全面。顺带一提导出文件里除了播放流水还有你在各个设备上产生的行为记录包含设备类型、操作系统、时间戳等字段这些在后面做场景分析时都是宝贝。如果你暂时不想等官方导出的邮件也可以先用 Spotify Web API 的 recently played 接口拿最近 90 天的数据。这个方案适合快速上手跑通整个流程但长期追踪建议还是以官方导出为主。我在后面的实操部分会以官方导出的 JSON 结构作为示例。2.3 项目环境与工具清单整个项目不需要重型依赖三个库就能覆盖主流程pandas负责 JSON 数据读取、清洗、聚合统计这是绝对的主角。matplotlib提供基础的图表绘制能力用来画时间趋势、分布直方图和条形图。seaborn基于 matplotlib 的高级封装图表默认样式更好看适合展示分组分布。此外如果你导出的是较大的历史文件建议同时安装一个jupyter或ipython交互环境。不是说必须用但交互式环境在探索数据阶段效率要高得多你可以逐行查看中间结果而不用反复执行整个脚本。pip install pandas matplotlib seaborn jupyter这是我在 Windows 和 Mac 上都验证过的安装命令Python 版本 3.9 到 3.12 均兼容。装完之后开工。3. 核心细节解析与实操要点3.1 官方导出数据的真实结构长什么样拿到 Spotify 的导出邮件后你可能面临的第一道门槛是数据文件不止一个而且命名相当口语化。我当时的导出包里包含十几个 JSON 文件其中真正和听歌行为相关的是StreamingHistory0.json有时会有多个分卷。来看这个文件的内部结构每条记录长这样{ endTime: 2024-03-15 20:45:12, artistName: 陈奕迅, trackName: 孤勇者, msPlayed: 217000 }字段只有四个但信息密度不低。endTime是这首歌播放结束的 UTC 时间artistName是歌手trackName是歌名msPlayed是实际播放的毫秒数。需要特别强调的是Spotify 只记录播放时长大于 5 秒的曲目且msPlayed不等于整首歌时长——它表示你实际听了多少比如你听了 30 秒切歌这里就是 30000 毫秒左右。一个非常容易踩的坑是endTime的时区问题。官方导出记录的是 UTC 时间而我们需要分析的是本地时区的日常作息比如“我每天晚上 11 点到底在听什么”这就必须先把 UTC 转成当地时间。下面这段代码是我在清洗阶段固定要做的import pandas as pd from datetime import timedelta # 读取数据按你自己的实际文件名调整 df pd.read_json(StreamingHistory0.json) # 将 endTime 转成 datetime 并 8 小时转到北京时间按需调整时区 df[endTime] pd.to_datetime(df[endTime], format%Y-%m-%d %H:%M:%S) df[endTime_local] df[endTime] timedelta(hours8) # 拆出年、月、日、小时、星期几等维度后面聚合全都要用 df[year] df[endTime_local].dt.year df[month] df[endTime_local].dt.month df[hour] df[endTime_local].dt.hour df[weekday] df[endTime_local].dt.weekday # 播放时长统一换算成秒便于后续阈值判断 df[msPlayed_sec] df[msPlayed] / 10003.2 清洗策略哪些记录必须剔除原始数据里充满了噪音如果不做清洗直接聚合分析你会得到错误的结论。根据我的实际踩坑经验有三个处理原则值得记下来第一剔除播放时长过短的记录。前面说过Spotify 会记录大于 5 秒的行为但 6 秒、7 秒的记录大概率是你切歌过程中的误触。这种数据放进“我常听歌曲”统计里会严重污染排序结果。我一般以 30 秒作为阈值听歌不足 30 秒就认为这次播放没有消费价值。第二决定怎么处理重复记录。同一首歌在同一天可能被播放多次有时候是你主动反复循环有时候只是播完自动下一首又跳回来。我的做法是保留所有记录但在统计“播放次数”时不做去重在统计“独立曲目数”时才去重。这两种统计口径服务于不同的结论。第三警惕同一歌名的不同版本。同一个现场版、伴奏版、重混版在 Spotify 里是多个独立 track但在你心里可能都是“那首歌”。如果要做“单曲热度 Top 10”建议先按 trackName 分组看一看到底有多少个同名变体而不是直接取最大值。清洗代码示意如下# 过滤掉实际播放时长小于30秒的记录 df df[df[msPlayed] 30000] # 备份原始记录数供对比 original_count len(df) print(f清洗前: {original_count} 条, 清洗后: {len(df)} 条, 删除占比: {(1-len(df)/original_count)*100:.2f}%)3.3 聚合统计从零散记录到结论数据清洗完成的 DataFrame 仍然是一张流水账我们需要的是可以画图、可以排名的统计结果。pandas 的groupby是这一阶段的核心工具三个典型的聚合操作按歌手和歌曲聚合播放次数与总时长# 歌手总播放时长 Top 20 artist_stats df.groupby(artistName).agg( 播放次数(trackName, count), 总播放时长秒(msPlayed_sec, sum), 平均播放时长秒(msPlayed_sec, mean) ).sort_values(总播放时长秒, ascendingFalse).head(20)按天聚合每日播放总时长# 每日播放总时长走势 df[date] df[endTime_local].dt.date daily_play df.groupby(date)[msPlayed_sec].sum().reset_index()按小时聚合时段偏好# 各小时播放记录数分布 hour_dist df.groupby(hour)[trackName].count().reset_index() hour_dist.columns [小时, 播放次数]这三个维度基本覆盖了“听的什么、什么时候在听”的全部核心信息。接下来要做的事情就是把这些统计结果变成人眼可以直接理解的图表。4. 实操过程与核心环节实现4.1 全流程脚本一步到位跑出核心图表我把自己项目中实际用到的完整流程整理成一份可以直接复用的脚本。它不追求花哨的可视化效果而是优先保证稳定性和可解释性每张图都对应分析中的一个原始问题。import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from datetime import timedelta # 设置中文字体避免绘图乱码 plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei, PingFang SC] plt.rcParams[axes.unicode_minus] False # 读取多个分卷文件如果只有一个文件列表就写一个 frames [] for i in range(0, 3): # 假设你有 StreamingHistory0~2.json try: frames.append(pd.read_json(fStreamingHistory{i}.json)) except FileNotFoundError: break df pd.concat(frames, ignore_indexTrue) # 时间转换和特征工程 df[endTime] pd.to_datetime(df[endTime], format%Y-%m-%d %H:%M:%S) df[endTime_local] df[endTime] timedelta(hours8) df[year] df[endTime_local].dt.year df[month] df[endTime_local].dt.month df[hour] df[endTime_local].dt.hour df[weekday] df[endTime_local].dt.weekday df[date] df[endTime_local].dt.date df[msPlayed_sec] df[msPlayed] / 1000 # 清洗过滤短播放 df df[df[msPlayed] 30000] print(f清洗后剩余记录数{len(df)}) # 图1歌手播放时长 Top 15 条形图 artist_top (df.groupby(artistName)[msPlayed_sec] .sum().sort_values(ascendingFalse).head(15)) plt.figure(figsize(10, 8)) artist_top.plot(kindbarh).invert_yaxis() plt.title(歌手播放总时长 Top 15) plt.xlabel(总播放时长(秒)) plt.tight_layout() plt.savefig(artist_top15.png, dpi150) # 图2各小时播放次数热力条形图 hour_dist df.groupby(hour)[trackName].count() plt.figure(figsize(10, 5)) hour_dist.plot(kindbar) plt.title(不同时段的播放活跃度) plt.xlabel(小时) plt.ylabel(播放次数) plt.tight_layout() plt.savefig(hour_dist.png, dpi150) # 图3每日播放时长折线图按月份聚合平滑一点 daily df.groupby(date)[msPlayed_sec].sum().reset_index() daily[date] pd.to_datetime(daily[date]) daily_month daily.set_index(date).resample(M).sum() plt.figure(figsize(12, 5)) daily_month.plot() plt.title(月度播放总时长走势) plt.ylabel(播放时长(秒)) plt.tight_layout() plt.savefig(monthly_trend.png, dpi150) # 图4星期偏好柱状图 weekday_names [周一, 周二, 周三, 周四, 周五, 周六, 周日] weekday_dist (df.groupby(weekday)[msPlayed_sec] .sum().reindex(range(7)).reset_index()) weekday_dist[weekday] weekday_names plt.figure(figsize(9, 5)) sns.barplot(dataweekday_dist, xweekday, ymsPlayed_sec) plt.title(一周各天播放时长对比) plt.ylabel(总播放时长(秒)) plt.tight_layout() plt.savefig(weekday_dist.png, dpi150)这个脚本跑完后你会在当前目录得到四张 PNG 图片。大部分情况下这几张图已经能回答“我喜欢什么风格”“我什么时候最依赖音乐”“我最近一年听歌习惯有没有变化”这类问题了。4.2 进阶玩法从播放足迹还原个人专属歌单真正有意思的分析往往发生在基础统计之后。我分享两个自己比较得意的进阶分析一个是“时间段专属曲目识别”另一个是“被低估歌手挖掘”。时间段专属曲目识别的思路是按“深夜、清晨、工作日白天、周末午后”这几个场景切分数据对每个场景找出该场景下播放次数显著高于其他场景的歌曲。比如我深夜场景下“City Pop”风格的歌曲播放占比极高而工作日白天听的更多是后摇和电子乐。这说明音乐消费本质上是情绪调节工具而不是单纯的“口味展示”。实现方式并不复杂def scene_label(hour, weekday): if hour 23 or hour 5: return 深夜 elif hour 10: return 清晨 elif weekday 5 and 10 hour 18: return 工作时段 else: return 晚间休闲 df[scene] df.apply(lambda r: scene_label(r[hour], r[weekday]), axis1) # 每个场景下播放次数 Top 10 歌曲 scene_groups df.groupby([scene, trackName]).size().reset_index(name次数) for scene in scene_groups[scene].unique(): top_tracks scene_groups[scene_groups[scene] scene].nlargest(10, 次数) print(f--- 场景: {scene} ---) print(top_tracks.to_string(indexFalse))被低估歌手挖掘的逻辑正好和头部统计互补我不光看总播放时长而是计算某歌手的平均播放时长。如果一个歌手的平均播放时长很高说明每次播放都听完了但总体播放次数不算高那这位歌手的歌大概率是“好听到能完整听完”的类型。识别出这类歌手后我再专门去翻他们的专辑经常能发现宝藏。4.3 参数选择背后的两个关键注意点第一个注意点是时间窗口的选择。如果你的导出数据跨越多年分析时要明确“月度走势图”的聚合口径是按自然月还是按“近 30 天滚动”。自然月聚合直观但边界不规整滚动窗口更平滑但解释成本高。我最终选择了自然月加 3 个月移动平均的组合方式既保留了原始波动又滤掉了短期噪音。第二个注意点和图表的中文字体有关。matplotlib 默认字体不含中文字符直接绘图会出现满屏方框。上面代码里我设置了Microsoft YaHei和SimHei两个备选字体但如果你在 Linux 服务器上运行还需要手动安装中文字体包否则输出图片里的中文全部会变成豆腐块。这一条的真实教训来自我在树莓派上跑同一套脚本时的惨痛经历。5. 常见问题与排查技巧实录5.1 读取 JSON 时报错或者数据行数凭空少了很多这个问题的九成原因是pd.read_json()在解析 Spotify 导出的 JSON 数组时对某些特殊字符处理不够稳定。官方导出的 JSON 里曲目名可能包含单引号、双引号甚至不完整的 Unicode 字符简单调用会有解析失败的风险。我的处理方案是先读成纯文本再交给json模块解析最后转为 DataFrameimport json with open(StreamingHistory0.json, r, encodingutf-8) as f: data json.load(f) df pd.DataFrame(data)另外如果发现清洗后数据量比预期少很多先检查过滤阈值是否设得太激进了。你可以临时把阈值改成 0 跑一遍对比各阈值的剩余行数就能定位到清洗环节的问题。5.2 画图时中文全部变成方框这个坑我在前文提过这里再给一个更彻底的解决方案。如果你试过设置rcParams仍然无效可能的原因是你的系统里压根没有对应字体。直接用下面的代码检查当前环境有哪些可用字体from matplotlib import font_manager fonts [f.name for f in font_manager.fontManager.ttflist] print([f for f in fonts if Hei in f or Song in f or Yuan in f or PingFang in f])如果列表为空说明需要手动安装中文字体。macOS 上通常自带 PingFang SCWindows 上自带微软雅黑。如果是 Linux 环境执行sudo apt install fonts-wqy-zenhei再重启 Python 进程即可。5.3 播放次数统计结果和我记忆明显不符这里有一个容易被忽略的细节Spotify 官方导出的StreamingHistory文件里只包含过去的播放历史而且有明确的隐私设置选项。如果你在账号设置里没有开启“记录播放历史行为”选项或者曾经在某些设备上用过私密会话那么这个数据就是不完全的。我看过不少网上的分析文章把个人数据当作“绝对真相”来解读实际上官方导出的数据也不是百分之百全面的——它缺少私密会话期间的行为记录也缺少在部分第三方设备上产生的聆听数据。所以我的建议是在做任何个人结论前先对一下数据的年份分布和总记录量做到心里有数。如果某个时间段明显缺失记录就别用那段时间的数据去得出任何结构性结论。5.4 文件太大导致程序内存不足老用户的完整导出文件可能包含几十万甚至上百万条播放记录直接pd.concat所有分卷确实会有内存压力。这时候没必要把全部数据一次读进内存可以按年份分批读取并处理把不需要的列直接丢弃。以我自己的数据为例导入时只保留四个核心字段内存占用下降了约 60%。usecols [endTime, artistName, trackName, msPlayed] df pd.read_json(StreamingHistory0.json, convert_dates[endTime]) df df[usecols]5.5 问题排查一张速查表为了方便你自查我做了一张极简的问题速查表基本覆盖了从拿到数据到出图全流程里最高频的四个问题场景。症状可能原因排查顺序解决方案JSON 解析报错特殊字符或不完整转义1. 数据文件是否完整下载 2. 用 json 模块解析换成json.load()方案图表中文方框系统缺少中文字体1. 检查字体列表 2. 确认 rcParams 是否生效安装 wqy-zenhei 或指定已安装字体播放次数异常偏少私密会话导致数据缺失1. 检查年度覆盖情况 2. 对比 Wrapped 统计数据接受数据局限不要强行解读脚本执行速度慢数据量大且反复 groupby1. 查看耗时瓶颈 2. 考虑缩短时间范围按年份分块处理减少不必要的列6. 写在最后这个分析项目还能怎么玩说了这么多其实整个项目的真正价值不在于那几张图而在于建立了一套“用数据理解自己”的思考方式。我在跑完这一次分析后又顺手写了一版定期运行的脚本每个月底自动拉取最近一个月的数据生成一份 PDF 报告包含播放总时长、新增歌手、最常听的专辑和一周内的时段分布。原本只是想打发周末时间结果玩成了一个持续一年的个人听歌行为追踪项目。按照我个人的体验下一步最有意思的扩展是引入歌词内容分析——把高频播放歌曲的歌词抓下来做简单的情绪词频统计看看自己的播放历史在情绪维度上呈现什么特征或者把播放数据做成 dashboard用交互式图表替代静态图片拖拽筛选不同年份和流派。再远一点的想法是把听歌数据和自己手账里的情绪打分关联起来用最简单的相关性分析看看“音乐口味波动”和“情绪状态”之间到底有没有联系。这些扩展方向技术上都不难难的是想清楚你想从数据里确认什么。你要知道你电脑里躺着的那份听歌数据可能是这个世界上最诚实的自我描述之一——它不会撒谎不会美化只会安静地记录你在每个深夜循环过的那些旋律。别浪费它。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →