易云音樂數(shù)據(jù)分析:從爬蟲到可視化的數(shù)據(jù)工程實(shí)戰(zhàn))
簡(jiǎn)介本資源是一套完整的本科畢業(yè)設(shè)計(jì)項(xiàng)目實(shí)現(xiàn)面向大數(shù)據(jù)專業(yè)學(xué)生及Spark初學(xué)者聚焦音樂平臺(tái)真實(shí)場(chǎng)景的數(shù)據(jù)分析實(shí)踐。項(xiàng)目基于Apache Spark構(gòu)建分布式分析流水線覆蓋用戶行為、歌曲熱度、群體畫像、時(shí)段偏好與評(píng)論情感五大分析方向可直接用于課程設(shè)計(jì)、畢設(shè)開題與工程復(fù)現(xiàn)。壓縮包共404個(gè)文件9.67MB包含123個(gè)Java/Scala核心業(yè)務(wù)代碼含Spark SQL與Streaming模塊、56個(gè)JS36個(gè)HTML前端可視化頁面、35個(gè)PNG/JPG圖表素材以及l(fā)og4j、Flume、Bootstrap等配套配置與樣式資源結(jié)構(gòu)清晰模塊解耦明確。已有2604人學(xué)習(xí)下載提供從數(shù)據(jù)采集、清洗、計(jì)算到Web展示的全鏈路代碼與配置附帶AmazeUI/Font Awesome等成熟前端組件集成方案便于快速部署與二次開發(fā)。1. 畢業(yè)設(shè)計(jì)真能跑通 Spark 分析網(wǎng)易云音樂數(shù)據(jù)別被“爬蟲Spark”標(biāo)題騙了這其實(shí)是數(shù)據(jù)工程能力的完整閉環(huán)檢驗(yàn)很多同學(xué)看到“基于Spark網(wǎng)易云音樂數(shù)據(jù)分析”這個(gè)畢業(yè)設(shè)計(jì)標(biāo)題第一反應(yīng)是爬點(diǎn)歌單、用Spark SQL查個(gè)播放量TOP10、畫個(gè)詞云交差。但真實(shí)落地時(shí)90%的人卡在第三步——數(shù)據(jù)根本進(jìn)不了Spark。不是因?yàn)椴粫?huì)寫sc.textFile()而是因?yàn)樵紨?shù)據(jù)壓根沒清洗成可計(jì)算形態(tài)歌單ID混著用戶ID、評(píng)論時(shí)間戳格式不統(tǒng)一、歌手字段里塞著“/”分隔的多人名、甚至JSON嵌套三層還帶HTML轉(zhuǎn)義字符。這不是Spark的問題是數(shù)據(jù)管道沒建起來。這個(gè)選題真正考驗(yàn)的是能否把一個(gè)非結(jié)構(gòu)化、高噪聲、強(qiáng)時(shí)效性的互聯(lián)網(wǎng)公開數(shù)據(jù)流通過合理分層原始層→清洗層→主題層→應(yīng)用層變成可復(fù)用、可驗(yàn)證、可回溯的分析資產(chǎn)。適合想扎實(shí)掌握大數(shù)據(jù)開發(fā)全流程的本科生尤其適合那些簡(jiǎn)歷上寫著“熟悉Hadoop生態(tài)”但連spark-submit --jars加依賴都配錯(cuò)三次的同學(xué)。它不追求模型多炫而要求每一步都有日志、有校驗(yàn)、有退路——比如某次ETL任務(wù)失敗后5分鐘內(nèi)能定位到是某條歌單的封面URL里多了個(gè)不可見的零寬空格。2. 從網(wǎng)頁源碼到DataFrame三步構(gòu)建可復(fù)用的網(wǎng)易云音樂數(shù)據(jù)采集鏈路2.1 為什么不用Selenium用RequestsBeautifulSoup正則組合拳才是畢業(yè)設(shè)計(jì)的務(wù)實(shí)選擇畢業(yè)設(shè)計(jì)不是工業(yè)級(jí)爬蟲項(xiàng)目不需要模擬登錄、處理滑塊驗(yàn)證碼或應(yīng)對(duì)JS渲染。網(wǎng)易云音樂的歌單頁、歌曲詳情頁、熱門評(píng)論頁其核心數(shù)據(jù)歌單名、創(chuàng)建者、歌曲列表、評(píng)論內(nèi)容、點(diǎn)贊數(shù)全部存在于HTML靜態(tài)響應(yīng)中。Selenium啟動(dòng)瀏覽器、等待渲染、管理驅(qū)動(dòng)版本對(duì)本地調(diào)試極其不友好——你改一行XPath等Chrome加載完再看報(bào)錯(cuò)節(jié)奏全亂。而RequestsBS4組合配合requests.adapters.HTTPAdapter設(shè)置重試策略和連接池5分鐘就能跑通一頁。關(guān)鍵代碼如下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() # 復(fù)用連接避免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() # 網(wǎng)易云有反爬必須加隨機(jī)延遲否則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是硬性要求網(wǎng)易云會(huì)校驗(yàn)來源頁。漏掉這行所有請(qǐng)求返回空HTML。User-Agent建議用最新版Chrome避免被識(shí)別為爬蟲。2.2 解析歌單頁用正則提取JSON數(shù)據(jù)比XPath更穩(wěn)因?yàn)榫W(wǎng)易云把結(jié)構(gòu)化數(shù)據(jù)藏在script里翻看網(wǎng)易云歌單頁源碼你會(huì)發(fā)現(xiàn)頁面主體是空的div idg_main真正的數(shù)據(jù)藏在script標(biāo)簽里的一段JavaScript變量賦值中形如var playlist { id: 123, name: ...};。用XPath定位div classu-cover u-cover-1下的文本極易因前端微調(diào)而失效而正則匹配var playlist (.*?);只要變量名不變就穩(wěn)如磐石。實(shí)測(cè)對(duì)比XPath在網(wǎng)易云2023年Q4前端重構(gòu)后全部失效正則方案零修改繼續(xù)運(yùn)行。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: # 去除注釋、修復(fù)末尾逗號(hào)部分版本JS有語法錯(cuò)誤 json_str re.sub(r//.*?$, , match.group(1), flagsre.MULTILINE) json_str re.sub(r,\s*}, }, json_str) # 修復(fù)非法結(jié)尾逗號(hào) return json.loads(json_str) except json.JSONDecodeError as e: print(fJSON解析失敗: {e}) return {} # 示例調(diào)用 html fetch_playlist_page(23456789, HEADERS) playlist_data parse_playlist_html(html) print(f歌單名: {playlist_data.get(name)}, 歌曲數(shù): {playlist_data.get(trackCount)})邏輯說明re.DOTALL讓.匹配換行符確??缧蠮SON能捕獲json.loads()前先做兩處容錯(cuò)刪JS單行注釋、修結(jié)尾逗號(hào)——這是網(wǎng)易云前端工程師留下的“彩蛋”不處理就會(huì)JSONDecodeError。參數(shù)playlist_data字典結(jié)構(gòu)穩(wěn)定含tracks鍵歌曲列表、creator鍵創(chuàng)建者信息、tags鍵歌單標(biāo)簽直接可轉(zhuǎn)為Pandas DataFrame。2.3 構(gòu)建最小可行數(shù)據(jù)集只抓10個(gè)高質(zhì)量歌單勝過1000個(gè)臟數(shù)據(jù)畢業(yè)設(shè)計(jì)不是數(shù)據(jù)競(jìng)賽數(shù)據(jù)質(zhì)量數(shù)據(jù)量。盲目擴(kuò)大爬取范圍只會(huì)讓后續(xù)清洗工作爆炸式增長(zhǎng)。我的做法是人工篩選10個(gè)典型歌單——3個(gè)華語熱歌榜含周杰倫、陳綺貞、新褲子樂隊(duì)、3個(gè)小眾獨(dú)立音樂實(shí)驗(yàn)電子、后搖、City Pop、2個(gè)語種混合日語動(dòng)漫OST、韓語K-Pop、2個(gè)場(chǎng)景化歌單“咖啡館背景音”“深夜emo專用”。理由很實(shí)在覆蓋不同數(shù)據(jù)模式中文名/外文名/混合名、單歌手/多歌手/樂隊(duì)名、標(biāo)簽豐富/標(biāo)簽稀疏且每個(gè)歌單歌曲數(shù)控制在50首以內(nèi)總數(shù)據(jù)量約500條本地CSV僅2MBSpark本地模式秒級(jí)完成測(cè)試。記住畢業(yè)答辯時(shí)老師問“你如何保證數(shù)據(jù)代表性”你指著這10個(gè)歌單的命名邏輯和覆蓋維度比說“我爬了10萬條”有力十倍。3. 數(shù)據(jù)清洗與分層建模用PySpark把臟數(shù)據(jù)煉成分析燃料3.1 清洗層Clean Layer用DataFrame API做原子化操作拒絕UDF黑匣子很多同學(xué)一上來就寫UDF用戶自定義函數(shù)處理歌手名分割結(jié)果性能暴跌、調(diào)試?yán)щy。PySpark的內(nèi)置函數(shù)足夠應(yīng)付90%清洗需求。以“歌手字段含‘/’分隔多人”為例正確姿勢(shì)是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() # 假設(shè)原始DF有列song_id, song_name, artists, album_name df_raw spark.read.csv(data/raw/songs.csv, headerTrue, inferSchemaTrue) # 步驟1清理artists字段——去空格、去HTML實(shí)體、標(biāo)準(zhǔn)化分隔符 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, /) # 統(tǒng)一分隔符為單斜杠 ) # 步驟2拆分為多行一對(duì)多每行一個(gè)歌手 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處理專輯名中的括號(hào)和年份如“范特西 (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)) )參數(shù)說明split(..., /)按斜杠切分explode()將數(shù)組展開為多行trim()防一手中間空格。全程無Python循環(huán)、無UDF執(zhí)行計(jì)劃清晰可見Shuffle可控。若強(qiáng)行用UDFpandas_udf需序列化/反序列化udf在JVM側(cè)調(diào)用Python進(jìn)程小數(shù)據(jù)集都慢半拍答辯演示時(shí)卡頓就是事故。3.2 主題層Theme Layer構(gòu)建“歌曲-歌手-歌單”星型模型為分析打地基清洗后的數(shù)據(jù)是扁平的但分析需要關(guān)聯(lián)。例如“周杰倫的歌在哪些歌單里被收藏最多”就需要歌曲、歌手、歌單三張表關(guān)聯(lián)。我們建三張表表名主鍵關(guān)鍵字段存儲(chǔ)路徑dim_songsong_idsong_name,album_name_clean,duration_msdata/dim/song/dim_artistartist_idMD5(artist)artist_name,artist_type主唱/伴唱/樂隊(duì)data/dim/artist/fact_playlist_song(playlist_id, song_id)add_time,position_in_playlistdata/fact/playlist_song/建模邏輯dim_artist用md5(artist)作主鍵避免中文名重復(fù)如“張楚”和“張楚歌手”fact_playlist_song不存歌單名只存ID和關(guān)系解耦維度所有表用Parquet格式存儲(chǔ)壓縮率高、謂詞下推快。代碼示例生成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(樂隊(duì)), band) .otherwise(solo)) df_artist.write.mode(overwrite).parquet(data/dim/artist/)注意md5(lower())確保大小寫不敏感去重artist_type用when/otherwise而非UDF保持SQL優(yōu)化器可見性。3.3 應(yīng)用層Application Layer用Spark SQL寫分析腳本比DataFrame API更貼近業(yè)務(wù)語言畢業(yè)設(shè)計(jì)答辯時(shí)老師更愿聽你講“我分析了用戶收藏行為”而不是“我用了join()和groupBy()”。把分析邏輯寫成SQL可讀性、可維護(hù)性、可解釋性拉滿。例如“各語種歌單的平均歌曲時(shí)長(zhǎng)分布”-- 文件: 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;執(zhí)行方式spark-sql -f sql/analysis_lang_duration.sql -o result/lang_duration.csv優(yōu)勢(shì)SQL天然支持CTE、窗口函數(shù)、復(fù)雜CASE業(yè)務(wù)邏輯一目了然.sql文件可單獨(dú)測(cè)試、版本管理答辯時(shí)直接貼SQL老師掃一眼就懂你在算什么。4. 避坑指南那些讓我重跑3遍Spark任務(wù)的血淚經(jīng)驗(yàn)4.1 現(xiàn)象spark-submit本地運(yùn)行正常集群提交后java.lang.ClassNotFoundException: org.jsoup.Jsoup原因Jsoup是解析HTML必需的jar包本地模式local[*]自動(dòng)加載$SPARK_HOME/jars/下的jar但YARN集群模式默認(rèn)不傳。--jars參數(shù)必須顯式指定且路徑要是HDFS或HTTP可訪問地址。解決將jsoup-1.17.2.jar上傳至HDFShdfs dfs -put jsoup-1.17.2.jar /lib/提交命令加--jars hdfs:///lib/jsoup-1.17.2.jar更穩(wěn)妥做法用--packages org.jsoup:jsoup:1.17.2Spark自動(dòng)下載需集群能聯(lián)網(wǎng)4.2 現(xiàn)象清洗后artist字段出現(xiàn)亂碼且數(shù)量隨數(shù)據(jù)量增大而增多原因網(wǎng)易云部分歌單頁響應(yīng)頭聲明charsetGBK但實(shí)際內(nèi)容是UTF-8。Requests默認(rèn)按響應(yīng)頭解碼導(dǎo)致中文變。解決強(qiáng)制指定編碼resp session.get(url, headersheaders, timeout10) resp.encoding utf-8 # 覆蓋響應(yīng)頭聲明 html resp.text4.3 現(xiàn)象fact_playlist_song表中playlist_id為空導(dǎo)致后續(xù)JOIN全丟棄原因爬取時(shí)部分歌單頁script里playlist.id字段缺失如私密歌單parse_playlist_html()返回空字典song_id等字段全為None寫入Parquet后變NULL。解決清洗層加強(qiáng)校驗(yàn)在df_raw讀入后立即過濾df_raw df_raw.filter(col(playlist_id).isNotNull() (col(playlist_id) ! ))4.4 現(xiàn)象spark-sql執(zhí)行COUNT(*)極慢EXPLAIN顯示全表Scan未走分區(qū)剪枝原因Parquet表未按常用過濾字段如date_partition分區(qū)物理文件無目錄結(jié)構(gòu)。解決寫入時(shí)顯式分區(qū)df_cleaned.write \ .mode(overwrite) \ .partitionBy(date_partition) \ # date_partition為字符串列如20240315 .parquet(data/clean/songs/)查詢時(shí)加WHERE date_partition 20240315Spark自動(dòng)跳過其他分區(qū)目錄。4.5 現(xiàn)象本地spark-shell能跑通IDEA里spark-submit報(bào)NoClassDefFoundError: scala/Product原因Scala版本沖突。Spark 3.4用Scala 2.13而你的項(xiàng)目pom.xml可能引了Scala 2.12的庫。解決統(tǒng)一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. 讓分析結(jié)果“活”起來用輕量級(jí)Web服務(wù)暴露Spark計(jì)算結(jié)果5.1 為什么不用FlaskSpark Context用REST API橋接更安全、更解耦有人想在Flask路由里直接調(diào)spark.sql()這是大忌。SparkContext是線程不安全的Web服務(wù)器多線程并發(fā)請(qǐng)求會(huì)引發(fā)狀態(tài)混亂且Web進(jìn)程常駐Spark資源無法及時(shí)釋放。正確做法Spark預(yù)計(jì)算結(jié)果存入輕量數(shù)據(jù)庫SQLiteWeb服務(wù)只讀取。SQLite單文件、零配置、ACID完美匹配畢業(yè)設(shè)計(jì)——result.db就是一個(gè)文件答辯拷貝走即可。# job/export_to_sqlite.py每日定時(shí)導(dǎo)出分析結(jié)果 import sqlite3 from pyspark.sql import SparkSession spark SparkSession.builder.appName(ExportToSQLite).getOrCreate() conn sqlite3.connect(result.db) # 導(dǎo)出“各語種平均時(shí)長(zhǎng)”結(jié)果 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寫一個(gè)30行接口讓老師掃碼看圖表FastAPI比Flask更現(xiàn)代、自動(dòng)生成文檔、異步友好。核心接口只需3個(gè)GET /api/lang-duration返回JSON數(shù)據(jù)GET /api/top-songs返回播放量TOP10GET /返回Vue前端單HTML文件內(nèi)聯(lián)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畫柱狀圖數(shù)據(jù)通過fetch(/api/lang-duration)加載。部署只需uvicorn api.main:app --host 0.0.0.0 --port 8000老師手機(jī)掃碼即看比截圖PPT高級(jí)十倍。5.3 答辯現(xiàn)場(chǎng)應(yīng)急技巧當(dāng)Spark任務(wù)卡住5分鐘內(nèi)救場(chǎng)的3個(gè)命令畢業(yè)答辯演示最怕卡死。我備了三招查進(jìn)度curl http://localhost:4040/api/v1/applications/獲取當(dāng)前App ID再查/applications/{app-id}/stages看Stage卡在哪殺任務(wù)yarn application -kill {app-id}YARN模式或kill -9 {pid}本地模式切降級(jí)數(shù)據(jù)提前準(zhǔn)備data/sample/小數(shù)據(jù)集100行演示腳本里加開關(guān)if os.getenv(DEMO_MODE): df spark.read.csv(data/sample/songs.csv) else: df spark.read.parquet(data/clean/songs/)最后說句實(shí)在話這個(gè)選題的價(jià)值不在于你做出多驚艷的結(jié)論而在于你能否把“數(shù)據(jù)從網(wǎng)頁到圖表”的每一步都解釋清楚為什么這么做、不那么做會(huì)怎樣。我?guī)н^的某高校畢業(yè)設(shè)計(jì)學(xué)生答辯時(shí)被問“為什么用Parquet不用CSV”他答“CSV沒Schema每次讀都要infer耗時(shí)且不準(zhǔn)Parquet自帶Schema還能列裁剪我查10個(gè)字段只讀2個(gè)快3倍?!薄蠋煯?dāng)場(chǎng)點(diǎn)頭。這種細(xì)節(jié)里的確定性才是工程師的底氣。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取