初始归档:健康管理资料库(大医网/药膳/体质监测/中医理论等12个资料集,7712个文件)

This commit is contained in:
512song committed 2026-09-23 21:59:25 +08:00
commit 80ae3811cf
7712 files changed
+4628547

No files matched your search

@@ -0,0 +1,669 @@
#!/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())