☰
从Spotify导出数据到Python可视化:打造个人听歌报告
2026/10/13 7:59:32 网站建设 项目流程

1. 项目整体思路:从数据导出到听歌报告

先说结论:Spotify的隐私设置里藏着一个“下载我的数据”的入口,它会把你账户里所有的听歌历史打包成一份JSON文件发到你的邮箱。这其实就是整个项目的基础原料——一份记录了你过去几个月甚至几年每一次播放行为的原始日志。

我接触这个项目是因为一次很偶然的契机。某天在群里看到有人晒自己的听歌年度报告,评论区有人问“这种报告能不能自己做”,当时没太当回事。后来一位朋友抱怨说,他在不同平台切换之后,发现自己的听歌口味变得很杂,想回溯一下自己到底在什么时候开始大量听某种风格的歌,结果翻遍各个App都只能看到“最近一周”的数据。这事启发了我:既然平台不给我们完整的历史洞察,那能不能把数据导出来自己分析?

答案是肯定的,而且实现路径很清晰。你只需要做三件事:

  1. 从Spotify导出个人数据(JSON格式,含每一次播放的歌曲、艺人、专辑、时间、时长、播放来源等字段)
  2. 用Python做清洗、统计、聚合
  3. 把结果可视化,生成一份自己的“听歌数据报告”

这个项目解决的核心痛点是:流媒体平台只给你看品牌方想让你看的数据(比如“年度歌单”),却不给你原始明细。哪怕你的听歌记录在你自己的账户里,法律和产品逻辑上都属于你,但日常界面上就是找不到历史趋势。而自己动手导出分析,明细数据就全部掌握在手里了。

这个项目适合谁来参考?三类人:

  • 已经是Spotify深度用户、想知道自己真实听歌规律的人
  • 正在学Python数据分析、需要一个不那么枯燥的真实数据集来练手的人
  • 对数据隐私和“个人数据主权”敏感、希望拿回自己数据控制权的普通用户

我下面的内容会从数据导出讲起,一路走到清洗、分析、可视化,最后附带一套踩坑实录。所有操作都在本地完成,不需要联网调用任何付费接口,前提是你能登录自己的Spotify账户。

2. 数据获取:两种主流路径与取舍

2.1 路径一:通过Spotify官方API拉取近期数据

Spotify为开发者开放了一套Web API,其中有一个接口可以返回用户的近期播放记录。用Python发一个GET请求,带上OAuth Token,就能拿回最近90天左右、最多跨50条记录的数据。流程大概是:

pip install spotipy

然后写一个脚本,走授权码流程,拿refresh_token,最后请求播放历史接口。这里给一个极简示例:

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) for item in results["items"]: track = item["track"] played_at = item["played_at"] print(track["name"], track["artists"][0]["name"], played_at)

这条路线的优点是接入快、字段语义干净,返回的数据已经帮你做好了归一化,歌名、艺人ID、专辑ID都是标准格式,做联动分析会很方便。缺点是API只给近90天的数据,而且recently-played接口存在去重逻辑——同一首歌连续听多次,可能只返回一条记录。如果你想分析“过去一年我到底听了什么”,这条路基本走不通。

2.2 路径二:提交账户数据导出申请,完整拉取历史

Spotify官方支持用户下载自己的完整账户数据,路径是:账号设置 → 隐私设置 → 找到“Download your data” → 选择你要的目录(默认全选即可) → 点击申请 → 等邮件。数据准备时间从几小时到几天不等,邮件会带一个下载链接,点进去能拿到一个ZIP压缩包。

我个人实测,数据量大的人(几年连续使用、数万条播放记录)等待时间明显更长,最长的一次我等了4天。压缩包里通常包含:

  • endsong_0.json、endsong_1.json……(按时间段切分的听歌历史)
  • Spotify Account Data目录(账户信息、支付信息等,与听歌分析无关)
  • Your Library目录(收藏的歌曲、艺人、专辑,可作为补充分析素材)

