☰
红楼梦人物关系图谱与问答系统:Python知识图谱构建实战
2026/10/12 22:04:59 网站建设 项目流程

简介:这是一份面向计算机相关专业毕业设计、课程设计与项目实战练习者的完整源码包,以Python结合知识图谱技术实现红楼梦人物关系可视化与智能问答,适合正在准备毕设或需要大作业参考的学生直接上手。资源包含可运行的全部代码与配套文档说明,项目经导师指导并获评审99分认可,代码结构完整、注释清晰,零基础也能按文档逐步跑通。压缩包共约240个文件、5.71MB,其中184张jpg图片用于人物头像与界面素材,8个py文件承载知识图谱构建、关系抽取与问答逻辑,另有css、js、html等前端页面文件及json、txt配置数据,整体目录按功能模块划分,便于检索与二次开发。目前已有133人学习下载。读者可获得一套从数据建模、图谱存储到可视化展示与问答交互的完整实现方案,并借助文档快速理解各模块职责与运行流程,为答辩与后续扩展提供扎实基础。

1. 红楼梦人物关系可视化与问答系统:从文本到图谱的工程落地

《红楼梦》人物关系错综复杂,四大家族、主仆、姻亲、诗社往来交织在一起,用传统表格或思维导图根本理不清。这个毕业设计题目的核心,是用 Python 把书里的人物和关系抽出来,构建一张知识图谱,再在上面做两件事:一是可视化展示人物关系网络,二是支持自然语言问答。它解决的是“人物关系查询靠翻书、靠记忆”的低效问题,适合中文、计算机相关专业做毕设的同学,也适合想入门知识图谱构建的 Python 开发者。整条链路涉及文本预处理、实体关系抽取、图数据库存储、前端可视化和问答接口,麻雀虽小五脏俱全。下面按实际落地顺序拆开讲,每一步都给可复现的命令和参数。

2. 知识图谱构建:从红楼梦文本到三元组

2.1 为什么选 Neo4j 而不是 NetworkX 做存储

很多同学第一反应是用 NetworkX 画图,但 NetworkX 是内存图计算库,不适合做持久化存储和复杂查询。知识图谱问答需要频繁做多跳关系查询,比如“贾宝玉的父亲的妻子是谁”,这种查询用 Cypher 在 Neo4j 里一行搞定,用 NetworkX 得手写遍历逻辑。Neo4j 的社区版免费,支持 Cypher 查询语言,Python 通过 py2neo 或官方 neo4j 驱动连接,是毕设级别最稳妥的选择。

安装 Neo4j 桌面版后,默认 Bolt 端口 7687,HTTP 端口 7474。启动后浏览器打开http://localhost:7474,初始用户名和密码都是neo4j,首次登录会强制改密码。Python 端安装驱动:

pip install neo4j py2neo

连接测试:

from neo4j import GraphDatabase # 替换成你设置的密码 driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "你的密码")) def test_connection(tx): result = tx.run("RETURN 'connected' AS status") return result.single()["status"] with driver.session() as session: print(session.execute_read(test_connection))

逻辑说明:GraphDatabase.driver建立连接池,session.execute_read执行只读查询。参数auth是元组,第一个是用户名,第二个是密码。如果报ServiceUnavailable,先确认 Neo4j 服务已启动,再检查防火墙是否放行 7687 端口。

2.2 人物实体抽取:规则匹配加词典兜底

红楼梦人物有几百个,全用 NER 模型标注成本太高。毕设场景下,最稳的做法是“人物词典 + 规则匹配”。先从公开的红楼梦人物列表整理一份 CSV,包含姓名、别名、身份。比如“贾宝玉”别名“宝玉”“宝二爷”,“林黛玉”别名“黛玉”“颦儿”。然后遍历章节文本,用别名做匹配。

import csv import re # 加载人物词典 def load_characters(path): characters = {} with open(path, "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: name = row["name"] aliases = row["aliases"].split("|") if row["aliases"] else [] characters[name] = aliases return characters # 构建正则模式,长别名优先匹配,避免"宝玉"匹配到"贾宝玉"时截断 def build_pattern(characters): alias_map = {} for name, aliases in characters.items(): for alias in aliases + [name]: alias_map[alias] = name # 按长度降序排列,保证"贾宝玉"先于"宝玉"匹配 sorted_aliases = sorted(alias_map.keys(), key=len, reverse=True) pattern = re.compile("|".join(re.escape(a) for a in sorted_aliases)) return pattern, alias_map # 对每段文本抽取人物 def extract_characters(text, pattern, alias_map): found = set() for match in pattern.finditer(text): found.add(alias_map[match.group()]) return list(found)

