☰
用Python分析Spotify听歌记录,生成专属音乐数据报告
2026/10/14 22:08:37 网站建设 项目流程

你是不是也好奇过:自己一年到底在音乐软件上听了多少小时的歌?周末和工作日的听歌口味是不是完全不一样?那些被你单曲循环无数次的歌,究竟是哪些?

我一直觉得,听歌数据是每个人最真实的"生活侧写"之一。它记录的不只是歌名,更是你加班到深夜的情绪、通勤路上的陪伴、健身时的肾上腺素。用Python把这些数据翻出来拆一遍,既是一次有趣的数据分析练手,也是一个重新认识自己的过程。这篇文章就从零开始,带你把Spotify的听歌记录变成一份专属的音乐报告,过程不需要多高深的编程基础,跟着思路走就能复现。

1. 数据从哪来:官方导出和API接口两条路线怎么选

动手分析之前,先解决最关键的问题:怎么把你的听歌记录弄出来。

和很多人的直觉不同,Spotify本身不提供"一键导出全部听歌历史"的按钮。要拿到数据,主流有两条路:官方数据导出(GDPR数据请求)和Web API接口拉取。两条路各有利弊,我实际用下来,体验差别还挺大的。

1.1 官方数据导出:最省心的全量历史数据

Spotify账号设置里有一个"隐私"相关的数据下载入口,提交申请后,官方会把你的账号数据打包成几个JSON文件发给你。这个过程通常要几天,少则两三天,多则一两周。等待虽久,但拿到的数据非常全:听歌历史、搜索历史、播放列表、收藏歌曲、关注艺人,甚至是你的观看记录都在里面。

对于做"听歌数据分析"这个目标来说,最核心的是StreamingHistory开头的文件。我拿到手的时候是两份,StreamingHistory0.json和StreamingHistory1.json,这是因为Spotify会在数据量达到一定规模后自动拆分成多个文件。每个文件里是数组结构,每条记录包含四个字段:

  • endTime:这首歌播放结束的时刻,精确到分钟
  • artistName:艺人名
  • trackName:歌曲名
  • msPlayed:实际播放的毫秒数

关键要理解msPlayed这个字段。它不是歌曲总时长,而是你这次会话中真正播放的毫秒数。也就是说,如果一首歌你只听了10秒就切了,这里记录的就是10000毫秒。这字段是后面判断"跳歌行为"的核心依据,千万别把它当作歌曲时长来用。

1.2 API接口路线:适合增量分析和音乐特征

相比导出,Web API走的是另一套玩法。你得先申请一个开发者应用,拿到Client ID和Client Secret,然后用OAuth流程换取用户访问令牌,之后才能调用接口拿到你的播放历史。

但这里有个硬伤:Web API的播放历史接口只能返回最近50条记录,且不提供更早的数据。所以如果你想分析的是"我从注册到现在"的全量听歌史,API这条路直接走不通。那API的价值在哪?在"增强分析"。

很多有趣的维度,比如歌曲的舞曲性、能量值、调性、和声时长占比,导出数据里根本没有。这些所谓"音频特征"只能通过API按歌曲ID去查询。我的做法是:用导出数据做主体分析,再拿歌曲的艺人名和歌名去API反查音频特征,做品味层面的深度挖掘。这两条路线不是替代关系,而是前后手配合。

下面这张表可以帮你快速判断该用哪条路:

对比维度官方数据导出Web API
历史深度全量历史,多份JSON仅最近50条
上手成本提交申请等几天,零代码需要鉴权流程,有代码门槛
字段丰富度基础播放记录、歌单列表可拿到音频特征、艺人热度
适合场景全量时间线分析品味画像、补充特征维度

我对新手的建议很明确:先走导出,把基础分析跑通,再考虑要不要接API加菜。

2. 数据结构拆解:JSON文件里藏着哪些分析资产

拿到文件后别急着写代码,先花十分钟把结构看清楚。用任意文本编辑器打开一个StreamingHistory.json,你会看到类似这样的内容:

[ { "endTime": "2023-05-01 08:15", "artistName": "某乐队", "trackName": "某首歌", "msPlayed": 216000 }, { "endTime": "2023-05-01 08:19", "artistName": "某歌手", "trackName": "另一首歌", "msPlayed": 178523 } ]

每条记录代表一次完整的播放事件。注意endTime是精确到分钟的字符串,没有时区信息,这一点后面会坑人。msPlayed是原始毫秒数,后续需要转成秒或分钟才能感知"听了多久"。

2.1 多文件合并与数据清洗

如果你拿到的不止一个StreamingHistory文件,第一步要做的就是合并、清洗。直接用Python标准库就能搞定:

