Files
health/辅行诀五脏用药法要-药性探真/05_脚本工具/15_link_tanzhen.py
T

670 lines
41 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())