资讯动态

电影知识图谱实战:CSV到Cypher的完整问答系统

发布时间:2026/10/3 17:59:29 来源:尧图企业网站定制
简介这是一套面向高校计算机及相关专业学生如人工智能、自动化、电子信息等的毕业设计级知识图谱实践项目聚焦电影领域问答场景解决结构化语义查询与自然语言理解落地问题。资源共45个文件涵盖7个核心Python脚本含question_classification、process_question、data2neo4j等模块、5个CSV数据集movie、genre、person及关联表、5张PNG系统架构与流程图、3类文本资源vocabulary、label、question_template等以及设计文档.doc、说明文档.md、前端页面HTML/CSS/JS和IDE配置文件整体压缩包仅1.19MB轻量易部署。已有71人学习下载资源经严格测试可直接运行附详细设计报告与模块化代码结构便于理解知识图谱构建、Cypher查询映射、问题分类与模板匹配全流程同时支持小白入门学习与进阶二次开发如迁移至其他垂直领域或扩展多轮对话功能。1. 这不是个“问答Demo”而是一套能跑通从CSV到Cypher查询的完整知识图谱闭环电影领域可复现、可调试、可扩展的毕业设计级工程你手头那份标着“PythonNeo4j数据集”的.zip包别急着解压——它不是教你怎么写MATCH (m:Movie) RETURN m.title的Neo4j入门练习而是一套真实走完知识图谱全链路的工业级轻量实现从原始movie.csv/person.csv等6张结构化表格出发用data2neo4j.py脚本批量建节点、建关系用process_question.py做中文问句分词实体识别没用BERT但用了结巴自定义词典用question_classification.py把“周星驰演过哪些喜剧”和“豆瓣评分高于8.5的科幻片有哪些”归到不同模板最后通过server.py暴露HTTP接口前端index.html输入自然语言后端返回带高亮的Neo4j查询结果。它不炫技但每一步都留了日志、写了注释、配了requirements.txt它没上Docker但neo4j版本锁死在4.4.30社区版避免你装完最新版发现apoc插件不兼容它甚至把userdict3.txt里“阿凡达”“泰坦尼克号”这些电影名加了词性标注就为了process_question.py里一句jieba.load_userdict(userdict3.txt)能真正生效。如果你正卡在毕设开题——查不到可用的中文电影KG数据、调不通Neo4j Python驱动、分不清MERGE和CREATE的边界、或者被导师问“你的问答逻辑怎么跟图谱结构对齐”时哑口无言——这份资源就是你该立刻解压、逐行读test.py验证、然后照着改自己数据的最小可行知识图谱基线系统。2. 从6个CSV文件到Neo4j图数据库data2neo4j.py的建模逻辑与可复用的数据清洗脚本2.1 为什么选这6张表——电影领域知识图谱的三类核心实体与两类关键关系项目里的movie.csv、genre.csv、person.csv、person_to_movie.csv、movie_to_genre.csv不是随意凑数。它们对应知识图谱建模中最基础也最关键的三元组结构实体层NodesMovie电影、Genre类型、Person人物是三个独立节点类型各自有明确属性如Movie.title、Person.name、Genre.name关系层Relationshipsperson_to_movie.csv定义ACTED_IN/DIRECTED/WRITTEN_BY等带方向的关系movie_to_genre.csv定义BELONGS_TO关系设计意图这种拆分规避了“电影-类型-导演-演员”全塞进一张大表导致的冗余和查询爆炸。比如查“王家卫导演的文艺片”只需MATCH (d:Person)-[:DIRECTED]-(m:Movie)-[:BELONGS_TO]-(g:Genre) WHERE d.name王家卫 AND g.name文艺路径清晰、索引友好。提示genre.csv只有genre_id,genre_name两列但movie_to_genre.csv里movie_id,genre_id是外键关联——这是典型的“多对多关系中间表”设计比在movie.csv里存genres:爱情|喜剧|剧情字符串更利于图查询。2.2data2neo4j.py执行流程分步导入、事务控制、错误跳过机制该脚本是整个项目的数据基建中枢它不依赖任何ETL工具纯PythonNeo4j Driver实现。核心逻辑分三步清空旧库可选driver.execute_query(MATCH (n) DETACH DELETE n)—— 注意生产环境务必注释掉这行此处仅用于毕设演示重置批量创建节点对每张实体表movie.csv等用UNWIND $rows AS row CREATE (:Movie {title:row.title, year:row.year})Cypher语句配合session.run()传入字典列表批量创建关系对关系表person_to_movie.csv先查出Person和Movie节点ID再用MERGE (p:Person)-[r:ACTED_IN]-(m:Movie)建立连接# data2neo4j.py 关键片段已简化 from neo4j import GraphDatabase def create_nodes_from_csv(session, csv_path, label, props): # 读取CSV转换为字典列表 rows [] with open(csv_path, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: # 清洗空值将空字符串转为None避免Neo4j报错 cleaned_row {k: (v if v.strip() else None) for k, v in row.items()} rows.append(cleaned_row) # 批量执行CypherUNWIND CREATE query fUNWIND $rows AS row CREATE (:{label} {{ {, .join([f{k}:row.{k} for k in props])} }}) session.run(query, rowsrows) # 调用示例创建Movie节点 create_nodes_from_csv(session, data/movie.csv, Movie, [title, year, rating])参数说明sessionNeo4j Driver的会话对象确保事务安全csv_path绝对路径注意Windows下反斜杠需转义或用raw stringlabel节点标签名如Movie必须与后续查询一致propsCSV列名列表如[title,year]决定节点属性字段2.3 实体属性映射表为什么movie.csv里year字段要转成整型观察movie.csv原始数据可打开查看movie_id,title,year,rating,director,actors 1,阿凡达,2009,7.8,詹姆斯·卡梅隆,萨姆·沃辛顿|佐伊·索尔达娜year列是数字字符串但Neo4j中2009整型和2009字符串在WHERE m.year 2010查询时行为不同。data2neo4j.py在读取时做了隐式转换# data2neo4j.py 中的类型转换逻辑 if col year and row[col].strip(): try: row[col] int(row[col].strip()) # 强制转int except ValueError: row[col] None # 转换失败则置空为什么必须做图数据库索引对数值型字段优化更好WHERE m.year 2020能走索引而WHERE m.year 2020可能全表扫描避免后续问答模块中2009 ! 2009导致条件匹配失败比如问“2010年以后的电影”却漏掉year:2015的节点2.4 关系类型动态推断person_to_movie.csv如何区分ACTED_IN和DIRECTEDperson_to_movie.csv结构如下person_id,movie_id,role 101,1,director 102,1,actor 103,2,writerdata2neo4j.py没有硬编码role到关系类型的映射而是用字典配置ROLE_TO_RELATION { director: DIRECTED, actor: ACTED_IN, writer: WRITTEN_BY, producer: PRODUCED } # 在循环处理person_to_movie.csv时 for row in csv_reader: rel_type ROLE_TO_RELATION.get(row[role].lower(), RELATED_TO) query f MATCH (p:Person {{id: $person_id}}), (m:Movie {{id: $movie_id}}) MERGE (p)-[:{rel_type}]-(m) session.run(query, person_idrow[person_id], movie_idrow[movie_id])这个设计的价值在于你替换自己的数据时只需修改ROLE_TO_RELATION字典无需改Cypher语句支持同一张关系表承载多种语义避免为每种关系建单独CSV如director_to_movie.csvactor_to_movie.csv3. 中文问句理解process_question.py的分词、实体识别与槽位填充实战3.1 为什么不用BERT/ERNIE——轻量级问答系统的词典规则双引擎设计项目没引入任何预训练模型而是用jieba分词 userdict3.txt自定义词典 正则规则匹配原因很现实毕设答辩现场演示要求启动快、依赖少、显存零占用不用GPU电影领域词汇高度结构化“阿凡达”“泰坦尼克号”“周星驰”“豆瓣评分”都是固定名词规则覆盖率达92%jieba的cut_for_search()模式对长尾词如“速度与激情7”切分更准比cut()更适合问答场景userdict3.txt内容示例阿凡达 nz 100 泰坦尼克号 nz 100 周星驰 nr 100 豆瓣评分 nz 100 IMDb评分 nz 100nz专有名词nr人名100为词频权重——jieba.load_userdict(userdict3.txt)后“阿凡达”不再被切成“阿/凡/达”。3.2process_question.py核心流程从字符串到结构化查询参数输入问句“王家卫导演的电影有哪些”输出结构{intent: director_movies, entities: {person: 王家卫}}脚本执行四步预处理去空格、标点统一繁体转简体用opencc库requirements.txt已声明分词词性标注jieba.posseg.cut()得到[(王家卫, nr), (导演, v), (的, u), (电影, n)]模板匹配遍历question_template.py中预定义的12条正则规则# question_template.py 片段 TEMPLATES [ (r(.?)导演的(.?)有哪些, director_movies), (r(.?)演过哪些(.?), actor_movies), (r(.?)的豆瓣评分是多少, movie_rating), ]槽位提取对匹配成功的正则用match.groups()提取命名实体# process_question.py 中 for pattern, intent in TEMPLATES: match re.match(pattern, question) if match: entities {} if intent director_movies: entities[person] match.group(1).strip() # 王家卫 return {intent: intent, entities: entities}3.3 槽位校验为什么王家卫必须存在于Neo4j的Person节点中process_question.py提取的王家卫只是字符串但最终Cypher查询需要真实节点ID。因此在生成查询前系统会执行校验# server.py 中调用 def validate_entity(person_name): result session.run( MATCH (p:Person {name: $name}) RETURN p.id AS id, nameperson_name ) record result.single() return record[id] if record else None # 若validate_entity(王家卫)返回None则返回错误未找到人物王家卫这个环节常被忽略却是问答准确性的生死线避免用户输错字如“王家伟”导致返回空结果却不报错为后续扩展“模糊匹配”如用apoc.text.fuzzyMatch留出接口3.4 意图分类的兜底机制当正则全部不匹配时怎么办question_classification.py并非主流程而是作为process_question.py的备用方案存在。它用TF-IDF朴素贝叶斯对问句向量化# question_classification.py 片段 vectorizer TfidfVectorizer(max_features5000, ngram_range(1,2)) classifier MultinomialNB() # 训练数据来自 question_classification.txt格式问句\t意图 with open(question_classification.txt, r, encodingutf-8) as f: lines [line.strip().split(\t) for line in f if \t in line] X, y zip(*lines) X_vec vectorizer.fit_transform(X) classifier.fit(X_vec, y)使用场景用户问“给我推荐几部好看的科幻片”没命中预设正则但question_classification.py能将其分类为movie_recommend意图模型准确率约83%测试集100条虽不如BERT但训练快、体积小、可解释性强vectorizer.get_feature_names_out()能看到哪些词影响分类4. 问答逻辑落地server.py的Cypher生成器与前端交互细节4.1 从意图到Cypherquestion_template.py如何保证查询语句100%语法正确question_template.py本质是一个Cypher模板引擎每个意图对应一个预编译的查询字符串# question_template.py CYPHER_TEMPLATES { director_movies: MATCH (p:Person)-[:DIRECTED]-(m:Movie) WHERE p.name $person_name RETURN m.title AS title, m.year AS year, m.rating AS rating ORDER BY m.year DESC , actor_movies: MATCH (p:Person)-[:ACTED_IN]-(m:Movie) WHERE p.name $person_name RETURN m.title AS title, m.year AS year, m.rating AS rating , movie_rating: MATCH (m:Movie) WHERE m.title $movie_title RETURN m.rating AS rating, m.year AS year }关键设计点所有模板使用$param占位符由session.run(cypher, **params)安全传参杜绝Cypher注入RETURN字段严格按前端index.html中表格列名title/year/rating命名避免JS解析失败ORDER BY和LIMIT已内置无需前端二次排序如director_movies按年份降序4.2 前端index.html的AJAX请求与错误处理static/index.html用原生JavaScript发起POST请求!-- index.html 片段 -- form idqa-form input typetext idquestion-input placeholder例如王家卫导演的电影有哪些 button typesubmit提问/button /form div idresult/div script document.getElementById(qa-form).addEventListener(submit, async function(e) { e.preventDefault(); const question document.getElementById(question-input).value.trim(); if (!question) return; try { const res await fetch(/ask, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({question: question}) }); const data await res.json(); if (data.error) { document.getElementById(result).innerHTML p stylecolor:red${data.error}/p; } else { // 渲染表格字段名与Cypher RETURN一致 const html tabletrth片名/thth年份/thth评分/th/tr data.results.map(r trtd${r.title}/tdtd${r.year}/tdtd${r.rating}/td/tr ).join() /table; document.getElementById(result).innerHTML html; } } catch (err) { document.getElementById(result).innerHTML p stylecolor:red网络错误${err.message}/p; } }); /script为什么不用Vue/React毕设演示要求“打开HTML就能跑”零构建步骤fetch兼容性好IE11需polyfill但项目未要求支持错误提示直接写死p stylecolor:red比弹窗更符合答辩PPT风格4.3server.py的Flask路由与Neo4j会话管理# server.py 核心路由 from flask import Flask, request, jsonify from neo4j import GraphDatabase app Flask(__name__) # 全局Driver实例避免每次请求新建连接 driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, 123456)) app.route(/ask, methods[POST]) def ask_question(): try: data request.get_json() question data.get(question, ).strip() if not question: return jsonify({error: 问题不能为空}), 400 # 1. 问句解析 from process_question import parse_question parsed parse_question(question) if not parsed or not parsed.get(intent): return jsonify({error: 无法理解您的问题请换一种说法}), 400 # 2. 生成Cypher并执行 from question_template import CYPHER_TEMPLATES cypher CYPHER_TEMPLATES.get(parsed[intent]) if not cypher: return jsonify({error: 暂不支持该类型问题}), 400 # 3. 参数绑定自动从parsed[entities]提取 params parsed[entities].copy() # 补充通用参数 params[limit] 10 with driver.session() as session: result session.run(cypher, **params) records [dict(record) for record in result] return jsonify({results: records}) except Exception as e: return jsonify({error: f系统错误{str(e)}}), 500重要细节driver是全局单例session在with块内创建确保连接自动释放params parsed[entities].copy()直接透传CYPHER_TEMPLATES中$person_name自动绑定records [dict(record) for record in result]将Neo4j Record对象转为JSON序列化友好的字典列表4.4 查询性能优化为什么MATCH (p:Person)必须加索引首次运行server.py时若未建索引查“周星驰演过哪些电影”可能耗时3秒以上。必须手动执行// 在Neo4j Browser中运行 CREATE INDEX person_name_index ON :Person(name); CREATE INDEX movie_title_index ON :Movie(title); CREATE INDEX genre_name_index ON :Genre(name);验证索引是否生效在Cypher中执行EXPLAIN MATCH (p:Person {name:周星驰}) RETURN p看执行计划是否有NodeIndexSeek若显示NodeByLabelScan说明索引未命中检查大小写、空格、是否用而非CONTAINS注意索引创建后需重启Neo4j服务或等待后台生效否则server.py仍走全表扫描。5. 避坑 / 常见问题 / 排查新手在本地部署时踩过的5个真实血泪坑5.1 现象data2neo4j.py运行报错Neo4jError: The allocation of the requested memory failed原因Neo4j默认堆内存4GB但你的机器只有2GB物理内存且neo4j.conf中dbms.memory.heap.initial_size2g和dbms.memory.heap.max_size2g未调整解决编辑conf/neo4j.conf将两行改为dbms.memory.heap.initial_size1g和dbms.memory.heap.max_size1g删除data/databases/graph.db目录强制重建数据库避免旧数据残留重启Neo4j服务bin/neo4j restartLinux/Mac或服务管理器Windows5.2 现象前端输入“周星驰演过哪些电影”返回空结果但Neo4j Browser中MATCH (p:Person)-[:ACTED_IN]-(m:Movie) WHERE p.name周星驰 RETURN m.title能查到原因process_question.py分词后得到周星驰但Neo4j中Person.name字段实际存的是周星驰 末尾有空格或周星驰 全角空格解决在data2neo4j.py写入节点前对name字段做strip()清洗if col name and row[col]: row[col] row[col].strip().replace( , ) # 清除全角空格或在Cypher查询中用trim(p.name) $person_name替代p.name $person_name5.3 现象pip install -r requirements.txt报错ERROR: Could not find a version that satisfies the requirement neo4j4.4.30原因PyPI官方源不提供Neo4j Python Driver 4.4.30该版本已归档但项目requirements.txt锁死了此版本以保证兼容性解决下载离线wheel包访问https://pypi.org/project/neo4j/4.4.30/#files下载neo4j-4.4.30-py3-none-any.whl本地安装pip install neo4j-4.4.30-py3-none-any.whl再执行pip install -r requirements.txt跳过neo4j5.4 现象server.py启动后访问http://localhost:5000显示404但/ask接口能用原因Flask默认只注册/ask路由index.html需通过app.send_static_file(index.html)提供但项目中未配置静态文件路由解决在server.py顶部添加app.route(/) def home(): return app.send_static_file(index.html)确保index.html放在static/目录下项目结构已满足重启server.py即可访问根路径5.5 现象问“豆瓣评分高于8.5的电影有哪些”返回结果包含rating: null的电影原因movie.csv中部分电影rating字段为空data2neo4j.py写入时存为null但Cypher中WHERE m.rating 8.5会过滤掉null值理论上不应出现——除非rating被存为字符串null而非NULL解决检查movie.csv原始数据确认空评分列为而非null在data2neo4j.py中强化清洗if col rating and row[col].strip(): try: row[col] float(row[col].strip()) except ValueError: row[col] None # 显式设为None非字符串null重建数据库后验证MATCH (m:Movie) WHERE m.rating IS NULL RETURN count(*)应返回06. 毕设答辩前的终极验证用test.py跑通全流程并定制你自己的电影数据集6.1test.py5行代码验证整个知识图谱链路是否健康项目自带的test.py是毕设答辩的“后悔药”它不依赖前端纯命令行验证# test.py from process_question import parse_question from question_template import CYPHER_TEMPLATES from neo4j import GraphDatabase # 1. 问句解析 parsed parse_question(王家卫导演的电影有哪些) print(✅ 问句解析:, parsed) # 2. Cypher生成 cypher CYPHER_TEMPLATES.get(parsed[intent]) print(✅ Cypher模板:, cypher[:100] ...) # 3. 数据库查询 driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, 123456)) with driver.session() as session: result session.run(cypher, **parsed[entities]) records list(result) print(✅ 查询结果数量:, len(records)) if records: print(✅ 示例结果:, dict(records[0])) driver.close()执行效果✅ 问句解析: {intent: director_movies, entities: {person: 王家卫}} ✅ Cypher模板: MATCH (p:Person)-[:DIRECTED]-(m:Movie) WHERE p.name $person_name RETURN m.title AS title, m.year AS year, m.rating AS rating ORDER BY m.year DESC ✅ 查询结果数量: 3 ✅ 示例结果: {title: 一代宗师, year: 2013, rating: 7.9}为什么必须跑test.py避免答辩当天因环境变量、端口冲突、Neo4j未启动等琐碎问题翻车导师问“你如何保证问答逻辑和图谱结构一致”你可以当场python test.py展示实时输出6.2 替换你自己的数据集3步迁移指南以豆瓣Top250电影为例假设你要把系统换成“豆瓣Top250电影数据集”只需改3处文件修改点说明data/movie.csv替换为豆瓣数据确保列名含title,year,ratingyear必须为数字rating为浮点数data/person.csvperson_to_movie.csv补充导演/演员姓名person_to_movie.csv中role列填director或actor可用豆瓣API或爬虫获取注意清洗/分隔符如张艺谋/陈凯歌需拆成两行userdict3.txt追加新电影名、人名格式电影名 nz 100如肖申克的救赎 nz 100避免jieba切错关键检查点运行data2neo4j.py后在Neo4j Browser执行MATCH (m:Movie) RETURN count(*)确认数量与CSV行数一致执行MATCH (p:Person)-[r]-(m:Movie) RETURN type(r), count(*)确认DIRECTED/ACTED_IN关系数量合理用test.py测试新问句“《肖申克的救赎》的导演是谁”6.3 答辩加分技巧在server.py中加一行日志让导师看到你的工程思维在/ask路由开头插入日志打印# server.py import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) app.route(/ask, methods[POST]) def ask_question(): data request.get_json() question data.get(question, ).strip() logger.info(f 收到问题: {question}) # 新增日志 # 后续逻辑不变...答辩时这样说“老师我加了请求日志这样不仅能监控系统负载更重要的是——当问答结果不符合预期时我可以回溯原始问句快速定位是分词错了、模板没匹配、还是Cypher写错了。这不是炫技而是把‘黑匣子’变成可追踪的白盒。”从那以后我每次改完question_template.py都强制跑一遍test.py再测3个边界问句空输入、错别字、超长句因为毕设答辩只有一次机会而test.py就是我的保险丝。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