用Python分析Spotify听歌数据:从导出到可视化全攻略
你是不是也好奇过自己每天戴着耳机刷的那些歌一年下来到底听了多少小时哪些歌手在后台数据里悄悄霸榜深夜循环的歌和白天的节奏到底差在哪我自己前阵子在整理 Spotify 的年度报告时忽然意识到一个事——这些数据 Spotify 其实都替你记着只是平时藏在设置深处。只要愿意花一晚上用 Python 把这些原始记录拉出来就能挖出一份完全属于你自己的听歌进化史。这篇博文就围绕“用 Python 分析 Spotify 听歌数据”这个项目展开从数据获取、清洗、探索性分析到进阶玩法一步步讲清怎么做、用什么工具、有哪些坑。适合有一定 Python 基础、想用真实数据练手的开发者也适合对数据分析和可视化感兴趣的爱乐人。文章里的代码和思路都基于我的实际整理经验你可以直接拿自己的导出数据照着跑一遍。1. 整体方案设计与数据来源选型1.1 两条数据获取路径的取舍做个人听歌数据分析第一步不是写代码而是想清楚数据从哪来。目前获取 Spotify 个人历史播放记录主要有两条路一种是从隐私中心直接导出官方数据包另一种是申请开发者密钥、调用 Web API 拉取最近播放记录。这两条我先后都试过感受差别很大。API 方案的优点是实时性强能拿到当前正在播放的曲目、设备状态、音频特征等还能做在线交互。但它的硬伤也很明显Spotify 的 Web API 对个人播放历史只开放最近 90 天且默认最多返回最近 50 条连续翻页实测勉强能捞到两万多条记录历史再早的就彻底看不见了。更麻烦的是每次请求都要处理 token 过期和重定向授权对只想安静看个数据的人来说中间步骤繁琐得劝退。官方导出方案则完全不同。你在账户隐私设置里提交数据导出申请后Spotify 会把一个包含多个 JSON 文件的压缩包发到你的邮箱。里面从早期注册到现在每一次播放都被记成一条结构化记录包括时间戳、歌曲名、歌手名、播放时长、跳过原因等等数据量是完整的历史全集而且不用处理任何鉴权问题解压之后直接就能读。1.2 为什么把官方导出作为主方案我个人最后选择官方导出的核心原因就三个字完整性。做个人数据分析完整的历史价值远大于新鲜的实时数据。你可以一眼看到五年前那个冬天循环了四百遍的歌也可以看到某天深夜三点还在反复切歌的焦虑痕迹这些叙事感是 API 方案永远给不了的。还有一个实际层面的考量官方导出数据是纯静态文件分析过程完全本地化不管你有没有稳定的网络环境代码写好了就能跑想什么时候复现就什么时候复现不存在接口变更导致脚本失效的脆弱问题。当然我也不是说 API 一无是处只是两条路定位不一样。你的目标如果是做一个“此刻正在播放”的桌面挂件那 API 是首选如果目标是分析“我这几年听歌品位怎么变的”官方导出的历史数据才是主料。1.3 分析目标拆解先问自己想知道什么拿到数据之前最好先列几个具体问题否则容易陷入“数据在哪我不知道、分析什么我也不知道”的尴尬。我当时的提问清单是这样的我总共有多少条播放记录总累计播放时长折合多少天播放量最高的 Top 20 歌手和 Top 20 歌曲分别是谁一周里哪天听得最多一天 24 小时里哪个时段最活跃被跳过最多的歌曲有什么共同特征我是不是总是在深夜循环某些特定类型的歌这些问题拆解出来其实就是统计频次、聚合时间序列、筛选异常值这几类基础操作。后面所有代码都是围绕这张问题清单展开的。建议你先拿出一张纸写三五个最想知道的“关于自己”的问题再动手写代码方向会比直接跑 demo 清晰得多。2. 数据格式深入拆解与预处理2.1 导出包的目录结构长什么样Spotify 发来的导出包解压后目录里通常包含以下几个核心文件夹Spotify Account Data、MyData以及若干 JSON 文件。真正的主角是StreamingHistory系列文件命名形如StreamingHistory0.json、StreamingHistory1.json如果播放量特别大可能会拆分成好几个文件。我当初第一次解压时还很天真地以为只有一个文件数了数居然有七个分片有点懵。后来查看每个文件的结构才发现它们字段一致只是按时间倒排或分片存储处理时直接合并读取即可。除此之外还有Identifiers.json设备与身份信息、Userdata.json账户基础资料、Follow.json、Inferences.json等。这些辅助文件里Inferences.json包含平台对你音乐偏好的推断作为交叉验证挺有意思但主分析集中在 StreamingHistory 就够了。2.2 读懂 StreamingHistory 的关键字段先展示一条真实的典型播放记录是什么样的{ endTime: 2024-06-15 14:32, artistName: 某独立乐队, trackName: 雨夜漫游, msPlayed: 252004 }四个字段各有含义处理时一点都不能马虎endTime播放行为结束的时间格式为本地时间字符串没有带时区信息。注意这是“结束时间”而不是“开始时间”差了几分钟会直接影响你在哪个时段听歌的聚合判断。artistName歌手名字符串跨语言场景下偶尔会出现大小写不一致或空值清洗时要归一化。trackName歌曲名字符串。真正值得留意的是部分记录里它是空的这类记录一般是电台播放、没有元数据的流或者某些一体化的播客内容。msPlayed这次播放实际持续了多少毫秒注意是毫秒不是秒。如果歌曲只放了 10 秒就被切走这里就只有 10000。这四个字段构成了全部分析的基础。可以把每条记录理解成一次“收听行为事件”而不是“歌曲主数据”后面很多洞察都来自对这个行为事件的切片观察。2.3 用 pandas 完成数据的读取与清洗数据读取本身不难难的是处理格式细节和异常值。我一次性加载所有分片文件的代码是这样的import json import glob import pandas as pd files sorted(glob.glob(MyData/StreamingHistory*.json)) frames [] for f in files: with open(f, r, encodingutf-8) as fp: chunk json.load(fp) frames.append(pd.DataFrame(chunk)) df pd.concat(frames, ignore_indexTrue) print(df.shape) print(df.head())运行后你大概率会得到一个几万到几十万行的 DataFrame这很正常。接下来做四步清洗第一把endTime转成真正的日期时间类型并且拆出小时、星期、月份等派生字段df[endTime] pd.to_datetime(df[endTime]) df[hour] df[endTime].dt.hour df[weekday] df[endTime].dt.dayofweek # 0周一 df[month] df[endTime].dt.month第二把msPlayed换算成秒和分钟方便后续统计df[sec_played] df[msPlayed] / 1000 df[min_played] df[msPlayed] / 60000第三处理缺失值。trackName为空、artistName为空、msPlayed异常这些记录要么直接筛掉要么单独存一个“异常记录”表用于交叉验证。我这里保守处理只保留三个核心字段都非空的记录避免脏数据污染 Top 榜。第四按去重的维度思考一下同一首歌同一时间被记录成两条、或者一首歌重复在多个分片里出现这种情况要基于endTime artistName trackName做唯一性判断必要时直接删除重复行。做完这些预处理你的数据就是干净的分析底稿了。整个过程看似琐碎但恰恰是这一步决定了后面所有图表的可信度偷懒不得。3. 探索性分析与可视化实战3.1 总量统计你听了多少小时音乐拿到干净数据后的第一个冲动当然是看总量。这里代码很直白total_ms df[msPlayed].sum() total_hours total_ms / (1000 * 3600) total_days total_hours / 24 print(f累计播放时长: {total_hours:.1f} 小时) print(f折合整天数: {total_days:.1f} 天) print(f累计播放事件数: {len(df)}) print(f去重后的独立歌曲数: {df[trackName].nunique()})我跑出来数字的瞬间还是挺震撼的——几千个小时换算下来等于连续听了将近 200 天不睡觉。这种总量数字放在年度报告里只是一个小色块放在自己的真实分析里才有一种被时间击中的感觉。建议你在这一步把trackName artistName联合去重算一下真正“听过但不重复的歌”有多少通常会比直觉中少很多因为大家反复循环的比例远超想象。3.2 Top 歌手与 Top 歌曲的统计陷阱接着就是最喜闻乐见的排行环节。统计方式很简单top_artists df[artistName].value_counts().head(20) top_tracks df.groupby([trackName, artistName]).size().reset_index(namecount).sort_values(count, ascendingFalse).head(20)但这里埋着一个很隐蔽的坑如果单纯按播放次数排序你排出来的“最常听的歌手”其实全是“被随机播放最多”的歌手而不是“你真正喜欢”的歌手。因为随机播放会均匀地轮一遍歌单那些被你主动跳过次数高的歌手也会被轮出高频。更公平的排序方法是考虑实际播放时长占比或者只看播放完成度超过 80% 的记录甚至加权“完整播放 1 分听到一半 0.5 分三秒切走 0”的算法。我自己最后用的改动是先过滤掉min_played 0.5的记录即听了不到 30 秒的全都记为无效再做排行。这样筛出来的 Top 20 明显更符合我的真实偏好不再是随机播放的“雨露均沾”名单。valid df[df[min_played] 0.5] top_tracks_valid valid.groupby([trackName, artistName]).agg( play_count(msPlayed, count), total_min(msPlayed, sum) ).sort_values(total_min, ascendingFalse).head(20)3.3 重复播放行为的多维分析很多人以为“重复播放”只体现在统计播放次数上其实至少可以拆出三个维度。第一是循环完播率同一首歌如果被完整播放多次说明它是你的真命天子如果是只放开头几秒就被切回来那可能只是歌单里卡了个前奏。我按msPlayed / 歌曲标准时长比例做近似估计过滤出那些完整播放且重复出现超过十次的记录得到了一个真正意义上的“无限循环名单”。第二是跨时段重复同一首歌如果出现在深夜和早晨两个完全不同的时段说明你对它没有场景绑定随时随地都能进入情绪。这种歌往往是最能代表你长期品位的歌曲。第三是月度重复趋势选某一首歌按月统计它被播放的次数画一条折线。能在同一年里出现多个波峰的歌就是你的年度“渡劫神曲”。我当时分析某个老乐队的一首歌就是这个形态春天和秋天分别冲高中间的夏天完全沉寂复现得特别准。3.4 时段偏好热力分布一天 24 小时与一周模式这一节就是画图最出效果的部分。两种视图非常直观小时分布条形图与“星期-小时”热力图。import matplotlib.pyplot as plt import seaborn as sns fig, axes plt.subplots(1, 2, figsize(14, 4)) hour_count df[hour].value_counts().sort_index() axes[0].bar(hour_count.index, hour_count.values, color#1DB954) axes[0].set_title(24小时播放事件分布) axes[0].set_xlabel(小时) axes[0].set_ylabel(播放次数) pivot df.pivot_table(indexweekday, columnshour, valuesmsPlayed, aggfunccount) sns.heatmap(pivot, axaxes[1], cmapYlGnBu) axes[1].set_title(星期-小时播放热力) plt.tight_layout() plt.show()画完你会发现自己的作息规律暴露无遗。我这边是工作日下午几乎没低点但周日中午有一段诡异的空窗说明那个时段我基本都在外面没有听歌深夜 23 点到凌晨 1 点永远高高隆起。这类图表不需要任何复杂解读一眼就能看出你的音乐生活习惯特别适合分享给朋友一起分析。3.5 播放时长分布与跳过行为画像msPlayed这个字段最值得深挖的行为学就是“什么时候你会选择切歌”。把播放持续时间按从短到长排序你会发现大多数记录集中在两个极端要么是两三秒就切掉的“秒切”要么是完整听完全曲的“善终”。以秒为单位统计占比可以直接发现自己的耐心阈值。我实测下来对不熟悉的歌自己的“切歌决策点”基本集中在 10 秒附近一超过 10 秒就大概率会继续听下去。这种行为画像很有趣仿佛能看到自己在播放器前无意识移动鼠标的肌肉记忆。cut_points [0, 3, 10, 30, 60, 120, 300, 600, 100000] labels [3秒, 3-10秒, 10-30秒, 30-60秒, 1-2分, 2-5分, 5-10分, 10分] df[range_sec] pd.cut(df[sec_played], binscut_points, labelslabels, rightFalse) range_dist df[range_sec].value_counts().reindex(labels) print(range_dist)这个分布表做完基本可以回答“我是冲动切歌型还是淡定听完型”这种灵魂拷问。4. 更深一层的音频特征与上下文分析4.1 引入官方音频特征做风格画像只靠播放记录本身很难回答“我到底喜欢快歌还是慢歌”“我听的歌偏能量还是偏舒缓”。要补充这个维度的信息就不得不提 Spotify 官方 API 里的音频特征字段包括danceability跳舞性、energy能量值、valence积极情绪、tempo节奏 BPM、acousticness原声比例等等。思路是先从历史记录里挑出高频歌曲列表比如播放次数前 200 的歌然后请求 API 获取每首歌的音频特征把播放行为数据和音频特征拼接成一个大宽表。虽然我对前面说 API 不擅长拿历史但这里它反而是最佳方案因为单曲特征查询接口按 track ID 查询只查高频歌曲不会触发限流。4.2 特征加权让播放次数成为权重拿到特征之后不是简单取平均而是要做次数加权。播放 100 次的歌与播放 1 次的歌在“你的听歌风格”里的权重理应天差地别。代码示意merged top_tracks.merge(features_df, ontrack_id) weighted_energy (merged[energy] * merged[play_count]).sum() / merged[play_count].sum() print(f按播放次数加权的平均能量值: {weighted_energy:.2f})加权之后你再对比不同年份、不同时段的加权特征就能看出自己品位的漂移轨迹。比如我是工作日中午的加权能量值明显高于深夜说明白天需要刺激晚上需要安抚。这个结论靠纯播放记录完全无法得出但“数据 特征”一拼接就出现了。4.3 设备与播放上下文还能挖出什么官方导出包里的Identifiers.json还有设备关联信息但真正场景化的是通过 API 可拿到的context字段能分辨播放行为是来自你的私人歌单、算法推荐还是单曲循环。做深度分析时可以把每首歌的播放来源类型单独分桶看自己到底是被推荐流裹挟着听还是主动在歌单里选着听。我就发现一个有意思的比例自己主动从自己歌单里播放的歌曲占全部播放次数的比例其实不到六成剩下四成都是各种自动续播和推荐救场。这个数字会让你重新审视自己到底有多大的“审美主权”。4.4 用统计结果反哺“年度总结”的生成把前面这些分析汇总成一个结构化摘要其实你就有能力生成一份完全属于自己的年度报告。字段可以包括总播放小时、Top 歌曲、Top 歌手、最活跃时段、月度最佳循环曲、平均切歌时间。将这些字段组装成一份 Markdown 文本或 HTML 报告就是远超官方报告颗粒度的个性化成果。如果你愿意还可以把这套摘要喂给大语言模型让它基于数据写一段个人性格速写。要注意的是上传公开服务前做好隐私考量推荐使用本地模型跑更有安全感。不过就算不接模型单纯把数字罗列出来也已经足够惊人。5. 项目工程化与长期运行5.1 构建可复用的分析函数库只写一次跑完就丢的脚本很快会被新的导出数据覆盖掉。想把这件事做成能反复使用的个人项目建议把核心流程封装成函数或类输入导出包路径输出结构化统计结果。def analyze_streaming_history(data_dir: str) - dict: # 加载、清洗、派生字段 # 返回统计字典 pass def build_daily_report(stats: dict) - str: # 把统计字典转成 Markdown 报告 pass这类接口稳定的小函数库以后每次拿到新导出包一行代码就能重新生产报告边际成本几乎为零。5.2 设计增量更新策略官方导出的历史数据不会自动更新每次想更新都要重新申请导出。如果你不想频繁等邮件可以设计一个增量策略首次用官方导出做全量底稿后续定期调 Web API 拉取最近 90 天的播放记录把新增记录合并进本地数据。合并逻辑要注意幂等以endTime trackName artistName为唯一键去重保证同一播放行为不会重复入库。这样的本地数据仓库才能长期保持新鲜。5.3 用 SQLite 做长期存储dataframe 只在会话内存里很爽但关了电脑就没了。长期维护个人听歌数据的正解是落到 SQLite 里。结构上拆两张表一张plays存播放事件一张tracks存歌曲与音频特征按 track_id 关联。CREATE TABLE plays ( id INTEGER PRIMARY KEY AUTOINCREMENT, end_time TEXT, artist_name TEXT, track_name TEXT, ms_played INTEGER, track_id TEXT ); CREATE TABLE tracks ( track_id TEXT PRIMARY KEY, song_name TEXT, artist_name TEXT, danceability REAL, energy REAL, valence REAL, tempo REAL );有了这个基础你随时可以写一句 SQL 查“上个月播放次数最多的歌”比每次打开 Python 加载整个 Json 要轻太多。5.4 定时产出可视化报告工程化的最后一环是自动化产出。可以用 Python 生成静态 HTML、Markdown 或图表文件再借助操作系统的计划任务macOS 的 launchd 或 Windows 的任务计划程序每周生成一份新报告放到本地目录或同步盘里。配合文首提到的邮箱导出更新整个流程几乎是“零人工干预”的自我维护系统。6. 常见问题与避坑指南6.1 高频问题速查表问题现象可能原因解决方案StreamingHistory文件读取为空未正确指定解压路径检查文件路径用绝对路径 glob播放次数统计严重偏高把秒切记录也算进去了过滤min_played 0.5再统计部分歌曲没有trackName电台或空白元数据流单独归类避免污染主分析时间全部偏移数小时endTime是按本地时间记录的保持本地时间分析不要误转 UTC同一歌单重复计算多分片存在重复记录按唯一键去重导出包过大、读取慢数据量大用pandas.read_json替代 json.load或分块读取6.2 我踩过的几个特殊坑第一个坑是可视化时中文乱码。我的歌曲名和歌手名大量含中文matplotlib 默认字体渲染不了所有中文变成了方框。解决办法是手动指定一个支持中文的字体比如在 macOS 上用Arial Unicode MS在 Windows 上用Microsoft YaHei设置plt.rcParams[font.sans-serif]才能正常出图。plt.rcParams[font.sans-serif] [Arial Unicode MS, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False第二个坑是endTime的字符串格式里没有秒但部分文件末尾带空格或多出的时区标记用pd.to_datetime解析时偶尔会出现 NaT。稳妥做法是统一先str.strip()再解析解析失败的行集中打印出来判断。第三个坑是导出包里可能会有重复文件或多层嵌套目录有的用户会遇到解压两次还多出一个内部MyData/MyData的情况。glob 路径要容忍这种嵌套直接用**/StreamingHistory*.json递归匹配更保险。第四个坑是 API 音频特征请求限流。免费开发者应用大约一分钟只能发几次请求如果高频歌曲列表有两百首歌串行请求会跑很久。要分批 限速过滤掉重复 track_id 缓存结果避免同一首歌反复请求浪费配额。6.3 数据安全与隐私提醒导出的数据包里有你的完整播放时间轴这实际上是一份非常私密的行为日志敏感程度不比位置历史低多少。不要轻易把整个压缩包传到公开网盘也不建议把原始 JSON 直接贴进在线 AI 工具。分析过程完全可以本地完成即使要接大模型总结也应只传统计聚合结果不传原始明细。7. 一些最后的实操体会按这套流程完整跑过一次之后我最大的感受是把抽象的音乐品位变成了可量化的行为序列。你不必依赖年度总结那种“统计平均数”式的归纳而可以精确到某一周、某一天的某一小时回看当时那个自己正在用什么音乐给情绪上色。过程中最值得认真打磨的还是数据预处理这一步。很多人急着画图看排行最后 Top 榜却被脏数据搅乱等于白忙一趟。先花半小时把清洗做好后面所有分析都会顺滑地在你面前铺开。如果你想更进一步还能把分析维度扩展到歌词情绪、BPM 分布、不同流派的更替周期甚至把多个人的数据放在一起做对比看看朋友之间口味的重合度。总之站在这份历史数据之上想挖的方向几乎无穷无尽。希望这篇文章能成为你动手打开自己听歌数据的第一块跳板。