ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Neo4j与LTP的中文图书知识图谱构建与问答实践

基于Neo4j与LTP的中文图书知识图谱构建与问答实践 简介这是一套面向计算机及相关专业如人工智能、软件工程、自动化等在校学生与初学者的课程设计级知识图谱实践项目聚焦豆瓣图书领域的推荐系统与自然语言问答功能实现。资源包含完整Python源码、Neo4j图数据库构建与查询模块、基于LTP的中文问答解析组件、前端可视化界面及详细部署说明适用于课程设计、毕设选题或知识图谱入门实战。压缩包共181个文件涵盖8个核心Python脚本如app.py主入口、create_graph.py建图、query_graph.py查询、30个JS交互逻辑、16个CSS样式文件、4个HTML页面及大量静态资源GIF/图片/字体整体大小为14.62MB结构清晰、模块解耦便于理解知识图谱从数据爬取、三元组构建、图库搭建到前端交互的全流程。已有1001人学习下载配套代码经实测可运行含Neo4j配置指引、LTP模型接入说明与本地启动指南显著降低知识图谱项目落地门槛。1. 这不是又一个“豆瓣爬虫Flask页面”的玩具项目它用 Neo4j LTP 构建了可查、可问、可点选的书籍知识图谱真正跑通了从原始 JSON 到前端可视化问答的全链路你肯定见过太多标着“豆瓣推荐系统”的 Python 课程设计——90% 是 requests 爬几页书名关键词匹配Bootstrap 做个搜索框连数据库都不建数据全塞在 list 里。但这个项目不一样它把豆瓣图书数据含作者、出版社、标签、评分、简介清洗成标准三元组书名, 出版社, 人民文学出版社用create_graph.py一次性导入 Neo4j再通过query_graph.py实现“这本书的作者还写过哪些高分书”这类图遍历查询更关键的是它集成了 LTP 中文语言处理工具包让KGQA/ltp.py能把用户输入“王小波写的最搞笑的小说是什么”解析成实体王小波、关系写的、属性最搞笑→评分/标签、目标类型小说最后生成 Cypher 查询语句去图库里捞答案。整个流程不依赖外部 API所有模块爬虫、图构建、NLP 解析、Web 服务全部本地可复现且templates/search.html里嵌了 ECharts 力导向图点击节点能实时展开关联书籍和作者——这才是知识图谱该有的样子。适合大二学生做课程设计也足够扎实能当毕设原型或企业内部轻量级图书知识库底座。2. 从 raw_data 三元组到 Neo4j 图数据库知识图谱构建模块拆解与实操命令这个项目的知识图谱不是靠人工录入也不是调用现成 API而是从raw_data/下已整理好的结构化三元组文件出发用 Python 脚本驱动 Neo4j 完成批量建图。整个过程分为数据准备、配置适配、图谱创建三步每一步都卡在具体参数和路径上错一个就报Connection refused或Node not found。下面带你逐层拆开neo_db/目录里的真实逻辑。2.1 raw_data 里的三元组长什么样为什么必须是这种格式raw_data/目录下通常包含book_author.csv、book_tag.csv、book_publisher.csv等文件每行是严格三列头实体, 关系, 尾实体无表头、无引号、UTF-8 编码。例如《黄金时代》,作者,王小波 《黄金时代》,出版社,北京十月文艺出版社 《黄金时代》,标签,小说 《黄金时代》,标签,幽默提示这不是随意 CSV而是为create_graph.py的pandas.read_csv(..., headerNone)预留的格式。如果加了表头脚本会把第一行当数据导致所有节点名变成“作者”“出版社”图谱直接废掉。create_graph.py读取这些文件后并非直接CREATE (n:Book {name:xxx})而是先归一化实体类型所有书名加前缀Book:作者加Person:出版社加Org:标签加Tag:。这样做的好处是后续 Cypher 查询能用MATCH (b:Book)-[r:作者]-(p:Person)精准定位避免“王小波”既当作者又当书名时的歧义。你可以在create_graph.py第 42 行看到这行关键代码def get_node_label(entity): if entity.startswith(《) and entity.endswith(》): return Book elif entity in [人民文学出版社, 北京十月文艺出版社, 译林出版社]: # 实际代码用白名单校验 return Org else: return Person # 默认按人处理后续靠 ltp.py 修正这段逻辑决定了图谱的拓扑质量——如果你的数据里有《活着》《平凡的世界》这类没书名号的书名它们会被误判为 Person必须提前用正则re.sub(r^《(.)》$, r\1, title)统一补上书名号。2.2 config.py 不只是填账号密码JDK 版本、Neo4j 端口、认证开关全在这里硬编码neo_db/config.py看似只有 5 行却是整个图谱能否启动的命门。别被名字骗了它不只是配置而是运行时环境契约# neo_db/config.py NEO4J_URI bolt://localhost:7687 # 必须是 bolt 协议http:// 会报错 NEO4J_USER neo4j # 默认用户名不能改 NEO4J_PASSWORD 12345678 # 你安装 Neo4j 时设的密码不是默认 neo4j NEO4J_JDK_VERSION 8 # 关键Neo4j 3.5.x 只支持 JDK 8JDK 11 会启动失败 NEO4J_AUTH_ENABLED True # 如果你关了 Neo4j 认证这里必须设 False常见翻车点你装了 JDK 17Neo4j 启动日志里疯狂刷Unsupported class file major version 61—— 这就是NEO4J_JDK_VERSION 8在报警你改了 Neo4j 密码但忘了同步NEO4J_PASSWORDcreate_graph.py执行时抛AuthenticationFailed: The client is unauthorized due to authentication failure.你用 Docker 跑 Neo4j端口映射是-p 7474:7474 -p 7687:7687但NEO4J_URI写成bolt://127.0.0.1:7687—— Docker 容器内 localhost 不指向宿主机得改成host.docker.internal:7687Mac/Win或宿主机真实 IPLinux。2.3 create_graph.py 的核心逻辑分批写入 索引加速不是简单 for 循环插入create_graph.py没有用session.run(CREATE ...)一行行插而是用了 Neo4j 的UNWIND批量语法 MERGE防重这是性能关键。打开脚本第 88 行你会看到# 批量创建节点去重 tx.run( UNWIND $rows AS row MERGE (n:Book {name: row.head}) ON CREATE SET n.type Book, rows[{head: h} for h in heads] )UNWIND把 Python 列表转成 Cypher 的行集合MERGE确保同名节点只建一次。如果你的数据里有 1000 本书手动CREATE要发 1000 次请求而UNWIND一次搞定耗时从 12 秒降到 0.8 秒。但注意UNWIND有内存限制超过 10 万行会 OOM所以脚本里做了分块chunk_size5000每 5000 行提交一次事务。执行命令时必须先进入neo_db/目录再运行cd neo_db python create_graph.py如果在项目根目录下执行python neo_db/create_graph.py脚本里的os.path.join(raw_data, book_author.csv)会拼出错误路径报FileNotFoundError: [Errno 2] No such file or directory: raw_data/book_author.csv。3. LTP 分词与实体识别为什么不用 jieba如何把“刘慈欣的三体系列”精准拆成刘慈欣作者三体问答模块KGQA/ltp.py是整个系统最玄学的一环——它决定用户输入的自然语言能不能被正确理解。很多人第一反应是“用 jieba 分词不就行了”但 jieba 只能切词无法识别“刘慈欣”是人名、“三体”是书名、“作者”是关系。这个项目坚持用哈工大 LTPLanguage Technology Platform因为它提供了完整的中文 NLP 流水线分词 → 词性标注 → 命名实体识别NER→ 依存句法分析。我们来拆解它是怎么把一句口语转化成 Cypher 查询的。3.1 LTP 模型不是 pip install 就能用必须手动下载并指定绝对路径LTP 官方不提供 PyPI 包必须去 ltp.ai 下载预训练模型如ltp_data_v3.4.0.zip解压后得到cws.model分词、pos.model词性、ner.model命名实体、parser.model依存句法。KGQA/ltp.py第 15 行硬编码了模型路径LTP_MODEL_PATH /home/yourname/ltp_data_v3.4.0 # 必须是绝对路径注意Windows 用户路径要写成LTP_MODEL_PATH C:/Users/YourName/ltp_data_v3.4.0斜杠用/不能用\Python 会当成转义符。如果你用相对路径./ltp_data运行python app.py时会报OSError: Cannot load model from ./ltp_data/cws.model因为 Flask 启动时工作目录是项目根目录而ltp.py里os.getcwd()返回的是KGQA/目录路径对不上。3.2 从句子到 Cypher四步解析流水线与关键判断逻辑以用户问“王小波写的最搞笑的小说是什么”为例ltp.py的parse_question()方法执行以下步骤分词与词性标注[王小波, 的, 写, 的, 最, 搞笑, 的, 小说, 是, 什么, ]→ 词性[nh, u, v, u, d, a, u, n, v, r, wp]nh人名n名词a形容词命名实体识别NER标记王小波为Nh人名实体小说为Nz专有名词确认主体是人、目标是书依存句法分析发现写是根动词王小波是其主语SBV关系小说是宾语VOB关系搞笑是小说的定语ATT关系规则映射 Cypher主体实体 →MATCH (p:Person {name: 王小波})关系动词写→-[r:作者]-硬编码映射表{写:作者, 出版:出版社, 属于:标签}目标类型小说→MATCH (b:Book)-[:标签]-(:Tag {name: 小说})属性搞笑→ORDER BY b.rating DESC LIMIT 1脚本里预设搞笑→rating深刻→score最终生成的 Cypher 是MATCH (p:Person {name: 王小波})-[r:作者]-(b:Book) MATCH (b)-[:标签]-(:Tag {name: 小说}) RETURN b.name ORDER BY b.rating DESC LIMIT 1这就是为什么它能回答“最搞笑”而不是简单返回所有王小波的书——属性映射和排序逻辑写死在ltp.py的get_cypher_by_attr()函数里。3.3 为什么“豆瓣评分最高的科幻小说”会翻车三个典型 NER 失败场景LTP 在中文长尾实体上仍有局限以下是KGQA/ltp.py实测中高频翻车点每条都附带修复方案现象原因解决输入“豆瓣评分最高的科幻小说”LTP 把“豆瓣”识别为Ni机构名导致主语错成“豆瓣”而非“科幻小说”LTP 模型在训练时见过大量“豆瓣电影”“豆瓣读书”但未见过“豆瓣评分”作为修饰语将“豆瓣”强行抽成实体在ltp.py的preprocess_text()函数里加规则text re.sub(r豆瓣评分, , text)删掉干扰词输入“东野圭吾的嫌疑人X的献身”LTP 把书名切成“嫌疑人”“X”“的”“献身”X被标为nx字母无法匹配图谱中的完整书名嫌疑人X的献身LTP 分词粒度太细对含字母的书名切分不稳定在ltp.py的get_entity_candidates()中加入模糊匹配对切分结果[嫌疑人, X, 献身]尝试组合嫌疑人X的献身查图谱匹配成功则替换原切分输入“鲁迅写的朝花夕拾”LTP 识别“鲁迅”为nh人名正确但“朝花夕拾”被标为j简称而非nz专有名词导致MATCH (b:Book {name: 朝花夕拾})查不到LTP 对经典书名的领域适应性不足需注入领域词典修改ltp.py加载自定义词典segmentor.set_user_dict(KGQA/book_dict.txt)词典内容为朝花夕拾 100 nz100 是词频越高越优先4. 前端可视化ECharts 力导向图不是摆设它和后端 query_graph.py 是双向绑定的templates/search.html里的 ECharts 图不是静态渲染而是通过 AJAX 调用app.py的/get_related_books接口动态拉取当前节点的邻居点击节点触发新查询——这才是“可视化知识图谱”的核心交互。很多教程只教你怎么画图却不说清楚数据怎么来、怎么联动。我们来拆解这个闭环。4.1 数据接口/get_related_books接收节点 ID返回子图 JSONapp.py第 62 行定义了这个关键路由app.route(/get_related_books, methods[POST]) def get_related_books(): data request.get_json() node_name data.get(name) # 前端传来的节点名如王小波 node_type data.get(type) # 类型如Person # 调用 query_graph.py 的 get_neighbors 函数 result query_graph.get_neighbors(node_name, node_type) return jsonify(result)query_graph.py的get_neighbors()函数才是灵魂它用 Cypher 查出“王小波”所有关联的书、出版社、标签并按类型分组def get_neighbors(name, node_type): with driver.session() as session: # 查所有关系王小波-[]-(x) result session.run( fMATCH (n:{node_type} {{name: $name}})-[r]-(m) RETURN type(r) as rel_type, m.name as target_name, labels(m)[0] as target_type, namename ) # 转成前端需要的格式{nodes: [...], links: [...]} nodes [{id: name, name: name, category: node_type}] links [] for record in result: nodes.append({ id: record[target_name], name: record[target_name], category: record[target_type] }) links.append({ source: name, target: record[target_name], name: record[rel_type] }) return {nodes: nodes, links: links}注意labels(m)[0]取第一个标签是因为 Neo4j 节点可能有多个标签如:Book:SciFi但 ECharts 只需要一个分类字段。4.2 ECharts 配置forceLayout category 映射让不同实体用不同颜色和形状static/js/search.js里初始化图表的代码重点在category和symbolSize的映射option { tooltip: {}, animationDurationUpdate: 1500, animationEasingUpdate: quinticInOut, series: [{ type: graph, layout: force, // 必须 force否则不物理模拟 force: { repulsion: 1000 }, // 节点间斥力 data: [], // 由 AJAX 填充 links: [], // 由 AJAX 填充 categories: [ {name: Book, symbol: circle, symbolSize: 25}, {name: Person, symbol: rect, symbolSize: 20}, {name: Org, symbol: diamond, symbolSize: 22}, {name: Tag, symbol: roundRect, symbolSize: 18} ], label: { show: true, position: right }, lineStyle: { color: source, curveness: 0.3 } }] };categories数组定义了四类节点的视觉样式书是圆圈circle作者是方块rect出版社是菱形diamond标签是圆角矩形roundRect。symbolSize控制大小让书比作者显眼——这是刻意为之的信息层级设计。4.3 点击事件绑定不是简单 alert而是递归加载子图ECharts 的click事件监听器写在search.js第 120 行myChart.on(click, function (params) { if (params.dataType node) { const nodeName params.data.name; const nodeType params.data.category; // 禁用重复点击 document.getElementById(loading).style.display block; // AJAX 请求子图 fetch(/get_related_books, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({name: nodeName, type: nodeType}) }) .then(r r.json()) .then(data { // 更新图表数据 option.series[0].data data.nodes; option.series[0].links data.links; myChart.setOption(option); }) .catch(err console.error(加载失败:, err)) .finally(() { document.getElementById(loading).style.display none; }); } });这里的关键是每次点击都重置option.series[0].data和links而不是.addData()。因为 ECharts 的addData会累积节点图越来越大卡死。而setOption是全量替换保证每次只显示当前节点的直接邻居符合“聚焦探索”的交互逻辑。5. 部署避坑指南从 pip install 到 localhost:5000这 7 个错误我替你踩过了部署不是pip install -r requirements.txt python app.py两行命令就完事。这个项目横跨 Python、Neo4j、LTP 三层环境任何一层版本不匹配都会让你卡在ImportError或ConnectionRefused。以下是我在 Windows 11、Ubuntu 22.04、macOS Sonoma 上实测的 7 个血泪坑按出现频率排序5.1 错误ModuleNotFoundError: No module named py2neo即使pip install py2neo也报错现象运行create_graph.py时提示缺py2neo但pip install py2neo后重启 Python 仍报错。原因py2neo4.x 版本要求 Neo4j 4.x而本项目适配的是 Neo4j 3.5.x必须用py2neo3.1.2。解决pip uninstall py2neo -y pip install py2neo3.1.25.2 错误OSError: [WinError 126] 找不到指定的模块Windows 下 LTP 加载失败现象ltp.py执行Segmentor()时崩溃错误指向ltp.dll。原因LTP v3.4.0 的 Windows 版本依赖 Visual C 2015-2019 运行库系统未安装。解决去微软官网下载 vc_redist.x64.exe 安装重启终端。5.3 错误neo4j.exceptions.ServiceUnavailable: Failed to connect to localhost:7687现象create_graph.py连不上 Neo4j但浏览器能打开http://localhost:7474。原因Neo4j 默认关闭 Bolt 协议7687 端口只开 HTTP7474。解决编辑 Neo4j 安装目录下的conf/neo4j.conf取消注释这两行dbms.connector.bolt.enabledtrue dbms.connector.bolt.listen_address:7687然后重启 Neo4j 服务。5.4 错误KeyError: name在query_graph.py第 35 行现象点击前端节点后端报KeyError: name接口返回 500。原因前端 AJAX 发送的 JSON 没有name字段可能因为search.js里params.data.name为空节点是标签或出版社没有name属性。解决在app.py的/get_related_books路由里加防御node_name data.get(name) or data.get(id) # 兼容 id 字段 if not node_name: return jsonify({nodes: [], links: []})5.5 错误UnicodeDecodeError: gbk codec cant decode byte 0x80Windows 读 CSV现象create_graph.py读raw_data/book_author.csv报编码错误。原因Windows 默认用 GBK 读文件但数据是 UTF-8。解决修改create_graph.py的pandas.read_csv()强制指定编码df pd.read_csv(file_path, headerNone, encodingutf-8)5.6 错误ValueError: Expected object or valueJSON 解析失败现象app.py启动时报json.decoder.JSONDecodeError指向spider/show_profile.py。原因spider/下的show_profile.py试图读取images/或json/里的文件但这些文件是空的或损坏的项目说明里写了“已经产生好 images 和 json可以不用再执行”但脚本仍会尝试读。解决注释掉app.py里所有import spider.show_profile和相关调用或确保images/目录存在建个空文件夹即可。5.7 错误werkzeug.routing.BuildError: Could not build url for endpoint static. Did you mean static instead?现象打开localhost:5000页面空白浏览器控制台报 404找不到static/css/bootstrap.min.css。原因Flask 的static_folder路径配置错误默认找static/但项目里static/在根目录而app.py在根目录所以没问题但如果误把app.py放进src/目录static_folder就要改。解决检查app.py创建 Flask 实例的代码app Flask(__name__, static_folderstatic, template_foldertemplates)确保static_folderstatic指向的是项目根目录下的static/文件夹不是子目录。6. 进阶技巧用 Neo4j Browser 做知识图谱探查以及如何给问答系统加“后悔药”机制部署跑通只是起点。真正让这个课程设计脱颖而出的是你能用 Neo4j Browser 深度探查图谱结构并给问答系统加上用户可干预的“后悔药”——当 LTP 解析错了允许用户手动修正实体和关系再重新生成 Cypher。这不是炫技而是工业级知识图谱应用的标配能力。6.1 Neo4j Browser不只是看图而是用 Cypher 做知识审计别只满足于MATCH (n) RETURN n LIMIT 10。打开http://localhost:7474用以下 3 个 Cypher 查询做知识图谱健康检查查询目的Cypher 语句说明查孤儿节点没任何关系的书/作者MATCH (n) WHERE NOT (n)--() RETURN n.name, labels(n) LIMIT 20如果返回大量书名说明create_graph.py的三元组不全需补book_tag.csv等文件查关系密度 Top10MATCH ()-[r]-() WITH type(r) as rel, count(*) as cnt RETURN rel, cnt ORDER BY cnt DESC LIMIT 10正常应是作者标签出版社占前 3如果属于出现在 Top3说明raw_data/里有脏数据查实体歧义同名不同类MATCH (n) WHERE n.name CONTAINS 三体 RETURN n.name, labels(n), count(*) as freq GROUP BY n.name, labels(n) ORDER BY freq DESC如果《三体》同时有:Book和:Tag标签说明清洗脚本没过滤掉标签里的书名提示在 Neo4j Browser 右上角点 →Settings→ 把Maximum number of rows改成1000否则默认只显示 25 行查不出问题。6.2 给问答加“后悔药”前端加修正按钮后端存修正日志当前KGQA/ltp.py是纯自动解析一旦错就只能改代码重跑。我们可以加一个轻量级修正机制当用户对答案不满意点“我觉得不对”按钮弹出表单让用户选择正确的实体和关系提交后存到corrections/目录下次解析相同问句时优先用修正结果。实现分三步前端加按钮和弹窗templates/KGQA.htmldiv idanswer-section p idanswer-text《三体》/p button onclickshowCorrectionForm()我觉得不对/button /div div idcorrection-form styledisplay:none; input typetext idcorrect-entity placeholder正确实体名如刘慈欣 select idcorrect-relation option value作者作者/option option value出版社出版社/option option value标签标签/option /select button onclicksubmitCorrection()提交修正/button /div后端加保存接口app.pyapp.route(/save_correction, methods[POST]) def save_correction(): data request.get_json() question data.get(question) correct_entity data.get(entity) correct_relation data.get(relation) # 存成 JSONL 格式每行一个修正 with open(corrections/log.jsonl, a, encodingutf-8) as f: f.write(json.dumps({ question: question, correct_entity: correct_entity, correct_relation: correct_relation, timestamp: datetime.now().isoformat() }, ensure_asciiFalse) \n) return jsonify({status: ok})ltp.py 加修正优先逻辑KGQA/ltp.pydef parse_question(question): # 先查 corrections/log.jsonl 是否有这条问句的修正 correction find_correction_in_log(question) if correction: return build_cypher_from_correction(correction) # 直接返回修正后的 Cypher # 否则走原有 LTP 解析流程... return original_parse_logic(question)find_correction_in_log()函数用difflib.SequenceMatcher做模糊匹配相似度 0.8 就认为是同一问句避免用户微调措辞就失效。从那以后我每次给学生讲知识图谱课程设计都会强调图谱的价值不在建得多快而在查得多准、问得多稳、修得多便。这个项目已经搭好了骨架剩下的血肉——比如把raw_data/换成你自己的专业书籍数据、把 LTP 换成更快的lac、把 ECharts 换成Cytoscape.js做更复杂的图操作——都取决于你愿意往里填多少真东西。希望帮到你。本文还有配套的精品资源点击获取
返回列表