你有没有在年底翻自己的年度听歌报告时想过一个问题这些数据能不能直接导出来自己用 Python 跑一遍分析我试过几次结论是可以而且玩法比官方给的年度报告多得多。今天这篇博客就拿自己的真实导出数据从拿到原始 JSON 文件开始到生成可视化和结论完整走一遍流程。中间会穿插不少我踩过的坑以及你大概率会关心的边界场景。适合想拿 Spotify 听歌数据练手的同学也适合那种“只会用 pandas 读 csv、看到嵌套 JSON 就头大”的初级选手。照着这套思路操作你至少能回答“我到底在几点最爱听歌”“哪段时间循环得最凶”“自己是不是真的把某一首循环了两百遍”这三个问题。1. 先摸清数据从哪来两种方案各有各的取舍1.1 官方导出胜在全量历史但文件结构要先熟悉很多人不知道流媒体平台在账户设置里提供了“导出个人数据”的功能。去账户页面的数据与隐私相关入口申请一份完整的数据副本提交之后等通知邮件下载压缩包解压就能拿到自己在平台上的历史记录。这个申请过程不是秒回有时候几小时有时候好几天。我自己的经验是把它当作“沉淀式分析”的唯一数据源因为它覆盖了你注册以来的几乎所有播放行为时间跨度最长颗粒度也最稳定。打开压缩包之后不要懵里面并不是一个单文件而是一堆按类型组织好的 json、csv、html。跟播放历史直接相关的是StreamingHistory开头的文件历史越长文件越多常见的有StreamingHistory0.json、StreamingHistory1.json……直到 N。这类文件的结构非常统一每条记录只有四个字段字段名含义备注ts播放发生的时间ISO 8601 格式默认是 UTC 时间msPlayed播放时长单位是毫秒不是秒artistName演出者名称注意存在同名艺术家trackName曲目名称有时会有重名歌曲导出数据里没有歌曲总时长也没有专辑封面、音频特征、设备信息。这意味着“我完整听完了这首歌的百分之多少”这种问题官方导出文件本身回答不了需要后续去对接曲目信息库补数据。但作为“行为数据”它已经非常够用了尤其是在统计分析层面已经有 mp 的毫秒级时间戳比很多爬虫拿到的数据都干净。1.2 接口拉取适合增量数据和音频特征但有限流第二种方式是通过开发接口直接拉取自己账户的数据。使用第三方封装库比如社区里最常见的 spotipy就可以拿到“最近播放记录”“音频特征”“艺术家详情”等信息。但用之前必须先去开放平台后台创建应用拿到 client id 和 client secret。这里有一条安全红线不要把凭据硬编码进脚本里更不要把凭据写成常量提交到公开仓库。我习惯用环境变量来注入脚本里只读取os.environ.get(SPOTIFY_CLIENT_ID)。接口方案有一个硬伤它无法提供完整历史。以“最近播放”接口为例最多只能拿到最近 50 条播放记录分页上限也远够不到年度报告那种全量统计。那它存在的意义是什么答案是补足导出文件缺失的部分尤其是音频特征。导出文件没有“这首歌能量多少、节奏多快、调性是什么”但接口可以按曲目 ID 批量查询。所以我更推荐的做法是“两条腿走路”导出数据负责全量时间线接口数据负责特征和补充信息最后不管是 Excel 还是 pandas 的 merge 都能把两边合起来。两种方案的适用路径我放在一起对比一下对比维度官方导出接口拉取历史完整性全量覆盖只保留最近 50 条左右音频特征没有有按曲目批量查询数据延迟申请需等待有延迟实时获取成本零门槛需要申请开发者凭据适合场景年度/月度全量分析增量监控、特征分析、歌单构建1.3 一个更优的合并思路时间线走导出特征走接口如果只是玩票只分析导出文件就够但如果想让分析跑得更深我强烈建议把两边数据结合起来。具体做法是先把导出文件整理成一张大表得到每天、每小时的播放行为再把出现的曲目统一去重去调用接口批量取音频特征最后以曲目 ID 或者“歌曲艺术家”作为关联键把特征合并回主表。这样既不会因为接口限流拿不到全量数据又能让后面的分析覆盖节拍、情绪、能量等维度。这一步也是我看过很多半途而废的分析项目失败的原因他们一开始就想用接口拉全部历史结果发现限流严重或者发现接口只给最近 50 条最后拿到一份根本没有分析价值的数据。所以不要偷懒先把官方导出流程走通再去碰接口。2. 数据清洗把一堆 JSON 变成一张能分析的表格2.1 多文件合并先写个健壮的拼接脚本拿到多个StreamingHistory文件之后最容易想到的方法是手动复制但这不是该做的事。正确姿势是写几行 Python用glob批量匹配文件再用json逐个读取。这里不需要太复杂的逻辑但要注意每个文件都可能有不同大小的数据集所以最后要ignore_indexTrue重新编号索引。import glob import json import pandas as pd frames [] for file_path in sorted(glob.glob(MyData/StreamingHistory*.json)): with open(file_path, r, encodingutf-8) as f: data json.load(f) frames.append(pd.DataFrame(data)) df pd.concat(frames, ignore_indexTrue) print(df.shape) print(df.dtypes)这里多提一句StreamingHistory文件为了兼容老系统字段名一直是很朴素的artistName、trackName没有用下划线命名。你可以在第一次读入后顺手统一改成artist_name、track_name之类的列名后面写聚合代码会舒服很多。另外如果输出的结果有 0 行先检查 glob 的路径对不对别纠结后面的分析逻辑。2.2 时间戳换算最容易翻车的一道坎官方导出文件里的时间戳是 UTC 时间。如果你处于东八区直接对ts做dt.hour统计那你的凌晨两点就会变成前一天晚上的 18 点整个“熬夜热力图”会完全错位。这个问题我踩过一次当时看到热力图里下午四点到六点是收听高峰还以为自己是什么健康作息标兵实际那是我凌晨两点到四点的真实写照只是时区没换算。正确的做法是用 pandas 先转成带时区的时间再转换到你所在的时区然后才提取小时和星期字段。下面这段代码我把“原字段”和“本地时间字段”都保存下来方便后面做时间区间筛选import pandas as pd df[ts] pd.to_datetime(df[ts], utcTrue) # 把 UTC 时间换算到本地时区这里以 UTC8 为例 df[local_time] df[ts].dt.tz_convert(Asia/Shanghai) df[hour] df[local_time].dt.hour df[weekday] df[local_time].dt.weekday # 0周一, 6周日 df[date] df[local_time].dt.date细心的人可能会问tz_convert的Asia/Shanghai是什么就是一个普通时区名自己所在时区不在这个列表里的可以用pytz.all_timezones查一下或者直接使用timezone(timedelta(hours8))这种相对偏移。关键在于“先标 UTC再转本地”别直接对ts字符串做字符串替换去加几个小时否则不同日期之间的夏令时问题迟早会冒出来。2.3 零长度播放记录不删会严重拉低平均时长导出数据里有一种很常见的脏数据msPlayed等于 0。看起来像是“没听就结束”实际是音频在后台被拉起、被唤醒预加载、或者设备在锁屏状态下发生了错误播放。这种 0 毫秒记录如果保留在表里会让“平均播放时长”这种统计被拉得极其难看甚至在聚合时把它们当成一次完整的播放。所以在清洗阶段我会直接过滤掉msPlayed 0的记录。df df[df[msPlayed] 0].copy()另外一个容易被忽略的点是“短时长播放”例如msPlayed只有几秒。它可能是用户手动切歌也可能是误触。做总体趋势分析时可以先保留但做“单曲循环率”分析时要把低于 5 秒的记录单独拆出来观察否则循环率会被非真实播放刷上去。2.4 构造辅助列让后续聚合少写十行代码清洗完成后我会顺手生成一组辅助列播放时长秒、年-月、年-周、播放日期、歌曲唯一键。这样做的好处是后面所有分组聚合都不需要再反复写pd.to_datetime和字符串拼接。df[play_seconds] df[msPlayed] / 1000 df[year_month] df[local_time].dt.to_period(M).astype(str) df[year_week] df[local_time].dt.to_period(W).astype(str) # 用“歌曲名 分隔符 艺术家名”做唯一键 df[track_key] df[track_name] // df[artist_name]track_key这个字段比直接合并track_name和artist_name更好用因为同一首歌在不同精选集里可能被标成不同版本但track_name经常完全一样。以后你想去重、去重后用特征数据回填都可以直接对这个键操作。3. 画像分析从几个经典维度看懂自己的听歌习惯3.1 时间热力图你的熬夜指数会先暴露自己清洗好的表已经有weekday和hour接下来只需要一次分组就能得到一周 7 天 × 24 小时的热力图数据。这种图非常适合回答“我是不是只有半夜才听歌”“周末和平时作息差异多大”这类问题。month_day_hour ( df.groupby([weekday, hour]) .size() .reset_index(nameplay_count) )如果你用的是matplotlib可以直接pivot成宽表后用imshow画热力图更省事的方案是用数据分析可视化库比如seaborn的heatmap。想要一劳永逸推荐把结果转为透视表pivot month_day_hour.pivot(indexweekday, columnshour, valuesplay_count) pivot pivot.fillna(0)解读时不要只盯着最大值所在格子真正有价值的是“夜间时段是否单独形成一座小山峰”。如果周日凌晨 2 点到 4 点有明显的隆起而工作日没有说明周末熬夜听歌是你固定的休息仪式如果一周七天都一样说明你的生物钟已经被长期且均匀地破坏掉了。3.2 歌手和单曲榜统计口径决定了你看到的真相统计“我最爱听谁”时必须想清楚一个问题按播放次数排还是按播放时长排很多年度报告默认是“次数”这对那些只有一分半钟的口水歌极其有利因为你一天能循环它 40 遍。但如果你想衡量“陪伴自己最久的音乐”时长更重要。一位歌手用一首 7 分钟的长歌陪你跑了 5 公里和另一位歌手用一首 90 秒的短歌被你在通勤路上快进两种度量会给出完全不同的排名。我的做法是两个指标都算并且把二者放在一起观察artist_stats ( df.groupby(artist_name) .agg(play_count(track_key, count), total_seconds(play_seconds, sum)) .reset_index() .sort_values(total_seconds, ascendingFalse) ) artist_stats[total_hours] artist_stats[total_seconds] / 3600这样就能发现藏在“次数榜”里的长情歌手也能找出“次数多但实际听得很浅”的列表。如果发现自己某位歌手只在入睡前出现可能意味着你已经把他当成了环境音而不是欣赏对象。3.3 月度/年度总量挖掘情绪转折点播放总量的时间趋势不需要太复杂的统计按月份汇总播放时长画一条折线图就够了。但真正有趣的是把月度总量和当时经历过的场景跨起来——某个月突然跌到谷底很可能对应着出差、备考、失恋或者赶项目某个月突然冲高往往是换了一副降噪耳机或者找到了新的歌单。从代码角度按月聚合就是一行monthly df.groupby(year_month)[play_seconds].sum() / 3600如果想看累计曲线再加一个cumsum()那就会得到一条“从零开始、到年底累计了 xxx 小时”的平滑曲线。这条曲线特别适合用来做自己的年终数据复盘哪个月你在加班哪个月你在放假曲线都写得很诚实。3.4 切歌与循环给“听歌行为”增加的微观指标听歌数据里最细颗粒度的信号就是“短播放”和“重复播放”。我给它们起了两个非正式但很贴切的指标跳歌率和循环率。跳歌率计算的是“播放时长小于 30 秒”的记录占比。如果某张歌单的跳歌率特别高说明它不是用来认真听的而是被用来随机刷氛围。循环率就更有意思了同一首歌在一天内播放次数超过 5 次基本可以认定为“循环上头”。这里要小心区分“因为单曲循环导致的多次播放”和“因为专辑顺序播放导致的前后多次播放”用track_key按日期分组计数就能看出来。daily_track_count ( df.groupby([date, track_key]) .size() .reset_index(namedaily_plays) ) repeat_tracks daily_track_count[daily_track_count[daily_plays] 5]这个 step 很轻量但能大幅提升分析故事的细节感。如果你把结果按月份聚合还会发现每年总有那么几个月“重复次数猛增”那几个点通常对应着刚经历完一次大型情感波动的时期。4. 进阶玩法把音频特征和日历行为结合在一起4.1 给每首歌补一组“情绪坐标”官方导出文件里没有歌曲本身的属性只记录了播放行为和曲目名所以先要从曲目信息接口把每首歌曲的音频特征批量拉回来。音频特征包含调性、节拍、能量值、舞蹈性、声学性、愉悦度等数值。说得夸张点这相当于把每首歌变成了一组情绪坐标高能量的歌适合运动低愉悦度的歌适合雨天发呆。批量请求时需要注意接口单次查询上限是 100 个曲目 ID。先对track_key去重再分批请求并把结果统一按曲目 ID 合并。这里还会遇到一个常见问题你自己听的一些翻唱、播客、本地导入文件可能没有对应的曲目 ID。所以接口数据返回后要做一次“外连接检查”看看有多少缺失缺失太多就考虑用导出数据里的artistName trackName再去模糊匹配。4.2 情绪—时间交叉表回答“我几点听什么样的歌”拿到音频特征之后可以做一件很出效果的分析按小时分组看不同小时的“平均能量”和“平均愉悦度”。大多数人的曲线会呈现一个 U 形或者倒 U 形白天听歌偏噪深夜听歌情绪往往两极分化。这时候用一小段代码就足够看清全貌feature_cols [danceability, energy, valence, acousticness] hourly_features ( df.groupby(hour)[feature_cols] .mean() .round(3) )你不用急着读懂所有特征含义只看energy和valence两条曲线就行。energy高说明你当时需要刺激valence高说明你听到的歌整体情绪偏积极。如果深夜 1 点的valence明显低于白天那基本可以确认“深夜是情绪小作文时段”如果深夜和白天几乎没差别只能说你已经是稳定的情绪管理大师。4.3 把分析结果沉淀成“个人音乐年报”分析完了不能只留在代码里我建议输出一张简单的个人音乐年报。不用做成很复杂的页面把几个关键结论点拼成一张 PDF 或 HTML 即可总播放时长、最常听的歌手们、最常单曲循环的 Top 10、周内时间分布、音频特征曲线。在输出 HTML 时可以用plotly生成交互式图表方便你在浏览器里 hover 看具体数值。如果要导出 PDFmatplotlib的savefig加PdfPages也足够。更轻量的做法是直接生成一个summary.md把表格和图片路径丢进去既适合自己复盘也适合发到社交平台秒变“数据党”朋友圈素材。5. 避坑清单这些问题我全都遇到过5.1 数据里少了一段历史怎么办先确认是不是漏掉了StreamingHistory的多个文件。很多新手只解压出第一个 json看到只有几千行就以为拿到了全部数据。正解是看压缩包里的完整文件列表把所有StreamingHistory*.json全部读进来。还有一种可能是“离线模式”或“隐私会话”下产生的播放记录不会进导出文件这部分缺失无解不用过分纠结。5.2 时间戳对不上热力图整体偏移最常见的偏移就是“忘记把 UTC 转本地时区”。排查方法很简单随便取一条播放记录看它当前时间字段和你的真实播放场景对不对得上。如果所有记录都比真实时间慢 8 小时那就是时区问题。碰到这种问题别去手动改字符串直接用pd.to_datetime(..., utcTrue)再tz_convert。5.3 接口批量取音频特征失败失败原因通常集中在两个点一是没有在请求头里带 token或者 token 过期了二是单次查询超过 100 个 ID。前者按官方文档重新刷新即可后者只要分批循环就会解决。建议把所有音频特征结果缓存成一个本地 parquet 或 CSV 文件这样后续再跑分析时重复歌曲不用重新请求接口。5.4 智能音箱和手机后台会产生虚高播放家里有智能音箱的同学要格外注意音箱经常把环境音乐当背景播放工作时听歌和睡前助眠可能形成非常大的播放时长占比。如果你的目的是分析“这段时间我在干嘛”可以不处理但如果目的是“品味自己的听歌审美”建议先按时间把明显是助眠、白噪音、轻音乐的类型过滤掉或者至少单列一个“环境音”分类。这在导出数据里没有现成字段但可以通过在深夜音频特征里看到低能量、低节奏的高聚合自动识别。5.5 常见问题快速参考问题现象可能原因处理方案时间热力图整体偏移未处理时区用tz_convert统一转本地统计次数明显偏少只读了一个 Stream 文件glob 匹配所有文件播放时长为 0后台预加载/错误播放过滤msPlayed 0音频特征大量缺失请求超限或 ID 不存在分批查询缓存外连接检查切歌率偏高误触/试听按 30 秒阈值统计单独观察月份趋势不自然换了设备/家庭共享检查是否有多个账号数据混入6. 把分析流程沉淀成可复用脚本6.1 模块化清洗、分析、出图三件事分开我之前也写过一版几百行的“一步到位”脚本后来维护时发现改起来很难受。这里建议把整个流程拆成三个文件load_data.py负责读取和清洗analysis.py负责生成聚合指标visualize.py负责出图。这样之后每年导出一次新数据只需要重新跑一遍load_data.py分析逻辑基本不用动。6.2 把中间结果缓存下来清洗后的 DataFrame 一定不要每次重新生成我会直接存成 parquet 或者 CSV。原因是官方导出文件可能很大多文件拼接虽然不慢但反复执行会消耗时间更重要的是当你去调试可视化代码时反复重新解析原文件会让你失去专注度。缓存好之后可视化脚本每次只读中间结果分析迭代速度快十倍。6.3 保留原始数据别在源文件上动手脚还有一点是花了很多次教训才养成的习惯永远保留下载压缩包的原始 json清洗脚本只做“读取-转换-输出新文件”绝对不回头改写源文件。分析思路到后期往往会变比如最开始只用artist_name后来想做“版本区分的专辑分析”这时原始 json 里保留的完整字段就能派上用场。如果你一开始就把原始文件洗得面目全非后面想换角度重跑就只能重新下载导出数据非常被动。最后再分享一个小技巧分析完自己的播放历史后如果你还想做预测下一首会听什么、或者给歌单排序这类更复杂的项目前面清洗出来的这张大表已经是最好的训练集。不用刻意找公开数据你自己的播放记录就记录了大量上下文信息。对着这份数据多跑几轮你会发现很多官方年度报告里不会告诉你的细节。