
簡介面向Python爬蟲與數(shù)據(jù)分析初學(xué)者的B站動漫數(shù)據(jù)項目以嗶哩嗶哩番劇為對象演示從總榜爬取動漫列表、訪問詳情頁抓取追番人數(shù)與評分再到數(shù)據(jù)清洗、統(tǒng)計分析與可視化的完整流程適合作為爬蟲與數(shù)據(jù)分析方向的入門實戰(zhàn)參考。壓縮包共23個文件體積約3.58MB包含3個交互式筆記代碼文件、2份表格數(shù)據(jù)、10張可視化圖片、1份說明文檔及少量配置類文件覆蓋爬取、預(yù)處理、分析三條主要環(huán)節(jié)能直觀看到不同步驟的產(chǎn)出。目前已有2978人學(xué)習(xí)下載。借助這套資料讀者可同時獲得可運行代碼、真實抓取數(shù)據(jù)和成圖結(jié)果結(jié)合文檔中對請求解析、字段抽取、數(shù)據(jù)清洗、圖表繪制等細(xì)節(jié)的介紹能較快理解一類番劇數(shù)據(jù)項目的實現(xiàn)思路節(jié)省自行搭建和排錯的時間。1. 先想清楚B站動漫爬蟲項目到底要抓哪些數(shù)據(jù)想給B站動漫做數(shù)據(jù)分析第一個能落地的選題就是爬番劇列表標(biāo)題、評分、播放量、追番人數(shù)、平臺歸屬和動漫類型一個周末能出結(jié)果。別被“爬蟲”兩個字嚇住這個項目的難點根本不在反爬而在數(shù)據(jù)清洗和口徑選擇——B站接口返回的score是一個嵌套字典play字段可能是字符串評分0的番劇到底算“沒開分”還是“真爛”這些才是真正花時間的地方。適合三種人第一次寫完整爬蟲的新手、想把數(shù)據(jù)分析技能用在真實場景的學(xué)生、想觀察二次元內(nèi)容走勢的從業(yè)者。下面我把從請求到可視化的完整路徑拆給你照著跑晚上就能看到第一張B站動漫數(shù)據(jù)圖表。2. 用requestsxpath抓B站動漫數(shù)據(jù)請求、解析與翻頁參數(shù)2.1 為什么選requestsxpath而不是scrapy說到爬蟲很多人的第一反應(yīng)是scrapy但遇到這個項目我一般會勸新手先別上框架。scrapy的學(xué)習(xí)曲線體現(xiàn)在項目結(jié)構(gòu)、Item Pipeline、Middleware 這些概念上而你要抓的不過是B站動漫索引這一個站點、幾十個字段、幾百條數(shù)據(jù)。用requests加xpath三步就能跑通發(fā)請求、拿響應(yīng)、解析字段。等量級大到要天天跑、要防封、要分布式采集時再換scrapy也不遲。方案適用場景上手成本主要坑點requests xpath小批量、單站點、字段結(jié)構(gòu)穩(wěn)定低重試和頻率得自己管scrapy大規(guī)模、多站點、需要長期維護(hù)中框架概念多調(diào)試成本高playwright頁面要執(zhí)行JS才能渲染高內(nèi)存占用大運行慢選擇邏輯很簡單B站動漫索引頁在服務(wù)端渲染了大部分列表內(nèi)容HTML里能直接拿到標(biāo)題和鏈接同時它的JSON接口也能返回格式化的字段。用requests就是把網(wǎng)絡(luò)爬蟲原理里的“請求—響應(yīng)—解析”三步走完xpath負(fù)責(zé)從HTML靜態(tài)結(jié)構(gòu)里把節(jié)點摘出來。接口變動時還能切到JSON方案兜底兩條路都不丟。2.2 抓取熱播番劇列表請求接口的完整代碼與參數(shù)說明我先給出用JSON接口抓取番劇列表的完整寫法。B站有一個半開放的番劇索引接口返回的是結(jié)構(gòu)清晰的JSON比從HTML里用xpath摳文本要穩(wěn)。第一次請求時先把響應(yīng)打出來看結(jié)構(gòu)這步很重要后面所有清洗邏輯都建立在字段名正確的前提上。import requests from requests.adapters import HTTPAdapter import time session requests.Session() session.mount(https://, HTTPAdapter(max_retries2)) session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36, Referer: https://www.bilibili.com/anime/, Origin: https://www.bilibili.com, }) API_URL https://api.bilibili.com/pgc/season/index/result params { season_type: 1, # 1 代表番劇 type: sort, # 排序請求 sort_type: 2, # 2 按追番人數(shù)排序 page: 1, pagesize: 20, # 每頁返回條數(shù) } resp session.get(API_URL, paramsparams, timeout10) resp.raise_for_status() data resp.json() # 先看返回結(jié)構(gòu)確認(rèn)字段名再寫后續(xù)邏輯 print(data[code], data[message]) print(data[data][list][0])這段代碼里有三個地方新手容易忽略。第一session.mount配合HTTPAdapter可以把超時重試交給底層處理不用自己寫try except套循環(huán)第二User-Agent和Referer是B站服務(wù)端識別請求來源的重要依據(jù)少了Referer很容易拿到412或403響應(yīng)第三params里的season_type和sort_type決定你抓的是“番劇”還是“電影”是“按播放量排”還是“按追番數(shù)排”。先打印第一頁數(shù)據(jù)確認(rèn)score是字典還是數(shù)字確認(rèn)platform存在與否再往下寫。2.3 翻頁、請求頭與超時重試穩(wěn)定采集三件套單頁數(shù)據(jù)撐不起分析翻頁才是采集的主體。翻頁時有個血淚經(jīng)驗不要for page in range(1, 100)一口氣跑完控制請求頻率不是玄學(xué)是給服務(wù)端留響應(yīng)時間也是給自己留后退的余地。每請求一頁sleep 0.5到1秒看起來慢實際上20頁只需要幾十秒遠(yuǎn)比觸發(fā)風(fēng)控后換IP來得省事。def fetch_anime_page(page: int): params[page] page resp session.get(API_URL, paramsparams, timeout10) resp.raise_for_status() payload resp.json() if payload[code] ! 0: # 接口返回異常時先停下來看 message不要盲目重試 print(fpage {page} return code: {payload[code]}, msg: {payload[message]}) return [] items payload[data][list] rows [] for item in items: rows.append({ title: item.get(title), score: item.get(score), # 這里可能是 dict后面再拆 play: item.get(play), follow: item.get(follow), type_name: item.get(type_name), platform: item.get(platform), }) return rows all_rows [] for page in range(1, 11): # 先抓前10頁200條 rows fetch_anime_page(page) print(fpage {page}: got {len(rows)} rows) all_rows.extend(rows) time.sleep(1) # 請求間隔控制頻率代碼邏輯不復(fù)雜但有兩個設(shè)計取舍值得講。第一fetch函數(shù)內(nèi)部先判斷payload[code]code不等于0時直接返回空列表并打印message。B站索引接口在請求過密時會返回風(fēng)控提示把code打印出來能幫你區(qū)分“我方請求有問題”和“服務(wù)端拒絕服務(wù)”避免在錯誤方向上加戲。第二rows里統(tǒng)一用item.get()而不是item[]這樣字段缺失時返回None而不是拋KeyError后面pandas清洗會省很多事。如果你遇到接口完全不可用另一個常見做法是用requests抓HTML再用lxml的etree和xpath解析列表節(jié)點核心區(qū)別只是在解析層請求層的session邏輯可以原樣復(fù)用。3. 數(shù)據(jù)清洗與存儲把JSON響應(yīng)整理成能分析的DataFrame3.1 原始字段里哪些能用、哪些要拆開用抓回來的原始數(shù)據(jù)是列表套字典的結(jié)構(gòu)直接塞進(jìn)DataFrame之前先看清每個字段的“真實身份”。我在第一次跑這個項目時犯過一個典型錯誤天真地以為score是一列數(shù)字結(jié)果DataFrame里那一列全是字典對象排序直接翻車。B站接口里的score實際是嵌套結(jié)構(gòu)包含score和user_count兩個子字段必須提前拆開。原始字段實際類型分析用途清洗動作titlestr番劇名稱主鍵去重、去空格playint 或 str播放量轉(zhuǎn)數(shù)值處理非數(shù)字字符followint追番人數(shù)轉(zhuǎn)數(shù)值scoredict 或 int評分和評分人數(shù)拆出score子字段type_namestr動漫類型如“搞笑”“戰(zhàn)斗”分組聚合platformstr播出平臺統(tǒng)計平臺分布拆字段的操作其實在爬蟲環(huán)節(jié)就能做。每一條item經(jīng)過清洗后只保留分析需要的維度不要貪多。尤其是platform這個字段部分番劇是B站獨播部分是多平臺聯(lián)播后面做類型分析時它是個很容易被忽略的維度。數(shù)據(jù)是黑匣子你存進(jìn)去什么分析時就只能看到什么提前把字段拆干凈比事后補救便宜。3.2 空值、字符串?dāng)?shù)字和評分0pandas清洗三板斧數(shù)據(jù)落地到pandas后第一件事不是畫圖是看df.info()和df.isna().sum()。這兩個命令能告訴你哪些列有空值、哪些列類型不對。play和follow是數(shù)字但如果接口在某些情況下返回了帶逗號的字符串排序時就會按字典序排1萬播放會被排在2千后面。import pandas as pd df pd.DataFrame(all_rows) print(df.info()) # 字符串?dāng)?shù)字統(tǒng)一轉(zhuǎn)成數(shù)值轉(zhuǎn)不動的置為 NaN df[play] pd.to_numeric(df[play], errorscoerce) df[follow] pd.to_numeric(df[follow], errorscoerce) # score 是字典的拆出真正分?jǐn)?shù)直接是數(shù)值的保留 def extract_score(value): if isinstance(value, dict): return value.get(score) return value df[score] df[score].apply(extract_score) df[score] pd.to_numeric(df[score], errorscoerce) # 去掉標(biāo)題為空的記錄保留數(shù)據(jù)完整的行用于評分分析 df_dedup df.drop_duplicates(subset[title]) df_dedup df_dedup.dropna(subset[title, play]) print(df_dedup.head()) print(df_dedup.isna().sum())這段清洗有兩個參數(shù)值得講清楚。pd.to_numeric里的errorscoerce作用是把無法解析的字符串置成NaN而不是拋異常這是處理臟數(shù)據(jù)的標(biāo)準(zhǔn)姿勢你永遠(yuǎn)不知道接口里會出現(xiàn)什么奇怪的垃圾值。score的提取用isinstance判斷類型兼容“接口返回字典”和“已經(jīng)優(yōu)化成數(shù)值”兩種情況避免接口一變代碼就報廢。drop_duplicates和dropna的目的是保證后續(xù)排序、聚類的數(shù)據(jù)干凈標(biāo)題去重按番劇名播放量缺失的行直接丟棄因為排行分析里缺播放量就沒有意義。3.3 存CSV還是SQLite按你的分析目標(biāo)選清洗完的數(shù)據(jù)總要落盤。兩種選擇CSV適合一次性的分析流程打開即用Excel也能看SQLite適合你后面要反復(fù)查、要做條件篩選的場景。如果你只是寫完這篇就交差CSV就夠了如果你想在周末里反復(fù)調(diào)整分析邏輯SQLite的體驗會好很多尤其數(shù)據(jù)量超過幾千條時SQL的聚合效率遠(yuǎn)超一遍遍跑pandas。# 方案一CSV注意編碼要用 utf-8-sig df_dedup.to_csv(bilibili_anime.csv, indexFalse, encodingutf-8-sig) # 方案二SQLite方便后續(xù) SQL 查詢 import sqlite3 conn sqlite3.connect(bilibili_anime.db) df_dedup.to_sql(anime, conn, if_existsreplace, indexFalse) conn.close()這兩個參數(shù)是踩坑后才懂的to_csv的encodingutf-8-sig專門解決Excel打開中文亂碼的問題稍后避坑章節(jié)會細(xì)說to_sql的if_existsreplace解決重復(fù)跑腳本時表已存在的問題replace會直接重建表適合分析階段的反復(fù)迭代。至于列類型pandas會自動推斷int列存成INTEGERfloat列存成REAL不需要手工建表省一步是一步。4. 數(shù)據(jù)分析播放量、評分和類型分布怎么算4.1 播放量TOP10與追番人數(shù)排行數(shù)據(jù)干凈了分析才有底氣。第一個必出的分析是播放量排行找頭部番劇。這里有個細(xì)節(jié)nlargest比sort_values().head()更直接它在底層做了優(yōu)化而且只對單列做排序不會打亂其他列的順序。top10_play df_dedup.nlargest(10, play)[[title, play, follow, score]].copy() print(top10_play.to_string(indexFalse)) top10_follow df_dedup.nlargest(10, follow)[[title, follow, play, score]].copy() print(top10_follow.to_string(indexFalse))排行表出來后第一個結(jié)論往往就藏在對比里。你會發(fā)現(xiàn)播放量最高的番劇不一定追番人數(shù)最多有的番劇是“路人盤大”點開看一眼但不追番有的番劇是“粉絲粘性高”追番人數(shù)遠(yuǎn)超播放量。這兩個指標(biāo)一對比就能看出內(nèi)容的出圈屬性和粉絲屬性。分析時不要只看絕對數(shù)把play和follow都打印出來再算一列“播放追番比”這個比值是內(nèi)容運營里很常見的健康度指標(biāo)。4.2 評分分布、類型結(jié)構(gòu)與數(shù)據(jù)口徑評分是這個項目里最容易翻車的維度。我的做法是先把評分切成區(qū)間看整體分布再按動漫類型分組看均值。pd.cut的bins參數(shù)決定著分的顆粒度B站番劇評分普遍偏高8分以下并不多見所以bins要按7.0到10.0的區(qū)間切切5分以下沒有實際區(qū)分度。# 評分分箱看分布 bins [0, 6, 7, 8, 9, 10] labels [0-6, 6-7, 7-8, 8-9, 9-10] df_dedup[score_bin] pd.cut(df_dedup[score], binsbins, labelslabels, rightFalse) score_dist df_dedup[score_bin].value_counts().sort_index() print(score_dist) # 按動漫類型統(tǒng)計播放量groupby 后 agg 一次算三個指標(biāo) type_stats ( df_dedup.groupby(type_name)[play] .agg([sum, mean, count]) .sort_values(sum, ascendingFalse) ) print(type_stats.head(10))這段分析有兩條邏輯要說明。第一pd.cut的rightFalse表示區(qū)間左閉右開8會進(jìn)到8-9而不是7-8這個參數(shù)不設(shè)的話默認(rèn)是右閉邊界數(shù)據(jù)會分到意外的桶里。第二groupby之后用agg一次算總和、均值、條數(shù)sum體現(xiàn)類型總盤mean體現(xiàn)單部平均水平count體現(xiàn)作品數(shù)量三個數(shù)字并排看才能判斷一個類型是“大而稀”還是“小而精”。至于波動大的分析結(jié)論先確認(rèn)你的數(shù)據(jù)口徑是“當(dāng)前累計播放量”還是“某時間段增量”——B站公開接口返回的是累計值說“播放量”之前心里得有數(shù)。4.3 把分析結(jié)論收斂成可視化指標(biāo)清單分析做得再深最后都是為了輸出。我一般會在動手畫圖前先列一張指標(biāo)和圖表類型的對照表想清楚“誰要看什么”。要回答的問題核心指標(biāo)推薦圖表哪些番劇播放最猛play柱狀圖TOP10追番粘性和播放量是否一致play/follow比散點圖評分高的番劇集中在哪個類型score均值 type_name箱線圖或柱狀圖B站獨播還是多平臺聯(lián)播占主流platform 占比餅圖這張表的價值在于讓你不盲目堆圖表。數(shù)據(jù)分析的方法不是圖越多越好而是每個圖都要對應(yīng)一個決策或一個判斷。播放量TOP10用柱狀圖平臺分布用餅圖評分分布用箱線圖。下一步畫圖時就有明確目標(biāo)了尤其是做可視化大屏?xí)r指標(biāo)清單能幫你擋住“再加個圖會顯得很厲害”的沖動。5. 避坑B站爬蟲里的403、字段變動和編碼問題排查5.1 請求被拒403UA、Referer與請求頻率現(xiàn)象前兩頁抓得好好的第三頁開始返回403隨后連第一頁都打不開。原因請求頻率太快加上請求頭里Referer缺失或錯誤。B站服務(wù)端會把“無Referer的Python請求”和“高頻同IP請求”都識別為異常訪問。解決給session配齊瀏覽器級的User-Agent和Referer每頁間隔加到1秒以上。請求節(jié)奏上做退避而不是硬碰硬遇到403先停30秒再繼續(xù)比一直重試有效。另外一個常用做法是把頁碼順序打亂尤其抓幾十頁時從中間頁開始抓能降低連續(xù)翻頁的行為特征。5.2 xpath解析返回空列表先看響應(yīng)再排查現(xiàn)象寫好的xpath表達(dá)式在瀏覽器里能定位到節(jié)點代碼里運行卻一直返回空列表。原因B站的動漫索引頁有些板塊走接口后在客戶端渲染HTML源碼里根本沒有你要的節(jié)點還有一種是class名帶了動態(tài)變化的hash后綴每次請求都不一樣。解決遇到空列表第一件事不是改xpath而是打印resp.text前500個字符看響應(yīng)體里到底有什么。如果確定是接口渲染直接換JSON接口方案如果只是class動態(tài)變化用xpath的contains函數(shù)模糊匹配比如//p[contains(class,score)]而不是寫死完整class名。5.3 評分全是0或NaN取字段與類型轉(zhuǎn)換的坑現(xiàn)象清洗完的DataFrame里score列要么是0要么是NaN沒有一個正常分?jǐn)?shù)。原因接口返回的score是字典直接當(dāng)成數(shù)值讀出來自然是錯另外部分未開分或評分人數(shù)太少的番劇接口給的score就是0。二者混在一起如果不處理分析時會把“沒評分”和“低分差評”混為一談。解決在爬蟲解析層就用isinstance判斷字典就取出score子字段數(shù)值就原樣保留。分析時用布爾掩碼區(qū)分兩類數(shù)據(jù)df[df[score] 0]做評分分析df[df[score].isna()]單獨看未開分作品占比。這個區(qū)分直接影響結(jié)論的準(zhǔn)確性。5.4 CSV用Excel打開亂碼編碼選utf-8-sig現(xiàn)象CSV文件用Notepad看正常用Excel打開中文全是亂碼。原因Excel打開CSV時默認(rèn)按GBK解碼而pandas默認(rèn)用utf-8寫入編解碼不一致就亂了。解決寫入?yún)?shù)改成encodingutf-8-sig。utf-8-sig會在文件開頭寫入BOM標(biāo)記Excel就能自動識別為UTF-8編碼。這個問題幾乎每個入門者都會遇到早改早省心。5.5 翻頁超過50頁就沒有數(shù)據(jù)接口頁數(shù)上限現(xiàn)象循環(huán)翻到第50頁左右返回的list為空但code還是0程序沒有報錯。原因B站索引接口對單次排序條件下能翻的頁數(shù)有限制超過深度后返回空列表同時連續(xù)高頻翻頁也會觸發(fā)限流。解決不要執(zhí)著于“一頁頁翻到底”。要么增大pagesize到50減少請求次數(shù)要么換一個排序條件從另一個維度重新抓比如先按追番人數(shù)抓前50頁再按播放量抓前50頁。數(shù)據(jù)采集講究用不同路徑拼全景單一路徑是有邊界的。6. 用pyecharts做可視化落地從單圖到HTML看板6.1 匯報場景用柱狀圖起手pyecharts是ECharts的Python封裝畫完直接生成HTML文件瀏覽器就能打開不需要搭建前端環(huán)境。對數(shù)據(jù)分析與可視化這個場景來說這是最省事的落地方式。柱狀圖是匯報場景的默認(rèn)起手式因為排行信息的傳達(dá)效率最高。from pyecharts.charts import Bar from pyecharts import options as opts top10 df_dedup.nlargest(10, play) bar Bar() bar.add_xaxis(top10[title].tolist()) bar.add_yaxis( 播放量, top10[play].tolist(), label_optsopts.LabelOpts(positionright), ) bar.set_global_opts( title_optsopts.TitleOpts(titleB站新番播放量TOP10), yaxis_optsopts.AxisOpts(name播放量), ) bar.set_series_opts(itemstyle_optsopts.ItemStyleOpts(color#fb7299)) bar.render(bilibili_anime_top10_play.html)有兩點細(xì)節(jié)我是吃過虧后養(yǎng)成的習(xí)慣。add_xaxis和add_yaxis都接收list類型pandas的Series要先tolist()不然圖表上會出現(xiàn)索引錯位set_global_opts里的yaxis_opts用來改坐標(biāo)軸名稱不要省后期看圖的同事不會知道你腦子里想的是播放量。顏色改成B站主色 #fb7299匯報時觀感會好很多。6.2 多圖合成一個HTML看板單張圖只能回答問題的一個側(cè)面把柱狀圖、餅圖、表格拼進(jìn)同一個HTML文件就形成了最簡版可視化大屏。pyecharts的Page組件可以串起多個圖表配套的SimplePageLayout會自動做上下布局不需要寫HTML模板。from pyecharts.charts import Bar, Pie, Page import pandas as pd platform_stats df_dedup[platform].value_counts() pie Pie() pie.add_xaxis(platform_stats.index.tolist()) pie.add_yaxis(平臺占比, platform_stats.values.tolist()) pie.set_global_opts(title_optsopts.TitleOpts(title播出平臺分布)) page Page(layoutPage.SimplePageLayout) page.add(bar, pie) page.render(bilibili_anime_dashboard.html)Page組件會把bar和pie兩個圖表對象依次渲染進(jìn)一個HTML文檔里layout參數(shù)控制排列方式。做這種組合看板時我的教訓(xùn)是圖表數(shù)量控制在三到四個以內(nèi)指標(biāo)清單里沒有對應(yīng)分析結(jié)論的圖不要加。爬蟲和數(shù)據(jù)清洗占掉了這個項目七成的時間可視化的價值在于讓分析結(jié)論一眼被看懂而不是把圖表堆成花架子。第一次跑通后你把pagesize改大、把season_type換成別的分類整套流程可以原樣復(fù)用到B站電影和紀(jì)錄片板塊。數(shù)據(jù)分析需要學(xué)哪些最重要的不是工具數(shù)量而是從抓取到出結(jié)論這整條鏈路你都親手走過一遍后面換任何數(shù)據(jù)源都只是微調(diào)參數(shù)的事。希望幫到你。本文還有配套的精品資源點擊獲取