ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python的B站视频热度分析:爬虫、清洗与可视化实战

基于Python的B站视频热度分析:爬虫、清洗与可视化实战 简介这是一份基于Python的B站视频热度分析毕业设计文档面向数据分析、爬虫方向学习者及需要完成类似课题的在校生。文档以哔哩哔哩视频网为研究对象系统说明如何利用Scrapy框架抓取视频标题、描述、播放量等数据再借助Pandas完成清洗与预处理并通过Pyecharts绘制分区占比、平均播放量、平均三连、各区平均播放及热门标签等可视化图表完整呈现从数据采集到结论输出的分析流程。资源包共1个文件格式为doc大小1.86MB属于可直接阅读和修改的论文文档。已有517人学习下载。相比零散代码片段这份资料的优势在于结构完整既有研究背景与技术框架也有数据处理细节和可视化结果汇总适合作为课程设计、毕业设计或论文写作的参考模板也可帮助初学者快速了解B站数据热度分析的整体思路。1. 从榜单到热度画像这套 Python 视频热度分析做了什么“基于 Python 的哔哩哔哩视频网视频热度分析”这个项目是一份从数据采集走到可视化结论的完整毕业设计实现而不是单个爬虫脚本。整条链路分三段Scrapy 框架抓取 B 站排行榜和视频详情数据Pandas 做清洗和去重pyecharts 做可视化最终产出分区占比、平均播放量、平均三连、各区平均播放和热门标签五类分析结果。这个项目最适合四类人拿爬虫做课程设计的学生刚学完 Python 基础想练数据分析的开发者研究 B 站内容生态的运营或 UP 主以及需要一套“爬虫 清洗 可视化”完整范例来改造业务需求的工程师。项目的原始数据规模是 1300 行 13 列字段覆盖作者、播放量、点赞、投币、收藏、得分、评论、分享、分类和标签分析结论可以直接对应到 15-45 岁群体的内容偏好。这份资源最值钱的地方不是那 1300 行数据本身而是数据从请求到成图的完整流转方式。单独抽开任何一环你在官方文档里都能找到对应说明但把它们拼成一条能跑通、能复现、能写进论文的链路文档不会告诉你边界只有动手拆过才会懂。2. 用 Scrapy 抓全站榜单页面拆解与爬虫主流程拿到这份资源的第一步是先搞明白原论文第三章“基于 Scrapy 的数据抓取”在讲什么。作者在这一章用了不少篇幅做页面分析看起来朴素其实是整个项目里最容易被跳过的关键环节。B 站页面结构不是给爬虫准备的排行榜页和视频详情页能拿到的字段完全不同不分清楚后面的 Item 定义和管道处理都会跟着错。2.1 先分清两类页面榜单页拿概览详情页拿三连B 站排行榜页展示的是概览数据视频题目、作者、播放量、评论数、综合得分。这些信息集中在榜单列表的节点里用 XPath 可以一次提取。但点赞、投币、收藏这三连数据榜单页拿不全需要进入视频详情页才能抓到。至于视频标签论文里说是从详情页的 div 标签里通过 XPath 抓取的。按常见做法Spider 的 parse 方法先解析榜单页把概览字段填进 Item 实例同时取出视频链接用 response.follow 请求详情页再在第二个回调里补齐三连和标签字段。这样每个视频只产生一条完整记录。下面给一个贴近原项目结构的 Spider 骨架XPath 以你 F12 看到的实际结构为准import scrapy from bilibili_spider.items import BilibiliItem class RankSpider(scrapy.Spider): name rank start_urls [https://www.bilibili.com/v/popular/rank/all] def parse(self, response): rank_items response.xpath(//div[contains(class, rank-item)]) for rank in rank_items: item BilibiliItem() item[rank_tab] 全站 item[title] rank.xpath(.//a[contains(class, title)]/text()).get() item[author] rank.xpath(.//a[contains(target, _blank)]/text()).get() item[score] rank.xpath(.//div[contains(class, pts)]/text()).get() item[view] rank.xpath(.//div[contains(class, detail-state)]/span[1]/text()).get() detail_url rank.xpath(.//a[contains(class, title)]/href).get() yield scrapy.Request( urlresponse.urljoin(detail_url), callbackself.parse_detail, cb_kwargs{item: item}, ) def parse_detail(self, response, item): item[like] response.xpath(//*[contains(class, like)]/text()).get() item[coin] response.xpath(//*[contains(class, coin)]/text()).get() item[favorite] response.xpath(//*[contains(class, favorite)]/text()).get() item[tag_name] response.xpath(//*[contains(class, tag)]/text()).getall() yield item代码逻辑parse 先解析榜单页的 rank-item 节点把榜单来源、题目、作者、得分、播放量填进同一个 Item 实例解析到视频详情链接后用 scrapy.Request 把 item 通过 cb_kwargs 传给 parse_detail第二个回调补齐三连和标签字段后再 yield 一条完整记录。这样即使详情页解析失败概览字段也还在队列里不会整条丢失。参数说明start_urls 设的是全站排行榜路径想抓分区榜就把路径改成对应分区前缀cb_kwargs 是 Scrapy 跨回调传数据的标准方式比手动塞 meta dict 更不容易写错。tag_name 用 getall() 拿列表后面 Pandas 处理时拆分成多个标签正好对应原论文里的热门标签统计。2.2 定义 Item在原表 13 列之外多留一个 rank_tab原始数据表有 13 列作者、投币数、弹幕数、三连数、作品 id、点赞数、类别、回复数、得分、分享数、观看数、题目、标签。但第 3.3 节的预处理代码里用到了一个 rank_tab 字段用来标记榜单来源说明抓取时实际还存了这一列。所以 Item 定义时应该在 13 列的基础上额外加一个 rank_tab 字段共 14 列import scrapy class BilibiliItem(scrapy.Item): author scrapy.Field() # 作者 coin scrapy.Field() # 投币数 danmaku scrapy.Field() # 弹幕数 favorite scrapy.Field() # 收藏数 video_id scrapy.Field() # 作品 id like scrapy.Field() # 点赞数 category scrapy.Field() # 类别/分区 reply scrapy.Field() # 回复数 score scrapy.Field() # 综合得分 share scrapy.Field() # 分享数 view scrapy.Field() # 观看数 title scrapy.Field() # 题目 tag_name scrapy.Field() # 标签 rank_tab scrapy.Field() # 榜单来源用于去重字段键名建议统一用英文原因是后面 Pandas 读 CSV、pyecharts 画图、DataFrame 切片都要引用列名中文列名在换 IDE 或终端环境后容易出现编码不一致的问题。如果你想在图表中显示中文列名可以在生成图表时做一次映射数据层和展示层各管各的。2.3 调度与请求头延迟、UA、Referer 怎么设同一 IP 短时间请求次数过多B 站会返回 412 或弹出验证码。这个问题见过太多次绝不是网络玄学是并发太激进。settings.py 里建议这样配DOWNLOAD_DELAY 2 USER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 DEFAULT_REQUEST_HEADERS { Referer: https://www.bilibili.com/, Accept-Language: zh-CN,zh;q0.9, } RETRY_ENABLED True RETRY_TIMES 3DOWNLOAD_DELAY2 表示每个请求间隔 2 秒。1300 条数据里大约三成要二次请求详情页全站榜单一轮下来大约 40~60 分钟能接受。Referer 设成 B 站首页是爬虫处理里的常规操作很多站点会校验 Referer少了它容易被拒。RETRY_TIMES3 负责处理偶发的超时和 5xxScrapy 会自动换请求重试。提示DOWNLOAD_DELAY 不要设成 0。爬得越快封得越快这个项目的数据量并不大稳稳跑完比跑得快重要。2.4 落盘与初步清洗从响应到可读的 CSV爬虫跑完后用scrapy crawl rank -o results.csv导出一份原始 CSV。原论文展示的示意数据里观看数有的带“万”字得分是字符串数字三连字段可能因为详情页请求失败而空缺。这些留给预处理环节解决。但有一个动作建议在管道里做从详情页 URL 里抽出作品 id省得后面还要回源查。class SavePipeline: def process_item(self, item, spider): if item.get(video_id): item[video_id] item[video_id].split(/)[-1].split(?)[0] return item这个管道只做了一件事把 video_id 里的路径和查询参数剥掉。逻辑说明详情页 URL 通常长这样/video/BV1xx411c7mD?spm_id_fromxxx按“/”切分取最后一段再按“?”切分取前一段得到纯 ID。参数说明如果某条数据没有 video_id用 if 判断跳过避免 None.split 报错。管道记得在 settings.py 的 ITEM_PIPELINES 里注册否则 Scrapy 不会执行它。3. 用 Pandas 预处理脏数据去重、补缺与标签拆分抓下来的数据不能直接画图。原论文 3.3 节提到两个关键问题数据里有缺失值rank_tab 里包含“全站”榜单的数据要和分区数据分开。另外数值列可能有“万”结尾的字符串不换算就画图柱状图会把 1.2 万当成 1.2 来算差四个数量级属于典型翻车点。3.1 排除全站榜避免同一个视频被重复统计原论文唯一给出的具体代码如下df_without_all df[~df[rank_tab].isin([全站])]这行代码的意思是取 rank_tab 列用 isin([全站]) 判断哪些行属于全站榜~ 取反最后选出不属于全站榜的所有行。逻辑说明全站榜的视频往往也会出现在各分区榜里不去掉就会导致一个视频在分区统计中被计算两次分区占比直接失真。参数说明[全站] 是排除名单如果你后续还要排除其他榜单类型往这个列表里加就行isin 天然支持多值匹配。3.2 缺失值处理与数值换算含“万”的字符串要单独处理用df.isnull().sum()先看一眼哪些列缺数据。三连字段缺失通常是详情页请求失败导致的。处理原则很简单核心字段缺失的行直接删掉非核心字段缺失的可以保留。否则 pandas 的 mean() 会静默跳过 NaN你看到的平均值到底基于多少个样本心里没底。import pandas as pd df pd.read_csv(results.csv, encodingutf-8) print(df.shape) print(df.isnull().sum()) df df.dropna(subset[view, like, coin, favorite]) df df.reset_index(dropTrue) print(df.shape) def parse_count(s): if pd.isna(s): return 0.0 if isinstance(s, str) and 万 in s: return float(s.replace(万, )) * 10000 return float(s) for col in [view, like, coin, favorite, reply, share]: df[col] df[col].map(parse_count)逻辑说明先 dropna 删掉三连和播放量缺失的行reset_index 让索引重排否则后面按行操作会带洞。parse_count 函数处理两种输入纯数字字符串直接转 float带“万”的字符串先去掉“万”再乘 10000。map 对整列批量应用。参数说明缺失值补 0 只用于像回复数这类不影响结论的列view 列缺失绝对不能用 0 填充0 播放和缺失播放是完全不同的语义填 0 会把平均值往下拉得很惨。注意处理完数值列后跑一遍df.dtypes确认 view、like 这些列都是 float 类型。如果还是 object说明 parse_count 没生效后面所有聚合结果都会是错的。3.3 标签列拆分从逗号串到前 20 个热门标签原论文里写了一种循环遍历 tag_name 的 One-Hot 方法能跑通但代码量大。用 pandas 的 explode 方法两行就能拿到同样结果df_tag df_without_all.copy() df_tag[tag_list] df_tag[tag_name].str.split(,) tag_exploded df_tag.explode(tag_list) tag_counts tag_exploded[tag_list].str.strip().value_counts().head(20) print(tag_counts)逻辑说明str.split(,) 把每个视频的标签串拆成列表explode 把列表元素展开成多行一个视频有几个标签就变几行value_counts() 统计每个标签出现次数最后 head(20) 取前 20 个热门标签。这个写法与原论文“建 DataFrame 逐列赋 1”的方法结果一致但更短、更快、也更容易读懂。参数说明str.strip() 用来去掉标签首尾可能存在的空格不处理的话“搞笑 ”和“搞笑”会被当成两个标签统计数直接分散属于隐蔽的数据错误。注意explode 后的数据是“一标签一行”后面做其他聚合时记得用回 df_without_all不要用 tag_exploded否则每个标签都会把视频计数一次数值全部虚高。4. 可视化落地五张图的聚合、取值与导出细节原论文第四章做了五张图分区占比、平均播放量、平均三连、各区平均播放、热门标签。大部分图表直接用 pyecharts 就能生成真正需要留意的步骤是把 HTML 导出成 PNG 图片的环节这个放在第五章避坑里详细说。下面讲每张图的聚合逻辑和实现要点。4.1 分区占比综合评分前 100 名的切片先对 df_without_all 按 score 降序排序取前 100 条统计 category 出现次数画饼图df_top100 df_without_all.sort_values(score, ascendingFalse).head(100) category_counts df_top100[category].value_counts() data_pair [list(z) for z in zip(category_counts.index, category_counts.values)] from pyecharts.charts import Pie from pyecharts import options as opts pie ( Pie() .add(, data_pair, radius[40%, 70%]) .set_global_opts(title_optsopts.TitleOpts(title分区占比)) .set_series_opts(label_optsopts.LabelOpts(formatter{b}: {d}%)) ) pie.render(分区占比.html)逻辑说明sort_values 按综合得分降序head(100) 取前 100 条value_counts() 统计各分区出现次数data_pair 转成 pyecharts 需要的 [名称, 数值] 对。radius 设成 [40%, 70%] 画出环形饼图视觉上比实心饼图更容易比较占比。label_opts 里的 {b} 是分区名{d} 是百分比。参数说明如果想看整体分布而不是 top100把 head(100) 去掉即可但要注意全站榜去重必须在前否则同一视频重复计入。4.2 平均播放量柱状图先聚合再排序avg_view df_top100.groupby(category)[view].mean().sort_values(ascendingFalse) from pyecharts.charts import Bar bar ( Bar() .add_xaxis(avg_view.index.tolist()) .add_yaxis(平均播放量, avg_view.values.tolist()) .set_global_opts( title_optsopts.TitleOpts(titletop100 平均播放量), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate30)), ) ) bar.render(平均播放量.html)逻辑说明groupby(category) 按分区分组[view] 取出播放量列mean() 求平均sort_values 降序排列这样柱状图从左到右天然是递减的视觉上不用读坐标就知道谁高谁低。参数说明rotate30 是让分区名斜着显示分区一多名字挤在一起是柱状图最常见的观感问题这个参数几乎每次都要调。提示pyecharts 的 render 默认生成 HTML 文件放到本地浏览器直接打开即可。如果需要 PNG再看第五章的导出避坑。4.3 三连情况一张雷达图装下赞、币、藏原论文把点赞、投币、收藏分成了三张雷达图分开看有个问题不同分区在三个维度上的相对关系很难一眼对比。合并成一张雷达图更直接from pyecharts.charts import Radar from pyecharts import options as opts radar_data [ {zone: 动画, like: 82000, coin: 46000, fav: 31000}, {zone: 生活, like: 91000, coin: 52000, fav: 38000}, {zone: 影视, like: 87000, coin: 39000, fav: 42000}, ] radar ( Radar() .add_schema(schema[ opts.RadarIndicatorItem(name点赞, max_100000), opts.RadarIndicatorItem(name投币, max_60000), opts.RadarIndicatorItem(name收藏, max_60000), ]) .add(动画, [[radar_data[0][like], radar_data[0][coin], radar_data[0][fav]]]) .add(生活, [[radar_data[1][like], radar_data[1][coin], radar_data[1][fav]]]) .add(影视, [[radar_data[2][like], radar_data[2][coin], radar_data[2][fav]]]) ) radar.render(三连雷达.html)这里的数据是示意用的实际要从 DataFrame 按分区聚合出来。逻辑说明add_schema 定义三个雷达轴每个轴设最大值max_ 要根据实际数据量级调整否则小数值会被压到圆心附近图直接变“花骨朵”。参数说明radar_data 换成你自己 groupby 出来的真实值注意三个 add 的顺序决定图中多边形的堆叠顺序先画的会被后画的盖住所以把数值大的分区放后面。4.4 各区平均播放与热门标签折线图加词云各区平均播放量在原论文里存了一遍 CSV 又读出来画图其实 groupby 一次就够avg_zone df_without_all.groupby(category)[view].mean().sort_values(ascendingFalse) from pyecharts.charts import Line line ( Line() .add_xaxis(avg_zone.index.tolist()) .add_yaxis(平均播放量, avg_zone.values.tolist()) .set_global_opts(title_optsopts.TitleOpts(title各区平均播放量)) ) line.render(各区平均播放.html)热门标签用词云展示输入是上一章算好的 tag_countsfrom pyecharts.charts import WordCloud wc ( WordCloud() .add(, tag_counts.items(), word_size_range[20, 100]) .render(热门标签.html) )逻辑说明WordCloud 的 .add 接收一个 (词, 词频) 的可迭代对象tag_counts.items() 正好满足。word_size_range 控制显示尺寸下限和上限数值越大词越突出。参数说明如果想看更长尾的标签把 head(20) 改成 head(50)词云形状也会更密但图表的可读性会下降。5. 常见问题与避坑清单反爬、乱码与图表空白做这类项目最耽误时间的不是抓不到数据而是抓一半数据出各种状况。下面这五条是我在复现类似项目时踩过或见别人踩过的坑按“现象、原因、解决”写清楚。5.1 爬到一半开始返回 412甚至出现验证码现象爬虫前几百条数据正常跑到三四百条时响应里全是 412 状态码或者返回的 HTML 出现安全验证页面。原因同一 IP 请求太密集。B 站的频控不会在一开始就触发它是对一段时间内的请求总数做统计所以前面正常不代表后面安全。解决DOWNLOAD_DELAY 至少设 2 秒再把并发降到 8 以下CONCURRENT_REQUESTS 4 更稳。如果目标数据量大不要只依赖延迟把榜单 URL 拆成多段分时段运行。5.2 XPath 提取突然拿不到内容现象昨天还能跑的 Spider今天 parse 里 get() 返回 None字段全空。原因B 站前端页面的 DOM 结构会随版本迭代变化类名和标签结构都可能改版。解决写爬虫时不要只写 XPath 就跑加一个检查逻辑比如对返回结果做断言if item.get(title) is None: spider.logger.error(title 提取失败请重新检查页面结构) return这样页面改版后能第一时间在日志里发现而不是等数据落盘后才发现一堆空数据。更省事的方式是优先用 B 站开放接口或官方榜单接口页面 DOM 变了接口结构基本不动。5.3 pyecharts 导出 PNG 时黑屏或空白现象render() 生成的 html 用浏览器打开正常但 snapshot_selenium 导出 png 时图片是全黑或全白。原因snapshot_selenium 依赖浏览器驱动和 Chrome 环境需要配合无头模式使用另外图表在 html 里是异步渲染的截图时图表还没完成绘制截到的是空白 canvas。解决用 snapshot_selenium 前先确认两点。第一本机装了对应版本的 ChromeDriver并且 selenium 能正常启动 Chrome。第二给截图留出渲染等待时间。pyecharts 提供的 snapshot_selenium 封装里已经带了等待逻辑但如果你自己用 selenium 截图务必用 WebDriverWait 等待 canvas 元素出现后再截图。如果只想快速出图也可以不转 PNG直接输出 HTML 放进 Jupyter Notebook 里看这是最省事的方式。5.4 CSV 用 Excel 打开时中文乱码现象文件在 VS Code 里看正常用 Excel 打开后中文全是乱码。原因Scrapy 的 -o results.csv 默认写入编码通常是 UTF-8Excel 在 Windows 环境下默认用 GBK 打开无 BOM 的 UTF-8 文件于是中文列名和中文内容全乱。解决写 CSV 时指定 UTF-8 带 BOM或者落盘后用 pandas 重新保存一次df.to_csv(bilibili_data_clean.csv, indexFalse, encodingutf-8-sig)utf-8-sig 是带 BOM 的 UTF-8Excel 能正确识别。落盘时统一用 utf-8-sig交付给别人时就不用解释乱码问题。5.5 “万”结尾的数值被当成普通字符串平均值计算离谱现象画平均播放量柱状图数值比实际小 4 个数量级或者排序完全不对。原因原始数据里的观看数是“1.2万”这种字符串没有做换算直接 astype(float) 时抛 ValueError或者用 replace 把“万”替换成空字符串后当成 1.2 参与计算排序自然乱套。解决在预处理阶段用 parse_count 统一处理这一步必须放在所有数值聚合之前。我一般会在读取 CSV 后立刻做类型转换并且跑一遍 df.dtypes 确认 view、like 这些列都是 float 类型而不是 object。6. 验证分析结果抽样核对与造数回归图表画完论文写到最后最容易产生的错觉是“图能出来就说明结果没问题”。图的正确性要单独验证最低成本的做法有两个抽样去 B 站原页面人工核对以及造数回归验证聚合逻辑。6.1 抽样核对拿原始页面数据对比分析结论先抽出 5 条记录打开 B 站原视频页面核对播放量、点赞、投币、收藏是否和 CSV 一致。这一步不是可有可无——XPath 选择器完全可能因为页面结构调整把两列数据抓反比如把播放量填进点赞列。sample df.sample(5, random_state42) print(sample[[title, author, view, like, coin, favorite]])把这 5 个标题复制到 B 站搜索框点进详情页逐一对比。随机种子 42 让抽样结果可复现方便和团队讨论。6.2 造数回归用 3 行假数据验证聚合逻辑抽样核对验证的是“数据对不对”造数回归验证的是“聚合逻辑对不对”。很多聚合错误是逻辑导致的比如 groupby 后忘了 reset_index、explode 后统计错了对象。用 3 行已知结果的小数据集就能试出来。test_df pd.DataFrame({ category: [生活, 动画, 生活], view: [1000, 2000, 3000], }) print(test_df.groupby(category)[view].mean())预期生活区 (10003000)/2 2000动画区 2000。如果结果不符说明 groupby 或 mean 调用有误比对着 1300 行真实数据找错快得多。从那以后我每完成一套数据分析流程都会强制走一遍“抽样核对 → 造数回归”这两步哪怕只花二十分钟也能把写代码时根本没注意到的隐蔽错误揪出来。希望这套验证习惯能帮你在自己的 B 站热度分析项目里少走几个弯路。本文还有配套的精品资源点击获取
返回列表