
简介基于Python与Django框架实现的电影信息智能问答系统项目面向计算机相关专业学生、毕业设计开发者以及NLP入门学习者。项目以电影知识问答为核心涵盖问句预处理、问题分类、模板匹配、答案生成等完整流程并配有SQLite数据库与基础电影数据可作为毕业设计、课程作业或二次开发的起点。资源内含67个文件压缩包约875KB以18个Python源码为主另有Django配置、HTML/CSS/JS静态页面、XML配置文件、SQLite数据库及说明文档结构清晰便于按模块阅读和运行调试。目前已有78人浏览学习。代码经过测试可运行下载后可参考README快速启动也可在此基础上修改扩展用于理解基于模板的问答系统实现思路。1. 电影信息智能问答系统到底在解决什么问题从一句问话到一条 SQL“基于Python实现的电影信息智能问答系统”这个名字听起来很唬人拆开看它要解决的事其实非常具体输入一句“霸王别姬是谁导演的”系统返回“陈凯歌”输入“评分最高的悬疑片”系统返回对应的电影列表。它的核心不是让模型去“理解”语义而是把自然语言翻译成一条可执行的结构化查询再对着电影数据库把答案捞回来。这个方案适合做课程设计、毕业设计的同学也适合手里有一份电影JSON或CSV数据、想快速搭一个可演示问答原型的从业者——它能让你在一周内做出一个不依赖在线大模型、可本地运行、可解释性强的成品。我为什么强调“不依赖在线大模型”因为这类项目最常见的失败点不是算法不够新而是环境依赖太重、模型太大、现场演示时网络一断就哑火。2. 先把数据铺平电影库的建表设计、导入与文件组织2.1 为什么选 MySQL 而不是 SQLite 或 Redis这个系统最吃“稳定交付”问答系统的底层是一个结构化电影库选型第一原则是“别人拿到源代码后能直接跑起来”。我一般会选 MySQL理由很简单课程设计和工程交付场景里MySQL 5.7/8.0 是出镜率最高的数据库老师或同事的机器上大概率装过即便没装安装和排错资料也最多。SQLite 虽然零配置但你在文档说明里写“嵌入式数据库”会显得项目分量不够而且它在多线程写入时容易出现锁等待Redis 适合做缓存和热数据索引但让一个问答系统把全量电影数据放 Redis 并不合适毕竟电影库本身才几万条远没到需要分布式缓存的程度。数据量级在这里决定架构复杂度。一万部电影每部十几个字段MySQL 建一张表就够问答阶段把全表载入内存做成字典单次查询时间在毫秒级。有人会纠结“要不要上 Elasticsearch”我的看法是没必要——你只有一万条数据ES 的索引优势完全发挥不出来反而把部署复杂度拉高了一个档次。先想清楚数据规模再决定要不要引入重型组件。存储引擎用 InnoDB字符集用 utf8mb4这两条几乎是固定答案。InnoDB 支持事务和行级锁导入数据时即使中途报错回滚也方便utf8mb4 能存下 emoji 和生僻字避免电影名里的特殊字符让入库直接失败。表结构设计也要考虑“问答系统要查什么”而不是“电影信息要存什么”——字段够用即可不用堆太多。2.2 建表与字段挡住 80% 中文乱码和空值问题的设计电影信息问答系统需要支撑的问题类型基本决定了表字段。需要支持的问法包括导演、主演、上映年份、类型、评分、片长、简介。把这几类问题对应的字段列出来表结构自然就出来了CREATE DATABASE IF NOT EXISTS movie_qa DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE movie_qa; DROP TABLE IF EXISTS movie; CREATE TABLE movie ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(128) NOT NULL COMMENT 电影名, director VARCHAR(64) DEFAULT NULL COMMENT 导演, actors VARCHAR(255) DEFAULT NULL COMMENT 主演多个用顿号分隔, genre VARCHAR(64) DEFAULT NULL COMMENT 类型多个用顿号分隔, year SMALLINT UNSIGNED DEFAULT NULL COMMENT 上映年份, rating DECIMAL(3,1) DEFAULT NULL COMMENT IMDb/豆瓣评分, runtime SMALLINT UNSIGNED DEFAULT NULL COMMENT 片长单位分钟, synopsis VARCHAR(1000) DEFAULT NULL COMMENT 一句话剧情简介, PRIMARY KEY (id), UNIQUE KEY uk_title (title) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建库语句里前后两处字符集有的人只写建库不写建表导致表还是继承默认的 latin1中文入库后直接变成问号。我的习惯是库和表都显式指定 utf8mb4。COLLATE 用 utf8mb4_unicode_ci它对比中文和英文字符时足够准排序规则也更接近 UTF-8 的通用定义。title 加 UNIQUE KEY 的作用不是省空间是为了让导入脚本可以幂等重跑。电影名是天然的业务主键同一部电影重复入库时靠唯一索引做“存在则更新”比先查一次再插入要省事得多。字段注释也建议写全文档说明里可以直接复用这份注释老师或同事阅读代码时能少问很多问题。2.3 批量导入从 CSV 到 MySQL 的幂等脚本常见做法是从公开数据集下载 CSV或者用 python 爬虫抓一份列表然后统一清洗入库。导入脚本用 pymysql 写一个方法核心逻辑是“每行一条 INSERT遇到重复电影名则更新部分字段”这样脚本跑两遍也不会产生脏数据import csv import pymysql conn pymysql.connect( host127.0.0.1, port3306, userroot, password你的密码, databasemovie_qa, charsetutf8mb4, ) def import_movies(csv_path: str) - int: count 0 with conn.cursor() as cur: with open(csv_path, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: # 空字符串统一转 None避免库里出现大量 row {k: (v.strip() if v and v.strip() else None) for k, v in row.items()} sql ( INSERT INTO movie (title, director, actors, genre, year, rating, runtime, synopsis) VALUES (%(title)s, %(director)s, %(actors)s, %(genre)s, %(year)s, %(rating)s, %(runtime)s, %(synopsis)s) ON DUPLICATE KEY UPDATE directorVALUES(director), ratingVALUES(rating) ) cur.execute(sql, row) count 1 conn.commit() return count代码里有三个细节值得说。第一文件打开用 encodingutf-8-sig原因是 CSV 常常带 UTF-8 BOM 头不去掉的话第一列字段名会变成“\ufefftitle”DictReader 匹配列名时直接翻车。第二SQL 参数用字典而不是 f-string 拼接既防注入也让字段和值一一对应排查少一个参数时一目了然。第三ON DUPLICATE KEY UPDATE 里我只更新 director 和 rating这相当于“重复导入时只刷新部分热门字段”actors、synopsis 这类长文本字段不会被旧数据覆盖。如果你希望整行覆盖把字段列全即可。2.4 数据来源与清洗python 爬虫拿到的数据不能直接入库导入前最容易被忽略的是数据清洗。从公开站点爬来的电影信息字段格式几乎不可能直接入库年份可能长成“2019-04-01”评分长成“8.7/10”类型长成“剧情 / 爱情”带空格和斜杠。你需要在写入之前把这些统一成表结构约定的格式一个很省事的做法是写一个 normalize 函数在 import_movies 的循环里先过一遍。常见坑是“豆瓣评分 8.7”这种带中文后缀的字符串如果直接用 DECIMAL 字段接收会报错需要用正则把数字部分抠出来。数据来源方面我一般会用 python 爬虫从公开的电影资料站点抓取也可以直接用网上整理好的 CSV。但不管来源是什么入库前都要过一遍 dedupe同样一部电影不同站点可能一个写“霸王别姬”一个写“霸王别姬 (1993)”后者会被当成另一部电影。简单的做法是把标题里的年份括号部分去掉再比较更稳的做法是同时比对导演和年份字段。这些清洗规则建议写进文档说明因为老师问你“数据哪里来的”的时候清洗过程比爬虫代码本身更能体现工作量。3. 问答系统的“理解”是怎么实现的实体识别 规则分类器3.1 不用余弦相似度的理由电影名分词才是最大的翻车点很多人拿到“智能问答”第一反应是“做语义相似度”把问题转成 TF-IDF 向量和库里的句子算余弦相似度取得分最高的当答案。这个思路在小规模测试里能跑但放到电影问答场景会撞上两个硬伤。第一电影名是专有名词jieba 默认词典不认识“霸王别姬”会把“霸王”和“别姬”切开向量化之后“霸王”被匹配到其他含“霸王”字样的内容上召回完全失控。第二相似度检索返回的是“句子”但用户要的是“字段值”——问“导演是谁”你要给的是一个人名不是一段相似文本中间还得再套一层抽取逻辑兜兜转转绕回来了。所以常见做法是“实体识别 规则分类器”这也是这类课程项目里最稳的方案。先把问题里的电影名抽出来再判断问的是导演、演员还是评分最后映射到一条 SQL。整个过程可解释、可测试、可排错而且不依赖外网模型演示时只要 MySQL 在本地就永远能跑通。3.2 让 jieba 认出电影名自定义字典的生成与加载问题里最先要被提取的是电影名而 jieba 默认词典对电影名基本没有覆盖。解决办法是把所有电影名导出来生成一个自定义词典加载后再分词“霸王别姬”就会作为一个完整词出现import jieba jieba.load_userdict(data/movie_names.txt)movie_names.txt 每行一个电影名。如果想顺便控制词性和词频可以写成“霸王别姬 10 nz”这种带空格的三段式格式词频数字填 10 以上词性填 nz专有名词或 nr。实际写作里我建议只用电影名不带频率让 jieba 自己根据词频统计处理减少格式出错的可能。这个词典文件必须从数据库里生成而不是手写否则库里新增了电影词典没同步更新问答系统会出现“数据库里有但答不出来”的怪现象。生成脚本也很简单import pymysql def dump_movie_dict(out_path: str data/movie_names.txt) - int: conn pymysql.connect( host127.0.0.1, port3306, userroot, password你的密码, databasemovie_qa, charsetutf8mb4, ) with conn.cursor() as cur: cur.execute(SELECT title FROM movie) titles [r[0] for r in cur.fetchall() if r[0]] with open(out_path, w, encodingutf-8) as f: f.write(\n.join(titles)) return len(titles)这里有个顺序问题需要强调先导库再生成词典再依赖加载。如果你在一开始就 load_userdict但库里还没数据词典为空文件jieba 会直接跳过之后即使库里有了数据也不会自动重新读 file 了。最稳妥的做法是把这两步拆成两个脚本做成“初始化流程”里的一先一后。3.3 问题分类器把“导演是谁”映射成 SQL 字段“智能”的另一半是判断用户在问什么。常见做法是维护一张规则表用关键词命中来做意图分类优先级从上往下取第一个命中的。问“霸王别姬的导演是谁”命中“导演是谁”返回 director问“这电影谁演的”命中“演员”返回 actorsdef classify_question(question: str) - str: rules [ (director, (导演是谁, 谁导演, 导演是, 谁拍的, 谁执导)), (actors, (主演, 演员, 谁演的, 谁主演, 饰演)), (year, (哪一年上映, 上映年份, 什么时候上映, 哪年)), (genre, (什么类型, 类型是, 属于什么类型, 分类)), (rating, (评分, 豆瓣评分, 几分, 多少分)), (runtime, (时长, 多长, 片长, 多少分钟)), (synopsis, (简介, 剧情简介, 讲了什么, 讲的什么)), ] for qtype, keywords in rules: for kw in keywords: if kw in question: return qtype return unknown规则的顺序不是随便排的优先级越高越靠前。比如“评分最高的悬疑片”同时含“评分”和“类型”如果 rating 排在前面先命中系统会去查某部电影的评分但实际这是一个“找列表”的统计问题后面 4.2 会专门说处理办法。设计成“先命中就先得”配合 whitelist 式的关键词比一次性把所有逻辑揉进一个函数好维护得多。3.4 电影名抽取分词命中与兜底策略意图识别和实体抽取是两个独立步骤顺序我一般先抽实体再判断意图。电影名抽取用上面生成的词表把问题交给 jieba 切词再逐个和电影名索引比对# 全局电影名索引加载完成后用于常数级匹配 movie_index {m[title]: m for m in load_all_movies()} _KNOWN_TITLES set(movie_index.keys()) def extract_movie(question: str) - str | None: words jieba.lcut(question) for w in words: w w.strip() if w in _KNOWN_TITLES: return w return None这个函数有两条分支让人容易踩坑。第一jieba.lcut 返回的列表里可能在词首词尾带空格或标点所以每轮都要 strip 一下。第二如果电影名是四个字以上比如“夏洛特烦恼”一旦自定义词典没加载成功jieba 会切出“夏洛特”和“烦恼”两个词都不在标题集合里extract_movie 返回 None。排查时先打印 jieba.lcut(question) 看看切成了什么就能判断是不是词典加载失效。兜底手段是“整句去掉疑问词后直接匹配”例如把“霸王别姬的导演是谁”去掉“的导演是谁”再与标题比对但这是一个模糊策略尽量留到规则无法处理时再用。4. 问答闭环从查库、格式化到高频问法覆盖4.1 一个能直接跑的回答函数查库、格式化、兜底有了实体和意图最后一步是把两者拼起来查库并组织答案。我习惯把所有逻辑收在一个 answer 函数里对外只暴露一个入口def answer(question: str) - str: qtype classify_question(question) title extract_movie(question) if title is None: return handle_stat_or_general(question, qtype) movie movie_index[title] field_map { director: (导演, movie.get(director)), actors: (主演, movie.get(actors)), year: (上映年份, movie.get(year)), genre: (类型, movie.get(genre)), rating: (评分, movie.get(rating)), runtime: (片长, movie.get(runtime)), synopsis: (剧情简介, movie.get(synopsis)), } if qtype in field_map: label, value field_map[qtype] if value: return f{title}的{label}是{value} return f抱歉库里还没有收录{title}的{label}信息 return 这个问题我暂时没有学会你可以试试问导演、主演、评分、上映年份。query 阶段不再连数据库而是直接查内存里的 movie_index这是速度快的关键。一万部电影的 dict 查找是常数级即使加上分词耗时整体响应也在几十毫秒内足够现场演示。field_map 把“意图类型”和“显示标签取值”绑定在一起新增一个可回答的字段只需要在这里加一行。注意一个问题如果用户连着问“那评分呢”没有带上电影名extract_movie 一定返回 None这时直接走 handle_stat_or_general它负责两件事——处理“评分最高的悬疑片”这类统计问题以及提示“你说的是哪部电影”。你可以在 session 里存上一次命中的电影名让“那评分呢”这种带指代的问题也能答上。这个功能属于加分项但对提升演示效果非常明显。4.2 高频问法覆盖表别名、无标点、倒序都怎么处理用户不会按你代码里的规则说话这是问答系统玄学最多的部分。同一个“霸王别姬的导演是谁”真实环境里会演化出好几种写法我把最常遇到的整理成一个覆盖表按这个表设计测试用例问法示例意图实际处理结果霸王别姬的导演是谁director命中“导演是谁”抽取“霸王别姬”霸王别姬 导演是谁无标点director分词后直接命中不需要特殊处理导演是谁 霸王别姬倒序director分类器先命中“导演是谁”抽取时扫到“霸王别姬”霸王别姬谁拍的director命中“谁拍的”关键词霸王别姬是哪一年上映的year命中“哪一年上映”评分最高的悬疑片rating genre 组合不走单值查询走统计路由霸王别姬 评分rating命中“评分”无标识符也能处理倒序问题的关键在于意图分类和实体抽取互不干扰。classify_question 只关心关键词是否在字符串里extract_movie 只关心分词结果是否命中标题集合两个函数都做“包含判断”而不是“顺序判断”所以“导演是谁霸王别姬”也能命中。这个设计比正则写 ORDER 顺序要省事得多代价是“张艺谋导演的电影有哪些”这类反查问题会全部落空——反查需要知道“张艺谋”是人名而非电影名需要另外维护导演名词表属于第 4.3 节的统计路由职责不在单值问答范围内。4.3 内存索引与 MySQL 的分工什么时候该走 SQL单部电影的信息查询走内存 dict列表类、统计类查询走 MySQL这是我给这类系统定的边界。理由很简单内存 dict 只适合“给定电影名取字段”的精确查询而“评分最高的悬疑片”要在 genre 里做模糊匹配再按 rating 排序取前几条用 Python 遍历一万条也能跑但既然有数据库用 SQL 表达更清晰也更有“数据库”的设计感def search_by_genre_top(genre: str, limit: int 5): conn get_conn() with conn.cursor() as cur: sql ( SELECT title, rating FROM movie WHERE genre LIKE %s ORDER BY rating DESC, year DESC LIMIT %s ) cur.execute(sql, (f%{genre}%, limit)) return cur.fetchall()LIKE %genre% 在这里完全可以接受因为数据量才一万条全表扫一次不过几毫秒不用上全文索引。ORDER BY rating DESC, year DESC 让评分相同的情况下优先展示较新的电影这个排序细节会让结果看起来“聪明”很多。统计路由函数里要做的判断是问题里是否包含“最高/最低/前/最老/最近”这类词且没有明确的电影名实体有就进 SQL 分支没有就走“请告诉我是哪部电影”的兜底话术。5. 避坑记录电影问答系统最容易翻车的 4 个地方5.1 中文乱码建库、连接、导入三处都要显式指定现象导入脚本跑完SELECT 一看电影名全是“”或者报错 “Incorrect string value: ‘\xE9\xBE\x99...’ for column ‘title’”。原因三层都有嫌疑。建库时没指定 utf8mb4表继承了 latin1或者 pymysql.connect 没传 charsetutf8mb4”或者 CSV 文件带 BOM被 DictReader 读出了残缺字段名。这三个问题经常同时出现但报错信息只指向其中一层容易让人误判。解决直接在初始化流程里把三处固定下来。建库 SQL 写死 DEFAULT CHARACTER SET utf8mb4pymysql.connect 显式加 charsetutf8mb4文件打开用 encodingutf-8-sig。写完脚本后不要看一眼就过先往库里插一条“霸王别姬”再查出来确认这一步 30 秒能挡住后面一整天的血泪。5.2 “霸王别姬”被 jieba 切碎自定义词典的常见失效姿势现象extract_movie 返回 None可你明明在词典文件里写上了“霸王别姬”。原因最常见的是加载顺序错了。有的同学把 load_userdict 放在程序开头但词典文件当时还没生成或生成失败jieba 静默跳过不报错。第二个常见原因是词典文件编码不对Windows 记事本默认可能存成 GBKjieba 读出来全是乱码词。第三个原因是你用的是 jieba.analyse 接口而不是 jieba.lcut前者不一定读取 userdict。解决把“生成词典”和“加载词典”做成两个单独步骤在启动脚本里按顺序执行。排查时直接打印 jieba.lcut(霸王别姬是谁导演的)如果输出是“霸王 / 别姬”说明词典没生效如果输出是“霸王别姬 / 是 / 谁 / 导演 / 的”说明已经正常。确认编码时用file data/movie_names.txt看一行输出出现 UTF-8 字样才放心。5.3 统计型问题被意图分类器误判评分最高 vs 评分是多少现象用户问“评分最高的悬疑片”系统返回“霸王别姬的评分是 9.6”完全答非所问。原因classify_question 只看关键词先命中了“评分”就当成单值问题处理。它没有区分“查某部电影的评分”和“按评分找电影列表”。解决在 answer 函数里先判断是否属于统计路由再走单值字段。统计路由的触发条件是问题里含“最高、最低、前 N、最老、最近”等排序词且 extract_movie 返回 None。命中统计路由就直接进数据库查询不再用 classify_question 的结果。注意把统计判断放在“识别到电影名”之后这样“霸王别姬评分最高的版本是哪年”这类混合问题还能优先按电影名单值处理。5.4 演示现场失联连接配置与依赖清单没有一起交付现象“导入电影失败pymysql 报 2003 Can’t connect to MySQL server”。演示现场十有八九是这个错。原因数据库连接密码写死在脚本里但现场机器上 MySQL 密码不一致或者 MySQL 服务没启动、端口被防火墙挡了。这不算 bug是交付时没把“别人怎么跑起来”的路铺好。解决把 host、user、password、database 统一收敛到一个 config.py 或环境变量文件里脚本全部从配置读取requirements.txt 里写全 pymysql、jieba、pandas 的版本再提供一个 start.sh 按顺序执行“建库→建表→导入→生成词典→启动问答”。做这一步的本质是把“我能跑”变成“你也能跑”文档说明里最有含金量的就是这个启动顺序和每条命令对应的报错排查方法。6. 交付一个能现场演示的完整项目验证脚本与文档说明怎么写6.1 回归验证脚本让你的问答对是可重复的项目交付前我会写一个纯 Python 的回归测试脚本把高频问法覆盖表里的问法变成可断言的真值表。这样每次改分类器或词典跑一遍就知道有没有改坏原来的功能CASES [ (霸王别姬的导演是谁, director, 霸王别姬), (霸王别姬谁拍的, director, 霸王别姬), (这个电影的评分多少, rating, None), # 缺实体期望走兜底 ] def run_tests() - None: passed 0 for question, expected_type, expected_title in CASES: qtype classify_question(question) title extract_movie(question) ok (qtype expected_type) and (title expected_title) print((PASS if ok else FAIL), question, qtype, title) passed int(ok) print(f通过 {passed}/{len(CASES)})这个测试脚本的价值不在于覆盖率而在于“可复现”。现场演示前花十秒跑一遍确认核心路径没有翻车比临场碰运气安心得多。把测试用例和电影数据一起放进源代码包对方也能通过跑测试验证系统真的可用。6.2 README 文档说明五段式结构就够了文档说明不需要长篇大论但要做到“按顺序执行就能跑通”。我一般按五段来写环境要求Python 版本、MySQL 版本、初始化步骤建库、建表、导入、生成词典、启动方式入口文件、端口、测试用例运行回归脚本、预期结果、项目结构源码目录里每个文件是干什么的。整个包的核心文件大致是建表 SQL、导入脚本、词典生成脚本、问答主程序、回归测试、requirements.txt、start.sh。把这几个文件组织好源代码文档说明数据库三件套就齐了。我自己在做类似问答项目时最后悔的一件事是在第一天没有把编码、自定义词典、统计路由三个基础问题定好导致后面每一轮调试都在跟翻车现场周旋。如果你在建表时就按 utf8mb4 写入、让词典从数据库自动生成、把统计问题单独分流这套系统会顺利得超出预期。希望帮到你。本文还有配套的精品资源点击获取