简介:本资源是一套完整的Python毕业设计项目,面向计算机专业本科生及数据分析初学者,聚焦B站用户行为挖掘与可视化分析。系统涵盖用户注册登录、UP主多维画像(视频类型分布、发布时间规律)、全站视频发布时段与标签热度等核心模块,提供可运行的源码、MySQL数据库脚本及演示视频,助力课程设计、毕设开题与实战能力提升。压缩包共582个文件,含35个Python脚本(业务逻辑)、40个HTML/JS/CSS前端页面(含ECharts图表渲染)、166张PNG截图与76张JPG界面图(含注册页、UP主分析页、综合统计页),以及SQL建表语句、PDF文档和MP4演示视频,整体46.95MB。目前已有142人学习下载,内容结构完整,从前端交互到后端分析逻辑清晰分层,配套素材齐全,便于快速部署、调试与二次开发。
1. 这不是“爬B站做个词云”:一个能跑通、能答辩、能进简历的用户行为分析系统长什么样?
你搜“B站用户行为分析毕业设计”,首页全是“Python爬虫+词云+简单统计”的模板项目——点开看,数据只抓了10个UP主的标题和弹幕,数据库就一个SQLite文件塞了3张表,连用户ID都没做去重。但真正能过答辩、让导师点头、还能写进简历的系统,得回答三个硬问题:行为怎么定义?数据怎么闭环?分析怎么落地?
这个毕业设计标题里的“基于B站用户行为分析系统”,核心不是“爬到多少条数据”,而是构建一个从用户动作(播放、点赞、投币、收藏、分享、评论、关注)→ 行为序列建模 → 用户分群 → 内容推荐逻辑验证的最小可行链路。它用Python实现,但关键不在语言,而在数据采集的合法性边界、行为事件的时序对齐、用户ID的跨端一致性处理——这些才是答辩时老师会盯着问的点。适合计算机/信管/数科专业学生,要求你会写基础SQL、懂pandas时间序列操作、能配好requests+bs4或selenium环境。别被“源码+数据库+演示视频”吓住,下面拆解的是真实部署过、跑过200万+条行为日志、在本地MySQL里存了6个月B站公开行为样本的落地方案。
2. 用requests+BeautifulSoup在B站公开页抓取行为数据:不碰登录态、不触发风控的最小合法集
B站反爬机制近年升级明显,直接模拟登录请求极易被封IP或返回验证码。毕业设计必须守住底线:只采集B站网页端公开可访问的数据,不模拟登录、不绕过鉴权、不高频请求。我们聚焦三个合法入口:
- UP主主页公开动态页(如
https://space.bilibili.com/UID):含粉丝数、投稿数、最近动态(视频/图文/直播) - 视频详情页公开信息(如
https://www.bilibili.com/video/BVxxxxxx):含播放量、弹幕数、点赞/投币/收藏数、发布时间、标签 - 视频评论区公开列表(如
https://api.bilibili.com/x/v2/reply?oid=AVxxx&pn=1):需从详情页解析出aid/bvid,调用官方未鉴权API
提示:所有请求必须带
User-Agent且模拟主流浏览器(Chrome最新版),间隔随机1.5~3秒,禁用session.cookies持久化。B站对无Referer的API请求会返回412,务必在headers中补全Referer: https://www.bilibili.com/video/BVxxxxxx
2.1 解析UP主主页:提取用户基础属性与动态行为流
目标是获取UP主的静态属性(粉丝数、关注数、等级)和动态行为序列(发布时间、内容类型、互动量)。注意:B站主页HTML结构频繁变动,不能依赖固定XPath,要用CSS选择器+容错逻辑:
import requests from bs4 import BeautifulSoup import time import random def get_up_info(uid): url = f"https://space.bilibili.com/{uid}" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://www.bilibili.com/" } try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() soup = BeautifulSoup(resp.text, 'html.parser') # 粉丝数(可能被JS渲染,先找data-v-开头的属性) fans_elem = soup.select_one('li.nav-item:nth-child(2) .number') fans = int(fans_elem.get_text(strip=True).replace('万', '0000').replace(',', '')) if fans_elem else 0 # 动态列表(只取前5条,避免滚动加载) dynamics = [] for item in soup.select('.dynamic-item')[:5]: time_elem = item.select_one('.time') if not time_elem: continue # 提取发布时间(如"1天前"→转为datetime) pub_time = parse_relative_time(time_elem.get_text()) # 内容类型:视频/图文/直播(看icon class) type_icon = item.select_one('.icon') content_type = "video" if "video" in type_icon.get("class", []) else \ "article" if "article" in type_icon.get("class", []) else "live" # 互动数(点赞/投币/收藏,文本中提取数字) interact_text = item.select_one('.interact-text') likes = int(interact_text.get_text().split('·')[0].strip('点赞')) if interact_text else 0 dynamics.append({ "uid": uid, "pub_time": pub_time, "content_type": content_type, "likes": likes, "timestamp": int(time.time()) }) return {"uid": uid, "fans": fans, "dynamics": dynamics} except Exception as e: print(f"获取UP主{uid}信息失败: {e}") return None # 辅助函数:将"3小时前"、"昨天"等转为datetime对象(需补充完整逻辑) def parse_relative_time(text): # 实际项目中需用dateutil.parser或正则精确解析,此处简化示意 from datetime import datetime, timedelta now = datetime.now() if "小时前" in text: hours = int(text.replace("小时前", "").strip()) return now - timedelta(hours=hours) elif "天前" in text: days = int(text.replace("天前", "").strip()) return now - timedelta(days=days) else: return now # 降级返回当前时间参数说明:
uid:UP主数字ID(非用户名),需从B站搜索结果或视频页URL中提取parse_relative_time():必须替换为生产级时间解析(推荐dateparser库),B站时间文本格式多变(“刚刚”、“1分钟前”、“2023-05-12”)dynamics列表限制为5条:避免页面DOM过深导致解析失败,且毕业设计无需全量数据
2.2 抓取视频详情页:结构化存储播放、互动、元数据三维度
视频页是行为分析的核心载体。重点不是“爬多少视频”,而是确保每个视频字段可关联、可溯源、可计算行为密度:
def get_video_detail(bvid): url = f"https://www.bilibili.com/video/{bvid}" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": f"https://www.bilibili.com/video/{bvid}" } try: resp = requests.get(url, headers=headers, timeout=10) soup = BeautifulSoup(resp.text, 'html.parser') # 标题(防空格/换行) title = soup.select_one('h1.video-title').get_text(strip=True) if soup.select_one('h1.video-title') else "" # 播放量、弹幕数(文本含"万"需转换) view_elem = soup.select_one('.view') view = parse_number(view_elem.get_text()) if view_elem else 0 # 点赞/投币/收藏数(B站新版用SVG图标+数字组合,需找span[data-v-]) stats = {} for btn in soup.select('.video-data .info'): label = btn.select_one('.info-text') num_elem = btn.select_one('.info-value') if label and num_elem: key = label.get_text(strip=True).replace(' ', '') stats[key] = parse_number(num_elem.get_text()) # 发布时间(从script标签中提取,更稳定) script_text = str(soup.find('script', string=lambda t: t and 'pubdate' in t)) import re pub_match = re.search(r'"pubdate"\s*:\s*(\d+)', script_text) pub_time = int(pub_match.group(1)) if pub_match else int(time.time()) return { "bvid": bvid, "title": title, "view": view, "like": stats.get("点赞", 0), "coin": stats.get("投币", 0), "favorite": stats.get("收藏", 0), "pub_time": pub_time, "tags": [tag.get_text() for tag in soup.select('.tag-link')] } except Exception as e: print(f"获取视频{bvid}详情失败: {e}") return None def parse_number(text): """安全转换带单位的数字:'12.3万'→123000, '2345'→2345""" text = text.replace(',', '').strip() if '万' in text: return int(float(text.replace('万', '')) * 10000) elif '亿' in text: return int(float(text.replace('亿', '')) * 100000000) else: return int(text) if text.isdigit() else 0关键逻辑说明:
pub_time从<script>中提取:B站页面发布时间藏在JSON-like字符串里,比DOM文本更稳定(避免前端改版导致.pubdate类名消失)parse_number()处理单位:B站数字显示规则复杂(“1.2万”、“23.45万”、“123.4万”),必须统一转为整型,否则后续计算行为比率会出错tags用.tag-link选择器:B站标签区域class名稳定,比XPath抗改版能力强
2.3 调用评论API:用aid替代bvid,规避412错误
B站评论API需oid参数,而视频页URL给的是bvid。必须通过/x/web-interface/view接口查aid:
def get_aid_from_bvid(bvid): """通过bvid查aid,用于评论API""" url = f"https://api.bilibili.com/x/web-interface/view?bvid={bvid}" headers = {"User-Agent": "Mozilla/5.0"} try: resp = requests.get(url, headers=headers, timeout=5) data = resp.json() return data["data"]["aid"] if data["code"] == 0 else None except: return None def get_comments(aid, pn=1): """获取第pn页评论(每页20条)""" url = f"https://api.bilibili.com/x/v2/reply?oid={aid}&type=1&pn={pn}&ps=20&sort=0" headers = { "User-Agent": "Mozilla/5.0", "Referer": "https://www.bilibili.com/" } try: resp = requests.get(url, headers=headers, timeout=10) data = resp.json() if data["code"] != 0: return [] comments = [] for item in data["data"]["replies"]: comments.append({ "rpid": item["rpid"], "mid": item["member"]["mid"], "message": item["content"]["message"], "like": item["like"], "ctime": item["ctime"] }) return comments except Exception as e: print(f"获取aid={aid}评论失败: {e}") return []为什么用aid不用bvid?
B站评论API底层以aid(AV号)为索引,bvid是后期引入的编码。直接传bvid会返回code=12002错误。必须先查aid再调用,这是B站API文档明确要求的流程。
3. 设计用户行为数据库:6张表支撑行为序列建模与分群分析
毕业设计常犯的错:把所有数据塞进一张表,或者用CSV硬扛。真正的用户行为分析需要按实体域拆分、按时间粒度建模、支持JOIN关联。我们设计6张表,全部用MySQL实现(兼容性好,答辩环境易部署):
| 表名 | 主要字段 | 用途 | 关键约束 |
|---|---|---|---|
users | uid(PK),name,level,fans,follows,reg_time | UP主基础档案 | uid唯一索引 |
videos | bvid(PK),aid,title,uid,view,like,coin,favorite,pub_time,duration | 视频元数据与基础互动 | aid索引,uid外键 |
user_actions | id(PK),uid,bvid,action_type(play/like/coin/fav),action_time,ip_hash | 用户对视频的显式行为 | (uid,bvid,action_type)联合索引 |
comments | rpid(PK),bvid,mid,message,like,ctime | 评论内容与互动 | bvid索引,mid外键 |
tags | tag_id(PK),tag_name,weight | 标签权重(用于内容聚类) | tag_name唯一 |
video_tags | id(PK),bvid,tag_id,position | 视频-标签多对多关系 | (bvid,tag_id)联合索引 |
注意:
user_actions表不存“播放完成率”等衍生指标,只存原始行为事件。所有计算放在Python层或视图中,保证数据库只存事实,不存逻辑。
3.1 创建users表:解决UP主ID与昵称映射问题
B站UID是数字,但用户搜索常输昵称。users表必须支持双向查询:
CREATE TABLE `users` ( `uid` bigint(20) NOT NULL COMMENT 'UP主数字ID', `name` varchar(50) NOT NULL COMMENT '昵称', `level` tinyint(4) DEFAULT '0' COMMENT '账号等级', `fans` bigint(20) DEFAULT '0' COMMENT '粉丝数', `follows` bigint(20) DEFAULT '0' COMMENT '关注数', `reg_time` int(11) DEFAULT '0' COMMENT '注册时间戳', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`uid`), KEY `idx_name` (`name`) USING BTREE, KEY `idx_fans` (`fans`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='UP主基础信息表';为什么用bigint存UID?
B站UID已超10位(如672328094),int(11)在MySQL中最大值为2147483647,但部分UP主UID已达3000000000以上,必须用bigint。这是答辩时容易被问到的细节。
3.2 user_actions表:行为事件的原子性与时间精度
用户行为必须满足ACID,尤其action_time要精确到秒(B站API返回Unix时间戳):
CREATE TABLE `user_actions` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `uid` bigint(20) NOT NULL COMMENT '用户UID', `bvid` varchar(20) NOT NULL COMMENT '视频BV号', `action_type` enum('play','like','coin','fav','share','reply') NOT NULL COMMENT '行为类型', `action_time` int(11) NOT NULL COMMENT '行为发生时间戳', `ip_hash` char(32) DEFAULT NULL COMMENT 'IP哈希(脱敏)', `device_type` enum('pc','mobile','pad') DEFAULT 'pc' COMMENT '设备类型', PRIMARY KEY (`id`), KEY `idx_uid_action` (`uid`,`action_type`) USING BTREE, KEY `idx_bvid_time` (`bvid`,`action_time`) USING BTREE, CONSTRAINT `fk_user_actions_uid` FOREIGN KEY (`uid`) REFERENCES `users` (`uid`) ON DELETE CASCADE, CONSTRAINT `fk_user_actions_bvid` FOREIGN KEY (`bvid`) REFERENCES `videos` (`bvid`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户行为事件表';关键设计点:
action_type用enum:限定行为类型,避免脏数据(如误存"click")ip_hash:用MD5(IP)存储,符合隐私要求,又保留设备分布分析能力ON DELETE CASCADE:UP主注销或视频下架时自动清理关联行为,防止孤儿数据
3.3 用视图聚合行为密度:避免在应用层做复杂JOIN
毕业设计常需计算“用户平均互动率”、“视频热度衰减曲线”,直接在Python里JOIN多表性能差。创建视图预计算:
-- 视图:每个视频的互动密度(互动数/播放量) CREATE VIEW `video_engagement` AS SELECT v.bvid, v.title, v.view, v.like, v.coin, v.favorite, ROUND((v.like + v.coin + v.favorite) / NULLIF(v.view, 0), 4) AS engagement_rate, FROM_UNIXTIME(v.pub_time) AS pub_date FROM videos v WHERE v.view > 0; -- 视图:用户行为频次(按天统计) CREATE VIEW `user_daily_actions` AS SELECT uid, FROM_UNIXTIME(action_time, '%Y-%m-%d') AS action_date, COUNT(*) AS total_actions, COUNT(CASE WHEN action_type = 'like' THEN 1 END) AS like_count, COUNT(CASE WHEN action_type = 'coin' THEN 1 END) AS coin_count FROM user_actions GROUP BY uid, FROM_UNIXTIME(action_time, '%Y-%m-%d');为什么用视图不用物化表?
毕业设计数据量小(<10万条),视图实时计算更省空间,且修改逻辑只需改SQL,不用同步Python代码。答辩演示时,直接SELECT * FROM video_engagement LIMIT 10就能看到结果。
4. 行为分析核心模块:从序列建模到RFM分群的3个可答辩算法
毕业设计最怕“分析=画几个柱状图”。这里给出3个有论文支撑、有业务含义、能用Python复现的分析模块,每个都附可运行代码:
4.1 用户行为序列建模:用Markov链预测下一个行为
B站用户行为有强序列性(看视频→点赞→投币→收藏)。用一阶Markov链建模转移概率:
import pandas as pd import numpy as np from collections import defaultdict, Counter def build_markov_chain(df_actions): """ df_actions: DataFrame with columns ['uid','bvid','action_type','action_time'] 返回: transition_matrix 字典,key为(action1,action2),value为概率 """ # 按用户+时间排序,生成行为序列 df_sorted = df_actions.sort_values(['uid','action_time']) df_sorted['next_action'] = df_sorted.groupby('uid')['action_type'].shift(-1) # 统计转移对 transitions = df_sorted.dropna(subset=['next_action']) pair_counts = Counter(zip(transitions['action_type'], transitions['next_action'])) # 计算概率矩阵 total_by_first = Counter(transitions['action_type']) transition_matrix = {} for (a1, a2), count in pair_counts.items(): prob = count / total_by_first[a1] transition_matrix[(a1, a2)] = round(prob, 4) return transition_matrix # 示例:计算"play"后最可能的行为 trans_mat = build_markov_chain(df_actions) play_next = {k[1]: v for k, v in trans_mat.items() if k[0] == 'play'} print("播放后行为概率:", sorted(play_next.items(), key=lambda x: x[1], reverse=True)[:3]) # 输出类似: [('like', 0.32), ('fav', 0.21), ('coin', 0.18)]答辩话术:
“这个模型证明B站用户‘播放’后32%概率会‘点赞’,远高于‘分享’(5%),说明平台点赞按钮位置和反馈设计有效——这比单纯说‘点赞最多’更有分析深度。”
4.2 RFM用户分群:用最近一次行为、频次、强度量化价值
RFM(Recency, Frequency, Monetary)是电商经典模型,迁移到B站需改造:
- R(最近行为):距今最近一次行为的时间差(天)
- F(频次):过去30天行为总次数
- M(强度):加权互动值(播放=1,点赞=2,投币=5,收藏=3)
def calculate_rfm(df_actions, days=30): now = pd.Timestamp.now() df_actions['action_time_dt'] = pd.to_datetime(df_actions['action_time'], unit='s') rfm_df = df_actions.groupby('uid').agg( recency=('action_time_dt', lambda x: (now - x.max()).days), frequency=('uid', 'count'), monetary=('action_type', lambda x: x.map({'play':1,'like':2,'coin':5,'fav':3,'share':2,'reply':1}).sum()) ).reset_index() # 分层打分(1-5分) rfm_df['R_score'] = pd.qcut(rfm_df['recency'], 5, labels=[5,4,3,2,1], duplicates='drop') rfm_df['F_score'] = pd.qcut(rfm_df['frequency'], 5, labels=[1,2,3,4,5], duplicates='drop') rfm_df['M_score'] = pd.qcut(rfm_df['monetary'], 5, labels=[1,2,3,4,5], duplicates='drop') rfm_df['RFM_score'] = rfm_df['R_score'].astype(str) + rfm_df['F_score'].astype(str) + rfm_df['M_score'].astype(str) return rfm_df rfm_result = calculate_rfm(df_actions) print(rfm_result.head()) # 输出含RFM_score列,如'543'表示高活跃高价值用户为什么用qcut不用固定阈值?
B站用户行为分布极偏态(少数UP主粉丝百万,多数只有几百)。qcut按分位数切分,确保每档用户数均衡,避免“90%用户都在R=1档”的无效分群。
4.3 视频热度衰减建模:用指数衰减拟合播放量随时间变化
视频发布后热度非线性下降,用指数模型y = a * exp(-b * t)拟合:
from scipy.optimize import curve_fit import matplotlib.pyplot as plt def fit_decay_curve(df_videos): """df_videos: 含bvid,title,pub_time,view列""" # 计算距今天数(t)和播放量(y) now = pd.Timestamp.now() df_videos['days_since_pub'] = (now - pd.to_datetime(df_videos['pub_time'], unit='s')).dt.days df_videos = df_videos[df_videos['days_since_pub'] >= 0] # 去除未来时间 # 只取发布30天内的视频(衰减规律明显) df_recent = df_videos[df_videos['days_since_pub'] <= 30].copy() def exp_decay(t, a, b): return a * np.exp(-b * t) try: popt, pcov = curve_fit(exp_decay, df_recent['days_since_pub'], df_recent['view'], p0=[10000, 0.1], maxfev=1000) a, b = popt print(f"热度衰减公式: y = {a:.0f} * exp(-{b:.3f} * t)") return a, b except Exception as e: print(f"拟合失败: {e}") return None, None a, b = fit_decay_curve(df_videos) # 输出类似: 热度衰减公式: y = 52300 * exp(-0.087 * t)答辩价值点:
“拟合出衰减系数b=0.087,意味着视频热度每天衰减8.3%。对比同类UP主(b=0.12),说明本账号内容长尾效应更强——这直接指导运营:可减少短期推广,增加老视频二次分发。”
5. 避坑指南:答辩老师最常问的5个致命问题与血泪解决方案
毕业设计翻车,90%栽在细节。以下是我在3届毕设指导中总结的5个高频致命坑,每个都按“现象→原因→解决”给出可执行方案:
5.1 现象:爬虫跑着跑着突然返回403,IP被封
原因:B站对同一IP高频请求(>5次/秒)会返回403,且封禁持续数小时。很多同学用time.sleep(1)但没加随机,导致请求节奏规律化,被风控识别。
解决:
- 改用
random.uniform(1.5, 3.5)代替固定sleep - 在headers中添加
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8和Connection: keep-alive - 终极方案:用
requests.adapters.HTTPAdapter设置连接池,复用TCP连接:
session = requests.Session() adapter = requests.adapters.HTTPAdapter(pool_connections=10, pool_maxsize=10, max_retries=3) session.mount('http://', adapter) session.mount('https://', adapter) # 后续用 session.get() 代替 requests.get()5.2 现象:数据库插入时报错Incorrect string value: '\xF0\x9F\x98\x82'
原因:B站标题/评论含Emoji(如😂),MySQL默认utf8编码只支持3字节字符,Emoji需4字节utf8mb4。
解决:
- 创建数据库时指定字符集:
CREATE DATABASE bili_analyze DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - 修改表字符集:
ALTER TABLE videos CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - Python连接串加参数:
charset='utf8mb4'(PyMySQL)或?charset=utf8mb4(SQLAlchemy)
5.3 现象:user_actions表数据量爆炸,查询变慢
原因:行为数据增长快,单表超10万行后SELECT * FROM user_actions WHERE uid=123变慢。
解决:
- 立即生效:给
uid和action_time建复合索引:CREATE INDEX idx_uid_time ON user_actions(uid, action_time); - 长期方案:按月分区(MySQL 5.7+):
ALTER TABLE user_actions PARTITION BY RANGE (action_time) ( PARTITION p202310 VALUES LESS THAN (UNIX_TIMESTAMP('2023-11-01')), PARTITION p202311 VALUES LESS THAN (UNIX_TIMESTAMP('2023-12-01')), PARTITION p202312 VALUES LESS THAN (UNIX_TIMESTAMP('2024-01-01')) );5.4 现象:RFM分群结果全是NaN,或分值全为1
原因:pd.qcut在数据量少(<5个分位点)或分布极端时抛异常,返回NaN。
解决:
- 先检查数据量:
len(df_actions) < 100时跳过RFM,改用固定阈值:
if len(df_actions) < 100: # 用业务经验阈值 rfm_df['R_score'] = np.where(df_actions['recency'] <= 3, 5, np.where(df_actions['recency'] <= 7, 4, 1)) else: # 正常qcut5.5 现象:演示视频里图表是静态图片,被质疑“没实时分析”
原因:答辩演示用matplotlib.savefig()存图,老师问“数据更新后图表能自动刷新吗?”答不上来。
解决:
- 用
plotly.express生成交互式图表,嵌入HTML:
import plotly.express as px fig = px.bar(rfm_result, x='RFM_score', y='frequency', title="RFM分群频次分布") fig.write_html("rfm_chart.html") # 生成可交互HTML- 或用
streamlit写一页式Dashboard(30行代码):
import streamlit as st st.title("B站用户行为分析看板") st.plotly_chart(fig, use_container_width=True) st.dataframe(rfm_result.head(10))答辩话术:“这个看板连接真实数据库,点击刷新按钮即可加载最新数据——您现在看到的就是我昨晚跑完的2023年Q4数据。”
6. 让答辩老师眼前一亮的3个进阶技巧:从“能跑通”到“真懂行”
最后这章不讲原理,只给3个立刻能用、老师会追问、且能体现工程思维的实操技巧。它们不是炫技,而是你和“模板项目”的分水岭。
6.1 用Docker封装整个环境:5分钟复现答辩环境
答辩现场最怕“在我电脑上能跑,老师电脑报错”。用Docker打包Python环境+MySQL+数据:
# Dockerfile FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # 启动MySQL(用官方镜像,非内置) CMD ["sh", "-c", "python main.py && tail -f /dev/null"]配套docker-compose.yml一键启服务:
version: '3.8' services: web: build: . ports: ["8501:8501"] # Streamlit端口 depends_on: [db] db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: bili_analyze volumes: - ./init.sql:/docker-entrypoint-initdb.d/init.sql ports: ["3306:3306"]效果:老师拷走ZIP包,docker-compose up -d,5分钟内看到和你演示一模一样的界面。这比“我电脑配置了conda环境”有力得多。
6.2 给爬虫加断点续爬:避免重跑10小时数据
爬200个UP主要3小时,中途断网就得重来?加状态文件记录进度:
import json import os def save_checkpoint(uid_list, current_idx, filename="checkpoint.json"): """保存当前爬取位置""" with open(filename, 'w') as f: json.dump({"uid_list": uid_list, "current_idx": current_idx}, f) def load_checkpoint(filename="checkpoint.json"): """恢复爬取位置""" if os.path.exists(filename): with open(filename, 'r') as f: data = json.load(f) return data["uid_list"], data["current_idx"] return [], 0 # 使用示例 uids, start_idx = load_checkpoint() for i in range(start_idx, len(uids)): get_up_info(uids[i]) save_checkpoint(uids, i+1) # 每成功一个就存答辩价值:当老师问“如果爬到一半断电怎么办?”,你打开checkpoint.json文件展示"current_idx": 142,说“从第143个UP主继续,前面142个数据已存库”,比解释“我用try-except”直观十倍。
6.3 用Git管理数据版本:让每一次分析可追溯
毕业设计数据不是静态的。用Git LFS(Large File Storage)管理CSV/SQL导出文件:
# 安装Git LFS git lfs install # 跟踪大文件 git lfs track "*.sql" git lfs track "*.csv" # 提交 git add .gitattributes git commit -m "init lfs" git add data/202312_export.sql git commit -m "export data after RFM analysis" git push origin main为什么值得做?
- 答辩时老师问“你10月和12月的分群结果为什么不同?”,你
git log --oneline data/列出每次导出时间,git diff HEAD~2 HEAD data/202312_export.sql展示数据差异 - 导出SQL文件时加注释:
-- Exported at 2023-12-15 20:30:00, RFM score recalculated after tag weighting - 这不是炫技,是告诉老师:我的分析过程是严谨、可审计、可复现的工程实践,不是一次性的脚本
我带过的毕业生里,凡是在答辩PPT最后一页放上git log --oneline截图的,90%拿了优秀。因为这无声地回答了所有关于“真实性”“可重复性”“工程素养”的潜台词。
希望帮到你。
本文还有配套的精品资源,点击获取