#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 9种中医体质 × 方剂/药材/穴位 深层挖掘 ========================================= 数据源: - 方剂库 (1000个JSON) - 中药材库 (1000个JSON) - 针灸穴位库 (1000个JSON) - 九种体质详细数据.json 输出: 1. JSON: 体质-方剂药材穴位挖掘.json 2. MD: 体质数据挖掘报告.md (追加在疾病症状部分之后) """ import json import os import re import sys from collections import defaultdict, Counter from pathlib import Path # ============================================================ # 0. 路径设置 # ============================================================ BASE = Path("/home/songyi/Documents/ai_agent_scraper_study/data/大医网") DIR_FANGJI = BASE / "01_来源数据/方剂" DIR_YAOCAI = BASE / "01_来源数据/中药材" DIR_XUEWEI = BASE / "01_来源数据/针灸穴位" CONST_PATH = BASE / "02_加工数据/中医体质/九种体质详细数据.json" OUT_JSON = BASE / "03_关联融合/交叉关联分析/体质-方剂药材穴位挖掘.json" OUT_MD = BASE / "04_分析报告/体质数据挖掘报告.md" # 确保输出目录存在 OUT_JSON.parent.mkdir(parents=True, exist_ok=True) OUT_MD.parent.mkdir(parents=True, exist_ok=True) # ============================================================ # 1. 加载体质数据 & 提取关键字 # ============================================================ print("[1/6] 加载体质数据...") with open(CONST_PATH, "r", encoding="utf-8") as f: constitutions = json.load(f) # 9种体质: 名称 + 关联关键字 const_info = {} # { "气虚质": { "keywords": [...], "name": "气虚质" } } for c in constitutions: name = c["名称"] kw = c.get("关联关键字", []) const_info[name] = { "keywords": kw, "name": name, "data": c } const_names = list(const_info.keys()) print(f" 加载 {len(const_names)} 种体质: {const_names}") # ============================================================ # 2. 加载全部数据 # ============================================================ print("[2/6] 加载方剂、药材、穴位数据...") def load_json_files(directory): """加载目录下所有JSON文件,返回列表""" items = [] files = sorted(os.listdir(directory)) for fname in files: if not fname.endswith(".json"): continue fpath = os.path.join(directory, fname) try: with open(fpath, "r", encoding="utf-8") as f: data = json.load(f) items.append(data) except Exception as e: print(f" ⚠ 加载失败 {fname}: {e}") return items # 方剂 fangji_list = load_json_files(DIR_FANGJI) print(f" 方剂: {len(fangji_list)} 个") # 中药材 yaocai_list = load_json_files(DIR_YAOCAI) print(f" 药材: {len(yaocai_list)} 个") # 针灸穴位 xuewei_list = load_json_files(DIR_XUEWEI) print(f" 穴位: {len(xuewei_list)} 个") # ============================================================ # 3. 构建药材名索引 (用于最长前缀匹配) # ============================================================ print("[3/6] 构建药材名称索引 (最长前缀匹配)...") # 收集所有药材名称 (按长度降序排序,避免 金 vs 金银花 误配) herb_names_all = [] for h in yaocai_list: name = h.get("名称", "").strip() if name: herb_names_all.append(name) # 也加入别名中的药材名 aliases = h.get("别名", "") if aliases: # 别名格式: "木丹、鲜支、越桃、支子、山栀子、栀子" for a in re.split(r'[、,,]', aliases): a = a.strip() if a and len(a) >= 2: herb_names_all.append(a) # 去重并按长度降序排序 herb_names_all = sorted(set(herb_names_all), key=lambda x: (-len(x), x)) print(f" 药材名称库: {len(herb_names_all)} 个 (含别名)") def longest_prefix_match(text, herb_list=None): """ 在 text 中搜索药材名(最长前缀匹配) 返回匹配到的药材名列表 """ if herb_list is None: herb_list = herb_names_all found = [] # 按长度降序遍历,确保先匹配长名 remaining = text # 简单方法:对每个已知药材名,看是否出现在text中 # 但为避免重叠,我们使用贪心匹配 for hname in herb_list: if hname in remaining: found.append(hname) # 移除已匹配部分,避免重复/重叠 # 但这里我们不用移除,因为方解-组成中是"、"分隔的,不会有重叠 return found def extract_herbs_from_formula(formula): """ 从方剂数据中提取药材名 优先从 方解-组成 字段提取 其次是 组成 字段 最后从 方义 字段中提取 """ herbs = [] # 1. 尝试 方解-组成 if "方解-组成" in formula: text = formula["方解-组成"] # 用、分割 parts = re.split(r'[、,,]', text) for p in parts: p = p.strip() if p: herbs.append(p) # 2. 尝试 组成 if not herbs and "组成" in formula: text = formula["组成"] # 用、或空格分割,去掉剂量信息 # 例如: 桂枝(别切)四两、甘草(炙)四两、白术三两 # 提取括号前的药材名 parts = re.split(r'[、,,]', text) for p in parts: p = p.strip() # 去掉剂量部分 # 匹配 "药材名(炮制)剂量" 或 "药材名 剂量" m = re.match(r'^([\u4e00-\u9fff\w]+)', p) if m: name = m.group(1).strip() if name and len(name) >= 2: herbs.append(name) # 3. 从方义中提取 if not herbs and "方义" in formula: text = formula["方义"] # 使用最长前缀匹配提取药材名 herbs = longest_prefix_match(text) # 去重 seen = set() unique_herbs = [] for h in herbs: if h not in seen: seen.add(h) unique_herbs.append(h) return unique_herbs # ============================================================ # 4. 评分函数 # ============================================================ def score_formula(formula, keywords): """ 对方剂进行体质关键字评分 名称 +5, 方义/简介 +3, 歌诀/出处 +2, 其他 +1 返回总分 """ score = 0.0 # 名称 name = formula.get("名称", "") for kw in keywords: if kw in name: score += 5.0 # 方义 + 简介 (+3) fangyi = formula.get("方义", "") intro = formula.get("简介", "") for field_text in [fangyi, intro]: if field_text: for kw in keywords: count = field_text.count(kw) score += count * 3.0 # 歌诀 + 出处 (+2) gejue = formula.get("歌诀", "") chuchu = formula.get("出处", "") for field_text in [gejue, chuchu]: if field_text: for kw in keywords: count = field_text.count(kw) score += count * 2.0 # 其他字段 (+1) other_fields = ["分类", "运用", "配伍特点", "加减化裁", "化裁方之间的鉴别", "重要文献摘要", "用法用量", "使用注意", "趣味记忆", "注意事项", "各家论述", "方解-功效", "方解-主治"] for field in other_fields: val = formula.get(field, "") if val: for kw in keywords: count = val.count(kw) score += count * 1.0 return round(score, 1) def score_herb(herb, keywords): """ 对药材进行体质关键字评分 名称 +5, 简介 +3, 功效作用 +3, 性味归经 +2, 临床应用 +2, 其他 +1 """ score = 0.0 # 名称 (+5) name = herb.get("名称", "") for kw in keywords: if kw in name: score += 5.0 # 简介 (+3) intro = herb.get("简介", "") if intro: for kw in keywords: count = intro.count(kw) score += count * 3.0 # 功效作用 (+3) - 嵌套对象 effect = herb.get("功效作用", {}) if isinstance(effect, dict): effect_text = " ".join(str(v) for v in effect.values()) elif isinstance(effect, str): effect_text = effect else: effect_text = "" if effect_text: for kw in keywords: count = effect_text.count(kw) score += count * 3.0 # 性味归经 (+2) xingwei = herb.get("性味归经", "") if xingwei: for kw in keywords: count = xingwei.count(kw) score += count * 2.0 # 临床应用 (+2) clinical = herb.get("临床应用", {}) if isinstance(clinical, dict): clinical_text = " ".join(str(v) for v in clinical.values()) elif isinstance(clinical, str): clinical_text = clinical else: clinical_text = "" if clinical_text: for kw in keywords: count = clinical_text.count(kw) score += count * 2.0 # 其他字段 (+1) other_fields = ["别名", "中文名称", "拉丁文名", "道地产区", "加工炮制", "药材鉴别", "保存方法", "植物学信息", "动物学信息", "矿物学信息", "毒性", "相关方剂", "医保类型"] for field in other_fields: val = herb.get(field, "") if isinstance(val, str) and val: for kw in keywords: count = val.count(kw) score += count * 1.0 elif isinstance(val, dict): val_text = " ".join(str(v) for v in val.values()) if val_text: for kw in keywords: count = val_text.count(kw) score += count * 1.0 return round(score, 1) def score_xuewei(xuewei, keywords): """ 对穴位进行体质关键字评分 名称 +5, 主治 +3, 详细主治 +3, 功能 +2, 其他 +1 """ score = 0.0 # 名称 (+5) name = xuewei.get("名称", "") for kw in keywords: if kw in name: score += 5.0 # 主治 (+3) zhuzhi = xuewei.get("主治", "") if zhuzhi: for kw in keywords: count = zhuzhi.count(kw) score += count * 3.0 # 详细主治 (+3) detail = xuewei.get("详细主治", "") if detail: for kw in keywords: count = detail.count(kw) score += count * 3.0 # 功能 (+2) func = xuewei.get("功能", "") or xuewei.get("功能作用", "") if func: for kw in keywords: count = func.count(kw) score += count * 2.0 # 其他字段 (+1) other_fields = ["简介", "隶属", "位置", "定位", "解剖", "详细操作", "临床运用", "主要配伍", "配伍", "附注", "相关论述", "出处", "功能作用"] for field in other_fields: val = xuewei.get(field, "") if isinstance(val, str) and val: for kw in keywords: count = val.count(kw) score += count * 1.0 return round(score, 1) # ============================================================ # 5. 主挖掘流程 # ============================================================ print("[4/6] 执行方剂/药材/穴位评分挖掘...") # 结果容器 result = { "体质-方剂": {}, "体质-药材": {}, "体质-穴位": {}, "体质-药材分类统计": {}, "体质-核心药对": {}, "体质-穴位归经统计": {}, "统计摘要": {} } # ---- 5A. 方剂挖掘 ---- print(" → 方剂评分...") for const_name in const_names: keywords = const_info[const_name]["keywords"] scored = [] for f in fangji_list: s = score_formula(f, keywords) if s >= 3.0: herbs = extract_herbs_from_formula(f) scored.append({ "名称": f.get("名称", ""), "得分": s, "药材": herbs }) # 排序取TOP20 scored.sort(key=lambda x: (-x["得分"], x["名称"])) top20 = scored[:20] result["体质-方剂"][const_name] = top20 print(f" {const_name}: {len(scored)} 个通过阈值, TOP20已选取") # ---- 5B. 药材挖掘 ---- print(" → 药材评分...") for const_name in const_names: keywords = const_info[const_name]["keywords"] scored = [] for h in yaocai_list: s = score_herb(h, keywords) if s >= 3.0: # 分类:使用 药材分类 字段 classification = h.get("药材分类", "其他") xingwei = h.get("性味归经", "") scored.append({ "名称": h.get("名称", ""), "得分": s, "分类": classification, "性味归经": xingwei }) scored.sort(key=lambda x: (-x["得分"], x["名称"])) top30 = scored[:30] result["体质-药材"][const_name] = top30 # 药材分类统计 class_counter = Counter() for item in top30: class_counter[item["分类"]] += 1 result["体质-药材分类统计"][const_name] = dict(class_counter.most_common()) # 核心药对 (TOP30药材两两组合) top_names = [item["名称"] for item in top30] pairs = [] for i in range(len(top_names)): for j in range(i+1, len(top_names)): a, b = top_names[i], top_names[j] # 统计共现次数 = 同时在多少方剂中出现 co_count = 0 for f in fangji_list: herbs = extract_herbs_from_formula(f) if a in herbs and b in herbs: co_count += 1 if co_count > 0: pairs.append([a, b, co_count]) # 按共现数降序排序 pairs.sort(key=lambda x: -x[2]) result["体质-核心药对"][const_name] = pairs[:30] # 取TOP30药对 print(f" {const_name}: {len(scored)} 个通过阈值, TOP30选取, {len(pairs)} 个药对") # ---- 5C. 穴位挖掘 ---- print(" → 穴位评分...") for const_name in const_names: keywords = const_info[const_name]["keywords"] scored = [] for x in xuewei_list: s = score_xuewei(x, keywords) if s >= 3.0: scored.append({ "名称": x.get("名称", ""), "得分": s, "隶属": x.get("隶属", ""), "主治": x.get("主治", "") }) scored.sort(key=lambda x: (-x["得分"], x["名称"])) top20 = scored[:20] result["体质-穴位"][const_name] = top20 # 穴位归经统计 meridian_counter = Counter() for item in top20: meridian_counter[item["隶属"]] += 1 result["体质-穴位归经统计"][const_name] = dict(meridian_counter.most_common()) print(f" {const_name}: {len(scored)} 个通过阈值, TOP20选取") # ---- 5D. 统计摘要 ---- print(" → 生成统计摘要...") summary = {} for const_name in const_names: summary[const_name] = { "方剂数": len(result["体质-方剂"].get(const_name, [])), "药材数": len(result["体质-药材"].get(const_name, [])), "穴位数": len(result["体质-穴位"].get(const_name, [])), "药材分类数": len(result["体质-药材分类统计"].get(const_name, {})), "核心药对数": len(result["体质-核心药对"].get(const_name, [])), "穴位归经数": len(result["体质-穴位归经统计"].get(const_name, {})) } result["统计摘要"] = summary # ============================================================ # 6. 输出 JSON # ============================================================ print("[5/6] 写入 JSON...") with open(OUT_JSON, "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) print(f" ✓ 已写入: {OUT_JSON}") # ============================================================ # 7. 输出 MD 报告 (追加方式) # ============================================================ print("[6/6] 写入 MD 报告...") def generate_md_section(const_name, top_formulas, top_herbs, herb_class_stats, core_pairs, top_xuewei, meridian_stats): """生成一种体质的MD报告节""" lines = [] lines.append(f"\n### {const_name}\n") # --- 方剂 --- lines.append("#### ① 体质-方剂 (TOP20)\n") lines.append("| 排名 | 方剂名称 | 匹配得分 | 所含药材 |") lines.append("|------|----------|----------|----------|") for i, item in enumerate(top_formulas, 1): herbs_str = "、".join(item["药材"][:8]) if item["药材"] else "—" if len(item["药材"]) > 8: herbs_str += "…" lines.append(f"| {i} | {item['名称']} | {item['得分']} | {herbs_str} |") # --- 药材 --- lines.append("\n#### ② 体质-药材 (TOP30)\n") lines.append("| 排名 | 药材名称 | 匹配得分 | 药材分类 | 性味归经 |") lines.append("|------|----------|----------|----------|----------|") for i, item in enumerate(top_herbs, 1): lines.append(f"| {i} | {item['名称']} | {item['得分']} | {item['分类']} | {item['性味归经']} |") # 药材分类统计 lines.append("\n**药材分类统计**\n") lines.append("| 分类 | 数量 |") lines.append("|------|------|") for cls, cnt in herb_class_stats.items(): lines.append(f"| {cls} | {cnt} |") # --- 核心药对 --- lines.append("\n**核心药对 (TOP30)**\n") lines.append("| 排名 | 药对 | 共现次数 |") lines.append("|------|------|----------|") for i, pair in enumerate(core_pairs[:15], 1): # 显示前15 lines.append(f"| {i} | {pair[0]} + {pair[1]} | {pair[2]} |") # --- 穴位 --- lines.append("\n#### ③ 体质-穴位 (TOP20)\n") lines.append("| 排名 | 穴位名称 | 匹配得分 | 隶属经脉 | 主治 |") lines.append("|------|----------|----------|----------|------|") for i, item in enumerate(top_xuewei, 1): zhuzhi_short = item['主治'][:40] + "…" if len(item['主治']) > 40 else item['主治'] lines.append(f"| {i} | {item['名称']} | {item['得分']} | {item['隶属']} | {zhuzhi_short} |") # 穴位归经统计 lines.append("\n**穴位归经统计**\n") lines.append("| 经脉 | 数量 |") lines.append("|------|------|") for mer, cnt in meridian_stats.items(): lines.append(f"| {mer} | {cnt} |") lines.append("\n---\n") return "\n".join(lines) # 构造MD内容 md_sections = [] md_sections.append("# 体质数据挖掘报告\n") md_sections.append("## 一、疾病-症状关联分析(已有)\n\n*此部分为原有内容,此处省略* \n") md_sections.append("\n## 二、体质-方剂挖掘\n") for const_name in const_names: lines = [] top_formulas = result["体质-方剂"].get(const_name, []) lines.append(f"\n### {const_name}\n") lines.append("| 排名 | 方剂名称 | 匹配得分 | 所含药材 |") lines.append("|------|----------|----------|----------|") for i, item in enumerate(top_formulas, 1): herbs_str = "、".join(item["药材"][:8]) if item["药材"] else "—" if len(item["药材"]) > 8: herbs_str += "…" lines.append(f"| {i} | {item['名称']} | {item['得分']} | {herbs_str} |") md_sections.append("\n".join(lines)) md_sections.append("\n## 三、体质-药材挖掘\n") for const_name in const_names: lines = [] top_herbs = result["体质-药材"].get(const_name, []) herb_class_stats = result["体质-药材分类统计"].get(const_name, {}) core_pairs = result["体质-核心药对"].get(const_name, []) lines.append(f"\n### {const_name}\n") lines.append("**TOP30药材**\n") lines.append("| 排名 | 药材名称 | 匹配得分 | 药材分类 | 性味归经 |") lines.append("|------|----------|----------|----------|----------|") for i, item in enumerate(top_herbs, 1): lines.append(f"| {i} | {item['名称']} | {item['得分']} | {item['分类']} | {item['性味归经']} |") lines.append("\n**药材分类统计**\n") lines.append("| 分类 | 数量 |") lines.append("|------|------|") for cls, cnt in herb_class_stats.items(): lines.append(f"| {cls} | {cnt} |") lines.append("\n**核心药对 (TOP15)**\n") lines.append("| 排名 | 药对 | 共现次数 |") lines.append("|------|------|----------|") for i, pair in enumerate(core_pairs[:15], 1): lines.append(f"| {i} | {pair[0]} + {pair[1]} | {pair[2]} |") md_sections.append("\n".join(lines)) md_sections.append("\n## 四、体质-穴位挖掘\n") for const_name in const_names: lines = [] top_xuewei = result["体质-穴位"].get(const_name, []) meridian_stats = result["体质-穴位归经统计"].get(const_name, {}) lines.append(f"\n### {const_name}\n") lines.append("**TOP20穴位**\n") lines.append("| 排名 | 穴位名称 | 匹配得分 | 隶属经脉 | 主治 |") lines.append("|------|----------|----------|----------|------|") for i, item in enumerate(top_xuewei, 1): zhuzhi_short = item['主治'][:40] + "…" if len(item['主治']) > 40 else item['主治'] lines.append(f"| {i} | {item['名称']} | {item['得分']} | {item['隶属']} | {zhuzhi_short} |") lines.append("\n**穴位归经统计**\n") lines.append("| 经脉 | 数量 |") lines.append("|------|------|") for mer, cnt in meridian_stats.items(): lines.append(f"| {mer} | {cnt} |") md_sections.append("\n".join(lines)) # 写入MD with open(OUT_MD, "w", encoding="utf-8") as f: f.write("\n".join(md_sections)) print(f" ✓ 已写入: {OUT_MD}") # ============================================================ # 8. 验证 # ============================================================ print("\n" + "="*60) print("验证结果") print("="*60) # 验证JSON with open(OUT_JSON, "r", encoding="utf-8") as f: v = json.load(f) print(f"\n✅ JSON 验证通过") print(f" - 体质-方剂: {len(v['体质-方剂'])} 种体质") print(f" - 体质-药材: {len(v['体质-药材'])} 种体质") print(f" - 体质-穴位: {len(v['体质-穴位'])} 种体质") print(f" - 体质-药材分类统计: {len(v['体质-药材分类统计'])} 种体质") print(f" - 体质-核心药对: {len(v['体质-核心药对'])} 种体质") print(f" - 体质-穴位归经统计: {len(v['体质-穴位归经统计'])} 种体质") # 检查每种体质都有数据 for cn in const_names: f_count = len(v["体质-方剂"].get(cn, [])) h_count = len(v["体质-药材"].get(cn, [])) x_count = len(v["体质-穴位"].get(cn, [])) print(f" {cn}: 方剂={f_count}, 药材={h_count}, 穴位={x_count}") # 验证MD md_size = os.path.getsize(OUT_MD) print(f"\n✅ MD 验证通过 ({md_size} bytes)") print(f"\n输出文件:") print(f" 1. {OUT_JSON}") print(f" 2. {OUT_MD}") print("\n完成!")