简介:基于Neo4j的水浒传人物关系可视化及问答系统,是一份面向计算机、通信、人工智能、自动化相关专业学生与从业者的毕业设计源码及答辩资料。项目以Python实现后端逻辑,结合Neo4j图数据库构建人物关系图谱,并提供Web端可视化与问答入口,可完整支撑课程设计、大作业或毕业设计二次开发;答辩评审分数达98分,代码经调试测试可运行,降低了直接上手使用的门槛。资源共197个文件,压缩包约22.86MB,主要包含8个Python源码、HTML/CSS/JS前端页面、Neo4j数据与配置文件、答辩PPT及PDF文档等,另附大量人物关系图与界面截图,便于对照查看效果。前端基于Bootstrap、Font Awesome、DataTables等常见组件搭建,目录结构清晰,适合分层阅读与局部修改。目前已有274人浏览学习,整体具有较强的学习借鉴价值,从入门进阶到答辩展示均能提供完整参考。
1. Neo4j水浒传人物关系可视化及问答系统:一份能一次跑通的毕设源码
Neo4j水浒传人物关系可视化及问答系统这套毕设源码,我拿到手的第一反应不是看代码,而是先把它跑起来。它把Neo4j图数据库、Python后端和前端关系图可视化串成了一条完整链路,相当于一个迷你版知识图谱应用:108将的人物关系用图结构铺开,带一个能回答“宋江和吴用是什么关系”这类问句的问答模块,还配了答辩PPT。对于正在做毕业设计、课程大作业的人,或者想从关系数据库跳到图数据库、想搞懂知识图谱落地套路的从业者,这套源码的价值在于结构完整、能运行、能照着改。
2. 数据建模先行:人物节点、关系类型与CSV导入的落库设计
2.1 节点与关系类型的设计:为什么关系要比人物多花心思
拿到这套源码,第一步先看它的数据建模。人物节点好设计,无非是姓名、绰号、星号、座次这类属性,难的是关系怎么抽。水浒传里人物关系远比“认识”复杂:宋江和吴用是结义兄弟,卢俊义和燕青是主仆,林冲和鲁智深是结拜兄弟,扈三娘和王英是夫妻,中间还穿插师徒、父子、同僚、敌对。建模时如果把所有关系都塞进一个“related”标签,后面问答系统根本没法区分“宋江的师父是谁”和“宋江的结义兄弟是谁”。
我一般会把关系类型收敛成 8 种以内:兄弟、师徒、夫妻、父子、主仆、同僚、敌对、恩仇。关系上挂两个属性,一个是事件出处,记录这段关系出自原著第几回或哪个情节,另一个是强度权重,取值 1 到 5,这个权重后面做推荐排序和问答打分都能用上。人物属性里除了基础信息,建议保留“上山前身份”和“结局”,这样问答系统能覆盖“武松的绰号是什么”“鲁智深最后去了哪里”这类信息查询题。
表:这套系统里常见的关系类型与权重参考
| 关系类型 | 典型场景 | 强度权重 |
|---|---|---|
| 兄弟 | 宋江与吴用、林冲与鲁智深 | 5 |
| 师徒 | 卢俊义与史文恭、王进与史进 | 4 |
| 夫妻 | 王英与扈三娘、张清与琼英 | 4 |
| 父子 | 宋太公与宋江、阮氏三兄弟之父 | 4 |
| 主仆 | 卢俊义与燕青 | 3 |
| 同僚 | 梁山五虎将之间 | 2 |
| 敌对 | 武松与蒋门神 | 2 |
| 恩仇 | 杨志与牛二 | 2 |
2.2 数据导入:LOAD CSV批量导入与py2neo脚本两种方式
人物和关系数据整理成 CSV 后,导入 Neo4j 有两条路。最省事的是用 Neo4j 自带的 LOAD CSV,把文件丢进安装目录的 import 文件夹,直接写 Cypher 导入。另一条路是项目里用 Python 写脚本,通过 py2neo 或官方驱动逐条写入,适合数据需要先清洗或者要动态生成关系类型的场景。
// 导入人物节点,CSV放在Neo4j安装目录的import文件夹下 LOAD CSV WITH HEADERS FROM 'file:///person.csv' AS line CREATE (p:Person { name: line.name, // 姓名 nick: line.nick, // 绰号 star: line.star, // 星号,如“天魁星” rank: toInteger(line.rank), // 座次 identity: line.identity // 上山前身份 });这段 Cypher 的关键是WITH HEADERS,它会把 CSV 第一行当作字段名,后面通过line.字段名逐行取值。rank用了toInteger做类型转换,因为座次在 CSV 里是字符串,存进 Neo4j 后需要是整数才能排序和范围查询。人物节点建完,下一步导关系。
// 导入关系,relation.csv包含start_name、end_name、rel_type、weight、source LOAD CSV WITH HEADERS FROM 'file:///relation.csv' AS line MATCH (a:Person {name: line.start_name}) MATCH (b:Person {name: line.end_name}) CALL apoc.merge.relationship(a, line.rel_type, {}, {weight: toFloat(line.weight), source: line.source}, b, {}) YIELD rel RETURN count(rel);这里用了 APOC 插件的apoc.merge.relationship,好处是关系类型可以从 CSV 里动态读,不用针对每种关系写一条MERGE。如果你的环境没装 APOC,常见做法是按关系类型拆多个MATCH ... MERGE (a)-[:兄弟]->(b)。注意MATCH定位两端的节点依赖第 2.3 节的唯一约束,没有约束的话,同名人物会匹配出多行,导致关系成倍重复,这是后面避坑章节第一个要讲的坑。
2.3 约束与索引:问答请求每次都靠它定位节点
这套系统的问答接口,每个问题进来都要按人物名字定位节点,所以Person.name上必须建唯一约束。数据量虽然只有一百多人,但约束带来的索引能让 Cypher 的MATCH (a:Person {name:'宋江'})走索引查找而不是全表扫描,这也是知识图谱应用的一个基本习惯。
CREATE CONSTRAINT person_name_unique IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE;约束建完以后,重复执行人物 CSV 的导入脚本会直接报错而不是产生脏数据。这一点在毕设答辩时很加分:评审问“你怎么保证数据一致性”,这一句约束就是最直接的答案。我自己的习惯是数据一进库就先建约束,再跑一遍节点数和关系数统计,确认数字对得上再开发上层接口。
3. 可视化与问答落地:Flask接口、ECharts关系图和Cypher查询的串接
3.1 后端查询接口:Flask路由与Neo4j官方驱动的连接姿势
源码的后端是 Python 写的,查询接口走 Flask 路由,连接 Neo4j 用官方驱动neo4j而不是 py2neo。原因很简单,py2neo 到 4.1 版本后基本停更,新项目用官方驱动更稳妥,连接池管理、session 生命周期都是现成的。我一般把 driver 实例化放在模块级别,避免每个请求都新建连接。
from flask import Flask, jsonify from neo4j import GraphDatabase app = Flask(__name__) # bolt协议连到Neo4j的7687端口,auth里的密码改成你自己的 driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "123456")) def fetch_all_relationships(limit=300): with driver.session() as session: result = session.run( "MATCH (a:Person)-[r]->(b:Person) " "RETURN a.name AS source, b.name AS target, " "type(r) AS relation, r.weight AS weight " "LIMIT $limit", limit=limit ) return [dict(record) for record in result.data()] @app.route("/api/graph") def api_graph(): return jsonify({"data": fetch_all_relationships()})三个参数值得说明:bolt://localhost:7687是 Neo4j 的二进制协议地址,和浏览器访问用的http://localhost:7474不是一回事,写错端口必报错;session.run是短查询的常见用法,事务性操作可以换session.execute_write;LIMIT $limit用参数化写法,一个是防 Cypher 注入,一个是前端图谱渲染几百条边已经是上限,全量导出一千多条关系会让浏览器卡死。
3.2 前端渲染:ECharts关系图与nifty后台模板的资源规划
源码包里那一串 CSS 文件,对应的是这套系统的前端骨架。bootstrap.min.css 是基础样式,nifty.min.css 是后台管理框架,侧边栏、顶栏、卡片布局都靠它;wiki.css 是人物百科详情页的样式;datatables.bootstrap.css 是人物列表表格用的,配合 datatables.responsive.css 做响应式;ionicons.min.css 和 font-awesome.min.css 是两套图标库;那个带 hash 的 css 文件多半是打包工具生成的指纹文件,不用管它。这种结构意味着系统不是一个单页 demo,而是有后台管理味道的完整 Web 应用。
人物关系可视化图是这套系统的门面,毕设里最常做的就是 ECharts 的 graph 关系图。把后端/api/graph返回的边数据映射成节点和连线,再扔给 ECharts 渲染。
// 从后端拉取关系数据 const response = await fetch('/api/graph'); const data = await response.json(); const nodes = []; const links = []; data.data.forEach(item => { if (!nodes.find(n => n.name === item.source)) { nodes.push({ name: item.source, symbolSize: 28 }); } if (!nodes.find(n => n.name === item.target)) { nodes.push({ name: item.target, symbolSize: 28 }); } links.push({ source: item.source, target: item.target, label: { show: true, formatter: item.relation } }); }); // 力导向布局,节点可拖拽 chart.setOption({ series: [{ type: 'graph', layout: 'force', roam: true, draggable: true, data: nodes, links: links, force: { repulsion: 300, edgeLength: 100 }, lineStyle: { color: 'source', curveness: 0.1 } }] });layout: 'force'让节点按力导向自动排布,repulsion控制节点间的斥力,数值越大节点越分散,edgeLength是边长理想值,curveness: 0.1给边加一点弧度——这个细节很实用,两个人之间有来有回的双向关系,直线会重叠成一根,加弧度才能看出是两条边。
3.3 问答系统:正则模板匹配加Cypher参数化查询
问答模块没有上大模型,毕设级别用正则模板加 Cypher 查询完全够用。核心是把问句分类,每类对应一个查询模式。我拿到源码后第一件事就是把问句分类列出来:查询两人关系、查询人物属性、查询某人师父、查询某人结义兄弟。
import re def answer_question(question): # 模板1:A和B是什么关系 m = re.search(r'(.+?)和(.+?)是什么关系', question) if m: a, b = m.group(1), m.group(2) with driver.session() as session: records = session.run( "MATCH (a:Person {name:$a})-[r]-(b:Person {name:$b}) " "RETURN type(r) AS rel, r.weight AS weight", a=a, b=b ).data() if records: return f"{a}和{b}之间是{'、'.join(r['rel'] for r in records)}关系" return f"没查到{a}和{b}的直接关系" # 模板2:A的师父/兄弟/妻子是谁 m2 = re.search(r'(.+?)的(师父|兄弟|妻子|父亲)是谁', question) if m2: name, rel = m2.group(1), m2.group(2) rel_map = {"师父": "师徒", "兄弟": "兄弟", "妻子": "夫妻", "父亲": "父子"} with driver.session() as session: records = session.run( "MATCH (a:Person {name:$a})-[:$rel]->(b:Person) " "RETURN b.name AS name", a=name, rel=rel_map[rel] ).data() return records[0]['name'] if records else f"没查到{name}的{rel}" return "这个问题我还答不上来"正则顺序是个坑:必须先匹配“A 和 B 是什么关系”,再匹配“A 的 X 是谁”,如果反过来,“宋江的师父是谁”会被模板 1 的正则误拆成“宋江的师父”和“谁”两个实体。$a、$b这类参数化写法,除了防注入,还能避免中文特殊字符破坏 Cypher 语句。问句模板还可以继续叠,比如“武松的绰号”“宋江排第几”,每加一个模板,问答系统的覆盖范围就大一圈,这部分扩展空间很大。
表:问答模板与对应的 Cypher 模式
| 问句类型 | 正则示例 | Cypher 核心模式 |
|---|---|---|
| 两人关系 | (.+?)和(.+?)是什么关系 | (a)-[r]-(b) RETURN type(r) |
| 人物属性 | (.+?)的绰号是什么 | (a {name:$a}) RETURN a.nick |
| 关系对象 | (.+?)的师父是谁 | (a)-[:师徒]->(b) RETURN b.name |
| 座次范围 | 排在第几位 | (a) RETURN a.rank ORDER BY a.rank |
4. 联调避坑:Neo4j安装、驱动连接与前端资源加载的常见问题排查
4.1 环境版本搭配:Neo4j、Python与驱动的兼容性选择
跑这套源码之前,先把环境版本对齐,不然翻车都翻在起步阶段。Neo4j 社区版 4.x 需要 Java 11,5.x 需要 Java 17;Python 驱动方面,neo4j 官方驱动 4.4 系列配 Neo4j 4.4 最稳,5.x 驱动配 5.x 服务器。Python 解释器建议 3.8 到 3.10,太新的版本有时候会和驱动二进制依赖打架。
表:本套系统推荐的环境搭配
| 组件 | 推荐版本 | 注意点 |
|---|---|---|
| Neo4j Community | 4.4.x 或 5.x | 5.x 必须装 Java 17 |
| Java | 11 或 17 | 版本不对服务直接起不来 |
| Python | 3.8–3.10 | 3.11+ 需确认驱动有对应 wheel |
| neo4j 驱动 | 4.4.x 或 5.x | 大版本与服务器匹配 |
| APOC 插件 | 与 Neo4j 同版本 | 手动放 plugins 目录后重启 |
APOC 插件是最容易忽略的一环,如果源码里的导入脚本用了apoc.merge.relationship,而你的 Neo4j 没装 APOC,报错会非常隐晦,直接提示Unknown function。装 APOC 就是把 jar 包丢进 Neo4j 的 plugins 目录,然后重启服务。如果你不想依赖 APOC,把导入脚本改成按关系类型拆多条MERGE也能跑,就是代码冗余一些。
4.2 五条踩坑记录:现象、原因、解决
坑一:浏览器打不开 Neo4j 页面,Python 连接报Failed to establish connection。现象是 localhost:7474 白屏,代码报连不上 bolt 端口。原因多半是 Neo4j 服务根本没启动,或者启动了但 7687 端口被占用。解决:用neo4j console前台启动,日志直接打到终端,能看到启动到哪一步挂了;端口占用的话lsof -i:7687查一下是谁占的。血泪经验是别用neo4j start后台启动,报错信息会被吞掉,前台启动才能看清问题。
坑二:LOAD CSV 导入后中文乱码,字段名带\uFEFF。现象是人物名字变成乱码,或者第一个字段匹配不上。原因是 Windows 记事本另存为 UTF-8 时会写入 BOM 头,Neo4j 把 BOM 当成了字段名的一部分。解决:用 VS Code 或 Notepad++ 把 CSV 转成 UTF-8 无 BOM 格式,或者导入时在文件路径后加参数,但最省心的还是直接另存为纯 UTF-8。
坑三:关系重复导入,越导越多。现象是同样一条“宋江-吴用-兄弟”在图谱里出现好几遍。原因是导入脚本用了CREATE而不是MERGE,或者节点没有唯一约束导致MATCH匹配到多个节点。解决:先建 Person 唯一约束,再导入;关系写入用MERGE或者 APOC 的merge.relationship。这套顺序不能颠倒,我在这上面吃过亏,导完数据发现关系数翻了三倍,又清空重来。
坑四:前端页面白屏,CSS 全部 404。现象是页面结构出来了但没有任何样式,F12 一看一堆 css 文件请求失败。原因是源码里的 HTML 直接写死了bootstrap.min.css这类相对路径,和 Flask 的 static 目录结构对不上。解决:把 CSS 文件放进static/css,HTML 里改用url_for('static', filename='css/bootstrap.min.css')。那些 nifty 模板的 CSS 文件之间有依赖顺序,别乱改加载顺序,否则导航栏会塌掉。
坑五:问“宋江的师父是谁”,问答系统答成“宋江和谁是什么关系”。现象是答案驴唇不对马嘴。原因是正则模板的匹配顺序写反了,宽泛模板(.+?)和(.+?)是什么关系先命中,把“宋江的师父”和“谁”当成了两个人名。解决:把窄模板放前面,先匹配“A 的 X 是谁”,再匹配“A 和 B 是什么关系”,并且正则里对“的”字结构单独处理。问答系统的玄学往往不在算法,就在规则顺序。
4.3 数据核验:导入完先跑几个统计Cypher再开发
导入完数据别急着写接口,先跑几分钟的查询核验数据质量。这一步能避免你后面调试接口时,根本分不清问题是出在代码还是出在数据。
// 查节点总数和关系总数 MATCH (n:Person) RETURN count(n) AS person_cnt; MATCH ()-[r]->() RETURN count(r) AS rel_cnt; // 查孤立节点(没有任何关系的角色) MATCH (p:Person) WHERE NOT (p)--() RETURN p.name AS lonely_person; // 查重复关系 MATCH (a)-[r]->(b) WITH a, b, type(r) AS t, count(*) AS c WHERE c > 1 RETURN a.name, b.name, t, c LIMIT 20;第一个查询确认人物数量对不对,第二个查孤立节点,原著里有些角色提过名字但没建立关系,这类节点如果太多,前端图谱上会出现一堆孤零零的点,演示时很难看。第三个查重复关系,如果查出 c 大于 1 的记录,回头检查导入脚本的MERGE写没写对。这三个查询跑完,数据层基本就心里有底了。
5. 进阶玩法:给问答系统加多跳查询与关系权重推荐
5.1 多跳关系检索:用可变长度路径查宋江的“朋友的朋友”
问答系统只能查直接关系,是这套源码的一个边界。把它往上推一步,可以做多跳查询:用户问“宋江的结义兄弟的师父有哪些”,这就是两跳路径。Cypher 的可变长度路径就是为这个场景设计的。
MATCH p = (a:Person {name: '宋江'})-[:兄弟|师徒|同僚*1..2]-(b:Person) WHERE a <> b RETURN b.name AS target, length(p) AS hops, [r IN relationships(p) | type(r)] AS rels ORDER BY length(p) LIMIT 30;*1..2表示路径长度一到两跳,关系类型限定在兄弟、师徒、同僚三种,避免把“敌人的敌人”这种太远的路径也捞进来。返回的rels数组会显示路径上经过的关系类型,比如["兄弟","师徒"],前端可以直接展示成“宋江—结义兄弟—某人—师徒—目标”。
5.2 关系权重排序:让答案从“有一堆”变成“有顺序”
多跳查询的结果往往很多,直接列出来没有说服力。我一般会把第 2.1 节定义的关系权重叠加起来,路径上所有边的权重累加,作为这条路径的相关度得分,然后按分数倒序返回。结义兄弟权重 5、师徒权重 4、同僚权重 2,同样两跳路径,“兄弟+师徒”的组合自然排在“同僚+同僚”前面,符合人对关系密切程度的直觉。
def multi_hop_ranked(name, max_hops=2): with driver.session() as session: records = session.run( "MATCH p = (a:Person {name:$name})-[:兄弟|师徒|夫妻|父子|主仆|同僚*1..2]-(b) " "WHERE a <> b " "RETURN b.name AS target, " "reduce(s=0, r IN relationships(p) | s + coalesce(r.weight, 1)) AS score " "ORDER BY score DESC LIMIT 20", name=name ).data() return recordsreduce函数把路径上每条边的 weight 累加起来,coalesce(r.weight, 1)处理老数据里没写权重的边,默认给 1 分,防止降级报错。这一步做完,问答系统就从“返回一堆关系”升级成“返回按相关度排序的关系推荐”,这个能力放在毕设的创新点里,评审基本都会认可。后来我每次做图数据库项目,都强制先把约束、数据核验、权重设计走一遍再写业务代码,这套流程能救你于各种数据脏乱差的水火之中。希望帮到你。
本文还有配套的精品资源,点击获取