import json import glob files = glob.glob("StreamingHistory*.json") records = [] for f in files: with open(f, "r", encoding="utf-8") as fp: records.extend(json.load(fp)) print(f"共加载 {len(records)} 条播放记录")

清洗阶段我习惯做三件事。第一,剔除msPlayed小于30000(即30秒)的记录,这些多半是误触播放或者试听几秒就切走的,会污染"有效收听"的分析;第二,统一艺人名的空格和大小写,因为Spotify偶尔会对同一个艺人使用不同的显示形式(这是真实发生过的,比如空格、特殊符号差异);第三,把endTime字符串解析成真正的datetime对象,方便后续按时间维度聚合。

import pandas as pd from datetime import datetime df = pd.DataFrame(records) df = df[df["msPlayed"] >= 30000].reset_index(drop=True) def parse_time(s): return datetime.strptime(s, "%Y-%m-%d %H:%M") df["end_datetime"] = df["endTime"].map(parse_time) df["play_minutes"] = df["msPlayed"] / 60000

2.2 时区问题:凌晨听歌会被记到前一天

前面提到endTime没有时区信息,这在你凌晨00:30听歌时会炸出一个大坑。Spotify在生成导出数据时,使用的是欧洲中部时间还是你的本地时间?官方文档说得含糊,社区里也吵过好几次。我自己的实测结果是:数据里的endTime与我的本地时间一致,但如果你跨时区旅行过,那段时间的记录就会偏离。

稳妥的做法是,在读取时直接把字符串解析成带时区的本地时间,或者干脆把所有记录统一转换到某个固定时区后再分析。我的建议是别过度纠结,先按本地时间解析,如果发现凌晨1点到5点的记录明显稀疏或异常,再考虑加时区偏移。

这一段虽然不起眼,却是最容易让最终图表看起来"不对"的元凶。因为按小时聚合时,如果时区偏了,你看到的高峰时段会整体平移好几个小时,导致"晚上听歌最多"变成"下午听歌最多",严重误导结论。

3. 你的听歌时间线:从总量到时辰分布的逐层展开

数据干净了,可以先做最直观的分析:你到底听了多少歌、多少小时?什么时候听得最投入?

这种分析用pandas加matplotlib就能完成,不需要额外引入重量级可视化库。我自己常用seaborn做样式微调,但核心逻辑和pandas完全一样。

3.1 总时长、日均和播放次数

先算总量。把play_minutes求和,除以60就是小时数;按天聚合就能得到日均听歌时长。

total_hours = df["play_minutes"].sum() / 60 total_streams = len(df) days_active = df["end_datetime"].dt.date.nunique() print(f"有效播放次数: {total_streams}") print(f"有效收听总时长: {total_hours:.1f} 小时") print(f"有播放记录的天数: {days_active}")

这一步的结果往往超出直觉。我第一次跑出来的总时长是400多小时,比我自己预估的翻了一倍。原因很简单:通勤、做饭、工作时后台播放的"背景音乐"占了很大比例,我们下意识不认为那是"在听歌",但数据如实记录下来了。

3.2 按小时、按星期聚合的节奏感

接下来是最有画面感的部分——你一天的听觉节奏。

df["hour"] = df["end_datetime"].dt.hour hour_stats = df.groupby("hour")["play_minutes"].sum() / 60 df["weekday"] = df["end_datetime"].dt.dayofweek weekday_stats = df.groupby("weekday")["play_minutes"].sum() / 60

画出图后你会发现,几乎每个人的听歌曲线都有清晰的波峰波谷。子弹时间:通勤两个高峰、深夜档、午休小高峰……这些波峰其实就是你作息规律的真实投影。有个朋友分析完自己的数据后跟我说,他终于知道自己为什么总觉得晚上时间不够用——数据里显示他晚上8点到11点的听歌量占全天40%,这段时间全被音乐"背景化"了,实际有效工作时间远比想象中少。

从周维度看,周末和白天的分布差异也很明显。工作日播放集中在早晚,周末则平滑展开,说明周末听歌更多是"陪伴型"的持续播放。

3.3 月度趋势和年度热度

如果你的数据跨了多年,可以按月份聚合看趋势。这一步能帮你快速回忆"去年夏天我在疯狂循环什么"。亲测最有共鸣的玩法是:把每月播放量最高的那首歌拎出来,列成一张时间线表单,每一首歌都是一个月度里程碑,看到歌名的瞬间,那个月的记忆就全回来了。

df["year_month"] = df["end_datetime"].dt.to_period("M") monthly = df.groupby("year_month")["play_minutes"].sum() / 60 monthly_top = (df.groupby(["year_month", "trackName"]) .size().reset_index(name="count")) monthly_top_idx = monthly_top.groupby("year_month")["count"].idxmax() monthly_top.loc[monthly_top_idx]

