2026/10/11 21:55:59

用Python分析Spotify听歌数据:从隐私导出到可视化复盘

用Python分析Spotify听歌数据:从隐私导出到可视化复盘 我最早开始用Python梳理Spotify听歌数据不是因为它有多酷而是因为每次年度总结推送都只告诉我“今年你听了很多XX风格”却回答不了几个更具体的问题我到底在一天里的哪个时段最沉迷音乐每周哪一天听得最疯那些被我循环到怀疑人生的歌究竟是听完了还是每次听到一半就切走于是我去Spotify账户后台导出了自己的完整数据写了几个脚本把一堆JSON文件变成了可以反复翻阅的统计报告。这篇文章就是把这条完整流程整理出来给每个想自己动手复盘听歌记录的读者一条可以直接走的路。你会需要的东西不复杂一台能运行Python的电脑一个可以申请数据导出的Spotify账号以及一点耐心——不是代码有多难而是第一次拿到几千条听歌记录时你会忍不住翻很久。文章会按“数据导出→清洗→统计→可视化→API补充→避坑”的顺序展开代码和数据字段我都会尽量写完整适用于Python基础刚入门、想拿真实数据练手的朋友。1. 先弄清Spotify能导出哪些数据隐私文件与API的边界做数据分析之前最忌讳的就是还没看清数据长什么样就开始写代码。我身边不少人一上来就搜“Spotify API”以为只有API才能拿到播放历史折腾半天发现权限申请麻烦、接口限制又多其实账户后台本身就提供了一份完整的历史数据包只是名字比较隐蔽。1.1 从账户申请一份完整的数据包申请入口在账号设置里的“Privacy settings”隐私设置找到“Download your data”这类选项提交请求之后Spotify会通过邮件通知你数据准备好了。这个打包过程通常要等几天有的账号甚至会等一两个星期所以建议先申请、再慢慢准备分析环境。解压出来你会看到一堆文件和文件夹不同账号稍有差异但常见的有这些文件/目录内容StreamingHistory0.json、StreamingHistory1.json…听歌记录/播放历史按时间顺序切分Playlist1.json、Playlist2.json…你创建和收藏的歌单Userdata.json账户基本信息、设备信息等Follow.json你关注的人和被你关注的人Payment…和付费相关的信息部分账号有重点永远是StreamingHistory开头的JSON文件它们记录了你每一次播放行为的结束时间、艺人、歌曲名和播放时长。不同版本的导出文件数量和字段可能不完全一样有的账号可能只有一个StreamingHistory0.json有的可能有20多个原因通常是历史数据太长一张表装不下。1.2 播放历史文件的结构到底是什么样打开任意一个StreamingHistory文件里面其实是一个JSON数组每条记录长这样[ { endTime: 2024-03-15 20:14, artistName: some artist, trackName: some track, msPlayed: 183604 } ]四个字段的含义分别是endTime播放结束的时刻格式是“年-月-日 时:分”不包含秒和时区信息。artistName艺人名。trackName歌曲名。msPlayed这首歌唱了多少毫秒注意单位不是秒换算成秒要除以1000。这里有一个容易忽略的细节Spotify记录的是播放结束时间不是开始时间。这意味着如果一首歌播放时间很短说明大概率是在快进之后切走的而不是真正听完了。分析习惯时我会把短时间播放单独拎出来处理这部分后面细说。另外新版导出文件里如果包含播客或有声书记录字段会不一样比如可能出现episode_name、show_name之类的字段artistName可能为空。如果你的数据里混了播客分析歌曲之前最好先把它筛掉不然统计“艺人”时会出现一堆空值。1.3 隐私导出数据和Web API各自能拿到什么很多教程一上来就教你去注册Spotify开发者应用然后用Spotipy调用Web API这确实能拿到一些很炫的指标比如音频特征、歌曲总时长、热度值等但Web API拿不到你自己的历史播放记录。想拿到完整历史必须靠隐私数据导出。这两个渠道不是替代关系而是互补关系。维度隐私数据导出Spotify Web API是否需要开发者账号不需要需要注册应用并创建凭据是否免费免费免费但有配额限制能否拿到历史播放记录能完整列表不能直接拿完整历史能否拿到音频特征不能能如danceability、energy能否拿到歌曲总时长不能能duration_ms拿到的是自己的数据是取决于授权范围我的建议是先把隐私数据导出这份免费的“大礼包”榨干用得差不多了再考虑要不要接API补细节。这篇文章前面几节完全不依赖API后面介绍补充分析时才引入Spotipy。2. 环境准备与JSON清洗把零散记录变成规整表格拿到了文件和数据结构接下来就是干活环节。我习惯把每一类分析都做成独立脚本但清洗是共用的第一步所以我会先把所有JSON统一读进来转成Pandas的DataFrame。2.1 准备Python环境和依赖库建议直接用Python 3.9以上的版本我用的是3.10后面版本的兼容性只会更好。需要安装的库不多最基本的是pandas、matplotlib画热力图的话再加一个seaborn最后如果你想做进阶API分析还需要spotipy。pip install pandas matplotlib seaborn spotipy2.2 一次性读取所有StreamingHistory文件由于历史记录可能被拆成多个文件我不会手动去数到底有几个而是用glob把StreamingHistory开头的JSON都搜出来挨个读再合并import glob import json import pandas as pd frames [] for file in glob.glob(StreamingHistory*.json): with open(file, encodingutf-8) as fp: data json.load(fp) frames.append(pd.DataFrame(data)) df pd.concat(frames, ignore_indexTrue) df[endTime] pd.to_datetime(df[endTime]) df[play_seconds] df[msPlayed] / 1000 df[play_minutes] df[play_seconds] / 60 print(df.shape) print(df.head())这里我做了一个很关键的转换把毫秒数转成了秒和分钟。因为在后面所有计算里用分钟看收听时长比较直观直接看毫秒很容易产生错觉尤其是你看到一亿多毫秒的时候第一反应是“我怎么可能听了这么多”换算成分钟后才缓过来。2.3 清洗逻辑过滤播客、处理大小写、切歌阈值拿到DataFrame之后第一步是先把歌曲和播客分开。我的判断逻辑很简单如果artistName字段是空的就认为它不是普通歌曲记录music_df df[df[artistName].notna()].copy()接着处理艺人名的大小写问题。同一首歌曲可能因为版本、大小写差异被当成两条记录比如“Adele”和“adele”这种虽然真实数据里未必出现但为了保险起见我都会生成一个标准化字段music_df[artist_clean] music_df[artistName].str.strip().str.lower()清洗之后还得想清楚切歌阈值。什么叫“切歌”没有统一答案。我个人的习惯是30秒以下的播放记录基本不是认真听歌可能是切歌预览、误触播放、播客快进等等。所以我分析“完整的收听习惯”时会把短播放单独放一边short_play music_df[music_df[play_seconds] 30] long_play music_df[music_df[play_seconds] 30]但这不是说短播放没用。恰恰相反切歌行为本身就是一种信号它可以说明哪些歌容易让你不耐烦。所以我会保留短播放在统计“切歌率”时再拿出来用而不是直接丢掉。3. 基础统计总时长、最爱艺人和月度趋势数据洗干净之后开始回答最开始那几个朴素的问题我到底听了多少谁是我听得最多的艺人哪个月最沉迷3.1 到底听了多少首歌、多少分钟最简单的统计用几个聚合函数就能完成。这里我建议区分“播放次数”和“不重复歌曲数”因为循环播放是常态前者会远大于后者total_plays len(music_df) unique_tracks music_df[[artist_clean, trackName]].drop_duplicates().shape[0] total_minutes music_df[play_minutes].sum() total_hours total_minutes / 60 print(f播放总次数: {total_plays}) print(f不重复歌曲数: {unique_tracks}) print(f累计收听: {total_minutes:.0f} 分钟约 {total_hours:.0f} 小时)拿我自己的数据举例子第一次算出来的时候我还是挺意外的播放总次数过了一万多但真正不重复的歌只有两千出头说明我的日常确实高度依赖少数几首歌来回放。如果你算出来的不重复歌曲数很多说明你更偏向探索新歌而不是循环。3.2 找出Top艺人和Top歌曲Top艺人排行应该是最没有惊喜感的统计但也是很多人最想看的。我会按清洗过的艺人标准名分组汇总播放总分钟数然后排序取前20top_artists ( music_df.groupby(artist_clean)[play_minutes] .sum() .sort_values(ascendingFalse) .head(20) ) print(top_artists)Top歌曲的逻辑类似但要注意同名歌曲存在不同版本甚至不同歌手的可能所以我会用“艺人 歌曲名”组合键music_df[track_key] music_df[artist_clean] - music_df[trackName].astype(str) top_tracks ( music_df.groupby(track_key)[play_minutes] .sum() .sort_values(ascendingFalse) .head(20) ) print(top_tracks)这个Top榜单其实只是入口真正的价值在下一步拿到榜单里排前面的歌之后去检查这些歌是被完整听完的还是频繁被切歌。有些歌排进Top纯粹是因为播了一会儿就被切循环了几十次不代表你真的喜欢它。后面我会用Spotipy补上歌曲总时长然后算“听完率”那才是更有趣的指标。3.3 月度趋势哪个月最沉迷很多人以为自己的收听量是均匀分布的实际上波动特别大。把endTime设为索引后用resample按月聚合就能得到月度总分钟数music_df_time music_df.set_index(endTime) monthly music_df_time.resample(ME)[play_minutes].sum().sort_values(ascendingFalse) print(monthly.head())注意这里resample的“ME”是pandas新版本中“月末”的写法旧版本用的是“M”。如果你用的是较老版本可能需要改成monthly music_df_time.resample(M).sum()具体以你运行环境报错信息为准。画出折线图之前请先确认你的endTime是否有时区偏差否则月度聚合结果可能整个偏移一两天个别月份出现离奇的低值或高值。时区的影响我在后面会专门讲这里先留个心眼。4. 收听习惯分析星期×小时热力图与切歌率基础的排行榜只能回答“听了谁、听多久”但真正能反映生活方式的是“什么时候听”。我在这个环节会做一张“星期×小时”热力图它能一眼看出你是通勤型听众、深夜型听众还是上班摸鱼型听众。4.1 拆出小时与星期从endTime这个时间戳里提取两个字段一个是小时一个是星期几music_df[hour] music_df[endTime].dt.hour music_df[weekday] music_df[endTime].dt.dayofweekPandas的dayofweek返回的是0到6其中0代表周一6代表周日。这个映射很容易记混我通常会顺手建一个映射字典方便后面出图时把坐标轴标签改成“周一”“周二”这种中文weekday_names {0: 周一, 1: 周二, 2: 周三, 3: 周四, 4: 周五, 5: 周六, 6: 周日} music_df[weekday_name] music_df[weekday].map(weekday_names)4.2 画出一周收听热力图热力图本质上是一个二维透视表行是星期列是小时单元格的值是收听总分钟数。用pivot_table可以很自然地做出来import seaborn as sns import matplotlib.pyplot as plt heat_data music_df.pivot_table( indexweekday_name, columnshour, valuesplay_minutes, aggfuncsum, fill_value0, ) sns.heatmap(heat_data, cmapviridis, annotFalse) plt.xlabel(小时) plt.ylabel(星期) plt.title(一周收听热力图) plt.show()看到这张图之后你就会明白听歌习惯的差异非常明显。像我自己的数据里工作日早上8点和下午18点有两个明显的高亮条基本对应通勤时间周末则是上午十点以后整片亮起来说明周末我习惯自然醒之后才开始放歌。如果你发现自己的热力图里凌晨2点到4点还有一堆深色块那你可能需要反思一下睡眠质量或者你压根就是夜行动物。4.3 切歌率与真正的“单曲循环”热力图之外我还喜欢算一个指标切歌率也就是短播放记录在总播放记录里的占比。阈值用前面提过的30秒total_plays len(music_df) short_play_count len(music_df[music_df[play_seconds] 30]) switch_rate short_play_count / total_plays print(f切歌率: {switch_rate:.1%})切歌率高了不一定代表你没耐心也可能是你处在一个大量试听的场景里比如睡前浏览新歌、开车时快速切换电台歌单。把它和Top歌曲榜单放在一起看更有意思如果某首歌被播放了很多次但每次的平均播放时长只有十几秒说明你不是喜欢它你是习惯性地切过它。反过来一首歌播放次数不是最高但每次记录都接近完整时长那才是真正的反复爱上同一首歌。5. 更精细的指标用Spotipy补上歌曲时长与音频特征到目前为止用的都是隐私数据导出里的字段够玩一阵子但它的上限很明显没有歌曲总时长没有流派没有音频特征。如果你想进一步回答“我最常听的歌是什么节奏”“我是不是只听高能量歌曲”“切歌时切掉的到底是前奏太长还是副歌太晚”就必须接Spotify Web API。5.1 为什么导出文件里查不到歌曲总时长隐私数据导出为了控制文件体积只保留了播放行为相关的核心字段不会给你存歌曲元数据。这里的“歌曲总时长”和“音频特征”属于另一个数据体系需要通过Spotify Web API按歌曲ID或搜索关键词去查。5.2 快速跑通Spotipy首先去Spotify开发者后台创建一个应用拿到Client ID、Client Secret然后在本地设置环境变量。我习惯用一个单独的终端窗口设置避免污染全局配置export SPOTIPY_CLIENT_ID你的client_id export SPOTIPY_CLIENT_SECRET你的client_secret export SPOTIPY_REDIRECT_URIhttp://localhost:8888/callback代码里用SpotifyOAuth完成授权然后搜索歌曲就能拿到总时长和音频特征import spotipy from spotipy.oauth2 import SpotifyOAuth sp spotipy.Spotify(auth_managerSpotifyOAuth(scopeplaylist-modify-private)) def get_track_info(artist, track_name): query fartist:{artist} track:{track_name} result sp.search(qquery, typetrack, limit1) if not result[tracks][items]: return None item result[tracks][items][0] audio_features sp.audio_features([item[id]])[0] return { track_id: item[id], duration_ms: item[duration_ms], danceability: audio_features[danceability], energy: audio_features[energy], valence: audio_features[valence], }这里的search并不是百分之百精准同名歌曲、翻唱版本、Live版本都会干扰结果。拿不准的时候可以先用“艺人专辑歌曲名”搜索或者手动比对track_id的预览音频。API还有速率限制批量处理几千首歌时会遇到429错误我会加个time.sleep来缓解每隔几十个请求停一两秒。5.3 把“听完率”和音频特征合起来看拿到歌曲总时长之后就可以算真正意义上的“听完率”了。假设某一首歌的总时长是240秒你的播放记录里平均每次播了220秒那基本可以认定是认真听完的如果平均每次只有40秒那多半是进来听了个开头就撤了avg_play_seconds music_df[music_df[track_key] track_key][play_seconds].mean() playback_ratio avg_play_seconds / (total_duration_ms / 1000)结合danceability和energy还能做更细的分析。比如把每小时的平均收听energy和时间画在一起就能直观看到一天里你听歌的“兴奋度”曲线早上可能是温和的下午突然变high深夜又沉下去。这一步夹带了大量API请求建议只对Top 50歌曲做没必要把全部几千首歌都跑一遍。6. 实操中容易踩的坑时区、版本字段和隐私最后这部分我不太想叫它“踩坑记录”更像是一份写给未来自己的提醒。第一次跑完整套流程时数据处理本身没出什么大问题倒是几个和数据内容有关的坑让我花了不少时间。6.1 endTime的时区问题我的月度统计曾整体偏移Spotify导出的endTime虽然看起来是一串普通时间但它记录的是哪个时区的时间很多人会想当然认为是自己所在时区。我的经验是不同账号、不同时间段导出的文件观测到的时区基准不完全一样。最靠谱的做法是先找一个你明确记得“当时几点在听歌”的时间点去数据里比对一下能差出几小时就说明存在时区偏移。如果确认有固定偏移直接调整时间列即可。假设你的数据整体比真实时间早了8个小时那么df[endTime_local] df[endTime] pd.Timedelta(hours8)调整后再做小时和月度聚合。这个步骤很容易被忽略但它直接影响一整张热力图的可信度。6.2 多次导出的文件字段不一致我第一次拿到的是一个很老的导出包结果第二次重新导出时发现新增了几个字段比如包含播客记录。这个字段不一致会导致代码报错尤其是在你直接访问某个不存在列的时候。解决方案是在合并前先做字段对齐对缺失的列补一个空值for col in [artistName, trackName]: if col not in frame.columns: frame[col] pd.NA这样至少不会在读列时报错。至于那些新增字段除非你确实要分析播客否则忽略就行。6.3 数据包里藏着隐私别急着公开分享整个数据包里的StreamingHistory文件只是冰山一角Userdata.json里面通常包含邮箱、设备列表、支付相关信息。就算只导出StreamingHistory连续几个月的听歌记录也能还原出你的作息、通勤路线、情绪波动这在某种程度上比点击记录更私密。所以我有两个习惯第一处理完数据后把原始压缩包放到本地加密目录不放到云盘或公开仓库第二分享分析结果时只发图表截图不发原始JSON文件和DataFrame内容。我曾经看到有人为了演示把完整Userdata.json丢进代码仓库这个行为非常危险希望看到这篇文章的人都别干同样的事。最后再分享一个我自己的小习惯整套流程跑通之后我最喜欢的功能其实不是Top艺人排行榜而是“最后播放时间”分析。按endTime排序后对每首歌去重保留最后一次播放时间按年份看就能知道自己的口味是如何迁移的某一年反复听的民谣歌手隔一年再也不碰了某个曾经深夜循环的乐队现在只在早晨通勤时出现一次。这种变化比单纯的播放次数更有人味。如果你也打算动手跑一遍我的建议是先把“隐私数据导出”申请提交了等待期间再搭环境看这篇文章。拿到数据后从第2节开始一步步走不用急着上API先把基础统计和热力图做出来那种“原来我一直在这种时候听这类歌”的发现感会是你继续研究下去的最大动力。