☰
Python实现B站视频热度分析:从数据采集到曲线可视化的完整方案
2026/10/14 11:19:37 网站建设 项目流程

简介:一份基于Python的哔哩哔哩视频网视频热度分析论文文档,面向爬虫、数据分析与可视化方向的初学者及相关课题研究者。内容围绕B站15—45岁用户群体展开,系统介绍基于Scrapy框架抓取视频标题、描述、播放量、热度等数据,利用Pandas完成清洗、转换与分析,并通过Pyecharts绘制分区占比、平均播放量、平均三连、各区平均播放和热门标签等可视化结果。资源为单个doc格式文档,压缩包大小1.86MB,已有517人学习。文档结构完整,包含中英文摘要、目录、绪论、相关技术介绍、数据抓取与预处理、可视化分析及总结展望等章节,可作为课程设计或毕业设计参考,帮助读者快速掌握B站视频热度从数据采集、清洗、分析到可视化展示的完整实现路径。

1. 做哔哩哔哩视频热度分析前,先想清楚这三个问题

先泼一盆冷水:把一批视频按播放量排个序,叫排行榜,不叫热度分析。真正的视频热度分析,是把「热度」拆成一条随时间展开的曲线,看视频发布后 24 小时怎么起量、第几天到峰值、衰退期有多陡——这些信息才决定你要不要追这个选题、要不要在某个时间点加大投放。

围绕基于 Python 的哔哩哔哩视频网视频热度分析这个方向,动手前必须回答三个问题:第一,你的数据从哪来?没有官方后台权限的普通研究者和第三方团队,只能靠公开接口和爬虫。第二,你分析的是「最终成绩」还是「增长过程」?我强烈建议只做过程追踪。第三,热度是单一数字还是多维结构?播放量是规模,硬币是认可度,转发是传播力,混在一起需要一个能被复现的权重方案。这套流程适合做竞品分析的运营、研究内容风向的团队,也适合刚学完 Python 基础语法、想找一个完整项目练手的人。

2. 把哔哩哔哩数据拉到本地:接口选型与最小 Python 爬虫

2.1 为什么不用官方后台,而选择公开接口

B站创作中心里能看到每个视频的完播率、粉丝画像和阅读来源,但那是 UP 主自己的数据。你要分析别人家的视频,或者做一个垂类赛道的内容监测,后台权限拿不到,唯一稳定可复现的路径是 B 站面向网页端的公开接口。

这里说的公开接口,是视频详情页的 JSON 数据源。用 bvid(视频编号)作为参数,返回的 JSON 里直接包含热度分析最关心的几个字段:播放量、点赞、投币、收藏、分享、评论、弹幕。核心接口是https://api.bilibili.com/x/web-interface/view,我一般叫它「详情接口」。

选择公开接口而不是去抓 HTML 页面,理由有三条:第一,返回结构是 JSON,Python 不用正则或者 CSS 选择器去抠文本;第二,字段命名稳定,stat区块从 2018 年到现在没大改过;第三,页面里播放量可能被前端格式化成「12.3 万」,接口里是原始整数 123456,省去一步转换,也避免精度丢失。

字段接口位置说明
播放量stat.view原始整数,不带「万」
点赞stat.like反映内容质量
投币stat.coin用户认可度,成本高于点赞
收藏stat.favorite实用型内容的强信号
分享stat.share传播意愿最直接的体现
评论stat.reply讨论深度
弹幕stat.danmaku实时互动氛围
发布时间pubdateUnix 时间戳,做时间轴用

这份字段清单,后续做热度指数、画时间序列曲线都要反复用到。建议先把接口返回的完整 JSON 打印出来看一遍,你会发现比网页上展示的信息多很多,比如duration视频时长、owner作者信息,这些后续都可能成为热度分析的辅助维度。

2.2 采集单个视频详情的 Python 最小可用代码

先写一个能跑通的单视频采集函数。这个函数是整套热度分析的地基,后续批量采集、定时任务都从它扩展。

