☰
基于Django的B站数据分析可视化系统设计与实践
2026/10/13 14:47:24 网站建设 项目流程

又到毕设季,后台私信里问得最多的方向之一就是“B站数据分析”。有问选型的,有问数据从哪来的,也有拿着代码跑不起来要远程看的。问的人多了,我干脆把手上这套基于Django的B站数据分析可视化系统从需求、设计到落地的完整链路拆一遍:数据怎么采集、表怎么设计、接口怎么返回、ECharts怎么接,以及答辩时那些会被反复追问的细节。

整个项目后端只用Django,前端可视化部分用ECharts,覆盖视频、UP主、弹幕三个维度的采集、清洗、入库、统计和展示,是一条能完整跑通的大数据链路。准备拿它做毕业设计的同学、想入门Python Web+可视化方向的新手,都可以顺着这篇文章把整套方案吃透。不只是给代码思路,我会把当时踩过的坑也一并交代清楚。

1. 选型思考:为什么是Django+B站数据,而不是Flask或Spring Boot

1.1 三个技术栈的对比

很多同学第一反应是“Python做后端那就Flask呗,轻量”。但毕设场景下,轻量并不等于省事。Flask确实自由,ORM要自己接SQLAlchemy,后台管理要自己写或找第三方插件,用户认证、表单验证、分页这些常规组件基本都要手动拼装。在一个以“数据分析可视化”为主线的项目里,这些拼装工作会吃调大量本该用在核心业务上的时间。

Django则把大部分基础设施都给你配好了:自带ORM和migration机制、自带Admin后台、自带模板和表单处理。再加上Django REST framework,接口开发直接从“手写JSON解析”变成“定义序列化器+ViewSet”。

Spring Boot适合本身就在学Java的同学,但大数据可视化方向的毕设题目通常都围绕Python生态展开,如果论文题目已经写了“基于Python+Django”,那就更没必要换语言。

我整理了一张选型对比表,方便你直接参考:

对比维度DjangoFlaskSpring Boot
上手成本中等,约定多于配置低,但组件要自己集成偏高,需要Java生态基础
ORM/数据模型Django ORM + migrationsSQLAlchemy需要另配JPA/MyBatis
Admin后台自带,开箱即用需要插件需要额外实现
接口开发DRF,序列化/分页/认证齐全Flask-RESTful手动拼Spring MVC成熟
适合场景快速搭建数据管理与展示系统轻量单接口服务企业级微服务

Django的migration机制对毕设尤其友好。开发过程中临时要加一个字段,比如给视频表加一个“收藏数”,直接改models.py然后执行makemigrations和migrate,数据库结构就同步了,不用手工去改表。这对于经常改动需求的毕设阶段来说,能省很多事。

1.2 B站数据源天然适合做分析

数据源的选择决定了整个论文能讲多少东西。选一个只有二三十个字段的普通网站,分析维度会很单薄。B站的优势在于数据维度足够丰富:

  • 视频维度:标题、简介、标签、分区、发布时间、播放量、弹幕数、点赞、投币、收藏、分享
  • UP主维度:昵称、性别、粉丝数、视频数、签名、认证信息
  • 弹幕维度:内容、发送时间点、用户ID哈希

这三个维度凑在一起,已经能支撑“热门视频排行”“分区热度分析”“头部UP主对比”“弹幕高频词统计”这些经典模块。而且B站本身自带分区、榜单这些业务概念,毕设界面上直接按分区筛选、按播放量排序,产品逻辑很自然。

另外,B站的视频信息和弹幕数据都有公开可访问的接口。这里要提醒一句:不要高频并发去刷接口,合理控制请求频率,间隔1到2秒抓一次,做毕设的数据量足够,也不会对平台造成压力。

1.3 系统整体分层

整个系统的数据流是单向的,从采集到展示一共五层:

  • 采集层:requests/httpx请求公开接口,拿原始JSON
  • 清洗层:pandas配合自写解析规则,清理字段
  • 存储层:MySQL或者SQLite保存业务表
  • 后端层:Django + DRF提供图表API和后台管理
  • 可视化层:ECharts渲染图表,前端原生JavaScript接管交互

这五层就是大数据架构里面常说的“采集、存储、计算、应用”四层结构的落地版。答辩时画一个这样的分层图,评委会觉得你思路是清晰的。

2. 数据采集与清洗:B站数据落库要过的那些坎

2.1 数据范围怎么定

