简介:面向Python大作业与知识图谱入门者的《西游记》知识图谱压缩包,以《西游记》原著中的实体与关系为主线,演示从数据抽取、知识融合到关系抽取的完整构建流程。压缩包共670个文件,其中447个Python脚本为核心实现,搭配135个编译缓存文件、20个说明文本、运行所需的exe与依赖安装包等,整体仅3.83MB,结构轻量,适合直接解读或二次扩展。资源已有547人学习,说明在同类课设资料中具备一定参考价值。通过对项目源码与配置文件的研读,可快速理解图谱建模思路,复用其抽取与存储逻辑,并借助自带运行环境完成演示,节省从零搭建的时间。
1. 拿到《西游记》知识图谱.zip,先分清它是数据包不是电子书
第一次看到《西游记》知识图谱.zip 这个包,大多数人以为是电子书资源,解压后才发现是一堆 CSV、JSON 和 Cypher 脚本——这不是小说,是给图数据库吃的“结构化骨架子”。它把《西游记》里的人物、地点、兵器、事件、阵营拆成节点和关系,让你用一条查询就回答“猪八戒和沙僧在取经路上共同经历了哪些事件”这类在文本里翻半天的问题。对打算入门知识图谱、想跑通 Neo4j 导入全流程的人,以及做文学计算、NLP 语料构建的从业者,这个包是最好的练手样本:数据规模小、语义关系清晰、跑完可视化效果好。下面我按拿到一个真实 zip 包的落地顺序展开,从包内结构、本体建模到 Neo4j 导入、前端可视化与踩坑排查,每一步都能直接抄作业。
2. 拆开 zip 看结构:本体文件、数据文件与导入脚本三件套
2.1 zip 里通常装的是什么:三类文件的职责与读取顺序
这类知识图谱压缩包,无论题材是文学、医疗还是工业设备,内部结构高度相似。先把 zip 解压到一个干净目录,一般能看到三类东西:本体定义文件、实体关系数据文件、导入脚本。不要一上来就打开 CSV 猛看,先找本体文件——它决定了这套数据能回答什么层级的问题。
| 文件类型 | 常见格式 | 作用 | 对应热词 |
|---|---|---|---|
| 本体定义 | .ttl / .owl / schema.json | 定义实体类型、关系类型、属性约束 | 本体建模、语义层 |
| 实体数据 | .csv / .json | 存放人物、地点、兵器等节点数据 | 知识图谱构建 |
| 关系数据 | .csv / .json | 存放节点之间的边,如师徒、交战 | 知识图谱构建 |
| 导入脚本 | .cypher / .py | 把数据灌入图数据库的自动化入口 | neo4j构建知识图谱 |
| 可视化配置 | .html / .json | 前端渲染时的样式与布局参数 | 知识图谱前端插件 |
读取顺序有讲究。我一般先看本体文件,因为数据文件里的字段名可能是缩写的p_id、rel_type,没有本体定义根本猜不出含义。再看导入脚本,它能告诉你作者预期的图数据库是 Neo4j 还是其他引擎,以及有没有踩过约束、索引这些坑。最后才看 CSV 数据,核对字段是否跟脚本对得上。很多人在这一步就翻车了:脚本里用的是MERGE,数据里主键却包含空值,导进去直接报错。先把三件套的对应关系理清,后面所有步骤都顺。
2.2 本体建模决定查询上限:人物、地点、事件三类实体的关系设计
拿《西游记》来说,最核心的实体类型是人物(Person)、地点(Location)、事件(Event),再加上兵器(Weapon)和阵营(Faction)作为辅助维度。关系类型比实体类型更重要,因为它直接决定查询能怎么写。常见的关系设计有:人物之间的“师徒”“敌对”“同门”,人物参与事件的“参与”,地点承载事件的“发生于”,取经队伍途经地点的“途经”。
我把关系设计成一张表,方便你在导入前检查自己的 zip 是否覆盖了这些语义:
| 头节点 | 关系 | 尾节点 | 示例 |
|---|---|---|---|
| 人物 | 师徒 | 人物 | 唐僧 -师徒-> 孙悟空 |
| 人物 | 敌对 | 人物 | 孙悟空 -敌对-> 白骨精 |
| 人物 | 参与 | 事件 | 孙悟空 -参与-> 三打白骨精 |
| 事件 | 发生于 | 地点 | 三打白骨精 -发生于-> 白虎岭 |
| 取经队伍 | 途经 | 地点 | 唐僧 -途经-> 火焰山 |
| 人物 | 拥有 | 兵器 | 孙悟空 -拥有-> 金箍棒 |
这些关系和属性不是拍脑袋定的,背后是本体建模里的“语义层”设计。说得直白一点:语义层决定了你的图是只能做“找邻居”的玩具,还是能支撑起知识管理系统的骨架。比如“参与”和“发生于”这两条边,如果把事件和地点的关系做成事件的属性字段,查询“白虎岭发生过哪些大事”就得扫描所有事件节点再过滤,而做成关系之后,一条MATCH (l:Location {name:'白虎岭'})<-[:发生于]-(e:Event) RETURN e就是索引级命中。这就是语义层和普通字段的差别,也是拿到任何知识图谱 zip 后最值得花时间琢磨的部分。
2.3 回目编号是现成的时序轴:把“第几回”变成可查询维度
《西游记》这类章回体小说有个天然优势:每一回都有编号,这是现成的时序轴。很多知识图谱项目会把它浪费掉,只把回目号当成一个普通属性存在事件节点里,导致“唐僧师徒先到高老庄还是先到流沙河”这种顺序问题根本查不出来。正确做法是把回目号建模成事件节点的chapter属性,并额外建一条NEXT关系把相邻事件串成链。
我处理这类文学知识图谱时,会先跑一条查询确认时间轴有没有建好:
MATCH (e:Event) WHERE e.chapter IS NOT NULL RETURN e.name, e.chapter ORDER BY e.chapter LIMIT 20;如果返回的数据里chapter是字符串,导出时要注意按整数排序,否则第 9 回会排在第 10 回后面,这是数据清洗阶段最常见的坑。如果 zip 里已经把回目建模成属性,我建议你在导入后用toInteger()统一转换,再补建NEXT关系。补建关系的 Cypher 不复杂,用apoc.nodes.sequence或按 chapter 排序后逐对MERGE都行,但前提是章节编号不能有重复,碰上“第一百回”和“第100回”混写的情况,得先做一次规范化。这也是这一类 zip 包里数据质量参差不齐的重灾区。
3. 用 Neo4j 构建《西游记》知识图谱:LOAD CSV 导入与索引参数
3.1 为什么选 Neo4j 而不是关系数据库:多跳查询和图遍历的差距
知识图谱这东西,关系数据库也能存,把实体放一张表、关系放一张表,再 JOIN 起来。但问题在于“多跳查询”。查“孙悟空的朋友的朋友是谁”,MySQL 要写三层 JOIN,跳到五层时 SQL 已经成了天书,而 Neo4j 里一条MATCH (n:Person {name:'孙悟空'})-[:朋友*2]-(m)就够了,跳数只是路径长度参数。这正是“neo4j构建知识图谱”在搜索里热度高的原因:图数据库天生就是为这类遍历设计的。
版本选择上,我用的是 Neo4j Community 5.x,单机足够支撑几十万节点级别的《西游记》图谱。Windows/macOS 直接装 Desktop 版,Linux 服务器用 tarball 解压后跑bin/neo4j console。装完第一件事是改初始密码,默认neo4j/neo4j只是个第一印象的问候,不改的话浏览器访问 7474 端口弹出的强制改密页面会挡掉所有操作。
3.2 用 LOAD CSV 把人物和关系落库:最小命令与字段映射
先演示直接在 Cypher 里用内联数据建节点,不需要额外文件,方便你验证环境:
UNWIND [ {id: 'sunwukong', name: '孙悟空', title: '齐天大圣'}, {id: 'tangseng', name: '唐僧', title: '三藏法师'}, {id: 'baigujing', name: '白骨精', title: '白骨夫人'} ] AS row MERGE (p:Person {id: row.id}) SET p.name = row.name, p.title = row.title;这段代码的逻辑是:UNWIND把列表展开成三行,MERGE按id属性查找节点,存在就跳过,不存在就创建,SET再补全其他属性。id字段是整个导入流程的锚点,必须保证唯一且非空,否则MERGE会退化成CREATE,每次运行都生成重复节点。这也是一个知识点:MERGE匹配的是整个括号里的模式,如果你只写MERGE (p:Person {name: row.name}),碰到两个同名人物就会合到一起,所以我总是带一个稳定主键。
如果你 zip 里的数据是 CSV,就用标准的 LOAD CSV 语句:
:auto USING PERIODIC COMMIT 1000 LOAD CSV WITH HEADERS FROM 'file:///xiyouji/people.csv' AS row MERGE (p:Person {id: row.pid}) SET p.name = trim(row.name), p.title = row.title, p.first_chapter = toInteger(row.first_chapter);参数说明:WITH HEADERS表示第一行是字段名;trim()去掉名字两端的空格,防止“孙悟空 ”和“孙悟空”变成两个节点;toInteger()把回目号从字符串转成整数,排序和范围查询都靠它。file:///xiyouji/people.csv是 Neo4j 的虚拟路径,对应服务器$NEO4J_HOME/import/xiyouji/people.csv,注意是三个斜杠加相对路径。
3.3 导入关系前先建索引和约束:重复导入与查询性能的关键
关系导入比节点导入更容易出问题。节点还能靠MERGE去重,关系如果主键设计得不合理,每跑一次脚本就多一条边。我的惯例是先给节点建约束和索引,再导关系。约束保证不会产生重复主键,索引让MATCH按名字查询时不用全表扫描。
CREATE CONSTRAINT person_id FOR (p:Person) REQUIRE p.id IS UNIQUE; CREATE INDEX person_name_idx FOR (p:Person) ON (p.name);注意 5.x 的语法是REQUIRE ... IS UNIQUE,4.4 之前是ASSERT,跑在旧版本上会报语法错。约束会同时创建一个配套索引,所以person_id不需要单独再建索引;person_name_idx是给按名字模糊查询用的,比如查“所有叫‘行者’的人物”。有了这两个前提,关系导入就可以安全地使用MERGE:
LOAD CSV WITH HEADERS FROM 'file:///xiyouji/relations.csv' AS row MATCH (a:Person {id: row.from_id}) MATCH (b:Person {id: row.to_id}) MERGE (a)-[r:RELATES_TO {type: row.relation}]->(b) ON CREATE SET r.chapter = toInteger(row.chapter);这段代码先匹配两个端点,再创建关系。MERGE后面带了{type: row.relation}三元组,意味着一对节点之间每种关系只保留一条,避免重复导入时堆积多条相同关系。ON CREATE只在真正创建新关系时执行,已存在的关系不会覆盖原属性,这是幂等导入的核心。如果 zip 里的关系文件没有from_id、to_id这种规范字段,需要先做一个字段名映射的中间表,别硬改原始 CSV。导入完成后跑一遍统计,确认数据规模符合预期:
MATCH (n) RETURN labels(n) AS label, count(*) AS cnt; MATCH ()-[r]->() RETURN type(r) AS rel_type, count(*) AS cnt;顺带提一个参数:USING PERIODIC COMMIT 1000表示每处理 1000 行提交一次事务。如果不加,几十万行数据会攒在一个事务里,内存稍小就直接 OutOfMemory。但注意:auto USING PERIODIC COMMIT只能用于导入类语句,不能用在普通MATCH上。我见过有人把这段前缀复制到删除语句前,Neo4j 直接给了语法错误,这不是你写错了,是它本来就不支持。
4. 让图谱能被浏览器打开:前端渲染的三种接法与数据量边界
4.1 先用 Neo4j Browser 验证图渲染:零代码确认数据连通性
导入完成后不要急着写前端,先用 Neo4j Browser 打开http://localhost:7474,登录后跑一条路径查询,确认数据真的“连起来了”。浏览器自带的图渲染面板会把结果渲染成节点和边,拖动一下,看关系是否正确指向目标节点。
MATCH path = (a:Person)-[r]->(b:Person) WHERE a.name IN ['孙悟空', '唐僧', '白骨精'] RETURN path LIMIT 30;LIMIT 30是刻意加的。Browser 的渲染引擎处理几百个节点还可以,几千个节点直接卡死。先用小样本验证连通性,确认“三打白骨精”这条链路能出来,再逐步放宽条件。Browser 看的是数据质量,不是视觉效果,很多读者拿到 zip 后第一步就栽在“图太大渲染不出来在这里”,以为数据有问题,其实只是该上正经前端了。
4.2 用 neo4j-driver 加 vis.js 把图谱嵌进页面:最小可运行示例
如果要把图谱嵌进自己的页面,我常用的组合是 neo4j-driver 的浏览器版 UMD 包加 vis-network。前者负责跟数据库通信,后者负责渲染关系图,两者都支持浏览器直接通过 CDN 引入,不需要打包工具。下面是一个完整的最小 HTML,保存后改一下密码就能跑:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>西游记知识图谱预览</title> <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script> <script src="https://unpkg.com/neo4j-driver/lib/browser/neo4j-web.min.js"></script> </head> <body> <div id="graph" style="width:100%;height:600px;"></div> <script> const driver = neo4j.driver( 'bolt://localhost:7687', neo4j.auth.basic('neo4j', 'password') ); const session = driver.session(); session.run( 'MATCH (p:Person)-[r:RELATES_TO]->(q:Person) RETURN p, r, q LIMIT 100' ).then(result => { const nodes = []; const edges = []; const seen = new Set(); result.records.forEach(rec => { ['p', 'q'].forEach(key => { const node = rec.get(key); if (node && !seen.has(node.identity.toString())) { seen.add(node.identity.toString()); nodes.push({ id: node.identity.toString(), label: node.properties.name }); } }); const rel = rec.get('r'); edges.push({ from: rec.get('p').identity.toString(), to: rec.get('q').identity.toString(), label: rel.properties.type || rel.type, arrows: 'to' }); }); new vis.Network(document.getElementById('graph'), { nodes, edges }); session.close(); driver.close(); }).catch(err => console.error(err)); </script> </body> </html>这段代码的逻辑链是:建立连接 → 执行 Cypher → 遍历结果集 → 用node.identity作为唯一 ID 去重 → 组装 vis.js 的数据结构 → 渲染。identity.toString()这个细节容易踩坑:neo4j-driver 返回的整数是 Integer 对象,直接塞给字符串比较会得到[object Object],必须显式转字符串。seen集合用来去重,否则两个端点指向同一个节点时,vis.js 会画两个重叠的圆点。
LIMIT 100是给渲染性能兜底的。vis.js 在几千个节点的规模下表现还行,超过一万节点,拖拽和缩放就开始掉帧,这就是所谓“知识图谱前端插件”局限性的体现。它适合做展示、教学、小规模探索,不适合做 TB 级知识的入口。
4.3 工业场景下的知识图谱设计:前端这一步和数据库不在同一个量级
用 vis.js 这类通用可视化库去接《西游记》级别的图谱完全没有问题,但读者里如果有人想把这个方案迁移到工业场景下的知识图谱设计,我要泼一盆冷水。工业场景里,设备台账、故障工单、备件库存之间的关系动辄百万级边,浏览器里直接渲染所有节点是不现实的。常见做法是服务端做路径裁剪、节点聚合、按需加载,前端只渲染当前视野内的子图,配合 LOD 层级细节技术控制渲染数量。
这意味着,你从《西游记》这个 zip 里学到的价值,不在于前端插件本身,而在于“如何设计关系模型让它能被高效查询”。工业场景里的知识管理、故障诊断、工单流转,本质都在问同一类问题:“这个设备上次出过什么故障,跟哪个部件相关,修完换了什么备件”,这就是把取经路线里的“途经”换成“关联部件”,把“师徒”关系换成“依赖”关系。图谱模式不变,变的只是实体和关系的语义。所以,别急着给你的页面堆可视化特效,先把查询慢在哪搞清楚:是索引没建,还是图模式设计得不支持这个查询,这些基本功才是在工业场景里能复用和延伸的东西。
5. 解压与导入避坑:伪加密、乱码、路径和数据质量的五处翻车点
5.1 zip 提示需要密码或文件损坏,其实是伪加密标志位作祟
解压《西游记》知识图谱.zip 时,常见的翻车现场是:zip 还没解压完,弹窗要求输入密码;或者明明能解压出一部分文件,个别文件提示“已损坏”。如果你确认这个包没有加密,那大概率是 zip 伪加密。这不是数据真的被加密,而是文件头的通用位标志(general purpose bit flag)里第 0 位被置成 1,解压工具读到这个标志就要求输入密码。
解决思路不是找密码,而是把标志位复位。我一般用一段小脚本直接扫描并修复所有本地文件头的加密标志:
import struct def fix_zip_encryption_flag(zip_path, out_path): with open(zip_path, 'rb') as f: data = bytearray(f.read()) fixed = 0 pos = 0 while pos <= len(data) - 4: if data[pos:pos+4] == b'PK\x03\x04': flag_pos = pos + 6 flags = struct.unpack('<H', bytes(data[flag_pos:flag_pos+2]))[0] if flags & 0x0001: data[flag_pos:flag_pos+2] = struct.pack('<H', flags & 0xFFFE) fixed += 1 pos += 1 with open(out_path, 'wb') as f: f.write(data) print(f'修复文件头数量: {fixed}')这段脚本的作用是:定位每个 ZIP 本地文件头签名PK\x03\x04,在偏移 6 处读取两字节标志位,检测第 0 位是否为 1,是则清除后写回。参数说明里最重要的一点:它只复位加密标志位,不做任何密码破解,数据本身没有加密时改完就能正常解压。运行后拿到修复文件头数量大于 0,再用普通工具解压输出文件即可。如果脚本跑完仍然要求密码,说明这个包是真的加密过,别在解压上继续耗时间,回头检查来源渠道。
5.2 CSV 中文乱码:UTF-8 与 GBK 的编码战争
导入时另一高频问题:CSV 里中文全部变成乱码,或者 LOAD CSV 导入成功但查出来的名字是???。原因出在文件编码上。Windows 上很多工具默认以 ANSI(GBK)保存 CSV,而 Neo4j 的 LOAD CSV 默认按 UTF-8 读取。GBK 的字节流被当成 UTF-8 解析,自然满屏乱码。还有一个隐蔽细节:UTF-8 文件的 BOM 头(EF BB BF)会导致 CSV 第一列字段名变成\uFEFFid,WITH HEADERS解析时列名不匹配,数据导进去全是 null。
解决办法是用命令行做一次转码,我偏好 Python:
with open('people_gbk.csv', 'r', encoding='gbk') as f: content = f.read() with open('people_utf8.csv', 'w', encoding='utf-8-sig') as f: f.write(content)utf-8-sig是 Python 对带 BOM 的 UTF-8 的称呼。写入时带 BOM,正好匹配 Neo4j 对 UTF-8 BOM 的兼容逻辑,字段名不会被污染。如果你的 CSV 是 UTF-8 无 BOM 但被 Excel 打开过,Excel 可能已经把编码改成系统区域设置,需要重新另存为 CSV UTF-8 格式。转码完成后,用LOAD CSV ... WITH HEADERS再导一次,乱码问题基本清除。
5.3 LOAD CSV 找不到文件:import 目录和 file:/// URL 的路径规则
LOAD CSV 的路径报错是 Neo4j 新手最高频的问题。现象是Failed to read file 'file:///xiyouji/people.csv',原因几乎都是文件没放进 Neo4j 的 import 目录。Neo4j 出于安全考虑,默认只允许 LOAD CSV 读取$NEO4J_HOME/import目录下的文件,file:///xiyouji/people.csv对应的是import/xiyouji/people.csv,不是任意绝对路径。Windows 上尤其爱踩这个坑:用户把 CSV 放在D:\data,然后写file:///D:/data/people.csv,直接报错。
排查步骤固定:先把 CSV 复制到import目录下(Desktop 版可以在设置里看到 import 目录的绝对路径),再确认 URL 是相对路径前缀。如果用的是 Docker 版 Neo4j,还需要确认 CSV 文件挂载进了容器内的/var/lib/neo4j/import路径,宿主机路径与容器路径不是一回事。最后用一行最小语句验证:
LOAD CSV WITH HEADERS FROM 'file:///xiyouji/people.csv' AS row RETURN row LIMIT 1;能返回一行数据再往上堆逻辑。这一行只做路径验证,没有写库副作用,比反复跑完整导入脚本快得多。路径一直飘红的时候,还可以用RETURN 'file:///xiyouji/people.csv'确认字符串没被转义掉,不排除有人把\写进 Windows 路径里,Neo4j 直接把反斜杠当成转义符处理。
5.4 实体重复、关系挂不上:人物别名与主键设计
导入竟然成功,节点数和关系数都“挺好看”,但一查询发现“孙悟空”和“齐天大圣”是两个节点,唐僧和“三藏”也分开了,关系自然挂不到一起。这是文学类知识图谱数据质量最典型的暗坑:同名实体、别名实体没有做规范化。人物在小说里可以有多个称号,如果把name当主键,等于宣告“同一个人的所有称号都是合法节点”。
解决思路是引入规范化别名表,或者用统一主键字段。比如给人物加一个aliases数组属性,查询时用WHERE row.name IN p.aliases匹配。如果 zip 里没有提供这个映射,我一般先跑一条聚合查询找出疑似重复项:
MATCH (p:Person) WHERE p.name CONTAINS '行者' OR p.name CONTAINS '大圣' RETURN p.name, count(*) AS cnt ORDER BY cnt DESC;确认别名后,用MERGE把重复节点合并。具体做法是先建一个规范主节点,再用MERGE把其他节点的关系转移过来,最后把空节点删掉。这个过程要谨慎,最好在测试库上演练一遍,因为MERGE合并一旦转移关系出错,原数据已经被覆盖,没有后悔药。这也是为什么我反复强调导入前先做约束和索引:约束会拒绝重复主键,至少让你早一点发现问题,而不是等到可视化阶段才看到两个节点孤零零地漂在图上。
5.5 大批量导入内存爆掉:PERIODIC COMMIT 像一个节流阀
早期我拿到一个几十万行的 zip 关系文件,直接LOAD CSV不带任何参数,跑了十分钟后 Neo4j 直接弹 OutOfMemoryError。原因是 LOAD CSV 默认把整个导入包在一个事务里,行数太多时事务日志和堆内存都会被拖垮。解决办法就是前面提到的:auto USING PERIODIC COMMIT 1000。这个“1000”可以调,一般我按 CSV 行数和机器内存来定:4GB 内存的机器用 500,16GB 的机器用 5000,调太高会重新撞上内存上限,调太低事务频繁提交反而变慢。
另一个实际相关的参数是dbms.memory.heap.max_size,Neo4j 的堆内存上限默认值偏保守。如果导入过程中频繁出现 GC 停顿,可以在neo4j.conf里把堆内存从默认值提升到 2~4GB,但不能超过物理内存的一半,否则操作系统本身会开始换页,性能不升反降。这类“玄学”问题我经历过好几次,最后都是靠节流参数和堆内存配置一起解决,单独调一个往往无效。
6. 用路径查询给图谱做体检,再把这张图迁移到你的领域
图谱导入之后,最值得做的一件事不是写前端,而是用路径查询验证图的质量。路径查询是图数据库的灵魂,也是检验“节点和关系是否真的连通”的最佳手段。我的惯例是先跑一条最长路径:
MATCH p = shortestPath( (a:Person {name: '唐僧'})-[:途经*1..20]->(b:Location {name: '西天灵山'}) ) RETURN p;这条查询的意义在于:如果 zip 里的“途经”关系没有断链,这条路径应该串起长安到灵山之间的主要地点链;如果返回空结果,说明“取经路线”这条语义层根本不存在,数据可能是散装的,只有局部关系,没有完整叙事链。学会用路径查询做“体检”,比任何可视化都更能暴露数据质量问题。
把这个验证习惯迁移到你自己的领域时,模式完全一样。做设备故障知识图谱,就查“最短故障传播路径”,从“电机过热”追到“轴承磨损”再追到“润滑油缺失”;做知识管理系统,就查“某个概念到另一个概念的最短依赖链”。图谱模式本身是通用的,变的是实体和关系的命名。这是我几次导入这类文学知识图谱沉淀下来的习惯:先不管展示效果,先把路径跑通,再谈交付。希望帮到你。
本文还有配套的精品资源,点击获取