import json import requests def fetch_video_info(bvid: str, sessdata: str = "") -> dict: url = "https://api.bilibili.com/x/web-interface/view" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" } if sessdata: headers["Cookie"] = f"SESSDATA={sessdata}" params = {"bvid": bvid} resp = requests.get(url, params=params, headers=headers, timeout=10) resp.raise_for_status() payload = resp.json() if payload.get("code") != 0: raise ValueError(f"接口返回异常 code={payload.get('code')}") return payload["data"] info = fetch_video_info("BV1xx411c7mD") print(json.dumps({ "title": info["title"], "pub_ts": info["pubdate"], "view": info["stat"]["view"], "like": info["stat"]["like"], "coin": info["stat"]["coin"], "favorite": info["stat"]["favorite"], "share": info["stat"]["share"], "reply": info["stat"]["reply"], "danmaku": info["stat"]["danmaku"], }, ensure_ascii=False, indent=2))

代码逻辑分三层:先拼请求头,User-Agent伪装成正常浏览器,避免被 B 站的反爬策略按「空 UA 脚本」处理;再检查 HTTP 状态码和业务状态码,B 站接口即使请求成功也会在 JSON 里返回一个code字段,code=0才是真正拿到了数据;最后只把热度分析要用的字段抽出来落 JSON。

参数说明里最值得关注的是sessdata。这是登录后 cookie 中的凭证字段,从浏览器开发者工具里复制。不带它也能访问详情接口,但单位时间内的请求额度会低很多,某些风控严格的网络环境下更容易触发 412。timeout=10必须写,否则某个视频接口卡住时,后续批量任务会整体挂死。

2.3 批量采集时如何控制速率与异常重试

单视频接口通了对,下一步就是把它放到循环里批量跑。但批量不是简单 for 循环加函数调用,需要处理两个现实问题:接口频率限制和单条失败不拖垮整体。下面这个采集函数是我常用的骨架。

import csv import random import time def batch_fetch(bvid_list: list[str], out_path: str, sessdata: str = "", interval: float = 3.0) -> None: rows = [] for bvid in bvid_list: try: info = fetch_video_info(bvid, sessdata) stat = info["stat"] rows.append({ "bvid": bvid, "title": info["title"], "pub_ts": info["pubdate"], "view": stat["view"], "like": stat["like"], "coin": stat["coin"], "favorite": stat["favorite"], "share": stat["share"], "reply": stat["reply"], "danmaku": stat["danmaku"], }) print(f"[OK] {bvid} 播放 {stat['view']}") except Exception as exc: print(f"[FAIL] {bvid} -> {exc}") time.sleep(interval + random.uniform(0.5, 1.5)) with open(out_path, "w", newline="", encoding="utf-8-sig") as fp: writer = csv.DictWriter(fp, fieldnames=rows[0].keys()) writer.writeheader() writer.writerows(rows)

interval是核心参数。我实测的经验值:不带登录态时,interval低于 1.5 秒,大约第 30 到 50 个请求就会收到 412;带上SESSDATA后可以压到 2 秒,但我仍然用 3 秒基线。加的random.uniform(0.5, 1.5)是随机抖动,让请求间隔不是固定节奏,降低被风控识别为批量脚本的概率。

写入 CSV 时用encoding="utf-8-sig",这是给 Excel 用户留的后悔药。直接用utf-8编码,Excel 打开时中文标题会乱码;utf-8-sig带 BOM 头,Excel 识别无障碍。rows[0].keys()作为表头,省去手写字段名,只要你保证rows非空——所以空列表时建议提前抛异常。

2.4 把采集脚本配置成每天定时跑一次

热度分析要做成趋势分析,就必须在不同时间点重复采集同一个视频的数据。手动跑脚本不可靠,我自己的习惯是把采集做成一个每天凌晨执行的定时任务。Windows 上用「任务计划程序」调python batch_fetch.py,Linux 服务器上写 crontab:

0 3 * * * cd /data/bilibili_heat && /usr/bin/python3 batch_fetch.py >> run.log 2>&1

batch_fetch.py里要维护一个待采集 bvid 列表,通常是从你的选题库里读。每次运行追加一行记录到 CSV,而不是覆盖,这样同一个 bvid 会留下多条不同fetch_ts的数据,第 4 章画时间序列曲线就靠它们。如果一天只采一次,CSV 里同一日期只有一行,一周后你就有七行,足够看出曲线拐点。

