每年到毕设选题的时候,总有人来问我"做什么题目比较好过、又有东西写、答辩还能讲得清楚"。如果你手头正在纠结,我建议你认真看看Django+Vue.js小说推荐系统这个方向——它不是那种烂大街的图书管理系统,而是把爬虫、推荐算法、可视化、前后端分离、大数据分析全串在了一条链路上。一个题目就能把后端框架、前端框架、数据库设计、算法实现、数据采集这些点全部覆盖,工作量肉眼可见,答辩的时候随便挑一个模块都能讲十分钟不冷场。
这篇文章我打算从一个真正做完了这个项目的角度,把小说爬虫怎么设计、推荐算法怎么落地、可视化看板怎么搭、前后端怎么联调、答辩前有哪些坑这些环节完整拆开讲一遍。不管你是打算源码二开还是从头写,这篇文章都能帮你在开工之前把整体地图画清楚。
1. 为什么小说推荐系统是毕设的"高分题":选题逻辑与价值拆解
1.1 选题三要素:工作量看得见、技术栈够全、答辩有讲头
先聊选题逻辑。毕设最怕的不是难,而是"做完之后没东西可讲"。一个单纯的信息管理系统,比如"学生选课系统""仓库管理系统",界面点一圈就结束了,评委问两句业务逻辑就没什么可深入的了。但小说推荐系统的好处在于:它天然自带一个完整的数据闭环。
这个闭环长这样:数据采集(爬虫抓小说元数据、章节、评论)→ 数据清洗与入库(MySQL)→ 离线计算(推荐算法生成候选集)→ 在线服务(Django提供推荐接口)→ 前端展示(Vue+ECharts图表、小说列表、推荐理由)。任何一个环节拿出来都能展开讲原理,而且环环相扣,答辩时从爬虫讲到推荐再讲到可视化,整个故事线非常完整。
我见过不少同学做推荐系统类题目翻车,原因高度一致:只做了一个"能看的搜索列表+假推荐"。他所谓的推荐逻辑是"按热度排序",这在答辩时一眼就被问穿。真正能拿到高分的做法,是至少实现一种可解释的推荐算法(协同过滤或者基于内容的匹配),然后在可视化看板上把推荐效果展示出来。这样评委问"推荐结果怎么来的",你才能讲出算法逻辑。
1.2 技术栈选型的理由:Django+Vue.js为什么是毕设黄金组合
Django和Vue.js的搭配,我做了好几个项目之后仍然觉得是毕设场景下最稳的选择。
Django这边,它有自带Admin后台、ORM、模板系统,对新手友好;同时它的ORM在做推荐系统时特别有用,因为推荐算法本质上离不开对用户行为表、物品表、评分表这三类数据的频繁关联查询。你写纯SQL当然也可以,但用Django的ORM来做"查找用户读过的所有小说→找到相似小说→排除已读→生成推荐列表"这种操作,代码量少一半。
Vue.js则解决了前端交互的部分。小说推荐系统不是简单展示静态页面,它需要:用户登录后有个性化推荐位、点击小说后要看详情、评分之后推荐列表刷新、还要有看板页面展示统计图表。Vue的组件化开发让这些UI模块可以拆开写,互不干扰。尤其是可视化看板这种页面,用Vue组件配合ECharts,一个组件就是一个图,数据变了图自动更新,体验比JQuery时代舒服太多。
前后端之间走Restful API。Django用DRF(Django Rest Framework)提供JSON接口,Vue用axios调用。这个模式你工作后也会一直用,所以不算"为了毕设而学",是真正的行业通用技能。
2. 系统架构与数据流:从爬虫到前端图表的完整链路
2.1 总体架构:三个端、一条数据管道
我习惯把这个系统拆成三块:采集端、服务端、展示端。采集端是Python爬虫,跑在本地或者服务器上,定时抓取小说数据;服务端是Django,负责业务接口、用户体系、推荐计算、数据统计;展示端是Vue单页应用,负责页面交互和可视化。
三个端之间的数据流向你要在脑海中画一条清晰的线:
爬虫抓取 → 清洗入库(MySQL) → Django定时任务触发推荐计算 → 推荐结果写入Redis/数据表 → 前端调用接口 → Vue渲染 → 用户点击/评分 → 行为数据回写MySQL → 更新推荐这条链路的好处是,它把"大数据"的概念落在了实际操作上。虽然小说数据量可能达不到几十亿那样夸张的规模,但几万本小说、几十万条用户行为记录,已经足够让你用上"离线计算+在线读取"这套大数据系统里最常见的架构思路。毕设答辩时讲"为什么推荐结果要预计算而不是实时算",本身就是一个很好的技术选型问题。
2.2 数据库设计:五张表的关联逻辑
小说推荐系统的核心表,我建议至少设计这么五张:
| 表名 | 关键字段 | 作用 |
|---|---|---|
| 小说表(book) | book_id、书名、作者、分类、简介、字数、状态、封面、最新章节 | 存储爬虫采集的图书元数据 |
| 用户表(user) | user_id、用户名、密码哈希、注册时间 | 系统用户基础信息 |
| 用户行为表(user_behavior) | behavior_id、user_id、book_id、行为类型(阅读/收藏/评分/点击)、评分值、时间戳 | 推荐算法的核心数据来源 |
| 推荐结果表(recommendation) | id、user_id、book_id、推荐分值、生成时间 | 离线计算后写入,在线接口直接读取 |
| 评论/书评表(comment) | id、user_id、book_id、评论内容、时间 | 可挂在小说详情页,增加内容维度 |
要特别提醒的是"用户行为表",它直接决定了推荐算法的上限。我见过有人只做了收藏功能,没有做评分也没有记录点击行为,结果协同过滤算法跑起来几乎没有数据可用。正确的做法是:用户打开小说详情页记录一条"点击"行为,点收藏记录"收藏"行为,打星评记录"评分"行为,这样每个用户至少有几条有效行为数据,推荐算法才有东西算。
3. 小说爬虫模块的工程化设计:采集、清洗、入库
3.1 爬虫目标与合规边界
小说爬虫是整个系统的"原材料来源"。没有数据,推荐算法就是空中楼阁,可视化也没东西可展示。所以爬虫看似是辅助模块,实际上是整个大数据链路的起点。
先说合规问题。很多同学一听到爬虫就心虚,其实只要把握住几点就没问题:只采集公开可访问的数据、遵守目标网站的robots协议、采集频率控制不要造成对方服务器压力、不采集任何需要付费或登录后才能看到的内容。毕设场景下,数据用途是学习研究,采集对象尽量选公开的小说信息页(书名、作者、简介、分类、字数),不要去动VIP章节内容。这样做既安全,也够用——推荐算法只需要元数据和行为数据,并不需要全文。
3.2 抓取与解析的关键实现
爬虫技术选型上,我推荐用requests配合BeautifulSoup。为什么不用Scrapy?Scrapy当然更专业,但对毕设来说学习成本偏高,而且你自己写框架能更好展示爬虫原理——答辩时评委问"你的爬虫怎么写的",你能从URL构造、请求头、响应解析、异常重试讲得清清楚楚,比说"我用了框架"更有说服力。
核心逻辑可以分成四步:
- 列表页URL构造:分析目标网站的小说分类列表URL规则,按分类和页数循环生成待爬取URL。
- 请求与容错:每个请求带伪装User-Agent,加随机延时(0.5到1.5秒),设置超时和重试机制。
- 内容解析:用BeautifulSoup按HTML结构提取书名、作者、简介、分类、封面图地址、最后更新时间等字段。
- 入库更新:数据交给Django的ORM写入MySQL。
给个简化版解析代码,让你感受下结构:
import requests from bs4 import BeautifulSoup def parse_book_list(html): soup = BeautifulSoup(html, 'html.parser') books = [] for item in soup.select('.book-item'): book = { 'title': item.select_one('.title a').text.strip(), 'author': item.select_one('.author').text.strip(), 'category': item.select_one('.category').text.strip(), 'intro': item.select_one('.intro').text.strip()[:200], 'book_url': item.select_one('.title a')['href'], 'cover_url': item.select_one('img')['src'] } books.append(book) return books def fetch(url): headers = {'User-Agent': 'Mozilla/5.0 ...'} resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() return parse_book_list(resp.text)3.3 数据清洗与归一化处理
爬下来的数据一定脏。这个坑我踩过,所以后面每个项目都老老实实加清洗步骤。
常见的脏数据包括:书名里有全角空格、简介超过配置的字段长度、分类字段不统一(有的叫"玄幻"有的叫"玄幻小说")、封面图URL失效、字数字段带"万字"这种中文单位。这些不处理干净,后面做分类统计图的时候会出现一团乱麻。
清洗方案我建议在入库前用一个统一的clean_book_data()函数处理:
- 字符串字段统一
strip()并去除不可见字符 - 分类字段映射到标准分类字典(比如把"玄幻小说"映射为"玄幻")
- 字数、点击量等数字字段从"123.4万"这种格式解析为整数
- 简介截断到不超过指定长度,过长会导致Django字段超长报错
我的经验是:清洗逻辑所占的工作量,往往不亚于爬虫本身。但这一步做好了,后面所有统计图表、推荐算法、搜索功能都会非常省心。
3.4 增量更新与调度策略
小说数据不是爬一次就结束的,因为小说网站每天都有更新:新书上架、连载小说更新章节。如果是毕设演示,数据爬一次基本够用;但如果你的系统要持续运行,需要一个增量更新的策略。
我当时的做法是:建一个crawl_task表记录每次爬虫运行的时间和采集数量;爬虫启动时按上次记录的页数和当前页数差决定增量范围。由于是毕设,不需要上Celery那种重量级的东西,直接写一个Django management command,用python manage.py crawl_books手动触发,或者挂在系统cron上每天跑一次。这样做,代码简洁、逻辑清晰,而且答辩时讲起来也好理解——"我校准了增量爬取,每次只采集新增和更新的数据,避免重复抓取"。
4. 推荐引擎核心:从协同过滤到混合推荐的落地实现
4.1 三种推荐算法的适用场景对比
推荐算法是系统的灵魂。对这个项目来说,市面上最常见的三种算法你可以全部实现,然后对比效果,这本身就是很好的毕设工作量和答辩亮点。
| 算法 | 原理 | 适用场景 | 实现难度 |
|---|---|---|---|
| 基于内容的推荐 | 根据用户读过的书的内容属性(分类、标签、作者)找相似书籍 | 冷启动时管用,新用户也有推荐 | 低 |
| 基于物品的协同过滤 | 找到喜欢同一本书的用户群,推荐他们喜欢的其他书 | 用户行为数据充足时效果好 | 中 |
| 基于用户的协同过滤 | 找到相似用户,推荐相似用户喜欢的书 | 用户量大且行为丰富时效果好 | 中 |
我建议主推基于物品的协同过滤(ItemCF)。原因很直接:小说站里热门书籍数量有限,大部分用户不会读过太多的书,ItemCF计算"物品之间的相似度"比UserCF计算"用户之间的相似度"更稳定,而且结果也更好解释——"因为你读过《A》和《B》,所以给你推荐《C》"。
4.2 基于物品的协同过滤实现细节
ItemCF的核心分两步:第一步算物品间的相似度矩阵,第二步根据用户历史行为生成推荐。相似度我这里用余弦相似度,公式不展开了,用Python代码落地大约是这样:
from math import sqrt from collections import defaultdict def build_item_similarity(user_behavior): # user_behavior: {user_id: {book_id: rating}} # 1. 统计喜欢每个物品的用户列表 item_users = defaultdict(list) for user, books in user_behavior.items(): for book in books: item_users[book].append(user) # 2. 计算物品间共同被喜欢的次数 co_occur = defaultdict(int) for book, users in item_users.items(): for i in range(len(users)): for j in range(i + 1, len(users)): # 这里简化为任意两个被同一用户喜欢的物品的共现计数 u1, u2 = users[i], users[j] for b1 in user_behavior[u1]: for b2 in user_behavior[u2]: if b1 != b2: co_occur[(b1, b2)] += 1 # 3. 归一化得到相似度矩阵 similarity = {} for (b1, b2), cnt in co_occur.items(): similarity[(b1, b2)] = cnt / sqrt(len(item_users[b1]) * len(item_users[b2])) return similarity这是最基础的思路。实际项目里,我会用user_behavior表里的"阅读+收藏"行为构造正反馈,评分数作为权重。为了效率,相似度计算只在离线任务里跑,算完之后把每个物品最相似的Top-N物品写入book_similarity表。在线推荐时就简单了:找到用户最近读过的几本书,去相似度表里查找对应的相似书,加权累加排序,取前20本就是推荐结果。
这段逻辑建议你讲答辩时画图配合,从"离线计算"到"在线召回"到"排序"三个阶段,评委听到这里基本就能确认这不是一个花架子系统。
4.3 冷启动问题的处理
冷启动是推荐系统最经典的问题,也是评委最爱问的问题。新用户没有行为数据,协同过滤必然失效,怎么办?我当时用了两个方案,组合起来效果很好:
- 基于内容的兜底推荐:如果用户行为表为空,直接按分类偏好(或者不区分偏好)推荐热门高分小说。用户可以手动选择"我喜欢的分类",系统按分类推荐该分类下评分最高的书。
- 最近热门推荐:统计近7天阅读/收藏量最高的书,作为新用户的默认推荐列表。
这两个方案本质上就是规则推荐,不用复杂模型,但它在产品逻辑上把推荐系统做完整了。答辩时被问"冷启动怎么解决",你直接回答"协同过滤覆盖不了的时候,用内容召回和热门召回兜底",这就很有说服力。
4.4 离线计算与在线接口的配合
说到"离线计算"和"在线接口",很多同学觉得是高大上的分布式系统才有的概念,其实在单机项目里一样可以实现这种分工。
我当时的做法是:写一个recommend/management/commands/offline_compute.py命令,里面调相似度计算和推荐结果生成函数,跑完把每个用户的预测推荐列表写入recommendation表。这个命令手动执行,或者放定时任务。在线接口呢,就是一个Django DRF的GET /api/recommendations/,逻辑非常简单——到recommendation表查当前用户的推荐列表返回即可。
这样做的好处是:在线接口响应只有几十毫秒,前端体验流畅;而离线计算任务跑十几秒也不会卡住页面。如果答辩老师问"为什么不用实时计算",你可以说——实时计算每请求一次都要做物品相似度计算,在大数据量下延迟不可控,业界通用做法就是离线计算好结果、在线读取。这个思想用到真实企业项目里也是一样的。
5. 可视化交互层:Vue+ECharts的图表设计与复用
5.1 看板设计:哪些图表是评委爱看的
可视化模块是很多人忽略的加分项。小说推荐系统的可视化不只是把数据画出来,更关键的是用图表讲清楚系统里的数据情况。我做的看板包含这么几块:
- 图书分类分布饼图:展示各个分类的小说数量占比,一眼看出书库里玄幻、都市、科幻各占多少。
- 热门小说Top10柱状图:按阅读量/收藏量排序,展示系统内最受欢迎的小说。
- 用户活跃度趋势折线图:按天展示每日新增用户数和行为数,体现系统运营情况。
- 推荐效果统计图:展示推荐位曝光数、点击次数、推荐位点击率,这个图是我最推荐做的一个,因为它直接证明推荐算法有效果。
其中"推荐效果统计图"特别提一下,我从第一次做完项目后的复盘经验是:推荐系统的可视化不能只展示"输入数据",更要展示"输出效果"。你展示了推荐点击率从无推荐时的X%提升到了有推荐时的Y%,这就是推荐算法价值的直接证据,答辩时拿着这个数据讲,比任何理论都硬。
5.2 前端组件化:图表组件怎么拆
Vue这边我建议把ECharts封装成可复用的图表组件。不要每个页面都写一堆配置项,那代码会非常臃肿。我的做法是写一个BaseChart.vue组件,接收option作为prop,内部完成ECharts初始化、自动resize、数据更新监听:
<template> <div ref="chartRef" style="width: 100%; height: 400px"></div> </template> <script> import * as echarts from 'echarts'; export default { name: 'BaseChart', props: { option: { type: Object, required: true } }, data() { return { chart: null }; }, mounted() { this.chart = echarts.init(this.$refs.chartRef); this.chart.setOption(this.option); window.addEventListener('resize', this.resize); }, watch: { option: { deep: true, handler(newVal) { this.chart.setOption(newVal); } } }, methods: { resize() { this.chart && this.chart.resize(); } }, beforeUnmount() { window.removeEventListener('resize', this.resize); this.chart && this.chart.dispose(); } }; </script>这样在页面里用的时候,只需要传入不同的option就行。分类饼图、热门柱状图、趋势折线图,各自写一个数据加载函数,调用同一个组件渲染。代码量一下子少了一半。
5.3 前后端联调的接口规范
前后端分离的项目,接口设计规范直接影响联调效率。我开始时吃过亏:前端要的数据结构和后端返回的差别很大,就靠前端再转换,越转越乱。后来定了规矩:
- 所有列表接口返回
{ code, data, message }的固定结构; data里统一用{ list, total }分页结构;- 后端字段名一律用驼峰转下划线后再转换一次,前端只用驼峰字段(Django的serializer可以配置字段名);
- 时间字段统一返回时间戳,前端自己格式化。
这些规矩看起来琐碎,但没有它们,Vue页面和后端会互相扯皮改来改去。定好规矩之后,前后端基本可以并行开发——前端拿Mock数据先写页面,后端好了再切换真实接口。
6. 部署联调与答辩准备:最容易翻车的几个环节
6.1 本地联调与跨域问题
Django跑在8000端口,Vue跑在5173或8080端口,这样的前后端分离项目必遇跨域问题。解决办法有两个方向:
- 后端用
django-cors-headers放行前端的域名,这是开发阶段最简单的方式; - 生产环境把Django和Vue的构建产物放到同一个服务下,用Nginx托管,就不存在跨域了。
我个人的建议是:开发阶段用cors-headers,最终部署用Nginx统一托管。这样开发效率高,演示的时候也不会因为浏览器拦截跨域请求而出洋相。
跨域问题还有一个坑要提醒:不要在演示时打开两个页面同时访问前后端,万一前端开发服务器的端口忘了开,页面打不开。最终演示最好用打包后的静态文件。
6.2 打包部署的注意事项
Vue项目npm run build之后会生成dist目录,里面是纯静态文件。Django这边你需要把dist目录配置为STATICFILES_DIRS之一,同时让Django直接渲染一个index.html入口。这样评委访问你部署好的地址,一个端口搞定所有功能,不用解释两个服务的问题。
数据库方面,如果换一台电脑演示,一定要记得导出MySQL数据。标题里写了"源码+文档+讲解",但很多人容易漏掉数据库初始化脚本。我的习惯是提供两个文件:install.sql(建库建表)和seed_data.sql(爬虫抓取的核心数据)。否则别人拿到源码,启动后端啥都没有,推荐算法跑不动,看板全是空图表,直接整个系统就崩了。
6.3 答辩演示的预演清单
毕设答辩翻车十次有八次在上台前没检查环境。列个简单的预演清单:
- 提前一天在答辩用的电脑上完整跑一遍系统,确认Python、Node、MySQL、Redis的依赖都齐了。
- 准备离线模式:如果现场没网,爬虫展示就用现有的seed_data,可视化展示直接读取数据库。有一个万能方案——录一段3分钟的演示视频放在U盘里,万一环境崩塌,放视频也比干站着强。
- 准备讲故事的顺序:不要从首页代码开始讲,而是从"数据→算法→展示→效果"这条链路走,讲清楚每个环节做了什么以及为什么这么做。
6.4 文档与演示材料组织
最后说下配套文档。毕设评审最看重的是"你的工作量是否饱满、逻辑是否清楚"。所以文档里我建议按这个顺序组织:
- 需求分析:系统分为哪几个模块,每个模块有什么功能点;
- 系统设计:架构图、数据库ER图、接口清单;
- 核心模块实现:爬虫设计、推荐算法详解、可视化图表设计;
- 系统演示截图与效果分析:放上实际运行时看板截图和推荐点击率数据。
PPT就不要和文档一样了,PPT是讲故事用的,一页一个主题。我的经验是:封面标题之后先放一张全链路架构图,让评委30秒内知道你做了什么;然后按数据流转顺序讲三四个核心模块的实现;最后放演示截图和效果数据收尾。你按这个顺序讲过之后,基本不会出现"讲得不清不楚"的评价。
做这个项目最容易掉进去的坑,其实就是贪多。有人爬虫要爬12个网站,推荐要上深度神经网络,看板要搞3D大屏——最后全卡住。我在实际带项目时一直跟人说:毕业设计的本质是"完整"不是"炫技"。把爬虫做到干净、把推荐做到可解释、把可视化做到能说明问题,这一条链路跑通,就已经超过大多数人了。如果后续还有时间,再考虑加语义标签、弹幕热词、实时协同过滤这些进阶玩法不迟。