
简介这是一套基于Python与Flask搭建《红楼梦》人物关系知识图谱的高分毕业设计资源适合计算机、软件工程、人工智能等专业的在校生用于毕设、课设或项目立项演示。压缩包内含完整源码、数据集与详细文档覆盖数据清洗、人物关系抽取、图谱构建、可视化展示和问答交互等主要环节代码经过运行验证可直接使用或二次扩展。资源共248个文件核心包括8个Python后端程序、4个HTML页面以及CSS、JavaScript、字体和大量JPG演示图片整体约5.68MB目录结构便于按模块查阅。目前已有563人学习下载。借助详细文档和已跑通的工程读者能快速理解Flask与前端可视化联动方式也能掌握知识图谱问答系统从数据到交互落地的完整设计思路。1. 从《红楼梦》人物关系说起这个毕设项目到底拆了什么《红楼梦》120 回里有名有姓的角色超过 700 个主仆、亲属、姻亲、同门关系层层嵌套光靠回目索引根本回答不了贾赦和贾政是什么亲林黛玉进贾府后谁在照顾她这类问题。这套基于 Python Flask 的知识图谱项目把人物文本数据清洗成结构化节点和关系边导入 Neo4j 图数据库对外暴露关系查询接口、ECharts 人物关系可视化页面以及基于模板匹配的中文问答接口。整套代码走完等于亲手做了一遍数据清洗 → 图建模 → 接口封装 → 前端渲染 → 问答检索的完整闭环。尤其适合计算机相关专业的毕设参考也适合想入门知识图谱开发的 Python 工程师。数据量不大本地跑起来很快换成本地文化作品或某个行业领域同样成立。2. 图数据建模与 Neo4j 批量入库先把人物和关系结构化2.1 为什么这类问题更适合图数据库人物关系查询的核心是任意两个人之间的关联路径。比如问贾宝玉和林黛玉什么关系不是查一张表就能回答的它可能需要先命中表兄妹这条直接边也可能要绕道王夫人这条链路才能解释清楚。在 MySQL 里实现二度以上关系查询要写多层 JOIN人物数量和关系类型一变多SQL 就失控了查询意图稍有变化整条语句都要重写。Neo4j 把图遍历作为原生能力Cypher 里一条MATCH path(a:Person)-[:*1..3]-(b:Person)就能表达多跳路径执行计划也会针对边遍历做优化。这套项目几百个节点、上千条关系图数据库的性能优势还不明显但选型给后续做最短关联路径、社区发现、亲密度排序留了空间。我拆这套代码时最关注的是数据模型。人物节点上定义了 id、name、alias、gender、generation、description、clan 字段generation不是必填的但做同辈人物聚合时非常好用。关系类型在数据集中归成 11 种主流关系从血缘、婚姻到主仆和社交优先级是后面可视化配色的依据。特别注意一点项目没有把贾宝玉和宝玉拆成两个节点而是用 alias 字段做多值存储避免图谱里出现两个孤立实体导致查询结果断裂。实体关键属性示例人物节点name, alias, gender, generation, clan林黛玉alias 含颦儿黛玉关系边relation_type, source, target, weight贾政 → 贾宝玉父子权重由亲密度决定关系类型11 类夫妻、父子、母子、兄弟、姐妹、主仆、师生、好友等权重越高排序越靠前2.2 数据集清洗别名归一与空值兜底拿到原始数据第一步要做去重和键值校验。我一般会先把 csv 或 json 读进来逐条检查 name 是否为空、关系两端是否指向同一个人、是否存在重复关系三元组。下面的代码是一段通用清洗逻辑import csv def load_persons(path): persons {} with open(path, encodingutf-8-sig) as f: for row in csv.DictReader(f): name (row.get(name) or ).strip() if not name: continue person { id: row.get(id) or name, name: name, alias: [a.strip() for a in (row.get(alias) or ).split(|) if a.strip()], gender: (row.get(gender) or 未知).strip(), clan: (row.get(clan) or ).strip(), desc: (row.get(description) or ).strip() } persons[name] person return persons这段代码做了三件事用utf-8-sig去掉 csv 的 BOM 头避免首列中文乱码过滤掉名字为空的行防止后续建节点时空指针alias 字段约定用竖线分隔多个别名方便在问答系统的实体抽取阶段直接复用。清洗阶段不把别名归并好后面就会把颦儿判成未登录词。关系数据的坑主要在重复记录上。比如贾宝玉-林黛玉-表兄妹在数据集里可能出现方向相反的两条三元组此时要么按关系类型语义保留单向边要么在入库时用 MERGE 语句去重。另一个坑是关系端点指向了没在人物表里出现的名字我的处理方式是把这个名字补成新节点而不是直接丢弃很多关系链只有补进去才完整。2.3 批量入库用 MERGE 而不是 CREATE批量导入的关键是用 py2neo 的 Graph 对象配合事务提交不要在 for 循环里逐条写 Cypher。先 MERGE 保证节点存在再建关系重复执行脚本不会把数据翻倍。核心代码如下from py2neo import Graph, Node, Relationship graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) def build_graph(persons, relations): tx graph.begin() node_map {} for person in persons: node Node(Person, nameperson[name], genderperson[gender]) if person.get(desc): node[description] person[desc] tx.merge(node, Person, name) node_map[person[name]] node for rel in relations: src node_map.get(rel[source]) dst node_map.get(rel[target]) if not src or not dst: continue tx.merge(Relationship(src, rel[type].upper(), dst)) tx.commit()这段代码有两个容易忽略的细节。tx.merge(node, Person, name)的第三个参数是唯一键保证同名人物只保留一个节点关系上的 merge 会把起点、类型、终点相同的三元组合并。入库完成后跑两条统计查询验证数据分布MATCH (p:Person) RETURN count(p) AS persons; MATCH ()-[r]-() RETURN type(r) AS rel_type, count(*) AS cnt ORDER BY cnt DESC;如果边数明显高于数据集里的关系行数多半是重复关系没清干净回到清洗阶段检查去重逻辑。2.4 孤立角色与异常关系边的处理图谱导入完成后有一种很容易被忽视的问题没有任何关系边的孤儿节点。这类人物在问答和可视化里都检索不到却占着前端布局空间。用下面这条 Cypher 快速找出来MATCH (p:Person) WHERE NOT (p)--() RETURN p.name LIMIT 50;我在构建脚本里会直接执行这条查询把孤立节点输出成列表人工核对。是数据缺失导致的就回原数据集补关系如果这个角色只在旁白里出现过一次那就在导入阶段过滤掉。做完这一步前端渲染时就不会出现漂在画布角落的游离单点。3. Flask 查询服务与关系 API给前端一张干净的图数据3.1 项目路由与蓝图组织后端拆成三个模块app.py负责创建 Flask 实例和注册蓝图api/下放关系查询和问答接口core/里放 Neo4j 连接封装。这样把图数据库访问和 HTTP 接口解耦以后加查询逻辑不用动路由层。工厂模式创建应用from flask import Flask from api.graph import graph_bp from api.qa import qa_bp def create_app(): app Flask(__name__) app.config.from_object(config.Config) app.register_blueprint(graph_bp, url_prefix/api/graph) app.register_blueprint(qa_bp, url_prefix/api/qa) return appurl_prefix 把关系接口和问答接口分成两组前端调用时语义更清晰。config 里放 Neo4j 的 URI、用户名、密码和最大连接数开发时写在本地配置文件部署时改成环境变量读取。调试中发现 Flask debug 重载器和 py2neo 连接池偶尔有端口占用问题我的做法是只在非 debug 模式下预热连接池。3.2 关系查询接口一度关系和二度关系最核心的接口是GET /api/graph/relations?name贾宝玉返回这个人物的节点信息、直接关系边和必要的二度关系节点。二度关系做了限制只返回权重排在前面的目标节点from flask import Blueprint, request, jsonify from py2neo import Graph graph_bp Blueprint(graph, __name__) graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) graph_bp.route(/relations) def get_relations(): name request.args.get(name, ).strip() if not name: return jsonify({code: 400, msg: name is required}), 400 query MATCH (p:Person {name: $name})-[r]-(n:Person) WITH p, n, r ORDER BY r.weight DESC LIMIT 25 RETURN n.name AS name, collect(DISTINCT {name: type(r), id: id(r)}) AS rels try: with graph.session() as session: records session.run(query, namename).data() return jsonify({code: 0, data: records}) except Exception as e: return jsonify({code: 500, msg: str(e)}), 500这段代码有三个关键点。第一$name是参数化查询避免把用户输入直接拼进 Cypher这个习惯在公开部署时能挡住 Cypher 注入。第二ORDER BY r.weight DESC LIMIT 25刻意把单节点可视边数控制在 25 条以内页面渲染效果最稳定的上限基本就是 20 到 30。第三collect(DISTINCT ...)把同一个人物相关的多种关系聚合到一条记录前端只需按关系类型给边着色不需要再对边去重。会话用with上下文管理连接会自动归还池中。3.3 人物详情与全量路径查询除了关系图数据还要有人物详情接口返回人物基本属性和所有直接关系供侧边栏面板展示graph_bp.route(/person) def get_person(): name request.args.get(name, ).strip() query MATCH (p:Person {name: $name}) OPTIONAL MATCH (p)-[r]-(n:Person) RETURN p.name AS name, p.gender AS gender, p.description AS description, collect({rel: type(r), other: n.name}) AS relations with graph.session() as session: result session.run(query, namename).data() if not result: return jsonify({code: 404, msg: person not found}), 404 return jsonify({code: 0, data: result[0]})OPTIONAL MATCH用得很关键。如果直接用MATCH一个没有任何关系边的角色会让查询返回空集详情面板什么都展示不出来。而OPTIONAL MATCH保证至少返回一行collect 在没有关系时得到空列表JSON 结构始终稳定。接口对空参数和不存在的人物都做了兜底。3.4 连接池与请求日志处理实际项目里直接把 Graph 对象放在路由层会让所有模块依赖同一个全局连接不利于后期切换图库。更稳妥的做法是用官方驱动封装一个客户端from neo4j import GraphDatabase class Neo4jClient: def __init__(self, uri, user, password): self.driver GraphDatabase.driver(uri, auth(user, password)) def query(self, cypher, paramsNone): with self.driver.session() as session: return session.run(cypher, params).data()py2neo 的 ORM 风格在建节点和关系时方便但高并发场景下官方驱动的性能更好类型约束也更严格。driver 要做成模块级单例每次请求创建新连接是典型的反模式。调试时我习惯在 Flask 的after_request回调里打印接口耗时观察哪些 Cypher 走了全表扫描这对接下来的索引优化很有帮助。4. 模板驱动的问答系统把自然语言翻译成 Cypher4.1 问题类型拆解与意图分类设计问答部分最吸引眼球也是答辩时最容易讲清楚的一块。受限于数据标注成本项目用的是模板匹配而不是深度学习模型先把问句归一化再用正则抽取实体和关系词映射成 Cypher 查询执行。这种方案在毕设场景下是合理的开发成本低、可解释性强、每一条模板都能对应到图数据库里的一条查询逻辑。从样例数据中可以归纳出四类高频问题意图问法示例对应查询目标双实体关系贾宝玉和林黛玉是什么关系查找两人之间的路径或直接边单实体关系查询林黛玉的父亲是谁查找指定类型的关系邻居人物简介介绍一下王熙凤返回节点属性信息社交圈扩展和贾宝玉关系最好的人有哪些按权重排序返回邻居节点模板库设计原则是宁多勿缺每种意图至少准备三种不同问法。关系最好这类模糊表达最终落成ORDER BY weight DESC LIMIT 10的排序查询权重是数据集里预先标好的亲密度。4.2 实体抽取与别名映射最长匹配优先实体识别直接复用清洗阶段维护的别名表。最常见的坑是贾宝玉被拆成贾宝和玉所以代码里必须做最长优先匹配import re def merge_person_list(persons): name_map {} for p in persons: names [p[name]] p.get(alias, []) for n in names: name_map[n] p[name] return sorted(name_map.items(), keylambda x: len(x[0]), reverseTrue) def extract_entities(question, ordered_names): entities [] for name, canonical in ordered_names: if re.search(name, question): entities.append(canonical) return list(set(entities))按长度降序后长词优先被捕获避免玉先于林黛玉命中。re.search不加锚定符人名出现在句首、句中还是句尾都能命中。如果问句同时命中两个实体就走双实体关系模板只命中一个时再结合关系词判断是人物详情还是相关人物查询。关系词抽取用一张谓词映射表父亲爹爸爸都映射到FATHER关系类型服侍伺候映射到SERVANT。4.3 从问句到 Cypher 的查询生成与答案格式化拿到实体和谓词之后核心逻辑是生成 Cypher、执行查询、再把图数据转成自然语言。双实体关系查询的完整实现qa_bp.route(/ask, methods[POST]) def ask(): data request.get_json() question data.get(question, ) entities extract_entities(question, ordered_names) if len(entities) 2: return jsonify({answer: 我还不太明白换种说法试试}) a, b entities[0], entities[1] cypher MATCH p shortestPath((a:Person {name:$a})-[r*1..3]-(b:Person {name:$b})) UNWIND relationships(p) AS rel RETURN [x IN nodes(p) | x.name] AS path, [rel IN relationships(p) | type(rel)] AS rel_types with graph.session() as session: record session.run(cypher, aa, bb).data() if not record: return jsonify({answer: 暂时没找到这两个人物之间的联系}) path record[0][path] rels record[0][rel_types] answer - .join( [f{path[i]}({rels[i]}){path[i1]} for i in range(len(rels))] ) return jsonify({answer: answer})shortestPath配合*1..3的变长匹配求两个人之间的最短关联链再把节点名和关系类型提取出来拼成可读文本。实际部署时路径长度上限要卡住因为图谱连通性好的情况下*1..5会返回大量中间路径明显拖慢响应。答案格式化时把关系类型换回中文说法比如FATHER显示成父亲。4.4 兜底策略与后续演进方向未命中任何模板时接口不能返回空字符串我会给一句固定话术同时把问句和候选答案记录到日志文件方便后续补模板。谁和贾宝玉关系最亲近这类排行榜问题映射成权重倒序查询红楼梦里有多少人物这类统计问题直接映射成MATCH (n:Person) RETURN count(n)。再往后想提升问答覆盖率可以在意图识别层接入大模型 API 做兜底分类模板匹配作为快速通道模型兜底之前没见过的问法。数据量不大整个问答链路响应压在 50ms 内毕设演示完全够用。5. 可视化渲染、节点裁剪与图数据库索引调优5.1 前端关系图的数据接入前端资源里可以看到 bootstrap.min.css、nifty.min.css 这些 Nifty Admin 风格的静态文件页面骨架是基于后台模板改造的核心图表组件是 ECharts 的 graph 系列。接口返回的数据映射成 nodes 和 links 两个数组节点至少包含 name、category、symbolSize边包含 source、target、relation 名称const chart echarts.init(document.getElementById(graph)); const option { series: [{ type: graph, layout: force, roam: true, data: nodes, links: links, label: { show: true, fontSize: 10 }, force: { repulsion: 200, edgeLength: 80, gravity: 0.1 }, lineStyle: { color: source, curveness: 0.1 } }] }; chart.setOption(option);force 布局里 repulsion 和 edgeLength 的取值直接影响观感值太小节点挤成一团值太大图谱被拉得很稀疏。中文标签在小尺寸节点上会互相遮挡建议对低权重节点关闭 labelhover 时通过 tooltip 展示全名。5.2 节点与边数量裁剪策略查询贾宝玉这类中心人物时一度关系加二度关系很容易超过 150 个节点ECharts 明显掉帧。我的裁剪策略是被查询人物本身必须保留其余节点按连接边的权重降序取前 N 个边总量控制在 400 条以内。在前端过滤比在 Cypher 里过滤更灵活因为可以结合当前视口大小动态调整阈值缩小时少渲染放大时再请求一次接口补全细节。5.3 创建索引并验证查询计划Neo4j 默认不会对普通属性建索引MATCH (p:Person {name:贾宝玉})会走全表扫描。启动图库后先执行CREATE INDEX person_name IF NOT EXISTS FOR (p:Person) ON (p.name);然后通过PROFILE关键字验证查询计划确认 db hits 数量明显下降。人物表只有几百条时差距不明显图谱扩展到上万节点后没有索引的问答接口延迟会从毫秒级涨到秒级。另外还有一个容易踩的坑包含collect和ORDER BY的复合查询执行计划里排序发生在聚合前还是聚合后结果语义完全不同。要让ORDER BY在聚合前完成否则返回的记录根本没有按权重排序。跑这套代码的时候顺序是先验证索引命中再调前端裁剪阈值这两步真正做完你对整套系统的理解会比停留在首页图表深得多。本文还有配套的精品资源点击获取