4. 谁是陪你最久的歌手:排名、跳过率和长尾效应

很多人做听歌数据分析,第一个想知道的不是听了多久,而是"我到底最爱谁"。这一节把播放次数、播放时长、跳过行为三个维度拆开,你会发现"播放次数最多的歌手"和"陪你最久的歌手"很可能是完全不同的两个人。

4.1 播放次数排名 vs 播放时长排名

播放次数排名很简单:

top_artists_count = df.groupby("artistName")["trackName"].count() \ .sort_values(ascending=False).head(20)

但还有一个更有意义的指标:累计播放时长。

top_artists_time = df.groupby("artistName")["play_minutes"].sum() \ .sort_values(ascending=False).head(20)

两者的差异很有意思。次数多但时长短的歌手,说明你经常听他们的歌,但歌曲较短或者你平均听不完;时长靠前但次数不高的歌手,往往意味着他们的歌很长,比如一些前卫摇滚、古典乐、氛围音乐,单曲播放一次就是六七分钟,累积起来非常可观。

我自己的统计结果里,次数第一的歌手和时长第一的歌手就不同。次数第一的是那些节奏明快、长度三分钟出头的流行歌;时长第一的则是个做后摇的乐队,每首歌都是8分钟以上的长篇。两种排名各自代表一种"陪伴",没有高下之分,但了解这个差异会让你对自己的听歌动机有更清晰的认识。

4.2 跳过行为:哪些歌被你"试听即弃"

跳过率的分析依赖msPlayed和歌曲实际时长的对比。标准做法是从API取歌曲总时长duration_ms,如果msPlayed小于总时长的30%,可以认为这次播放是"被跳过的"。但如果你还没接API,可以用一个偷懒的近似:msPlayed小于60000毫秒(1分钟)且不是完整播放,就视为跳过。

df["skipped"] = df["msPlayed"].apply(lambda x: x < 60000) skip_rate_by_artist = df.groupby("artistName")["skipped"].mean() \ .sort_values(ascending=False).head(15)

这个指标非常好玩。你会发现,有些歌手你是"无条件听完"的,跳过率只有4%左右;另一些歌手则常年被快进。有个反直觉的结论是:跳过率高的艺人并不代表你不喜欢他们,更可能的情况是——你在试听新歌,或者他们的歌被塞进了某些自动生成的歌单里,你连着切了好几首。

4.3 只听一次的"一夜情"歌曲

我管这个叫"音乐长尾"。把所有歌曲按播放次数分组,你会看到极端的长尾分布:绝大部分歌曲只被播放过1到3次,只有极小一群歌积累了几十次播放。

play_count_per_track = df.groupby("trackName")["play_minutes"].count() one_hit_wonders = (play_count_per_track == 1).sum()

说白了,这个数字就是你"尝鲜"的频率。如果你的一年歌单里有几百首只听了一次的歌,说明你有很旺盛的探索欲;如果你有大量反复播放的旧歌,说明你是一个偏好稳定、喜欢熟悉感的人。这类分析没有对错,但非常能反映性格。

5. 从"听了什么"到"爱听什么风格":音频特征提升分析深度

基础统计做完了,如果你还想更进一步,就得请出API。这一节利用Spotify的Audio Features接口,给每首歌算出情绪、能量、舞蹈性等指标,最终给你的听歌品味画一张像。

5.1 申请API凭证和鉴权流程

先从开发者后台创建应用,拿到Client ID和Client Secret。这一步很简单,像注册一个普通账号一样,填个应用名和描述即可。

然后写代码换取访问令牌,注意走Client Credentials流程,不需要用户授权,就能访问公开的音频特征数据:

import requests CLIENT_ID = "你的_Client_ID" CLIENT_SECRET = "你的_Client_Secret" auth_url = "https://accounts.spotify.com/api/token" auth_resp = requests.post(auth_url, { "grant_type": "client_credentials", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, }) token = auth_resp.json()["access_token"]

5.2 批量查询音频特征:注意限流和并发控制

音频特征接口按歌曲ID查询,一次最多传100个ID。问题是你的导出数据里只有艺人名和歌名,没有Spotify歌曲ID,所以中间还得先通过搜索接口去匹配ID。完整链路是:歌名+艺人名 -> 搜索接口 -> track ID -> 音频特征接口。

这里有几个必须提的坑:

  • 搜索接口对每个query有速率限制,推荐加个time.sleep(0.5)控制节奏
  • 歌名匹配可能错位,同名歌曲很多,最好拿艺人名一起校验
  • 部分老歌、小众现场版在曲库里匹配不到ID,直接跳过即可