3. 热度不该是一个数字:播放、互动、传播的三维加权方案

3.1 一套不拍脑袋的指标权重:从行为漏斗出发

很多现成的热度分析代码喜欢写热度 = 播放*0.5 + 点赞*0.3 + 弹幕*0.2,这种权重基本靠拍脑袋。问题在于播放量经常比其他指标大两个数量级,一个一万播放的视频点赞可能只有五十,线性加权后点赞几乎不影响排名,最后等价于按播放排序,分析了个寂寞。

我习惯把指标按用户行为漏斗分成三层:第一层是规模层,看播放量,表示内容触达了多少人;第二层是质量层,看点赞、投币、收藏,表示看完后用户认可程度;第三层是传播层,看分享、评论、弹幕,表示用户愿不愿意把它扩散到站外或者拉人讨论。

从成本角度看,分享的行为成本最高,所以它不应该和点赞放在同一层加权。很多 UP 主做「三联」引导,本质就是同时在推高质量层里的点赞、投币、收藏。做权重方案时,我的默认分配是质量层内点赞 35%、投币 25%、收藏 20%、分享 20%;互动层内评论和弹幕各占一半权重。这个比例不是数学推导的,而是来自一个朴素假设:用户每一次行为的操作成本,决定它对热度的贡献上限。

3.2 热度指数的 Python 计算函数

指标量纲差太多,直接加权不行,所以要先做归一化。不用 min-max 归一化,它对异常大值太敏感——一个百万播放的头部视频会把其他所有视频的得分压到接近 0。我用的公式是score = value / (value + anchor),anchor 取分区中位数。这个函数有三个优点:值域恒在 0 到 1 之间;越接近 anchor 区分度越大;对极端大值不敏感,因为 x 越大函数增长越平缓。

import pandas as pd ANCHOR = { "view": 20000, "like": 1200, "coin": 300, "favorite": 400, "share": 80, "reply": 150, "danmaku": 600, } def heat_index(df: pd.DataFrame, anchor: dict = ANCHOR) -> pd.DataFrame: cols = ["view", "like", "coin", "favorite", "share", "reply", "danmaku"] for col in cols: df[f"{col}_score"] = df[col] / (df[col] + anchor[col]) df["quality_score"] = ( 0.35 * df["like_score"] + 0.25 * df["coin_score"] + 0.20 * df["favorite_score"] + 0.20 * df["share_score"] ) df["discussion_score"] = ( 0.50 * df["reply_score"] + 0.50 * df["danmaku_score"] ) df["heat"] = ( 0.45 * df["view_score"] + 0.35 * df["quality_score"] + 0.20 * df["discussion_score"] ) return df.sort_values("heat", ascending=False)

这个函数把原始播放量、点赞这些字段映射到*_score,再用两层加权合成heat。第一层加权是行为漏斗内的占比,第二层是规模、质量、讨论三个大维度之间的占比。0.45/0.35/0.20 是默认值,适合多数分区;如果是知识区、教程类视频,收藏的价值应该更高,我会把 quality 提到 0.45,discussion 压到 0.10;如果是鬼畜区、音乐区,弹幕互动不能低估,discussion 可以提到 0.25。

这里有一个我反复调试后的心得:权重方案不是一次定死的,要拿历史爆款去回测。你选定的权重,应该让那些年度爆款排进前 5%,让正常投稿落在中间区间,如果排名结果和你的主观感受严重不符,说明权重需要调整。这就像写量化交易策略代码——先定因子,再用历史数据验证因子有效性。

3.3 怎么用分区中位数锚定热度刻度

ANCHOR里的数据从哪来?我的做法是调用 B 站排行榜接口,对目标分区实时取中位数。排行榜接口返回的是当前热门视频,虽然整体偏高,但它的中位数正好代表「在这个分区里进入热门线需要的数据水平」,适合作为锚点。

