
简介一份基于 Python 的“哔哩哔哩视频网”视频热度分析文档面向对爬虫、数据分析与可视化感兴趣的 Python 学习者以及想了解 B 站用户内容偏好与热度规律的产品、运营研究人员。文档完整介绍了从 Scrapy 框架抓取视频标题、播放量、热度等数据到 Pandas 进行清洗、转换与过滤再到 Pyecharts 输出分区占比、平均播放量、平均三连、各区平均播放及热门标签的可视化分析全过程。压缩包仅含 1 个 doc 文档大小 1.86MB内容包含中英文摘要、目录、技术介绍、数据抓取结果、可视化图表与总结展望结构清晰适合作为课程设计或论文写作的参考模板。已有 517 人学习下载。通过这份文档既能掌握 ScrapyPandasPyecharts 的完整数据分析流程也能直接借鉴其研究框架和图表展示方式同时其中关于页面分析、字段抽取与可视化参数配置的细节可帮助学习者快速搭建自己的视频热度分析项目。1. 视频热度的答案不在榜单页这套分析流水线给你完整链路B 站排行榜页面只暴露题目、作者、观看和综合得分真正能说明“用户为什么喜欢”的三连、分享、标签和分区数据全部藏在视频详情页里。把这两层数据拼起来再落到分区占比、平均播放量、三连偏好和热门标签四个维度上才能回答“15-45 岁用户到底在为什么内容停留”这个问题。本文这套基于 Python 的 B 站视频热度分析方案完整覆盖 Scrapy 数据抓取、Pandas 预处理、Pyecharts 可视化三个阶段适合想系统做一次内容生态分析的产品、运营和数据新人。整条链路跑完只需要一个排行榜入口 URL1300 行 13 列的数据就能支撑起这份报告。2. Scrapy 数据抓取排行榜页与详情页的字段映射设计2.1 为什么选 Scrapy 而不是 requests BeautifulSoup单页抓取用 requests 完全够用但一旦涉及“列表页进详情页”的两级页面跳转requests 方案需要自己维护待抓取队列、去重集合、失败重试和请求频率控制这些逻辑写到最后通常比业务代码还长。Scrapy 在框架层把这些问题都收编了引擎负责调度、下载器负责并发请求、爬虫类只关心解析规则、管道负责数据落地。这个分层结构恰好匹配 B 站热度分析的两个抓取阶段——先抓排行榜页拿到视频 ID 和基础数据再构造详情页 URL 补齐三连、分享和标签。2.2 页面结构拆解列表页拿 ID详情页补三连B 站排行榜页/v/popular/rank/all的 HTML 结构里每个视频条目是一个li节点其中包含视频标题、作者、观看量、评论数和综合得分。但播放页更关键的数据——点赞、投币、收藏、分享、标签——不在这个列表页上。常见做法是先从列表页提取视频aid拼出详情页 URLhttps://www.bilibili.com/video/aid再从详情页的div标签里用 XPath 提取。# items.py import scrapy class BiliHotItem(scrapy.Item): rank_tab scrapy.Field() # 榜单分类全站/动画/游戏/生活... video_id scrapy.Field() # 视频 aid title scrapy.Field() # 视频标题 author scrapy.Field() # UP 主名称 category scrapy.Field() # 视频分区 score scrapy.Field() # 排行榜综合得分 view scrapy.Field() # 观看数 danmaku scrapy.Field() # 弹幕数 reply scrapy.Field() # 回复数 like scrapy.Field() # 点赞 coin scrapy.Field() # 投币 favorite scrapy.Field() # 收藏 share scrapy.Field() # 分享 tag_name scrapy.Field() # 视频标签逗号分隔字段设计的原则只有一个把列表页和详情页的字段分开建模但合并到同一个 Item 中。这样管道写起来统一后面 Pandas 读入时可以直接拿到完整的二维表。2.3 爬虫主体请求队列与 XPath 抽取逻辑# spiders/bilibili_rank.py import scrapy from bilibili_hot.items import BiliHotItem class BilibiliRankSpider(scrapy.Spider): name bili_rank def start_requests(self): tabs [all, animation, game, life, music] for tab in tabs: url fhttps://www.bilibili.com/v/popular/rank/{tab} yield scrapy.Request(url, callbackself.parse_list, meta{tab: tab}) def parse_list(self, response): tab response.meta[tab] rank_items response.xpath(//*[idapp]/div/div[2]/div[2]/ul/li) for item in rank_items: hot_item BiliHotItem() hot_item[rank_tab] tab hot_item[video_id] item.xpath(./a/href).re_first(r(BV\w)) hot_item[title] item.xpath(.//a[classtitle]/text()).get() hot_item[author] item.xpath(.//span[classdata-box]/a/text()).get() hot_item[score] item.xpath(.//div[classpts]/div/text()).get() # 进入详情页继续取三连和标签 detail_url fhttps://www.bilibili.com/video/{hot_item[video_id]} yield scrapy.Request(detail_url, callbackself.parse_detail, meta{item: hot_item}) def parse_detail(self, response): hot_item response.meta[item] hot_item[view] response.xpath(//*[classview]/text()).get() hot_item[danmaku] response.xpath(//*[classdanmaku]/text()).get() hot_item[like] response.xpath(//*[classlike]//text()).get() hot_item[coin] response.xpath(//*[classcoin]//text()).get() hot_item[favorite] response.xpath(//*[classfavorite]//text()).get() hot_item[share] response.xpath(//*[classshare]//text()).get() hot_item[tag_name] response.xpath(//*[classtag-list]//a/text()).getall() yield hot_item这段代码把数据抓取拆成parse_list和parse_detail两个回调充分利用了 Scrapy 的异步调度。yield scrapy.Request会把详情页请求交给引擎引擎再调度给下载器期间不会阻塞列表页的解析。有一点要注意详情页里阅读数、弹幕这些字段有的带了“万”这样的单位后缀Pandas 清洗时要统一处理这一层先原样保留。meta参数用于在详情页回调里拿到列表页已经解析好的 Item 数据这是两级页面抓取最常用的传参方式。re_first(r(BV\w))是 B 站链接里的视频编号2019 年之后 B 站把 avid 全面切换成了 BV 号这个正则能直接从href属性中抽出 BV 号用于拼详情页。2.4 settings.py 中的并发与反爬参数抓取 B 站这类平台频率控制比 UA 伪装更重要。常见做法是控制延迟和并发不要用默认的高并发参数# settings.py ROBOTSTXT_OBEY False CONCURRENT_REQUESTS 16 # 并发请求数B站建议不超过 32 DOWNLOAD_DELAY 1.2 # 请求间隔秒数太密容易触发验证 COOKIES_ENABLED True # 开启 cookies 保持会话状态 DEFAULT_REQUEST_HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.bilibili.com/, Accept-Language: zh-CN,zh;q0.9, }DOWNLOAD_DELAY是每次请求之间等待的时间设为 1-2 秒对这个体量的抓取约 1300 条数据只需要几分钟没必要冒更大风险。CONCURRENT_REQUESTS控制同时发出的请求数量B 站的反爬对高频请求比较敏感16 是个安全线。COOKIES_ENABLED开启后Scrapy 会保留登录态实测能降低详情页请求被拦截的概率。抓完的数据落到管道里建议直接用csv导出中间结果scrapy crawl bili_rank -o bili_raw.csv这样即使后续分析出错也不需要重新跑一遍网络请求。3. Pandas 数据预处理重复、单位、标签三个坑一次填平3.1 去掉全站榜重复条目抓回来的数据中rank_tab同时包含全站榜单和分区榜单。全站榜里的视频本身就来自各分区头部如果不剔除统计分区占比时会出现同一视频被重复计数的偏差。import pandas as pd df pd.read_csv(bili_raw.csv) print(df.shape) # (1300, 13) 原始规模 print(df[rank_tab].value_counts()) # 检查各榜单数据量 df_without_all df[~df[rank_tab].isin([全站])] print(df_without_all.shape) # 剔除后规模这段代码用~df[rank_tab].isin([全站])做布尔索引取反把全站榜行整体过滤掉。isin接受一个列表便于后续扩展过滤“全区榜”之类的其他榜单类型。实际操作时先跑一次value_counts()确认各榜单条数防止因为榜单页改版导致某类数据缺失。3.2 数值列中的“万”与空值处理抓取阶段保留了页面上的原始文本B 站的数据常见“1.2万”“3245”两种格式还有“--”表示无效值。Pandas 的to_numeric遇到这些格式会直接转成 NaN所以要在转换前做一次清洗def convert_count(value): if isinstance(value, str): value value.replace(,, ) if 万 in value: return float(value.replace(万, )) * 10000 if value in (--, , None, nan): return None return pd.to_numeric(value, errorscoerce) num_cols [view, danmaku, like, coin, favorite, share, reply, score] for col in num_cols: df_without_all[col] df_without_all[col].map(convert_count)errorscoerce是 Pandas 最常用的类型容错策略转不了的自动变成 NaN后续统一用fillna(0)或者dropna()决策。对热度分析来说三连和播放量缺失值用 0 填充是安全的因为没拿到数据不等于用户没互动但确实没有更好的估计方法时 0 是唯一不引入主观偏差的选择。3.3 标签列拆分与出现次数统计tag_name字段在抓取时是逗号分隔的字符串一个视频可能有 3-5 个标签。做热门标签可视化前需要把长字符串拆成多行df_tags df_without_all.assign(tagdf_without_all[tag_name].str.split(,)).explode(tag) df_tags[tag] df_tags[tag].str.strip() tag_counts df_tags[tag].value_counts().head(20) print(tag_counts)assignstr.splitexplode是 Pandas 处理“一列多值”的标准三步走。explode把列表中的每个元素拆成一行同时复制其他所有字段。这样每个标签一行value_counts()就直接得到出现次数。注意先strip()再统计否则抓取时标签前后的空格会让同一个标签被计成两个。如果不用explode原论文里也描述过另一种做法创建tag_df宽表遍历tag_name给对应列赋值 1再求和。两种方式都能出结果但explode在内存和代码量上都更优尤其是标签总量超过两百个的场景宽表会生成大量零值列拖慢计算。3.4 预处理后的数据集结构字段类型用途rank_tabstr来源榜单区分分区video_idstr去重主键categorystr视频分区名view / danmakufloat播放与弹幕衡量覆盖广度like / coin / favoritefloat三连数据衡量互动质量sharefloat分享衡量传播意愿tag_namestr标签拆分前的原始串清洗完成后df_without_all可以导出为 csv 供可视化直接使用。这里有一个容易踩的细节view如果是从详情页抓的数值和排行榜页展示的数据可能有细微延时差分析时统一以详情页为准即可。4. Pyecharts 可视化四种图表的配置要点与 PNG 输出4.1 整体结构从 Pandas 到图表实例Pyecharts 的通用写法是(Pie() .add(...) .set_global_opts(...))链式调用把所有配置串起来最后.render()输出 HTML。下面的代码覆盖分区占比、平均播放量、三连雷达图三个核心分析目标。from pyecharts.charts import Pie, Bar, Radar, Line from pyecharts import options as opts # 4.1.1 分区占比综合评分 top100 的 category 分布 top100 df_without_all.sort_values(score, ascendingFalse).head(100) cat_stats top100[category].value_counts() pie ( Pie() .add( series_name分区占比, data_pair[list(z) for z in zip(cat_stats.index, cat_stats.values)], radius[30%, 70%], # 环形饼图内外半径 ) .set_global_opts( title_optsopts.TitleOpts(title综合评分 Top100 分区占比), legend_optsopts.LegendOpts(orientvertical, pos_top15%), ) .set_series_opts(label_optsopts.LabelOpts(formatter{b}: {d}%)) ) # 4.1.2 平均播放量按分区聚合 play_mean df_without_all.groupby(category)[view].mean().sort_values(ascendingFalse) bar ( Bar() .add_xaxis(play_mean.index.tolist()) .add_yaxis(平均播放量, [round(v, 0) for v in play_mean.values]) .set_global_opts( title_optsopts.TitleOpts(title各分区平均播放量), yaxis_optsopts.AxisOpts(name播放量), ) )radius[30%, 70%]让饼图变成环形内半径 30% 外半径 70%观感上比实心饼图清爽而且中间留白可以放总样本量注释。formatter{b}: {d}%中{b}是类别名{d}是百分比这是 Pyecharts 内置的模板变量不需要手动计算占比。groupby(category)[view].mean()返回的是 Seriesindex.tolist()直接作为 X 轴类别注意需要先按值排序再取tolist()否则柱状图顺序是乱的。4.2 三连情况的雷达图参数设计三连分析要回答“哪些分区获得了用户的高质量互动”标准做法是分别计算 top100 内各分区的点赞、投币、收藏均值再画三张雷达图def build_radar(data_series, title): categories data_series.index.tolist() values [[round(v, 0)] for v in data_series.values] radar ( Radar() .add_schema( schema[opts.RadarIndicatorItem(namestr(cat), max_float(data_series.max() * 1.2)) for cat in categories], splitarea_optopts.SplitAreaOpts(is_showTrue), ) .add(series_nametitle, datavalues, color#ff6b81) .set_global_opts(title_optsopts.TitleOpts(titletitle)) ) return radar like_radar build_radar(top100.groupby(category)[like].mean(), 点赞均值) coin_radar build_radar(top100.groupby(category)[coin].mean(), 投币均值) fav_radar build_radar(top100.groupby(category)[favorite].mean(), 收藏均值)雷达图的max_是每个维度坐标轴的最大值这里用data_series.max() * 1.2留出 20% 余量避免最大值顶点贴边影响读数。另一个细节是data必须是二维数组即使每个分区只有一个数值也要包成[[v]]的形式否则 Pyecharts 会报维度错误。实际跑下来能看到明显的差异影视区的点赞均值最高生活区的投币量最高时尚区的收藏量最高。这说明不同分区用户的互动行为存在结构性差异收藏偏多意味着内容具有“工具属性”投币偏多则说明用户愿意用自己的“硬币”为创作者背书。4.3 snapshot_selenium 把 HTML 转成 PNGPyecharts 默认输出 HTML报告里贴图不方便。snapshot_selenium是官方配套的截图方案原理是启动浏览器渲染 HTML 后截屏保存from pyecharts.render import make_snapshot from snapshot_selenium import snapshot make_snapshot(snapshot, pie.render(pie.html), pie.png) make_snapshot(snapshot, bar.render(bar.html), bar.png) make_snapshot(snapshot, like_radar.render(like.html), like.png)第一次运行会自动检测本地 Chrome 环境。Windows 上如果报浏览器驱动相关错误通常是 SELENIUM 的 WebDriver 和 Chrome 版本不匹配把 Chrome 升级到最新版并安装对应版本的chromedriver即可解决。批量转换时建议每个图之间加 1-2 秒停顿避免浏览器进程堆积导致内存占用过高。4.4 各区平均播放与热门标签的补充视图各区平均播放量适合用折线图展示整体趋势热门标签则用横向柱状图# 各区平均播放 zone_mean df_without_all.groupby(category)[view].mean().sort_values(ascendingFalse) line ( Line() .add_xaxis(zone_mean.index.tolist()) .add_yaxis(平均播放, [round(v, 0) for v in zone_mean.values], is_smoothTrue) .set_global_opts(title_optsopts.TitleOpts(title各区平均播放量)) ) # 热门标签 top20 横向柱状图 bar_h ( Bar() .add_xaxis(tag_counts.index.tolist()[::-1]) .add_yaxis(标签出现次数, tag_counts.values[::-1].tolist()) .reversal_axis() # 转为横向条形图 .set_series_opts(label_optsopts.LabelOpts(positionright)) )reversal_axis()是横向条形图的关键方法配合label_opts.positionright把数值标签放在条形右侧阅读顺序更符合“从上往下看排名”的直觉。热门标签的结果显示“搞笑”依然占据第一梯队但生活类标签明显增多这和分区占比中生活区第一的结论形成了交叉验证。5. 榜单分析的三个边界结果验证、切片偏差与时效性最后收在几个容易忽略的细节上。第一个边界是结果验证。拿到 top100 分区占比后不要急着下结论先把结果和 B 站官方“每周必看”或热门榜做一个交叉比对。做法很简单从报告中随机抽取 10 个视频手动打开页面核对播放量、三连是否和报告一致。如果偏差超过 5%优先检查清洗阶段是否漏掉了“万”字单位的转换如果个别视频数据差一位数多半是详情页请求被限流导致拿到了默认值。第二个边界是切片偏差。原来的分析是“综合评分 top100”但综合评分本身是 B 站未公开的加权函数高评分不等于高播放。实际解读时要把分区占比、平均播放量、三连这三张图放在一起看分区占比高说明头部内容多平均播放量高说明整体流量大三连高说明互动质量好三者结论一致的话才能确认该分区确实是热度源。如果只看占比喜剧类这种头部效应明显的分区会掩盖中部内容不足的问题。第三个边界是时效性。B 站热榜小时级变化抓下来的数据只是某个时间切面的快照。做分析时在文件命名里带上抓取时间比如bili_raw_20250602_2000.csv这样后续如果要做周环比或者月度趋势还能拿旧快照重跑分析。爬虫脚本也可以加上Cron定时任务每天固定两个时间点跑一次数据积累两个月后就能做“用户兴趣迁移”这类更有深度的观察。以上这套从 Scrapy 到 Pandas 再到 Pyecharts 的链路完整跑一遍之后得到的图表已经能支撑起一份可靠的视频热度分析报告。如果后续想扩大样本量把列表页的分页参数p2、p3加进start_requests即可商品逻辑不需要改动。本文还有配套的精品资源点击获取