中药方剂知识图谱问答系统:从爬虫到Neo4j全链路实现
简介一套面向中医药信息化的完整项目源码基于知识图谱实现对中药方剂的深度组织、可视化呈现与智能问答。该项目适合计算机专业或中医药交叉领域的毕设、课程设计学习者旨在解决方剂数据碎片化、检索效率低等问题。压缩包共56个文件仅4.33MB其中10个Python脚本承担数据爬取、知识抽取、图谱写入与问答流程CSS/JS/HTML组成前端展示界面JSON文件保存方剂及关系数据辅以字体图片等资源结构清晰。已有68人学习下载。通过本套代码可掌握从爬虫采集与三元组构建、LTP自然语言处理、Neo4j图数据库建图到基于Flask的Web交互与可视化展示的完整链路目录包含KGQA问答模块、neo_db图数据库操作、templates前端页面等附依赖清单和说明文档便于快速运行。适合希望快速搭建中医药知识图谱演示系统或作为毕业设计框架的开发者参考。1. 为什么中药方剂需要一个知识图谱问答系统传统的中药方剂数据大多以关系型数据库或文本表格的形式存在一张表存药材一张表存方剂外键关联一下查起来不难但人对信息的理解是网状的——一味药影响哪些方剂、一个方剂针对哪些症状、药材之间的配伍禁忌链路用表结构很难直观表达。这个项目把方剂、药材、功效、主治症状全部抽成三元组SPO导入 Neo4j 图数据库再配合 LTP 做中文分词与依存句法分析让你可以直接输入治疗风寒感冒的方剂有哪些这类自然语言问题系统先解析意图再转成 Cypher 查询最后把子图返回给前端 ECharts 渲染。适合三类人做中医药信息化的研究者、毕业设计需要完整技术栈演示的学生、以及想了解爬虫 NLP 图数据库 Web 可视化如何串成一条链路的后端工程师。源码里能直接看到从原始文本到图查询的全过程不是 demo 级别的空壳。2. 从爬虫到三元组中药方剂数据的 SPO 抽取与对齐2.1 数据文件到底长什么样解压后raw_data目录里有三个核心文件zhongyao_spo.txt、fangji.json、relations_zhongyao.json。前两个是数据源头第三个是关系约束文件。我用一个最简示例说明 SPO 格式黄芩 清热燥湿 功效 黄芩 味苦 性味 黄芪 补气固表 功效 黄芪 治气虚乏力 主治每一行是实体1 \t 关系 \t 实体2对应知识图谱里的(头实体)-[关系]-(尾实体)。relations_zhongyao.json则定义了关系白名单保证导入 Neo4j 时关系类型不会失控。fangji.json是方剂维度的结构化数据一个方剂包含组成药材、剂量、功效、主治、用法等字段。这里有个值得注意的设计选择zhongyao_spo.txt只覆盖单味中药的属性关系而方剂与药材的组成关系、方剂与症状的主治关系是由fangji.json结合关联脚本生成的。也就是说系统把中药属性和方剂配伍分成两张数据视图最后统一折叠进图数据库。这样做的理由是属性关系可以无限扩展而方剂关系需要专业数据源约束分开管理更利于后续更新。2.2 扒数据与关系抽取脚本spider目录下是采集脚本常见做法是请求百科或药典页面用 XPath 抽取药材信息表再解析功效主治性味归经这些字段转成上述 SPO 行。以get_character_array.py和get_hlm_character.py为例前者抓取药材性状特征数组后者抓取特定药材条目下的每一条属性生成的特征数组会直接追加到 SPO 文本中。# get_character_array.py 核心逻辑示意 import requests from lxml import etree url https://example.com/zhongyao/黄芩 resp requests.get(url, headers{User-Agent: Mozilla/5.0}) html etree.HTML(resp.text) # 抽取药材属性表格 attrs html.xpath(//table[classproperties]//tr) spo_list [] for tr in attrs: tds tr.xpath(./td/text()) if len(tds) 2: head, tail tds[0].strip(), tds[1].strip() spo_list.append(f黄芩\t{head}\t{tail}) # 写回 zhongyao_spo.txt with open(raw_data/zhongyao_spo.txt, a, encodingutf-8) as f: f.write(\n.join(spo_list))这段代码说明一个基本方法把网页表格的行映射成 SPO。你需要在调试时把tds打印出来看字段是否对齐很多页面会把别名写成多行文本直接split会错位。我一般会先统计一下head字段的取值分布如果出现几十种非预期值说明数据源页面结构调整了需要同步更新选择器。2.3 数据质量问题与对齐策略SPO 抽取后必须做实体对齐否则知识图谱就是一堆孤岛。常见问题有三个同一药材多种写法山萸肉 vs 山茱萸、剂量单位混用三钱 vs 9g、功效描述口语化治咳嗽 vs 止咳。源码中get_hlm_character.py的末尾会做一个简单归一化把全角括号转半角、去掉尾部句号、按预置词典做同义词替换。这个词典没有单独文件写死在脚本的alias_dict变量里你如果要扩展数据需要在这里维护一份药材别名映射表。问题类型示例处理策略药材别名山萸肉 / 山茱萸维护别名 dict统一成正名剂量混写三钱 / 9g统一换算成克功效冗余治咳嗽 / 止咳去除动词前缀按功效词归并关系歧义归肺经 vs 肺热区分归经与病症关系类型做完对齐后再把fangji.json里的方剂实体与 SPO 里的药材实体关联。这一步靠creat_graph.py执行它会读取方剂组成字段生成(方剂)-[组成]-(药材)关系。前处理做得好不好直接决定后面 Cypher 查询结果是否准确。3. 把三元组灌进 Neo4j创图脚本与 Cypher 查询设计3.1 py2neo 批量写入的打开方式neo_db目录下的creat_graph.py负责建图。它读取zhongyao_spo.txt和fangji.json通过py2neo连接本地 Neo4j 实例。这里的核心问题是写入性能一条条graph.create()在数据量上万时极慢所以要批量提交。# creat_graph.py 关键片段 from py2neo import Graph, Node, Relationship, Subgraph graph Graph(bolt://localhost:7687, auth(neo4j, password)) # 缓存节点避免重复创建 nodes_cache {} def get_or_create_node(label, name): key f{label}_{name} if key not in nodes_cache: node Node(label, namename) nodes_cache[key] node return nodes_cache[key] subgraph [] with open(raw_data/zhongyao_spo.txt, encodingutf-8) as f: for line in f: head, relation, tail line.strip().split(\t) h_node get_or_create_node(Herb, head) t_node get_or_create_node(Attr, tail) rel Relationship(h_node, relation, t_node) subgraph.append(rel) # 每 500 条提交一次 for i in range(0, len(subgraph), 500): graph.create(Subgraph(subgraph[i:i500])) print(f已提交 {ilen(subgraph[i:i500])}/{len(subgraph)} 条关系)这段代码有三个关键点用Subgraph批量提交代替逐条create速度提升一个数量级用nodes_cache缓存已存在的节点避免重复创建出多个同名实体关系类型直接用 SPO 文本里的关系字符串后续查询时再利用relations_zhongyao.json做关系归一化。注意bolt://端口是 7687不是 HTTP 的 7474很多人在连接这里卡住报错Unauthorized时先检查用户名密码再检查 Neo4j 是否启动了 bolt 协议。3.2 方剂关系的折叠与合并方剂关系不是直接来的需要从fangji.json解析。一个方剂的结构类似方名、组成、功效、主治。组装关系时要拆出三个维度(方剂)-[功效]-(症状)、(方剂)-[组成]-(药材)、(药材)-[主治]-(症状)。合并时注意不要重复建边。我会用 Cypher 的MERGE来保证幂等性。// 幂等建边示例 MATCH (f:FangJi {name: 麻黄汤}) MATCH (m:Herb {name: 桂枝}) MERGE (f)-[:组成 {dosage: 9g}]-(m)MERGE和CREATE的区别在于CREATE无条件创建新边重跑脚本会生成重复关系MERGE先匹配再创建适合批量导入脚本重复执行。每条边的dosage属性来自fangji.json的剂量字段这是方剂图谱区别于一般药材图谱的关键信息后面做配伍分析时会用到。3.3 查询层封装query_graph.py 的设计图谱建好后业务层不能直接到处写 Cypher否则前端每个接口都得改查询。query_graph.py把所有查询封装成函数返回统一的字典结构方便 Flask 路由直接调用。# query_graph.py 核心函数 from neo4j import GraphDatabase class GraphQuery: def __init__(self, uri, user, password): self.driver GraphDatabase.driver(uri, auth(user, password)) def query_fangji_by_symptom(self, symptom): # 通过症状反向查方剂返回方剂组成药材功效 cypher MATCH (f:FangJi)-[:主治]-(s:Symptom {name: $symptom}) OPTIONAL MATCH (f)-[:组成]-(m:Herb) RETURN f.name AS fangji, collect(m.name) AS herbs, f.功效 AS effect LIMIT 20 with self.driver.session() as session: records session.run(cypher, symptomsymptom) return [dict(r) for r in records]单元测试时可以直接在 Cypher Shell 里执行这段语句验证返回的herbs数组有没有重复。LIMIT 20是为防止方剂过多时响应体过大。OPTIONAL MATCH非常关键如果某个方剂没有导出组成关系MATCH会直接过滤掉整行而OPTIONAL MATCH会保留方剂并让药材列表为空。做知识图谱查询时这个细节决定前端图谱是否会出现节点丢失。4. 基于 LTP 依存句法的问答解析与模板匹配4.1 为什么选 LTP 而不是简单关键词匹配问答系统部分在KGQA目录下核心依赖是哈工大 LTP模型文件在model/ltp_data_v3.4.0。之所以不用正则硬匹配因为自然语言问法的变体实在太多哪些药能治咳嗽和咳嗽吃什么方剂语义相同但表面对不齐。LTP 提供分词、词性标注、依存句法分析能从句子结构里抽出核心实体和意图动词。整体流程是用户输入 → LTP 分词和依存分析 → 提取核心词与疑问词 → 匹配意图模板 → 转 Cypher → 执行并返回结果。意图模板定义在KGQA目录的配置里每一类意图对应一个查询模式。4.2 依存句法怎么帮我们提取问句要素拿治疗风寒感冒的方剂有哪些这句话来说LTP 依存分析会给出类似结构治疗 -- 核心谓语 (HED) 风寒感冒 -- 宾语 (VOB) 方剂 -- 客体 有哪些 -- 疑问标记代码里通过遍历依存弧找到VOB或ATT关系中类型为症状的子节点再反查父节点是否为意图动词就能把整句话归一到症状查方剂模板。这样做的好处是即使换成什么方剂可以缓解风寒感冒只要词性标注和依存结构一致依然能命中。# KGQA/ltp.py 关键片段 from pyltp import Segmentor, Postagger, Parser segmentor Segmentor(model_pathmodel/ltp_data_v3.4.0/cws.model) postagger Postagger(model_pathmodel/ltp_data_v3.4.0/pos.model) parser Parser(model_pathmodel/ltp_data_v3.4.0/parser.model) words list(segmentor.segment(治疗风寒感冒的方剂有哪些)) postags list(postagger.postag(words)) arcs parser.parse(words, postags) # 遍历依存关系寻找 核心谓词-宾语 结构 for arc in arcs: # arc.head 是父节点索引arc.relation 是关系名 if arc.relation VOB: child_word words[arc.head - 1] # 注意 LTP 索引从 1 开始 parent_word words[arc.head - 1] print(f动词: {parent_word}, 宾语: {child_word})这段代码有个典型坑arc.head是父节点的位置但 LTP 的索引从 1 开始而words是 Python 列表从 0 开始所以取词必须words[arc.head - 1]写错就 IndexError。另一个坑是 LTP 模型文件对 Python 版本有编译要求pyltp在 Python 3.10 上经常编译失败我建议用 Python 3.7 或 3.8 跑问答模块Web 展示层可以用更高版本两个服务分开部署。4.3 意图模板与 Neo4j 查询的映射模板匹配是整个问答准确率的关键。下面这张表是项目里常见意图类型和对应 Cypher 的映射用户问题意图类型抽取要素目标查询治疗XX的方剂SYMPTOM_TO_FANGJI症状名MATCH (f)-[:主治]-(s {name: XX}) RETURN fXX方剂的组成FANGJI_TO_HERB方剂名MATCH (f {name: XX})-[:组成]-(m) RETURN mXX药材的功效HERB_TO_EFFECT药材名MATCH (h {name: XX})-[:功效]-(e) RETURN eXX和XX能否同用HERB_CONFLICT两个药材名MATCH (a)-[:配伍禁忌]-(b) WHERE a.nameXX AND b.nameXXKGQA里的query.py先做实体识别把分词结果与图数据库中已有实体做模糊匹配得到一个置信度高于阈值才进模板匹配。如果实体识别概率低会返回换个说法再试一次。这一步是问答系统体验差异的关键直接做字符串精确匹配的话黄芩写成黄岑就废了。4.4 语音输入与兜底机制README 里提到支持语音识别实现方式一般是在前端用 Web Speech API 或其他语音转文字 SDK把音频转成文字后再走上面的问答链路。templates/search.html里有一个麦克风按钮点击后录制识别结果填入搜索框。// templates/search.html 语音识别接口示意 const recognition new webkitSpeechRecognition(); recognition.lang zh-CN; recognition.onresult function (event) { const text event.results[0][0].transcript; document.getElementById(question).value text; submitQuestion(text); }; recognition.start();语音识别的文本往往带语气词嗯治疗那个风寒感冒的方剂这类输入直接进入 LTP 会干扰依存分析。我的做法是在提交前先做一次停用词过滤保留治疗、风寒感冒、方剂这类实词再进意图匹配。如果匹配失败兜底方案是直接用余弦相似度对用户问句和所有实体名计算相似度返回 Top 5 实体作为联想结果。5. 知识图谱的可视化输出与部署排错5.1 从查询结果到前端图形数据的转换后端返回的是 Cypher 记录列表前端 ECharts 需要nodes和links两个数组。neo2json.py就是这个转换层把每个方剂、药材、症状实体映射为节点把关系映射为带source和target的边。# neo2json.py 核心转换逻辑 import json def convert_to_echarts(records): nodes, links [], [] node_ids set() for record in records: source record.get(source) target record.get(target) rel record.get(rel) if source and source[name] not in node_ids: nodes.append({ id: source[name], name: source[name], category: source[label], symbolSize: 50 if source[label] FangJi else 30, }) node_ids.add(source[name]) if target and target[name] not in node_ids: nodes.append({ id: target[name], name: target[name], category: target[label], symbolSize: 30, }) node_ids.add(target[name]) links.append({source: source[name], target: target[name], label: rel}) return {nodes: nodes, links: links} # 供 Flask 路由调用jsonify(convert_to_echarts(records))转换时一个重要参数是symbolSize方剂类型的节点必须大于药材和症状否则图谱层次感出不来。前端KGQA.html里用graph.category做图例分组点击节点后触发邻接关系高亮这个交互是index.html和search.html共用的一套 JS 逻辑在static/js目录下建议你改样式时先确认是同一份还是各页面单独打包。5.2 启动与部署常见报错这个项目最常见的坑集中在环境依赖上。requirements.txt里锁定了py2neo、flask、pyltp等版本但缺少neo4jPython 驱动的显式依赖。如果你只装了py2neoquery_graph.py里的neo4j.GraphDatabase会直接 ImportError。正确做法是pip install -r requirements.txt pip install neo4j4.4.11 python app.pyapp.py启动前要确认 Neo4j 已运行并且conf里的bolt_port和认证信息与config.py匹配。config.py中NEO4J_URI默认写的是bolt://localhost:7687如果你的 Neo4j 装在 Docker 容器里这里要改成宿主机的 IP且docker run时要映射7474:7474和7687:7687两个端口。前端图谱不显示时打开浏览器开发者工具看 Network 面板确认/api/graph返回的数据里nodes非空。很多时候后端报错被 Flask 吞掉只返回 500你需要在app.py里临时打开debugTrue或者在neo2json.py的convert_to_echarts入口加一行records list(records)强制物化结果因为 Neo4j 的 Result 对象只能迭代一次前端拿不到数据往往是后端已经迭代完了。最后一个性能技巧图谱全量渲染时节点超过 200 个浏览器会明显卡顿。我一般会在query_graph.py里加一个limit参数默认只返回实体度最高的前 50 个节点用户在图例上点击某类实体时才动态展开。这样的交互方式既保住了可视化的直观感又不会让前端卡死。本文还有配套的精品资源点击获取

