简介:这是一套基于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, encoding="utf-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", name=person["name"], gender=person["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, name=name).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, name=name).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, params=None): with self.driver.session() as session: return session.run(cypher, params).data()py2neo 的 ORM 风格在建节点和关系时方便,但高并发场景下官方驱动的性能更好,类型约束也更严格。driver 要做成模块级单例,每次请求创建新连接是典型的反模式。调试时我习惯在 Flask 的after_request回调里打印接口耗时,观察哪些 Cypher 走了全表扫描,这对接下来的索引优化很有帮助。
4. 模板驱动的问答系统:把自然语言翻译成 Cypher
4.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(), key=lambda x: len(x[0]), reverse=True) 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, a=a, b=b).data() if not record: return jsonify({"answer": "暂时没找到这两个人物之间的联系"}) path = record[0]["path"] rels = record[0]["rel_types"] answer = " <-> ".join( [f"{path[i]}({rels[i]}){path[i+1]}" 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 的取值直接影响观感:值太小节点挤成一团,值太大图谱被拉得很稀疏。中文标签在小尺寸节点上会互相遮挡,建议对低权重节点关闭 label,hover 时通过 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在聚合前完成,否则返回的记录根本没有按权重排序。跑这套代码的时候,顺序是先验证索引命中,再调前端裁剪阈值,这两步真正做完,你对整套系统的理解会比停留在首页图表深得多。
本文还有配套的精品资源,点击获取