
每年毕设季一到总能看到不少人在音乐数据分析这类题目上卡壳。选网易云音乐作为数据源确实是个经典做法平台热度高、歌曲评论数据丰富、网页版有公开接口可抓而且“爬虫—清洗—可视化”三个环节刚好能把一门课里学过的知识点串起来。用 Python 做这件事技术栈无非是 requests 抓网页、BeautifulSoup/json 解析数据、pandas 做清洗、matplotlib 画图但真正动手之后才会发现从拿到原始数据到输出一张能写进论文的图中间全是细节。这篇内容我按一套可以直接落地的项目路径来讲先拆需求和技术选型再带你把爬虫模块、数据清洗、可视化分析一步步走通最后把毕设落地和答辩时容易被问住的坑单独拿出来说。无论你是准备用它做毕业设计还是单纯想练手一个完整的 Python 数据处理项目这套流程都可以直接抄作业。1. 项目整体设计与思路拆解1.1 毕设视角下的需求拆解先想清楚这个项目到底要交什么。一个合格的“音乐数据分析可视化”毕业设计评分的核心从来不是爬虫本身而是三个问题数据是怎么来的、数据是怎么被整理干净的、整理之后能讲出什么结论。所以需求可以拆成四层数据采集层从网易云音乐的公开页面拿到一批歌曲及评论信息数据存储层把爬到的内容落地成 csv 文件保证后续分析不依赖网络数据清洗层用 pandas 处理缺失值、重复值、类型错误、时间格式等问题分析展示层用 matplotlib 输出图表对播放热度、评论趋势、用户活跃时段等维度做解读。为什么这样拆因为毕设答辩时老师翻代码第一眼看的是目录结构是否清晰第二眼看的是有没有异常处理和注释第三眼才看可视化做得漂不漂亮。把分层做好后面所有环节都会顺。1.2 数据源与技术栈选型数据源我建议用网易云音乐网页版的“歌单页”和“歌曲详情页”。这两个页面里都嵌有 JSON 数据不需要走移动端那套复杂加密参数抓包分析成本低适合教学和毕设场景。技术栈选型其实没有悬念requests发 HTTP 请求拿 HTMLPython 生态里最成熟的库BeautifulSoup 加正则从 HTML 中定位 JSON 数据块抽取出结构化字段pandas数据清洗和预处理功能覆盖缺失值处理、去重、类型转换、时间序列分析matplotlib静态图表绘制满足论文插图需求wordcloud可选做评论关键词词云能提升展示效果。选 matplotlib 而不是 pyecharts 的考虑是毕设论文要求图表静态、配色正式、能在文档里稳定显示matplotlib 默认风格虽然朴素但可控性最高。1.3 代码目录结构规划拿到题目先别急着写爬虫先把工程结构定义好。我习惯用一个 src 目录放源码data 目录放原始数据output 目录放图和清洗后的数据music-analysis/ │ ├── src/ │ ├── spider.py # 爬虫模块请求、解析、存储 │ ├── clean.py # 数据清洗模块 │ ├── analyze.py # 分析逻辑统计计算、特征工程 │ └── visualize.py # 可视化模块matplotlib绘图 │ ├── data/ │ ├── raw_songs.csv # 爬虫落地的原始数据 │ └── clean_songs.csv # 清洗后的分析用数据 │ ├── output/ │ ├── top20_songs.png │ ├── comment_distribution.png │ └── wordcloud.png │ ├── requirements.txt ├── README.md └── main.py # 主入口串联整个流程这种结构的好处是每个文件职责单一后面扩展比如换成其他数据源、增加聚类分析只需要加模块不需要推翻重写。2. 环境准备与爬虫模块实现2.1 环境依赖与安装建议直接用 Python 3.8 以上的版本。先建虚拟环境再把依赖写进 requirements.txtpip install requests beautifulsoup4 pandas matplotlib jieba wordcloud做一个记录一下这几个库的版本不需要刻意追求最新稳定就好。我在实际项目中用的是 requests 2.31、pandas 2.0、matplotlib 3.7 这一档没遇到兼容性问题。2.2 页面请求与 JSON 数据提取爬虫模块的核心是搞清楚“数据以什么形式放在哪个位置”。网易云音乐的网页版歌单页在返回的 HTML 里会有一段带window.__INITIAL_STATE__的脚本里面就嵌了歌单歌曲列表的完整 JSON。解析思路很简单拿到整个页面源代码用正则和 json 模块把它取出来。import requests import json import re headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://music.163.com/, } def get_song_list(playlist_url): resp requests.get(playlist_url, headersheaders, timeout10) resp.raise_for_status() resp.encoding utf-8 # 从HTML里取出初始化数据 pattern rwindow\.__INITIAL_STATE__\s*\s*(.*?);/script match re.search(pattern, resp.text, re.S) if not match: raise ValueError(未找到初始化数据可能页面结构已更新) state json.loads(match.group(1)) tracks state[playlist][detail][tracks] return tracks注意window.__INITIAL_STATE__名字可能因平台改版而变化实际写代码时先用浏览器开发者工具在页面源码里搜一下再定正则。这就是我在代码注释里强调“页面结构可能已更新”的原因。2.3 字段解析与数据落地拿到 tracks 列表后每条歌曲数据里包含 songId、songName、artists、album、duration 等字段。把这些字段转成规整的字典列表再用 csv 模块落盘import csv def parse_track_info(tracks): rows [] for t in tracks: rows.append({ song_id: t.get(id), song_name: t.get(name), artist: 、.join(a.get(name, ) for a in t.get(artists, [])), album: t.get(album, {}).get(name, ), duration_ms: t.get(duration, 0), play_count: t.get(playCount, 0), }) return rows def save_to_csv(rows, filepath): with open(filepath, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesrows[0].keys()) writer.writeheader() writer.writerows(rows)用utf-8-sig编码保存是有讲究的直接存 utf-8 的话生成的 csv 用办公软件打开会出现中文乱码utf-8-sig能规避这个问题。2.4 请求节奏与异常处理爬虫模块最容易被忽略的就是请求节奏。网易云音乐页面请求频率过高会触发风控所以循环抓取时务必要加随机延时。同时要给每次请求包一层异常处理避免单条数据失败导致整个程序中断import time import random def crawl_multi_playlists(playlist_ids): all_rows [] for pid in playlist_ids: try: tracks get_song_list(fhttps://music.163.com/playlist?id{pid}) rows parse_track_info(tracks) all_rows.extend(rows) print(f歌单{pid}抓取成功新增{len(rows)}条歌曲) except Exception as e: print(f歌单{pid}抓取失败: {e}) time.sleep(random.uniform(2, 5)) return all_rows这里把延时间隔设为 2 到 5 秒的随机值既是模拟真实用户行为也是给自己留出缓冲避免给目标站点造成压力。 这里我再强调一次做毕设爬虫控制频率不是技术问题而是习惯问题数据够用就行不必追求全量。3. 数据清洗与预处理细节3.1 先看看脏数据长什么样爬下来的原始 csv 通常存在几类问题歌曲名带多余空格或括号注释、artist 字段里混入空值、duration_ms 是整型毫秒数、play_count 理论上应该是数字但也可能被爬成空字符串。做清洗之前先执行df.info()和df.describe()看看全貌import pandas as pd df pd.read_csv(data/raw_songs.csv) print(df.info()) print(df.head())这一步符合一个朴素的工程原则先看数据长什么样再决定清洗策略。跳过探索直接清洗很容易把不该删的字段删掉。3.2 用 pandas 完成核心清洗动作我按常见操作整理了一套清洗流水线def clean_songs(df): # 1. 去掉完全重复的记录 df df.drop_duplicates(subset[song_id]) # 2. 删除关键字段为空的记录 df df.dropna(subset[song_name, artist, play_count]) # 3. 将播放量转为数值类型非数字统一填0 df[play_count] pd.to_numeric(df[play_count], errorscoerce).fillna(0) # 4. 时长从毫秒转为分钟 df[duration_min] df[duration_ms] / 60000 df[duration_min] df[duration_min].round(2) # 5. 去除歌曲名首尾空格和引号 df[song_name] df[song_name].str.strip().str.strip(\\) # 6. 把无歌手信息的记录标记为未知 df[artist] df[artist].fillna(未知歌手) return df这里有个细节值得展开pd.to_numeric(..., errorscoerce)会把非法字符串转成 NaN再fillna(0)将缺失播放量归零。直接astype(int)会因为某一行脏数据而整个报错这不是数据问题而是清洗手法不到位。3.3 特征工程为分析阶段铺路清洗不只是处理脏数据还要构建分析需要的派生字段。比如歌曲时长分段、歌手维度展开、播放量分箱def build_features(df): # 按时长分组3分钟以内、3-4分钟、4-5分钟、5分钟以上 bins [0, 180, 240, 300, float(inf)] labels [3分钟以内, 3-4分钟, 4-5分钟, 5分钟以上] df[duration_band] pd.cut(df[duration_min] * 60, binsbins, labelslabels) # 播放量取对数常用于观察长尾分布 df[log_play] np.log1p(df[play_count]) return df这里构造log_play的原因是播放量通常高度右偏几首头部歌曲可能占掉九成流量画图时直接看原始数值会把差异拉平取对数后分布形态更明显。4. 数据分析与可视化实践4.1 分析维度这样定可视化不是凭空画图每一个图表背后都必须对应一个可以回答的问题。我这个项目设计了四个分析方向播放量 Top20 歌曲排名回答“这个歌单里哪些歌曲最受欢迎”歌曲时长分布回答“大众向歌曲集中在什么时长范围”播放量的对数分布回答“播放量是否存在明显的长尾特征”歌手维度统计回答“哪些歌手在歌单中出现频率最高”。每个问题对应一张图每张图在论文里都有独立小节支撑这是毕设拿分的核心。4.2 matplotlib 绘图核心配置matplotlib 最坑的一点是默认字体不支持中文。直接plt.title(播放量)大概率输出方块。所以开局先做全局配置import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 用于正常显示中文 plt.rcParams[axes.unicode_minus] False # 用于正常显示负号配完字体再做柱状图就顺手了。播放量 Top20 的横线图是最直观的展示top20 df.nlargest(20, play_count) plt.figure(figsize(12, 8)) bars plt.barh(top20[song_name][::-1], top20[play_count][::-1]) plt.xlabel(播放量) plt.ylabel(歌曲名称) plt.title(歌单播放量 Top20 歌曲) plt.tight_layout() plt.savefig(output/top20_songs.png, dpi200) plt.show()这里用nlargest先排序再倒序画图能保证图表里播放量最高的歌曲排在最上方阅读体验更好。4.3 分布类图表饼图与直方图歌曲时长分段是一个典型的分类变量适合用饼图展示比例band_counts df[duration_band].value_counts().reindex( [3分钟以内, 3-4分钟, 4-5分钟, 5分钟以上] ) plt.figure(figsize(8, 8)) plt.pie(band_counts, labelsband_counts.index, autopct%.1f%%, startangle90, counterclockFalse) plt.title(歌曲时长分布) plt.savefig(output/duration_pie.png, dpi200) plt.show()播放量对数分布则用直方图能看出大部分歌曲处在什么量级plt.figure(figsize(10, 6)) plt.hist(df[log_play], bins30, edgecolorwhite) plt.xlabel(播放量对数(log)) plt.ylabel(歌曲数量) plt.title(播放量对数分布) plt.savefig(output/log_play_hist.png, dpi200) plt.show()4.4 展示效果提升技巧matplotlib 默认图确实朴素但通过几个小改动就能达到论文插图标准所有图统一设置dpi200保证放大打印不糊保存时加bbox_inchestight避免坐标轴标签被裁切同一个项目的图表配色保持一致比如统一用深蓝色系图标题用“结论式”描述比如直接写“播放量 Top20 集中在少数头部歌曲”比“播放量分布图”更像分析而非例行公事。在论文里插入图形时记得补充一段文字解读说明从图中看到了什么趋势、这个趋势是否与常识一致、可能的业务原因是什么。这部分文字才是论文和博文的灵魂图本身只是辅助。5. 常见问题与排查技巧实录5.1 高频问题速查表我把自己做这类项目时遇到过的问题整理成了表格按出现频率排序症状可能原因处理方案requests 请求报 403缺少请求头或频率过高补全 User-Agent、Referer增加随机延时正则提取不到数据页面结构更新变量名变动先用浏览器源码搜索定位真实变量名中文字体显示方块matplotlib 默认字体不支持中文设置rcParams为 SimHei 或系统中文字体csv 打开中文乱码保存编码不是 utf-8-sig写入时改用encodingutf-8-sig播放量字段报转换错误字符串里混有非数字pd.to_numeric(errorscoerce)再补零清洗后行数骤减去重、去缺失逻辑过严查看各步骤删除数量区分关键字段和非关键字段图表坐标轴文字被截断布局参数未设置保存时加bbox_inchestight这个表基本覆盖了从爬虫到可视化的整个链路排查时可以按“请求阶段、解析阶段、清洗阶段、绘图阶段”分段定位。5.2 爬虫模块的防坑心得爬虫代码本身不难难在优雅地处理失败。我的经验是每条请求都函数化单独负责一件事失败信息要带上下文打印出来而不是一句笼统的except Exception。加一个成功计数器和失败计数器跑完能直接看到成功率这样调试效率会高很多。另外在上手阶段不要一次性爬全部数据。先爬一个歌单约 30 首生成一个小样本跑通解析和清洗流程再扩大到多个歌单。我见过太多同学一上来就全量抓取结果页面结构解析错了几千条数据全浪费。5.3 毕设评审老师常问的问题答辩时老师会盯着代码问实现思路。提前准备这几个回答会显得很有底气为什么选这个数据源答数据公开、结构丰富、平台活跃度代表性强数据量多少答以实际抓取为准重点是分析逻辑完整而不是量级大清洗过程做了什么答去重、去缺失、类型转换、构造派生特征分析的结论是什么答用图表对应结论逐条说明有没有考虑数据时效性答可以定期重跑脚本设计上已预留更新入口。6. 毕业设计落地的工程化建议6.1 把代码当产品写而不是当作业写毕设源码交给老师看之前一定要做一次“陌生人测试”把项目发给一个没听过你需求的人看他能否只靠 README 和目录结构直接跑通。我写 README 的习惯是包含五个部分项目简介、安装步骤、运行方式、目录说明、常见问题。工程化不是加分项而是基础分。源码里必须做到函数有注释、关键逻辑有说明、模块导入路径统一、main.py能从零串联完整流程。使用绝对路径或者基于Path(__file__)的相对路径避免换机器后路径直接把程序跑崩。6.2 主入口这样设计main.py的价值是把散落在各模块里的功能串成一条流水线。我的写法是分步骤执行每做完一步打印一次进度from src.spider import crawl_multi_playlists, save_to_csv from src.clean import clean_songs, build_features from src.visualize import draw_top20, draw_duration_pie if __name__ __main__: # 第一步爬取数据 playlist_ids [3778678, 3779629] # 实际使用时替换为目标歌单 rows crawl_multi_playlists(playlist_ids) save_to_csv(rows, data/raw_songs.csv) # 第二步清洗数据 df pd.read_csv(data/raw_songs.csv) df_clean clean_songs(df) df_clean build_features(df_clean) df_clean.to_csv(data/clean_songs.csv, indexFalse, encodingutf-8-sig) # 第三步可视化 draw_top20(df_clean) draw_duration_pie(df_clean) print(项目流程运行完毕)这样设计的好处是每个阶段之间通过 csv 文件解耦爬虫跑挂了不影响后面手写数据测试分析清洗结果也可以单独打开检查。6.3 让图表更有分析价值的扩展方向如果基础图表做完还有时间我会建议往三个方向扩展每个都会让项目上一个小台阶一是评论数据的文本分析抓取歌曲评论区后用 jieba 分词和 wordcloud 生成词云能明显提高视觉冲击力二是时间序列分析把评论时间戳转成日期后绘制评论数随时间变化的折线图三是对比分析抓取多个不同风格歌单对比播放量和时长特征差异。扩展部分的代码量不需要很大但能展示你对数据分析和可视化有系统性理解而不只是会用工具。我自己的体会是这种毕设项目真正拉开差距的往往不是爬虫技术而是把数据讲清楚的能力。每张图都要有一段话说出背后的逻辑比如某类歌曲更受欢迎可能是因为时长适中、容易在碎片时间听完。从这个角度去组织报告内容自然就充实了。最后再分享一个实战中很容易被忽略的小细节所有中间数据都保留一份原始版本不要一上来就在原文件上做修改清洗。我习惯把 raw 和 clean 严格分目录清洗脚本跑崩了重来一遍就行不用回头重新爬数据。这个习惯在项目后期改需求时会帮你省下大量时间。