简介:这套基于Python实现的电影信息智能问答系统项目,包含完整源代码、数据库与文档说明,面向计算机相关专业学生、毕业设计者、Python/Django初学者,也适合作为课程设计或项目演示的参考。系统基于Django框架,通过模板匹配与规则分类对电影类问题进行识别、解析并给出答案,内置预训练模型和SQLite数据库,可快速体验问答流程。资源压缩包共67个文件,类型涵盖18个Python源码、8个XML配置文件、5个CSS样式、4个JS脚本、3个HTML页面,以及SQLite数据库、requirements依赖清单和run.bat启动脚本,整体大小约875KB。项目按Django标准结构组织,主配置在movie_system,业务逻辑位于movie_main,核心问答模块movie_question_solve包含问句预处理、模板求解、问题分类等功能,结构清晰、便于参考与二次开发。目前已有78人学习浏览,下载后可直接获取源码、数据库、README说明及运行方法,代码经过验证可用,是理解和搭建智能问答系统的实用资料。
1. 电影信息智能问答系统:不是 Chatbot,是检索式问答的最小实用样本
如果你在课程设计或简历项目里看到“基于Python实现的电影信息智能问答系统”,先别把它想象成 ChatGPT 那种大模型对话。它本质上是把“用户问一句电影相关的问题”映射到“数据库里的某条或某几条记录”,再把记录整理成自然语言答案返回。常见实现是检索式问答:问题进来,先分词、做向量化,再和预置的问答库或电影字段做相似度匹配,最后把命中的内容包装成回答。这套系统的价值在于,它用很小的算力成本让你把 Python、SQL、中文分词、文本相似度四样东西串成一条完整链路,是入门问答系统落地的性价比最高的方案之一,也是数据库课程设计和 Python 课程设计里出现频率极高的一类题目。适合谁?适合手里有一份“源代码+文档说明+数据库”却不知道怎么讲清楚、改明白、跑起来的在校生,也适合想用最短路径验证“检索式问答”思路的后端开发者。
2. 系统架构与核心原理:为什么电影问答选检索式而不是生成式
2.1 技术选型:Python 解释器、数据库、相似度算法的确定依据
这个标题指向的系统,最常见的落地组合是 Python 3.8+ 配 Flask 做服务层,MySQL 或 SQLite 做存储层,jieba 做中文分词,再用 TF-IDF 向量加余弦相似度做匹配。为什么不选生成式?因为电影信息问答的事实性极强——问“《盗梦空间》的导演是谁”就只有一个标准答案,生成式模型在离线环境下很难保证准确率,而且还要显卡、要大量训练数据;检索式则是查表,准确率可控,行为可解释。
数据库这一层,课程设计常见 MySQL,单机演示常见 SQLite。MySQL 的好处是数据导入导出方便,文档里能写“使用 Navicat 连接数据库”这类操作,答辩时演示效果更好;SQLite 的好处是零配置、文件即库,代码里用sqlite3标准库就能打开,不用单独装服务。我的建议是,如果文档里写了完整的 SQL 建表和插入脚本,优先 MySQL,因为题目明确带了“数据库”三个字,MySQL 更能体现数据库设计能力;如果只求一键跑通,SQLite 能省掉很多环境问题。
相似度算法上,别一上来就上 BERT。这个场景的数据量通常只有几千条问答对,TF-IDF 加余弦相似度已经能取得不错的效果。核心代码如下:
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity import jieba def tokenize(text): return ' '.join(jieba.cut(text)) questions = [tokenize(q) for q in qa_pairs] # qa_pairs 是问题列表 vectorizer = TfidfVectorizer() question_vectors = vectorizer.fit_transform(questions) user_vec = vectorizer.transform([tokenize(user_question)]) scores = cosine_similarity(user_vec, question_vectors)[0] best_idx = scores.argmax()这段代码里,TfidfVectorizer会把每一句分词后的问句转成一个稀疏向量,cosine_similarity计算用户输入和库里所有问句的夹角余弦。注意到scores.argmax()只取分数最高的下标,这个操作在数据量小的时候没问题,但后面会遇到“所有分数都很低却依然返回答案”的坑,所以实际项目里我会加一个阈值判断。
2.2 数据流:一条问题从输入到答案输出经过的五步
一个标准请求在系统内部走五步:第一步,接收自然语言输入;第二步,用 jieba 分词并去掉停用词;第三步,加载数据库中的问答对并做同样的预处理;第四步,计算余弦相似度,找到最高分;第五步,判断分数是否大于阈值,是则返回对应答案,否则返回“我没听懂,换个说法试试”这类兜底回答。
这个流程里最容易出问题的是第二步和第四步的衔接。很多源码里会有两套分词逻辑:建库时把问答对全部分词存成中间表,查询时再分用户输入。我在复现时更倾向于直接维护一个preprocessed_questions列表,内存里做匹配,省掉一张中间表,因为问答对的量级撑不起数据库索引的复杂度,没必要为了“规范化”把简单事情变复杂。
3. 从零复现最小可运行版本:建库、分词、召回、排序
3.1 环境准备与数据库初始化脚本
拿到标题对应的源码包后,第一步不是看代码,而是先把数据库跑起来。这里给出一份 MySQL 初始化脚本,覆盖电影表、问答对表和标签表三张核心表:
CREATE DATABASE IF NOT EXISTS movie_qa DEFAULT CHARSET utf8mb4; USE movie_qa; CREATE TABLE movie ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, director VARCHAR(50), actors VARCHAR(255), genre VARCHAR(100), rating DECIMAL(3,1), release_year INT, description TEXT ); CREATE TABLE qa_pair ( id INT PRIMARY KEY AUTO_INCREMENT, question VARCHAR(255) NOT NULL, answer TEXT NOT NULL, movie_id INT, FOREIGN KEY (movie_id) REFERENCES movie(id) ); CREATE TABLE movie_tag ( movie_id INT, tag VARCHAR(50), FOREIGN KEY (movie_id) REFERENCES movie(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;utf8mb4必须写,因为电影名和答案里可能有 emoji 或生僻字,老项目的utf8字符集会报 Incorrect string value。rating用DECIMAL(3,1)而不是FLOAT,避免 10.0 变成 10 这类显示问题。qa_pair里的movie_id外键可以让问答对和电影详情关联起来,方便文档里画 ER 图。
3.2 分词与停用词处理:最小可跑的预处理函数
下面这段代码是整条问答链路里改动最频繁的地方,我建议单独放进preprocess.py:
import jieba STOP_WORDS = set(['的', '了', '吗', '呢', '请', '问', '一下', '什么', '怎么', '哪', '部', '个', '是']) def clean_text(text: str) -> str: words = jieba.lcut(text) filtered = [w.strip() for w in words if w.strip() and w not in STOP_WORDS] return ' '.join(filtered) if __name__ == '__main__': print(clean_text('请问《流浪地球》的导演是谁?'))这个函数的输出结果决定了后面的匹配质量。注意jieba.lcut返回的是列表,jieba.cut返回的是生成器,在多次调用场景里lcut更直观。停用词表不要贪大,把提问语气词和疑问代词去掉就够了。如果你发现“导演是谁”里的“是”被去掉后问题变短反而更容易匹配,那就说明停用词表起作用了;但如果“不是”也被拆掉导致语义翻转,就要把“不是”加入白名单。这种细节就是问答系统调优的血泪经验来源。
3.3 核心问答引擎:召回加阈值判断的完整实现
接下来是主逻辑,直接用一个类封装加载、匹配、兜底三件事:
import json, sqlite3 from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity from preprocess import clean_text class MovieQAEngine: def __init__(self, db_path='movie_qa.db'): self.conn = sqlite3.connect(db_path) self.questions, self.answers = self.load_qa() self.vectorizer = TfidfVectorizer() self.vectors = self.vectorizer.fit_transform(self.questions) self.threshold = 0.3 def load_qa(self): cur = self.conn.cursor() cur.execute('SELECT question, answer FROM qa_pair') rows = cur.fetchall() questions = [clean_text(q) for q, a in rows] answers = [a for q, a in rows] return questions, answers def ask(self, user_input: str): q_vec = self.vectorizer.transform([clean_text(user_input)]) scores = cosine_similarity(q_vec, self.vectors)[0] best = scores.argmax() if scores[best] < self.threshold: return '我还不会这个问题,可以换个问法试试。' return self.answers[best]注意几个关键点:fit_transform只在加载问答对时调用一次,用户输入到来时只做transform,否则会把用户问句也混进词表,导致向量维度漂移。threshold=0.3是我在几千条问答对规模下试出来的经验值,数据量翻倍后建议下探到 0.25,否则召回率会明显下降。这个参数是整套系统里最值得调的玄学参数,后面避坑章节会展开讲。
4. 电影数据表设计:字段级别决定问答质量的上限
4.1 核心表结构:问答对和电影详情如何联动
如果你手上的源码包里的数据库只有一张表,那大概率是简化版。它的问答质量会受限,原因是电影领域的问题天然分为两类:一类是事实查询,比如“《教父》的评分是多少”,这类可以直接查movie.rating;另一类是关系查询,比如“诺兰导演的电影有哪些”,这类必须通过movie.director或movie_tag表反查。为了同时支持这两类,表结构必须是“电影详情 + 问答对”双轨制。
一个实用做法是,预生成 800 到 1500 条问答对覆盖高频问题类型,同时保留原始电影表供 SQL 查询兜底。比如用户问“科幻电影有哪些”,如果问答对库里没有现成的组合,就降级成 SQL:
SELECT title FROM movie WHERE genre LIKE '%科幻%' ORDER BY rating DESC LIMIT 5;这个降级逻辑可以放在ask方法里,当相似度低于阈值时尝试走 SQL 模板匹配。很多源码包不会给你写这条路径,但我建议你补上,因为答辩时“如果问答对命中不了,系统还能基于结构化数据回答”是很加分的亮点。
4.2 准备数据:爬来的数据要清洗,手工数据要扩写
数据库里的电影数据来源一般有两个:爬虫抓取和手工整理。爬虫抓的豆瓣或 TMDB 数据最大问题不是版权,而是脏数据:演员名里带空格、年份格式不统一、评分字段混入“暂无评分”字符串。我在处理时一般写三步清洗脚本:第一步统一编码为 utf8mb4,第二步把“暂无评分”“N/A”替换成 0.0 并标记不可用,第三步把演员列表用 | 分隔而不是逗号,因为电影对白里经常出现逗号,导入 SQL 时容易串列。
手工整理数据则要扩写,不要只写电影名和导演,每个问答对尽量覆盖“评分、上映年份、演员、剧情简介、奖项”五个维度。我见过最省力的做法是拿 JSON 文件做中转,用 Python 批量生成INSERT语句:
movies = json.load(open('movies.json', encoding='utf-8')) with open('insert_movie.sql', 'w', encoding='utf-8') as f: for m in movies: f.write( f"INSERT INTO movie (title, director, actors, genre, rating, release_year, description) " f"VALUES ('{m['title']}', '{m['director']}', '{m['actors']}', " f"'{m['genre']}', {m['rating']}, {m['year']}, '{m['desc']}');\n" )这里有个细节:f-string 直接拼接 SQL 有注入风险,但在本地课程设计场景里可以接受,而且写进文档反而容易解释。如果你要做得严谨,用execute加参数占位符才是正确姿势。我通常两种写法都保留,文档里写明“生产环境禁止用 f-string 拼 SQL”,这能体现对安全边界的认知。
5. 常见问题避坑:为什么你的电影问答系统总是答非所问
5.1 现象:问“评分最高的电影”,返回的是电影简介
原因很直白:问答对库里没有匹配的问题模板,但相似度最高的一条仍然是某个无关问答对的变体,系统不管三七二十一返回了它。解决方法是强制加阈值判断,阈值低于 0.2 时直接走 SQL 兜底模版,而不是硬答。这属于检索式问答的通病,不做阈值等于没有拒答能力。
if scores[best] < 0.2: sql_answer = self.sql_fallback(user_input) return sql_answer if sql_answer else '抱歉,目前我还不会回答这个问题。'sql_fallback里你可以维护一组规则,比如“最高分”对应ORDER BY rating DESC LIMIT 1,这个函数在源码包里不一定存在,但它是把项目从“玩具”拉到“演示可用”的关键补丁。
5.2 现象:输入“导演是谁”和库里的“导演是谁”字面一样,相似度却是 0
这是分词不一致导致的翻车现场。常见原因有两处:一处是建库时存的是原始问句,匹配时才做clean_text,导致两边分词结果不同;另一处是停用词表里既有“是”又有“谁”,把关键词全部滤光后剩下的空字符串无法计算相似度。解决方法是把所有问句在加载阶段就做统一预处理,并保证停用词过滤后字符串非空。
filtered = [w for w in jieba.lcut(text) if w.strip() and w not in STOP_WORDS] if not filtered: return '该问题是纯停用词,无法匹配'这行防御代码要放在分词函数的最后,避免空输入直接进TfidfVectorizer导致报错。
5.3 现象:MySQL 导入 SQL 文件时中文全变问号
原因几乎可以锁定:数据库连接字符集不是 utf8mb4。用 Navicat 导入时,连接设置里的“编码”要选 UTF-8;用命令行导入时,执行前先加SET NAMES utf8mb4;。再补一句,数据库本身、表、连接、客户端四个位置字符集不一致就会出现各种乱码问题,检查顺序就是这四层。
5.4 现象:运行源码包里的app.py立刻闪退或提示端口被占用
源码包里的 Web 服务默认端口通常是 5000,macOS 的 AirPlay 接收器也占 5000,这是最常见的闪退原因。解决:换 8080 或 8000,不要纠结为什么源码默认端口不对。另一个闪退原因是缺少依赖包,pip install -r requirements.txt装完仍然闪退的话,把报错信息里最后一个 ModuleNotFoundError 单独拿去搜索,比整体搜索更高效。
5.5 现象:同义词问题——“哪部电影得分高”和“评分最高的片”匹配不到
解决这类问题不能全靠相似度算法,要在预处理层加同义词替换表。比如“得分高”“评分高”“口碑好”都统一替换成“高评分”。这个替换表维护成本低,但对效果提升非常明显,属于性价比最高的调优手段。
6. 把问答系统做深一层:权重调整、Web 界面与量化验证
6.1 相似度加权:让“问题模板”比“普通描述”更高优先级
基础版系统里,所有问答对权重相等。但用户真正高频问的是“评分”“导演”“上映时间”,我们可以把这类问题的向量乘以一个放大系数。做法是在ask里对不同模板打标签,对命中高频标签的问题相似度乘以 1.2:
tag_boost = {'导演': 1.2, '评分': 1.2, '上映': 1.1} label = detect_label(user_input) if label in tag_boost and scores[best] > 0.1: scores[best] *= tag_boost[label]这个乘法的尺度要小,1.2 已经是上限,乘大了会淹没真正的文本相似度,导致系统只认关键词不认语义。这也是我反复调过的参数,实践证明:加权是给“犹豫不决”的匹配一个倾向,不是给错误答案开绿灯。
6.2 用 Flask 包一层 Web 界面:十分钟把控制台变成可演示服务
问答引擎本身是控制台程序,但答辩演示一定要有界面。最小实现是 Flask 加一个文本框:
from flask import Flask, request, jsonify from engine import MovieQAEngine app = Flask(__name__) engine = MovieQAEngine() @app.route('/ask', methods=['POST']) def ask(): data = request.get_json() question = data.get('question', '') answer = engine.ask(question) return jsonify({'answer': answer, 'question': question}) if __name__ == '__main__': app.run(host='0.0.0.0', port=8000, debug=False)用curl -X POST http://localhost:8000/ask -H 'Content-Type: application/json' -d '{"question": "《流浪地球》好看吗"}'就能验证接口。这里要把debug=False,原因是你不想让 Werkzeug 的重载器把数据库连接加载两遍,导致初始化时抛出database is locked的 SQLite 错误。
6.3 做一次量化验证:用 50 条手工问题算准确率
没有量化就没有调优依据。我一般会准备 50 条用户可能问的问题,对每条跑一次系统,按“回答正确、回答错误、拒答”三档打标,计算两个数字:正确率(回答正确数除以总问题数)和召回率(非拒答数除以总问题数)。课程设计文档里放上这个表格,说服力远高于“经测试,本系统运行正常”这类空话。做完这轮验证,你会发现 80% 的错误集中在两种问题上:一是模板稀疏(库里没有对应问法),二是同义词未替换。这两个问题的修复成本都不高,但它们暴露出的恰恰是检索式问答系统的边界所在。
另外如果你想让系统再进阶一步,可以加上“问题推荐”功能:当用户问的问题相似度低于阈值时,把相似度最高的三条历史问题展示出来,类似“你是不是想问这些”。这比强行作答更体面,也是网易云音乐评论区里那种“猜你想问”的简化实现,作为拓展功能写进文档说明里,能让整个项目的完整度上一个台阶。
我做这类课程设计项目的习惯是:第一版永远是能跑通的最小系统,第二版才看准确率,第三版才补界面和文档。别一开始就想把 BERT 接上去,检索式的天花板很低,但作为 Python 问答系统落地的第一步,它是你理解“用户输入到结构化答案”全链路最低成本的方式。希望这份思路能帮你把手头的源码包讲清楚、改明白,并且在自己的项目文档里写出不心虚的结论。
本文还有配套的精品资源,点击获取