2026/10/14 17:13:09

Python爬虫实战:B站动漫数据抓取与可视化分析全流程

Python爬虫实战:B站动漫数据抓取与可视化分析全流程 简介这是一份面向Python爬虫初学者与数据分析爱好者的实战项目资源以B站番剧为切入点串联爬虫抓取、数据清洗、统计分析与可视化全流程。项目先爬取总榜获取动漫粗略信息与直达链接再逐条访问详情页补充数据最终结合追番人数与评分等指标观察历年ACG文化发展脉络。压缩包共23个文件、约3.58MB包含3个ipynb代码笔记、2个xlsx数据集、10张png可视化结果图及若干xml配置与说明文档覆盖爬虫、数据处理、数据分析三个环节并附有readme对实现细节做系统讲解。目前已有2980人学习下载。读者可据此掌握请求构造、页面解析、数据持久化与图表呈现的完整思路也可直接复用数据集与可视化结果进行二次分析适合作为爬虫入门与数据分析练手的参考案例。1. 从番剧列表到可视化看板Python爬虫做B站动漫数据分析的完整落地路径很多人第一次想拿B站动漫数据做分析卡住的地方不是爬虫写不出来而是爬到一半发现字段对不上、翻页翻不动、可视化出来一堆看不懂的图。这个标题讲的事情其实很具体用 Python 把 B 站动漫番剧的公开列表数据抓下来清洗成结构化表格再做播放量、评分、追番人数这些维度的可视化分析。它适合两类人——一类是刚学完 requests 和 pandas、想找一个真实场景练手的新手另一类是想给内容选题、番剧推荐、舆情观察做数据支撑的从业者。整条链路的核心难点不在抓而在抓什么字段、怎么翻页、怎么把评分和播放量放在同一张图里讲清楚。2. 先想清楚抓什么B站动漫数据的字段拆解与接口选型2.1 番剧列表页到底有哪些可用字段做动漫数据分析第一步不是打开编辑器而是打开浏览器开发者工具把番剧索引页的请求看一遍。B站番剧索引页番剧区通常按全部 / 连载 / 完结分类支持按播放数、追番数、评分、更新时间排序。你在页面上能看到的每一个数字背后都是一个可抓取的字段。我一般会先列一张字段清单明确哪些是列表页直接给的、哪些必须进详情页才能拿到字段名含义来源是否必抓media_id / season_id番剧唯一标识列表接口是title番剧名称列表接口是order_type排序类型播放/追番/评分列表接口是score评分0-10部分未开分列表接口是play_count播放量列表接口是follow_count追番人数列表接口是danmaku_count弹幕数详情接口否pub_time开播时间详情接口否tags标签热血/日常/恋爱等详情接口否这里有个血泪经验评分字段在未开分时返回的是 0 或者空字符串如果你直接拿它算平均值会把整体评分拉低一大截。所以清洗阶段必须把 score 为 0 的记录单独标记而不是当成0 分处理。2.2 列表接口 vs 详情接口选型理由列表接口的好处是一次能拿几十条字段够做宏观分析播放量分布、评分分布、追番转化率缺点是标签、开播时间这类维度拿不到。详情接口字段全但请求量大几十条还好上千条就要考虑限速和缓存。我的建议是分两层抓第一层用列表接口把全量番剧的基础字段拉下来存成 CSV第二层只对分析需要的子集比如评分 Top 100 或追番 Top 100补抓详情。这样既控制了请求量又保证了分析维度够用。常见做法是先用列表接口跑通全流程确认字段没问题再决定要不要补详情。2.3 翻页与排序参数怎么设列表接口一般靠page和page_size控制分页靠order_type控制排序。这里最容易翻车的是不同排序方式下同一部番剧的排名会变但总数不变。如果你按播放量抓一遍、再按追番抓一遍会得到两份顺序不同但内容重叠的数据直接 concat 会出现重复行。正确做法是选定一个主排序比如按播放量把全量抓完排序维度作为分析时的操作而不是抓取时的操作。翻页时要注意当某一页返回空列表或条数明显少于 page_size说明已经到底应该停止而不是死循环翻下去。3. 用 requests pandas 把番剧列表抓成结构化表格3.1 最小可运行抓取脚本下面这段代码是我常用的骨架抓的是番剧索引的列表数据。注意接口地址和参数名会随站点调整实际以你开发者工具里看到的为准这里用占位说明结构。import requests import pandas as pd import time import random # 请求头带上 UA 和 Referer模拟正常浏览器访问 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Referer: https://www.bilibili.com/anime/index/, } # 列表接口地址以实际抓包为准此处为结构示意 LIST_API https://api.bilibili.com/pgc/season/index/result def fetch_page(page: int, page_size: int 20, order_type: int 3): 抓取单页番剧列表order_type3 通常表示按播放量排序 params { order: order_type, # 排序方式 page: page, # 页码从 1 开始 page_size: page_size, # 每页条数 season_type: 1, # 1 一般代表番剧 } resp requests.get(LIST_API, headersHEADERS, paramsparams, timeout10) resp.raise_for_status() data resp.json() # 不同接口返回结构不同这里取 list 字段 return data.get(data, {}).get(list, []) def crawl_all(max_page: int 50): 翻页抓取遇到空页停止 rows [] for page in range(1, max_page 1): items fetch_page(page) if not items: print(f第 {page} 页为空停止抓取) break for it in items: rows.append({ media_id: it.get(media_id), title: it.get(title), score: it.get(score), play_count: it.get(play_count), follow_count: it.get(follow_count), }) print(f第 {page} 页抓取 {len(items)} 条) # 随机休眠降低被封风险 time.sleep(random.uniform(1.0, 2.5)) return pd.DataFrame(rows) if __name__ __main__: df crawl_all(max_page30) df.to_csv(anime_list_raw.csv, indexFalse, encodingutf-8-sig) print(总条数, len(df))逻辑说明fetch_page负责单页请求把排序、页码、每页条数作为参数传进去crawl_all负责翻页循环遇到空列表就 break避免无意义请求。time.sleep(random.uniform(1.0, 2.5))是关键的限速手段固定间隔容易被识别随机间隔更接近真人操作。参数说明order_type决定排序维度常见取值里 3 是播放量、4 是追番数、5 是评分具体以抓包为准page_size不要设太大20-30 比较稳max_page是安全上限防止接口异常时无限翻页。3.2 数据清洗评分、播放量、追番数的三个坑抓下来的原始表通常不能直接用至少要处理三类问题。第一类是评分缺失。未开分的番剧 score 可能是 0 或空直接算均值会失真。处理方式是加一列标记import pandas as pd df pd.read_csv(anime_list_raw.csv) # 评分缺失标记0 或空都视为未开分 df[score_valid] df[score].apply(lambda x: pd.notna(x) and float(x) 0) # 只对有效评分算均值 valid_score_mean df.loc[df[score_valid], score].astype(float).mean() print(有效评分均值, round(valid_score_mean, 2)) print(未开分番剧数, (~df[score_valid]).sum())第二类是播放量单位。接口返回的 play_count 通常是纯数字但页面上显示的是1.2亿3456.7万。如果你从页面文本抓必须先做单位换算从接口抓就省了这一步但要注意数值类型可能是字符串。第三类是重复。同一部番剧可能因为多季、多语言版本出现多条记录media_id 相同或标题高度相似。去重时优先按 media_id其次按标题归一化去掉第X季国语版等后缀后再判断。3.3 存成 CSV 还是 SQLite小数据量的选择几百到几千条数据CSV 完全够用pandas 读写都方便还能直接用 Excel 打开检查。数据量上万、或者需要多次增量更新时SQLite 更合适可以用 media_id 做主键做 upsert避免重复抓取。我一般先用 CSV 跑通分析确认字段和清洗逻辑没问题再迁到 SQLite 做增量。迁移成本很低pandas 的to_sql一行就能写进去。不要一上来就上数据库会拖慢调试节奏。4. 可视化怎么做才有信息量评分、播放量、追番数的组合分析4.1 评分分布直方图与播放量对数散点可视化最容易犯的错是图很漂亮但看不出结论。做动漫数据分析我建议先做两张基础图评分分布直方图、播放量-追番数散点图。import matplotlib.pyplot as plt import numpy as np # 设置中文字体避免乱码 plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False df_valid df[df[score_valid]].copy() df_valid[score] df_valid[score].astype(float) df_valid[play_count] pd.to_numeric(df_valid[play_count], errorscoerce) df_valid[follow_count] pd.to_numeric(df_valid[follow_count], errorscoerce) fig, axes plt.subplots(1, 2, figsize(14, 5)) # 左图评分分布 axes[0].hist(df_valid[score], bins20, color#4C72B0, edgecolorwhite) axes[0].set_title(番剧评分分布) axes[0].set_xlabel(评分) axes[0].set_ylabel(番剧数量) # 右图播放量 vs 追番数对数坐标 axes[1].scatter(df_valid[play_count], df_valid[follow_count], alpha0.5, s20, color#DD8452) axes[1].set_xscale(log) axes[1].set_yscale(log) axes[1].set_title(播放量 vs 追番数对数坐标) axes[1].set_xlabel(播放量) axes[1].set_ylabel(追番人数) plt.tight_layout() plt.savefig(anime_overview.png, dpi150) plt.show()逻辑说明评分分布用直方图看整体集中在哪个区间播放量和追番数跨度极大必须用对数坐标否则大部分点会挤在左下角。散点能直观看出高播放是否一定高追番很多番剧播放高但追番低说明是路人盘而非核心粉。参数说明bins20控制直方图柱子数量太少看不出分布形状太多会碎alpha0.5让重叠点可见dpi150保证导出图清晰。4.2 追番转化率比播放量更能说明粘性单看播放量只能说明曝光追番数/播放量的比值才是粘性指标。我一般会算一个追番转化率再按这个指标排序看 Top 20。# 追番转化率 追番人数 / 播放量 df_valid df_valid[df_valid[play_count] 0] df_valid[follow_rate] df_valid[follow_count] / df_valid[play_count] top20 df_valid.nlargest(20, follow_rate)[[title, follow_rate, play_count, follow_count]] plt.figure(figsize(10, 8)) plt.barh(top20[title], top20[follow_rate], color#55A868) plt.xlabel(追番转化率) plt.title(追番转化率 Top 20) plt.gca().invert_yaxis() plt.tight_layout() plt.savefig(follow_rate_top20.png, dpi150) plt.show()这个指标能帮你发现一些播放不高但粉丝极黏的番剧对内容选题和推荐策略很有参考价值。注意播放量极低的番剧转化率可能虚高分析时要加一个播放量下限过滤比如只保留播放量大于 100 万的记录。4.3 标签维度聚合用 groupby 看哪类题材更吃香如果补抓了详情接口的 tags 字段就能做题材维度的聚合分析。标签通常是列表需要先展开再 groupby。# 假设 df_detail 里有 media_id 和 tags列表形式 exploded df_detail.explode(tags) agg exploded.groupby(tags).agg( anime_count(media_id, count), avg_score(score, mean), avg_play(play_count, mean), ).reset_index() # 只看样本量足够的标签 agg agg[agg[anime_count] 5].sort_values(avg_play, ascendingFalse) print(agg.head(15))逻辑说明explode把一行多标签拆成多行每个标签单独参与聚合anime_count 5是过滤样本太少的标签避免某标签只有 1 部番剧但播放极高这种噪声结论。参数说明agg里可以换任意聚合函数比如中位数median比均值更抗极端值排序字段按分析目标选看热度用 avg_play看口碑用 avg_score。5. 避坑与排查抓取和分析阶段最容易翻车的 5 个点5.1 现象抓了几页后返回空数据或 412原因请求频率过高触发了站点的风控或者请求头缺少关键字段Referer、Cookie。解决把休眠时间调大加上随机抖动补齐 Referer如果接口需要登录态手动从浏览器复制 Cookie 到请求头。不要用固定 1 秒间隔硬刷那是典型的翻车姿势。5.2 现象评分均值明显偏低原因把未开分的 0 分记录当成了真实评分。解决清洗阶段先标记 score_valid只对有效评分做统计。这个坑非常隐蔽因为 0 分看起来像个正常数值不检查就会一直错下去。5.3 现象翻页抓到重复数据原因换了排序方式重新抓或者接口在翻页时因为数据更新导致顺序漂移。解决抓取时只用一种排序抓完存原始数据去重按 media_id 做 drop_duplicates增量抓取时用 media_id 做 upsert 而不是直接 append。5.4 现象可视化中文显示成方块原因matplotlib 默认字体不含中文。解决设置plt.rcParams[font.sans-serif] [SimHei]并加axes.unicode_minus False处理负号。Linux 环境下如果没有 SimHei换成系统里已有的中文字体比如 WenQuanYi 系列。5.5 现象散点图所有点挤在一起原因播放量和追番数跨度几个数量级线性坐标下小值全挤在原点附近。解决对数和线性坐标切换set_xscale(log)和set_yscale(log)一起用。注意对数坐标不能有 0 值清洗时要把 0 过滤掉否则会报错或丢点。6. 把分析做成可复用的脚本参数化与增量更新做到这一步你已经能跑通抓取-清洗-可视化的完整链路。但真正让这套东西有价值的是可复用——下次想换排序维度、换时间范围、换分析指标不用重写代码。我的习惯是把配置抽成一个字典放在脚本顶部CONFIG { order_type: 3, # 3播放量, 4追番数, 5评分 max_page: 50, page_size: 20, sleep_range: (1.0, 2.5), min_play_for_rate: 1_000_000, # 转化率分析的最低播放量 output_dir: ./output, }抓取函数、清洗函数、绘图函数都从这个 CONFIG 读参数换分析目标时只改配置不改逻辑。增量更新则靠 media_id 判断读入已有 CSV把新抓的数据按 media_id 合并保留最新字段值。还有一个我踩过的坑不要把抓取和分析写在同一个脚本里。抓取脚本负责产出原始 CSV分析脚本只读 CSV。这样抓取失败不会影响已有数据的分析分析逻辑调整也不用重新抓一遍。两个脚本之间用文件解耦是最省心的做法。验证这套流程是否可靠有个简单办法换一个排序维度重抓一次对比两次结果的 media_id 集合是否一致。如果一致说明翻页逻辑没问题如果差很多说明中途有页丢失或重复需要检查 break 条件和休眠策略。最后说个我自己的教训早期做这类分析我总想一次抓全、一次做完美结果脚本越写越复杂一个报错就全盘卡住。后来改成先抓 3 页跑通全流程再放大到全量调试效率高了很多。数据分析和爬虫一样小步验证比一步到位靠谱。希望帮到你。本文还有配套的精品资源点击获取