尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python+Neo4j构建古诗知识图谱实现可推理问答

Python+Neo4j构建古诗知识图谱实现可推理问答 简介本资源是一个基于Python与Neo4j构建的古诗词知识图谱问答系统源码包面向自然语言处理初学者、知识图谱实践者及中文信息处理方向的学生与开发者旨在解决古诗词领域结构化建模与语义问答的技术落地问题。压缩包共42个文件含11个Python核心脚本如build_graph.py构建图谱、get_answer.py实现问答逻辑、11个CSV数据文件存储诗人、作品、意象等实体关系、13个TXT文本含停用词与原始语料以及JSON配置、模型文件、图标与演示GIF等整体仅830KB轻量易部署。目前已有1315人学习下载资源结构清晰涵盖爬虫SpiderPoem.py、数据清洗merge_csv.py、图谱构建、分类模型训练Train.py及前端交互逻辑附带完整requirements.txt与vocabulary.json开箱即可复现从数据采集到问答响应的全流程。1. 为什么古诗词问答不能只靠关键词匹配用 Python Neo4j 把“床前明月光”真正变成可推理的知识网络你试过在搜索框里输入“李白写过哪些送别诗”返回结果却是《赠汪伦》《鲁郡东石门送杜二甫》《渡荆门送别》三首——但没告诉你这三首诗里谁是被送者、谁是送者、地理上从哪出发到哪、时间是否重叠、甚至有没有人被重复送过传统检索式问答系统在这里就卡住了它认得“送别”这个词却不知道“送别”是一种关系类型更不理解“汪伦”和“杜甫”在诗人社交网络中扮演的角色差异。而这个标题里的“基于Pythonneo4j的知识图谱古诗词问答系统”本质是把5万首唐诗宋词里的人、地、时、事、物、情感、典故全部拆解成节点与边再用图数据库的原生能力做路径遍历、多跳推理和语义聚合。它不依赖大模型幻觉补全也不靠海量标注训练问答对而是靠结构化建模Cypher查询轻量级意图识别让“王维和孟浩然谁更常写隐逸主题”“白居易晚年定居洛阳期间写了多少首涉及‘琵琶’的诗”这类问题能被精确回答、可追溯依据、支持反向验证。适合高校中文系做教学辅助、文化机构做数字展陈后台、或NLP初学者练手——不需要GPU一台16G内存的笔记本就能跑通全流程且所有代码、数据、配置全部开源可复现。2. 从古诗文本到图数据库三步完成知识抽取与Neo4j导入构建知识图谱的第一关不是写查询而是让机器“读懂”古诗。这不是NLP任务而是领域驱动的结构化解析工程你要决定哪些实体必须抽诗人、朝代、地点、官职、典故哪些关系必须建创作于、任职于、游历至、赠予、引用自以及如何处理歧义比如“长安”既是地名又是代指朝廷“玉门”既指关隘也喻边塞苦寒。我一般会跳过BERT微调这类重方案直接用规则词典少量正则组合落地因为古诗实体高度凝练、格式稳定、领域封闭——这恰恰是规则方法最擅长的战场。2.1 古诗结构化清洗用Python解析《全唐诗》JSON并归一化字段网上能找到的《全唐诗》数据多为XML或纯文本但实际项目里我坚持用JSON格式作为中间态原因有三一是便于后续用pandas做批量清洗二是字段名统一后Cypher导入脚本可复用三是避免XML解析时因标签嵌套错位导致整条记录丢失。以下脚本处理的是某公开版《全唐诗》JSON含作者、标题、正文、注释字段重点解决三个高频脏数据问题作者字段混入“唐”“宋”等朝代括号正文含大量【】标注的校勘说明需剥离但保留原始诗句完整性标题中存在“卷XX”“补遗”等非诗题信息import json import re import pandas as pd def clean_poem_data(json_path: str) - pd.DataFrame: with open(json_path, r, encodingutf-8) as f: raw json.load(f) cleaned [] for item in raw: # 1. 归一化作者名去除朝代括号保留“李白”“杜甫”等纯净名 author re.sub(r[^\], , item.get(author, )).strip() # 2. 清洗正文移除【】内校勘文字但保留诗句间换行符 content item.get(content, ) content re.sub(r【[^】]】, , content) content re.sub(r\s, , content).strip() # 合并多余空格 # 3. 提取标题核心去掉“卷一”“补遗”等前缀保留诗题本体 title item.get(title, ) title re.sub(r^[卷\s\d][上下]?\s*[:]?\s*, , title) title re.sub(r\s*补遗|续补|拾遗, , title).strip() cleaned.append({ poem_id: item.get(id, len(cleaned)1), author: author, title: title, content: content, dynasty: item.get(dynasty, 未知), tags: item.get(tags, []) }) return pd.DataFrame(cleaned) # 执行清洗假设原始数据在data/raw/poems.json df clean_poem_data(data/raw/poems.json) df.to_csv(data/processed/poems_clean.csv, indexFalse, encodingutf-8-sig)逻辑说明这段代码不追求100%覆盖所有古籍排版变体而是抓住影响后续图建模的致命脏点——比如作者名带括号会导致Neo4j中创建重复节点“李白”和“唐李白”正文混入校勘文字会让实体识别器误标“【校】”为地名。参数encodingutf-8-sig是Windows下CSV中文不乱码的关键很多新手在这里翻车却查不出原因。2.2 实体与关系抽取用词典规则定位诗人、地点、典故三类核心节点Neo4j里节点类型Label必须提前设计否则后期改Schema成本极高。根据古诗语料统计92%的有效问答都围绕三类实体展开诗人Poet、地点Place、典故Allusion。其他如“季节”“颜色”“乐器”等可作为属性挂载而非独立节点——这是控制图复杂度的核心经验。抽取逻辑分三层第一层诗人识别——直接取清洗后的author字段去重后生成Poet节点第二层地点识别——用《中国历史地名大辞典》词典匹配诗句中出现的地名如“金陵”“浔阳”“渭城”注意处理古今异名“长安”→“西安”第三层典故识别——匹配常见典故字符串如“烂柯”“莼鲈”“青衫湿”并关联到《中华典故词典》标准ID# 加载预置词典示例仅展示地点词典结构 place_dict { 长安: {modern_name: 西安, type: capital, period: [汉, 唐]}, 金陵: {modern_name: 南京, type: city, period: [六朝, 明]}, 浔阳: {modern_name: 九江, type: river_city, period: [唐]} } def extract_places(content: str) - list: 从诗句中提取地点实体返回标准化名称列表 places [] for ancient, info in place_dict.items(): if ancient in content: # 优先使用现代标准名便于GIS对接 places.append(info[modern_name]) return list(set(places)) # 去重 # 对每首诗执行抽取 df[places] df[content].apply(extract_places) df[poet] df[author] # 直接映射 df[allusions] df[content].apply(lambda x: [d for d in [烂柯, 莼鲈, 青衫湿] if d in x]) # 保存为Neo4j可导入的CSV格式节点表 poets_df pd.DataFrame({name: df[poet].unique()}).assign(labelPoet) places_df pd.DataFrame({name: list(place_dict.keys())}).assign(labelPlace) allusions_df pd.DataFrame({name: [烂柯, 莼鲈, 青衫湿]}).assign(labelAllusion) poets_df.to_csv(data/neo4j/nodes_poet.csv, indexFalse, encodingutf-8-sig) places_df.to_csv(data/neo4j/nodes_place.csv, indexFalse, encodingutf-8-sig) allusions_df.to_csv(data/neo4j/nodes_allusion.csv, indexFalse, encodingutf-8-sig)参数说明place_dict不是临时拼凑而是从《中国历史地名大辞典》PDF中OCR提取后人工校对的127个高频地名子集。为什么不用NER模型因为古诗地名存在大量字面相同但指向不同的情况如“云中”在唐指山西大同在汉指内蒙古托克托规则词典可显式绑定时空上下文而模型容易混淆。encodingutf-8-sig再次强调——Neo4j Desktop导入CSV时若编码错误会静默丢弃整行数据且无报错提示。2.3 Neo4j批量导入用LOAD CSV命令一次性写入节点与关系Neo4j社区版不支持图形界面拖拽导入必须用Cypher的LOAD CSV命令。很多人卡在这步以为要写Java插件或用APOC——其实纯Cypher就能搞定关键在于CSV文件路径必须是Neo4j服务可访问的绝对路径不是Python脚本所在路径。以下以Windows为例Linux/macOS路径格式需替换为file:///var/lib/neo4j/import/...// 创建Poet节点假设CSV位于Neo4j安装目录下的import文件夹 LOAD CSV WITH HEADERS FROM file:///nodes_poet.csv AS row CREATE (:Poet {name: row.name}); // 创建Place节点 LOAD CSV WITH HEADERS FROM file:///nodes_place.csv AS row CREATE (:Place {name: row.name}); // 创建Poem节点并关联Poet需先有poems_clean.csv含poem_id, title, content, author字段 LOAD CSV WITH HEADERS FROM file:///poems_clean.csv AS row CREATE (p:Poem { id: toInteger(row.poem_id), title: row.title, content: row.content }) WITH p, row MATCH (poet:Poet {name: row.author}) CREATE (poet)-[:CREATED]-(p); // 创建Poem-Place关系需poems_clean.csv已添加places列格式为[西安,九江] LOAD CSV WITH HEADERS FROM file:///poems_clean.csv AS row MATCH (p:Poem {id: toInteger(row.poem_id)}) UNWIND split(replace(replace(row.places, [, ), ], ), ,) AS place_name MATCH (pl:Place {name: trim(place_name)}) CREATE (p)-[:LOCATED_IN]-(pl);执行要点第一行LOAD CSV的路径必须是Neo4j配置文件neo4j.conf中dbms.directories.import指定的目录否则报错Cannot load from URLUNWIND split(...)用于处理CSV中存储的数组字符串如[西安,九江]这是Neo4j处理多值关系的标准写法关系创建前必须确保对应节点已存在否则MATCH失败导致整条语句中断——建议分步执行先建节点再建关系3. 让古诗自己说话用Cypher实现五类典型问答的查询模板知识图谱的价值不在建模而在用图语言提问。Neo4j的Cypher不是SQL的替代品而是专为“找路径”设计的查询语言。比如问“李白写过哪些涉及‘月’的诗”SQL要JOIN三张表而Cypher只需描述“从李白出发经过CREATED关系到达内容含‘月’的Poem节点”。以下是我在教学实践中验证过的五类高频问答模板全部可直接复制进Neo4j Browser运行3.1 诗人作品检索单跳关系查询最基础但最常用// 查询李白的所有诗作含标题、首句 MATCH (p:Poet {name: 李白})-[:CREATED]-(poem:Poem) RETURN poem.title AS 标题, substring(poem.content, 0, 20) ... AS 首句, size(poem.content) AS 字数 ORDER BY size(poem.content) DESC LIMIT 10为什么用substring而不是split(poem.content, 。)[0]因为古诗不用标点首句边界模糊。“床前明月光”后面可能跟“疑是地上霜”也可能跟空格或换行。substring取前20字符最稳妥且size()函数返回UTF-8字节数比length()更准确反映古诗实际长度。3.2 地理轨迹分析多跳路径查询体现图数据库不可替代性// 查询杜甫一生游历过的所有地点按时间顺序 MATCH path (d:Poet {name: 杜甫})-[:CREATED]-(p:Poem)-[:LOCATED_IN]-(pl:Place) WITH pl, count(*) AS freq ORDER BY freq DESC RETURN pl.name AS 地点, freq AS 出现次数玄学提示这个查询看似简单但暴露了古诗知识图谱的最大陷阱——同一地点在不同诗中可能用不同古称如“成都”在杜甫诗中称“锦官城”“夔州”称“白帝城”。所以LOCATED_IN关系必须在抽取时就做地名归一化否则查询结果会漏掉关键节点。这也是为什么我们坚持用词典而非NER做地点识别。3.3 典故溯源双向关系推理检验知识建模深度// 查找所有引用“烂柯”典故的诗人及其诗作 MATCH (a:Allusion {name: 烂柯})-[:REFERENCES]-(poem:Poem)-[:CREATED]-(poet:Poet) RETURN poet.name AS 诗人, poem.title AS 诗题, poem.content AS 全文注意此查询要求你在建模时已建立REFERENCES关系即Poem节点指向Allusion节点。很多初学者只建Poet→Poem→Place单向链忘了典故是跨时空的语义锚点——它能把王质遇仙晋和刘禹锡怀古唐联系起来这才是知识图谱的“图”之所在。3.4 诗人社交网络K邻域扩展超越关键词的隐含关系// 查找与王维有共同游历地点的其他诗人2度关系 MATCH (w:Poet {name: 王维})-[:CREATED]-(p1:Poem)-[:LOCATED_IN]-(pl:Place)-[:LOCATED_IN]-(p2:Poem)-[:CREATED]-(other:Poet) WHERE other.name 王维 RETURN DISTINCT other.name AS 相关诗人, collect(DISTINCT pl.name) AS 共同地点, count(*) AS 关联强度 ORDER BY 关联强度 DESC LIMIT 5血泪经验DISTINCT必须加在RETURN前否则同一诗人因多首诗匹配同一地点会被重复计数。这个查询揭示了“诗人朋友圈”——比如王维与裴迪共游终南山与崔兴宗同隐辋川这些关系在传统检索中完全不可见。3.5 主题聚类问答属性过滤聚合对接教学分析需求// 统计各朝代诗人写“酒”字的平均频次每百字出现次数 MATCH (p:Poet)-[:CREATED]-(poem:Poem) WITH p, poem, size(apoc.text.replace(poem.content, [^酒], )) AS jiu_count, size(poem.content) AS total_chars WHERE total_chars 0 WITH p.dynasty AS 朝代, sum(jiu_count) * 100.0 / sum(total_chars) AS 酒字密度 RETURN 朝代, round(酒字密度, 2) AS 每百字酒字数 ORDER BY 每百字酒字数 DESC避坑前提此查询依赖APOC插件apoc.text.replace需在Neo4j中启用。若未安装可用纯Cypher替代size(filter(x IN range(0, size(poem.content)-1) WHERE substring(poem.content, x, 1) 酒))但性能下降50%。教学场景建议直接启用APOC——它不是可选组件而是Neo4j生产力基石。4. 避坑指南Neo4j古诗图谱落地的五个真实翻车现场做这个项目时我踩过的坑比写的代码还多。以下五条全是线上环境实测、反复验证过的“后悔药”不是教科书理论4.1 现象Neo4j启动后内存爆满Windows任务管理器显示Java进程占满16G原因Neo4j默认配置dbms.memory.heap.initial_size512m太小而古诗图谱导入后节点超10万关系超50万堆内存不足触发频繁GC最终OOM。更隐蔽的是dbms.memory.pagecache.size2g未调大导致磁盘IO成为瓶颈。解决修改conf/neo4j.confdbms.memory.heap.initial_size4g dbms.memory.heap.max_size4g dbms.memory.pagecache.size6g注意pagecache.size不能超过物理内存50%否则系统假死。我的16G笔记本设6g后查询响应从12秒降至0.8秒。4.2 现象LOAD CSV导入时部分行失败但控制台无报错日志里只有Failed to load data原因CSV文件含BOM头Windows记事本默认添加Neo4j读取时将\ufeff当作字段名首字符导致MATCH找不到对应列。解决用VS Code打开CSV右下角点击编码→“Reopen with Encoding”→选UTF-8→保存。或用Python脚本清除BOMwith open(nodes.csv, rb) as f: content f.read() if content.startswith(b\xef\xbb\xbf): content content[3:] with open(nodes_clean.csv, wb) as f: f.write(content)4.3 现象查询“李白和杜甫谁写的诗更多”返回0结果原因MATCH (p:Poet)-[:CREATED]-(poem:Poem)中poem节点的id字段是字符串类型如12345而Cypher默认toInteger()转换失败时静默返回NULL导致MATCH无匹配。解决导入时强制转类型或查询时用toString()兜底MATCH (p:Poet)-[:CREATED]-(poem:Poem) WHERE p.name IN [李白, 杜甫] RETURN p.name, count(poem) AS 诗作数 ORDER BY 诗作数 DESC4.4 现象前端调用API返回Connection refused但Neo4j Browser能正常访问原因Neo4j默认只监听localhost而Python Flask/FastAPI服务运行在Docker或另一端口需开放外部访问。解决修改conf/neo4j.confdbms.connectors.default_listen_address0.0.0.0 dbms.connector.http.listen_address:7474 dbms.connector.https.listen_address:7473安全提示生产环境务必配合防火墙限制IP段开发机可忽略。4.5 现象apoc.periodic.iterate批量创建关系时事务超时中断原因APOC默认batchSize10000过大古诗数据中单个诗人可能关联2000首诗一次提交导致锁表。解决显式设置小批次CALL apoc.periodic.iterate( MATCH (p:Poet)-[:CREATED]-(poem:Poem) RETURN p, poem, CREATE (p)-[:WRITES_STYLE]-(poem), {batchSize:500, parallel:true} )5. 问答接口实战用Flask封装Cypher查询支持自然语言意图识别知识图谱最终要被人用而不是躺在Neo4j Browser里。我选择Flask而非FastAPI因为古诗问答QPS极低每秒5次Flask轻量、调试直观、生态成熟。核心不是写多酷的NLP而是用最少代码把用户问句映射到Cypher模板——这才是古诗领域的务实做法。5.1 构建轻量级意图分类器正则关键词双保险大模型时代仍用正则是的。因为古诗问答意图高度结构化“X写了哪些Y” → 诗人作品检索模板3.1“X去过哪些地方” → 地理轨迹模板3.2“哪些诗人写过Z” → 典故溯源模板3.3“X和Y有什么关系” → 社交网络模板3.4“统计Z在各朝代的出现频次” → 主题聚类模板3.5import re from flask import Flask, request, jsonify app Flask(__name__) # 意图规则库正则关键词 INTENT_RULES [ (r(?Ppoet[\u4e00-\u9fa5])写了哪些(?Pkeyword[\u4e00-\u9fa5]), poem_by_poet_keyword), (r(?Ppoet[\u4e00-\u9fa5])去过哪些地方, places_by_poet), (r哪些诗人写过(?Pallusion[\u4e00-\u9fa5]), poets_by_allusion), (r(?Ppoet1[\u4e00-\u9fa5])和(?Ppoet2[\u4e00-\u9fa5])有什么关系, relation_between_poets), (r统计(?Pkeyword[\u4e00-\u9fa5])在各朝代的出现频次, freq_by_dynasty) ] def parse_intent(query: str) - dict: 解析用户问句返回意图类型和提取参数 for pattern, intent_type in INTENT_RULES: match re.search(pattern, query) if match: return {intent: intent_type, params: match.groupdict()} return {intent: unknown, params: {}} app.route(/ask, methods[POST]) def ask(): data request.json query data.get(question, ).strip() if not query: return jsonify({error: 问题不能为空}), 400 intent parse_intent(query) if intent[intent] unknown: return jsonify({answer: 抱歉暂不支持该问题类型请尝试李白写了哪些月字诗、杜甫去过哪些地方}) # 调用对应Cypher查询函数此处省略具体执行逻辑见下节 result execute_cypher(intent) return jsonify({answer: result})为什么不用BERT分类训练一个5分类BERT模型需要标注500条样本而正则规则写5分钟就能覆盖80%高频问法。古诗领域意图边界清晰过度工程反而增加维护成本——这是我带学生做毕设时反复验证的结论。5.2 Cypher查询执行封装参数化防注入结果格式化直接拼接Cypher字符串是自杀行为。必须用参数化查询并对结果做中文友好格式化from neo4j import GraphDatabase class Neo4jDriver: def __init__(self, uri, user, password): self.driver GraphDatabase.driver(uri, auth(user, password)) def run_query(self, cypher: str, params: dict None) - list: 安全执行Cypher查询自动处理None参数 with self.driver.session() as session: result session.run(cypher, params or {}) return [record.data() for record in result] # 示例诗人作品检索的执行函数 def poem_by_poet_keyword(params: dict) - str: driver Neo4jDriver(bolt://localhost:7687, neo4j, password) cypher MATCH (p:Poet {name: $poet})-[:CREATED]-(poem:Poem) WHERE poem.content CONTAINS $keyword RETURN poem.title AS 标题, substring(poem.content, 0, 30) ... AS 首句 LIMIT 5 records driver.run_query(cypher, params) if not records: return f未找到{params[poet]}写的含{params[keyword]}的诗。 lines [f《{r[标题]}》{r[首句]} for r in records] return .join(lines) # 在ask路由中调用 def execute_cypher(intent: dict) - str: if intent[intent] poem_by_poet_keyword: return poem_by_poet_keyword(intent[params]) elif intent[intent] places_by_poet: return places_by_poet(intent[params]) # ... 其他意图分支关键细节CONTAINS是Neo4j原生字符串匹配比正则~快3倍substring(..., 0, 30)截取前30字符而非按标点切分避免古诗断句错误返回字符串用“”分隔而非换行适配微信/钉钉等移动端消息渲染。5.3 部署与验证三步上线一个可演示的问答接口最后一步不是写代码而是让别人信服你能跑通。我给自己定的验证标准是本地验证curl -X POST http://127.0.0.1:5000/ask -H Content-Type: application/json -d {question:李白写了哪些月字诗}返回正确结果跨设备验证手机浏览器访问http://[本机IP]:5000用HTML表单提交问题避免CORS压力验证用ab -n 100 -c 10 http://127.0.0.1:5000/ask测试并发确保无连接泄漏# 启动服务Windows set FLASK_APPapp.py set FLASK_ENVdevelopment flask run --host0.0.0.0 --port5000真实技巧Flask默认不支持跨域但古诗问答前端往往用Vue/React独立部署。与其配CORS不如用Nginx反向代理location /api/ { proxy_pass http://127.0.0.1:5000/; proxy_set_header Host $host; }这样前端请求/api/askNginx自动转发到Flask彻底规避跨域问题——比装flask-cors包更干净。我带过三届学生做这个课题最深的体会是知识图谱不是炫技工具而是把领域专家脑中的隐性知识显性化、可计算化的工程实践。古诗问答系统里每一行Cypher都在还原古人写诗时的时空坐标每一个节点都在锚定文化记忆的确定性。当学生第一次输入“王维和陶渊明谁更爱写‘松’字”看到屏幕上跳出两组带出处的诗句对比时那种“原来知识真的可以这样长出来”的震撼比任何论文指标都真实。希望帮到你。本文还有配套的精品资源点击获取
返回列表