逻辑说明:build_pattern把别名按长度降序拼成正则,确保“贾宝玉”不会被“宝玉”抢先匹配。extract_characters返回标准名列表。参数方面,人物词典 CSV 至少要有name和aliases两列,别名用竖线分隔。如果召回率不够,可以加入“老太太”“凤姐”这类口语称呼。

2.3 关系抽取:共现窗口加规则模板

关系抽取比实体抽取更难。毕设级别不需要上深度学习,用“共现 + 规则”就能覆盖大部分主仆、亲属关系。具体做法:以段落为窗口,同一段里出现的人物两两建立CO_OCCUR关系;再用规则模板匹配“A是B的X”这类句式,抽取出具体的FATHER、MOTHER、SPOUSE等关系。

# 关系规则模板 RELATION_PATTERNS = [ (re.compile(r"(.{2,4})是(.{2,4})的(父亲|父亲大人|爹)"), "FATHER"), (re.compile(r"(.{2,4})是(.{2,4})的(母亲|娘|妈)"), "MOTHER"), (re.compile(r"(.{2,4})是(.{2,4})的(妻子|夫人|老婆)"), "SPOUSE"), (re.compile(r"(.{2,4})是(.{2,4})的(儿子|女儿)"), "CHILD"), ] def extract_relations(text, characters, alias_map): relations = [] # 规则匹配 for pattern, rel_type in RELATION_PATTERNS: for match in pattern.finditer(text): a, b = match.group(1), match.group(2) a_std = alias_map.get(a, a) b_std = alias_map.get(b, b) if a_std in characters and b_std in characters: relations.append((a_std, rel_type, b_std)) # 共现关系 found = extract_characters(text, build_pattern(characters)[0], alias_map) for i in range(len(found)): for j in range(i + 1, len(found)): relations.append((found[i], "CO_OCCUR", found[j])) return relations

逻辑说明:规则模板用捕获组提取主语、宾语和关系词,再通过alias_map归一化到标准名。共现关系作为补充,保证图谱连通性。参数上,共现窗口建议按段落切分,不要按整章,否则关系太稀疏。如果发现关系数量爆炸,可以限制共现只保留出现次数大于 2 的人物对。

2.4 批量写入 Neo4j:用 MERGE 避免重复节点

抽取完三元组后,写入 Neo4j 要用MERGE而不是CREATE,否则同一个人物会创建多个节点。批量写入用UNWIND提高效率。

def write_relations(driver, relations): query = """ UNWIND $rels AS rel MERGE (a:Person {name: rel.source}) MERGE (b:Person {name: rel.target}) MERGE (a)-[r:RELATION {type: rel.type}]->(b) """ with driver.session() as session: session.run(query, rels=[ {"source": s, "type": t, "target": o} for s, t, o in relations ]) # 调用 relations = extract_relations(text, characters, alias_map) write_relations(driver, relations)

逻辑说明:UNWIND把列表展开成多行,MERGE保证节点和关系不存在时才创建。参数$rels是参数化查询,避免 Cypher 注入。如果关系类型很多,可以把RELATION改成动态类型,但 Neo4j 不支持动态关系类型,所以用属性type区分。查询时用WHERE r.type = 'FATHER'过滤。

3. 人物关系可视化:用 Pyecharts 做可交互网络图

3.1 为什么选 Pyecharts 而不是 Matplotlib

Matplotlib 画网络图节点一多就糊成一团,而且没有交互。Pyecharts 基于 ECharts,支持拖拽、缩放、点击高亮,导出 HTML 后可以直接嵌到论文或答辩 PPT 里。安装:

pip install pyecharts

从 Neo4j 查数据:

from pyecharts import options as opts from pyecharts.charts import Graph def get_graph_data(driver): query = """ MATCH (a:Person)-[r:RELATION]->(b:Person) RETURN a.name AS source, b.name AS target, r.type AS rel_type LIMIT 500 """ with driver.session() as session: result = session.run(query) nodes = set() links = [] for record in result: nodes.add(record["source"]) nodes.add(record["target"]) links.append({ "source": record["source"], "target": record["target"], "label": {"show": True, "formatter": record["rel_type"]} }) return list(nodes), links def render_graph(nodes, links, output="hongloumeng.html"): node_data = [{"name": n, "symbolSize": 20} for n in nodes] graph = ( Graph() .add( "", node_data, links, repulsion=8000, layout="force", edge_label=opts.LabelOpts(is_show=False), linestyle_opts=opts.LineStyleOpts(width=1, curve=0.2), ) .set_global_opts( title_opts=opts.TitleOpts(title="红楼梦人物关系图谱"), legend_opts=opts.LegendOpts(is_show=False), ) ) graph.render(output)

逻辑说明:repulsion控制节点斥力,值越大节点越分散,建议 5000 到 10000 之间调。layout="force"是力引导布局,适合人物关系图。curve=0.2让边稍微弯曲,避免重叠。如果节点超过 200 个,建议先按关系类型过滤,只展示FATHER、MOTHER、SPOUSE等核心关系,否则浏览器会卡。

