简介:这是一份面向高校Python课程学习者与初阶开发者的综合性大作业项目,聚焦音乐播放软件的全栈实现,覆盖网络爬虫获取音乐数据、SQLite数据库存储、Matplotlib/PyEcharts可视化分析及GUI界面开发四大核心能力,适合作为高分课程设计或期末大作业参考。资源包共6个文件,含1个主程序py文件(含完整注释,逻辑清晰易读)、1个SQL数据库脚本(建表与示例数据)、1个Word手册(含功能说明与运行指南)、2张界面效果图(展示UI布局与交互效果)及1个ICO图标文件,整体仅556KB,轻量便携,解压即用。已有279人学习下载,项目代码结构规范、模块分工明确,小白可快速上手理解,进阶者亦可基于现有框架拓展在线搜索、播放列表持久化或API对接等二开功能,是兼具教学性、实用性与延展性的优质Python实践案例。
1. 用 Python 搭建一个能“听懂数据”的音乐播放软件:从爬取歌单到生成听歌报告
这不是一个带 UI 的播放器 Demo,而是一套闭环的数据驱动型音乐工具链——它自动抓取主流平台公开的歌单与歌曲元数据(不涉及用户登录态或私有接口),把原始音频信息存进 SQLite 或 MySQL,用 Pandas 清洗出播放趋势、歌手热度、风格分布,再用 Matplotlib + Plotly 输出交互式图表,最后支持按「最近 7 天高频词」生成词云、「收藏量 Top20 歌单」导出 Excel、「某歌手作品时长分布」直方图等可落地的分析结果。适合课程设计、毕设选题、数据分析入门实战,也适合作为爬虫+数据库+可视化三模块联动的标准化练手项目。你不需要会前端框架,但需要理解 HTTP 请求边界、SQL 建模逻辑、图表坐标轴含义;如果你刚学完 requests 和 pandas,这个项目就是你第一次把“代码跑通”升级为“数据说话”的临界点。
2. 爬虫模块:用 requests + BeautifulSoup 抓取网易云音乐公开歌单页(非登录态)
2.1 为什么选网易云音乐公开榜单而非 QQ 音乐或酷狗?
网易云音乐对未登录用户的歌单列表页(如https://music.163.com/#/discover/toplist?id=3779629)采用静态 HTML 渲染,关键字段(歌名、歌手、播放量、收藏数)直接存在于<ul class="f-hide">下的<a>标签中,无需逆向 JS 加密或模拟点击。相较之下,QQ 音乐榜单页依赖 Ajax 动态加载且返回加密 JSON,酷狗则对 User-Agent 和 Referer 做强校验。本项目定位是教学级可复现性,因此放弃高并发、高隐蔽性方案,聚焦“最小可行抓取路径”。
2.2 构建可重试、带基础反爬的请求会话
import requests from bs4 import BeautifulSoup import time import random # 设置会话对象,复用连接并携带必要头信息 session = requests.Session() session.headers.update({ "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", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Referer": "https://music.163.com/" }) def fetch_playlist_page(playlist_id: str, max_retries: int = 3) -> BeautifulSoup: url = f"https://music.163.com/playlist?id={playlist_id}" for attempt in range(max_retries): try: response = session.get(url, timeout=10) response.raise_for_status() # 网易云音乐页面实际内容在 iframe 内,需解析 body 中的 #content-frame # 但公开榜单页(如 top list)可直接解析主 HTML,此处以榜单页为例 soup = BeautifulSoup(response.text, 'html.parser') return soup except requests.exceptions.RequestException as e: print(f"第 {attempt + 1} 次请求失败: {e}") if attempt < max_retries - 1: time.sleep(random.uniform(1, 3)) # 指数退避式等待 else: raise return None提示:此代码不使用 Selenium 或 Playwright,因目标页面无 JavaScript 渲染依赖;若后续扩展至需登录的私人歌单,则需切换为无头浏览器方案,但本项目严格限定在公开数据范围内,避免引入复杂依赖。
2.3 解析歌单页并提取结构化字段
网易云音乐公开榜单页中,每首歌曲信息嵌套在<li>标签内,典型结构如下:
<li> <div class="hd"> <span class="num">1</span> </div> <div class="bd"> <div class="f-thide"><a href="/song?id=1816121162">起风了</a></div> <div class="text"> <span class="txt"><a href="/artist?id=12116112">买辣椒也用券</a></span> <span class="sep">/</span> <span class="txt"><a href="/album?id=3779629">起风了</a></span> </div> </div> </li>对应解析逻辑:
def parse_songs_from_soup(soup: BeautifulSoup) -> list[dict]: songs = [] # 定位所有歌曲 li 元素 song_items = soup.select("ul.f-hide li") for item in song_items: try: title_tag = item.select_one("a[href^='/song?id=']") artist_tag = item.select_one("a[href^='/artist?id=']") if not title_tag or not artist_tag: continue song_id = title_tag["href"].split("=")[-1] title = title_tag.get_text(strip=True) artist = artist_tag.get_text(strip=True) # 播放量和收藏数需另发请求获取(因不在当前页面),此处设为占位符 # 实际项目中可调用 /song/detail 接口(需构造合法 referer) songs.append({ "song_id": song_id, "title": title, "artist": artist, "album": "", # 可通过 album tag 补充 "play_count": 0, "favorite_count": 0, "crawl_time": time.strftime("%Y-%m-%d %H:%M:%S") }) except Exception as e: print(f"解析单条歌曲失败: {e}") continue return songs # 示例调用 soup = fetch_playlist_page("3779629") # 热歌榜 ID songs = parse_songs_from_soup(soup) print(f"成功提取 {len(songs)} 首歌曲")2.3.1 关键参数说明
song_id: 从/song?id=xxx中提取,作为后续关联数据库主键及去重依据;title/artist: 使用.get_text(strip=True)去除换行与空格,避免入库时字段污染;crawl_time: 记录抓取时间戳,支撑后续「时间维度分析」(如周环比播放量变化);play_count和favorite_count当前设为 0,因公开页不展示该数据——这是本项目明确的能力边界声明,不伪造、不猜测、不调用未授权接口。
3. 数据库模块:SQLite 建模与批量写入(兼顾课程设计可演示性与生产可迁移性)
3.1 为什么首选 SQLite 而非 MySQL 或 PostgreSQL?
课程设计场景下,SQLite 具备零配置、单文件、Python 内置支持三大优势:无需安装服务端、无需管理用户权限、import sqlite3即可用。更重要的是,其 SQL 语法与 MySQL 高度兼容(如CREATE TABLE,INSERT OR REPLACE,JOIN),学生完成作业后只需修改连接字符串即可迁移到 MySQL,符合“先跑通、再升级”的教学逻辑。本项目数据库设计遵循第三范式,但允许适度冗余(如歌手名重复存储)以降低 JOIN 复杂度,平衡可读性与性能。
3.2 创建四张核心表并定义外键约束
import sqlite3 def init_database(db_path: str = "music.db"): conn = sqlite3.connect(db_path) cursor = conn.cursor() # 歌曲主表(含唯一 song_id) cursor.execute(""" CREATE TABLE IF NOT EXISTS songs ( id INTEGER PRIMARY KEY AUTOINCREMENT, song_id TEXT UNIQUE NOT NULL, title TEXT NOT NULL, artist TEXT NOT NULL, album TEXT, duration_sec INTEGER DEFAULT 0, crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) # 歌单关联表(解决多对多关系) cursor.execute(""" CREATE TABLE IF NOT EXISTS playlists ( id INTEGER PRIMARY KEY AUTOINCREMENT, playlist_id TEXT UNIQUE NOT NULL, name TEXT NOT NULL, creator TEXT, description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) # 歌曲-歌单关联表(记录某首歌属于哪些公开歌单) cursor.execute(""" CREATE TABLE IF NOT EXISTS song_playlist_map ( id INTEGER PRIMARY KEY AUTOINCREMENT, song_id TEXT NOT NULL, playlist_id TEXT NOT NULL, position INTEGER, -- 在歌单中的序号 FOREIGN KEY (song_id) REFERENCES songs(song_id), FOREIGN KEY (playlist_id) REFERENCES playlists(playlist_id), UNIQUE(song_id, playlist_id) ) """) # 分析快照表(存储每次运行后的统计结果,供可视化读取) cursor.execute(""" CREATE TABLE IF NOT EXISTS analysis_snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, snapshot_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, top_artists TEXT, -- JSON 字符串,如 '[{"name":"周杰伦","count":12}]' genre_distribution TEXT, -- 同上 avg_duration REAL, total_songs INTEGER ) """) conn.commit() conn.close() init_database()3.3 批量插入歌曲数据并处理重复主键
def insert_songs_to_db(songs: list[dict], db_path: str = "music.db"): conn = sqlite3.connect(db_path) cursor = conn.cursor() # 使用 executemany 提升性能,避免逐条 INSERT insert_sql = """ INSERT OR REPLACE INTO songs (song_id, title, artist, album, duration_sec, crawl_time) VALUES (?, ?, ?, ?, ?, ?) """ values = [ ( s["song_id"], s["title"], s["artist"], s.get("album", ""), s.get("duration_sec", 0), s["crawl_time"] ) for s in songs ] try: cursor.executemany(insert_sql, values) conn.commit() print(f"成功写入 {len(values)} 条歌曲记录(含更新)") except sqlite3.IntegrityError as e: print(f"数据库写入异常: {e}") finally: conn.close() # 调用示例 insert_songs_to_db(songs)3.3.1 参数设计深意
INSERT OR REPLACE: 替代INSERT IGNORE(MySQL 语法),当song_id冲突时自动覆盖旧记录,适应爬虫增量更新场景;duration_sec字段预留但暂不填充,因公开页面无时长信息——后续若接入第三方 API(如 NCM API 公开端点),可在此字段补全;analysis_snapshots表不存原始数据,只存聚合结果,隔离分析层与原始层,避免可视化查询拖慢主业务表。
4. 数据分析与可视化模块:用 Pandas 统计歌手热度,用 Plotly 输出交互式柱状图
4.1 用 Pandas 从 SQLite 提取并聚合歌手出现频次
import pandas as pd import sqlite3 def get_artist_frequency(db_path: str = "music.db") -> pd.DataFrame: query = """ SELECT artist, COUNT(*) as frequency FROM songs GROUP BY artist ORDER BY frequency DESC LIMIT 20 """ conn = sqlite3.connect(db_path) df = pd.read_sql_query(query, conn) conn.close() return df # 执行分析 df_artist = get_artist_frequency() print(df_artist.head(10))4.2 用 Plotly Express 绘制可缩放、可下载的 Top20 歌手柱状图
import plotly.express as px def plot_top_artists(df: pd.DataFrame, output_html: str = "top_artists.html"): # 设置中文字体(需系统已安装 SimHei 或 Noto Sans CJK) fig = px.bar( df, x="artist", y="frequency", title="Top 20 歌手出现频次(基于爬取的公开歌单)", labels={"artist": "歌手", "frequency": "出现次数"}, color="frequency", color_continuous_scale="Blues" ) # 强制 X 轴标签垂直显示,避免重叠 fig.update_layout( xaxis_tickangle=-45, height=600, font=dict(size=14), title_font_size=18, showlegend=False ) # 导出为独立 HTML 文件,双击即可打开交互 fig.write_html(output_html) print(f"图表已保存至 {output_html}") plot_top_artists(df_artist)注意:Plotly 默认使用英文,若图表中文乱码,请确保系统字体支持,并在代码开头添加:
import matplotlib.pyplot as pltplt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
(此设置影响 Matplotlib,Plotly 需依赖系统字体,Windows 一般自带 SimHei)
4.3 生成词云:基于所有歌名提取高频词
from wordcloud import WordCloud import jieba import matplotlib.pyplot as plt def generate_title_wordcloud(db_path: str = "music.db", output_path: str = "title_wordcloud.png"): conn = sqlite3.connect(db_path) titles = pd.read_sql_query("SELECT title FROM songs", conn)["title"].tolist() conn.close() # 中文分词 text = " ".join(titles) words = jieba.lcut(text) # 过滤停用词(简易版,实际项目应加载外部停用词表) stopwords = {"的", "了", "在", "是", "我", "有", "和", "就", "不", "人", "都", "一", "一个"} filtered_words = [w for w in words if len(w) > 1 and w not in stopwords] # 生成词云 wc = WordCloud( font_path="simhei.ttf", # Windows 系统字体路径,macOS 用 "/System/Library/Fonts/PingFang.ttc" width=1200, height=800, background_color="white", max_words=100 ).generate(" ".join(filtered_words)) plt.figure(figsize=(10, 8)) plt.imshow(wc, interpolation="bilinear") plt.axis("off") plt.savefig(output_path, dpi=300, bbox_inches="tight") plt.show() print(f"词云已保存至 {output_path}") generate_title_wordcloud()4.3.1 词云关键参数说明
font_path: 必须指定中文字体路径,否则显示方块;Windows 默认simhei.ttf位于C:\Windows\Fonts\;max_words=100: 控制词云最大词汇量,避免低频词干扰视觉焦点;background_color="white": 适配导出 PNG 场景,如需嵌入网页可改为"transparent"。
5. 项目整合与调试技巧:如何快速验证每个模块是否正常工作
5.1 三步验证法:从爬虫输出 → 数据库记录 → 图表渲染
不要等到全部写完才测试。按顺序执行以下命令,任一环节失败立即定位:
验证爬虫是否拿到数据
python -c " from crawler import fetch_playlist_page, parse_songs_from_soup soup = fetch_playlist_page('3779629') songs = parse_songs_from_soup(soup) print(f'抓取到 {len(songs)} 首歌,示例:{songs[0] if songs else '空'}') "✅ 预期输出:
抓取到 50 首歌,示例:{'song_id': '1816121162', 'title': '起风了', ...}验证数据库是否写入
sqlite3 music.db "SELECT COUNT(*) FROM songs;"✅ 预期输出:
50(与上步数量一致)验证可视化是否生成文件
ls -lh top_artists.html title_wordcloud.png✅ 预期输出:两个非零字节文件存在
5.2 常见报错与精准修复方案
| 报错现象 | 根本原因 | 修复命令/代码 |
|---|---|---|
requests.exceptions.ConnectionError | 网易云音乐临时封禁 IP 或域名解析失败 | 在fetch_playlist_page中增加session.get(..., proxies={"http": "http://127.0.0.1:8080"})测试代理连通性,或更换 User-Agent 字符串 |
sqlite3.OperationalError: no such table: songs | init_database()未执行或路径错误 | 运行python -c "from database import init_database; init_database()",确认music.db文件生成 |
ValueError: max() arg is an empty sequence(词云报错) | titles列表为空,即数据库无数据 | 执行sqlite3 music.db "SELECT COUNT(*) FROM songs;",若为 0 则回溯爬虫模块 |
| 词云中文显示为方块 | font_path路径错误或字体文件缺失 | Windows 下运行dir C:\Windows\Fonts\simhei*确认文件存在;macOS 下用fc-list :lang=zh查看可用中文字体 |
5.3 一键运行脚本:整合全流程(main.py)
# main.py if __name__ == "__main__": print("=== 开始执行音乐数据分析全流程 ===\n") # 步骤1:爬取热歌榜 print("1. 抓取网易云热歌榜...") from crawler import fetch_playlist_page, parse_songs_from_soup soup = fetch_playlist_page("3779629") songs = parse_songs_from_soup(soup) # 步骤2:初始化并写入数据库 print("2. 初始化数据库并写入数据...") from database import init_database, insert_songs_to_db init_database() insert_songs_to_db(songs) # 步骤3:生成分析图表 print("3. 生成歌手热度图与词云...") from visualization import plot_top_artists, generate_title_wordcloud import pandas as pd df = pd.read_sql_query("SELECT artist, COUNT(*) c FROM songs GROUP BY artist ORDER BY c DESC LIMIT 20", "sqlite:///music.db") plot_top_artists(df) generate_title_wordcloud() print("\n✅ 全流程执行完毕!查看 top_artists.html 和 title_wordcloud.png")运行python main.py,全程无交互、无报错、输出两个可视化文件——这就是本项目交付的最小完整形态。
本文还有配套的精品资源,点击获取