from statistics import median def build_anchor(rid: int, sessdata: str = "") -> dict[str, float]: url = "https://api.bilibili.com/x/web-interface/ranking/v2" params = {"rid": rid, "type": "all"} headers = {"User-Agent": "Mozilla/5.0"} if sessdata: headers["Cookie"] = f"SESSDATA={sessdata}" resp = requests.get(url, params=params, headers=headers, timeout=10) items = resp.json()["data"]["list"] keys = ["view", "like", "coin", "favorite", "share", "reply", "danmaku"] anchor = {} for key in keys: anchor[key] = median(item["stat"][key] for item in items) return anchor

注意这里取的是stat区块里的原始值,和前面的采集口径保持一致。rid是分区 ID,1 是动画、3 是音乐、4 是游戏、36 是知识类,完整列表可以从 B 站官网的频道链接里看到。带登录态能降低接口返回失败的频率,但排行榜接口比详情接口宽松一些,短时间调用几次问题不大。

拿到分区实时中位数后,把它传给heat_index就能算热度分。这个方案的好处是随市场水位动态变化:两年前一个 5 万播放的视频在知识区算热门,今天可能只是中游水平,用固定阈值会严重失真。用中位数锚定后,不同时间采集的数据放在一起排序,某些视频热度下降不一定是视频被限流了,也可能是整个分区的水位涨了——这是视频热度分析里最容易被忽略的隐藏变量。

4. 用 Python 数据可视化把热度曲线画出来:播放量与互动量的双轴分析

4.1 把多次采集结果整理成时间序列

热度分析如果只看某一个时刻的快照,那叫抽查。要看出一个视频发布后 7 天的走势,前提是你对同一个 bvid 积累了多个时间点的采集记录。第 2 章的定时采集脚本会把每次结果追加进 CSV,所以在画图之前,先把数据转成时间序列格式。

import pandas as pd df = pd.read_csv("bilibili_heat.csv", encoding="utf-8-sig") df["fetch_ts"] = pd.to_datetime(df["fetch_ts"], unit="s", errors="coerce") df["pub_ts"] = pd.to_datetime(df["pub_ts"], unit="s", errors="coerce") bvid = "BV1xx411c7mD" one = df[df["bvid"] == bvid].sort_values("fetch_ts") one["hours_since_pub"] = (one["fetch_ts"] - one["pub_ts"]).dt.total_seconds() / 3600

fetch_ts是每次采集的实际时间,pub_ts是视频发布时间。两者相减得到的hours_since_pub是分析时间轴的核心变量:视频发布后第 1 小时采集到一次,第 24 小时采集到一次,第 72 小时一次,横轴就有意义了。如果 CSV 里只有最新的一次采集数据,画出来的图只有一个点,所以定时采集从视频发布当天就要开始,越早越好。

清理数据时还要注意重复项。定时任务如果某天跑了两遍,同一个fetch_ts可能出现两行,画图前用drop_duplicates(subset=["bvid", "fetch_ts"])去重。我遇到过最隐蔽的问题是两个采集任务并发写入同一个 CSV,导致一行记录被写了一半,时间字段解析失败变成NaT,画图时这些行会被直接跳过,所以在定时任务里加了文件锁,避免并发写同一个文件。

4.2 双轴曲线:播放量和互动量不能共用一条 Y 轴

播放量和投币量的数量级差太多,共用一条 Y 轴的结果是投币曲线变成紧贴底部的直线。正确的画法是双 Y 轴:左边画播放量柱状图,右边画投币量或互动量折线图。这种 Python 数据分析可视化的处理方式,在项目报表里也最容易被非技术同事理解。

import matplotlib.pyplot as plt fig, ax_left = plt.subplots(figsize=(12, 5)) ax_left.bar(one["hours_since_pub"], one["view"], color="#fb7299", alpha=0.6, label="播放量") ax_left.set_xlabel("发布后小时数") ax_left.set_ylabel("播放量", color="#fb7299") ax_right = ax_left.twinx() ax_right.plot(one["hours_since_pub"], one["coin"], color="#00aeec", marker="o", label="投币量") ax_right.set_ylabel("投币量", color="#00aeec") fig.autofmt_xdate() plt.title(f"{bvid} 播放量 vs 投币量") plt.show()