这个路径最大的优势是全量数据、原始日志。每一条记录不仅包含播放的歌,还包含播放时长、是否手动播放、播放平台、连接国家、设备信息等字段。缺点是等待时间长、数据量大之后解析有点麻烦,而且格式是半结构化的JSON嵌套,新手第一次打开可能有点懵。

我的建议很明确:如果你只想做个快速Demo,用API路径;如果你是认真想做一份完整的个人听歌报告,必须走数据导出这条路径。后面的分析全部基于导出数据来写,因为这里面才藏着真正有趣的细节。

注意:申请数据导出时,有些选项是默认勾选的,比如“账户信息”“支付信息”,这些和听歌分析无关但会拖慢处理速度。如果你只关心听歌历史,只勾选“Streaming History”或名为“播放历史”的选项即可,能明显缩短等待时间。

3. 数据结构拆解:读懂JSON里每一行是什么

3.1 核心字段与单位陷阱

解压ZIP之后,endsong_*.json是核心文件。每个文件是一个JSON数组,数组里每个元素对应一次播放事件。以我清洗时遇到的真实字段为例,一条记录长这样:

{ "ts": "2024-03-15T08:32:11Z", "username": "someone", "platform": "android", "ms_played": 234891, "conn_country": "US", "ip_addr_decrypted": "x.x.x.x", "user_agent_decrypted": "okhttp/4.9.2", "master_metadata_track_name": "Blinding Lights", "master_metadata_album_artist_name": "The Weeknd", "master_metadata_album_album_name": "After Hours", "spotify_track_uri": "spotify:track:xxxxxx", "reason_start": "clickrow", "reason_end": "trackdone", "offline": false }

一眼看过去全是机器日志级别的字段,但大部分字段对分析是有价值的。我整理了一个“重点字段速查表”,方便你读的时候对照:

字段名含义分析价值
ts播放开始时间(ISO 8601格式,含时区)时间维度分析的基础
ms_played本次播放持续毫秒数计算真实收听时长,过滤无效播放
master_metadata_track_name歌名核心分析维度
master_metadata_album_artist_name艺人名统计喜欢的艺人
master_metadata_album_album_name专辑名分析专辑偏好
spotify_track_uri歌曲唯一URI精确去重、跨平台关联
reason_start播放发起方式(手动点击、自动播放等)区分主动听歌和被动背景播放
reason_end播放结束原因(播完、手动跳过等)判断“听完率”
platform播放设备平台(Android/iOS/Web等)分析设备使用习惯
offline是否离线播放判断收听场景

这份表里我特别想强调两个陷阱,几乎每个第一次处理这组数据的人都会踩:

陷阱一:ms_played的单位是毫秒,不是秒。我见过有人直接把ms_played除以1000当秒数用,结果算出来一首歌播放了400秒也没发现不对。一定要先除以1000转成秒,再除以60换算成分钟。

陷阱二:ts字段带时区偏移。大多数情况下Spotify给你的是UTC时间(以Z结尾),UICN和真实本地时间差了好几个小时。如果你直接把字符串扔给Pandas解析而不管时区,后面按小时统计的图就会整体偏移,做昼夜习惯分析时会出大笑话。这一步处理不干净,后面所有时间相关分析都会失真。

3.2 文件组织方式与批处理思路

endsong_0.json到endsong_N.json之间,文件数量取决于你的数据量大小。Spotify官方切分的逻辑很简单:每个文件控制在约20MB以内,超了就另起一个文件。所以数据量越大,文件越多。

读取时不能只读某一个文件,正确做法是把目录下所有endsong_*.json全部读进来再合并。我常用的一段代码是这样的:

import json import glob import pandas as pd all_records = [] for file_path in glob.glob("data/endsong_*.json"): with open(file_path, "r", encoding="utf-8") as f: records = json.load(f) all_records.extend(records) print(f"已读取 {file_path},包含 {len(records)} 条记录") df = pd.DataFrame(all_records) print(f"合并完成,共 {len(df)} 条原始记录")

这段代码有个隐藏要点:encoding="utf-8"一定要显式写。部分机器导出的JSON文件带BOM头,直接默认编码读取会报UnicodeDecodeError,加上utf-8-sig作为备选编码有时更保险。如果你发现报错,改成encoding="utf-8-sig"再跑一次就好。

