最近整理个人数据的时候,顺手把Spotify的听歌记录导出了一份,然后用Python做了个相对完整的分析。说实话,这个项目属于那种“工作量不大但收获感很强”的类型,适合刚学数据分析的人练手,也适合老手快速摸清自己的音乐偏好。你不需要能访问什么内部接口,只要会基础的Python和pandas,就能跑通。
先说结论:整个流程分四步——向Spotify申请导出个人数据、拿到JSON格式的播放历史文件、用pandas清洗和统计、再用matplotlib做可视化。每一步都有坑,但都不深,认真看完这篇基本能一次跑通。下面我从项目设计讲到编码实现,再讲我实际跑数据时踩过的几个问题,最后补充一点用API扩展分析的玩法。
1. 项目拆解:先从“听歌数据”里能挖出什么
1.1 这个项目为什么值得做
很多人以为听歌数据就是“我听了多少分钟”“我最爱的歌手是谁”,但实际上Spotify导出的原始数据包含的时间、时长、曲目和歌手信息,能组合出非常多维度。比如说,你能知道自己在一天中的哪个时段最沉迷音乐,周末和工作日的听歌习惯有什么差异,单曲循环最多的歌是哪几首,甚至通过播放列表的历史记录反推自己口味的变化轨迹。
这种分析的意义不在于“看排行榜”,而在于把散落在云端的行为痕迹变成可量化的自我观察。对Python学习者来说,它又是一个真实、非玩具的数据集,包含时间解析、字符串处理、分组聚合、排序筛选这些高频操作,比翻教科书上的示例数据有代入感得多。
1.2 数据来源对比:主动导出和API接口怎么选
在做任何分析之前,第一件事是搞清楚数据从哪来。Spotify的数据获取有两种主流方案:一种是直接在账户设置里申请导出数据文件,另一种是通过官方API按需拉取。两者看着都叫“数据”,但差别很大。
主动导出走的是隐私数据下载流程,你需要在Spotify账户页面的隐私设置里找到“Download your data”,提交申请后等邮件通知。下载的文件是一堆JSON和HTML的压缩包,其中最重要的就是StreamingHistory开头的JSON文件,里面记录了每次播放的曲目、歌手和结束时间。这个方案更适合做历史全量分析,因为它能把几年前的数据都给你。
API方案则灵活,能实时查询当前播放的曲目、获取歌单详情、拉取音频特征数据,但它需要注册开发者应用,拿到client_id和client_secret,而且默认配额也只覆盖用户授权范围内的数据,历史播放记录并不能完整回溯。我的建议是:做历史回顾用导出文件,做实时扩展用API,两者不冲突。
1.3 整体分析流程设计
我把整个项目拆成了五个阶段,这样写代码和排查问题都清晰:
- 阶段一:数据获取,申请导出并解压,确认文件结构。
- 阶段二:数据导入,用Python读取JSON,拼接成DataFrame。
- 阶段三:数据清洗,处理时间字段、空值、异常时长。
- 阶段四:统计分析,按小时、星期、月份、歌手、曲目分组。
- 阶段五:可视化,生成排行榜和趋势图,输出结论。
这五个阶段不是串行的,实际情况中经常要回头改。比如我清洗完发现时间列变成了带时区的对象,导致分组统计的结果差了8个小时,又得折回去重新处理时间字段。所以你写代码的时候不必追求一次完美,先把流程跑通,再逐步优化,反而高效。
2. 环境准备与数据导入:把数据格式搞清楚
2.1 Python环境与依赖库
这个项目需要Python 3.8以上版本,核心依赖只有三个:pandas负责数据处理,matplotlib负责画图,numpy在个别计算里偶尔会用到。如果你还没安装,命令行里直接执行:
pip install pandas matplotlib numpy如果你用的是Anaconda,这三个库默认就有,连安装都省了。我在Windows环境跑通后,又在Linux服务器上跑了一次,代码不需要改动,说明跨平台没问题。建议你直接在项目目录下建一个虚拟环境,避免依赖冲突。Python虚拟环境的管理工具我用的是venv,够用,不需要上conda那些重工具。
提示:虚拟环境不是可选项。我见过很多人在全局环境里装包,结果某个库的版本升级把其他项目弄崩了。用venv隔离后,这个分析项目的依赖就锁死了,不会波及你别的项目。
2.2 StreamingHistory结构的详细解读
Spotify导出的压缩包解压后,常见的文件夹叫MyData,里面会有多个文件。我只关注StreamingHistory,它通常按照数据量拆成多个文件,命名规律是StreamingHistory0.json、StreamingHistory1.json,以此类推。每个文件内部是一个JSON数组,数组里每个对象代表一次播放记录,字段结构固定如下:
[ { "endTime": "2024-11-01 18:30", "artistName": "钢琴曲收藏家", "trackName": "River Flows In You", "msPlayed": 172000 }, { "endTime": "2024-11-01 18:33", "artistName": "Yiruma", "trackName": "River Flows In You", "msPlayed": 92000 } ]四个字段的含义非常直白:endTime是这首歌播放结束的本地时间,精确到分钟;artistName是歌手名;trackName是曲目名;msPlayed是实际播放的毫秒数。注意它记录的是“结束时间”而不是“开始时间”,这会影响你后续对时段的分析口径。我在第一次做小时分布图时就踩了这个坑,后面会专门说。
还有一点值得注意:msPlayed并不等于歌曲总长度,而是Spotify实际计算到的播放时长。用户手动切歌、断网重连、跳过前奏,都会产生各种不一致的播放时长。这也意味着你可以在分析时对时长做很多文章,比如判断哪些歌是播完的,哪些是秒切的。
2.3 数据加载与合并的完整代码
拿到文件之后,读取的逻辑很简单,但要注意路径别写死,最好让脚本自动扫描目录下的所有StreamingHistory文件:
import pandas as pd import json import glob # 扫描当前目录下所有StreamingHistory文件 files = glob.glob('StreamingHistory*.json') print(f"找到 {len(files)} 个播放历史文件") # 逐个读取并合并 all_dfs = [] for f in files: with open(f, 'r', encoding='utf-8') as fp: data = json.load(fp) df = pd.DataFrame(data) all_dfs.append(df) print(f"{f}: {len(df)} 条记录") df = pd.concat(all_dfs, ignore_index=True) print(f"合并后总记录数: {len(df)}")这段代码里有个小细节:encoding='utf-8'必须显式指定,不然在Windows中文环境下容易触发UnicodeDecodeError。还有,拼接DataFrame时设了ignore_index=True,这样行号会连续递增,后面做过滤或去重会更方便。
读完之后建议先看一眼数据长什么样,确认列名没有因为版本更新而变化:
print(df.head()) print(df.dtypes)如果一切正常,你会看到endTime是object类型、artistName和trackName是object类型、msPlayed是int64类型。这个类型分布很关键,object代表它是Python字符串,int64代表它是整数,后续所有处理都基于这个认知展开。
3. 数据清洗与核心统计:让零散记录变成可读指标
3.1 时间字段处理与时区注意项
时间处理是整个项目里最容易出错、也最影响后续分析的环节。Spotify导出的endTime字符串格式是“YYYY-MM-DD HH:MM”,没有秒,也没有时区信息。我的建议是先把字符串转成pandas的datetime类型,这样后面按小时、星期、月份分组非常方便:
df['endTime'] = pd.to_datetime(df['endTime'])这里有一个隐藏问题:endTime到底记录的是哪个时区?从我的实际数据来看,它用的是你账号当时所在位置的本地时间。如果你长期在同一个时区,那就没什么影响;如果你经常跨国旅行或开了代理节点,那么小时分布图里会出现明显的“幽灵时段”。这种情况没有完美的修正方案,因为你无法从导出文件里反推出每个时刻的真实偏移量。我的处理办法是假设绝大多数记录都来自常住时区,这个假设在统计框架下是可接受的。
时间列转成datetime之后,我习惯同时生成几个衍生列,后面能少写很多重复代码:
df['date'] = df['endTime'].dt.date # 日期 df['hour'] = df['endTime'].dt.hour # 小时 df['weekday'] = df['endTime'].dt.dayofweek # 星期几,0=周一 df['month'] = df['endTime'].dt.to_period('M') # 月份 df['duration_minutes'] = df['msPlayed'] / 60000 # 播放时长,单位分钟这里最简单也最实用的衍生列就是duration_minutes。因为msPlayed动辄十几万,肉眼完全没法读,比如172000毫秒你心算要反应几秒,但转成2.87分钟就一目了然了。后面所有“播放时长”的分析我都用这个派生列。
3.2 播放时长过滤:先把噪音去掉
真实数据里,很大一部分记录的msPlayed都非常小。比如几秒钟就切歌了,或者不小心误触播放了三秒就暂停。这些数据如果不过滤,会严重干扰你对歌曲真实热度的判断——它计数了,但并没有实际的收听价值。
我的过滤标准是:只保留播放时长大于等于30秒的记录。为什么选30秒而不是10秒?因为多数平台的“播放一次”标准是30秒,你用这个阈值算出的播放次数,和平台官方统计口径能对上。当然你也可以用60秒,看你想分析什么。如果你关心的是“哪些歌我连30秒都撑不过”,那这堆短时长记录本身就是很好的分析素材。
df_valid = df[df['duration_minutes'] >= 0.5].copy()过滤之后,数据量通常会有10%到20%的缩减。这种“丢弃”不是浪费,而是让后续分析更聚焦。同时我建议把原始df保留在内存里,别急着覆盖,因为后面做对比分析时可能还会用到。
3.3 核心指标计算:总时长、歌手画像、曲目热度
处理完数据,第一个要算的指标是“总播放时长”。这个值直观,适合用来描述个人音乐消费的总体水平:
total_hours = df_valid['duration_minutes'].sum() / 60 print(f"有效播放总时长: {total_hours:.1f} 小时")接着看歌手维度。我习惯同时算两个视角:按播放次数排名和按累计播放时长排名。这两个排名经常不一样,差异本身就是信息。比如某位歌手的歌你经常单曲循环,那它按次数排名会很高;但如果某个歌手的歌都是五六分钟的长歌,每次你也都能听完整首,那按时长排名就会更靠前。
# 按播放次数排名 artist_count = df_valid.groupby('artistName')['trackName'].count().sort_values(ascending=False) # 按累计播放时长排名 artist_duration = df_valid.groupby('artistName')['duration_minutes'].sum().sort_values(ascending=False) artist_stats = pd.DataFrame({ '播放次数': artist_count, '累计时长_分钟': artist_duration }) print(artist_stats.head(10))曲目维度的分析类似,但要考虑同名歌曲的问题。不同歌手可能有同名歌曲,所以groupby时最好同时按artistName和trackName分组,才够精确:
track_stats = df_valid.groupby(['artistName', 'trackName']).agg( 播放次数=('duration_minutes', 'count'), 累计时长=('duration_minutes', 'sum') ).sort_values('播放次数', ascending=False) print(track_stats.head(10))这一步跑完,你已经能回答“我最常听的歌手和歌曲是什么”这个最基础的问题了。但我还想多提一个指标,就是单曲循环率。它可以用“播放次数超过20次的曲目数量”占“去重后曲目总数”的比例来表示。这个比例越高,说明你的听歌偏好越固定,越不容易接纳新歌;比例越低,说明你的歌单越多元。这个指标不高深,但很有个人洞察加成。
4. 可视化与结果解读:用图表讲出你的听歌故事
4.1 图表选型与中文字体配置
统计数字能说明问题,但一张好图的信息密度往往比十个数字更高。我的可视化选型原则很简单:能简洁就不复杂,能用柱状图就不堆三层嵌套。这次项目里最常用的几个图如下:
- Top 10 歌手柱状图,横向柱子好读,歌手名字不会被截断。
- 24小时播放量分布柱状图,一眼看出夜间和高峰。
- 每周各天播放量柱状图,对比工作日和周末差异。
- 月度播放趋势折线图,展示时间跨度的变化。
做图之前必须先配置中文字体,否则matplotlib默认字体里没有中文,坐标轴的标签全部显示成方框。我的配置如下:
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = Falseaxes.unicode_minus也很关键。不设成False的话,图表里的负号会显示成乱码方块,虽然听歌数据很少涉及负数,但图例和坐标轴有时仍会触发这个问题。这两个配置放在脚本开头,全局生效。
4.2 四张最有信息量的图
第一张图是Top 10 歌手播放次数柱状图。我用的是横向条形图,因为歌手名字一般都比较长,横向排列阅读更自然:
top_artists = artist_count.head(10)[::-1] # 反转顺序,让最大的在顶部 fig, ax = plt.subplots(figsize=(10, 6)) ax.barh(top_artists.index, top_artists.values) ax.set_xlabel('播放次数') ax.set_title('Top 10 歌手播放次数') plt.tight_layout() plt.savefig('top_artists.png', dpi=150)第二张图是24小时播放量分布。这部分数据能反映你的作息习惯,比如你是夜猫子还是早鸟,午休时间是不是也在听歌:
hourly_play = df_valid.groupby('hour')['duration_minutes'].sum() fig, ax = plt.subplots(figsize=(10, 5)) ax.bar(hourly_play.index, hourly_play.values) ax.set_xticks(range(0, 24)) ax.set_xlabel('小时') ax.set_ylabel('累计播放时长(分钟)') ax.set_title('24小时播放时长分布') plt.tight_layout() plt.savefig('hourly_distribution.png', dpi=150)第三张图是星期分布。周末和工作的对比通常非常明显。有人周末听歌多,因为时间自由;有人反而工作日通勤路上听得多,周末安静下来反而不开音乐。
第四张图是月度播放趋势折线图。如果你的数据跨越两三年,这张图能清晰地展示你对音乐的热情是逐年上升还是逐渐冷却。我看自己的数据时发现,年中有一个明显的低谷,回头看那是工作最忙的几个月份,音乐消费直接腰斩。
4.3 结果解读:图表背后能看出什么
图做出来后,不要只发“哦真好看”,要学会解读。比如我在自己的数据里发现了一个很有意思的现象:周末深夜的播放量占比明显高于工作日。这说明我的听歌行为不只是“通勤时段”,还承担着放松助眠的功能。
另一个解读维度是累计播放时长的周期性。如果月度趋势图里出现明显的季节性波动,先别急着下结论,想想是不是因为寒暑假、年终加班季、或者某个月迷上了播客导致纯音乐时间被挤占。这种观察不一定准确,但它能引导你回到原始数据里去验证,这本身就是数据分析的正循环。
5. 常见问题与排查技巧实录
5.1 UnicodeDecodeError和中文乱码
这是Windows用户最容易踩的坑。读取JSON文件时,如果不指定编码或指定错了编码,会出现UnicodeDecodeError: 'gbk' codec can't decode byte类似报错。解决方案就是所有open操作都显式指定encoding='utf-8',读JSON用utf-8,写CSV时加上encoding='utf-8-sig'。
utf-8-sig比utf-8好在哪?它会在文件开头写入BOM标记,这样你用Excel打开CSV时中文不会乱码。如果只用utf-8,Excel默认用ANSI解析,中文就全变问号了。这个细节我吃过两次亏,第一次还以为是pandas的问题,后来才明白是编码标记的锅。
5.2 数据量大导致内存和性能问题
有些人的播放历史跨度很长,数据量可能达到几十万条记录。这种情况下,直接pd.concat拼接所有StreamingHistory文件通常还是没问题,但如果你的电脑配置比较老,每次运行脚本都要等好几秒,体验会下降。
我常用的优化手段有几种:一是在读取时就删除不关心的列,比如如果你不分析专辑信息,就不加载它;二是只保留需要的字段,尽早减小DataFrame的尺寸;三是用df_valid = df[df['duration_minutes'] >= 0.5].copy()过滤后及时把中间变量释放掉。还有一点是,如果StreamingHistory拆成了几十个文件,你可以改成循环里边读边拼接,而不是先存一个大list再concat,内存峰值会低很多。
如果你后续还要做更多计算,可以把清洗后的结果存成pickle或parquet格式,下次直接读取比重新跑一遍JSON解析快得多:
df_valid.to_pickle('spotify_history.pkl') df_loaded = pd.read_pickle('spotify_history.pkl')5.3 时间统计差8小时或13小时
这是时区问题最常见的外在表现。我在第一次跑24小时分布时,发现凌晨时段几乎没数据,高峰出现在下午5点到晚上7点,但我的真实听歌高峰明明是睡前11点左右。排查半天才发现,原始时间字段里存的是UTC,我却按本地时间处理了。后来统一把endTime转成datetime后,又用dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai')做了显式转换,数据才正常。
不过要再次强调,Spotify导出文件的endTime通常是本地时间,并不是UTC。所以遇到时间偏移问题时,不要盲目套用tz_convert,先单独打印几条原始记录,对照你自己的真实听歌时刻,确认偏移方向再动手。
5.4 播放时长出现0或者少数异常大值
有些记录msPlayed是0,说明歌曲可能改成了私密会话,或者播放被打断得非常快。另一些记录可能是几个小时的播客,或者某个直播类音频,msPlayed会比普通歌曲长很多,达到几万秒。如果这些极端值混在歌曲分析里,会导致你的“最常听歌曲”排名被播客或环境音霸榜。
我的处理方法是做一个使用场景判断:如果你只想分析音乐类曲目,可以按播放时长设置一个上限,比如超过30分钟的记录全部剔除,或者单独挑出来归类为“长音频”。按需过滤后,歌曲排名才会回归正常。
5.5 去重与重复记录
原始数据里有一个容易被忽略的问题:同一首歌曲在同一天内播放多次时,会生成多条记录,这本身是正确的;但如果你不小心用相同的处理逻辑跑了两次数据合并,会导致记录数翻倍,统计结果直接失真。我建议在脚本里打印一个“总记录数”的校验值,并且在不同阶段重复打印比对。如果发现数字异常翻倍,多半是脚本被重复执行了,或者concat时没有排除掉已读入的文件。
还有一个去重场景是同一秒钟或同一分钟内出现两条完全一样的记录,这很可能是Spotify因为网络重试机制导致的重复上报。可以用drop_duplicates()按关键列去重:
df_clean = df_valid.drop_duplicates(subset=['endTime', 'artistName', 'trackName', 'msPlayed'])去重数量通常很少,但如果有,最好还是在统计之前干掉,免得“最热歌曲”被重复计数虚高。
6. 扩展玩法:用API补充音频特征分析
6.1 spotipy接入与授权流程
本地导出数据能告诉你“听了什么、什么时候听、听了多久”,但它回答不了“这些歌听起来是什么风格、能量多高、是否忧伤”。这就要借助Spotify官方API来补全音频特征了。Python生态里最常用的库是spotipy,安装一行命令搞定:
pip install spotipy使用之前需要先去Spotify开发者后台创建一个应用,拿到Client ID和Client Secret。然后通过用户授权流程获取访问令牌。这个流程不是直接把账号密码交给脚本,而是通过OAuth协议,让用户在浏览器里确认授权,安全性其实更高。我第一次用的时候觉得繁琐,但看完流程就明白了,它本质上和你用微信登录某个网站是一个逻辑。
6.2 获取音频特征的代码示例
授权完成后,spotipy会返回一个client对象,你可以拿着歌手名或曲目名去搜索,再通过track id获取音频特征。音频特征里包括danceability、energy、valence、acousticness等数值,全部在0到1之间。
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:8080/callback", scope="user-library-read" )) # 示例:搜索一首歌并获取特征 results = sp.search(q='River Flows In You', type='track', limit=1) track = results['tracks']['items'][0] features = sp.audio_features(track['id'])[0] print(f"danceability: {features['danceability']}") print(f"energy: {features['energy']}") print(f"valence: {features['valence']}")拿到这些特征后,可以和你前面统计出的“高频曲目表”做关联,看看你反复听的歌到底偏high还是偏low、偏欢快还是偏忧郁。我跑完发现我的高频曲目里valence平均值明显偏低,这倒是符合我平时用音乐平静心绪的习惯。
6.3 扩展玩法的更多可能性
音频特征还可以组合出很多有意思的分析方向。比如把一天24小时拆成几段,分别计算你在每个时段播放歌曲的平均energy值,很可能发现早上听歌能量高、晚上能量低这种规律。或者把每周每天的valence均值画成热力图,配合星期数据看情绪波动。
接口本身还有推荐功能,能基于音乐特征生成相似曲目推荐。我知道有些朋友把这个项目做成了自动化周报,每周自动分析听歌习惯变化,然后推送到邮箱或聊天软件。这些都是后话,但说明这个项目的扩展空间很大,够你玩很久。
我在实际跑完整套流程后最有感触的一点是:分析自己听歌数据,最大的收获不是一张张图表,而是突然理解了自己很多无意识的行为模式。比如我从来没意识到自己在深夜时段播放的歌曲重复率那么高,也没想到周末中午会有一个明显的播放空白期。这些洞察不靠数据分析很难浮出水面。如果你也想试着跑一遍,建议先从导出数据、计算总时长和Top歌手开始,跑通之后再慢慢往上加东西,这个项目没有标准答案,做得越多、越像你自己。