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

322 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
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 第 6 步(2026-09-17):本库内部**逐条对应**
输入:02_加工数据/整订稿_清洗版.md(衣之镖《整订稿》篇目/方目提纲,101 方目)
02_加工数据/辅行诀五脏用药法要_基础文本_清洗版.md(★底本,印刷本)
02_加工数据/印刷本结构_上下卷.json(底本篇/方索引,用作篇界与方目锚)
输出:03_关联融合/整订稿-底本_方目对照表.md
03_关联融合/整订稿-底本_方目对照.json
关联主轴(用户 2026-09-17「核对关联」指令):
主=底本(印刷本清洗版,用户 2026-09-16 定为基础文本);从=整订稿篇目提纲。
粒度=方目条 + 药行(草木药行/金石药行)行号锚定;**不改动任何一方原文**。
对位规则:
① 整订稿「X汤」 ↔ 底本「X汤散」条之**草木药行**(心篇四首无金石对应,底本即作「X汤」)
② 整订稿「X散汤」↔ 底本「X汤散」条之**金石药行**
③ 整订稿「大X汤/大X散汤」(救诸劳损病方篇)↔ 底本无独立方条,系依底本「若欲作大汤散者……」
加法(底本第 409 行)推得,关联至加法条 + 对应小方
④ 论说条(论证N条/论N条):与底本篇内方条之前的论说段落逐条核数
幂等:可重复运行;若底本或整订稿被重跑更新,本脚本按 md5 记录来源版本。
"""
import os, re, json, hashlib
BASE = os.path.expanduser("~/Documents/ai_agent_scraper_study/data/辅行诀五脏用药法要-药性探真")
F_ZD = os.path.join(BASE, "02_加工数据/整订稿_清洗版.md")
F_BASE = os.path.join(BASE, "02_加工数据/辅行诀五脏用药法要_基础文本_清洗版.md")
F_JSON = os.path.join(BASE, "02_加工数据/印刷本结构_上下卷.json")
OUT_MD = os.path.join(BASE, "03_关联融合/整订稿-底本_方目对照表.md")
OUT_JS = os.path.join(BASE, "03_关联融合/整订稿-底本_方目对照.json")
# ── 篇对位表:整订稿篇(清洗版字面)→ 底本篇(结构 JSON 标签)+篇题对位说明 ──
SEC_MAP = {
"辨肝脏病证文并方(论证四条,方八首)": ("上卷·辨肝脏病证文并方", "篇题同名(底本有篇题)", "方"),
"辨心脏病证文并方(论证五条,方四首)": ("上卷·辨心脏病证文并方", "篇题同名(底本有篇题)", "方"),
"又心包病方(论证一条,方八首)": ("上卷·辨心胞病证文(篇题缺)", "底本篇题缺;本提纲作「又心包病方」(字面),可补底本编次", "方"),
"辨脾脏病证文并方(论证四条,方八首)": ("上卷·辨脾脏病证文并方", "篇题同名(底本有篇题)", "方"),
"辨肺脏病证文并方(论证四条,方八首)": ("上卷·辨肺脏病证文并方", "篇题同名(底本有篇题)", "方"),
"辨肾脏病证文并方(论证四条,方八首)": ("上卷·辨肾脏病证文并方", "篇题同名(底本有篇题)", "方"),
"救诸病误治方(论证一首,方二十首)": ("上卷·救误泻五脏方(篇题缺)", "底本篇题缺;本提纲作「救诸病误治方」,底本引文作「又有泻方五首,以救诸病误治」", "方"),
"救诸劳损病方(论证二首,方二十首)": ("下卷·救五脏诸劳损病方(小方五首)", "底本引文作「经方有救诸劳损病方」;本提纲作「救诸劳损病方」", "方"),
"检录伊尹《汤液经法》方(六十一首)": ("下卷·汤液经法(二十五味药精)", "篇级对位(底本只存「今检录常情需用者六十一首」之数,无逐方目,与本提纲同)", "叙"),
"诸药五味五行互含文": ("下卷·汤液经法(二十五味药精条文)", "底本作「今者约列二十五种,以明五行互含之迹」(二十五味药精)", "叙"),
"心病诸药五行互含文": ("下卷·汤液经法(十三种药)", "底本作「又有药十三种,宜明其五行互含之事,以备心病方之用」", "叙"),
"五味补泻体用图": ("下卷·汤液经法(图注)", "底本图版缺失,仅存图注「此图乃《汤液经法》尽要之妙」", "叙"),
"外感天行病方(论二条,方十二首)": ("下卷·二旦四神大小汤(篇题缺)", "底本篇题缺;本提纲作「外感天行病方」", "方"),
"治中恶卒死方(论一条,方五首)": ("下卷·救五脏中恶卒死方(篇题缺)", "底本篇题缺;本提纲作「治中恶卒死方」", "方"),
"附:拟补心兼属土火金石补泻方四首": ("附录一·拟补心兼属土火金石补泻方四首", "篇题同名(底本作「附:拟补心兼属土火金石补泻方四首」)", "叙"),
"附:从火论心草木金石小补泻汤散四首": ("附录二·从火论心草木金石小补泻汤散四首", "篇题同名(底本作「附:从火论心草木金石小补泻汤散四首」)", "叙"),
}
# 篇题对位类型(用于差异分类)
HEAD_MAP_NOTE = {k: v[1] for k, v in SEC_MAP.items()}
CN = {"一":1,"二":2,"三":3,"四":4,"五":5,"六":6,"七":7,"八":8,"九":9,"十":10,
"十一":11,"十二":12,"十三":13,"十五":15,"二十":20}
DOSE = re.compile(r"(?:各|)[一二三四五六七八九十百]+(?:两|升|合|枚|斤|握|茎|锭|大枚|钱匕)")
# 底本「通论/结语/引文」类行首标记:方条正文范围到此为止(不含药材目录行)
EXCL_PREFIX = ("此篇所列", "陶云", "治疗劳损之方", "若欲作", "又有泻方",
"弘景曰", "陶隐居", "右五方", "若六畜", "上四十字")
FANG_RE = re.compile(r"^(?:救误|养生|调中|建中|凝息|固元)?(?:小|大)?[\u4e00-\u9fff()、]{2,12}?(?:汤散?|散|方)[::]")
OPEN_RE = re.compile(r"^(?:点眼|着舌|启咽|吹鼻|灌耳|熨耳)[\u4e00-\u9fff]{1,6}[::]")
def read(p):
raw = open(p, "rb").read()
return raw.decode("utf-8"), hashlib.md5(raw).hexdigest()
def lines_of(text):
return text.split("\n")
def load_bendi():
"""底本:篇界(结构 JSON)+ 方目(同名正则重算,含药行锚)"""
text, md5 = read(F_BASE)
js = json.load(open(F_JSON, encoding="utf-8"))
ls = lines_of(text)
secs = [dict(篇=n["篇"], 起始行=n["起始行"], 结束行=n["结束行"]) for n in js["篇"]]
excl = EXCL_PREFIX + tuple(a for v in ARGUE.values() for _ln, a in v)
starts = [f["起始行"] for f in js["方"]]
fangs = []
for idx, f in enumerate(js["方"]):
a = f["起始行"]
nxt = starts[idx + 1] if idx + 1 < len(starts) else len(ls) + 1
body = []
for i in range(a, nxt):
s = ls[i - 1].strip()
if i > a and (s.startswith("#") or any(s.startswith(x) for x in excl)):
break
if s:
body.append(i)
end = body[-1] if body else a
if end < a:
raise SystemExit(f"方条范围异常:{f['方名']}")
drug = [i for i in body if DOSE.search(ls[i - 1]) and not ls[i - 1].strip().startswith("右")]
fangs.append(dict(篇=f["篇"], 方名=f["方名"], 起始行=a, 结束行=end,
草木药行=drug[0] if drug else None,
金石药行=drug[-1] if len(drug) >= 2 else None))
return text, md5, js, secs, fangs
def load_zhengding():
text, md5 = read(F_ZD)
js = json.load(open(os.path.join(BASE, "02_加工数据/整订稿结构_篇方.json"), encoding="utf-8"))
return text, md5, js["方"], js["篇"]
def norm_base_name(name):
"""整订稿条目名 -> 底本方名候选(去「散汤」/「汤」后缀、心包->心胞、去附注)"""
n = re.sub(r"(附[^)]*)", "", name)
n = n.replace("(心包)", "(心胞)")
return n, re.sub(r"(散汤|汤)$", "", n)
# 论说条核对:底本各篇「论证段」行号(人工核定 + 首字锚校验;不含篇题/方条/通论结语)
ARGUE = {
"上卷·辨肝脏病证文并方": [(9, "肝虚则恐"), (11, "肝病者"), (13, "邪在肝"), (15, "陶云:肝德在散")],
"上卷·辨心脏病证文并方": [(55, "心虚则悲不已"), (57, "心病者"), (59, "邪在心"), (61, "经云:诸邪在心"), (63, "陶云:心德在耎")],
"上卷·辨心胞病证文(篇题缺)": [(89, "心胞气实者")],
"上卷·辨脾脏病证文并方": [(129, "脾实则"), (131, "脾病者"), (133, "邪在脾"), (135, "陶云:脾德在缓")],
"上卷·辨肺脏病证文并方": [(175, "肺虚则鼻息不利"), (177, "肺病者"), (179, "邪在肺"), (181, "陶云:肺德在收")],
"上卷·辨肾脏病证文并方": [(221, "肾气虚则厥逆"), (223, "肾病者"), (225, "邪在肾"), (227, "陶云:肾德在坚")],
"上卷·救误泻五脏方(篇题缺)": [(267, "又有泻方五首")],
"下卷·救五脏诸劳损病方(小方五首)": [(361, "陶云:经方有救诸劳损病方"), (363, "治疗劳损之方")],
"下卷·二旦四神大小汤(篇题缺)": [(437, "弘景曰:外感天行"), (511, "弘景曰:阳旦者")],
"下卷·救五脏中恶卒死方(篇题缺)": [(513, "陶隐居云:中恶卒死者")],
}
def main():
btext, bmd5, bjs, secs, bfangs = load_bendi()
ztext, zmd5, zfangs, zheads = load_zhengding()
bls = lines_of(btext)
by_sec_name = {}
for f in bfangs:
by_sec_name.setdefault(f["篇"], {})[f["方名"]] = f
rows, diffs, miss = [], [], []
for z in zfangs:
sec = z["篇"]
bsec, head_note, kind = SEC_MAP[sec]
zname = z["方名"]
jixing = "散汤(金石)" if zname.endswith("散汤") else "汤(草木)"
base, stem = norm_base_name(zname)
cands = ["%s汤散" % stem, "%s汤" % stem, stem]
hit = next((by_sec_name.get(bsec, {}).get(c) for c in cands
if by_sec_name.get(bsec, {}).get(c)), None)
note = ""
if hit is None and stem.startswith("大") and sec.startswith("救诸劳损"):
small = "小" + stem[1:]
s2 = by_sec_name.get(bsec, {}).get(small + "汤散") or by_sec_name.get(bsec, {}).get(small + "汤")
hit = dict(篇=bsec, 方名="(底本无独立方条)", 起始行=409, 结束行=409,
草木药行=None, 金石药行=None) if s2 else None
note = (f"底本只列小方五首({small}汤散 行 {s2['起始行']}–{s2['结束行']}),"
f"大方依底本行 409「若欲作大汤散者,补肝汤内加鸡肝……」加法推得,无独立条文"
if s2 else "未找到小方")
hit = dict(hit, 关联小方=f"{small}汤散(行 {s2['起始行']}–{s2['结束行']})" if s2 else "")
if hit is None:
miss.append((z["编号"], zname, sec))
rows.append(dict(编号=z["编号"], 整订稿方名=zname, 篇=sec, 剂型=jixing,
底本方名="—", 底本行="—", 说明="**未落位**"))
diffs.append(f"编号 {z['编号']}「{zname}」未能在底本落位")
continue
if jixing.startswith("散汤"):
anchor = hit.get("金石药行")
anchor_txt = f"金石药行 {anchor}" if anchor else "金石药行(未检出)"
else:
anchor = hit.get("草木药行")
anchor_txt = f"草木药行 {anchor}" if anchor else "草木药行(未检出)"
# 差异分类
cls = []
if hit["方名"].endswith("汤散") and zname.endswith("散汤"):
cls.append("体例:底本「X汤散」条之金石散方 = 整订稿「X散汤」")
elif hit["方名"].endswith("汤散") and not zname.endswith("散汤"):
cls.append("体例:底本「X汤散」条之草木方 = 整订稿「X汤」")
if "(心包)" in zname or "心包" in zname:
cls.append("用字:整订稿「心包」/底本「心胞」(不校,只登记)")
if stem.startswith("大") and sec.startswith("救诸劳损"):
cls.append("底本无独立方条(依大汤加法推得)")
if re.search(r"(附[^)]*)", zname):
cls.append("篇内附方(底本另出条文,非编号方)")
m = re.search(r"(附([^)]*))", zname)
fu = by_sec_name.get(bsec, {}).get(m.group(1))
if fu:
note = (note + f";底本另出附方「{m.group(1)}」(行 {fu['起始行']}–{fu['结束行']},"
f"底本结语作「右五方」故不计入编号)").strip(";")
if anchor is None and sec.startswith("治中恶"):
note = (note + ";本篇为外用/吐法方,底本无常规药味行").strip(";")
rows.append(dict(编号=z["编号"], 整订稿方名=zname, 篇=sec, 剂型=jixing,
底本篇=bsec, 底本方名=hit["方名"],
底本行=f"{hit['起始行']}–{hit['结束行']}",
锚=anchor_txt, 差异=cls, 说明=note))
# 论说条核对:底本各篇「论证段」条数 vs 整订稿篇题标称(锚校验)
def argue_count(sec_label):
ref = ARGUE.get(sec_label, [])
n = 0
for ln, anchor in ref:
if not bls[ln - 1].strip().startswith(anchor):
raise SystemExit(f"论证段锚校验失败:底本行 {ln} 应以「{anchor}」起,实为「{bls[ln-1][:24]}」")
n += 1
return n, [ln for ln, _ in ref]
def declared(t, kw):
m = re.search(kw + r"([一二三四五六七八九十]+)(?:条|首)", t)
return CN.get(m.group(1)) if m else None
arg_rows = []
for h in zheads:
sec = h["篇"]
bsec, head_note, kind = SEC_MAP[sec]
if kind == "叙":
arg_rows.append(dict(整订稿篇=sec, 底本篇=bsec, 整订稿标称="(无)",
底本实测="—", 判定="篇级对位(无方目)", 篇题对位=head_note))
continue
dec_a = declared(sec, "论证") or declared(sec, "论")
act_a, arg_lines = argue_count(bsec)
dec_f = h.get("篇题标称方数")
nb = len([f for f in bfangs if f["篇"] == bsec])
nz = h["方数"]
nb_note = f"{nb} 首"
if sec.startswith("治中恶"):
nb_note = "5 首 + 篇内附 2 首(启咽方、熨耳以通心气,不编号)"
nb = 5
if sec.startswith("辨心"):
ratio = "本篇底本无同构金石方,故汤/散不分立"
elif sec.startswith("救诸劳损"):
ratio = "小方 5 × 2(汤/散分立)+ 大方 5 × 2(依底本大汤加法推得,底本无独立方条)"
elif nb and nz == nb * 2:
ratio = "整订稿「汤/散」分立计"
else:
ratio = "本篇无汤/散分立(一草一汤)"
ok_a = "✅ 相符" if dec_a == act_a else f"⚠ 不符(标称 {dec_a} / 实测 {act_a})"
ok_f = "✅ 相符" if dec_f == nz else f"⚠ 不符(标称 {dec_f} / 实列 {nz})"
arg_rows.append(dict(整订稿篇=sec, 底本篇=bsec,
整订稿标称=f"论证 {dec_a} 条 · 方 {dec_f} 首",
底本实测=f"论证 {act_a} 段(行 {'、'.join(map(str, arg_lines))})· 方条 {nb_note}",
判定=f"论证 {ok_a};方数 {ok_f}({nz} = 底本 {nb} × {nz // nb if nb else '—'};{ratio})",
篇题对位=head_note))
# 落位自检
n_ok = sum(1 for r in rows if r["底本方名"] != "—")
checks = [f"方目落位 {n_ok}/{len(zfangs)}"
+ ("(全部落位)" if n_ok == len(zfangs) else "**有未落位项**")]
checks.append("论说条 / 方数 逐篇核对:" +
("全部相符" if all("⚠" not in r["判定"] for r in arg_rows) else "**有不符项**"))
checks.append(f"底本方目总数 {len(bfangs)}(结构 JSON)→ 整订稿方目 {len(zfangs)}(汤/散分立计)")
js = dict(
底本=dict(文件=os.path.basename(F_BASE), md5=bmd5),
整订稿=dict(文件=os.path.basename(F_ZD), md5=zmd5),
关联主轴="主=底本(印刷本清洗版);从=整订稿篇目提纲;粒度=方目条 + 药行行号;不改动原文",
自检=checks, 篇级核对=arg_rows,
方目对照=[dict(编号=r["编号"], 整订稿方名=r["整订稿方名"], 篇=r["篇"], 剂型=r["剂型"],
底本篇=r.get("底本篇"), 底本方名=r["底本方名"], 底本行=r["底本行"],
锚=r["锚"], 差异=r["差异"], 说明=r["说明"]) for r in rows],
未落位=miss)
os.makedirs(os.path.dirname(OUT_JS), exist_ok=True)
with open(OUT_JS, "w", encoding="utf-8") as f:
f.write(json.dumps(js, ensure_ascii=False, indent=2) + "\n")
md = ["# 整订稿 ⇄ 底本 方目对照表(本库内部逐条对应)", "",
"> **关联主轴**:主=**底本**(`02_加工数据/辅行诀五脏用药法要_基础文本_清洗版.md`,"
"印刷本,用户 2026-09-16 定为基础文本);从=**整订稿篇目提纲**"
"(`02_加工数据/整订稿_清洗版.md`,衣之镖《整订稿》篇目/方目,编号 1–101)。",
"> **粒度**:方目条 + 药行(草木药行/金石药行)行号锚定,行号一律指各自清洗版;**不改动任何一方原文**。",
f"> 版本:底本 md5 `{bmd5}`;整订稿 md5 `{zmd5}`;脚本 `05_脚本工具/06_link_zhengding.py`(幂等可重跑)", "",
"## 一、对位规则", "",
"| 整订稿条目 | 底本对位 | 落地锚 |", "|-----------|---------|--------|",
"| X汤 | 底本「X**汤散**」条(草木方+金石散同条)之**草木药行** | 方条行范围 + 草木药行行号 |",
"| X散汤 | 同上条之**金石药行**(同构金石方) | 同条行范围 + 金石药行行号 |",
"| 大X汤/大X散汤(救诸劳损病方篇) | 底本**无独立方条**;依底本行 409「若欲作大汤散者……」加法推得 | 加法行 409 + 对应小方行范围 |",
"| 篇内附(启咽方、熨耳以通心气) | 底本另出条文(非编号方) | 底本对应行 |",
"| 检录伊尹《汤液经法》方(六十一首)/诸药五味五行互含文/心病诸药五行互含文/五味补泻体用图 | 篇级对位(无逐方目) | 底本篇行范围 |",
"", "## 二、篇级核对(论证条数 · 方数 · 篇题)", "",
"| 整订稿篇 | 底本篇(本库标签) | 整订稿标称 | 底本实测 | 判定 |", "|---------|------------------|-----------|---------|------|"]
for r in arg_rows:
md.append(f"| {r['整订稿篇']} | {r['底本篇']} | {r['整订稿标称']} | {r['底本实测']} | {r['判定']} |")
md += ["", "## 三、方目逐条对照(整订稿 1–101 → 底本行号)", "",
"> 行号一律指各自**清洗版**;「落地锚」给出该条在底本中的药行行号(散汤=金石药行,余=草木药行)。",
"> 差异登记栏只列**体例/用字**之异,非校勘结论;底本为准,不作改写。", "",
"| 编号 | 整订稿方名 | 篇 | 剂型 | 底本方名 | 底本行 | 落地锚 | 差异登记 | 备注 |",
"|------|-----------|----|------|---------|--------|--------|---------|------|"]
for r in rows:
md.append(f"| {r['编号']} | {r['整订稿方名']} | {r['篇'].split('(')[0]} | {r['剂型']} | "
f"{r['底本方名']} | {r['底本行']} | {r['锚']} | {';'.join(r['差异']) or '—'} | "
f"{r['说明'] or '—'} |")
md += ["", "## 四、附录二种与篇内附方(整订稿不编号者)", "",
"| 整订稿篇/附 | 底本对位 | 底本行 |", "|-------------|---------|--------|",
"| 附:拟补心兼属土火金石补泻方四首 | 小泻心散/大泻心散/小补心散/大补心散(4 首,底本不另编号) | "
+ "、".join(str(f["起始行"]) for f in bfangs if f["篇"].startswith("附录一")) + " |",
"| 附:从火论心草木金石小补泻汤散四首 | 小泻心汤散/小补心汤散(题称四首 = 草木方+金石方各一) | "
+ "、".join(str(f["起始行"]) for f in bfangs if f["篇"].startswith("附录二")) + " |",
"| 99.启咽以通脾气(附启咽方) | 启咽以通脾气(行 523)+启咽方(行 527,附) | 523/527 |",
"| 101.灌耳以通肾气(附熨耳以通心气) | 灌耳以通肾气(行 535)+熨耳以通心气(行 539,附) | 535/539 |",
"", "## 五、自检", ""]
for c in checks:
md.append(f"- {'✅' if '**' not in c else '⚠'} {c}")
if miss:
md += ["", "**未落位项**:"] + [f"- 编号 {a}「{b}」({c})" for a, b, c in miss]
md += ["", "---", "",
"> 📅 2026-09-17 —— 建立本库内部逐条对应(用户「核对关联」指令);"
f"底本方目 {len(bfangs)} 首 ⇄ 整订稿方目 {len(zfangs)} 条(汤/散分立);"
f"落位 {n_ok}/{len(zfangs)}。差异清单与结论见 `整订稿核对报告.md`。"]
with open(OUT_MD, "w", encoding="utf-8") as f:
f.write("\n".join(md) + "\n")
print("对照表:", OUT_MD, os.path.getsize(OUT_MD), "bytes")
print("对照 :", OUT_JS, os.path.getsize(OUT_JS), "bytes")
for c in checks:
print(" ", c)
if miss:
print(" 未落位:", miss)
print("--- 篇级核对 ---")
for r in arg_rows:
print(f" {r['整订稿篇'][:14]:<16} | {r['整订稿标称']:<22} | {r['底本实测']:<30} | {r['判定']}")
if __name__ == "__main__":
main()