4. 数据清洗:三块最难啃的骨头

拿到原始DataFrame之后,直接开跑统计你会发现结果非常“脏”,榜单里全是奇怪的东西。我把清洗拆成三步走,每一步都有明确目的。

4.1 时间戳与时区:做了这一步,后面的时间分析才可信

ts字段的标准化处理是整个清洗环节里最基础的,也是最容易出问题的。原始字段是带时区的ISO字符串,比如2024-03-15T08:32:11Z。Pandas的to_datetime能直接解析,但默认会保留UTC信息,后续聚合时如果不处理,本地时间的统计就会错位。

我是一个习惯把所有时间统一转成本地时间的做法:

df["ts"] = pd.to_datetime(df["ts"]) df["ts_local"] = df["ts"].dt.tz_convert("Asia/Shanghai") # 根据你所在的时区来 df["hour_local"] = df["ts_local"].dt.hour df["weekday_local"] = df["ts_local"].dt.dayofweek df["month_local"] = df["ts_local"].dt.to_period("M")

转成本地时间之后,再抽取出小时、星期、月份三个列。后面统计“我几点最爱听歌”“周几听歌最多”“每月听歌总量变化”,全部基于这三个清洗之后的新列。

4.2 短播放记录:没有它,你的榜单会被“误触”淹没

手机在口袋里误触播放、隔壁小孩乱点了一下、试听三五秒就切走——这些记录在原始数据里大量存在。如果不加过滤,所有统计维度都会被污染。你可能会看到某首歌只有3秒播放时长却出现在“播放次数最多”榜单的顶部,这显然没有意义。

我的处理策略是保留规则和丢弃规则并举:

# 保留收听时长超过30秒且不是offline误触的记录 df = df[df["ms_played"] >= 30000].copy()

30秒这个阈值不是拍脑袋定的。主流流媒体平台计算有效播放大多以30秒为界,少于30秒通常不计入版权方的有效播放量。按这个规则过滤之后,榜单会立刻“显形”——真正长听的歌浮上来,误触的都沉下去。

如果是做“听完率”分析,还要结合reason_end字段来判断。reason_end等于trackdone表示自然播完,等于trackdone以外的(比如fwdbtn表示点了下一首)说明用户主动切歌了。可以算一个“完播率”指标,看着很高其实没意义的歌会是分析中特别有趣的反差点。

实操心得:过滤阈值不能一上来就定死,建议先跑一遍不同阈值的对比。比如分别按10秒、30秒、60秒过滤,看Top20榜单变化。你会发现30秒和60秒的Top10差异很大,而10秒和30秒差异不大。找到那个“突变点”,再用那个阈值。

4.3 重复数据与URI去重:合并多个文件时的隐藏问题

多文件合并之后,天然存在同一条播放记录出现在两个文件里的可能性吗?实际上Spotify的切分是按时间顺序的,正常情况下不会跨文件重复。但我在处理过程中遇到了另一种“伪重复”:同一首歌一天内被你重放了三遍,三行记录除了ts不同,其余字段完全一致。

这种不能简单用drop_duplicates()去重,因为三行都是真实播放事件,只是同一首歌。更合适的做法是用spotify_track_uri做主键,通过分组聚合来统计播放次数:

track_stats = df.groupby("spotify_track_uri").agg( 播放次数=("ts", "count"), 累计收听毫秒=("ms_played", "sum"), 歌曲名=("master_metadata_track_name", "first"), 艺人名=("master_metadata_album_artist_name", "first") ).reset_index() track_stats["累计收听分钟"] = track_stats["累计收听毫秒"] / 60000 track_stats = track_stats.sort_values("累计收听分钟", ascending=False)

把“有效播放次数”和“累计收听时长”两个维度拆开,你会发现一个有趣的现象:某些歌你播放次数很高但时长很短(大概率是听到一半就切了),某些歌播放次数不高但累计时长惊人(单曲循环了几百遍)。只有两个指标合在一起看,才能真正反映你和一首歌的关系。