twinx()创建共享 X 轴的第二个 Y 轴,两条曲线互不干扰。播放量用柱状图展示规模,投币量用折线图展示变化趋势,可以直观看到「投币高峰」和「播放高峰」是否同步。颜色选用 B 站品牌色粉蓝组合,纯粹是为了阅读体验。

这类图最有价值的场景是判断「视频是否被外部渠道带火了」。如果播放量折线在某个时间点突然上翘,但投币量和点赞量没跟着起,说明流量来自首页推荐或搜索,内容本身没有被认可;如果播放和投币同步跳涨,才是真正的优质内容破圈。这是热度分析里最常见的两种曲线形态,分不清就会误判。

4.3 自动标记拐点:涨幅超过 30% 就要警惕

曲线肉眼看不准,需要程序自动标出「爆发时刻」。我用的方法是计算播放量变化率,设定阈值找拐点。

one["view_growth"] = one["view"].pct_change() hot = one[one["view_growth"] > 0.30] print("涨幅超过 30% 的时间点:") for _, row in hot.iterrows(): print(f"发布后 {row['hours_since_pub']:.1f} 小时,播放 {row['view']},涨幅 {row['view_growth']:.1%}")

pct_change()是 pandas 里的一阶差分比例,表示当前值相对上一个采集点的增长百分比。0.30 这个阈值不是拍脑袋定的,它对应的是「两天内播放量从 1 万涨到 1.3 万以上」的显著变化;如果采集频率更高,比如每 6 小时一次,阈值可以降到 0.15,因为时间窗口变短,同样的日增长率会被拆散。

拐点标记的真正用途是反推内容传播路径。视频发布后 4 小时涨幅超 100%,大概率是粉丝基础好或者进了编辑推荐;发布后 48 小时突然出现第二波高峰,通常是被人转发到了站外平台。把这些时间点和外部投放记录放在一起,就能评估每次推广到底产生了多少播放增量。

5. 视频热度分析的避坑清单:五个让我翻车的问题

5.1 播放量显示--或长期为 0:接口字段滞后和缓存口径

现象:接口返回的视频信息里,stat.view是 0,但网页端明明能看到上万播放;或者网页上显示--,接口里却拿到了具体数字。无论哪种,直接拿stat.view算热度都会严重低估。

原因:view接口里stat区块是 CDN 缓存后的数据,不是实时读库。新发布的视频,或者长时间无人访问的冷门视频,缓存刷新频率很低;网页端显示--是前端对「低播放量」的格式化降级,不代表真实数据为 0。

解决:把view == 0当成「数据未就绪」而不是「零热度」。我在采集函数里对 0 值做了重试逻辑,间隔 10 秒后再拉一次,连续 3 次为 0 才打上flag_missing标记。另外不要只看view一个字段,用点赞、硬币等多个指标交叉判断,即使播放量滞后,互动量通常已经反映出真实热度。

5.2 批量请求到一半全部返回 412:频率触发风控

现象:程序跑前 30 个 bvid 都正常,第 31 个开始每个请求都返回412 Precondition Failed,停止程序等待几分钟后又能跑通。

原因:B 站的接口风控会统计单位时间内的请求数,连续高频请求同一个接口会触发临时封禁。412 不是封 IP,更像一次限流警告,但如果不处理,后续请求全部白费。

解决:我实测最稳的节奏是串行请求加随机间隔,不带 cookie 时interval至少 3 秒,带SESSDATA可以压到 2 秒。配合随机 UA 池,每次请求从 20 个 User-Agent 里随机选一个。避免使用多线程并发去刷同一个接口,B 站热度分析用串行就足够了,并发收益很小但封禁概率倍增。

5.3 接口返回code = -404并不代表视频被删

现象:批量采集中某个 bvid 返回code=-404,你以为视频被删了,就直接跳过,但事后在网页上搜索,视频还能正常观看。

原因:-404是 B 站统一业务错误码,触发条件很宽:bvid 拼写错误、视频处于转码审核期、视频被设置为「仅自己可见」、接口访问者地区限制,都可能返回这个码。它只是告诉你「这个 bvid 在当前条件下取不到数据」,不是已删除。