track_ids = [] for _, row in df[["trackName", "artistName"]].drop_duplicates().head(50).iterrows(): q = f"track:{row['trackName']} artist:{row['artistName']}" r = requests.get( "https://api.spotify.com/v1/search", headers={"Authorization": f"Bearer {token}"}, params={"q": q, "type": "track", "limit": 1}, ) items = r.json().get("tracks", {}).get("items", []) if items: track_ids.append(items[0]["id"])

5.3 品味画像:散点图看能量与情绪分布

拿到一批歌曲的音频特征后,最常用的可视化是散点图。横轴是valence(情绪积极性,越接近1越欢快),纵轴是energy(能量强度),点的大小映射播放次数,点的颜色映射danceability(舞蹈性)。

import matplotlib.pyplot as plt plt.scatter(features_df["valence"], features_df["energy"], s=features_df["play_count"] * 8, c=features_df["danceability"], cmap="viridis", alpha=0.7) plt.xlabel("valence (积极程度)") plt.ylabel("energy (能量)")

每个人的散点聚集区域都不一样。有人集中在右上角,说明听歌口味偏亢奋、快节奏;有人沉在左下角,偏爱低能量、情绪深沉的音乐;也有人的散点图呈"两极化"分布——工作时听静谧的纯音乐,健身时切到高能量的电子乐。这其实就是你的生活场景在听觉上的分形投影。

还有一个值得做的:把一年十二个月的音频特征均值连成一条时间序列,看情绪积极性的起伏。某个月你听的歌突然整体变得悲伤、低能量,基本可以断定那个月你状态不太好——数据有时候比你自己更早发现情绪波动。

6. 实战中的三个大坑:我从翻车现场学到的排查经验

说了这么多顺利的部分,也该聊聊那些让我翻过车的事。这三个问题几乎每个做听歌数据分析的人都会撞上,单独拎出来给你排雷。

6.1 导出数据只有一半:StreamingHistory文件并非总是完整

我第一次分析时,只处理了StreamingHistory0.json,跑出来的总时长不到30小时,当时觉得"我怎么可能只听了这么点"。后来才发现,Spotify把数据拆成了两份文件,而我只加载了第一份。这个坑特别隐蔽,因为程序不会报错,它只会安安静静地给你一个看起来很合理、但严重偏低的数字。

排查思路很简单:先打印所有匹配StreamingHistory前缀的文件名,再打印每个文件的行数,确认加起来是不是合理量级。如果某个月突然没有任何播放记录,而你又确定那个月没弃用账号,那大概率是还有文件没加载进来。

6.2 凌晨时段的记录"神隐":时区偏移导致的幽灵缺口

有位朋友分析自己的数据,发现凌晨2点到5点几乎没有播放记录。他坚称自己是个夜猫子,天天深夜听歌,我们对着数据想了很久,最后定位到时区问题。

他的使用习惯是熬夜到凌晨两三点,但endTime记录的是欧洲中部时间,跟他所在的东八区差了6小时。所以凌晨2点在本地算是深夜,在数据里却被还原到了前一天晚上8点,导致凌晨时段像被挖空了一样。处理办法就是前面说的,先确认时区,再做整体偏移,把时间统一回本地参考系。

6.3 API请求被限流:429错误与指数退避

接API做大范围音频特征补充时,我最凶的一次直接在循环里狂发几百个搜索请求,结果是连续吃429状态码。Spotify的限流策略不是固定的每分钟上限,而是基于滑动窗口的动态限流,密集连发非常容易被临时封禁。

如果你碰到了这个错,别急着硬顶,用带重试的退避策略。在requests请求外层包一层重试:

import time from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session = requests.Session() retry = Retry(total=5, backoff_factor=1, status_forcelist=[429]) adapter = HTTPAdapter(max_retries=retry) session.mount("https://", adapter)

这个配置会让requests在遇到429时自动等待指数增长的时间后重试,实测下来非常稳。有些调用量大的人还会自己维护令牌池,但个人项目没必要搞那么复杂,退避策略完全够用。

7. 这个项目还能往哪些方向继续挖

到这里,一份完整的Spotify听歌数据分析已经跑通了。从数据获取、清洗、时间线分析、艺人偏好、跳过行为,到音频特征和品味画像,整个链路用到的都是Python数据分析最核心的技术点:JSON解析、pandas聚合、时间序列处理、API对接和可视化。这套能力迁移到任何"个人数据"分析上都能用。

最后再分享一个我最近在玩的方向:把播放数据和你手机里的位置信息、天气历史做交叉。比如"雨天最爱听的歌""在某个城市最常听的专辑""跑步时段的BPM均值",这些跨数据源的玩法才是个人数据分析真正迷人的地方。分析完自己的数据后,你会对"数据即记忆"这句话有完全不同的体会。

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

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

立即咨询