5. 五个值得深挖的分析维度与实现代码

清洗完成之后,数据终于变成干净的结构化表格了。这时候分析维度可以放开了想。我把自己实际做过并且觉得最有价值的五个维度列出来,按易到难排序。

5.1 Top歌曲与Top艺人:最直观的排名

这个最简单,直接用value_counts()或者分组聚合就能出结果。我习惯同时展示“播放次数”和“累计时长”两个榜单,因为前面说过,这两者会呈现出截然不同的故事。

top_tracks_by_count = df["master_metadata_track_name"].value_counts().head(20) top_tracks_by_duration = track_stats.sort_values("累计收听分钟", ascending=False).head(20) top_artists_by_count = df["master_metadata_album_artist_name"].value_counts().head(20)

一个细节是:master_metadata_album_artist_name是专辑艺人和曲目艺人的混合字段。对大多数单曲来说,它就是歌曲艺人;但对Featuring类歌曲,它可能包含多位艺人。如果你对艺人维度要求更精细,建议用spotify:artist:xxx的URI去做二次映射,这个做法比较进阶,这里不展开。

5.2 听歌随时间的波动:看出你的音乐生涯轨迹

这一块我强烈建议做,因为这是流媒体年度报告永远不会给你看的视角——月度总量趋势。

monthly_play = df.groupby("month_local")["ms_played"].sum() / 3600000 # 转小时 monthly_count = df.groupby("month_local")["ts"].count()

把每个月的播放总量画成折线图,你会看到非常清晰的“高峰月”和“低谷月”。高峰往往对应考试周、加班期、长途旅行,低谷通常对应社交密集期。我从数据里发现,换手机那几个月我的播放量会暴跌,因为旧手机没有同步离线缓存的歌单,新手机上连WiFi都不太勤快。

这部分的另一个组合维度是“每周播放规律”。周日到周六的播放分布能反映你的作息模式:工作日的峰值出现在通勤时段和晚间,周末的峰值则整体后移。

5.3 昼夜倾向与作息规律:你到底是晨型听歌还是夜型听歌

基于清洗时生成hour_local列,可以做一个24小时的播放频次统计。这是我个人最喜欢做的一个维度,因为它结果非常直观:

hour_dist = df.groupby("hour_local").agg( 播放次数=("ts", "count"), 收听时长分钟=("ms_played", "sum") ).apply(lambda x: x / 60000 if x.name == "收听时长分钟" else x) hour_dist["有效占位比"] = hour_dist["收听时长分钟"] / hour_dist["收听时长分钟"].sum()

统计结果会呈现几种典型的“听歌人格”:

  • 通勤型:早晚各一个高峰,午间有小低谷
  • 深夜型:峰值从21点开始爬升,凌晨1-2点达到最高
  • 工作背景音型:上午10点到下午17点持续高位,整体平滑

5.4 艺人忠诚度与专辑偏好:数据里藏着的“真本命”

判断“我最喜欢哪个艺人”,不能只看播放次数。一个艺人你听了200次但每次10秒就切,和一个艺人你听了80次但每首都完整播完,后者才是真正的“本命”。我构造了一个“忠诚度指数”:

artist_stats = df.groupby("master_metadata_album_artist_name").agg( 播放次数=("ts", "count"), 累计时长分钟=("ms_played", "sum"), 完播次数=("reason_end", lambda x: (x == "trackdone").sum()) ).reset_index() artist_stats["完播率"] = artist_stats["完播次数"] / artist_stats["播放次数"] artist_stats["单均时长分钟"] = artist_stats["累计时长分钟"] / artist_stats["播放次数"]

用“完播率降序 + 播放次数不少于50次”过滤,你会发现自己的忠诚度榜单和年度报告完全不同。年度报告倾向给你推“播放次数最多的艺人”,而忠诚度榜单揭示的是“你每次点开TA的歌都能听完整”的艺人,这才是品位的真实写照。

专辑维度的分析稍微复杂一点,因为Spotify导出数据的历史版本不一定带专辑名,新版本才稳定包含master_metadata_album_album_name。如果你的数据没有这个字段,跳过专辑分析也不影响主体。