“B站数据分析”听起来很大,但毕设系统不会也不能真的把全站数据抓下来。更合理的做法是选定几个分区,比如科技、游戏、生活区,按当前热门视频列表和UP主主页抓取相对完整的样本。

我当时的建议数据量是:视频表1000到3000条、弹幕5万到10万条。这个规模既能让图表有看头,也不会在演示时因为数据量太大导致页面卡顿。更重要的是,这个量级的清洗和处理逻辑足以支撑论文里“数据预处理”章节的写作。

如果不知道怎么选视频,可以按分区从B站的热门列表接口取,每个区取前50个视频,然后再顺着UP主主页抓他们的历史热门作品,这样既能保证视频本身有热度,也能为后续的UP主分析积累数据。

2.2 采集模块的工程化处理

采集脚本不能只写一个requests.get放在那,要考虑网络抖动、超时、接口返回异常这些真实环境里的问题。封装一个带重试机制的请求函数是基本功:

import time import requests from requests.adapters import HTTPAdapter session = requests.Session() session.mount('https://', HTTPAdapter(max_retries=3)) def fetch_json(url, params, headers): for attempt in range(3): try: resp = session.get(url, params=params, headers=headers, timeout=10) resp.raise_for_status() return resp.json() except (requests.ConnectionError, requests.Timeout) as e: time.sleep(2 * (attempt + 1)) return None

我习惯把每次请求的原始JSON顺手存一份到本地文件。这样做有两个好处:第一,后续清洗逻辑改动时不用重新抓数据;第二,接口数据格式变化时,可以对照原始JSON排查是哪个字段出了问题。

请求间隔建议在1到2秒之间,加一点随机抖动。这个频率下,抓几千条视频和几万条弹幕也就是一两个小时的事,完全可以接受。

2.3 清洗环节里最容易被忽略的四个细节

清洗是整个项目里最容易被敷衍,但又是最影响分析结果的环节。有四个细节我在这里集中说一下。

第一是数量字段的单位问题。B站网页端很多数量会用“12.3万”这样的字符串展示,虽然接口返回的原始值大多是数字,但如果混用了页面解析数据,就会遇到这种情况。建议写一个统一的解析函数:

def parse_count(value): if isinstance(value, (int, float)): return int(value) value = str(value).strip() if value.endswith('万'): return int(float(value[:-1]) * 10000) if value.endswith('亿'): return int(float(value[:-1]) * 100000000) try: return int(value) except ValueError: return 0

这个函数一定要在数据落库之前跑一遍,否则排序和聚合时会出现“字符串比数字还大”的笑话。

第二是空值处理。视频简介可能为空、标签可能为空、UP主签名可能为空,这些字段在入库时统一填充默认值,不建议直接丢弃整条数据。

第三是时间字段的时区问题。弹幕接口返回的时间戳通常是毫秒级整数,需要先除以1000再转datetime。数据库里存储时统一用UTC,展示时再转东八区,避免不同机器的本地时区不一样导致图表上的日期错位。

第四是去重。视频表以BV号做唯一键,增量更新时用Django的update_or_create而不是先delete再insert,这样不会打断主键连续性和快照表的历史记录。

2.4 表结构设计:别把所有东西塞进一张表

数据模型是整个系统的地基。我把主要表拆成四张:视频表、弹幕表、UP主快照表、视频每日快照表。