相关新闻

外贸网站零流量真相:Google不收录的7个技术堵点

外贸网站零流量真相:Google不收录的7个技术堵点

1. 外贸网站上线后零流量的真实原因:不是Google不给流量,是你没进它的“审核队列”“花钱做了外贸网站,为什么Google一直没有流量?”——这句话我每年至少听300遍,来自不同国家、不同行业、不同预算的客户。他们共同点…

2026/11/13 1:10:26 阅读更多 →
Cataclysm-DDA 地形/家具检查行为(examine_action)完全指南:硬编码动作与 Examine Actors 详解

Cataclysm-DDA 地形/家具检查行为(examine_action)完全指南:硬编码动作与 Examine Actors 详解

Cataclysm-DDA 地形/家具检查行为(examine_action)完全指南:硬编码动作与 Examine Actors 详解 【免费下载链接】Cataclysm-DDA Cataclysm - Dark Days Ahead. A turn-based survival game set in a post-apocalyptic world. 项目地址: htt…

2026/10/25 15:25:09 阅读更多 →
资金核对平台演进全解:从Excel手工对账到实时智能对账系统

资金核对平台演进全解:从Excel手工对账到实时智能对账系统

1. 为什么资金核对平台会存在:先搞清楚对账在解决什么问题1.1 对账是“账实相符”的守门员做过支付、电商、财务或者任何跟资金流水打交道的人,应该都懂这个场景:系统里显示用户付了100元,银行渠道侧却只到了99.7元;或…

2026/10/27 10:53:32 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/11/12 5:00:00 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/11/12 5:13:20 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/12 5:13:20 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/11/12 18:22:26 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/11/12 18:22:25 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/11/12 5:13:20 阅读更多 →

日新闻

周新闻

月新闻