解决:把-404和-400分开记录,前者标注为「待复核」,后者标注为「参数错误」。批量任务遇到-404不要中断,记录到单独的missing.log文件,全部跑完后人工复核。我踩过的坑是把它当成删除信号,结果漏掉了一个后来爆起来的视频,导致数据集里少了一条关键样本。

5.4 cookie 过期导致数据结构突变、字段缺一大半

现象:采集脚本稳定跑了三天,第四天突然大量视频只返回view和like,coin、favorite、share全是空值或缺失,代码在取stat["coin"]时抛KeyError。

原因:SESSDATA是登录态凭证,过期后接口退回游客权限。位于登录态的接口在返回字段上会比游客态更完整,cookie 一失效,部分字段直接从 JSON 里消失,而不是返回 0。

解决:给采集函数加一个返回结构自检,检测stat里是否包含全部 7 个指标字段,发现缺失就打印明确的「cookie 可能过期」告警并停止任务,而不是继续产出残缺数据。同时把 cookie 写在单独的配置文件里,过期后只需要更新配置文件,不用改业务代码。这个教训让我养成了每次采集前先跑一次「体检」函数的习惯,浪费 10 秒能省下半天排查时间。

5.5pubdate是 Unix 时间戳,不是字符串,处理不当全表时间错乱

现象:把pubdate直接用str()转成字符串写入数据库,后面做时间排序时出现 1970 年和 2023 年混在一起的数据。

原因:pubdate字段是 Unix 时间戳,单位是秒。很多新手教程里直接datetime.fromtimestamp(info["pubdate"])转成日期再落库,这一步没问题;问题出在有的视频接口会返回pubdate: 0,转换后变成 1970 年,干扰后续按时间分组统计。

解决:落库时对pubdate做合法性校验,小于某个阈值(比如 2015 年对应的时间戳)就直接标记为脏数据,不参与时间序列分析。同时保留原始时间戳字段和转换后的人类可读字段,方便排查。这个坑在数据量小时显不出来,一旦积累到几千个视频,时间轴上有几个 1970 年的点会让所有趋势图都变得没法解释。

6. 拿 24 小时播放增速检验你的热度指数是否健壮

热度指数做好之后,怎么验证它不是一堆数字拼出来的花架子?我的习惯是做一个 24 小时增速检验。这个方法的思路和量化交易回测很像:如果你的热度权重方案是有效的,那么视频发布后 24 小时的播放增速应该能预测它未来 30 天的热度排名。

具体做法是取你分析过的历史视频,算出每个视频发布后 24 小时的播放量,除以 0 到 24 小时平均小时数,得到growth_24h指标。然后按数据从小到大分成三档:低于 33 分位数的是慢热型,高于 66 分位数的是爆发型,中间是正常型。现在拿这个分桶结果和视频最终的热度指数做交叉验证。

import numpy as np import pandas as pd backtest = pd.read_csv("history_videos.csv") q33, q66 = np.percentile(backtest["growth_24h"], [33, 66]) def growth_bucket(value: float) -> str: if value <= q33: return "slow" elif value <= q66: return "normal" return "fast" backtest["bucket"] = backtest["growth_24h"].apply(growth_bucket) cross = backtest.groupby("bucket")["heat"].mean() print(cross)

这个结果通常会很漂亮:爆发型视频的平均热度分显著高于慢热型。如果你的数据不是这样,比如三挡的平均热度分几乎一样,说明热度指数的权重设置有大问题——可能是播放量权重过高,让一个 24 小时只涨了三成播放的内容拿到了和爆发内容一样的热度分,或者是互动权重没有真正参与区分。

我现在的习惯是,拿到任何一份热度分析结果,先去看视频发布后 24 小时曲线斜率是正还是负,再看热度指数有没有把它排进前列。斜率是正的但热度分不高,说明权重低估了早期增长信号;热度分高但 24 小时斜率是负的,说明视频靠的是存量累计,不是内容本身出圈,这种视频往往不值得追。这套交叉验证方法是对热度指数最直接的体检,做完这一步,那个基于 Python 的视频热度分析方案才算真正能拿去支撑决策。

热度分析这件事没有一劳永逸的公式,每个分区的用户行为习惯都不一样,权重需要按季度回看和微调。希望帮到你,少走我走过的这些弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询