from django.db import models class Video(models.Model): bvid = models.CharField(max_length=32, unique=True, db_index=True) title = models.CharField(max_length=255) partition = models.CharField(max_length=64, db_index=True) author = models.CharField(max_length=64, db_index=True) pub_time = models.DateTimeField(db_index=True) view_count = models.BigIntegerField(default=0) danmaku_count = models.BigIntegerField(default=0) like_count = models.BigIntegerField(default=0) coin_count = models.BigIntegerField(default=0) favorite_count = models.BigIntegerField(default=0) share_count = models.BigIntegerField(default=0) tags = models.CharField(max_length=500, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Danmaku(models.Model): video = models.ForeignKey(Video, related_name='danmakus', on_delete=models.CASCADE) content = models.TextField() time_sec = models.FloatField() user_hash = models.CharField(max_length=64) created_at = models.DateTimeField(auto_now_add=True) class VideoDailySnapshot(models.Model): video = models.ForeignKey(Video, on_delete=models.CASCADE) date = models.DateField(db_index=True) view_count = models.BigIntegerField() like_count = models.BigIntegerField() coin_count = models.BigIntegerField() favorite_count = models.BigIntegerField() class Meta: unique_together = ('video', 'date')

视频表单独存当前最新数据,快照表每天记录一次各个维度的数值。为什么要单独建快照表?因为播放量、点赞数是随时间变化的。如果只存一份静态数据,“播放趋势分析”这个论文模块就没法做。有了按天的快照,就可以画出单条视频两周内的热度曲线,也是后续所有时间趋势图的数据来源。

弹幕表用外键关联到视频,查询某视频下所有弹幕时,直接用video__danmaku_set或者related_name的反向查询,效率高,语义也清晰。

3. Django后端设计:接口不能只有增删改查

3.1 统一接口返回结构

前后端对接最忌讳各写各的。前端不知道后端什么时候返回错误,后端不知道前端要什么格式。我建议把所有接口统一成这个结构:

{ "code": 200, "message": "success", "data": {} }

封装一个通用函数放在公共模块里:

def response_ok(data=None, message='success'): return JsonResponse({'code': 200, 'message': message, 'data': data}) def response_error(message='error', code=500): return JsonResponse({'code': code, 'message': message, 'data': None})

前端只管判断code是否等于200,其他字段一律不看,这能少掉一大半联调的沟通成本。

3.2 核心图表接口:ViewSet加聚合查询

Django REST framework的ViewSet是写这类接口的最高效方式。一个视频趋势接口可以这样写:

from rest_framework import viewsets from rest_framework.response import Response class TrendViewSet(viewsets.ViewSet): def list(self, request): bvid = request.query_params.get('bvid') qs = VideoDailySnapshot.objects.filter( video__bvid=bvid ).order_by('date') dates = [str(item.date) for item in qs] views = [item.view_count for item in qs] likes = [item.like_count for item in qs] return Response(response_ok({ 'dates': dates, 'views': views, 'likes': likes }))

排行榜接口用Django的聚合查询,一条语句就能算出每个UP主的总播放量:

from django.db.models import Sum, Count top_ups = ( Video.objects .values('author') .annotate(total_views=Sum('view_count'), video_count=Count('id')) .order_by('-total_views')[:20] )

这里有个实际执行的坑:如果视频表已经有几千条数据,而author字段没有索引,每次排行榜接口调用都要做一次全表扫描加分组。几千条的时候感觉不明显,一旦到几万条,接口响应时间会明显上升。所以前面建表时专门给author和partition加了db_index。

另外说一句Django删除对象的坑。很多人习惯用Video.objects.filter(...).delete()直接删,但要注意关联数据的行为。弹幕表外键设置了on_delete=models.CASCADE,删除视频会连带删除所有弹幕。如果后续想保留历史数据做审计分析,就要考虑软删除方案,给模型加一个is_active字段,接口查询时默认过滤掉已删除数据,而不是真正从库里删掉。

3.3 缓存层:为什么统计接口要套缓存

排行榜、分区热度这类接口每次请求都在重复计算聚合结果,完全可以用缓存扛掉。Django内置的caches框架可以无缝切换内存缓存和Redis。我当时的策略是:热门接口缓存60秒,后台采集任务跑完之后主动删除对应缓存key,这样新数据一进库,下一次接口请求就能拿到最新结果。

from django.core.cache import cache key = 'top_videos_partition:%s' % partition data = cache.get(key) if data is None: data = list( Video.objects.filter(partition=partition) .order_by('-view_count')[:50] .values('bvid', 'title', 'author', 'view_count') ) cache.set(key, data, 60) return response_ok(data)

演示环境下如果不想搭Redis,Django自带的本地内存缓存LocMemCache也能撑住。单机演示场景完全够用,答辩的时候不用非得起一个Redis服务。

3.4 WebSocket推送:后台采集完数据前端自动刷新

很多同学看到“后台有数据变化,前端自动推送”就头大。其实在Django生态里,用Channels做这件事不算复杂。当时我给系统加了一个小功能:在管理后台点“开始采集”按钮,采集任务跑完后通过channel layer给所有在线页面推送一条“数据更新完成”的消息,前端收到后自动刷新图表。

# consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer class DataConsumer(AsyncWebsocketConsumer): async def connect(self): await self.channel_layer.group_add('data_group', self.channel_name) await self.accept() async def data_updated(self, event): await self.send(text_data=json.dumps(event['data']))

采集结束后触发推送:

from channels.layers import get_channel_layer from asgiref.sync import async_to_sync channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)('data_group', { 'type': 'data_updated', 'data': {'code': 200, 'message': '采集完成'} })

