多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

红楼梦知识图谱问答系统毕设源码解析:从Neo4j到Flask全链路

红楼梦知识图谱问答系统毕设源码解析:从Neo4j到Flask全链路 简介基于Python知识图谱的红楼梦人物关系可视化与问答系统毕业设计项目面向计算机相关专业正在筹备毕设的学生以及需要项目实战经验的学习者同样适合课程设计或期末大作业场景。项目经导师指导并获评审99分代码完整、运行可靠小白也能按文档快速上手。压缩包为zip格式共247个文件核心包含Python源码.py、前端样式及交互脚本.css/.js、数据配置与说明文档.json/.txt/.md并附大量运行截图.jpg/.png辅助对照包体仅5.71MB。目前已有133人学习使用。通过源码、文档和截图可直观理解知识图谱构建、人物关系抽取及问答系统实现思路既能作为毕业设计直接参考也可扩展用于其他文学领域节省从零搭建的时间。1. 十分钟跑通红楼梦问答系统这个毕设源码到底拆成了什么拿到这套 Python 知识图谱毕设的第一反应是别急着跑数据。源码里文件不少入口脚本、图谱构建脚本、问答接口、前端页面各管一摊但真正决定你可不可以复现的是两样一是 Neo4j 的版本和驱动是否对上二是前端页面里那几份 JS 和 CSS 依赖能否在本机加载。这个项目主体就是把《红楼梦》主要人物与关系落到 Neo4j用 Flask 提供两个接口一个把人物关系网用 ECharts 画出来一个把「宝玉和林黛玉什么关系」这类问句匹配成 Cypher 语句去查。适合正在做毕设的计算机相关专业学生也适合想把本体建模、图查询、Flask API 串起来练一遍的人。评审档案里写着 99 分代码完整核心是能跑没有暗坑。2. 走进项目的目录与数据流先搞清楚图谱从哪来拿到源码包之后先别双击 main.py这套东西的启动顺序是有讲究的。很多人在这一步翻车代码在自己电脑上跑不起来十有八九不是代码本身的问题而是没弄明白这条数据流水线——原始文本在哪里、谁负责把文本变成结构化三元组、谁把三元组写进图数据库、谁把图数据库的数据再捞出来喂给前端。把这四段拆开后面所有排错都有的放矢。2.1 解压后先别慌文件清单与职责边界源码解压之后大致是下面这个布局每个文件管一段事文件/目录职责备注data/红楼梦人物数据与关系数据源一般是 CSV 或 JSON是图谱的原料build_graph.py读取数据源把人物实体和关系写入 Neo4j毕设里最常见的入口脚本app.py或main.pyFlask 主程序注册路由与问答接口启动后访问http://localhost:5000models/或kg/图谱操作封装比如查询实体的函数不一定每个包都有看源码组织习惯templates/前端页面通常是 index.html放 ECharts 关系图和问答框static/JS、CSS、图片资源bootstrap、nifty、font-awesome 等都在里面requirements.txtPython 依赖清单建虚拟环境后先装它文档说明开题、论文或说明文档毕设答辩直接能用的那部分动手之前先确认三件事Python 版本是不是 3.7 到 3.9 之间Neo4j 桌面版装没装本机有没有至少 4GB 空闲内存。这套技术栈不算重但 Neo4j 本身是 Java 应用内存不够会直接卡死。提示把 data 目录里的 CSV 用 Excel 或 VSCode 打开看一遍表头比先看代码更有价值。表头就是整份数据的 Schema后面图谱能查什么关系、问答能答什么问题全由这张表决定。2.2 人物实体的本体建模类、属性与关系类型知识图谱设计的核心不在代码在本体。打开那份人物关系数据里面的人物实体通常会有这么几个属性name姓名、alias别名/字/号、gender性别、identity身份比如贾母、金陵十二钗、generation辈分。别小看这些字段问答系统里的「贾宝玉的丫鬟是谁」靠的就是identity和关系类型一起筛。关系类型一般不会太多常见的是这几种关系语义举例spouse_of夫妻贾宝玉 → 林黛玉按婚约关系建模时parent_of亲子贾政 → 贾宝玉sibling_of兄弟姐妹贾珠、贾宝玉、贾环servant_of主仆袭人 → 贾宝玉friend_of朋友/知己贾宝玉 → 北静王concubine_of妾侍赵姨娘 → 贾政为什么不用 MySQL 而用图数据库我的理解是查询模式不同。MySQL 里想问「宝玉和黛玉隔了几层关系、路径是什么」要么递归 CTE要么写代码多次 join而 Neo4j 里一条MATCH pshortestPath(...)就出来了。毕设答辩时老师最吃这一套你能说清楚为什么选图数据库比会跑代码加分。2.3 从文本到图数据库入库脚本与 Cypher 批量写入图上建模想清楚了入库脚本就好写了。常见做法是先把 CSV/JSON 读进来清洗掉空值和重复昵称再用py2neo批量写节点。我见到的实现里build_graph.py核心逻辑长下面这样import csv from py2neo import Graph, Node, Relationship # 连接 Neo4j注意账号密码要与本地 neo4j.conf 保持一致 graph Graph(bolt://localhost:7687, auth(neo4j, 123456)) # 先清空旧数据避免重复跑脚本时节点堆积 graph.run(MATCH (n) DETACH DELETE n) with open(data/characters.csv, r, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: # 每个角色创建一个 Person 节点属性直接取自 CSV 列 p Node(Person, namerow[name], aliasrow.get(alias, ), genderrow.get(gender, ), identityrow.get(identity, )) graph.create(p) with open(data/relationships.csv, r, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: start graph.nodes.match(Person, namerow[source]).first() end graph.nodes.match(Person, namerow[target]).first() if start is None or end is None: # 源/目标人物不存在时跳过不影响后续入库 continue rel Relationship(start, row[relation], end) graph.create(rel)这段脚本有三个参数值得注意bolt://localhost:7687是 Neo4j 的二进制协议端口不是浏览器里那个 7474 端口auth(neo4j, 123456)里的密码是你第一次启动 Neo4j 时自己设置的encodingutf-8-sig是为了吃掉 Windows 下 CSV 的 BOM 头不然第一列人名前面会多出一个看不见的\ufeff节点坑就是在这埋的。跑完脚本后别急着开前端先用 Neo4j Browser 验证一下MATCH (p:Person) RETURN count(p) AS personCount;这个查询用来确认人物节点数是否和数据 CSV 的行数一致。如果节点数明显变少多半是relationships.csv里出现了Person表里没有的名字被脚本里的continue跳过了。这种「数据对不上」的问题越早发现越好解决。3. 人物关系可视化Flask 接口与 ECharts 前端图谱入库只是地基真正让人眼前一亮的是那套人物关系可视化页面。整条链路是浏览器请求 Flask 接口Flask 从 Neo4j 查出全部人物与关系再组装成 ECharts 能认的节点和边结构返回前端。这一章我拆开讲三层接口返回什么、前端怎么画、交互参数怎么调。3.1 后端接口怎么把图谱数据喂给前端后端要做的事情很朴素把 Neo4j 里的节点和关系拉出来转成 JSON塞给页面。见过两种实现一种是直接在 Flask 路由里写查询另一种是单独封一个graph_service.py我偏向后者因为问答系统里多处地方要复用「查人物」「查关系」这两个动作。核心路由大致是这样from flask import Flask, jsonify, render_template from py2neo import Graph app Flask(__name__) graph Graph(bolt://localhost:7687, auth(neo4j, 123456)) app.route(/api/graph) def api_graph(): # 查询所有 Person 节点构建前端需要的 nodes 与 edges nodes [] edges [] result graph.run(MATCH (p:Person) RETURN p) node_map {} for record in result: p record[p] node_id p[name] node_map[node_id] { id: node_id, name: node_id, category: p.get(identity, ), # 用身份作为节点分类前端按类着色 symbolSize: 30 } nodes.append(node_map[node_id]) result graph.run( MATCH (a:Person)-[r]-(b:Person) RETURN a.name, type(r), b.name ) for record in result: edges.append({ source: record[a.name], target: record[b.name], relation: record[type(r)], value: 1 # 这条边的权重前端可以用它控制连线粗细 }) return jsonify({nodes: nodes, edges: edges}) app.route(/) def index(): return render_template(index.html) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)这段里的node_map很关键ECharts 的 edge 只认source和target的 id如果直接用节点数组里的索引后续增加筛选功能时会乱套。category字段对应人物身份用它做节点颜色分组页面上就能一眼分清主子和丫鬟的区别。注意开发时debugTrue没问题但如果你把服务部署到公网演示务必改成debugFalse并绑127.0.0.1不然调试器会暴露任意代码执行入口这在毕设答辩演示时是高风险问题。3.2 前端关系图渲染与交互配置templates/index.html里的内容才是视觉主菜。它依赖echarts.min.js这个文件通常在static/vendor/目录里。页面加载时用fetch拉接口数据再setOption渲染。渲染核心就这几行fetch(/api/graph) .then(resp resp.json()) .then(data { const chart echarts.init(document.getElementById(graph)); chart.setOption({ tooltip: { trigger: item, formatter: function(params) { if (params.dataType node) { return params.data.name br/身份 params.data.category; } return params.data.source —— params.data.relation —— params.data.target; } }, series: [{ type: graph, layout: force, // 力导向布局节点会自动弹开 roam: true, label: { show: true, fontSize: 12 }, force: { repulsion: 300, // 节点之间斥力值越大图越散 edgeLength: [50, 120], gravity: 0.1 }, data: data.nodes.map(n ({ id: n.id, name: n.name, category: n.category, symbolSize: n.identity.includes(丫鬟) ? 18 : 35 // 按身份调整点大小 })), links: data.edges.map(e ({ source: e.source, target: e.target, value: e.value, label: { show: true, formatter: e.relation } })), categories: [...new Set(data.nodes.map(n n.category))].map(c ({ name: c })) }] }); });这里有几个参数值得抄进自己的项目。repulsion控制节点间距edgeLength是理想连边长度这两个值一起决定了图是挤成一团还是松散到找不到人。如果你想要「家族核心人物在中心、边缘人物在外圈」的视觉效果把gravity调低然后给贾母、贾政、王夫人这些核心节点在返回时加一个symbolSize的放大逻辑。数据多的时候可以关闭label.show或者在鼠标悬浮时才显示所有 label不然整个页面会被字盖满。3.3 关系类型与连线的视觉编码图谱的构图不是只求「能显示」就完了关系类型不同的边得让用户一眼看出差别。这个源码里的做法是把关系映射成连线的颜色和线宽映射表可以单独放在一个 JS 对象里const RELATION_COLOR { spouse_of: #e15759, parent_of: #59a14f, sibling_of: #76b7b2, servant_of: #ff9da6, friend_of: #9c755f, concubine_of: #f28e2b };前端拿到edge.relation后查表取颜色查不到就用默认灰。这样页面加载完成后红色连线表示夫妻绿色表示亲子一眼就能看出贾府的主干血缘网。还有一个小细节边的粗细可以用value来设但前提是后端真的返回了「关系强度」这种数值。如果后端只返回 1前端强行lineStyle.width value * 3你会看到所有边都一样粗反而不如直接把线宽写成固定值。这个项目的可视化部分整体不难难点全在参数调试和数据校验。把接口返回的 JSON 先复制进浏览器地址栏跑一遍确认节点数量和边数量都有值再去调配色能省下大量「图为什么只显示一半」的排查时间。4. 问答系统实现从问句到 Cypher 的四步解析这个项目里问答系统的定位很明确不是做深度学习对话而是用模板匹配把自然语言问题转成图查询。别嫌它「不智能」毕设答辨里「简单有效」比「复杂但跑不动」得分高得多。它省去了训练语料和模型部署的麻烦实现核心就四个步骤问句分词、实体识别、意图分类、模板转 Cypher。4.1 实体抽取用 jieba 加自定义词典锁定人名问答系统的第一关是把「宝玉」「黛玉」「宝二爷」这类称呼映射成图谱里的Person.name。源码里常见做法是引入 jieba 并加载自定义词典import jieba # 把人物别名表加载进 jieba保证分词时不会把宝玉拆成宝和玉 person_dict_path data/person_dict.txt jieba.load_userdict(person_dict_path) def extract_entity(question): words jieba.lcut(question) # 聚合词表里的人物别名比如宝二爷、林妹妹都归一为宝玉/黛玉 entity None name_map { 宝玉: 贾宝玉, 宝二爷: 贾宝玉, 怡红公子: 贾宝玉, 黛玉: 林黛玉, 林妹妹: 林黛玉, 颦儿: 林黛玉 } for w in words: if w in name_map: entity name_map[w] break return entityload_userdict的作用是往 jieba 的词表里加自定义词否则「宝二爷」会被切得七零八落。name_map是昵称归一化的最后一道保险。写这一段的时候注意别把name_map里的键和值搞反了——前端页面问的是「宝二爷」图谱里存的是「贾宝玉」这一步不做后续查询一定查不到结果。4.2 关系识别从问句模式匹配到查询意图实体有了接下来要判断用户问的是「谁和谁什么关系」还是「某人的亲戚都有谁」。两种问题对应的 Cypher 完全不一样前者查的是两点之间的一条边后者查的是某个节点周围一圈邻居。源码里通常维护一个意图模板库INTENT_PATTERNS [ (relation_query, [什么关系, 关系, 认识吗]), (spouse_query, [妻子, 丈夫, 老婆, 老公]), (parent_query, [父亲, 父亲是谁, 母亲, 爹娘, 父母]), (servant_query, [丫鬟, 仆人, 下人]), (neighbor_query, [亲戚, 家人, 家人有谁, 家族]) ] def detect_intent(question): # 按优先级遍历模板库命中第一个关键词就返回该意图 for intent, keywords in INTENT_PATTERNS: for kw in keywords: if kw in question: return intent return relation_query # 默认按两人关系处理这里要注意优先级顺序。关系这个词在所有问句里几乎都出现所以 must 放在所有更具体的意图后面。我调过一个真实案例问「宝玉的爸妈是谁」如果关系先命中它会被当成relation_query答出来的是任意一条边而不是父母关系。4.3 模板转 Cypher 与回答生成意图和实体都确定了工作就剩下「填空」——把实体名字填进对应的 Cypher 模板def build_cypher(intent, entity_a, entity_bNone): # 根据意图返回 Cypher 语句和自然语言答句模板 if intent parent_query: return ( MATCH (a:Person {name: $a})-[:parent_of]-(p:Person) RETURN p.name, f{entity_a}的父母是: ) elif intent servant_query: return ( MATCH (a:Person {name: $a})-[:servant_of]-(s:Person) RETURN s.name, f{entity_a}的下人有: ) elif intent relation_query and entity_b: return ( MATCH (a:Person {name: $a})-[r]-(b:Person {name: $b}) RETURN type(r) AS rel , f{entity_a}和{entity_b}的关系是: ) elif intent neighbor_query: return ( MATCH (a:Person {name: $a})-[:parent_of|:sibling_of|:spouse_of]-(p:Person) RETURN DISTINCT p.name, f{entity_a}的核心亲属有: ) return None, None def answer_question(question): entity_a extract_entity(question) intent detect_intent(question) cypher, prefix build_cypher(intent, entity_a) if not cypher: return 这个问题我还不会换个说法试试 result graph.run(cypher, aentity_a).data() if not result: return 图谱里没找到对应的关系可能数据不全 names [r[p.name] if p.name in r else r[rel] for r in result] return prefix 、.join(str(n) for n in names)graph.run(cypher, aentity_a).data()是 py2neo 里带参数查询的标准写法参数用$a占位能防止 Cypher 注入。这段回答逻辑有一个细节result为空时返回「数据不全」而不是抛异常在演示场景里不会让页面直接 500。这是毕设系统里一个很小但很加分的工程习惯。这个问答的实现思路可以直接移植到别的领域——把人物表换成课程表把关系换成「先修课」就是一门课程知识图谱问答系统。所以源码的价值不只在红楼梦本身更在这套「实体抽取 → 意图识别 → 模板映射」的管线结构上。5. 避坑指南Neo4j 连不上、页面白屏、中文乱码全在这里这个项目我完整复现过两遍也帮人排查过好几回。凡是报错基本集中在下面几个点我按踩坑频率从高到低写下来每一条都是能直接照做的。5.1 现象py2neo 报ConnectionError: Cannot connect to bolt://localhost:7687原因绝大多数时候不是你代码写错了而是 Neo4j 服务根本没启动或者启动的是 Neo4j 4.x而py2neo是旧版本不兼容。这套源码里的py2neo通常是 4.x 或 5.x对应的 URL 格式和 auth 写法有差异。解决先确认系统托盘或终端里 Neo4j 的状态neo4j status能看到运行情况然后检查requirements.txt锁定的 py2neo 版本如果是 4.x 但 Neo4j 是 5.x把它们统一为 Neo4j 4.4 py2neo 4.4 的组合这个搭配最稳。提示如果浏览器能打开http://localhost:7474但代码连不上几乎可以断定是 7687 端口没监听。Windows 上可以netstat -ano | findstr 7687验证。5.2 现象改了 Neo4j 密码后原来的脚本连不上了原因第一次启动 Neo4j 会强制改初始密码neo4j/neo4j改成123456之后脚本没更新还是旧密码。解决两处要同步——build_graph.py和app.py里的auth(neo4j, 123456)都要改成你设的密码。建议直接把密码统一写进一个config.py全局引用避免三处重复改。5.3 现象前端页面打开是白屏控制台报ECharts is not defined原因index.html里引用了static/vendor/echarts.min.js但这个文件在资源包里缺失或者路径层级不对。很多下载资源为了省体积会把本地 JS 删掉只留了 CSS。解决打开templates/index.html看 script 标签的src路径一个坑一个坑地对。缺失的echarts.min.js可以从 ECharts 官网下 5.x 版本放到对应目录API 兼容性没问题。5.4 现象图谱和问答结果里中文变\u2014或方框原因CDN 的字体或 HTML 没有声明 UTF-8。有些资源包里的index.html顶部没有meta charsetutf-8或者 Neo4j 里读出来的数据本身就是乱码建库时读的是 GBK 编码的 CSV。解决先给index.html的head加上 charset 声明再用python -c import csv; print(open(data/characters.csv, encodinggbk).read()[:200])验证数据源编码如果是 GBK就把build_graph.py里的encodingutf-8-sig改成encodinggbk。5.5 现象重复运行 build_graph.py 后人物节点翻倍原因建节点的脚本没有做「存在性检查」每次运行都无条件create新节点。解决入库前先查一次再决定是否mergep graph.nodes.match(Person, namerow[name]).first() if p is None: p Node(Person, namerow[name], ...) graph.create(p) else: # 节点已存在按需更新属性 graph.push(p)graph.nodes.match(...).first()是 Neo4j 里按属性查节点的通用手法first()在没查到结果时返回None。这段代码本质是「先查后建」能保证脚本重跑任意次结果都一样也就是幂等。毕设文档里如果写上「图谱构建支持重复执行」老师会觉得你考虑过可维护性是个加分项。6. 验证图谱数据质量的三个方法以及怎么把红楼梦换成其他小说图谱跑通之后别急着答辩先做一轮数据质量验证。第一招是查孤立节点MATCH (p:Person) WHERE NOT (p)--() RETURN p.name看有没有完全没有任何关系的人物。一个节点都没有时才说明关系数据完整有也正常因为你可能故意保留了「只出场一次」的边缘人物。第二招是查核心节点的度数MATCH (p:Person {name:贾宝玉})-[r]-() RETURN count(r) AS degree如果宝玉的度数连 10 都不到那要么关系数据不完整要么你的实体对齐有问题。第三招是验证问答系统的闭环输入「贾宝玉和林黛玉什么关系」回答结果和你人工核对的关系类型一致再输入「贾政的儿子有谁」看返回是否包含贾珠、贾宝玉、贾环当然贾环是赵姨娘所出实体关系建模时注意区分嫡庶这正好可以拿来答辨时讲数据建模细节。把红楼梦换成其他小说你需要改三处。第一处是data/目录下的 CSV 或 JSON把人物和关系数据整体替换字段结构保持不变第二处是person_dict.txt把「宝二爷」「林妹妹」这类别名换成新小说里的昵称体系第三处是问答模板库INTENT_PATTERNS里的关键词——换成三国就是「主公、军师、儿子、夫人」换成水浒就是「师傅、兄弟、岳父」。这套模板用词要贴合语境不是改个字典就能通吃但整体替换成本不高。验证时先用 Neo4j Browser 手工跑几条 Cypher确认数据进去了再调接口别让前端页面替你排错。我在这个项目上最深的体会是毕设源码能不能跑三分靠代码七分靠环境。从那以后我每次拿到类似的 Python 项目都强制自己先走一遍「requirements.txt 安装 → Neo4j 连通性测试 → 数据源编码检查 → 前端静态资源路径核对」这个顺序再谈功能修改。整个过程里最容易让人放弃的不是算法多难而是你想当然地以为pip install完就能直接跑——这个项目的价值就在逼你正视数据、服务和前端三者的边界。希望帮到你。本文还有配套的精品资源点击获取
返回列表