5.5 播放来源分析:你是在主动听歌还是被算法推着走

reason_start字段值得单独深挖。这个字段表示这次播放的发起方式:

  • clickrow:点击列表中的歌曲,主动播放
  • autoplay:当前歌曲结束后自动播放下一首
  • trackdone:上一首正常结束后衔接播放
  • appload:打开App后自动续播
  • remote:远程设备控制播放

统计一下reason_start的分布比例,可以得出一个很有意思的结论:你的听歌行为有多大比例是主动选择、多大比例是算法推荐。我的数据里有接近40%的播放是autoplay,这意味着我每天听的音乐里,有四成其实不是我“选”的,而是算法猜的。这个维度能帮你反思自己是不是正处于一个“信息茧房”里——越是被动播放,越容易陷入风格单一化。

6. 可视化呈现:把分析结果变成别人看得懂的图

分析部分生成了很多统计数据,但一堆数字只有变成图才有冲击力,也才真正像一个“个人报告”。

6.1 两张必做的图:柱状图与词云

第一张图是Top艺人柱状图。用Matplotlib画,纵向排序,颜色用统一的色系,避免花哨。别在图上加一堆没用的装饰元素,干净的数据图才耐看。

import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # 防止中文乱码 plt.rcParams["axes.unicode_minus"] = False # 坐标轴负号显示 top_artists = df["master_metadata_album_artist_name"].value_counts().head(15) fig, ax = plt.subplots(figsize=(10, 6)) top_artists.sort_values().plot.barh(ax=ax, color="#1DB954") ax.set_title("播放次数Top15艺人") ax.set_xlabel("播放次数") plt.tight_layout() plt.savefig("top_artists.png", dpi=150)

第二张是歌词/歌名词云。用WordCloud库,把所有歌曲名拼接成长字符串,按出现频率生成词云。尺寸建议用800x400,背景色白色,颜色映射用绿色系(如果你愿意,也可以自定义成任何主题色)。

from wordcloud import WordCloud all_tracks = " ".join(df["master_metadata_track_name"].dropna()) wordcloud = WordCloud( width=800, height=400, background_color="white", max_words=100, colormap="Greens" ).generate(all_tracks) wordcloud.to_file("track_wordcloud.png")

词云有个视觉陷阱:高频歌名通常都是单词很短的流行曲,比如“Stay”这种,字大但信息密度低。所以我会再做一张“艺人名词云”,把master_metadata_album_artist_name字段拼进去,效果比歌名词云更能代表口味。

6.2 小时热力图:一张图看清一周听歌节奏

如果你已经清洗好了hour_local和weekday_local,可以用pivot_table生成一个7行24列的透视表,再用imshow画热力图。横轴是24小时,纵轴是周一到周日,颜色越深代表播放量越大。

pivot = df.pivot_table( index="weekday_local", columns="hour_local", values="ts", aggfunc="count", fill_value=0 ) # 把索引从0-6改成星期一..星期日 pivot.index = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] import seaborn as sns plt.figure(figsize=(14, 4)) sns.heatmap(pivot, cmap="YlGnBu", linewidths=0.5) plt.title("一周听歌热力图") plt.xlabel("小时") plt.ylabel("星期") plt.tight_layout() plt.savefig("weekly_heatmap.png", dpi=150)

这张图的解读很有味道:你会在热力图上看到通勤日的双高峰(早8点、晚18点),也能看到周末晚上23点到1点的那条“深夜色带”。它本质上是把时间维度压缩到一张二维图里,信息密度比单看折线图高很多。

6.3 生成一份自包含的HTML迷你报告

图表生成之后,最后一步是把所有图和关键数字汇总到一个HTML文件里,方便存档或分享。我的做法是写一段简单的字符串拼接,把图表以Base64内嵌进HTML,这样整个报告只有一个文件,发给谁都能看:

import base64 def img_to_base64(path): with open(path, "rb") as f: return "data:image/png;base64," + base64.b64encode(f.read()).decode() html_content = f""" <html> <head><meta charset="utf-8"><title>我的Spotify听歌分析报告</title></head> <body> <h1>我的Spotify听歌分析报告</h1> <p>总播放次数:{total_count},累计播放时长:{total_hours:.1f}小时</p> <img src="{img_to_base64('top_artists.png')}" style="max-width:100%"> <img src="{img_to_base64('weekly_heatmap.png')}" style="max-width:100%"> <img src="{img_to_base64('track_wordcloud.png')}" style="max-width:100%"> </body> </html> """ with open("spotify_report.html", "w", encoding="utf-8") as f: f.write(html_content)

每次跑完这个脚本,我本地就会多出一份完整的可视化报告,用浏览器打开就能看到所有结果。这份报告可以直接作为个人年度复盘的一个组成部分,也能做成电子存档,不用依赖任何流媒体平台的年度活动。

7. 常见问题与排查技巧实录

最后这部分是实操之后沉淀下来的问题清单,每一个都是我自己真正踩过或者见别人踩过、并且定位到根因的。

7.1 问题一:读取JSON报UnicodeDecodeError,或者读出来一堆乱码

根因通常是文件编码不是标准UTF-8,或者带了BOM头。ZIP里的JSON文件虽然绝大多数是UTF-8,但不同导出时间、不同账户的编码可能不一致。

解决办法就两行:

with open(file_path, "r", encoding="utf-8-sig") as f:

如果用了utf-8-sig还是报错,可以进一步做二进制层面检查,或者用chardet自动检测编码。整体来说,utf-8-sig能覆盖95%以上的情况。

7.2 问题二:时间戳全是UTC,按小时统计结果对不上

现象是:你晚上20点听的歌,在DataFrame里显示是当天12点。这非常正常,因为ts字段带Z后缀表示UTC。如果你不做时区转换,groupby("hour")的结果整体就会偏移8小时(中国时区为例),画出来的“深夜听歌高峰”实际是下午的听歌情况。

排查技巧:先用df["ts"].dt.tz看一下当前时区信息,如果结果是UTC,立刻用tz_convert转目标时区。千万别用tz_localize,那是给无时区信息的时间添加时区用的,用错了会导致时间整体加8小时。

7.3 问题三:数据文件太大,Pandas内存耗尽或速度极慢

听歌好几年、数据几十万条的用户,直接pd.DataFrame(all_records)一次性展开确实会有点吃力。我的建议是分段处理:

  • 先分别读取每个JSON文件,单独清洗(过滤短播放记录、添加时间字段)
  • 把每个文件清洗结果存成Parquet或Pickle,最后再合并
  • 合并之后只保留分析需要的列,第一时间丢弃user_agent_decrypted这种无关字符串

这样处理之后,内存峰值可以降到整体读取的1/5左右,速度提升明显。

7.4 问题四:图表中文乱码

Matplotlib默认字体不支持中文,图上的“播放次数”会变成一格一格的小方块。解决方式就是在绘图之前加上:

plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False

7.5 问题五:申请数据导出后邮件迟迟不来

等了三天以上还没有邮件的话,大概率是导出的数据量太大,后台队列在排队。这个时候不要反复点击申请,因为重复申请会让你的请求排到队尾。正确做法是耐心等待,同时检查邮箱的垃圾邮件箱。

根据我个人体会,这个项目最迷人的地方不在于最后的图表多华丽,而在于分析过程中你会不断发现“原来我是这样的一个人”。我在跑完自己数据之后最惊讶的一件事是:我以为我很喜欢某个摇滚乐队,实际数据显示我对他们的完播率低得可怜——大部分时候是随机播放推到他们的歌,然后我迅速跳过了。而真正被我单曲循环最多的是一首很冷门的器乐作品,平时甚至没意识到。

如果你也想动手做一次,建议从一个小目标开始:先只画一张“Top20艺人”柱状图,跑通“导出-清洗-可视化”的最小闭环,再一步步加维度。数据是你的,分析工具也是免费的,整个过程全部离线完成,隐私和安全都有保障。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询