前端WebSocket收到消息后调用对应的加载函数就行。这个功能在答辩现场演示时效果很好,能直观体现“系统会自动更新”而不是死板的一张静态图表。但我建议把它定位为加分项,不要花太多时间调优,主链路还是“采集—接口—图表”三件套。

4. 可视化落地:ECharts选图与前后端配合

4.1 什么数据配什么图

ECharts本身功能强大,但选错图会直接影响分析结论的表达。我做这套系统时遵循一条原则:先明确要说明什么结论,再选能突出这个结论的图表。

分析需求推荐图表说明
播放趋势变化折线图时间序列首选,X轴日期,Y轴播放量
分区播放占比饼图/南丁格尔玫瑰图占比关系直观,玫瑰图更有视觉冲击
UP主播放量排行横向柱状图类别名称长时横向更易读
点赞/投币/收藏对比雷达图同时展示多个指标的对比
弹幕高频词词云文本分布可视化,直观反映用户讨论热点
视频时长分布直方图连续字段分段后统计频次

4.2 前端请求与渲染的代码约定

图表初始化和数据请求一定要分离。页面加载时先初始化一个空图表,数据请求回来之后只setOption填充数据,这样能避免接口慢时页面一片空白。

async function loadTrend(bvid) { const resp = await fetch(`/api/trend/?bvid=${bvid}`); const result = await resp.json(); if (result.code !== 200) return; trendChart.setOption({ xAxis: { type: 'category', data: result.data.dates }, series: [ { name: '播放量', type: 'line', data: result.data.views }, { name: '点赞数', type: 'line', data: result.data.likes } ] }); } document.addEventListener('DOMContentLoaded', () => { trendChart = echarts.init(document.getElementById('trendChart')); loadTrend('BV1xxxx'); });

这里有一个非常经典的坑:多次调用setOption,ECharts默认是merge模式,不是整体替换。如果第二次返回的数据里少了一个series,旧series还会残留。要整体替换就必须传第二个参数setOption(option, true)。我在做弹幕筛选功能时就被这个残留问题坑了一次,图表上一直多出一条已经不该存在的曲线。

4.3 词云和排行榜渲染的注意事项

弹幕词云看着绚丽,但处理不好容易翻车。弹幕文本要先清洗再分词。直接用原始弹幕文本渲染,大概率出现的是“的、了、啊、哈哈哈哈”这些没有分析价值的词。

我当时用了jieba分词,配合一份停用词表过滤低频词和语气词,再统计词频。这里给一个大致思路:

import jieba from collections import Counter stopwords = set(['的', '了', '啊', '哈哈', '2333']) def extract_keywords(danmaku_list, top_n=300): words = [] for text in danmaku_list: cut = jieba.lcut(text) words.extend([w for w in cut if len(w.strip()) > 1 and w not in stopwords]) return Counter(words).most_common(top_n)

词云组件渲染几千个词会导致浏览器卡顿,我实际测试下来,500个词以内体验比较流畅。所以后端接口里直接限定词云接口返回前300到500个词就够了。

4.4 页面性能的调整手段

大数据量项目最容易被人质疑的地方就是“图表转半天才出来”。除了前面提到的缓存,还有两个常用手段。

第一是控制单次请求的数据量。趋势图跨90天,前端X轴就不要一次性渲染所有点,可以用dataZoom组件让用户自己缩放视野。后端接口也可以增加limit参数,只返回最近30天的数据,配合提示文案“完整数据可在图表中缩放查看”。

第二是减少动画。ECharts默认的入场动画在数据量大的时候会让浏览器渲染很吃力。可以在option里设置animation: false,或者只保留第一次进入页面时的动画。演示环境中,稳定流畅比花哨重要得多。

5. 从“能跑”到“能答辩”:演示、文档和源码交付的实战细节

5.1 演示现场最容易翻车的三件事

我见过好几个同学代码写得没什么问题,结果答辩演示时翻车。总结下来主要三件事。

第一,数据库里只有几十条测试数据,图表看起来稀稀拉拉。这个问题最好解决也最致命:答辩前至少导入1000条以上真实数据,让排行榜、趋势图都有足够的样本量,否则评委一眼看出是空壳。

第二,图表没有在页面加载后自动请求。很多前端页面是手动触发某个按钮才加载图表,演示时紧张会忘记点。把DOMContentLoaded事件里绑定所有初始图表加载函数,打开页面即可看到完整图表。

