基于Django与协同过滤的漫画推荐系统开发实践

发布时间:2026/9/17 17:29:09
基于Django与协同过滤的漫画推荐系统开发实践 简介一份面向本科毕业设计的基于Python漫画平台推荐系统完整论文适合推荐系统类课题的学生参考选题思路、架构设计与论文撰写。文档以绪论、开发工具、系统分析、系统设计、实现与优化、结论展望为主线从漫画平台推荐的项目背景与开发意义切入重点阐述Python与B/S架构下的前后端分离实现涉及Django/Flask框架、MySQL数据库、用户画像构建与协同过滤等推荐算法同时覆盖功能模块划分、数据库表设计、性能优化及高并发应对等内容。资源包共1个docx文件大小3.15MB排版规范包含中英文摘要、目录和完整正文并配有需求分析及用例说明可直接用于论文查重前的内容比对和结构梳理。目前已有184人学习下载适合需要理解漫画平台推荐系统从需求调研到落地全过程、并快速产出毕业设计文档的读者。1. 漫画推荐系统的真实工作量论文之外要补的代码做漫画平台推荐系统很多人拿到手的是一份毕业设计论文里面写了需求分析、用例图、数据库表结构甚至把“协同过滤”“用户画像”都列为研究内容。但是论文和能跑的工程之间隔着大量细节Django 的模型怎么和论文里的表对上推荐算法怎么用真实用户行为数据计算相似度上线时 Nginx 和 uWSGI 怎么配合。这套系统本质上是 B/S 架构下的内容管理加个性化分发后端用 Python 和 Django前端用 Vue数据落在 MySQL 里。适合两类人一是需要把毕业论文补成完整项目的在校生二是想在现有内容站点里加推荐模块的工程师。本文按“架构 — 数据模型 — 推荐实现 — 功能落地 — 测试 — 部署”的路径拆开讲所有代码都按可直接运行的标准给。2. Django 与 B/S 架构把论文里的表结构变成可运行的模型2.1 B/S 模式下 Django 的分层逻辑B/S 架构的核心特点是浏览器负责展示服务器负责业务逻辑和数据。Django 的实现方式是 MVTModel 定义数据结构View 处理请求并返回响应Template 渲染页面。放到漫画平台场景里用户在前端页面点“收藏”按钮Vue 把请求发给 Django 的 URLconfURLconf 路由到 ViewView 操作 Model 读写 MySQL最后把 JSON 返给前端。这个链路里最容易被忽视的是 settings.py 的配置尤其是数据库连接和时区。连接 MySQL 的配置在 settings.py 里这样改DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: comic_db, USER: comic_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }这里有几个关键点。ENGINE指定使用 MySQL 后端NAME是数据库名需要在 MySQL 里提前建好utf8mb4字符集是为了兼容漫画标题里的特殊符号和表情符号如果只写utf8部分内容会插入失败。配置完成后用python manage.py makemigrations和python manage.py migrate生成数据库表。2.2 从论文表结构到 ORM 模型论文里给出了几张关键表的字段用户表包含用户名、密码、昵称、姓名、性别、头像、年龄、邮箱、手机动漫分类表有分类名称评论表有关联 id、用户 id、头像、用户名、评论内容、回复内容。用 Django 的 ORM 定义模型时常见做法是一张表对应一个继承models.Model的类。from django.db import models class ComicCategory(models.Model): name models.CharField(max_length200, verbose_name动漫分类) class Meta: db_table dongmanfenlei verbose_name 动漫分类 class UserProfile(models.Model): username models.CharField(max_length200, uniqueTrue, verbose_name用户名) password models.CharField(max_length200, verbose_name密码) nickname models.CharField(max_length200, blankTrue, verbose_name昵称) avatar models.TextField(blankTrue, verbose_name头像) age models.CharField(max_length50, blankTrue, verbose_name年龄) email models.CharField(max_length200, blankTrue, verbose_name邮箱) phone models.CharField(max_length50, blankTrue, verbose_name手机) class Meta: db_table yonghu verbose_name 用户 class Comic(models.Model): category models.ForeignKey(ComicCategory, on_deletemodels.CASCADE, verbose_name分类) title models.CharField(max_length200, verbose_name动漫名称) cover models.ImageField(upload_tocomic_cover/, blankTrue, verbose_name封面) video_url models.URLField(blankTrue, verbose_name视频地址) description models.TextField(blankTrue, verbose_name动漫简介) publish_date models.DateField(nullTrue, blankTrue, verbose_name上架日期) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: db_table dongmanshipin verbose_name 动漫视频字段类型的选择有讲究。CharField必须指定max_length对应数据库里的varcharTextField对应longtext适合存放较长的简介和头像的 base64 或 URLDateTimeField自动记录创建时间。db_table显式指定表名保证生成的物理表名和论文设计一致后续写原生 SQL 或者做报表查询时不会对不上。2.3 用户与评论两张表的设计细节用户表里建议加一个is_active字段用于账号状态控制评论表则需要记录它关联的是哪部作品和哪个用户。Django 的ForeignKey会自动生成关联表_id字段这正是论文里refid和userid的 ORM 表达。class ComicComment(models.Model): comic models.ForeignKey(Comic, on_deletemodels.CASCADE, related_namecomments, verbose_name关联动漫) user models.ForeignKey(UserProfile, on_deletemodels.CASCADE, verbose_name用户) nickname models.CharField(max_length200, blankTrue, verbose_name用户名) avatar models.TextField(blankTrue, verbose_name头像) content models.TextField(verbose_name评论内容) reply models.TextField(blankTrue, verbose_name回复内容) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: db_table discussdongmanshipin verbose_name 动漫视频评论on_deletemodels.CASCADE表示关联的作品或用户被删除时评论一并删除。related_namecomments可以让你在查询作品时直接用comic.comments.all()拿到全部评论避免手动写复杂的 filter。这个字段是新手最容易漏的漏了之后 Django 会生成默认的comment_set可读性差很多。3. 推荐算法落地基于物品的协同过滤实现3.1 为什么不直接上深度学习论文摘要里提到推荐算法可能包括协同过滤、基于内容、深度学习方法。实际开发时一个毕设或小团队项目的用户量撑不起深度神经网络的训练而且可解释性差。常见做法是先用基于物品的协同过滤ItemCF它的思想是用户对一部漫画的偏好可以用和它相似的其他漫画来预测。比如用户 A 收藏了《进击的巨人》和《电锯人》用户 B 收藏了《电锯人》和《咒术回战》那么《电锯人》和《咒术回战》之间相似度就高系统会把《咒术回战》推荐给用户 A。3.2 从用户行为表构造打分矩阵推荐依赖用户行为数据行为来源主要是收藏、评论、浏览记录。在数据库里可以建一张行为日志表字段包括用户 id、作品 id、行为类型、时间。代码实现时先把它读进来构造“用户-物品”矩阵。import pandas as pd from collections import defaultdict # 假设行为数据格式: [user_id, comic_id, score] # score 根据行为加权: 收藏3, 评论2, 浏览1 records [ [1, 101, 3], [1, 102, 1], [2, 101, 2], [2, 103, 3], [3, 102, 1], [3, 103, 2] ] df pd.DataFrame(records, columns[user_id, comic_id, score]) # 构建用户-物品矩阵 user_item df.pivot_table(indexuser_id, columnscomic_id, valuesscore).fillna(0)pivot_table把数据变成二维矩阵行是用户列是漫画值是行为加权分。fillna(0)把没有行为的格子补成 0因为余弦相似度计算需要完整的向量。3.3 余弦相似度与推荐列表生成物品相似度计算用余弦相似度公式是向量内积除以模的乘积。写成代码如下import numpy as np def cosine_similarity(vec_a, vec_b): dot np.dot(vec_a, vec_b) norm_a np.linalg.norm(vec_a) norm_b np.linalg.norm(vec_b) if norm_a 0 or norm_b 0: return 0 return dot / (norm_a * norm_b) # 计算所有物品两两相似度 item_matrix user_item.T # 转置后行是漫画列是用户 comic_ids list(item_matrix.index) sim_dict defaultdict(dict) for i in range(len(comic_ids)): for j in range(i 1, len(comic_ids)): sim cosine_similarity( item_matrix.loc[comic_ids[i]].values, item_matrix.loc[comic_ids[j]].values ) sim_dict[comic_ids[i]][comic_ids[j]] sim sim_dict[comic_ids[j]][comic_ids[i]] sim # 为指定用户推荐 def recommend(user_id, top_n5): rated df[df[user_id] user_id][comic_id].tolist() scores defaultdict(float) for comic in rated: for sim_comic, sim_val in sim_dict[comic].items(): if sim_comic not in rated: scores[sim_comic] sim_val return sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_n] print(recommend(1))推荐逻辑分三步第一步取出用户看过的漫画第二步遍历这些漫画的相似物品把相似度累加到候选物品上第三步按累加分数排序取 Top-N。top_n控制返回数量实际接口里可以作为查询参数。如果用户没有行为数据scores为空这时要返回热门漫画兜底。下表是一个简单的物品相似度示意实际数值由行为矩阵计算而来物品对余弦相似度含义漫画101 - 漫画1020.58有一定相似可交叉推荐漫画101 - 漫画1030.91高度相似强关联漫画102 - 漫画1030.32相似度低避免误推这个算法的计算复杂度是 O(n²)漫画数量上万时每次实时算会卡。常见做法是每天用定时任务离线算一遍相似度存到 Redis 或数据库表里推荐时只做查询。3.4 接口层把推荐结果交给前端推荐结果最终通过 Django 的 View 返回 JSON。用一个函数视图包装推荐逻辑from django.http import JsonResponse def recommend_api(request): user_id request.GET.get(user_id) if not user_id: return JsonResponse({code: 400, msg: 缺少 user_id}) result recommend(int(user_id), top_n10) comic_list [] for comic_id, score in result: comic Comic.objects.filter(idcomic_id).first() if comic: comic_list.append({ id: comic.id, title: comic.title, cover: comic.cover.url if comic.cover else , score: round(score, 4) }) return JsonResponse({code: 0, data: comic_list})request.GET.get负责取参数用JsonResponse返回结构化数据前端拿到后可以直接渲染。filter().first()比get()安全因为数据异常时get()会抛DoesNotExist而first()返回None。4. 核心功能模块登录鉴权与内容管理的实现4.1 注册登录django.contrib.auth 还是自定义用户表论文设计里用户表是独立的yonghu表字段包含用户名、密码、昵称、性别、头像等。实际开发有两种处理方式一种是用 Django 自带的auth.User通过OneToOneField扩展资料字段另一种是把自定义的UserProfile作为核心用户表。考虑到论文表结构已经定死用第二种更直接。登录视图用authenticate验证用户身份from django.contrib.auth.hashers import make_password, check_password from django.views.decorators.http import require_http_methods import json require_http_methods([POST]) def register(request): data json.loads(request.body) username data.get(username) password data.get(password) if UserProfile.objects.filter(usernameusername).exists(): return JsonResponse({code: 400, msg: 用户名已存在}) UserProfile.objects.create( usernameusername, passwordmake_password(password), nicknamedata.get(nickname, ) ) return JsonResponse({code: 0, msg: 注册成功}) require_http_methods([POST]) def login(request): data json.loads(request.body) username data.get(username) password data.get(password) user UserProfile.objects.filter(usernameusername).first() if user and check_password(password, user.password): request.session[user_id] user.id return JsonResponse({code: 0, msg: 登录成功, user_id: user.id}) return JsonResponse({code: 400, msg: 用户名或密码错误})密码用make_password哈希后存储用的是 PBKDF2 算法比明文存储安全得多。check_password在登录时校验原文和哈希值。require_http_methods限制接口只接受 POST防止参数通过 URL 暴露在日志里。登录成功后把user_id写入 session后续需要鉴权的接口读取 session 判断身份。注册登录接口的参数通过请求体 JSON 传递前端 Vue 里用axios调用axios.post(/api/login/, { username: test, password: 123456 }) .then(res { if (res.data.code 0) { this.$router.push(/home); } })4.2 管理员模块用户管理和动漫分类管理管理员功能包括用户管理、动漫分类管理、动漫视频管理、动漫分享管理、留言板管理。Django 的 admin 后台可以直接覆盖大部分需求但论文需要的是自定义业务页面所以通常用装饰器写权限校验逻辑from functools import wraps def admin_required(view_func): wraps(view_func) def wrapper(request, *args, **kwargs): if request.session.get(role) ! admin: return JsonResponse({code: 403, msg: 无权限}) return view_func(request, *args, **kwargs) return wrapper admin_required def add_category(request): name request.POST.get(name) ComicCategory.objects.create(namename) return JsonResponse({code: 0, msg: 添加成功})admin_required是装饰器每次执行管理员操作前先检查session[role]不是管理员直接返回 403。users表里的role字段就是论文中设计的“角色管理员”字段。动漫视频管理涉及图片上传Django 的ImageField配合 MEDIA 配置# settings.py MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media)上传文件会存入media/comic_cover/目录comic.cover.url生成可访问的 URL 路径Nginx 部署时把这个路径映射到静态文件服务。4.3 前台与后台的接口划分前台模块包括首页、动漫视频、动漫分享、动漫论坛、公告资讯、留言板、个人中心后台模块包括首页、个人中心、用户管理、动漫分类管理、动漫视频管理、动漫分享管理、留言板管理、动漫论坛、系统管理。接口设计按模块拆分如下模块请求路径方法说明用户/api/user/register/POST注册用户/api/user/login/POST登录内容/api/comic/list/GET漫画列表支持分类筛选内容/api/comic/ /GET漫画详情推荐/api/recommend/GET个性化推荐管理/api/admin/user/list/GET用户列表管理/api/admin/category/add/POST新增分类列表接口需要支持分页Django 的分页类可以这样用from django.core.paginator import Paginator def comic_list(request): category_id request.GET.get(category_id) comics Comic.objects.all() if category_id: comics comics.filter(category_idcategory_id) paginator Paginator(comics, 12) page request.GET.get(page, 1) comics_page paginator.get_page(page) data [{ id: c.id, title: c.title, cover: c.cover.url if c.cover else , category: c.category.name } for c in comics_page] return JsonResponse({code: 0, data: data, total: paginator.count})Paginator(comics, 12)表示每页 12 条get_page(page)自动处理页码越界传非数字时返回第一页比手动算 offset 安全。5. 功能测试与性能优化推荐效果和查询速度怎么验证5.1 功能测试用例设计测试阶段要覆盖正常流程和异常流程。以下是一组核心用例用例编号测试项前置条件操作预期结果TC01正常登录已注册用户输入正确用户名密码登录成功跳转首页TC02错误密码已注册用户输入错误密码提示用户名或密码错误TC03重复注册无使用已存在用户名注册提示用户名已存在TC04分类筛选存在多条漫画选择分类点击筛选只显示该分类漫画TC05推荐接口用户有行为记录调用推荐接口返回 Top-N 且不含已看漫画用 Django 的TestCase可以自动化回归from django.test import TestCase from django.test import Client class LoginTest(TestCase): def setUp(self): self.client Client() UserProfile.objects.create_user( usernametest, passwordpass123 ) def test_login_success(self): resp self.client.post(/api/user/login/, { username: test, password: pass123 }, content_typeapplication/json) self.assertEqual(resp.status_code, 200) self.assertEqual(resp.json()[code], 0)Client()模拟浏览器发起请求post的第三个参数指定 JSON 格式。setUp方法在每个测试用例执行前运行保证测试数据独立。注意如果UserProfile没有继承AbstractUsercreate_user是不存在的需要改为objects.create(username..., passwordmake_password(...))。5.2 推荐效果的离线验证推荐算法好坏不能靠感觉最常见的离线评估方法是把行为数据切成训练集和测试集用训练集计算相似度用测试集验证命中率。from sklearn.model_selection import train_test_split train, test train_test_split(df, test_size0.2, random_state42) # 用 train 重新执行相似度计算和推荐逻辑 # 命中率 推荐列表中出现在 test 里的次数 / 推荐总数 hit 0 total 0 for user_id in test[user_id].unique(): rec_list recommend(user_id, top_n10) actual set(test[test[user_id] user_id][comic_id]) hit len([x for x in rec_list if x[0] in actual]) total len(rec_list) print(命中率:, hit / total)train_test_split按 8:2 划分数据random_state固定随机种子保证结果可复现。命中率目标是 10% 以上如果过低说明行为数据太稀疏需要降低top_n或引入热度值作为平滑项。5.3 查询速度优化索引与 N1 问题漫画列表页最常见的性能问题是关联查询导致的 N1。Django ORM 查询Comic.objects.all()后再取c.category.name每取一条数据就查一次分类表。用select_related一次性查出关联对象comics Comic.objects.select_related(category).all()对应 SQL 是左连接查询只有一条 SQL 而不是 N1 条。数据量超过几万条时给筛选字段加上索引class Comic(models.Model): category models.ForeignKey(ComicCategory, on_deletemodels.CASCADE, db_indexTrue) publish_date models.DateField(nullTrue, blankTrue, db_indexTrue)db_indexTrue在迁移时会生成索引。排查慢查询可以用 Django 自带的connection.queries或者在 MySQL 里执行EXPLAIN SELECT * FROM dongmanshipin WHERE category_id 3 ORDER BY created_at DESC;看到type为ALL说明全表扫描需要加索引。6. 部署上线uWSGI 与 Nginx 配置的关键参数开发环境用python manage.py runserver就够了上线必须换 uWSGI Nginx。核心配置文件uwsgi.ini[uwsgi] http-timeout 60 socket 127.0.0.1:8001 chdir /var/www/comic_project module comic_project.wsgi:application master true processes 4 threads 2 vacuum true max-requests 5000 daemonize /var/log/uwsgi/comic.logsocket是 uWSGI 内部协议地址只对本地开放Nginx 通过这个端口转发请求。processes4表示启动 4 个 worker 进程threads2表示每个进程 2 个线程总并发能力是 8。max-requests5000让 worker 处理 5000 个请求后自动重启避免内存泄漏累积。chdir指向项目根目录module指向 wsgi 入口文件。Nginx 配置则处理静态文件和反向代理server { listen 80; server_name your-domain.com; location /static/ { alias /var/www/comic_project/static/; expires 30d; } location /media/ { alias /var/www/comic_project/media/; } location / { include uwsgi_params; uwsgi_pass 127.0.0.1:8001; uwsgi_read_timeout 120s; } }expires 30d让浏览器缓存静态资源 30 天减少重复请求。开始时uwsgi_pass配置为127.0.0.1:8001必须和 uWSGI 的 socket 地址一致不一致会报connection refused。如果项目里用到了 Django admin 的静态文件上线前要执行python manage.py collectstatic把各 app 的静态文件集中到配置的根目录否则管理中页面会丢样式。部署遇到 502 时先看 uWSGI 日志而不是 Nginx 日志多数情况是 worker 崩溃或者 socket 权限问题。上传图片无法显示时检查MEDIA_ROOT路径是否存在以及 Nginx 进程是否有读取权限。Django 的DEBUG必须设为False否则会暴露完整的报错堆栈ALLOWED_HOSTS要加上实际域名。配置完systemctl restart uwsgi nginx访问首页做一次注册、登录、推荐接口的冒烟验证即可。本文还有配套的精品资源点击获取