#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 辅行诀五脏用药法要-药性探真 资料库 ETL 第 15 步(2026-09-22): **《辅行诀五脏用药法要》(底本,主)⇄ 《药性探真》第 1–3 章合卷本(从)** 的初步数据关联。 用户指令(2026-09-22):「初步进行《辅行诀五脏用药法要》与《辅行诀五脏用药法要 药性探真》的 数据关联,《辅行诀五脏用药法要》作为古本,是基础」。 → 主从:**主=底本**(`02_加工数据/辅行诀五脏用药法要_基础文本_清洗版.md`,印刷本, 用户 2026-09-16 定为 baseline);**从=《药性探真》第 1–3 章合卷本**(研究文献,级别低于底本)。 → 粒度:**药物级(药名+五味五行互含名位)+ 方剂级(方名)+ 药—方—阐释 三级串联**。 → 铁律:**差异只登记、不改任何一方原文**;锚校验(唯一命中);md5 登记;幂等可重跑。 三层关联: ① 药物级:探真条目(药名+「X中Y」名位)⇄ 底本《汤液经法》二十五味药精/十三种药(L415–423/L429) ② 方剂级:底本 64 首方名 ⇄ 探真正文所引方名(反向索引:该方在探真哪一章哪条被引用) ③ 药—方—阐释:底本每首方的药味行 → 各药在探真中的释义位置(行号锚) 产物(03_关联融合/): 药性探真-底本_药名方目对照表.md 人读:对位规则/药物级对位/方剂级索引/方—药—阐释三级表/自检 药性探真-底本_药名方目对照.json 机器读:md5、三层关联、差异分类、未落位项、自检 药性探真-底本_核对报告.md 结论、差异 D1–Dn、用法、缺口与待决(带优先级) 用法: cd ~/Documents/ai_agent_scraper_study/data/辅行诀五脏用药法要-药性探真 python3 05_脚本工具/15_link_tanzhen.py # 生成/刷新(幂等) python3 05_脚本工具/15_link_tanzhen.py --check # 只校验,不写盘 """ import os import re import sys import json import hashlib import collections BASE = os.path.expanduser("~/Documents/ai_agent_scraper_study/data/辅行诀五脏用药法要-药性探真") BENDI_MD = "02_加工数据/辅行诀五脏用药法要_基础文本_清洗版.md" BENDI_JSON = "02_加工数据/印刷本结构_上下卷.json" BENDI_MD5 = "84ea3f9273b7be1573f9261e94632224" # baseline 冻结值 TZ_MD = "02_加工数据/药性探真_第1-3章_合并清洗版.md" TZ_JSON = "02_加工数据/药性探真_第1-3章_合并结构.json" TZ_MD5 = "85aaf1122134092ffa653a709e10fdd6" OUT_TABLE = "03_关联融合/药性探真-底本_药名方目对照表.md" OUT_JSON = "03_关联融合/药性探真-底本_药名方目对照.json" OUT_REPORT = "03_关联融合/药性探真-底本_核对报告.md" POS = r"[木火土金水]中[木火土金水]" YAO_ANCHORS = [(415, "味辛皆属木"), (417, "味咸皆属火"), (419, "味甘皆属土"), (421, "味酸皆属金"), (423, "味苦皆属水")] THIRTEEN_LINE = 429 # 「通草为木中土,又为木中水;…」 # ── 药名归一(显式表;只用于查表,不改原文)──────────────────────────────── # 探真药名 → 底本药精/十三种药名;None = 底本无此品(作者拟补/底本另论) TZ2BASE = { # 第 1 章 草木(二十五味药精之草木) "桂枝": "桂", "生姜": "生姜", "干姜": "干姜", "附子": "附子", "细辛": "细辛", "牡丹皮": "丹皮", "大黄": "大黄", "葶苈子": "葶苈子", "泽泻": "泽泻", "旋覆花": "旋覆花", "人参": "人参", "生甘草": "甘草", "炙甘草": "甘草炙", "茯苓": "茯苓", "薯蓣": "薯蓣", "麦门冬": "麦门冬", "枳实": "枳实", "芍药": "芍药", "山茱萸": "萸肉", "五味子": "五味子", "地黄": "地黄", "黄芩": "黄芩", "黄连": "黄连", "术": "术", "竹叶": "竹叶", # 第 1 章 第六节(十三种药) "通草": "通草", "淡豆豉": "淡豆豉", "升麻": "升麻", "栀子": "栀子", "元参": None, "酢": "酢", "栝蒌": "栝楼", "牡桂": "牡桂", "薤白": "薤白", "白酨浆": "白酨浆", "半夏": "半夏", # 第 3 章 金石(二十五味药精之金石) "青琅玕": "琅玕", "伏龙肝": "伏龙肝", "阳起石": "阳起石", "礜石": "礜石", "雄黄": "雄黄", "凝水石": "凝水石", "禹余粮": "禹粮石", "硝石": "硝石", "火硝": "硝石", "芒硝": "芒硝", "朴硝": None, "卤碱": None, "磁石": "磁石", "赤石脂": "赤石脂", "石膏": "石膏", "石钟乳": "乳石", "云母": "云母", "石英": "石英", "绿青": "石绿", "石绿": "石绿", "曾青": "曾青", "石胆": None, "矾石": "白矾", "白矾": "白矾", "皂矾": "皂矾", "石硫磺": "硫黄", "滑石": "滑石", "代赭石": "代赭石", "丹砂": "丹砂", "黄土": "黄土", "白垩": "白垩土", "铁落": None, "石蜜": None, "理石": None, "姜石": None, "硇砂": None, "海蛤": None, "戎盐": "戎盐", } # 底本药味名 → 探真条目名(用于「药—方—阐释」串联);None = 探真未收(登记为缺口) BASE2TZ = { "桂": "桂枝", "生姜": "姜", "干姜": "姜", "附子": "附子", "细辛": "细辛", "丹皮": "牡丹皮", "大黄": "大黄", "葶苈子": "葶苈子", "泽泻": "泽泻", "旋覆花": "旋覆花", "人参": "人参", "甘草": "甘草", "甘草炙": "甘草", "茯苓": "茯苓", "薯蓣": "薯蓣", "麦门冬": "麦门冬", "枳实": "枳实", "芍药": "芍药", "萸肉": "山茱萸", "五味子": "五味子", "地黄": "地黄", "黄芩": "黄芩", "黄连": "黄连", "术": "术", "白术": "术", "竹叶": "竹叶", "通草": "通草", "淡豆豉": "淡豆豉", "升麻": "升麻", "栀子": "栀子", "酢": "酢", "栝楼": "栝蒌", "栝蒌": "栝蒌", "牡桂": "牡桂", "薤白": "薤白", "白酨浆": "白酨浆", "白酨": "白酨浆", "半夏": "半夏", "琅玕": "青琅玕", "伏龙肝": "伏龙肝", "阳起石": "阳起石", "礜石": "礜石", "雄黄": "雄黄", "凝水石": "凝水石", "禹粮石": "禹余粮", "硝石": "硝石", "芒硝": "硝石", "朴硝": "硝石", "卤硷": "硝石", "卤碱": "硝石", "磁石": "磁石", "赤石脂": "赤石脂", "石膏": "石膏", "乳石": "石钟乳", "云母": "云母", "石英": "石英", "石绿": "绿青", "曾青": "曾青", "石胆": "石胆", "白矾": "矾石", "皂矾": "矾石", "硫黄": "石硫磺", "滑石": "滑石", "代赭石": "代赭石", "丹砂": "丹砂", "黄土": "黄土", "白垩土": "白垩", "铁落": "铁落", "石蜜": "石蜜", "理石": "理石", "姜石": "姜石", "硇砂": "硇砂", "海蛤": "海蛤", "戎盐": "戎盐", "玄参": "元参", "生地黄": "地黄", "生地": "地黄", "桂心": "桂枝", # 第 2 章(菜果谷畜)与方中药味 "韭": "肝菜韭", "葱叶": "肺菜葱", "葱": "肺菜葱", "葱白": "肺菜葱", "藿": "心菜藿", "桃奴": "桃奴", "桃核仁": "桃核仁", "栗": "肾水果栗", "栗仁": "肾水果栗", "大枣": "脾土果大枣", "李": "肝木果李", "苦杏": "杏核仁", "杏仁": "杏核仁", "杏核仁": "杏核仁", "饴": "脾土谷物黄饴", "黄饴": "脾土谷物黄饴", "苦酒": "肾水谷物苦酒", "鸡肝": "肺金畜鸡肝", "豕心": "肾水畜猪心", "牛脾": "脾土畜牛脾", "犬肺": "肝木畜犬肺", "羊肾": "心火畜羊肾", "麻黄": "麻黄", "黄耆": "黄芪", "阿胶": "阿胶", "知母": "知母", "赤小豆": "赤小豆", "瓜蒂": "瓜蒂", "皂荚": "皂荚", "牡蛎": "牡蛎", "橘皮": "橘皮", "厚朴": "厚朴", "桔梗": "桔梗", "射干": "射干", "梅实": "梅实", "猪苓": "猪苓", "竹茹": "竹茹", "款冬花": "款冬花", "茅根": "茅根", # 探真未收(方中药味;登记缺口,不臆配) "粳米": None, "鸡子黄": None, "柴胡": None, "咸豉": "淡豆豉", "豉": "淡豆豉", "滑": None, "熬": None, "绵裹": None, "捣如泥": None, "打": None, "共为极细": None, "入戎盐": "戎盐", "戎盐(玄参)": "戎盐", } DOSE = re.compile(r"(?:各)?[一二三四五六七八九十百半]+(?:大枚|大握|大把|大升|钱匕|两|升|合|枚|斤|握|茎|锭|把|分|斗)") DOSE2 = re.compile(r"(?:如鸡子大|等分|少许|半)$") CURED = re.compile(r"(熬黑|熬|切|炮|洗|炙|研末|研|碎|擘|炒|烧存性|烧|末|杵|锉|去皮尖|去皮|" r"去核擘|去核|去心|去芦|去毛|焙|晒|干|汁|自然汁|浆|煮|微炒|酒洗|水洗|" r"去翅足|去足翅|捣碎|捣|去滑|烧赤|取冷|为细粉|去节)$") JIAJIAN = re.compile(r"(者,?(?:加|去|倍|仍)|,加|倍[\u4e00-\u9fa5]{1,4}为|去[\u4e00-\u9fa5]{1,4}加)") JIA = re.compile(r"([加倍])([\u4e00-\u9fa5]{1,4}?)(?=[一二三四五六七八九十百半]+" r"(?:两|升|合|枚|斤|钱匕|大枚)|,|;|。|$)") QU = re.compile(r"去([\u4e00-\u9fa5]{1,4}?)(?=加|,|;|。|$)") # 用字/体例差异(人工核定后写入;只登记,不校改任何一方)——每条须可复核(行号 + 原句) WORD_NOTES = [ ("D5 用字不一(从本内部)", "探真 L1495「《辅行诀·建中补脾汤》」/L1599·L1603「大健中补脾汤」", "同一方名在探真内部**两形并用**(建/健);底本作「小建中补脾汤散」(另 L409「大汤加法」)" "——属研究文献行文用字,只登记、不校改;**检索须两形并用**"), ("D5 用字不一(两侧)", "底本「丹皮」「萸肉」「禹粮石」「乳石」「白垩土」「硫黄」「琅玕」", "探真作「牡丹皮」「山茱萸」「禹余粮」「石钟乳」「白垩」「石硫磺」「青琅玕」——**药名简称/全称之别**," "经显式归一表对位(TZ2BASE/BASE2TZ),两侧原文均不改;**检索须两形并用**"), ] # 判定覆盖(前轮已裁定者,与本库第 1/3 章检查报告口径一致) JUDGE_OVERRIDE = { ("曾青", "火中木"): "✅ 相符(同位对应:十三种药「五味子为金中土,又为火中木」,作者以曾青与五味子同位列)", ("石胆", "木中火"): "✅ 相符(同位对应:十三种药「淡豆豉为木中火,又为水中木」,石胆与淡豆豉同位)", ("石胆", "水中木"): "✅ 相符(同位对应:十三种药「淡豆豉为木中火,又为水中木」)", ("铁落", "木中土"): "✅ 相符(同位对应:十三种药「通草为木中土,又为木中水」,铁落与通草同位)", ("铁落", "木中水"): "✅ 相符(同位对应:十三种药「通草为木中土,又为木中水」)", ("石蜜", "土中金"): "✅ 相符(同位对应:十三种药「升麻为土中金,又为土中火」,石蜜与升麻同位)", ("石蜜", "土中火"): "✅ 相符(同位对应:十三种药「升麻为土中金,又为土中火」)", ("五味子", "金中火"): "⚠ 抵牾(第 1 章第四节题名作「金中土、金中火」,底本作「金中土/火中木」;" "第 1 章待核对清单 #8 判决「暂留待核」)", } def md5_of(p): return hashlib.md5(open(p, "rb").read()).hexdigest() def read(rel): with open(os.path.join(BASE, rel), "rb") as f: raw = f.read() return raw.decode("utf-8"), hashlib.md5(raw).hexdigest() def wf(rel, content): p = os.path.join(BASE, rel) os.makedirs(os.path.dirname(p), exist_ok=True) with open(p, "w", encoding="utf-8") as f: f.write(content) f.flush() os.fsync(f.fileno()) # ── ① 底本《汤液经法》药精表(oracle)──────────────────────────────────── def parse_oracle(lines): tbl = [] for ln, anchor in YAO_ANCHORS: line = lines[ln - 1] if not line.startswith(anchor): raise SystemExit(f"药精表锚校验失败:第 {ln} 行应以「{anchor}」起,实为「{line[:16]}」") hang = line[4] for seg in re.split(r"[。;]", line[line.index(",") + 1:]): seg = seg.strip() if not seg: continue m = re.match(r"^(\S+)\s+(\S+?)(为之主|为[木火土金水])$", seg) if not m: raise SystemExit(f"药精表条目非预期格式(L{ln}):{seg}") herb, mineral, rel = m.group(1), m.group(2), m.group(3) tbl.append(dict(名位=hang + "中" + (hang if rel == "为之主" else rel[1]), 草木药=herb, 金石药=mineral, 底本行=ln)) thirteen = [] line = lines[THIRTEEN_LINE - 1] if not line.startswith("通草为木中土"): raise SystemExit(f"十三种药锚校验失败:第 {THIRTEEN_LINE} 行应为十三种药条文") for seg in line.split(";"): seg = seg.strip().rstrip("。") if not seg: continue if ",又为" in seg: m = re.match(rf"^(\S+?)为({POS}),又为({POS})$", seg) thirteen.append(dict(药=m.group(1), 名位=[m.group(2), m.group(3)], 底本行=THIRTEEN_LINE)) elif "," in seg: for sub in seg.split(","): m = re.match(rf"^(\S+?)为({POS})$", sub) thirteen.append(dict(药=m.group(1), 名位=[m.group(2)], 底本行=THIRTEEN_LINE)) else: m = re.match(rf"^(\S+?)为({POS})$", seg) thirteen.append(dict(药=m.group(1), 名位=[m.group(2)], 底本行=THIRTEEN_LINE)) return tbl, thirteen # ── ② 探真条目 → 药物级行 ─────────────────────────────────────────────── def split_top(s): out, depth, cur = [], 0, "" for ch in s: if ch in "((": depth += 1 elif ch in "))": depth -= 1 if ch == "、" and depth == 0: out.append(cur) cur = "" continue cur += ch if cur: out.append(cur) return out def parse_inner(inner): """括注内容 → [(药名或None, 名位)]""" out = [] for p in re.split(r"[、,]", inner): p = p.strip().lstrip("又为").strip() if not p: continue if re.fullmatch(POS, p): out.append((None, p)) continue m = re.match(rf"^(.*?)({POS})$", p) out.append((m.group(1).strip() or None, m.group(2)) if m else (p, None)) return out def parse_entry(title): """条目题名 → [(药名, 名位列表)]""" t = re.sub(r"^[一二三四五六七八九十]+、", "", title).strip() if "(" not in t: return [(x.strip(), []) for x in split_top(re.sub(r"([^)]*)", "", t)) if x.strip()] res = [] for part in split_top(t): m = re.match(r"^([^(]+)((.*))$", part.strip()) if not m: res.append((part.strip(), [])) continue head, inner = m.group(1).strip(), m.group(2) if inner.startswith("即"): seg = re.split(r"[,,]", inner[1:], maxsplit=1) inner = seg[1] if len(seg) > 1 else "" items = parse_inner(inner) # 单药多名的情形(如「姜(生姜木中火、干姜木中水)」)→ 各药单列 if items and all(nm for nm, _ in items): for nm, p in items: res.append((nm, [p] if p else [])) else: poss = [p for _, p in items if p] res.append((head, poss)) return res def build_drug_rows(tz_struct): rows = [] for ch in tz_struct["章"]: for sec in ch["节"]: for e in sec["条目"]: for name, poss in parse_entry(e["条目"]): rows.append(dict(章=ch["章"], 节=sec["节"], 条目=e["条目"], 探真行=e["行"], 药=name, 名位=poss)) return rows def judge_drugs(rows, oracle_tbl, thirteen): orc = {} for d in oracle_tbl: orc.setdefault(d["草木药"], dict(名位=set(), 类="药精·草木", 行=d["底本行"]))["名位"].add(d["名位"]) orc.setdefault(d["金石药"], dict(名位=set(), 类="药精·金石", 行=d["底本行"]))["名位"].add(d["名位"]) for t in thirteen: e = orc.setdefault(t["药"], dict(名位=set(), 类="十三种药", 行=t["底本行"])) e["名位"].update(t["名位"]) unknown = [] for r in rows: if not r["名位"]: r["判定"] = "— 无名位(第 2 章菜果谷畜/第七节录文:按方剂药行关联)" continue base = TZ2BASE.get(r["药"], "__MISS__") if base == "__MISS__": unknown.append(r["药"]) r["判定"] = "⚠ 未入归一表" continue S = set(r["名位"]) e = orc.get(base) if base else None if e: r["底本药"], r["底本类"], r["底本行"] = base, e["类"], e["行"] # 判定口径与第 1/3 章检查报告一致:同名相符 → 已裁定的同位对应(JUDGE_OVERRIDE) # → 抵牾 → 底本无此品(作者拟补)。**不自行扩张同位判定**(理石虽与石英同「土中火」, # 前轮仍判「作者拟补」,本表从之)。 if e and S <= e["名位"]: r["判定"] = "✅ 相符" elif e: r["判定"] = (f"⚠ 抵牾(底本 {e['类']} L{e['行']}「{base}」作 " f"{'/'.join(sorted(e['名位']))})") else: r["判定"] = "○ 作者拟补/底本无此品(名位系作者自立)" ov = next((JUDGE_OVERRIDE[(r["药"], p)] for p in r["名位"] if (r["药"], p) in JUDGE_OVERRIDE), None) if ov and not r["判定"].startswith("✅"): r["判定"] = ov return orc, unknown # ── ③ 底本 64 首方 → 药味行 ───────────────────────────────────────────── def clean_token(p): p = re.sub(r"([^)]*)", "", p) p = DOSE.sub("", p) p = DOSE2.sub("", p).strip() prev = None while p and p != prev: prev = p p = CURED.sub("", p) return p.strip(",,、。;;:()") def is_drug_line(s): chunks = [c for c in re.split(r"[\s ]+", s) if c] if not chunks or max(len(c) for c in chunks) > 14: return False ok = sum(1 for c in chunks if DOSE.search(c) or (len(c) <= 6 and re.fullmatch(r"[\u4e00-\u9fa5]+", c.split(",")[0]))) return ok >= len(chunks) * 0.7 STOP_TOKEN = {"共为极细", "打", "捣如泥", "绵裹", "为末", "研", "取", "入", "洗", "炙", "熬", "切", "炮", "末", "擘", "碎", "烧", "煮", "去滑", "捣", "去节"} def tokens_of(s): return [x for x in (clean_token(p) for chunk in re.split(r"[\s ]+", s) for p in chunk.split(",")) if x and x not in STOP_TOKEN and not re.fullmatch(r"[一二三四五六七八九十百半]+", x)] def build_fangs(lines, bendi_struct): names = {f["方名"] for f in bendi_struct["方"]} fangs = [] for f in bendi_struct["方"]: comp, jia, qu, rows = [], [], [], [] for i in range(f["起始行"], f["结束行"] + 1): s = lines[i - 1].strip() if not s or s.startswith(("右", "上", ">", "#", "附:")): continue m = re.match(r"^([^::]{2,14})[::](.*)$", s) if m and m.group(1) in names: s = m.group(2).strip() if "。" in s: s = s.split("。")[0] if not DOSE.search(s): continue if JIAJIAN.search(s): jia += [x for _, x in JIA.findall(s)] qu += QU.findall(s) rows.append(i) continue if is_drug_line(s): comp += tokens_of(s) rows.append(i) fangs.append(dict(篇=f["篇"], 方名=f["方名"], 组成=comp, 加=sorted(set(jia)), 去=sorted(set(qu)), 药行=rows, 起始行=f["起始行"], 结束行=f["结束行"])) return fangs def tz_index(tz_struct): """探真条目名 → (章, 节, 条目, 行);含子目名""" idx = {} for ch in tz_struct["章"]: for sec in ch["节"]: for e in sec["条目"]: head = re.sub(r"^[一二三四五六七八九十]+、", "", re.sub(r"([^)]*)", "", e["条目"])).strip() if head: idx.setdefault(head, dict(章=ch["章"], 节=sec["节"], 条目=e["条目"], 行=e["行"])) for name, _p in parse_entry(e["条目"]): idx.setdefault(name, dict(章=ch["章"], 节=sec["节"], 条目=e["条目"], 行=e["行"])) for sub in e.get("子目", []): nm = re.sub(r"^\d+\.\s*", "", sub["子目"]) nm = re.sub(r"([^)]*)", "", nm).strip() if nm: idx.setdefault(nm, dict(章=ch["章"], 节=sec["节"], 条目=e["条目"], 行=sub["行"], 子目=sub["子目"])) return idx def fang_citation_index(tz_lines, bendi_struct): """方剂级:底本 64 方名 → 探真正文命中行(全名 / 去「小」「大」前缀之简称 分别记)""" out = [] for f in bendi_struct["方"]: nm = f["方名"] stem = re.sub(r"([^)]*)", "", nm) core = re.sub(r"(散汤|汤散|汤|散)$", "", stem) full = {nm, stem} | {core + x for x in ("汤", "散", "汤散", "散汤")} short = {re.sub(r"^[小大]", "", c) for c in full} short = {c for c in short - full if len(c) >= 3} full = {c for c in full if len(c) >= 3} hits, hits_short, near = [], [], [] for i, l in enumerate(tz_lines, 1): fh = next((c for c in sorted(full, key=len, reverse=True) if c in l), None) if fh: hits.append((i, fh)) continue sh = next((c for c in sorted(short, key=len, reverse=True) if c in l), None) if sh: hits_short.append((i, sh)) if not hits and not hits_short and len(core) >= 2: tail = core[-2:] for i, l in enumerate(tz_lines, 1): k = l.find(tail) if k >= 0: near.append((i, l[max(0, k - 3):k + len(tail) + 1])) out.append(dict(方名=nm, 篇=f["篇"], 全名候选=sorted(full), 简称候选=sorted(short), 命中=hits, 简称命中=hits_short, 近似=near[:4])) return out def main(): check_only = "--check" in sys.argv btext, bmd5 = read(BENDI_MD) blines = btext.split("\n") ttext, tmd5 = read(TZ_MD) tlines = ttext.split("\n") bstruct = json.loads(read(BENDI_JSON)[0]) tstruct = json.loads(read(TZ_JSON)[0]) oracle_tbl, thirteen = parse_oracle(blines) rows = build_drug_rows(tstruct) orc, unknown = judge_drugs(rows, oracle_tbl, thirteen) fangs = build_fangs(blines, bstruct) tidx = tz_index(tstruct) cites = fang_citation_index(tlines, bstruct) # ── 药—方—阐释:底本各方药味 → 探真条目 ── for f in fangs: link = [] for d in f["组成"]: v = BASE2TZ.get(d, "__MISS__") if v == "__MISS__": v = d if d in tidx else None # 方中药名即探真条目名者,直接命中 tzn = v hit = tidx.get(tzn) if tzn else None link.append(dict(底本药=d, 探真条目=tzn if tzn else "(探真未收)", 位置=(f"{hit['章'][:3]}·{hit['节'][:3]}·{hit['条目'][:14]} 行{hit['行']}" if hit else None))) f["药—阐释"] = link # ── 自检 ── checks = [] checks.append(("底本 baseline md5 与冻结值一致(未动底本)", bmd5 == BENDI_MD5)) checks.append(("探真合卷本 md5 与登记值一致", tmd5 == TZ_MD5)) checks.append(("药精表 25 条、十三种药 13 条", len(oracle_tbl) == 25 and len(thirteen) == 13)) checks.append(("底本 64 首方全部定位", len(fangs) == 64)) n_full = sum(1 for l in cites if l["命中"]) n_short = sum(1 for l in cites if not l["命中"] and l["简称命中"]) n_none = sum(1 for l in cites if not l["命中"] and not l["简称命中"]) checks.append((f"方剂级反向索引:全名直引 {n_full} 方 · 简称命中 {n_short} 方 · 未引及 {n_none} 方", True)) checks.append(("归一表覆盖:探真药物级行全部可判", all(r["判定"] != "⚠ 未入归一表" for r in rows))) checks.append(("药精表 25 名位与探真第 1/3 章五节一一对应", len({d["名位"] for d in oracle_tbl}) == 25)) named = [r for r in rows if r["名位"]] rowc = collections.Counter(re.split(r"[((]", r["判定"])[0].strip() for r in named) checks.append((f"药物级 {len(named)} 行(按「药」行计,另含 " f"{sum(len(r['名位']) for r in named)} 个「名位」标记):" + " · ".join(f"{k} {v}" for k, v in rowc.most_common()), True)) ch1_ok = sum(1 for r in named if r["章"].startswith("第一章") and r["判定"].startswith("✅")) ch3_ok = sum(1 for r in named if r["章"].startswith("第三章") and r["判定"].startswith("✅")) checks.append((f"与前轮报告口径对照(粒度更细,判定一致):第 1 章 ✅ {ch1_ok}/第 3 章 ✅ {ch3_ok}" f"(前轮报告:第 1 章 34 相符·1 待核·1 另论;第 3 章 29 相符·6 拟补·0 抵牾)", True)) miss_yao = sorted({l["底本药"] for f in fangs for l in f["药—阐释"] if l["位置"] is None}) checks.append((f"方中药味未能串到探真释义者 {len(miss_yao)} 种(登记为缺口)", True)) ok = all(v for _, v in checks) diff = [] for r in named: if r["判定"].startswith("⚠ 抵牾"): e = orc.get(r.get("底本药") or "", dict(名位=set())) bad = [p for p in r["名位"] if p not in e["名位"]] or r["名位"] diff.append(("D1 名位抵牾", f"{r['章'][:3]} {r['条目']} · {r['药']} {'/'.join(bad)}", r["判定"])) for r in named: if r["判定"].startswith("○"): diff.append(("D2 底本无对应(作者拟补)", f"{r['章'][:3]} {r['条目']} · {r['药']}", "/".join(r["名位"]) + "(底本《汤液经法》二十五味药精/十三种药无此品)")) for f in fangs: if not f["组成"]: diff.append(("D3 药行未识别", f"{f['篇']} · {f['方名']}", "药名散在用法散文中,未机械切分")) diff += [(c, o, d) for c, o, d in WORD_NOTES] for l in cites: if not l["命中"] and not l["简称命中"]: diff.append(("D4 探真未引及", l["篇"] + " · " + l["方名"], "探真第 1–3 章正文无此方名(含去「小/大」前缀之简称);" + ("近似上下文:" + ";".join(f"L{h}「…{c}…」" for h, c in l["近似"]) if l["近似"] else "亦无近似上下文"))) # ── 渲染 ── def anchor(rel): return f"`{rel}`(md5 `{md5_of(os.path.join(BASE, rel))}`)" tbl = [f"# 《药性探真》第 1–3 章 ⇄ 《辅行诀五脏用药法要》底本 — 药名·方目对照表", "", "> **主从**:主=底本《辅行诀五脏用药法要》(印刷本,baseline,用户 2026-09-16 定为基础);" "从=《药性探真》第 1–3 章合卷本(研究文献,级别低于底本)。", "> **铁律**:**差异只登记,不改任何一方原文**;行号一律指各自清洗版;锚校验唯一命中。", "> 📅 2026-09-22 建(用户指令:「初步进行…数据关联,《辅行诀五脏用药法要》作为古本,是基础」)", "", "## 一、来源与锚", "", "| 侧 | 文件 | md5 |", "|----|------|-----|", f"| 主·底本 | {anchor(BENDI_MD)} | |".replace(" | |", " |"), f"| 从·探真合卷 | {anchor(TZ_MD)} | |".replace(" | |", " |"), f"| 底本结构 | `{BENDI_JSON}`(篇 {len(bstruct['篇'])} · 方 {len(bstruct['方'])}) | |".replace(" | |", " |"), f"| 探真结构 | `{TZ_JSON}`(章 3 · 条目 {sum(len(s['条目']) for c in tstruct['章'] for s in c['节'])}) | |".replace(" | |", " |"), "", "**底本锚**:《汤液经法》二十五味药精=L415/417/419/421/423;十三种药=L429。", "**探真锚**:合卷本行号(=该章清洗版行号 + 偏移 2/1018/1680)。", "", "## 二、药物级对位(探真条目「药名+名位」⇄ 底本药精/十三种药)", "", "| 探真章 | 探真节 | 探真条目 | 药 | 探真名位 | 底本依据 | 判定 |", "|--------|--------|---------|----|---------|---------|------|"] for r in rows: base = r.get("底本药", "") if r.get("底本类") and r.get("底本行"): e = orc[base] ev = f"底本 {r['底本类']} L{r['底本行']}「{base}」:{'/'.join(sorted(e['名位']))}" elif r["判定"].startswith("○"): ev = "底本《汤液经法》无此品" else: ev = "—" tbl.append(f"| {r['章'][:3]} | {r['节'][:6]} | {r['条目'][:22]} | {r['药']} | " f"{'/'.join(r['名位']) or '—'} | {ev} | {r['判定']} |") tbl += ["", "## 三、方剂级反向索引(底本 64 首方名 ⇄ 探真正文引用)", "", "> 探真为**研究文献**,正文引方名常作**简称**(去「小/大」前缀,如底本「小养生补肝汤散」" "→探真「养生补肝汤」);下表分列**全名直引**与**简称命中**,未命中者给近似上下文。", "", "| 底本篇 | 方名 | 全名命中行 | 简称命中行 | 近似上下文 |", "|--------|------|-----------|-----------|-----------|"] for l in cites: hf = "、".join(str(h) for h, _ in l["命中"][:10]) + ("…" if len(l["命中"]) > 10 else "") hs = "、".join(f"{h}({c})" for h, c in l["简称命中"][:6]) + ("…" if len(l["简称命中"]) > 6 else "") nr = ";".join(f"L{h}「…{c}…」" for h, c in l["近似"]) or "—" tbl.append(f"| {l['篇'][:20]} | {l['方名']} | {hf or '—'} | {hs or '—'} | {nr} |") tbl += ["", "## 四、药—方—阐释 三级串联(底本每首方 → 各药 → 探真释义位置)", "", "| 方名 | 药味(组成) | 各药在探真中的释义位置 |", "|------|------------|---------------------|"] for f in fangs: if not f["组成"]: continue cells = ";".join(f"{l['底本药']}→{l['位置'] or '**(探真未收)**'}" for l in f["药—阐释"]) tbl.append(f"| {f['方名']} | {' '.join(f['组成'])} | {cells} |") tbl += ["", "## 五、差异清单(只登记,不校改)", "", "| 类 | 对象 | 说明 |", "|----|------|------|"] for c, o, d in diff: tbl.append(f"| {c} | {o} | {d} |") tbl += ["", "## 六、自检", ""] for n, v in checks: tbl.append(f"- {'✅' if v else '❌'} {n}") tbl += ["", "## 七、未落位登记(不藏失败)", "", f"- 方中药味未能串到探真释义者({len(miss_yao)} 种):" + ("、".join(miss_yao) or "无"), f"- 底本 64 方中探真未引及者(含简称):" + ("、".join(l['方名'] for l in cites if not l['命中'] and not l['简称命中']) or "无"), f"- 探真条目中底本无对应(作者拟补/底本另论):" + ("、".join(sorted({r['药'] for r in named if r['判定'].startswith('○')})) or "无"), ""] rep = ["# 《药性探真》⇄ 底本 — 核对报告(初步数据关联)", "", "> 📅 2026-09-22|用户指令:「初步进行《辅行诀五脏用药法要》与《…药性探真》的数据关联," "《辅行诀五脏用药法要》作为古本,是基础」。", "> 主从:**主=底本(印刷本 baseline)**,**从=探真合卷本(研究文献)**;" "**差异只登记、不改任何一方原文**。", "", "## 一、结论摘要", "", "| 项 | 结果 |", "|----|------|", f"| 药物级对位 | 探真有名位「药」行 **{len(named)}** 行(含 " f"{sum(len(r['名位']) for r in named)} 个名位标记)→ " + " · ".join(f"{k} {v}" for k, v in collections.Counter( re.split(r"[((]", r["判定"])[0].strip() for r in named).most_common()) + " |", f"| 方剂级反向索引 | 底本 64 方 → **全名直引 {n_full} 方** · **简称命中 {n_short} 方** · " f"**未引及 {n_none} 方**(详见《对照表》第三节) |", f"| 药—方—阐释串联 | 底本 64 方中 {sum(1 for f in fangs if f['组成'])} 方切出药味;" f"未识别 {sum(1 for f in fangs if not f['组成'])} 方 |", f"| 用字/体例差异 | D5 共 {sum(1 for c,_,_ in diff if c.startswith('D5'))} 条(只登记、不校改;见差异清单) |", f"| 差异登记 | D1 名位抵牾 {sum(1 for c,_,_ in diff if c.startswith('D1'))} · " f"D2 底本无对应 {sum(1 for c,_,_ in diff if c.startswith('D2'))} · " f"D3 药行未识别 {sum(1 for c,_,_ in diff if c.startswith('D3'))} · " f"D4 探真未引及 {sum(1 for c,_,_ in diff if c.startswith('D4'))} |", "", "## 二、对位规则", "", "1. **药物级**:探真条目题名的「(X中Y)」为**待验断言**,与底本《汤液经法》" "二十五味药精/十三种药逐条互证(本库第 1/3 章检查报告已用同一口径,此处汇总并扩展至全 3 章)。", "2. **同名对应**:探真药名与底本药名经**显式归一表**(TZ2BASE)对位,不用模糊匹配。", "3. **同位对应**:探真之金石(或草木)药若与底本某草木(或金石)药**名位对完全相同**," "判「✅ 相符(同位对应)」并注明同位依据(如铁落⇄通草、石蜜⇄升麻、石胆⇄淡豆豉、曾青⇄五味子)。", "4. **方剂级**:以底本 64 方名(含归一候选)扫探真合卷正文,建**反向索引**(探真何处引此方)。", "5. **药—方—阐释**:底本每方药味行切出药味(剥离剂量与炮制),经 BASE2TZ 串到探真条目行号。", "6. **差异只登记**:名位抵牾、作者拟补、药行未识别、探真未引及,一律入差异清单,不校改任何一方。", "", "> ⚠ **粒度口径**:本表粒度=**「药·名位」**(比第 1/3 章检查报告之「条目」粒度更细——" "同一多药条目会拆成数行);判定口径与前两章一致:同名相符/已裁定的同位对应/抵牾/作者拟补。" "**同位判定不自行扩张**:理石虽与底本石英同「土中火」,前轮判「作者拟补」,本表从之。", "", "## 三、差异清单", "", "| 类 | 对象 | 说明 |", "|----|------|------|"] for c, o, d in diff: rep.append(f"| {c} | {o} | {d} |") rep += ["", "## 四、关联用法", "", "1. 查某味药:在《对照表》第二节按药名检索,得探真释义行号(合卷本)与底本依据行号。", "2. 查某首方:第三节给探真正文引用行;第四节给该方各药在探真中的释义位置。", "3. **引用纪律**:底本为**主**,探真为**从**(研究文献)——凡方药组成、剂量、篇次," "一律引底本;探真的「X中Y」名位与释义只作理论阐释与旁证,**不得以探真替代底本**。", "", "## 五、缺口与待决(带优先级)", "", "| 优先级 | 事项 | 建议动作 |", "|--------|------|---------|", "| 高 | 名位抵牾 1 处(五味子「金中火」) | 本库第 1 章清单 #8 已判「暂留待核」;如核对原书可定案 |", "| 中 | 中恶卒死方 6 首药行未机械切分(药名在用法散文中) | 如需入表,另立「用法体方」人工核定表 |", f"| 中 | 方中药味探真未收者({'、'.join(miss_yao) or '无'}) | 属探真取材范围(第 1–3 章)之外,登记为缺口,待附录补入 |", "| 低 | 探真未引及之方(见《对照表》第七节) | 多为救误方与附录拟补方,可作后续挖掘线索 |", "", "## 六、产物", "", f"- `{OUT_TABLE}`(人读)/`{OUT_JSON}`(机器读)/本报告", f"- 脚本 `05_脚本工具/15_link_tanzhen.py`(幂等;`--check` 只校验不写盘)", ""] struct = dict( 定位="《药性探真》第 1–3 章 ⇄ 《辅行诀五脏用药法要》底本 初步数据关联", 主从="主=底本(印刷本 baseline,用户 2026-09-16 定为基础);从=探真合卷本(研究文献,级别低于底本)", 粒度="药物级(药名+五味五行互含名位)/方剂级(方名反向索引)/药—方—阐释 三级串联", 用户指令="2026-09-22「初步进行《辅行诀五脏用药法要》与《辅行诀五脏用药法要 药性探真》的数据关联," "《辅行诀五脏用药法要》作为古本,是基础」", 来源=dict(底本文件=BENDI_MD, 底本md5=bmd5, 底本结构=BENDI_JSON, 探真文件=TZ_MD, 探真md5=tmd5, 探真结构=TZ_JSON), 底本锚=dict(二十五味药精=[ln for ln, _ in YAO_ANCHORS], 十三种药=THIRTEEN_LINE), 药精表=oracle_tbl, 十三种药=thirteen, 药物级对位=[dict(章=r["章"], 节=r["节"], 条目=r["条目"], 探真行=r["探真行"], 药=r["药"], 名位=r["名位"], 底本药=r.get("底本药"), 底本类=r.get("底本类"), 底本行=r.get("底本行"), 判定=r["判定"]) for r in rows], 方剂级反向索引=[dict(篇=l["篇"], 方名=l["方名"], 全名命中行=[h for h, _ in l["命中"]], 简称命中行=[h for h, _ in l["简称命中"]], 近似上下文=[dict(行=h, 文=c) for h, c in l["近似"]]) for l in cites], 药方阐释=[dict(篇=f["篇"], 方名=f["方名"], 组成=f["组成"], 加=f["加"], 去=f["去"], 药行=f["药行"], 关联=f["药—阐释"]) for f in fangs], 差异清单=[dict(类=c, 对象=o, 说明=d) for c, o, d in diff], 未落位=dict(方中药味探真未收=miss_yao, 探真未引及之方=[l["方名"] for l in cites if not l["命中"] and not l["简称命中"]], 底本无对应之探真药=sorted({r["药"] for r in named if r["判定"].startswith("○")})), 自检=[dict(项=n, 结果="✅ 通过" if v else "❌ 未通过") for n, v in checks], 重跑="python3 05_脚本工具/15_link_tanzhen.py(幂等;--check 只校验)", ) if not ok: print("❌ 自检未通过,未写盘:") for n, v in checks: if not v: print(" -", n) return 1 if check_only: print("--check:自检通过,未写盘。") for n, _ in checks: print(" ✅", n) return 0 wf(OUT_TABLE, "\n".join(tbl) + "\n") wf(OUT_REPORT, "\n".join(rep) + "\n") wf(OUT_JSON, json.dumps(struct, ensure_ascii=False, indent=2) + "\n") print("对照表:", OUT_TABLE, os.path.getsize(os.path.join(BASE, OUT_TABLE)), "bytes") print("报告 :", OUT_REPORT, os.path.getsize(os.path.join(BASE, OUT_REPORT)), "bytes") print("JSON :", OUT_JSON, os.path.getsize(os.path.join(BASE, OUT_JSON)), "bytes") print(f"药物级 {len(rows)} 行(有名位 {len(named)})|方剂级 {len(cites)} 方|方—药 {len(fangs)} 方") for n, v in checks: print(" ✅", n) print(" ⓘ 方中药味探真未收:", "、".join(miss_yao) or "无") print(f" ⓘ 方剂级:全名直引 {n_full} · 简称命中 {n_short} · 未引及 {n_none}") print(" ⓘ 探真未引及之方:", "、".join(l["方名"] for l in cites if not l["命中"] and not l["简称命中"]) or "无") for rel in (OUT_TABLE, OUT_REPORT, OUT_JSON): print(f" md5 {md5_of(os.path.join(BASE, rel))} {rel}") return 0 if __name__ == "__main__": sys.exit(main())