第三,依赖服务没启动。MySQL没起、Redis没起、Django开发服务器没起,任何一个环节断了页面都白屏。写一份清晰的启动脚本,把这几个服务一次性拉起,并按顺序启动,能避免大量尴尬。

5.2 源码目录和README怎么写

源码交付的时候,目录结构越清晰,越能体现工程能力。我习惯把项目拆成独立子应用:

bilibili_analyzer/ ├── manage.py ├── config/ # 项目配置、URL路由 ├── apps/ │ ├── collector/ # 数据采集 │ ├── analyzer/ # 数据分析和API │ └── dashboards/ # 可视化页面与静态资源 ├── static/echarts/ # ECharts库文件 ├── scripts/ │ ├── import_data.py # 导入数据脚本 │ └── start_demo.sh # 一键启动演示环境 └── requirements.txt

README里必须写清楚四件事:Python版本和依赖安装命令、数据库初始化方式、数据导入命令、启动命令。写完自己从零按README操作一遍,如果自己能顺利跑起来,交付才有意义。

5.3 答辩时必讲的三个链路

答辩评委基本都会沿着技术线路追问。我在准备答辩时总结了三条链路,只要能完整讲清楚,提问基本都能应付。

数据链路:公开接口获取原始JSON,经过解析、清洗、去重后入库。讲清楚清洗规则是重点。

代码链路:前端发起请求,URL路由分发到ViewSet,通过ORM查询数据库,序列化后返回JSON。讲清楚聚合查询和缓存策略是重点。

可视化链路:图表组件初始化,Ajax请求数据,setOption渲染图表,交互组件联动请求新数据。讲清楚图表选择依据是重点。

这三条链路串起来,整个系统就是一个完整的数据产品,而不只是“几个页面加几个图”。

5.4 “程序+文档+代码讲解+一条龙定制”实际补的是什么

很多同学买源码之前以为拿到代码就能直接跑,后来发现真正花时间的反而是下面这几件事。调整表结构和字段语义,让自己论文题目和代码匹配;换一个数据方向,比如把B站换成小红书或电商平台;部署到答辩演示环境,处理依赖和端口问题;把系统的截图和模块说明整理进论文和PPT里。

这些事如果提前规划好,在拿到源码的基础上做定制,效率会高很多。最怕的是到最后一周才动手,数据没抓、图表没调、文档没写,再好的代码也救不了时间。

6. 想再进一步?三个扩展方向能拉开评分差距

6.1 给系统加一个“相似视频推荐”模块

基于视频标签的简单相似度计算,是性价比很高的扩展模块。把标签文本分词后向量化,再用余弦相似度计算视频之间的相似性,返回Top5相关视频。这个模块不用改现有表结构,只需要在analyzer应用里新增一个推荐接口,前端排行榜页面加一个“相关推荐”区域就行。

答辩时能讲出一点算法感觉:分词、向量化、相似度计算、召回排序。哪怕用的是最简单的词袋模型,也比“只用ORDER BY排序”要高级一个档次。

6.2 弹幕情感分析

用情感词典或者snownlp给弹幕打情感分,按视频维度聚合成正面指数、负面指数。这个模块能撑起论文的“情感分析”章节,展示上也很有话题性。视频详情页里加一个情感分数的小卡片,再配一张情感趋势曲线图,整个过程只改弹幕清洗和新增一个分析接口。

不过要提醒一句:snownlp对网络流行语和B站弹幕特有梗的识别效果一般,需要根据实际语料补充自定义词典。这一步也是可以写进论文的“模型优化”部分。

6.3 定时采集与自动日报

用APScheduler或者Celery配置定时任务,每天定时拉取一次数据,并把前一天的热门变化生成一份HTML日报。对这个方向感兴趣的同学可以做成“数据自动化运维”模块。实现上并不复杂,核心是把采集函数封装成可调用的task,再设置trigger。

这三个扩展方向里,我最推荐6.1或者6.2。改动范围小,论文里能单独开一章,答辩又有故事可讲,性价比最高。

最后说一点个人体会。这套系统真正难的不是代码,而是数据链路的设计——数据要能稳定进来、干净进来,后面的可视化才能有说服力。很多同学把时间都花在调ECharts样式上,表格和图表很好看,但一问数据从哪来、清洗规则是什么就卡壳。建议做的时候先把采集和表结构想清楚,前期丑一点没关系,跑通全链路之后再回头看,改起来也快。如果在做的过程中遇到具体问题,顺着“采集—清洗—入库—接口—图表”这条线逐一排查,大概率能找到问题出在哪个环节。

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

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

立即咨询