ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从《西游记》到知识图谱:构建、Neo4j导入与zip打包实战

2026/8/29 9:17:47 拓冰建站 浏览量
从《西游记》到知识图谱:构建、Neo4j导入与zip打包实战 简介知识图谱作为结构化知识的组织形式能将非结构化文本转化为可查询、可推理的关系网络。在构建领域图谱时实体识别与关系抽取是核心环节而将图谱数据交付给他人时压缩包的兼容性和编码问题则成为工程实践中的隐形门槛。许多开发者在使用zip分发数据时常遇到“File is not a zip file”或“Could not find EOCD”等报错或者因中文文件名在跨平台解压时乱码而困扰。本文以《西游记》人物关系图谱为例从文本清洗、词典匹配的实体识别到基于谓词规则的关系抽取再到使用LOAD CSV批量导入Neo4j完整还原从原著到图数据库的落地流程。同时针对zip打包分发中的编码修复、损坏压缩包的处理以及如何将知识图谱与RAG结合以增强大模型问答的准确性提供了一套可复用的经验方法。无论是入门知识图谱的开发者还是希望优化数据交付质量的工程师都能从中获得具有工程价值的参考。1. 收到这份知识图谱 zip 后我先看到了什么前阵子清理硬盘翻出一个名为《西游记》知识图谱.zip 的压缩包。这是我之前跟几个同学一起做的课程项目产物当时主要目标是解决一个问题能不能把《西游记》原著这种长文本变成一张可以查询、可以推理、可以喂给大模型的关系网络。压缩包解压后大概是十几个文件从人物 CSV、关系表、Cypher 脚本到 README 一应俱全顶层的目录结构长这样xiyou_graph/ ├── README.md ├── schema.json ├── data/ │ ├── characters.csv │ ├── places.csv │ ├── artifacts.csv │ ├── events.csv │ └── relations.csv ├── import/ │ └── import_graph.cypher ├── analysis/ │ ├── relation_stats.ipynb │ └── query_demo.ipynb └── dist/ └── xiyou_graph.pdf当时我们考虑到要发给不同平台的同学特意选择 zip 而不是 rar因为 zip 的兼容性最好微信、QQ、邮件都能直接收发。但在后续交流过程中我陆续收到过“File is not a zip file”“Could not find EOCD”之类的报错截图也遇到过中文文件名乱码的问题。这些看起来像是小问题实际处理起来每一个都能让人折腾半天。这份 zip 里的知识图谱核心内容就是一张以《西游记》原著为基础的人物关系网。它不仅能回答“孙悟空有哪些师兄弟”“白骨精和唐僧之间是什么关系”这种直观问题还能支撑更复杂的多跳查询比如“凡是住在山里的妖怪中哪些曾经被观音菩萨收服过”。传统全文检索对这种问题很无力图谱却是天然的解题工具。这份文件对三类人最有参考价值第一是刚接触知识图谱、想找一个中文经典文本做练习的开发者第二是做文学研究的文科背景朋友想用数据视角看小说第三是做大模型问答应用想通过结构化知识弥补 LLM 幻觉问题的技术同学。接下来我就把从文本到图谱、再到 zip 打包的完整过程拆开讲一遍。2. 构建人物关系图谱先从原著里抽出哪些实体和关系2.1 为什么选《西游记》而不是其他文本知识图谱的构建离不开高质量数据源。《西游记》原著在文本处理上最大的优势是章回体结构清晰、人物命名相对统一、法术法宝的设定固定。一百回目每一回都有明确的标题和叙述边界非常适合按段落切分人物虽然多但大部分都有固定的称谓比如“悟空”“行者”“孙大圣”指的是同一个人实体对齐时只要维护好别名表就能解决大部分问题。另外故事里的关系非常典型唐僧和三个徒弟是师徒关系取经路上不断有妖怪出场妖怪和某个菩萨或神仙之间往往存在“坐骑”或“门下弟子”的关系。这和真实世界里的社交网络、企业股权结构有非常相似的模式适合用来练习图谱建模。2.2 实体类型和属性怎么定我们最初的实体类型定得比较杂后来发现“杂”不是问题“边界不清”才是问题。最终我们收敛成四类。实体类型举例核心属性人物唐僧、孙悟空、白骨精、如来原名、称号、阵营、兵器地点花果山、灵山、高老庄、流沙河归属、类型法宝/物品金箍棒、九齿钉耙、紧箍咒持有人、功能事件三打白骨精、大战红孩儿发生回目、涉及人物为什么把“事件”也作为实体而不是关系这是建模时一个重要取舍。如果把“三打白骨精”当成关系那这个关系只能挂在孙悟空和白骨精之间但事件还涉及唐僧、猪八戒甚至后来赶走孙悟空这个后续情节。事件一旦实体化就可以作为一张“事实表”把参与人物、发生地点、所在回目全部挂上去。查询“孙悟空被赶走几次”时直接从事件实体上做聚合比在关系里来回跳转要直观得多。属性设计上有一个容易被忽略的点一定要保留“所属回目”这个属性。很多人做文学图谱时经常只关注“谁认识谁”忽略原著的文本定位。但保留回目信息后所有实体和关系都可以溯源到原文章节方便用户回到原著验证也方便后续做“按故事情节推进”的时序分析。这个设计在后来做知识问答时帮了大忙。2.3 关系类型够用但不冗余我们最终只保留了十几类关系原则是关系必须能让最简单的查询跑通同时不能让关系语义互相重叠。关系说明示例师徒师徒关系唐僧—→孙悟空师兄弟同门关系孙悟空—→猪八戒敌对战斗或对立孙悟空—→白骨精收服被higher力量收服观音—→红孩儿居住人物与地点关系牛魔王—→翠云山持有人物与法宝关系孙悟空—→金箍棒参与人物与事件关系孙悟空—→三打白骨精发生于事件与地点关系三打白骨精—→白虎岭这里要特别提醒关系类型不是越多越好。我见过有人建模时把“怕”“恨”“爱”这种情绪关系也加进去结果导致同样的“敌对”事实被拆成好几种语义查询时很难统一。文学类文本确实有情绪色彩但如果目标是做可计算的知识图谱前期宁可压缩关系类型把情绪分析作为单独的分析维度放在图谱之外也不要污染图谱本体的整洁性。3. 从原著原文到 Neo4j完整落地流程3.1 数据预处理从散文本到结构化句子整个流程的第一步是拿原著文本。我们用的是公开的 TXT 版本首先要做的是清洗。这一步我强烈建议用 Python 写脚本处理不要手动改。核心步骤包括将全书按“第X回”切分成段落保留回目名。去掉正文中的注释、校对标记、空行。将每一回内的内容按句子切分我用的是 [。] 作为切分点再合并过短的碎片。统一人名别称比如“悟净”“沙和尚”“沙僧”全部归一化为“沙僧”。清洗后的数据落到一个sentences.csv文件里包含chapter、chapter_title、sentence三列。这一步看起来简单实际上占了整个项目 40% 的时间。原著文本里经常有“那猴王”“这厮”“老孙”这种指代不处理的话后续关系抽取会大量误判。3.2 实体识别规则词典为主模型校对为辅实体识别我们采用了“词典匹配 规则 人工复核”的组合方式没有直接上复杂模型。原因是《西游记》的人名体系相对封闭与其耗费精力训练 NER 模型不如先把词典做厚。这里分享一下词典构建的思路# 简单的词典加载与匹配示例 import re alias_dict { 孙悟空: [悟空, 行者, 孙大圣, 齐天大圣, 美猴王, 猴王], 唐僧: [玄奘, 三藏, 金蝉子, 江流儿, 御弟], 猪八戒: [八戒, 悟能, 天蓬元帅, 呆子], 沙僧: [沙和尚, 悟净, 卷帘大将], } # 在句子中做词典匹配 def recognize_entities(sentence, alias_dict): found set() for canonical, aliases in alias_dict.items(): for alias in aliases: if alias in sentence: found.add(canonical) return found为了校验词典覆盖率我们每处理完一版文本就随机抽出目测若干句检查漏掉的人名。如果某个角色频繁出现但不在词典里就补进去。人工复核虽然费时但能显著提升最终图谱质量。除了人物地点和法宝也使用类似的词典匹配方式。地点词相对好办关键词库手动整理了两百多个地名基本能覆盖主要场景。法宝这一块依靠“关键词 引号/书名号”的规则去捕捉比如金箍棒在原著中常写作“金箍棒”“如意金箍棒”。“紧箍咒”这种既是咒语又是关键道具的要额外标记为“控制/收束类法宝”。3.3 关系抽取基于谓词和段落共现的组合策略实体抽取完以后关系抽取是真正的硬骨头。我们用了两层策略。第一层是“谓词规则”。根据实体A 动词/称谓词 实体B的模式做关系判断。比如句子中出现“拜”“为师”且同时出现唐僧和孙悟空就很有可能是一条“师徒”关系“打死”“打杀”“大战”则可能是“敌对”关系。这里可以写一套简单的规则引擎patterns [ (r(拜|拜师|为师), 师徒), (r(打死|打杀|大战|战), 敌对), (r(收服|收为|降伏), 收服), ]第二层是“段落共现统计”。同一个人物名称频繁出现在同一个回目里往往意味着他们有某种关联只是规则不一定能识别。我们按回目统计任意两个实体的共现次数阈值设定为 3共现次数大于 3 的实体对进入待确认列表再人工判断关系具体类型。这一步有效补充了“唐僧和如来”“孙悟空和观音”这类不以具体动作词为标记的关系。关系抽取完成后输出格式统一转成三元组source,target,relation,chapter 孙悟空,唐僧,师徒,第14回 孙悟空,白骨精,敌对,第27回 观音,红孩儿,收服,第42回3.4 导入 Neo4jCSV 批处理比逐条写入快得多数据量不大只有几百个节点和上千条关系但导入方式仍然建议用 Neo4j 的批量导入方式不要逐条写入。逐条写入虽然代码好写但速度慢且不够优雅。批量导入方式有两种选择如果是从空库开始可以直接用neo4j-admin import但要求节点文件和关系文件的表头设计很严格。我们是先在开发环境用 Cypher 的LOAD CSV导入方便调试。下面是一个LOAD CSV的导入示例// 导入人物节点 LOAD CSV WITH HEADERS FROM file:///characters.csv AS row CREATE (:Person { name: row.name, alias: row.alias, camp: row.camp, weapon: row.weapon }); // 导入关系 LOAD CSV WITH HEADERS FROM file:///relations.csv AS rel MATCH (a:Person {name: rel.source}) MATCH (b:Person {name: rel.target}) MERGE (a)-[:RELATION {type: rel.relation, chapter: rel.chapter}]-(b);很多人会问为什么用MERGE而不是CREATE原因很简单关系在文本里可能被重复抽取到重复导入会生成多条重复关系。MERGE能去重虽然慢一点但对这种规模的数据完全够用。导入完成后我们写了一组测试查询// 孙悟空所有的师父级人物 MATCH (n:Person {name: 孙悟空})-[:RELATION {type: 师徒}]-(master) RETURN master.name;通过这种查询能很快验证图谱的关系方向是否正确。方向问题是入门最常犯的错搞反之后所有查询结果都会对不上。4. zip 打包分发踩坑中文编码、损坏压缩包和加密问题4.1 为什么实际交付时选了 zip 而不是其他格式图数据库搭好之后项目面临一个现实问题要把它发给其他同学和老师。很多同学有 Windows也有用 macOS 的如果再传一个 Neo4j 数据目录环境差异会让人崩溃。所以我们决定把图谱的“静态版本”打包发布包括 CSV、Cypher 脚本、Jupyter Notebook 和说明文档。这样任何人拿到包以后只要有 Neo4j Desktop 或者 Docker就能快速重建整套图谱。zip 是当时最快的选择。它不需要额外安装压缩软件Windows、macOS、Linux 默认都支持。比起 tar.gzzip 在中文用户群体里接受度更高比起 rarzip 无需授权各种工具都能解压。后来在知乎和博客上看到很多人在问“zip 解压软件哪个好”其实系统自带的基本够用。4.2 中文文件名乱码跨平台解压最大的坑第一个实际问题就是中文文件名乱码。在 Windows 上右键压缩时压缩软件默认使用 GBK 编码记录文件名到了 macOS 或 Linux 上解压系统默认按 UTF-8 解码文件名就变成了一堆乱码。“相关热搜词”里最常见的场景之一就是“zip 解压乱码”相关的检索属于典型的跨平台编码问题。解决方案有两个思路思路一压缩时统一使用 UTF-8 编码。用 Python 的zipfile库手动压缩时可以通过设置flag_bits的0x800位来标记文件名是 UTF-8主流图形压缩软件也大多有“文件名编码”设置项。思路二压缩包内全部使用英文目录名和文件名中文说明写进 README。这个方法最笨但最可靠。我后来做交付包时直接选了思路二内部文件名用characters.csv而不是人物表.csv。使用者在解压后打开 README 就能看到中文字段说明既避免了乱码也降低了索引失败的问题。如果已经收到乱码的压缩包也可以手动处理import zipfile import shutil # 用 cp437 重新解码文件名再转成 gbk with zipfile.ZipFile(bad_package.zip) as zf: for info in zf.infolist(): new_name info.filename.encode(cp437).decode(gbk) with zf.open(info) as src, open(new_name, wb) as dst: shutil.copyfileobj(src, dst)这段代码不保证百分之百有效但能救回大部分用 GBK 压缩、被错误识别为 cp437 的文件名。4.3 “File is not a zip file”和“Could not find EOCD”到底怎么回事收到同学发来的解压报错最常见的两个信息是“File is not a zip file”和“invalid zip archive: could not find EOCD”。EOCD 是 zip 格式的结尾记录正常 zip 文件在末尾会有一段固定的末尾目录结构。如果文件末尾缺少这段标记解压工具就认为文件不是合法的 zip。我排查过几次主要原因通常有三个文件传输中断zip 文件不完整。尤其是通过聊天工具传输大压缩包时手机端如果没下载完成就转发极易出现这种问题。扩展名被改。有可能源文件并不是 zip可能是 rar、7z 或者纯文本只是被命名为.zip。压缩包被人为拼接或二次修改破坏了 EOCD 记录。排查方法很简单在 Linux 或 macOS 上用file命令查看真实类型file xiyou_graph.zip如果输出显示Zip archive data说明文件格式没问题如果是RAR archive data说明它其实是 rar。如果是data或ASCII text那大概率是文件没传输完整。确认是 zip 但 EOCD 缺失时可以尝试zip -FF修复zip -FF xiyou_graph.zip --out xiyou_graph_fixed.zip这个命令会重新扫描文件内容尝试重建 zip 的目录结构。实验中它对文件尾部轻微截断的情况有效但如果中间数据块缺失严重还是要重新传输源文件。4.4 分卷压缩包、加密压缩包的处理有朋友发来.z01和.zip混合文件这也是常见问题。分卷 zip 的第一个分卷后缀是.z01接着是.zip。解压时先把所有分卷放在同一目录然后直接用解压工具打开.zip文件软件会自动读取.z01。如果用命令行Linux 下可以先合并分卷cat file.z01 file.zip merged.zip但要注意合并后的merged.zip未必能被正常识别因为分卷的记录头还在文件里。更稳妥的方式是使用 7-Zip 或 WinRAR 打开.zip分卷不要手动合并。加密 zip 的密码恢复则是另一个高频需求。之前有同学忘了自己设的密码压缩包里有刚跑完的图谱数据急得不行。密码恢复本质上是暴力破解时间取决于密码长度和复杂度。我推荐的工具是zip2john配合 John the Ripperzip2john xiyou_graph.zip hash.txt john hash.txt但这个操作要谨慎只能用于自己创建的、忘记密码的压缩包。如果是从网络下载的加密压缩包去尝试破解密码可能涉及合规风险非常不建议。根据我个人经验密码设置成自己不会忘的格式比破解工具靠谱得多。比如把项目名称加上年份再补一个符号既不容易忘也比纯数字安全。4.5 从 GitHub 下载的 zip 如何在本地环境安装除了项目交付很多朋友也遇到过另一个问题从 GitHub 直接下载某个开源项目的 zip 压缩包后不知道该怎么安装到 Python 的 conda base 环境里。如果这个压缩包是源码包最直接的方式是解压后进入目录执行unzip project-main.zip cd project-main pip install .在 conda base 环境下pip install .会把项目作为本地包安装到当前环境。如果项目提供的是setup.py或pyproject.toml都可以用这种方式。如果目录里有requirements.txt可以先用pip install -r requirements.txt装依赖再执行项目入口文件。还有一个小技巧不要手动解压后再进目录也可以让 pip 直接读取本地 zip 文件pip install project-main.zippip 对 zip 包的支持很完善前提是这个压缩包内部结构是标准的 Python 打包目录。我当时就通过这种方式快速把别人的图谱工具安装到了 conda 环境里。4.6 交付 zip 之前的自查清单踩过若干次坑后我总结了一个交付自查清单现在每次发压缩包之前都会过一遍压缩包内是否有顶层目录避免解压时文件散落一地。文件名是否用英文命名中文说明是否集中放进了 README。是否有校验文件比如checksum.txt存放每个文件的 SHA256。压缩包生成后是否在另一个操作系统上测试解压过一次。是否有加密如果有密码是否已经告知对方且设置了合适的压缩工具兼容性。这个清单是我个人经历换来的不能保证覆盖所有场景但至少能避免 80% 的“解压失败”麻烦。5. 把知识图谱接进 RAG让问答系统能回答更准5.1 为什么图谱和 RAG 能互相补位大模型技术火起来之后“知识图谱 向量数据库”的搭配被频繁提起。很多人问有了向量数据库为什么还要知识图谱我的理解是向量数据库擅长做语义相似召回比如用户问“孙悟空在哪座山占山为王”系统可以通过 embedding 相似度把包含花果山的文本块召回来。但它不擅长精确的多跳推理比如“观音菩萨收服的妖怪中有谁住在火云洞附近”。这类查询如果靠向量召回很难一次性命中放到图数据库里只需要两步查询就能完成。RAG检索增强生成解决的是大模型“胡说八道”的问题但传统的向量 RAG 只解决“参考原文回答”没有真正解决“关系约束”。把知识图谱前置让 LLM 先生成图查询语句再返回结构化查询结果最后用 LLM 润色自然语言回答效果会明显更稳。5.2 从图谱导出向量数据的具体思路实际操作中我们不是把所有三元组直接存进向量库而是先把图谱内容变成自然语言描述再做 embedding。比如一个简单的三元组唐僧 —师徒→ 孙悟空可以转换成一段上下文唐僧是孙悟空的师父。在《西游记》故事中唐僧在五行山救出孙悟空并收他为徒。然后把这段描述交给 embedding 模型生成向量存进向量数据库。查询时用户的问题先做向量召回找到相关的三元组描述再把命中的三元组作为上下文交给大模型。这样做的好处是既保留了图谱的结构化能力又解决了纯图查询对自然语言理解不够的问题。用户用日常口语提问时向量召回能命中图谱里相关节点而需要精确关系时图数据库又能给出高可信度的结果。5.3 一个最小可用的问答链路示例我这里放一个简化的思路完整的代码不展开但核心链路就是三步# 第一步判断是否需要图查询 question 观音菩萨收服过哪些妖怪 # 第二步转换成 Cypher 查询 cypher_query MATCH (p:Person {name: 观音})-[:RELATION {type: 收服}]-(demon) RETURN demon.name # 第三步图查询结果 LLM 润色 graph_result execute_cypher(cypher_query) answer llm.rewrite_answer(question, graph_result) print(answer) # 输出观音菩萨收服过红孩儿、黑熊精等……步骤里的关键点在于怎么把自然语言问题稳定地转换成 Cypher。最简单的方案是直接让大模型生成 Cypher然后把 Cypher 交给图数据库执行。不过要小心大模型生成的 Cypher 经常有语法错误所以需要增加一个“试运行 报错重试”的机制。还有一个方案是用语义相似度匹配模板把用户问题先匹配到已有的 Cypher 模板上。5.4 后续扩展方向情节时序与人物命运曲线做完人物关系图谱以后最容易扩展的方向就是加入“时间维度”。《西游记》本身有明确的故事推进顺序每一难都发生在不同回目。如果把“事件”实体里的回目属性转化为事件发生的顺序就可以构建一条时间线。比如把“五行山收悟空”“高老庄收八戒”“流沙河收沙僧”这些事件按回目排序后就能回答“取经团队的完整集结过程是怎样的”。更进一步可以把每个角色的出场回目做成分布曲线看看谁是前期主角、谁在中后期才出现。这种分析层面的玩法已经超出了“关系查询”的范畴属于文学研究里的“量化叙事”以后有时间我会单独写一篇。6. 最后分享一点个人实操体会这个项目最早只是一个期末作业后来因为要做成 zip 分发反而让我把大量精力花在了压缩、传输、解压这些“非图谱”的事情上。回过头看这些“杂活”其实才是实际交付里最容易被低估的部分。我自己的一个体会是不要因为知识图谱听起来高大上就忽视最基础的文件管理规范。有一次我拿到别人发来的图谱包里面三层目录文件散乱还没有 README光是搞清楚每个文件是干嘛的就很费劲。后来我们团队就定了规矩任何交付包都必须有顶层目录、英文文件名、README 和校验文件。这个规矩在后来好几次合作里都救了我因为对方打开压缩包的第一印象往往决定了后续沟通的效率。另一个体会是知识图谱项目里的数据结构设计永远比工具使用重要。Neo4j、NetworkX、RDF 这些工具都是手段核心问题永远是“你想让这个图谱回答什么问题”。先想清楚问题清单再决定实体和关系比先装一堆工具再想怎么建模要高效得多。最后分享一个小技巧生成 zip 包之前在 Linux 或 macOS 上用下面这条命令看一眼文件类型和编码情况zipinfo -v xiyou_graph.zip | head -30zipinfo会列出压缩包里的文件权限、压缩算法和编码标志信息。如果看到[UTF-8]标识说明文件名用的是 UTF-8 编码如果没有就要警惕中文乱码风险。这个检查只要十秒钟能帮你避免发给别人之后被追着问“为什么解压出来是乱码”的尴尬。本文还有配套的精品资源点击获取