资讯动态

Python Flask实现豆瓣短评情感分析与协同过滤电影推荐系统

发布时间:2026/10/8 20:08:32 来源:尧图企业网站定制
1. 项目上手前瞻这个系统到底解决什么问题先说结论这是一套把豆瓣电影短评情感分析和个性化电影推荐结合起来的小型Web系统技术栈是Python Flask MySQL/SQLite附带完整源码、数据库脚本和设计文档典型的毕设/课程设计/练手项目形态。标题看着不大但它其实覆盖了三个独立难点前端展示与交互、中文文本情感分析、基于用户行为的协同过滤推荐。很多同学一看到情感分析和推荐系统就发怵觉得算法高不可攀实际上拆开看每一块都有非常成熟的实现路径难度并没有想象中那么大。这个系统能做什么通俗讲三件事第一抓取或导入豆瓣上某部电影的用户短评对每一条评论做情感打分正面/负面/中性算出整部电影的观众情绪指数第二根据用户对电影的历史评分行为推荐他可能喜欢的其他电影第三在Web页面上把这些结果可视化地展示出来管理员可以维护电影和评论数据普通用户可以浏览电影、发表评论、看情感分析结果、获取个性化推荐列表。适合谁来参考如果你是计算机相关专业的应届生拿它当毕业设计骨架再合适不过功能模块划分清晰、文档配合度高、答辩时容易讲清楚如果你是想入门Flask全栈开发的初学者它也是不错的练手项目——因为你会同时接触到数据库设计、后端接口、模板渲染、简单的机器学习应用一鱼多吃。当然如果你只是想要一个能跑的Demo快速改一改交作业那这套结构的可扩展性也能满足你。我见过太多类似的系统最常见的通病是功能堆了一堆但逻辑不自洽。比如情感分析只算了个平均分、推荐算法只用了随机电影填充答辩一问就露馅。所以这篇文章不打算只是泛泛介绍模块而是要把每一块为什么这样做、数据怎么流转、代码怎么组织、上线会踩什么坑都讲透。提示文中所有代码和思路都基于常见公开实践整理你拿到源码后可以对照着本文理解项目脉络真正改造成自己的东西。2. 数据从哪里来豆瓣电影数据的获取与入库方案2.1 数据源选型爬虫、公开数据集还是手动录入做情感分析和推荐系统数据是地基。这个项目里涉及两类核心数据电影信息片名、导演、演员、类型、简介、封面和用户评论短评内容、评分、点赞数、评论时间。推荐模块还需要用户行为数据——也就是哪些用户给哪些电影打了多少分。数据来源通常有四种方案我在实际搭建时都试过各有取舍方案优点缺点适用场景爬虫抓取豆瓣公开页面数据新鲜、真实、量可控有反爬限制需控制频率演示效果最好推荐公开数据集如MovieLens格式规整、量大、有真实评分没有中文短评情感分析素材缺失偏推荐算法实验手动录入/后台添加完全可控、无合规风险数据量小推荐效果和情感素材不足功能演示、验收兜底第三方API省事接口不稳定、需要额外鉴权不推荐作为主方案我最后采用的是爬虫抓取 手动兜底的组合策略。先用Python的requests库以较低频率请求豆瓣电影页面和短评接口解析出需要的字段批量入库同时后台保留手动录入入口防止某个电影确实抓不到数据的情况。这里必须提醒一下抓取公开数据时要遵守网站的robots协议控制请求频率不要并发请求轰炸、不要抓取用户隐私信息、不要用于商业用途。做毕设和练手的话少量低频抓取公开页面数据用于学习演示是行业内普遍接受的做法。源码里我默认也内置了一份示例数据库直接导入就能跑通全流程不需要你本地去爬。2.2 数据库表结构设计五张表如何撑起整个系统数据库用的是MySQL源码里也兼容SQLite切换到SQLite时把连接串换一下即可核心设计如下-- 电影表 CREATE TABLE movie ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, director VARCHAR(100), actors VARCHAR(500), genre VARCHAR(100), country VARCHAR(50), release_year INT, rating DECIMAL(3,1), -- 豆瓣基础评分 description TEXT, cover_url VARCHAR(500), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 短评表 CREATE TABLE comment ( id INT PRIMARY KEY AUTO_INCREMENT, movie_id INT NOT NULL, user_name VARCHAR(100), content TEXT NOT NULL, sentiment_score FLOAT, -- 情感得分 -1~1 sentiment_label VARCHAR(10), -- positive / negative / neutral create_time DATETIME, FOREIGN KEY (movie_id) REFERENCES movie(id) ); -- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, avatar VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 评分表用户对电影的打分推荐算法的核心输入 CREATE TABLE rating ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, movie_id INT NOT NULL, score FLOAT NOT NULL, -- 1~5 分 rated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_movie (user_id, movie_id), FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (movie_id) REFERENCES movie(id) );这套表结构有几点专门设计过电影表独立成表不把评论直接塞进电影字段里因为一部电影对应多条评论评论本身还要做情感分析、按时间排序拆开才能灵活查询。comment表增加sentiment_score和sentiment_label两个冗余字段。很多初写者喜欢实时分析、不打库但那样每次打开电影详情页都要重新跑一遍情感分析性能和结果稳定性都差。入库时算好展示时直接读这就是典型的计算与存储分离思路。rating表用联合唯一键约束(user_id, movie_id)保证同一用户对同一部电影只能有一条评分记录这是推荐数据质量的基本保证。2.3 入库的工程细节清洗、去重、编码三座大山数据入库远没有想象中顺利我跑第一批数据时踩了三个坑列出来帮你避雷第一个坑编码问题。豆瓣页面返回的内容包含大量中文标点和特殊字符比如·间隔号、繁体字、emoji。如果数据库连接串没指定charsetutf8mb4存入emoji时会直接报错。解决方式MySQL建库时指定DEFAULT CHARSETutf8mb4Flask的SQLAlchemy连接串写成mysqlpymysql://user:passlocalhost/db?charsetutf8mb4。第二个坑去重。同一部电影的短评翻页抓取时可能因为页面缓存出现重复内容。我在入库前加了一步MD5去重对content字段做哈希唯一索引直接打断重复插入。这是数据治理里最廉价有效的方案。第三个坑短评里的噪声。很多短评内容很短比如好看两个字这种数据情感分析可用但还有哈哈哈哈……这类无意义内容会拉低整体信号质量。我设了一个规则过滤掉字符长度小于2的评论在入库前就处理掉。3. 情感分析模块的算法拆解从词典打分到模型增强3.1 为什么中文短评情感分析不先上BERT做情感分析业内路线大致分三类基于情感词典、基于传统机器学习朴素贝叶斯/SVM、基于深度学习LSTM/BERT。很多人一上来就问为什么不用BERT原因很简单这是一个Flask单体项目要跑在普通笔记本上要能在文档里讲清楚原理BERT的部署代价、显存要求、推理延迟对毕设而言都是负担而且答辩时很难向老师解释清楚黑盒模型内部在做什么。所以我的实现方案是以情感词典打分为主以SnowNLP模型输出为辅两条线互相校验。这个组合的优势是词典方法逻辑透明、没网也能跑、可控性强SnowNLP提供基于统计的先验情感倾向尤其是处理长句、反讽、对比句式时比纯词典更鲁棒。3.2 情感词典打分的完整实现情感词典打分的逻辑其实就是给句子里的情感词加权import jieba # 简化的情感词典实际源码里有约8000个词条 POSITIVE_WORDS {好, 赞, 喜欢, 精彩, 感动, 震撼, 经典, 真实, 温暖} NEGATIVE_WORDS {差, 烂, 失望, 无聊, 尴尬, 拖沓, 虚假, 雷人, 浪费} NEGATION_WORDS {不, 没, 无, 非, 莫, 勿, 别, 不是, 没有, 毫无} # 程度副词权重 DEGREE_WORDS { 很: 1.8, 非常: 2.0, 太: 1.5, 特别: 2.2, 超级: 2.5, 有点: 0.7, 稍微: 0.6, 极其: 2.8, 真: 1.5, 最: 1.6 } def score_sentence(text): words list(jieba.cut(text)) total 0.0 count 0 negate False degree 1.0 for w in words: if w in NEGATION_WORDS: negate not negate # 切换否定状态 elif w in DEGREE_WORDS: degree DEGREE_WORDS[w] # 记录程度副词 elif w in POSITIVE_WORDS: val 1.0 * degree * (-1 if negate else 1) total val count 1 degree 1.0 negate False elif w in NEGATIVE_WORDS: val -1.0 * degree * (-1 if negate else 1) total val count 1 degree 1.0 negate False if count 0: return 0.0 return total / (count ** 0.5) # 归一化降低长度影响这段代码里有三个细节值得说否定词翻转不好是负面不是不好其实是正面双重否定。所以每遇到否定词就反转一次极性标记这个逻辑虽然简单但能处理大多数口语化短评。程度副词加权很好看和好看的情感强度差异是很大的靠程度副词加权可以把这一点体现出来。归一化句子越长情感词越多累加值越大。我除以count ** 0.5本质上是在平均和累加之间取一个折中避免两句话因为字数不同导致分数失真。3.3 SnowNLP的交叉校验词典方法虽然清晰但遇到编剧的脑子是不是被门夹了这种高语境高反讽的句子就会失效。所以我在源码里加了SnowNLP作为第二分析器from snownlp import SnowNLP def snownlp_score(text): s SnowNLP(text) # sentiments 输出 0~1 的概率值 return s.sentiments * 2 - 1 # 映射到 -1~1两路得分做加权融合def combine_scores(text): dict_score score_sentence(text) model_score snownlp_score(text) # 词典分置信度高时更信任词典否则更信任模型 weight min(abs(dict_score) * 2, 0.7) final_score dict_score * weight model_score * (1 - weight) final_score max(-1, min(1, final_score)) return final_score这个融合函数是实际运行中调出来的经验值核心逻辑是词典分越是极端越说明句子情感表达强烈给词典的信任度越高词典分接近0拿不准时让统计模型说了算。举几个实际效果短评内容词典分SnowNLP分融合结果人工判断太好看了全场爆哭年度最佳2.30.82正面强正面剧情拖沓演技尴尬-2.10.35负面负面导演的想法是好的但剧本撑不起0.2-0.45轻微负面负面就那样吧不算差也不算好0.1-0.1中性中性3.4 情感结果如何场景化情感得分算出来后不能只存个数我在页面上做了一组可视化情绪分布饼图正面/中性/负面占比、情感极性词云、该电影的情绪得分与豆瓣评分的对照。这里有个知识点情感得分和豆瓣基础评分不能直接等同——豆瓣评分是用户点星行为情感得分是文本内容的情绪表达两者叠加展示反而更有信息量。很多用户给电影打了两颗星但短评写的是有创意但虎头蛇尾情感分析捕捉到的就是肯定后带惋惜这种细腻信息是评星数据给不了的。4. 推荐模块的实现方案协同过滤与冷启动的折中4.1 选择基于用户的协同过滤而不是基于物品推荐算法三大流派基于内容的过滤Content-based、协同过滤CF、混合推荐。这个系统我选的是基于用户的协同过滤User-CF理由有三数据形态匹配评分表里天然有用户-电影-评分三元组这是User-CF的标准输入。解释起来容易推荐逻辑是和你口味相似的人也喜欢这些电影答辩和演示都直观好讲。和情感分析形成差异化情感分析解决电影好不好推荐解决你适不适合看两个模块逻辑上不重复、功能上互补。有人可能会问为什么不选Item-CF或矩阵分解。Item-CF在用户量小、行为稀疏的项目里效果可以但它的维护成本更高每次新增电影都要重新算相似度矩阵矩阵分解SVD效果确实更好但需要额外引入surprise等库且调参门槛对初学者不太友好。User-CF在这个规模下性价比最高。4.2 User-CF核心代码与相似度计算协同过滤的核心是两步找相似用户、聚集相似用户的偏好生成推荐。from math import sqrt from collections import defaultdict def user_similarity(ratings): # ratings: {user_id: {movie_id: score}} movie_users defaultdict(set) for uid, movies in ratings.items(): for mid in movies: movie_users[mid].add(uid) # 计算共现矩阵 co_occur defaultdict(lambda: defaultdict(int)) for mid, users in movie_users.items(): for u in users: for v in users: if u ! v: co_occur[u][v] 1 similarity {} for u, related in co_occur.items(): similarity[u] {} for v, count in related.items(): # 皮尔逊相关系数 common_movies set(ratings[u].keys()) set(ratings[v].keys()) if len(common_movies) 2: similarity[u][v] 0 continue s1 [ratings[u][m] for m in common_movies] s2 [ratings[v][m] for m in common_movies] mean1 sum(s1) / len(s1) mean2 sum(s2) / len(s2) num sum((a - mean1) * (b - mean2) for a, b in zip(s1, s2)) den sqrt(sum((a - mean1) ** 2 for a in s1) * sum((b - mean2) ** 2 for b in s2)) similarity[u][v] num / den if den ! 0 else 0 return similarity def recommend_for_user(user_id, ratings, similarity, top_n8): avgs {uid: sum(m.values()) / len(m) for uid, m in ratings.items()} scores {} sim_sum {} target ratings[user_id] for other_uid, sim in similarity.get(user_id, {}).items(): if sim 0: continue for mid, score in ratings[other_uid].items(): if mid in target: continue scores[mid] scores.get(mid, 0) sim * (score - avgs[other_uid]) sim_sum[mid] sim_sum.get(mid, 0) sim # 加权平均并填补用户均值 predictions {} for mid, s in scores.items(): if sim_sum[mid] 0: predictions[mid] avgs[user_id] s / sim_sum[mid] return sorted(predictions.items(), keylambda x: x[1], reverseTrue)[:top_n]代码里有几个工程化的关键处理直接决定推荐质量只计算有共同评分记录的用户对。全量两两用户算相似度在用户数上千时就是O(n²)灾难先通过电影-用户倒排索引筛出共现对计算量直接下降一个数量级。用皮尔逊系数而不是余弦相似度。余弦相似度对评分的宽松/严格风格不敏感皮尔逊通过减均值解决这个问题——一个用户习惯给3分一个用户习惯给4分他们口味其实一样皮尔逊能捕捉到这种一致性。预测公式中的均值偏移avg_user 加权偏移避免新用户/低评分用户的预测值系统性偏低。4.3 冷启动问题的三层兜底推荐系统最怕冷启动——新用户没有评分行为、新电影没有用户打过分协同过滤直接瘫痪。我做三层兜底注册引导评分新用户注册后系统展示一批热门电影让用户先评3~5部这是最有效的破解方式。热门加权兜底用户没有足够评分少于3条时改用全网收藏数豆瓣评分情感得分加权生成热门榜推荐代码里体现在if len(ratings[user_id]) 3: return hot_movies()。混合推荐最终推荐列表按0.7 * CF预测分 0.3 * 电影热度分热度收藏数/最高收藏数 * 2 情感得分 * 1排序。这样做的好处是即使推荐列表里有冷门片也不会全部端出没人看的文艺片保证页面观感。4.4 离线计算还是实时计算这里必须坦白一个教训协同过滤的相似度矩阵千万不要在请求里实时算。我第一次实现时每次打开为你推荐页面都现算相似度用户量几十个人还好上了几百个用户后页面直接卡死。后来改成启动项目时预计算一次相似度矩阵并存到内存或Redis评分数据发生变化时标记脏数据定时任务每5分钟重算一次。推荐这个颗粒度的更新频率用在展示型项目上完全够而且性能体验会好非常多。5. Flask后端架构蓝图为线索串联全栈并打通前端5.1 项目目录结构怎么组织才不被人骂很多毕设项目的Flask代码全都堆在app.py一个文件里几百行路由加几百行业务逻辑老师看着糟心你自己维护也崩溃。我推荐的目录结构是这样的movie_recommend/ ├── app.py # 应用入口注册蓝图 ├── config.py # 配置数据库、密钥、上传路径 ├── requirements.txt # 依赖清单 ├── models/ │ ├── __init__.py │ ├── movie.py # 电影模型 │ ├── comment.py # 短评模型 │ ├── user.py # 用户模型 │ └── rating.py # 评分模型 ├── services/ │ ├── sentiment.py # 情感分析服务 │ ├── recommend.py # 推荐算法服务 │ └── crawler.py # 数据抓取服务 ├── routes/ │ ├── __init__.py │ ├── auth.py # 注册/登录 │ ├── movie.py # 电影详情/列表 │ ├── comment.py # 短评提交/分析结果 │ └── recommend.py # 推荐接口 ├── static/ │ ├── css/ │ ├── js/ │ └── images/ ├── templates/ │ ├── base.html │ ├── index.html │ ├── login.html │ ├── movie_detail.html │ └── recommend.html └── data/ └── douban.sql # 数据库初始化脚本这个结构的核心思想是按职责分层models只管数据对象services放核心算法逻辑routes只做参数校验和流程调度templates专注页面渲染。好处是情感分析算法要换实现只动services/sentiment.py要加新页面只加routes和templates排查Bug时能一眼定位问题在哪一层。5.2 蓝图如何注册、路由如何设计Flask用蓝图模块化注册代码很简单但规范很重要# routes/movie.py from flask import Blueprint, render_template, request from models.movie import Movie from services.recommend import get_hot_movies movie_bp Blueprint(movie, __name__) movie_bp.route(/movie/int:movie_id) def movie_detail(movie_id): movie Movie.query.get(movie_id) if not movie: return 电影不存在, 404 comments movie.comments.order_by(Comment.create_time.desc()).limit(20) # 情感分析统计 stats analyze_stats(movie_id) return render_template(movie_detail.html, moviemovie, commentscomments, statsstats) movie_bp.route(/) def index(): hot_movies get_hot_movies(10) return render_template(index.html, hot_movieshot_movies) movie_bp.route(/search) def search(): keyword request.args.get(q, ) results Movie.query.filter(Movie.title.like(f%{keyword}%)).all() return render_template(search_result.html, resultsresults)路由设计的核心思路是RESTful风格 模板渲染合一不必上前后端分离。原因很简单这是一个以展示和教学为主的项目Jinja2模板直接渲染服务端页面省去前端跨域调试和接口联调的成本思路更聚焦在算法和功能本身。路由清单大致如下方法路径功能涉及模块GET/首页热门电影电影列表GET/POST/auth/register注册用户GET/POST/auth/login登录用户GET/movie/int:mid电影详情页电影评论情感分析POST/movie/int:mid/comment发表短评评论入库情感分析GET/recommend/list推荐列表协同过滤GET/admin/movies后台管理电影增删改查POST/admin/movie/add新增电影增删改查5.3 情感分析结果怎么传到前端页面联动细节后台情感分析算完后通过Jinja2渲染传递。电影详情页核心区我希望展示三块电影基本信息、短评列表带情感标签、情感分布图表。图表我用的是ECharts前端通过| tojson过滤器把后端统计结果转成JSON!-- movie_detail.html 中的情感分布图 -- div idsentimentChart styleheight: 300px;/div script const stats {{ stats | tojson }}; var chart echarts.init(document.getElementById(sentimentChart)); chart.setOption({ series: [{ type: pie, radius: [40%, 70%], data: [ { value: stats.positive, name: 正面评价 }, { value: stats.neutral, name: 中性评价 }, { value: stats.negative, name: 负面评价 } ] }] }); /script同时每条短评右侧用标签高亮显示情感极性——绿色正面、红色负面、灰色中性。这里有个不易察觉但很影响观感的细节负面评论不代表电影差。比如一部悬疑片负面评论可能是太吓人了看完做噩梦情感分析分出来是负面但观众意愿其实是被爽到了。所以页面上我额外加了一句说明文案负面短评可能包含恐怖/悬疑等情绪化表达请结合短评内容自行判断。这个小细节在答辩时反而容易成为亮点体现你对数据局限性的思考。6. 安全与性能优化上线前必须过的两道关6.1 安全三件事密码、SQL注入、XSS安全是Flask项目最容易被忽略的模块但毕设评委和公司面试官都会问。我做了三件事成本不高但效果扎实密码哈希存储。绝不存明文密码用werkzeug.security自带的generate_password_hash和check_password_hash内部默认走pbkdf2:sha256算法这是Flask生态的标准做法不需要引入额外依赖。from werkzeug.security import generate_password_hash, check_password_hash password_hash generate_password_hash(123456) # 校验时 check_password_hash(password_hash, 123456)SQL注入防护。所有数据库操作用SQLAlchemy ORM的查询构造器不手写拼接字符串SQL。唯一用了like(%keyword%)的地方也经过ORM参数绑定不会把用户输入直接拼进SQL。如果你看源码发现某处直接拼接那是Debug残留务必改掉。XSS防御。Jinja2模板默认对变量做HTML转义{{ comment.content }}不会被当作HTML执行。但ECharts的配置数据用| tojson传值时注意不要直接渲染HTML字符串。短评里的特殊字符入库前最好再统一清洗一遍避免恶意脚本进入页面。6.2 性能优化的三板斧缓存推荐列表接口加上了简单的内存缓存——用functools.lru_cache装饰器以user_id为key缓存结果10分钟电影详情页的情感分析统计值缓存1小时因为评论增量变化不大没必要每次查库重算。数据库索引rating表加联合索引(user_id, movie_id)因为协同过滤和推荐查询绝大多数是查某用户的所有评分和查某电影的所有评分这两个查询命中率最高。批量写入批量抓取评论入库时用session.bulk_insert_mappings一次插100条而不是单条循环insert速度提升一个量级。def batch_save_comments(comment_list): from models.comment import Comment db.session.bulk_insert_mappings(Comment, comment_list) db.session.commit()7. 运行时踩坑实录从爬虫被限制到推荐结果全相同这个项目我完整跑过不止一遍踩过的坑值得单独列一节。以下全是实际操作中的真实问题不是纸面推演。7.1 坑一爬虫请求头不完整直接返回418第一次抓豆瓣数据时我只简单设置了User-Agent结果请求直接返回418状态码页面内容被拒绝。后来翻了社区里的实践确认豆瓣对请求头校验非常严格至少要补全以下字段headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, }同时请求间隔至少控制在3~5秒一次每次抓完一个电影的短评后sleep(random.uniform(2, 5))模拟人工浏览节奏。否则哪怕请求头齐全短时间高频请求照样会被封IP。7.2 坑二推荐列表出来全是0.0分或者全一样调试协同过滤时最容易出现的两个现象预测分数全部是0或者每个人推荐列表一模一样。前者通常是相似度计算时den0——两个用户虽然在多部电影上都有评分但分数完全一致皮尔逊相关系数分母为0我代码里把这种情况的相似度设成了0于是没有任何有效邻居。解决方式是评分必须有方差引导用户在注册评分时尽量差异化不要全部打4分。后者全一样则是热门兜底在发力——当绝大多数用户评分数据太少少于3条协同过滤压根跑不起来全都走了热门榜。这说明数据量和用户行为丰富度不够不是代码bug。我在演示前会先创建5~6个种子用户每个用户给20部左右的电影打分故意让他们的偏好有明显差异比如A用户只打高分科幻片B用户喜欢文艺片这样推荐结果差异化才明显演示效果才有说服力。7.3 坑三情感分析把长的真丑判成了正面这是早期版本的经典翻车案例。长的真丑里真是程度副词丑是负面词词典打分应该是负分。但学生版词典里我没有收录丑而SnowNLP对这个句子的判断也很奇怪输出0.48接近正面。排查发现SnowNLP会把句子切分为长/的/真/丑丑在它的语料里权重不高而真被当作正面词给了一半权重。修复思路不是调SnowNLP而是给词典加负面词黑名单把丑、烂、渣、臭、假、空、乱、吵等高频贬义词单独设为一个强负面集合遇到直接给-2分强行覆盖模型输出。这类经验总结就是在有限数据下词典方法的核心不是词条多而是对高频极端词的控制力。后来我还专门加了规则句子中负面词如果超过2个且没有疑问语气直接判定为强负面跳过模型融合。7.4 坑四Flask Debug模式下做爬虫数据库连接被线程卡死Debug模式开着会把代码加载为自动重载模式爬虫运行中Flask后台会有两个进程在跑一旦爬虫和Web请求同时操作SQLite数据库就会出现database is locked的经典报错。解决方式部署运行时关闭Debug并使用连接池。如果用MySQLSQLAlchemy配置pool_size10, max_overflow5如果坚持用SQLite爬虫入库和Web查询必须错峰或者给SQLite连接设置check_same_threadFalsetimeout20。8. 部署上线实践从本机跑通到服务器发布8.1 本机跑通的最小步骤拿到源码后按这个顺序可以十分钟内跑起来# 1. 创建虚拟环境强烈推荐避免污染系统Python python -m venv venv # Windows激活: venv\Scripts\activate # Linux/Mac激活: source venv/bin/activate # 2. 安装依赖 pip install -r requirements.txt # 3. 初始化数据库用自带的SQL文件 mysql -uroot -p data/douban.sql # 4. 修改config.py里的数据库连接串和SECRET_KEY # 5. 启动应用 python app.py # 浏览器访问 http://127.0.0.1:5000requirements.txt里最关键的依赖版本建议如下依赖版本建议说明Flask2.3.x稳定文档丰富SQLAlchemy2.0.xORMPyMySQL1.1.xMySQL驱动requests2.31.x爬虫jieba0.42.x中文分词SnowNLP0.12.3情感分析numpy1.24.x算法计算Werkzeug2.3.x密码哈希8.2 服务器部署用什么方案本机Demo和真正部署上线是两回事。我用的是Gunicorn Nginx MySQL的经典组合Gunicorn跑Flask应用4个workerNginx做反向代理和静态资源服务MySQL存数据。# Gunicorn启动命令 gunicorn -w 4 -b 127.0.0.1:5000 app:appNginx关键配置server { listen 80; server_name your_domain_or_ip; location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias /var/www/movie_recommend/static/; } }注意三个实际部署中必须处理的细节静态文件别让Flask管。Flask自带的静态文件服务性能很差图片多时CPU容易爆Nginx直接处理静态资源能省很多事。启动脚本用systemd管理。写一个movie.service设置Restartalways服务器重启后自动拉起应用。不然哪天服务器重启你的系统就失踪了。定时任务别启动太多。如果用Cron定时重算推荐相似度加上锁比如flock防止上一个任务没跑完下一个又开始导致数据库连接风暴。8.3 备份与恢复数据库别裸奔数据库脚本文件里我默认带了示例数据但如果你之后录入了自己的电影和评论最好每周备份一次mysqldump -uroot -p movie_db backup_$(date %Y%m%d).sql恢复时执行mysql -uroot -p movie_db backup_xxx.sql即可。我个人习惯是再配一个定时任务保留最近7天的备份超过的自动清理避免占满磁盘。9. 可扩展方向与进阶改造清单基础版跑通只是起点这个系统有天然的扩展路径。如果你学有余力或者想在毕设里加亮点下面几个方向按性价比排序方向一用TF-IDF做评论主题聚类。目前的短评只做了正负情感分类但同一部电影里观众讨论的重点是完全不同的——有人聊剧情有人聊演员有人聊特效。用TF-IDF提取关键词再聚类出剧情派颜值派特效派的占比这个维度在电影分析场景非常自然页面展示也高级。from sklearn.feature_extraction.text import TfidfVectorizer comments [c.content for c in movie.comments] vectorizer TfidfVectorizer(max_features200, stop_wordsenglish) matrix vectorizer.fit_transform(comments) # 再丢进KMeans做3~4类聚类方向二把推荐算法升级为矩阵分解。User-CF在小数据量上效果还行但如果数据量上万条评分SVD的稳定性和精度会更好。surprise库封装了经典的SVD实现替换成本很低from surprise import SVD, Dataset, Reader reader Reader(rating_scale(1, 5)) data Dataset.load_from_df(ratings_df, reader) algo SVD(n_factors20, reg_all0.02) algo.fit(data.build_full_trainset())方向三接入更多数据源。豆瓣只是起步加上猫眼、IMDb或者IMDb中文站的公开页面数据可以做一个多平台口碑对比这个视角的项目在市场上更有竞争力。方向四前台互动升级。目前系统的互动主要是发表短评和打分。进阶可以加点赞/收藏电影、关注其他用户、生成观影报告类似每年底各平台出的个人年度报告这些功能虽然业务性强但实现起来都是套路化的CRUD能让整个项目的完整度上一个档次。说到底这套源码的真正价值不只是能跑而是它完整覆盖了从数据采集→数据清洗→算法建模→Web服务→部署上线的整个生命周期。你要是只把它当交作业的工具那挺可惜的如果你愿意顺着这些扩展方向改造一轮这个项目的深度足够撑起一次不错的面试项目复盘——因为每一个模块你都能讲清楚我做了什么、遇到了什么问题、为什么这么解决。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