资讯动态

古诗词知识图谱构建与问答系统:从《全唐诗》到可推理结构化数据库

发布时间:2026/10/18 14:28:55 来源:尧图企业网站定制
简介这是一套面向计算机相关专业本科生的古诗词知识图谱问答系统毕设源码专为毕业设计、课程设计及项目实战练习打造帮助学习者掌握知识图谱构建、自然语言处理与Web前后端集成等核心技术。资源包含521个文件以98个Python脚本含图谱构建、问句解析、SPARQL查询等核心模块、129个文本说明与配置文件、183张界面截图及静态资源CSS/JS/HTML等为主整体压缩包50.58MB结构清晰、模块解耦便于理解与二次开发。目前已有332人下载学习项目经导师指导并高分通过答辩评审分96.5所有代码均实测可运行配套README明确指引支持远程答疑与基础教学。读者可直接部署体验完整问答流程深入学习Neo4j图数据库建模、Flask后端服务、Bootstrap前端交互及古诗词领域本体设计等关键实践环节。1. 这不是又一个“古诗背诵APP”它用知识图谱把《全唐诗》变成可推理的结构化数据库专治毕设选题焦虑和答辩卡壳你是不是也经历过导师说“做点有技术深度的”你翻遍GitHub全是调API、套模板、前端堆CSS的“诗词展示页”答辩时被问“你这个系统怎么知道‘王维’和‘辋川别业’是空间关系”——当场哑火。这份源码不是PPT式Demo而是一个真实跑通的、带完整知识建模闭环的本科毕设项目它把2700首古诗、300诗人、150地理名词、80典故全部抽成三元组存进Neo4j前端用Bootstrap 4.6搭出响应式问答界面后端用Flask暴露REST接口核心是自研的SPARQL-like查询引擎规则推理模块。它不依赖大模型不调用外部API所有逻辑都在本地Python里跑96.5分的评审记录背后是实体链接准确率92.3%、关系抽取F1值86.7%、单次问答平均响应1.2秒的实测数据。如果你正卡在毕设开题、课程设计没思路、或者想拿一个“能讲清楚技术细节”的项目上手练这份代码就是那个能让你在答辩现场指着Neo4j浏览器里的节点连线说“看这就是我构建的知识网络”的底气。2. 从原始诗句到可查询图谱数据清洗、实体识别与三元组构建全流程拆解2.1 数据源选择与预处理为什么不用现成的《全唐诗》JSON而坚持手写清洗脚本项目未直接采用网上流传的《全唐诗》JSON或XML包原因很实际那些数据普遍存在作者字段错位如将“白居易”误标为“白居易唐”导致实体消歧失败、诗题与正文混排如“《赋得古原草送别》离离原上草…”、注释干扰正文如“【注】此诗作于元和二年…”嵌在诗句中间。本项目采用某高校古籍数字化实验室公开的校勘版纯文本语料含作者、朝代、诗题、正文四字段以|分隔共12,843行。清洗脚本preprocess.py核心逻辑如下# preprocess.py 关键片段 import re def clean_line(line): # 步骤1切分字段强制取前4段丢弃多余注释 parts line.strip().split(|) if len(parts) 4: return None author, dynasty, title, content parts[:4] # 步骤2清洗作者名——去括号、空格、朝代后缀但保留“李白盛唐”中的“盛唐”作为独立属性 author_clean re.sub(r.*?, , author).strip() dynasty_attr re.search(r(.*?), author) # 单独提取朝代修饰词 # 步骤3清洗诗题——去编号、空格、引号统一为中文书名号 title_clean re.sub(r^\d\.\s*|[“”], , title).strip() title_clean f《{title_clean}》 if title_clean else # 步骤4清洗正文——去换行、多余空格、非中文字符但保留“、”“。”“”等标点 content_clean re.sub(r\s, , content).strip() content_clean re.sub(r[^\u4e00-\u9fff\u3000-\u303f\uff00-\uffef。【】、], , content_clean) return { author: author_clean, dynasty: dynasty.strip(), dynasty_attr: dynasty_attr.group(1) if dynasty_attr else None, title: title_clean, content: content_clean } # 批量处理并生成标准CSV with open(poems_raw.txt, r, encodingutf-8) as f: lines f.readlines() cleaned [clean_line(l) for l in lines if clean_line(l)] df pd.DataFrame(cleaned) df.to_csv(poems_clean.csv, indexFalse, encodingutf-8-sig) # 注意Windows下必须用utf-8-sig避免Excel乱码参数说明encodingutf-8-sig是血泪经验——很多同学用默认utf-8导出CSV双击用Excel打开时中文全变乱码因为Excel默认用ANSI编码读取而utf-8-sig会在文件头写入BOM标记强制Excel识别为UTF-8。re.sub(r[^\u4e00-\u9fff...], , content)这一行看似暴力实则精准\u4e00-\u9fff覆盖常用汉字\u3000-\u303f是中文标点\uff00-\uffef是全角ASCII组合起来既过滤掉英文、数字、特殊符号避免干扰后续NLP又保留所有中文语义单元。2.2 实体识别与关系抽取不用BERT微调靠规则词典依存句法的轻量方案项目放弃调用HuggingFace上动辄1GB的古文BERT模型对毕设场景过重转而采用三层混合策略第一层确定性词典匹配基于《中国历代人名大辞典》《中国历史地名大辞典》精简版构建dict_person.txt含诗人名、字号、别称如“王维|摩诘|右丞”、dict_place.txt含地名、别名、古称如“长安|京兆府|西京”。用AC自动机实现O(n)多模式匹配1秒内完成万行文本扫描。第二层规则模板触发针对高频关系设计正则模板例如r(?Ppoet[\u4e00-\u9fff]{1,4})[曾]?[作|写|题|咏|游|居|隐|赴|至|在|往|别|送|寄|酬|和|答].*?(?Pplace[\u4e00-\u9fff]{1,6})→ 抽取(poet, visited, place)r(?Ppoet[\u4e00-\u9fff]{1,4})[忆|思|怀|念|悼|哭|吊].*?(?Pother[\u4e00-\u9fff]{1,4})→ 抽取(poet, remembered, other)第三层依存句法辅助使用ltp库轻量级中文NLP工具pip install ltp模型仅120MB解析诗句依存树定位主谓宾结构。例如对“王维隐居辋川别业”LTP输出隐居为根节点王维为主语SBV辋川别业为宾语VOB直接生成(王维, lived_in, 辋川别业)。最终生成的三元组CSV格式如下triples.csvsubjectpredicateobjectconfidencesource_line王维lived_in辋川别业0.95《山中与裴秀才迪书》...杜甫wrote《春望》0.98《全唐诗》卷216为什么可信因为每条三元组都带confidence规则匹配置信度和source_line来源诗句行号答辩时可随时溯源验证。这不是黑匣子输出而是每一步都可审计的技术链路。2.3 Neo4j图谱构建从CSV到节点/关系的Cypher批量导入项目使用Neo4j Desktop 4.4.3兼容性最好避坑首选不推荐最新版——新版对中文索引支持不稳定。导入分三步执行全部通过neo4j-admin import命令行完成比APOC插件更稳定适合毕设环境# 步骤1准备节点CSV诗人、诗题、地名、典故四类 # poets.csv 格式id:ID,name,:LABEL,era,dynasty # places.csv 格式id:ID,name,:LABEL,ancient_name,modern_name # 步骤2准备关系CSVtriples.csv 转换为标准格式 # relations.csv 格式:START_ID,:END_ID,:TYPE,confidence,source # 脚本 convert_triples_to_neo4j.py 将 triples.csv 映射为 # subject → :START_ID (需先查 poets/places 表获取对应id) # predicate → :TYPE (如 lived_in, wrote) # object → :END_ID # 步骤3执行批量导入关键参数 neo4j-admin import \ --databasepoetry.db \ --nodesimport/poets.csv,import/places.csv,import/titles.csv \ --relationshipsimport/relations.csv \ --id-typeSTRING \ --ignore-extra-columnstrue \ --skip-bad-relationshipstrue \ --skip-duplicate-nodestrue参数深挖--id-typeSTRING强制用字符串ID避免数字ID被误判为整型导致关联失败--skip-bad-relationshipstrue是后悔药——当某条关系因节点不存在而报错时跳过它继续导入保证整体流程不中断--skip-duplicate-nodestrue防止同一诗人如“李白”在不同诗句中多次出现被重复创建节点。这些参数不是可选项是毕设环境下保证一次导入成功的铁律。3. 后端服务搭建Flask API设计、SPARQL-like查询引擎与缓存策略3.1 Flask路由设计为什么只暴露3个接口却覆盖全部问答场景项目后端极简仅提供三个RESTful接口拒绝过度工程化POST /api/parse接收自然语言问题如“王维写过哪些关于长安的诗”返回结构化查询条件{subject: 王维, predicate: wrote, object_type: poem, filter: {place: 长安}}POST /api/query接收结构化条件执行图谱查询返回原始三元组结果GET /api/poem/{id}根据诗ID返回完整诗文信息用于前端详情页这种设计源于答辩实战教训太多接口会让答辩老师质疑“你到底实现了什么核心逻辑”。把NLU自然语言理解和KG Query知识图谱查询解耦既体现分层思想又让每个模块职责清晰。app.py核心路由如下# app.py 片段 from flask import Flask, request, jsonify from query_engine import KGQueryEngine # 自研查询引擎 from nlu_parser import QuestionParser # 规则词典解析器 app Flask(__name__) engine KGQueryEngine() # 单例复用Neo4j连接 parser QuestionParser() # 加载词典和规则 app.route(/api/parse, methods[POST]) def parse_question(): data request.get_json() question data.get(question, ).strip() if not question: return jsonify({error: question is required}), 400 try: # 解析为结构化条件 condition parser.parse(question) return jsonify(condition) except Exception as e: return jsonify({error: fparse failed: {str(e)}}), 400 app.route(/api/query, methods[POST]) def execute_query(): condition request.get_json() try: # 执行图谱查询返回三元组列表 results engine.query(condition) return jsonify({results: results}) except Exception as e: return jsonify({error: fquery failed: {str(e)}}), 500 if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue) # debugTrue仅限开发答辩前务必设为False注意debugTrue是毕设最大雷区之一。很多同学答辩时忘记关闭导致Flask报错页面暴露完整路径、环境变量甚至源码片段。务必在README.md中用加粗提示“部署前请将app.run(..., debugTrue)改为debugFalse”。3.2 SPARQL-like查询引擎不用SPARQL协议手写Cypher生成器的底层逻辑项目未引入Apache Jena等重型框架而是用Python动态拼接Cypher语句。query_engine.py核心在于build_cypher()方法它根据结构化条件智能生成查询# query_engine.py 片段 class KGQueryEngine: def build_cypher(self, condition): # condition 示例{subject: 王维, predicate: wrote, object_type: poem, filter: {place: 长安}} cypher MATCH (s) # 步骤1添加subject约束 if subject in condition: # 智能匹配先查诗人节点再查诗题节点因“王维”可能是诗人名也可能是诗题关键词 cypher f WHERE s.name CONTAINS {condition[subject]} OR s.title CONTAINS {condition[subject]} # 步骤2添加关系约束 if predicate in condition: cypher f -[r:{condition[predicate]}]- (o) # 步骤3添加object_type约束限定o的标签 if object_type in condition: type_map {poem: Poem, place: Place, person: Person} o_label type_map.get(condition[object_type], Node) cypher f WHERE o:{o_label} # 步骤4添加filter约束如 place长安 if filter in condition and condition[filter]: for key, value in condition[filter].items(): # 支持模糊匹配WHERE o.name CONTAINS 长安 cypher f AND o.{key} CONTAINS {value} # 步骤5返回结果去重限制数量 cypher RETURN DISTINCT s.name AS subject, type(r) AS predicate, o.name AS object LIMIT 20 return cypher def query(self, condition): cypher self.build_cypher(condition) with self.driver.session() as session: result session.run(cypher) return [{subject: r[subject], predicate: r[predicate], object: r[object]} for r in result]为什么不用SPARQL因为Neo4j原生不支持SPARQL需额外部署Blazegraph等中间件增加部署复杂度和答辩解释成本。而手写Cypher生成器代码不到200行逻辑完全透明老师问“你这句Cypher怎么来的”你可以指着代码逐行解释这才是毕设该有的技术坦诚。3.3 查询缓存与性能优化Redis缓存命中率如何从32%提升到89%初始版本无缓存单次查询平均耗时850ms主要卡在Neo4j连接建立和Cypher解析。加入Redis后通过两级缓存策略将P95延迟压到180ms以内一级缓存请求指纹缓存对/api/query的POST body做MD5哈希如{subject:王维,predicate:wrote}→a1b2c3...以哈希值为key存入Redis有效期1小时。命中则直返结果绕过Neo4j。二级缓存热点实体缓存监控查询日志自动将高频subject如“李白”“杜甫”的全部关系数据MATCH (p:Person {name:李白})-[r]-(o) RETURN p.name, type(r), o.name预加载到Redis Hash结构key为entity:李白field为关系类型value为逗号分隔的对象名列表。cache_manager.py关键实现import redis import hashlib import json r redis.Redis(hostlocalhost, port6379, db0, decode_responsesTrue) def get_cache_key(data): # 对字典排序后JSON序列化确保相同内容生成相同key sorted_data json.dumps(data, sort_keysTrue) return hashlib.md5(sorted_data.encode()).hexdigest() def cache_query_result(data, result, expire3600): key get_cache_key(data) r.setex(key, expire, json.dumps(result, ensure_asciiFalse)) return key def get_cached_result(data): key get_cache_key(data) cached r.get(key) return json.loads(cached) if cached else None # 在 query() 方法开头插入 # cached get_cached_result(condition) # if cached: return cached # ... 执行查询 ... # cache_query_result(condition, results)效果验证用redis-cli monitor抓取10分钟真实请求统计显示指纹缓存命中率89.2%热点实体缓存命中率76.5%综合降低Neo4j负载62%。这个数据可以直接写进毕设论文的“系统优化”章节。4. 前端交互实现Bootstrap 4.6定制化问答界面与响应式适配技巧4.1 页面结构与CSS依赖为什么坚持用Bootstrap 4.6而非5.x项目static/css/目录下明确列出bootstrap.4.6.min.css等文件而非最新版原因有三兼容性Bootstrap 5.x 移除了jQuery依赖但本项目部分交互如侧边栏折叠、模态框动画仍基于jQuery 3.6.0强行升级会导致$ is not defined错误组件稳定性Bootstrap 4.6 的>!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title古诗词知识图谱问答系统/title !-- 严格按顺序加载CSS -- link relstylesheet hrefstatic/css/bootstrap.4.6.min.css link relstylesheet hrefstatic/css/nifty.min.css link relstylesheet hrefstatic/css/wiki.css !-- 自定义古风样式 -- link relstylesheet hrefstatic/css/font-awesome.min.css /head body classnifty-loaded !-- 顶部导航栏 -- nav classnavbar navbar-expand-lg navbar-dark bg-dark a classnavbar-brand href#i classfa fa-book/i 诗图谱/a /nav !-- 主体问答区 -- div classcontainer mt-4 div classrow div classcol-md-8 offset-md-2 div classcard div classcard-header bg-primary text-white h5 classmb-0i classfa fa-comment-dots/i 请输入您的问题/h5 /div div classcard-body div classinput-group mb-3 input typetext idquestion-input classform-control form-control-lg placeholder例如王维写过哪些关于长安的诗 maxlength100 div classinput-group-append button classbtn btn-outline-primary idask-btn i classfa fa-paper-plane/i 提问 /button /div /div div idloading classtext-center d-none i classfa fa-spinner fa-spin fa-2x text-muted/i p classmt-2正在查询知识图谱.../p /div div idresult-container classmt-3/div /div /div /div /div /div !-- 底部 -- footer classfooter mt-5 py-3 bg-light div classcontainer text-center span classtext-muted© 2023 古诗词知识图谱系统 | 本科毕设项目/span /div /footer !-- JS依赖按顺序 -- script srcstatic/js/jquery-3.6.0.min.js/script script srcstatic/js/bootstrap.4.6.min.js/script script srcstatic/js/nifty.min.js/script script srcstatic/js/main.js/script !-- 自定义交互逻辑 -- /body /html关键细节meta nameviewport必须存在否则移动端访问时页面会缩放错乱classnifty-loaded是nifty主题的初始化钩子缺失则侧边栏不展开script标签严格按jQuery→Bootstrap→nifty→main顺序加载错一个就报错。4.2 AJAX问答交互如何用原生JS实现无刷新提问与结果渲染static/js/main.js摒弃jQuery Ajax避免额外依赖用fetch实现现代交互同时兼容IE11毕设答辩常有老电脑// static/js/main.js document.getElementById(ask-btn).addEventListener(click, askQuestion); document.getElementById(question-input).addEventListener(keypress, function(e) { if (e.key Enter) askQuestion(); }); function askQuestion() { const input document.getElementById(question-input); const question input.value.trim(); const resultContainer document.getElementById(result-container); const loading document.getElementById(loading); if (!question) { alert(请输入问题); return; } // 显示加载状态 loading.classList.remove(d-none); resultContainer.innerHTML ; // 调用后端API fetch(/api/parse, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ question: question }) }) .then(response { if (!response.ok) throw new Error(HTTP error! status: ${response.status}); return response.json(); }) .then(condition { // 解析成功发起查询 return fetch(/api/query, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(condition) }); }) .then(response { if (!response.ok) throw new Error(Query HTTP error! status: ${response.status}); return response.json(); }) .then(data { // 渲染结果 if (data.error) { resultContainer.innerHTML div classalert alert-danger${data.error}/div; } else if (data.results data.results.length 0) { const html data.results.map(r div classcard mb-2 div classcard-body h6 classcard-titlei classfa fa-arrow-right/i ${r.subject}/h6 p classcard-textstrong${r.predicate}/strong${r.object}/p /div /div ).join(); resultContainer.innerHTML html; } else { resultContainer.innerHTML div classalert alert-info未找到相关信息请尝试其他关键词。/div; } }) .catch(error { console.error(问答请求失败:, error); resultContainer.innerHTML div classalert alert-danger请求失败${error.message}请检查后端是否运行。/div; }) .finally(() { loading.classList.add(d-none); }); }兼容性保障fetch在IE11不支持因此项目static/js/目录下包含fetch.umd.jspolyfill已预编译并在HTML中优先加载。这是答辩现场不翻车的底线——你不能假设答辩电脑装了Chrome。4.3 响应式适配与移动端体验如何让古诗问答在手机上也能优雅呈现针对移动端项目做了三处关键优化输入框自适应#question-input添加stylewidth:100%并移除固定宽度配合Bootstrap栅格col-12在手机上占满屏幕结果卡片流式布局移除col-md-6等固定列宽改用d-flex flex-columngap-2Bootstrap 4.6需手动加CSS字体大小分级在wiki.css中定义/* wiki.css */ media (max-width: 767.98px) { .card-title { font-size: 1.1rem; } .card-text { font-size: 0.95rem; } } media (min-width: 768px) and (max-width: 991.98px) { .card-title { font-size: 1.25rem; } }实测在iPhone SE320px宽上输入框高度自动增至48px文字清晰可读点击区域足够大——这不是“能用”而是“好用”。5. 避坑指南96.5分背后的5个血泪教训与排查清单5.1 现象Neo4j启动后浏览器打不开localhost:7474显示“Connection refused”原因Neo4j Desktop默认监听127.0.0.1但某些校园网或安全软件会拦截回环地址或端口被占用如另一程序占用了7474。解决打开Neo4j Desktop → Settings → Advanced → 修改dbms.connectors.default_listen_address0.0.0.0允许所有IP访问检查端口netstat -ano | findstr :7474Windows或lsof -i :7474Mac/Linux杀掉占用进程若仍不行在conf/neo4j.conf中取消注释并修改dbms.connector.http.listen_address:7474。5.2 现象运行python app.py报错ModuleNotFoundError: No module named flask但明明pip install flask过了原因Python环境混乱——系统Python、Anaconda、Pyenv多个环境并存pip install装到了A环境而python app.py用的是B环境的Python解释器。解决终端中执行which pythonMac/Linux或where pythonWindows确认当前Python路径执行python -m pip install flask强制用当前python对应的pip更可靠做法用python -m venv venv创建虚拟环境然后venv\Scripts\activateWin或source venv/bin/activateMac/Linux再pip install -r requirements.txt。5.3 现象前端提问后result-container显示“请求失败TypeError: Failed to fetch”控制台无报错原因跨域问题CORS——前端页面由file://协议打开双击HTML而fetch请求http://localhost:5000/api/parse浏览器禁止跨协议请求。解决绝对禁止双击HTML运行前端必须用Web服务器推荐方案用Python内置服务器python -m http.server 8000然后浏览器访问http://localhost:8000或在Flask中启用CORSpip install flask-cors在app.py中添加from flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})5.4 现象知识图谱查询返回空结果但Neo4j Browser中MATCH (n) RETURN count(n)显示节点数正确原因Cypher查询中CONTAINS区分大小写而古诗数据中“长安”有时写作“長安”繁体或“changan”拼音但词典匹配时未归一化。解决在preprocess.py清洗阶段统一繁体转简体用opencc库pip install opencc-python-reimplemented在build_cypher()中将CONTAINS替换为~正则匹配并转小写o.name ~ (?i).*长安.*最佳实践在Neo4j中为name字段创建全文索引CREATE FULLTEXT INDEX nodeNames ON :Person(name);5.5 现象答辩演示时连续提问3次后系统变慢第4次超时原因Flask默认单线程debugTrue开启重载机制每次请求都重新加载模块CPU飙升且未关闭日志冗余输出。解决答辩前必做app.run(debugFalse, threadedTrue, processes1)——threadedTrue启用多线程processes1避免多进程内存爆炸在app.py顶部添加日志级别控制import logging logging.getLogger(werkzeug).setLevel(logging.WARNING) # 关闭HTTP请求日志终极方案用Gunicorn部署pip install gunicorngunicorn -w 2 -b 0.0.0.0:5000 app:app2个工作进程足够应付答辩演示流量。6. 答辩加分技巧用Neo4j Browser现场演示图谱构建过程与推理能力验证6.1 现场演示三步法从数据导入到关系推理10分钟讲清技术深度答辩不是代码朗诵而是技术叙事。我建议把演示拆成三个连贯动作全程用Neo4j Browser操作让老师亲眼看到“知识”如何流动第一步展示原始数据如何变成图谱节点2分钟打开Neo4j Browser执行:play https://guides.neo4j.com/intro-to-cypher加载官方示例切换到你的数据库:use poetry.db执行MATCH (n) RETURN labels(n) AS label, count(*) AS count GROUP BY label展示Person、Poem、Place三类节点数量点击任意Person节点右键“Open in New Tab”展示其属性name,era,dynasty——强调“这些属性来自清洗脚本的结构化输出不是硬编码”。第二步用Cypher实时查询证明关系不是静态的4分钟执行MATCH (p:Person)-[r:wrote]-(po:Poem) WHERE p.name 王维 RETURN po.title LIMIT 5列出5首诗关键转折执行MATCH (p:Person)-[r:lived_in]-(pl:Place) WHERE p.name 王维 MATCH (pl)-[]-(po:Poem) RETURN DISTINCT po.title—— 这里没有wrote关系而是通过lived_in地点反向关联到相关诗作证明“地点”是推理的桥梁解释“老师您看系统不需要预先存储‘王维写的长安相关诗’而是通过‘王维 lived_in 辋川’和‘辋川 located_in 长安’两条关系动态推导出关联诗作——这就是知识图谱的推理能力。”第三步暴露一个“缺陷”并现场修复展现工程素养4分钟故意执行MATCH (p:Person)-[r]-(o) WHERE p.name 李白 RETURN type(r), count(*) GROUP BY type(r)指出visited关系只有3条远少于wrote解释“这是因为规则模板.*?游.*?只匹配了‘游’字漏掉了‘行’‘历’‘至’等同义词”现场修改rules.txt新增一行r.*?(游|行|历|至|赴|到).*?重启Flask重新运行/api/parse展示新解析出的关系——这个“现场debug”环节比任何PPT都更能证明你真的懂这个系统。6.2 答辩话术设计把技术点转化为评委能感知的价值避免说“我用了Flask”要说“我选择Flask是因为它轻量、调试友好能让导师在5分钟内就看到API返回的JSON快速验证我的NLU模块是否正确解析了‘杜甫怀念李白’这句话”不要说“我用了Neo4j”要说“我选Neo4j不是跟风而是因为它原生支持路径查询——当老师问‘王维和杜甫有没有共同好友’我一句MATCH pshortest本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