
简介这是一份基于Python的B站用户行为分析系统设计与实现文档面向需要完成大数据分析类课程设计、毕业设计或研究社交媒体用户行为的学生与开发者。内容围绕B站UP主行为与用户观看偏好展开涵盖视频类型、视频标签、粉丝数、获赞数、互动与一键三连等指标通过柱状图、折线图等可视化方式呈现并融入数据清洗、预处理、数据挖掘及可视化工具的应用能够为理解UP主影响力与内容推荐策略提供参考。资源包为1个docx文件整体大小1.05MB文档包含中英文摘要、目录、研究背景与意义、开发技术介绍等完整结构便于直接阅读与借鉴。已有432人学习下载。借助这份文档读者可快速把握系统整体架构与实现思路作为撰写论文、制作答辩PPT或设计同类系统的参考资料。1. 为什么要给 B 站 UP 主做一套行为分析系统做数据分析的人大概都经历过这种尴尬手里有一堆 B 站接口抓回来的 UP 主数据却在如何真正“用起来”上卡住。单看粉丝数、播放量没有意义真正有价值的命题是——这位 UP 主擅长什么内容、粉丝喜欢在什么时间看他、哪类视频最容易引发三连。把这几个问题拆开就是一套用户行为分析系统的核心以 UP 主为主体把视频分类、标签、发布时间、互动数据变成可对比、可下钻的指标。这套系统选型很明确Python 做数据处理Django 做 Web 框架MySQL 做存储前段图表交给 ECharts。技术栈不算新但组合起来正好覆盖从「数据接入—指标建模—可视化展示」的完整链路。适合的人群有两类一是要做毕业设计或课程项目的学生拿它可以快速搭出完整的前后端项目二是在做内容运营、平台分析的一线工程师可以把里面的分析思路直接移植到自己的数仓看板中。下面按我自己拆项目的顺序把设计要点、实现细节和排障经验展开讲。2. 分析指标体系与数据模型设计2.1 从用户行为到指标先定维度再写代码B 站的数据看似庞杂真正落到分析层面无非是「主体—行为—内容」三个维度的交叉。UP 主是主体粉丝数、获赞数、播放量是状态指标发布时间、投稿频率是行为指标视频分区、标签是内容属性。这套系统把分析拆成五个模块UP 主分析、用户分析、综合分析、多维分析、排名分析本质就是对这三类维度做不同粒度的聚合。在设计时我先确定了几个核心口径。UP 主分析关注的是个体这位 UP 主最常发什么分区、什么标签一周内哪几天活跃粉丝增长和播放总量之间的关系。用户分析则反过来把“人”作为主体统计哪种视频类型最容易引发互动、分享、收藏三连。综合分析是对平台整体的观察按小时、星期、月份统计视频发布量观察平台内容供给的节奏。多维分析和排名分析则把多个指标叠加找出头部 UP 主的共性特征。这里有一个容易被忽略的点所有指标必须先定义清楚计算逻辑再写代码。比如“获赞数”是按 UP 主汇总还是按视频汇总如果一位 UP 主有 100 条视频总播放量是这 100 条之和还是只统计最近 30 天口径不一致后面所有图表都是错的。我的做法是先画一遍指标字典表把指标名称、计算公式、聚合粒度、时间范围写清楚再进入数据库设计。2.2 Django ORM 建表多对多关系与冗余字段数据库设计上核心涉及三张业务表UP 主信息表、视频信息表、用户行为表。UP 主表和视频表是一对多关系视频表和标签表是多对多关系。MySQL 中多对多关系要建中间表Django ORM 里用ManyToManyField可以自动生成但建议手动建中间表方便后续在中间表上加权重字段或时间维度。# models.py from django.db import models class UpMaster(models.Model): mid models.CharField(max_length50, uniqueTrue, verbose_nameB站UID) name models.CharField(max_length100, verbose_nameUP主昵称) fans models.IntegerField(default0, verbose_name粉丝数) likes models.IntegerField(default0, verbose_name获赞数) views models.IntegerField(default0, verbose_name总播放数) reads models.IntegerField(default0, verbose_name阅读数) created_at models.DateTimeField(auto_now_addTrue) class Tag(models.Model): name models.CharField(max_length50, uniqueTrue) class Video(models.Model): bvid models.CharField(max_length20, uniqueTrue, verbose_name视频BV号) title models.CharField(max_length300, verbose_name视频标题) zone models.CharField(max_length50, verbose_name分区) pub_time models.DateTimeField(verbose_name发布时间) play models.IntegerField(default0) like models.IntegerField(default0) coin models.IntegerField(default0) favorite models.IntegerField(default0) share models.IntegerField(default0) up models.ForeignKey(UpMaster, on_deletemodels.CASCADE, related_namevideos) tags models.ManyToManyField(Tag, throughVideoTagRelation)这段代码的关键设计有三处。mid和bvid都加了uniqueTrue这是为了防止爬虫重复写入脏数据UpMaster表把粉丝、获赞、总播放、阅读数值冗余进来避免每次分析都去 JOIN 视频表做聚合查询性能会好很多VideoTagRelation作为手动中间表后续如果要对标签加权重、加统计时间范围直接在这个表上扩展字段就可以。视频和标签的多对多选择through参数手动管理中间表而不是直接让 Django 自动生成是因为后续做「最受欢迎的标签 TOP20」时需要按标签维度 GROUP BY 再聚合播放、三连数据有中间表在 SQL 写起来更直观。2.3 数据采集策略与清洗规则数据来源有两种常见路径一种是用 B 站开放接口抓 UP 主投稿列表、视频详情、用户行为数据另一种直接使用已有的公开数据集。接口方式要注意频率控制B 站接口有风控短时间高频请求会触发验证码我会在采集脚本里加随机延时和代理池切换。数据清洗的核心是去重和空值处理。视频表按bvid去重UP 主表按mid去重。发布时间字段统一转为datetime类型分区字段做映射——B 站分区 ID 和名称不一致时需要维护一张映射表。数值字段如果接口返回-或空字符串统一转0避免后续聚合时类型报错。3. 后端聚合逻辑与 Django 视图实现3.1 按时间维度聚合发布量从原始数据到图表数据综合分析界面要求支持按时、按周、按月三个维度查看视频发布量。这个功能的本质是把视频表的pub_time字段按照不同的时间粒度分组统计。Django ORM 没有直接提供date_trunc但可以通过extract或Trunc来实现。# views.py from django.db.models.functions import TruncHour, TruncWeek, TruncMonth from django.db.models import Count def publish_trend(request): unit request.GET.get(unit, hour) trunc_map { hour: TruncHour(pub_time), week: TruncWeek(pub_time), month: TruncMonth(pub_time), } trunc_func trunc_map[unit] result ( Video.objects .annotate(periodtrunc_func) .values(period) .annotate(totalCount(id)) .order_by(period) ) data [{period: str(item[period]), count: item[total]} for item in result] return JsonResponse({code: 0, data: data, unit: unit})这段代码的逻辑是通过TruncHour把pub_time截断到小时再按截断后的时间分组计数。用户切换「按小时、按周、按月」时前端传一个unit参数后端在trunc_map中选择对应的截断函数其他地方完全复用。这样一个视图就能支撑三个时间粒度不需要写三个接口。要注意的是TruncWeek的起始日Django 默认一周从周日开始而国内业务习惯周一作为一周起点。如果不处理周一和周日的数据会被分到不同的周里展示出来会有偏差。解决方式是在配置里设置WEEK_START或者在查询前对日期做偏移。3.2 榜单计算与 TopN 过滤排名分析涉及五个维度粉丝量排名、播放总量排名、投稿数量排名、阅读量排名和“最有实力 UP 主”排名。前四个是单指标排序最后一个需要综合评分。综合评分没有标准答案我采用的方案是「播放总量 获赞总数 粉丝数」三个指标做归一化后加权求和。from django.db.models import Sum, F def rank_upmaster(request): rank_type request.GET.get(type, fans) # 各UP主视频指标汇总 agg_data ( Video.objects .values(up_id) .annotate( total_playSum(play), total_likesSum(like), video_countCount(id) ) ) result [] for item in agg_data: up UpMaster.objects.get(iditem[up_id]) fans up.fans or 0 total_play item[total_play] or 0 total_likes item[total_likes] or 0 # 分数归一化每个指标除以所有UP主中的最大值 score ( 0.4 * total_play / max_play 0.3 * total_likes / max_likes 0.3 * fans / max_fans ) result.append({ name: up.name, fans: fans, score: round(score, 4) }) result.sort(keylambda x: x[score], reverseTrue) return JsonResponse({code: 0, data: result[:20]})这个「最有实力」排名的计算思路可以作为模板复用到其他场景。归一化时没有用 Z-Score 而是用「除以最大值」好处是指标量纲统一到 0~1 之间且不受数据分布影响缺点是对离群值敏感。如果出现某个头部 UP 主播放量是第二名几十倍的情况其他人的分数会被压到很低这种情况下建议改用对数归一化即对每个指标取log1p后再做除法。代码逻辑上先把视频表按up_id聚合出各 UP 主的播放、点赞和投稿数再关联 UP 主表的粉丝数计算综合分最后排序取前 20。3.3 用户行为分析互动、分享与三连的统计口径用户分析模块要回答的问题是哪类视频最容易被互动、被分享、被收藏和点赞。这里的数据来源是视频的互动数据即每一条视频的like、coin、favorite、share字段。这些字段是用户行为的聚合结果而不是原始的用户级行为日志。如果以后接入实时行为流可以在用户行为表里按video_id和behavior_type分组统计。def user_behavior_analysis(request): result ( Video.objects .values(zone) .annotate( total_playSum(play), total_likeSum(like), total_coinSum(coin), total_favSum(favorite), total_shareSum(share), avg_like_rateAvg(F(like) * 1.0 / F(play), output_fieldFloatField()) ) .order_by(-total_play) )这段 SQL 按分区聚合出播放、点赞、投币、收藏、分享的总量并计算平均点赞率。互动率、分享率都可以套同一个公式只是分子分母换字段。需要注意的是Avg里如果直接写F(like) / F(play)两个整数字段相除在部分数据库里返回值是整数0.5会变成0所以先乘1.0强制转浮点。另外play可能为 0需要先过滤掉play__gt0的数据否则会出现除零错误。4. 可视化实现与多维分析交互4.1 ECharts 柱状图与折线图的配置要点前端图表统一使用 ECharts。UP 主分析页面需要展示视频类型分布、视频标签分布、发布频率趋势三种图分别对应柱状图、柱状图、折线图。图表数据来自后端 JSON 接口前端通过 Ajax 拉取后填入option对应字段。// static/js/up_analysis.js fetch(/api/up/video_zone/) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(zoneChart)); const option { tooltip: { trigger: axis }, grid: { left: 10%, right: 10%, top: 15%, bottom: 15% }, xAxis: { type: category, data: data.map(item item.zone), axisLabel: { rotate: 30 } }, yAxis: { type: value, name: 视频数 }, series: [{ type: bar, data: data.map(item item.count), barWidth: 45%, label: { show: true, position: top } }] }; chart.setOption(option); window.addEventListener(resize, () chart.resize()); });ECharts 的配置有三处容易踩坑。第一是axisLabel的rotate属性当分类轴标签文本较长比如“手机游戏”“知识-社科人文”不旋转会互相遮挡第二是grid的留白左侧10%是为了给 Y 轴刻度值留空间如果图表左侧被裁掉优先检查这个值第三是window.addEventListener(resize)页面容器尺寸变化时图表不会自动更新必须手动调用chart.resize()。折线图与柱状图的不同只在series的type字段数据结构和配置项完全兼容做「发布数量趋势」时直接把bar换成line即可。4.2 多维分析散点图的实现三维指标降维多维分析模块的设计目标是同时观察视频量、播放量、粉丝量三个指标之间的关系。三维数据直接展示需要 3D 散点图但在 Web 端 3D 图表性能较差且交互复杂。我的做法是把三个指标映射到二维散点图上X 轴为总播放量Y 轴为视频投稿量点的大小代表粉丝数。这样通过位置和气泡大小就可以同时展示三个维度。const option { xAxis: { type: log, name: 总播放量 }, yAxis: { type: log, name: 视频投稿数 }, series: [{ type: scatter, data: data.map(item ({ value: [item.total_play, item.video_count, item.fans], name: item.up_name })), symbolSize: function(val) { return Math.max(8, Math.log2(val[2] 1) * 3); }, itemStyle: { opacity: 0.6 } }] };这里的关键技巧是坐标轴使用type: log对数刻度。B 站 UP 主数据极度偏态分布——头部 UP 主粉丝几百万尾部 UP 主只有几十如果直接用线性坐标大部分点会挤在左下角完全看不出分布规律。symbolSize用对数函数映射粉丝数到气泡大小保证差异大的数据也能在图上区分出层级。Math.log2计算出来的值如果不加Math.max做下限控制粉丝数少的小点会小到看不见交互时鼠标也不容易点中。4.3 Django 视图返回 JSON 与前端图表接口对接图表模块需要遵循一个约定视图只返回 JSON 数据不渲染模板页面骨架由 Django 模板负责图表渲染全部交给前端。这样前后端职责分离接口可以复用给其他终端也不需要为每个图表单独写模板页面。def api_rank_players(request): data list( Video.objects .values(up__name) .annotate(totalSum(play)) .order_by(-total)[:20] ) return JsonResponse({code: 0, data: data})接口路径建议全部放在/api/前缀下与页面路由区分开。前端拿到 JSON 后再做map操作提取字段避免二次请求。如果图表展示的数据和后端接口数据不一致先打开浏览器开发者工具看 Network 面板确认请求返回的 JSON 结构是否和前端代码里取字段名一致这是图表不显示时最常见的定位方法。5. 数据预处理解决因数据缺失导致的统计结果偏差5.1 空值与脏数据的处理规则数据分析系统跑出来的图表如果和直觉不符八成不是 SQL 写错而是数据源本身有问题。B 站接口返回的数据里常见三类脏数据分区字段为空、发布时间为默认的 1970 年、播放量和点赞量为 0。如果不处理聚合结果会失真——比如TruncWeek会把所有无时间数据聚到 1970 年的第一周形成一个莫名其妙的尖峰。清洗逻辑放在 Django 的management/commands里做独立脚本不放进主业务流程。原因很简单采集、清洗、分析三个环节耗时差别大主流程的请求应在秒级返回而清洗任务可能分钟级做成定时任务或手动触发更合理。# management/commands/clean_data.py from django.core.management.base import BaseCommand from video.models import Video class Command(BaseCommand): def handle(self, *args, **options): # 清理无效发布时间 fixed_time Video.objects.filter(pub_time__year1970) print(finvalid pub_time: {fixed_time.count()}) # 空分区数据归入未知类型 empty_zone Video.objects.filter(zone__isnullTrue).update(zoneunknown) # 播放量为0且点赞不为0的异常数据 abnormal Video.objects.filter(play0, like__gt0) print(fabnormal data: {abnormal.count()})这里的filter(pub_time__year1970)是一个精准的脏数据识别方法。B 站接口对个别视频返回的时间戳可能是 0 或负数转成 datetime 后恰好落在 1970 年用年份过滤查一次就能确认数量级。空分区统一归入unknown后续分析时会单独作一类展示而不是被聚合函数直接丢弃。发现带play0但点赞大于 0 的记录说明接口某个字段提取有遗漏需要回源头检查抓取逻辑。5.2 基于 pandas 的字段补全与异常值标记当数据量大了之后清洗逻辑逐步复杂继续用 Django ORM 的链式过滤就有些吃力。此时可以改用 pandas 做数据清洗Django 查出来的数据转成DataFrame后做向量化处理性能更好。import pandas as pd data list(Video.objects.all().values(id, play, like, coin, favorite, share)) df pd.DataFrame(data) # 互动率字段 df[interact_rate] (df[like] df[coin] df[favorite]) / df[play].replace(0, 1) # 异常值标记播放量为0的记录 df[is_abnormal] df[play] 0 # 按播放量分位数打标 df[level] pd.qcut(df[play], q4, labels[低, 中, 高, 极高])df[play].replace(0, 1)这一步很实用避免了除零同时不改变其他正常数据的计算结果。用pd.qcut把播放量按分位数分成四档可以快速给 UP 主做分层分析——把「极高播放量」的 UP 主单独拎出来看他们的共性。pandas 的方式便于数据分析过程中探索生产环境还是建议把清洗逻辑固化在批处理脚本中不要频繁前段计算。5.3 清洗前后的数据对比验证清洗没有标准答案但必须要有前后对比。最直接的方式是跑一段 SQL 统计同一指标清洗前后的差异SELECT COUNT(*) AS total, SUM(play) AS total_play, AVG(play) AS avg_play, COUNT(DISTINCT zone) AS zone_cnt FROM video_video WHERE pub_time 2020-01-01 AND pub_time 2025-01-01;SELECT COUNT(*) AS total, SUM(play) AS total_play, AVG(play) AS avg_play, COUNT(DISTINCT zone) AS zone_cnt FROM video_video;第一段加了时间范围过滤排除了时间戳异常导致聚到 1970 年的数据第二段不过滤。两条 SQL 的total和avg_play差距如果超过 5%基本可以确定是脏数据污染严重需要回到清洗脚本里增加过滤规则。这个验证过程应该写进文档防止后续重新采集时再次引入同类问题。6. 排名窗口算法榜单动态与滑动时间窗口6.1 按时间窗口实现「上升最快 UP 主」榜单静态榜单是「谁最多」动态榜单是「谁涨得最快」。前者用SUM和ORDER BY就能做后者需要对比不同时间窗口的数据。传统做法是把当前周期数据与上一周期数据放在同一张表里计算增长率后排序。这类需求在数据分析系统里很常见想看“近 7 天粉丝增量 TOP10”不能直接对全量粉丝数排序而要按 UP 主分组后取近期与之前的差值。由于粉丝数是状态字段不是增量字段需要周期性采集快照用快照差值计算增量。from django.db.models import Max, Min def rising_snapshot(request): days int(request.GET.get(days, 7)) # 快照表里按 up 主和时间取最新与最早 latest Snapshot.objects.values(up_id).annotate(fans_nowMax(fans)) earliest Snapshot.objects.values(up_id).annotate(fans_prevMin(fans)) result [] latest_map {item[up_id]: item[fans_now] for item in latest} for item in earliest: up_id item[up_id] if up_id not in latest_map: continue diff latest_map[up_id] - item[fans_prev] result.append({up_id: up_id, diff: diff}) result.sort(keylambda x: x[diff], reverseTrue) return JsonResponse({code: 0, data: result[:10]})这里用Max和Min取同一时间窗口内最早和最晚的快照值需要快照表按周期写入数据。严格情况下应取每个 UP 主时间最早与最晚的记录而不是最大最小值——如果粉丝数波动较大Max会污染结果。更严谨的写法是先按up_id和时间排序用窗口函数取第一条和最后一条差值但 Django ORM 对窗口函数支持有限可以改用 pandas 读取全部快照后做groupby().first()和groupby().last()。实际场景中若快照每天采集一次这种滑动窗口算法能支持周榜、月榜等多种排行且复用同一个函数主体。榜单需求从「粉丝总量」扩展成「互动增量」「评论增速」时只需替换快照字段名逻辑保持不变。这个思路也是这套系统后期加需求时最值得保留的部分——指标定义一变只要保证快照在持续采集历史榜单都能回溯复算。本文还有配套的精品资源点击获取