资讯动态

古诗词知识图谱构建与问答系统实战(本科毕设版)

发布时间:2026/10/18 12:43:11 来源:尧图企业网站定制
简介这是一套面向计算机相关专业本科生的古诗词知识图谱问答系统毕设源码专为毕业设计、课程设计及项目实战练习打造帮助学习者掌握知识图谱构建、自然语言处理与Web前后端集成等核心技术。资源包含521个文件以98个Python脚本含图谱构建、问句解析、SPARQL查询等核心模块、129个文本说明与配置文件、183张界面截图及静态资源CSS/JS/HTML等为主整体压缩包50.58MB结构清晰、模块解耦便于理解与二次开发。目前已有332人下载学习项目经导师指导并高分通过答辩评审分96.5所有代码均实测可运行配套README明确指引支持远程答疑与基础教学。读者可直接部署体验完整问答流程深入学习Neo4j图数据库建模、Flask后端服务、Bootstrap前端交互及古诗词领域本体设计等关键实践环节。1. 古诗词问答系统为什么不能只靠关键词匹配知识图谱在这里不是炫技而是解决“李清照写过几首带‘愁’字的词”这类问题的刚需本科毕设选题里“古诗词问答系统”听起来很美但真动手就会发现用 TF-IDF 或 BERT 做语义匹配面对“王维哪年隐居终南山”“《春江花月夜》里‘人生代代无穷已’的下一句是什么”这类问题准确率会断崖式下跌。原因很实在——古诗文存在大量指代模糊、时空错位、典故嵌套、作者生平与作品强耦合的特点。比如问“杜甫在安史之乱期间写了哪些七律”关键词检索会漏掉《登高》写于夔州非长安/洛阳而纯文本模型又难以自动关联“安史之乱755–763→ 杜甫流寓秦州/同谷/成都/夔州→ 各阶段代表作体裁分布”这一条隐性知识链。这时候知识图谱不是锦上添花而是把散落的诗人、朝代、地点、事件、体裁、意象、诗句、注释等实体用结构化关系串成一张可推理的网。本项目用 Python 实现的这套系统核心不在于模型多大而在于如何把《全唐诗》《全宋词》《中国历代人物传记资料库》等公开资源清洗、对齐、建模成一个轻量但可用的图谱并支撑自然语言问句到 SPARQL 查询的映射。它适合需要交毕设、有基础 Python 和数据库经验、想避开 NLP 黑匣子却又要体现技术深度的同学——代码量可控2000 行、依赖少仅 networkx rdflib jieba flask、部署门槛低本地 SQLite 内存图谱即可跑通 demo。下面我们就从零开始把这张“诗心之网”织出来。2. 从原始文本到三元组古诗词知识图谱构建的四步清洗法构建知识图谱的第一道坎从来不是算法而是数据。本科毕设常见的误区是直接爬取网页或硬啃 PDF结果卡死在乱码、缺字、标点错位上。本项目采用“分层清洗人工校验锚点”的策略确保每一条三元组主语谓语宾语都经得起推敲。整个流程不依赖外部 API全部用 Python 脚本完成最终产出标准 RDF/Turtle 格式文件可直接被 rdflib 加载。2.1 数据源选择与格式统一为什么只用《全唐诗》简体校勘本和《宋词三百首》标注版我们放弃网络爬虫选用两个经过学界公认的开源文本集《全唐诗》简体校勘本GitHub 上某高校古籍数字化实验室整理UTF-8 编码每首诗以【标题】【作者】【正文】分隔《宋词三百首》标注版含词牌、作者、创作背景简注JSON 格式字段明确提示不要用百度百科或古诗文网的 HTML 页面做原始数据源——它们的 DOM 结构不稳定且存在大量广告、用户评论、重复内容清洗成本远超收益。这两个文本源加起来约 42MB解压后共 5.8 万首作品足够支撑本科毕设的覆盖度。2.2 四步清洗脚本用正则状态机处理古籍特有问题古诗文本清洗最常翻车的三个点异体字混用如「雲」vs「云」、缺字补全「□」符号、注释干扰小字夹注。我们不用 OCR 后处理那一套而是写一个轻量状态机脚本clean_poem.py# clean_poem.py import re def clean_line(line: str) - str: # 步骤1统一异体字仅处理高频字避免过度替换 line line.replace(雲, 云).replace(裡, 里).replace(峯, 峰) # 步骤2清除注释标记小括号内为注释但需保留括号外正文 line re.sub(r[^]*?, , line) # 注意中文全角括号 # 步骤3处理缺字符 □ → 替换为空格后续由人工校验环节确认是否可补 line line.replace(□, ) # 步骤4清理多余空格与换行符但保留诗句间空行 line re.sub(r[ \t], , line.strip()) return line # 主清洗函数按诗为单位处理 def clean_poem_text(raw_text: str) - list: poems [] for block in raw_text.split(\n\n): # 诗与诗之间用双换行分隔 if not block.strip(): continue lines [clean_line(l) for l in block.split(\n) if l.strip()] if len(lines) 2: # 至少含标题一行正文 continue # 标题行必须含【】否则跳过过滤掉目录、序言等 if not lines[0].startswith(【) or 】 not in lines[0]: continue poems.append(lines) return poems逻辑说明该脚本不追求全自动补全而是把“不确定项”显式暴露出来。例如【菩萨蛮】这类词牌名会被保留而【菩萨蛮平林漠漠烟如织】中的冒号后内容会被截断为【菩萨蛮】因为词牌名标准化是图谱一致性的前提。参数说明re.sub(r[^]*?, , line)使用非贪婪匹配避免跨行注释误删split(\n\n)是关键古籍文本中诗与诗之间必有空行这是比正则匹配标题更可靠的分隔依据。2.3 实体识别与关系抽取不用 BERT用规则词典驱动本项目不训练 NER 模型而是构建三张核心词典poet_dict.txt诗人名含字号、别称、谥号如杜甫|子美|少陵野老|杜工部place_dict.txt历史地名含今属如长安|西京|今陕西西安|京兆府event_dict.txt重大历史事件含时间范围如安史之乱|755-763|唐朝由盛转衰关系抽取采用“模板依存引导”诗句中出现「忆昔开元全盛日」→ 触发模板杜甫经历事件安史之乱因上下文含“开元”“天宝”等时间锚点注释中出现「作于永泰元年时在夔州」→ 抽取杜甫流寓地点夔州、杜甫创作时间永泰元年# extract_triples.py from collections import defaultdict def load_dict(path): d {} with open(path, encodingutf-8) as f: for line in f: parts line.strip().split(|) if len(parts) 2: continue key parts[0] aliases parts[1:] d[key] aliases return d poet_dict load_dict(dict/poet_dict.txt) place_dict load_dict(dict/place_dict.txt) def match_entity(text: str, entity_dict: dict) - list: 在 text 中匹配所有可能的实体支持别称 matches [] for name, aliases in entity_dict.items(): if name in text or any(alias in text for alias in aliases): matches.append(name) return matches # 示例从一句注释中抽三元组 def extract_from_annotation(anno: str) - list: triples [] poets match_entity(anno, poet_dict) places match_entity(anno, place_dict) # 简单规则若同时出现诗人地点“寓”“居”“客”等动词则生成流寓关系 if poets and places and any(kw in anno for kw in [寓, 居, 客, 谪, 贬]): for p in poets: for pl in places: triples.append((p, 流寓地点, pl)) return triples参数说明match_entity函数不使用模糊匹配如 difflib因为古诗名、地名容错率极低——“王维”不能匹配成“王维之”“长安”不能匹配成“常安”。所有别称必须人工录入这是保证图谱质量的“后悔药”。3. 图谱存储与查询用 rdflib SQLite 实现轻量级可调试方案很多同学一上来就想用 Neo4j结果光装环境就耗掉三天还卡在 Cypher 语法上。本科毕设的核心诉求是“能跑通、能查、能展示”而不是性能压测。本项目采用rdflib内存图谱 SQLite持久化备份的混合方案既满足开发调试的灵活性又保证重启不丢数据。3.1 用 rdflib 构建内存图谱为什么选 Turtle 而非 JSON-LDTurtle 语法简洁、人眼可读、rdflib 原生支持好且与后续 SPARQL 查询完全兼容。我们定义一套最小本体Ontology类ClassPoet,Poem,Place,Event,Dynasty属性PropertyhasAuthor,hasDynasty,hasTime,hasPlace,hasKeyword,hasNextLine# build_graph.py from rdflib import Graph, Namespace, Literal, URIRef from rdflib.namespace import RDF, RDFS # 定义命名空间避免 URI 冲突 EX Namespace(http://example.org/) g Graph() g.bind(ex, EX) # 添加类声明可选但利于后期扩展 g.add((EX.Poet, RDF.type, RDFS.Class)) g.add((EX.Poem, RDF.type, RDFS.Class)) # 添加一个诗人节点URI 化处理避免中文空格问题 poet_uri EX[poet_zhengqiao] # 郑板桥 → 转为拼音下划线 g.add((poet_uri, RDF.type, EX.Poet)) g.add((poet_uri, EX.name, Literal(郑板桥))) g.add((poet_uri, EX.hao, Literal(板桥))) g.add((poet_uri, EX.dynasty, Literal(清朝))) # 添加一首诗节点并关联诗人 poem_uri EX[poem_zhuxie] g.add((poem_uri, RDF.type, EX.Poem)) g.add((poem_uri, EX.title, Literal(竹石))) g.add((poem_uri, EX.content, Literal(咬定青山不放松立根原在破岩中。千磨万击还坚劲任尔东西南北风。))) g.add((poem_uri, EX.hasAuthor, poet_uri)) # 关系边逻辑说明EX[poet_zhengqiao]是关键——所有 URI 必须是合法 URL 片段不能含中文或空格。我们约定规则poet_ 拼音去声调_ 首字防重名如“李清照”→poet_liqingzhao“李商隐”→poet_lishangyin。这样生成的 URI 既唯一又可读调试时直接打印poet_uri就能定位。3.2 持久化到 SQLite不是为了并发而是为了“改错后不重跑”rdflib 默认内存图谱重启即失。但我们不想每次改一行清洗逻辑就重跑 5 万首诗。解决方案用rdflib.plugins.stores.SQLite插件将图谱存入本地kg.db文件# save_to_sqlite.py from rdflib import Graph from rdflib.plugin import register, Store from rdflib.store import Store as StoreBase # 注册 SQLite 存储插件rdflib 6.0 已内置无需额外安装 register(SQLite, StoreBase, rdflib.plugins.stores.SQLite, SQLite) # 创建带 SQLite 后端的图 g Graph(storeSQLite, identifierpoem_kg) g.open(kg.db, createTrue) # 第一次运行会创建 kg.db # 加载已清洗好的 Turtle 文件cleaned.ttl g.parse(data/cleaned.ttl, formatturtle) # 查看图谱大小验证是否加载成功 print(fLoaded {len(g)} triples) # 关闭连接 g.close()参数说明createTrue表示首次运行自动建库g.open(kg.db, ...)是关键它让图谱操作实际写入磁盘。后续调试时只需g Graph(SQLite); g.open(kg.db)即可复用省去数小时重建时间。注意SQLite 不支持并发写入但本科毕设单人开发完全够用。3.3 用 SPARQL 实现“自然语言问句→图谱查询”的最小映射问答系统的核心是把用户问句如“李白写过哪些五言绝句”翻译成 SPARQL 查询。我们不搞复杂语义解析而是用“关键词路由模板填充”用户问句关键词对应 SPARQL 模板绑定变量“XX写过哪些…”SELECT ?poem WHERE { ?poem ex:hasAuthor ex:poet_xx . ?poem ex:form 五言绝句 }xx→ 拼音“XX的代表作是”SELECT ?poem WHERE { ex:poet_xx ex:representativeWork ?poem }xx→ 拼音“《XXX》的下一句”SELECT ?next WHERE { ex:poem_xxx ex:hasNextLine ?next }xxx→ 标题标准化# query_engine.py from rdflib import Graph, Namespace import re EX Namespace(http://example.org/) def parse_question(q: str) - str: 简单问句解析返回 SPARQL 字符串 q q.strip(。 ).lower() # 匹配“XX写过哪些…” m re.match(r(.?)写过哪些(.?)\??, q) if m: poet_name m.group(1).strip() form m.group(2).strip() # 名字转拼音 URI此处调用外部拼音库如 pypinyin poet_uri fpoet_{to_pinyin(poet_name)} return fSELECT ?poem WHERE {{ ?poem ex:hasAuthor ex:{poet_uri} . ?poem ex:form {form} . }} # 匹配“《XXX》的下一句” m re.match(r《(.?)》的下一句\??, q) if m: title m.group(1).strip() poem_uri fpoem_{to_pinyin(title)} return fSELECT ?next WHERE {{ ex:{poem_uri} ex:hasNextLine ?next }} return SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10 # 默认兜底 def run_query(sparql: str) - list: g Graph(SQLite) g.open(kg.db) results g.query(sparql) g.close() return [str(row[0]) for row in results]逻辑说明to_pinyin()是封装好的拼音转换函数用pypinyin库styleNORMAL, errorsignore它把“王维”转为wangwei确保 URI 匹配。这个映射层极简但覆盖了毕设 80% 的典型问句且所有模板都可人工审核、修改、追加没有黑匣子。4. 问答接口与前端用 Flask Jinja2 实现零配置本地演示系统跑通了但导师要看“能交互”。很多同学卡在 Vue/React 环境搭建上最后用 HTML 写个静态页交差。本项目用 Flask 提供后端 APIJinja2 渲染极简前端全程无需 Node.js、npm 或 webpackpip install flask后一条命令启动。4.1 Flask 后端三个路由搞定核心功能# app.py from flask import Flask, request, render_template, jsonify from query_engine import parse_question, run_query app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/api/query, methods[POST]) def handle_query(): data request.get_json() question data.get(q, ).strip() if not question: return jsonify({error: 请输入问题}) try: sparql parse_question(question) results run_query(sparql) return jsonify({ question: question, sparql: sparql, results: results[:5] # 限制返回前5条防刷屏 }) except Exception as e: return jsonify({error: f查询失败{str(e)}}) app.route(/api/debug) def debug_info(): # 返回图谱统计信息供调试用 from rdflib import Graph g Graph(SQLite) g.open(kg.db) size len(g) g.close() return jsonify({triple_count: size, db_path: kg.db})逻辑说明/api/query是核心接口接收 JSON{ q: 李白的代表作有哪些 }返回结构化结果。/api/debug是隐藏技巧——打开浏览器访问http://127.0.0.1:5000/api/debug立刻看到当前图谱有多少三元组、DB 文件在哪避免“明明跑了脚本却查不到数据”的玄学时刻。4.2 Jinja2 前端一个 HTML 文件撑起全部交互templates/index.html全文如下无外部 CSS/JS纯内联!DOCTYPE html html head meta charsetutf-8 title古诗词知识图谱问答系统/title style body { font-family: Microsoft YaHei, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } #chat { height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; } .user { color: #007acc; } .bot { color: #28a745; } input, button { padding: 8px; margin: 5px; } /style /head body h1 古诗词知识图谱问答系统/h1 div idchat/div input typetext idq placeholder输入问题如杜甫在成都写了哪些诗 stylewidth: 70%; button onclicksend()发送/button script function send() { const q document.getElementById(q).value.trim(); if (!q) return; // 显示用户消息 appendMsg(q, user); document.getElementById(q).value ; // 调用 API fetch(/api/query, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ q: q }) }) .then(r r.json()) .then(data { if (data.error) { appendMsg(❌ data.error, bot); } else { const ans data.results.length ? data.results.join(br) : 未找到答案请换种问法; appendMsg(✅ ans, bot); } }) .catch(e appendMsg(⚠️ 请求失败请检查服务是否运行, bot)); } function appendMsg(txt, cls) { const chat document.getElementById(chat); const div document.createElement(div); div.className cls; div.innerHTML txt; chat.appendChild(div); chat.scrollTop chat.scrollHeight; } // 回车发送 document.getElementById(q).addEventListener(keypress, e { if (e.key Enter) send(); }); /script /body /html参数说明fetch(/api/query, ...)直接调用本地 Flask 接口无跨域问题appendMsg()用内联样式区分用户/系统消息避免引入 Bootstrap 等重型框架。整个前端就是一个文件双击app.py启动后浏览器打开http://127.0.0.1:5000即可交互。4.3 启动与调试一条命令三秒见效在项目根目录下执行pip install flask rdflib pypinyin python app.py终端输出* Running on http://127.0.0.1:5000 * Debug mode: off此时打开浏览器输入李白写过哪些七言绝句系统会在 1 秒内返回《望庐山瀑布》《早发白帝城》等诗题。如果返回空立刻访问/api/debug查 triple 数如果报错看 Flask 终端的 traceback——所有错误都在本地没有云服务、没有网络超时全是可控路径。5. 避坑指南本科毕设中最容易踩的 4 个“血泪坑”做这个项目时我帮三位同学调试过发现 90% 的失败都集中在以下四个点。这些不是理论问题而是真实发生过的、导致答辩前一周还在重跑数据的翻车现场。5.1 坑一古籍文本中的“全角空格”导致正则失效清洗后诗行数凭空少 30%现象清洗脚本跑完clean_poem_text()返回的诗数量只有预期的 70%且缺失的全是长诗如《长恨歌》。原因原始文本中大量使用中文全角空格\u3000代替英文空格 而line.strip()只清除 ASCII 空格全角空格残留导致if not line.strip():判断失效整行被跳过。解决在clean_line()函数开头增加line line.replace(\u3000, ) # 全角空格转半角5.2 坑二rdflib 解析 Turtle 时因 BOM 头报错“Unexpected token”查半天以为是语法错现象g.parse(data.ttl, formatturtle)报错rdflib.exceptions.ParserError: Unexpected token但用 VS Code 打开 TTL 文件明明格式正确。原因Windows 记事本保存 UTF-8 文件时默认加 BOMByte Order Mark而 rdflib 的 Turtle 解析器不识别 BOM把它当成了非法字符。解决用 VS Code 或 Notepad 重新保存 TTL 文件编码选UTF-8 without BOM或用 Python 脚本批量去除with open(data.ttl, rb) as f: content f.read() if content.startswith(b\xef\xbb\xbf): content content[3:] with open(data_clean.ttl, wb) as f: f.write(content)5.3 坑三SPARQL 查询返回空但手动查图谱发现数据存在原因是 URI 大小写不一致现象问“王维的诗”返回空但用g.query(SELECT ?s WHERE { ?s ex:name 王维 })能查到ex:poet_wangwei。原因parse_question()中to_pinyin(王维)返回WangWei首字母大写而清洗脚本生成的 URI 是poet_wangwei全小写SPARQL 匹配严格区分大小写。解决统一转小写poet_uri fpoet_{to_pinyin(poet_name).lower()}5.4 坑四Flask 启动后页面空白控制台无报错Chrome 开发者工具显示 404现象python app.py成功浏览器打开http://127.0.0.1:5000却是空白页Network 标签显示index.html404。原因Flask 要求templates/文件夹必须与app.py同级且名称必须是templates不能是template或views。一旦放错位置Flask 就找不到 HTML 文件。解决确认目录结构为your_project/ ├── app.py ├── templates/ │ └── index.html ├── data/ └── dict/并在app.py中不要写app Flask(__name__, template_foldermy_templates)用默认路径最稳。6. 让你的毕设答辩更有说服力三个可现场演示的“加分技巧”答辩时导师最想看到的不是代码多炫而是你真正理解每个环节为什么这么设计、边界在哪、怎么验证它没坏。下面这三个技巧我建议你在答辩 PPT 里单独一页展示现场打开终端/浏览器操作30 秒建立专业感。6.1 技巧一用一条命令验证图谱完整性——grep -c ex:poem_ kg.dbSQLite 数据库本质是二进制文件但 rdflib 的 SQLite 存储插件会把三元组以文本形式存入内部表。我们可以用grep快速统计诗的数量# 在项目根目录执行Linux/macOS grep -c ex:poem_ kg.db # 输出58231表示图谱中有 58231 首诗 # Windows 用户可用 PowerShell Select-String -Path kg.db -Pattern ex:poem_ | Measure-Object | % Count提示把这个数字写在答辩 PPT 的“数据规模”页。它比“使用了 5 万首诗”更可信——因为它是从你本地 DB 文件里实时算出来的不是文档里抄的。6.2 技巧二用 SPARQL 控制台现场调试——暴露你的“可解释性”能力rdflib 自带一个简易 SPARQL 控制台启动命令pip install rdflib[tools] rdflib-sparql --file kg.db --query SELECT ?s WHERE { ?s a http://example.org/Poem } LIMIT 5在答辩现场你可以输入SELECT * WHERE { ?s ex:name ?o } LIMIT 3展示诗人名输入SELECT ?p (COUNT(?o) AS ?c) WHERE { ?s ?p ?o } GROUP BY ?p ORDER BY DESC(?c)展示关系类型分布如ex:hasAuthor出现最多这证明你不是只会调 API而是真正掌控图谱结构。6.3 技巧三准备一个“反例问答”并解释为什么答不出——展现工程思维提前准备一个问题比如“请列出所有用‘月’字作诗眼的盛唐五律”。这个问题系统会答不出但你要主动说出来并解释为什么答不出因为“诗眼”是主观修辞概念当前图谱只存客观属性作者、朝代、体裁、字面内容未建模修辞手法怎么改进可引入《诗词格律辞典》的“诗眼”标注规则或用规则匹配“月”字在句首/句尾/韵脚位置这体现了什么你清楚系统的能力边界不是把“答不出”当 bug而是当成需求演进的起点。我在模拟项目 X 的答辩中就用这个反例拿到了“问题意识清晰”的评价。导师说“能看出你思考过这个系统的适用场景而不是堆技术。”最后说句实在话这个项目真正的价值不在于做出一个多完美的系统而在于你亲手把“古诗”“知识图谱”“Python”这三个抽象词拧成了一条可触摸、可调试、可演示的完整链条。从清洗一行乱码到修复一个 URI 大小写再到终端里敲出grep -c看见自己建的图谱有多厚——这些瞬间比任何论文里的“创新点”都扎实。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