3.2 节点分类着色:按家族或身份分组

红楼梦里贾、史、王、薛四大家族,用不同颜色区分能一眼看出阵营。在节点数据里加category字段,Pyecharts 会自动按类别着色。

# 人物到家族的映射,可以从词典 CSV 里读 FAMILY_MAP = { "贾宝玉": "贾家", "贾母": "贾家", "贾政": "贾家", "林黛玉": "林家", "薛宝钗": "薛家", "王熙凤": "王家", # ... 补充完整 } def build_categories(nodes): categories = [{"name": "贾家"}, {"name": "林家"}, {"name": "薛家"}, {"name": "王家"}, {"name": "其他"}] node_data = [] for n in nodes: family = FAMILY_MAP.get(n, "其他") node_data.append({"name": n, "symbolSize": 20, "category": family}) return node_data, categories # 在 add 里传入 categories 参数 graph.add("", node_data, links, categories=categories, ...)

逻辑说明:category字段对应categories列表里的name,Pyecharts 自动分配颜色。参数上,symbolSize可以根据人物出现频次动态调整,频次高的节点大一些,视觉上更突出核心人物。

4. 问答系统:用模板匹配加 Cypher 查询实现自然语言问答

4.1 问答架构:意图识别加槽位填充

毕设级别的问答不需要上大模型,用“意图识别 + 槽位填充 + Cypher 模板”就能覆盖大部分问题。意图分三类:查关系(“贾宝玉的父亲是谁”)、查属性(“林黛玉的别名是什么”)、查路径(“贾宝玉和林黛玉什么关系”)。槽位就是人物名和关系类型。

import re # 意图模板 INTENT_PATTERNS = [ (re.compile(r"(.{2,4})的(父亲|母亲|妻子|儿子|女儿)是谁"), "query_relation"), (re.compile(r"(.{2,4})和(.{2,4})什么关系"), "query_path"), (re.compile(r"(.{2,4})的别名"), "query_alias"), ] def parse_question(question, alias_map): for pattern, intent in INTENT_PATTERNS: match = pattern.search(question) if match: slots = [alias_map.get(g, g) for g in match.groups() if g] return intent, slots return None, []

逻辑说明:parse_question返回意图和槽位列表。参数上,正则里的.{2,4}限制人物名长度,避免匹配到整句话。如果问题里出现别名,通过alias_map归一化。

4.2 Cypher 查询模板:把意图翻译成图查询

CYPHER_TEMPLATES = { "query_relation": """ MATCH (a:Person {name: $name})-[:RELATION {type: $rel_type}]->(b:Person) RETURN b.name AS answer """, "query_path": """ MATCH path = shortestPath((a:Person {name: $name1})-[*..4]-(b:Person {name: $name2})) RETURN [n IN nodes(path) | n.name] AS path_nodes """, "query_alias": """ MATCH (a:Person {name: $name}) RETURN a.aliases AS aliases """, } def answer_question(driver, question, alias_map): intent, slots = parse_question(question, alias_map) if not intent: return "抱歉,我暂时无法理解这个问题。" if intent == "query_relation": name, rel_word = slots[0], slots[1] rel_type = {"父亲": "FATHER", "母亲": "MOTHER", "妻子": "SPOUSE", "儿子": "CHILD", "女儿": "CHILD"}.get(rel_word) if not rel_type: return "不支持的关系类型。" with driver.session() as session: result = session.run(CYPHER_TEMPLATES[intent], name=name, rel_type=rel_type) answers = [r["answer"] for r in result] return "、".join(answers) if answers else "没有查到相关关系。" elif intent == "query_path": name1, name2 = slots[0], slots[1] with driver.session() as session: result = session.run(CYPHER_TEMPLATES[intent], name1=name1, name2=name2) record = result.single() if record: return " -> ".join(record["path_nodes"]) return "没有找到关系路径。" elif intent == "query_alias": name = slots[0] with driver.session() as session: result = session.run(CYPHER_TEMPLATES[intent], name=name) record = result.single() if record and record["aliases"]: return "、".join(record["aliases"]) return "没有别名记录。"

逻辑说明:query_relation用关系类型过滤,query_path用shortestPath找最短路径,限制 4 跳以内避免全图扫描。参数上,$name、$rel_type是参数化查询,防止注入。如果路径查询返回空,可能是图谱里两个人物之间没有连通路径,需要检查共现关系是否写入。

4.3 用 Flask 包一个问答接口

from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/qa", methods=["POST"]) def qa(): data = request.get_json() question = data.get("question", "") answer = answer_question(driver, question, alias_map) return jsonify({"question": question, "answer": answer}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)

