ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spark网易云音乐数据分析:从爬虫到可视化的数据工程实战

Spark网易云音乐数据分析:从爬虫到可视化的数据工程实战 简介本资源是一套完整的本科毕业设计项目实现面向大数据专业学生及Spark初学者聚焦音乐平台真实场景的数据分析实践。项目基于Apache Spark构建分布式分析流水线覆盖用户行为、歌曲热度、群体画像、时段偏好与评论情感五大分析方向可直接用于课程设计、毕设开题与工程复现。压缩包共404个文件9.67MB包含123个Java/Scala核心业务代码含Spark SQL与Streaming模块、56个JS36个HTML前端可视化页面、35个PNG/JPG图表素材以及log4j、Flume、Bootstrap等配套配置与样式资源结构清晰模块解耦明确。已有2604人学习下载提供从数据采集、清洗、计算到Web展示的全链路代码与配置附带AmazeUI/Font Awesome等成熟前端组件集成方案便于快速部署与二次开发。1. 毕业设计真能跑通 Spark 分析网易云音乐数据别被“爬虫Spark”标题骗了这其实是数据工程能力的完整闭环检验很多同学看到“基于Spark网易云音乐数据分析”这个毕业设计标题第一反应是爬点歌单、用Spark SQL查个播放量TOP10、画个词云交差。但真实落地时90%的人卡在第三步——数据根本进不了Spark。不是因为不会写sc.textFile()而是因为原始数据压根没清洗成可计算形态歌单ID混着用户ID、评论时间戳格式不统一、歌手字段里塞着“/”分隔的多人名、甚至JSON嵌套三层还带HTML转义字符。这不是Spark的问题是数据管道没建起来。这个选题真正考验的是能否把一个非结构化、高噪声、强时效性的互联网公开数据流通过合理分层原始层→清洗层→主题层→应用层变成可复用、可验证、可回溯的分析资产。适合想扎实掌握大数据开发全流程的本科生尤其适合那些简历上写着“熟悉Hadoop生态”但连spark-submit --jars加依赖都配错三次的同学。它不追求模型多炫而要求每一步都有日志、有校验、有退路——比如某次ETL任务失败后5分钟内能定位到是某条歌单的封面URL里多了个不可见的零宽空格。2. 从网页源码到DataFrame三步构建可复用的网易云音乐数据采集链路2.1 为什么不用Selenium用RequestsBeautifulSoup正则组合拳才是毕业设计的务实选择毕业设计不是工业级爬虫项目不需要模拟登录、处理滑块验证码或应对JS渲染。网易云音乐的歌单页、歌曲详情页、热门评论页其核心数据歌单名、创建者、歌曲列表、评论内容、点赞数全部存在于HTML静态响应中。Selenium启动浏览器、等待渲染、管理驱动版本对本地调试极其不友好——你改一行XPath等Chrome加载完再看报错节奏全乱。而RequestsBS4组合配合requests.adapters.HTTPAdapter设置重试策略和连接池5分钟就能跑通一页。关键代码如下import requests from bs4 import BeautifulSoup import time import random def fetch_playlist_page(playlist_id: str, headers: dict) - str: url fhttps://music.163.com/playlist?id{playlist_id} session requests.Session() # 复用连接避免TIME_WAIT堆积 adapter requests.adapters.HTTPAdapter(max_retries3, pool_connections10, pool_maxsize10) session.mount(https://, adapter) try: resp session.get(url, headersheaders, timeout10) resp.raise_for_status() # 网易云有反爬必须加随机延迟否则IP被限速 time.sleep(random.uniform(0.8, 1.5)) return resp.text except Exception as e: print(f获取歌单 {playlist_id} 失败: {e}) return # headers 必须包含 Referer 和 User-Agent否则返回 403 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/ }提示Referer是硬性要求网易云会校验来源页。漏掉这行所有请求返回空HTML。User-Agent建议用最新版Chrome避免被识别为爬虫。2.2 解析歌单页用正则提取JSON数据比XPath更稳因为网易云把结构化数据藏在script里翻看网易云歌单页源码你会发现页面主体是空的div idg_main真正的数据藏在script标签里的一段JavaScript变量赋值中形如var playlist { id: 123, name: ...};。用XPath定位div classu-cover u-cover-1下的文本极易因前端微调而失效而正则匹配var playlist (.*?);只要变量名不变就稳如磐石。实测对比XPath在网易云2023年Q4前端重构后全部失效正则方案零修改继续运行。import re import json def parse_playlist_html(html: str) - dict: # 匹配 var playlist {...}; 中的 JSON 字符串 pattern rvar\splaylist\s*\s*(\{.*?\}); match re.search(pattern, html, re.DOTALL) if not match: return {} try: # 去除注释、修复末尾逗号部分版本JS有语法错误 json_str re.sub(r//.*?$, , match.group(1), flagsre.MULTILINE) json_str re.sub(r,\s*}, }, json_str) # 修复非法结尾逗号 return json.loads(json_str) except json.JSONDecodeError as e: print(fJSON解析失败: {e}) return {} # 示例调用 html fetch_playlist_page(23456789, HEADERS) playlist_data parse_playlist_html(html) print(f歌单名: {playlist_data.get(name)}, 歌曲数: {playlist_data.get(trackCount)})逻辑说明re.DOTALL让.匹配换行符确保跨行JSON能捕获json.loads()前先做两处容错删JS单行注释、修结尾逗号——这是网易云前端工程师留下的“彩蛋”不处理就会JSONDecodeError。参数playlist_data字典结构稳定含tracks键歌曲列表、creator键创建者信息、tags键歌单标签直接可转为Pandas DataFrame。2.3 构建最小可行数据集只抓10个高质量歌单胜过1000个脏数据毕业设计不是数据竞赛数据质量数据量。盲目扩大爬取范围只会让后续清洗工作爆炸式增长。我的做法是人工筛选10个典型歌单——3个华语热歌榜含周杰伦、陈绮贞、新裤子乐队、3个小众独立音乐实验电子、后摇、City Pop、2个语种混合日语动漫OST、韩语K-Pop、2个场景化歌单“咖啡馆背景音”“深夜emo专用”。理由很实在覆盖不同数据模式中文名/外文名/混合名、单歌手/多歌手/乐队名、标签丰富/标签稀疏且每个歌单歌曲数控制在50首以内总数据量约500条本地CSV仅2MBSpark本地模式秒级完成测试。记住毕业答辩时老师问“你如何保证数据代表性”你指着这10个歌单的命名逻辑和覆盖维度比说“我爬了10万条”有力十倍。3. 数据清洗与分层建模用PySpark把脏数据炼成分析燃料3.1 清洗层Clean Layer用DataFrame API做原子化操作拒绝UDF黑匣子很多同学一上来就写UDF用户自定义函数处理歌手名分割结果性能暴跌、调试困难。PySpark的内置函数足够应付90%清洗需求。以“歌手字段含‘/’分隔多人”为例正确姿势是from pyspark.sql import SparkSession from pyspark.sql.functions import col, split, explode, trim, when, regexp_replace, size spark SparkSession.builder \ .appName(NeteaseMusicClean) \ .master(local[*]) \ .getOrCreate() # 假设原始DF有列song_id, song_name, artists, album_name df_raw spark.read.csv(data/raw/songs.csv, headerTrue, inferSchemaTrue) # 步骤1清理artists字段——去空格、去HTML实体、标准化分隔符 df_cleaned df_raw.withColumn( artists_clean, trim(regexp_replace(col(artists), rnbsp;|#xa;, )) # 替换HTML空格和换行 ).withColumn( artists_clean, regexp_replace(col(artists_clean), r\s/\s, /) # 统一分隔符为单斜杠 ) # 步骤2拆分为多行一对多每行一个歌手 df_exploded df_cleaned.withColumn( artist_list, split(col(artists_clean), /) ).withColumn( artist, explode(col(artist_list)) ).withColumn( artist, trim(col(artist)) # 再次去首尾空格 ).filter(col(artist) ! ) # 过滤空歌手 # 步骤3处理专辑名中的括号和年份如“范特西 (2001)” → “范特西” df_final df_exploded.withColumn( album_name_clean, regexp_replace(col(album_name), r\s*\(\d{4}\)\s*, ) ).withColumn( album_name_clean, trim(col(album_name_clean)) )参数说明split(..., /)按斜杠切分explode()将数组展开为多行trim()防一手中间空格。全程无Python循环、无UDF执行计划清晰可见Shuffle可控。若强行用UDFpandas_udf需序列化/反序列化udf在JVM侧调用Python进程小数据集都慢半拍答辩演示时卡顿就是事故。3.2 主题层Theme Layer构建“歌曲-歌手-歌单”星型模型为分析打地基清洗后的数据是扁平的但分析需要关联。例如“周杰伦的歌在哪些歌单里被收藏最多”就需要歌曲、歌手、歌单三张表关联。我们建三张表表名主键关键字段存储路径dim_songsong_idsong_name,album_name_clean,duration_msdata/dim/song/dim_artistartist_idMD5(artist)artist_name,artist_type主唱/伴唱/乐队data/dim/artist/fact_playlist_song(playlist_id, song_id)add_time,position_in_playlistdata/fact/playlist_song/建模逻辑dim_artist用md5(artist)作主键避免中文名重复如“张楚”和“张楚歌手”fact_playlist_song不存歌单名只存ID和关系解耦维度所有表用Parquet格式存储压缩率高、谓词下推快。代码示例生成dim_artistfrom pyspark.sql.functions import md5, lower, when # 从df_exploded中提取唯一歌手 df_artist df_exploded.select(artist).distinct() \ .withColumn(artist_id, md5(lower(col(artist)))) \ .withColumn(artist_type, when(col(artist).contains(合唱), chorus) .when(col(artist).contains(乐队), band) .otherwise(solo)) df_artist.write.mode(overwrite).parquet(data/dim/artist/)注意md5(lower())确保大小写不敏感去重artist_type用when/otherwise而非UDF保持SQL优化器可见性。3.3 应用层Application Layer用Spark SQL写分析脚本比DataFrame API更贴近业务语言毕业设计答辩时老师更愿听你讲“我分析了用户收藏行为”而不是“我用了join()和groupBy()”。把分析逻辑写成SQL可读性、可维护性、可解释性拉满。例如“各语种歌单的平均歌曲时长分布”-- 文件: sql/analysis_lang_duration.sql WITH lang_tag AS ( SELECT playlist_id, CASE WHEN tags LIKE %日语% OR tags LIKE %J-POP% THEN Japanese WHEN tags LIKE %韩语% OR tags LIKE %K-POP% THEN Korean WHEN tags LIKE %英语% OR tags LIKE %英文% THEN English ELSE Chinese END AS lang FROM dim_playlist ), playlist_avg AS ( SELECT l.lang, AVG(s.duration_ms) / 1000.0 AS avg_duration_sec FROM fact_playlist_song f JOIN lang_tag l ON f.playlist_id l.playlist_id JOIN dim_song s ON f.song_id s.song_id GROUP BY l.lang ) SELECT * FROM playlist_avg ORDER BY avg_duration_sec DESC;执行方式spark-sql -f sql/analysis_lang_duration.sql -o result/lang_duration.csv优势SQL天然支持CTE、窗口函数、复杂CASE业务逻辑一目了然.sql文件可单独测试、版本管理答辩时直接贴SQL老师扫一眼就懂你在算什么。4. 避坑指南那些让我重跑3遍Spark任务的血泪经验4.1 现象spark-submit本地运行正常集群提交后java.lang.ClassNotFoundException: org.jsoup.Jsoup原因Jsoup是解析HTML必需的jar包本地模式local[*]自动加载$SPARK_HOME/jars/下的jar但YARN集群模式默认不传。--jars参数必须显式指定且路径要是HDFS或HTTP可访问地址。解决将jsoup-1.17.2.jar上传至HDFShdfs dfs -put jsoup-1.17.2.jar /lib/提交命令加--jars hdfs:///lib/jsoup-1.17.2.jar更稳妥做法用--packages org.jsoup:jsoup:1.17.2Spark自动下载需集群能联网4.2 现象清洗后artist字段出现乱码且数量随数据量增大而增多原因网易云部分歌单页响应头声明charsetGBK但实际内容是UTF-8。Requests默认按响应头解码导致中文变。解决强制指定编码resp session.get(url, headersheaders, timeout10) resp.encoding utf-8 # 覆盖响应头声明 html resp.text4.3 现象fact_playlist_song表中playlist_id为空导致后续JOIN全丢弃原因爬取时部分歌单页script里playlist.id字段缺失如私密歌单parse_playlist_html()返回空字典song_id等字段全为None写入Parquet后变NULL。解决清洗层加强校验在df_raw读入后立即过滤df_raw df_raw.filter(col(playlist_id).isNotNull() (col(playlist_id) ! ))4.4 现象spark-sql执行COUNT(*)极慢EXPLAIN显示全表Scan未走分区剪枝原因Parquet表未按常用过滤字段如date_partition分区物理文件无目录结构。解决写入时显式分区df_cleaned.write \ .mode(overwrite) \ .partitionBy(date_partition) \ # date_partition为字符串列如20240315 .parquet(data/clean/songs/)查询时加WHERE date_partition 20240315Spark自动跳过其他分区目录。4.5 现象本地spark-shell能跑通IDEA里spark-submit报NoClassDefFoundError: scala/Product原因Scala版本冲突。Spark 3.4用Scala 2.13而你的项目pom.xml可能引了Scala 2.12的库。解决统一Scala版本在pom.xml中properties scala.version2.13.12/scala.version /properties dependencies dependency groupIdorg.apache.spark/groupId artifactIdspark-sql_2.13/artifactId version3.4.1/version /dependency /dependencies注意spark-sql_2.13的_2.13后缀必须匹配。5. 让分析结果“活”起来用轻量级Web服务暴露Spark计算结果5.1 为什么不用FlaskSpark Context用REST API桥接更安全、更解耦有人想在Flask路由里直接调spark.sql()这是大忌。SparkContext是线程不安全的Web服务器多线程并发请求会引发状态混乱且Web进程常驻Spark资源无法及时释放。正确做法Spark预计算结果存入轻量数据库SQLiteWeb服务只读取。SQLite单文件、零配置、ACID完美匹配毕业设计——result.db就是一个文件答辩拷贝走即可。# job/export_to_sqlite.py每日定时导出分析结果 import sqlite3 from pyspark.sql import SparkSession spark SparkSession.builder.appName(ExportToSQLite).getOrCreate() conn sqlite3.connect(result.db) # 导出“各语种平均时长”结果 df_result spark.sql(SELECT lang, avg_duration_sec FROM ...) df_result.toPandas().to_sql(lang_duration, conn, if_existsreplace, indexFalse) conn.close()5.2 用FastAPI写一个30行接口让老师扫码看图表FastAPI比Flask更现代、自动生成文档、异步友好。核心接口只需3个GET /api/lang-duration返回JSON数据GET /api/top-songs返回播放量TOP10GET /返回Vue前端单HTML文件内联Chart.js# api/main.py from fastapi import FastAPI from fastapi.responses import HTMLResponse, JSONResponse import sqlite3 import json app FastAPI() app.get(/api/lang-duration) def get_lang_duration(): conn sqlite3.connect(result.db) cur conn.cursor() cur.execute(SELECT lang, avg_duration_sec FROM lang_duration ORDER BY avg_duration_sec DESC) rows cur.fetchall() conn.close() return JSONResponse(content[{lang: r[0], avg_sec: r[1]} for r in rows]) app.get(/, response_classHTMLResponse) def read_root(): with open(static/index.html, r, encodingutf-8) as f: return HTMLResponse(contentf.read(), status_code200)前端static/index.html用Chart.js画柱状图数据通过fetch(/api/lang-duration)加载。部署只需uvicorn api.main:app --host 0.0.0.0 --port 8000老师手机扫码即看比截图PPT高级十倍。5.3 答辩现场应急技巧当Spark任务卡住5分钟内救场的3个命令毕业答辩演示最怕卡死。我备了三招查进度curl http://localhost:4040/api/v1/applications/获取当前App ID再查/applications/{app-id}/stages看Stage卡在哪杀任务yarn application -kill {app-id}YARN模式或kill -9 {pid}本地模式切降级数据提前准备data/sample/小数据集100行演示脚本里加开关if os.getenv(DEMO_MODE): df spark.read.csv(data/sample/songs.csv) else: df spark.read.parquet(data/clean/songs/)最后说句实在话这个选题的价值不在于你做出多惊艳的结论而在于你能否把“数据从网页到图表”的每一步都解释清楚为什么这么做、不那么做会怎样。我带过的某高校毕业设计学生答辩时被问“为什么用Parquet不用CSV”他答“CSV没Schema每次读都要infer耗时且不准Parquet自带Schema还能列裁剪我查10个字段只读2个快3倍。”——老师当场点头。这种细节里的确定性才是工程师的底气。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进