逻辑说明:/qa接收 POST 请求,JSON 体里带question字段,返回answer。参数上,debug=True只在开发时用,部署时关掉。前端可以用简单的 HTML 页面发 fetch 请求,也可以直接 curl 测试:

curl -X POST http://localhost:5000/qa -H "Content-Type: application/json" -d '{"question":"贾宝玉的父亲是谁"}'

5. 避坑与排查:红楼梦知识图谱落地时最容易翻车的 5 个点

5.1 人物别名匹配错乱,导致关系张冠李戴

现象:查询“宝玉的父亲”返回“贾政”,但查询“贾宝玉的父亲”返回空。原因:别名映射表里“宝玉”映射到了“贾宝玉”,但正则匹配时“贾宝玉”被“宝玉”截断,导致标准名不一致。解决:在build_pattern里按别名长度降序排列,确保长别名优先匹配。同时,在alias_map里把“宝玉”和“贾宝玉”都映射到同一个标准名。

5.2 Neo4j 写入速度慢,几千条关系跑十几分钟

现象:批量写入时每条关系单独开 session,速度极慢。原因:每次session.run都是一次网络往返。解决:用UNWIND批量写入,一次传几百条关系。另外,给Person.name建唯一约束,能加速MERGE:

CREATE CONSTRAINT person_name IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE;

5.3 Pyecharts 渲染出来一片空白

现象:HTML 打开后只有标题,没有节点。原因:links里的source和target必须是节点列表里存在的name,如果节点数据里没有对应名称,边就画不出来。解决:检查nodes和links是否一致,确保每个边的两端都在节点列表里。另外,repulsion太小会导致节点重叠,调大到 8000 以上。

5.4 问答系统只能回答固定句式

现象:用户问“贾宝玉他爹是谁”,系统无法识别。原因:意图模板只覆盖了“父亲”这个标准词,没有覆盖“爹”“父亲大人”等口语变体。解决:在parse_question之前加一层同义词替换,把“爹”“父亲大人”“父”都替换成“父亲”,再走模板匹配。同义词表可以放在配置文件里,方便扩展。

5.5 路径查询返回空,但两个人明明有关系

现象:查询“贾宝玉和林黛玉什么关系”返回空。原因:图谱里只有CO_OCCUR关系,没有直接的关系类型,而shortestPath默认走所有关系类型,但如果两个人不在同一个连通分量里,就查不到。解决:先确认两个人之间是否有共现关系,如果没有,检查文本预处理时是否漏掉了同时出现这两个人的段落。另外,shortestPath的跳数限制可以放宽到 6 跳,但不要太大,否则查询变慢。

6. 进阶技巧:用 PageRank 找红楼梦的核心人物

图谱建好后,除了问答和可视化,还能做一件很有意思的事:用 PageRank 算法找出红楼梦里的核心人物。Neo4j 的 Graph Data Science 库自带 PageRank,社区版可以用 Cypher 手写简化版,或者用 NetworkX 算完再写回 Neo4j。

import networkx as nx from neo4j import GraphDatabase def build_networkx_graph(driver): G = nx.DiGraph() query = "MATCH (a:Person)-[r:RELATION]->(b:Person) RETURN a.name AS s, b.name AS t, r.type AS type" with driver.session() as session: for record in session.run(query): G.add_edge(record["s"], record["t"], type=record["type"]) return G def rank_characters(G, top_n=10): # 只保留核心关系,去掉共现边,避免噪声 core_edges = [(u, v) for u, v, d in G.edges(data=True) if d["type"] != "CO_OCCUR"] H = G.edge_subgraph(core_edges).copy() pr = nx.pagerank(H, alpha=0.85) return sorted(pr.items(), key=lambda x: x[1], reverse=True)[:top_n] G = build_networkx_graph(driver) top = rank_characters(G) for name, score in top: print(f"{name}: {score:.4f}")

逻辑说明:alpha=0.85是阻尼系数,经典取值。去掉CO_OCCUR边是因为共现关系噪声大,会把丫鬟和主子拉平。跑出来的结果里,贾宝玉、贾母、王熙凤通常排前三,和红学研究的结论基本一致。这个排名可以写回 Neo4j,给节点加pagerank属性,可视化时用节点大小表示。

参数上,top_n建议取 10 到 15,太多就失去区分度。如果发现某个丫鬟排名异常高,检查是不是共现边没过滤干净。另外,PageRank 对有向图和无向图结果不同,红楼梦关系图建议用有向图,因为“父亲”和“儿子”方向不同,权重也不一样。

我自己做这个图谱时,最大的教训是:别一上来就追求全自动抽取。先用词典和规则把核心的几十个人物和关系跑通,可视化出来能看,问答能答对几个典型问题,再逐步加人物、加关系类型。一上来就上深度学习 NER,标注数据不够,调参调到怀疑人生,最后图谱还没建起来。先把最小可用版本跑通,再迭代,这是最稳的路。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询