#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 辅行诀五脏用药法要-药性探真 资料库 ETL 第 5 步(2026-09-17) 01_来源数据/《辅行诀五脏用药法要》整订稿.md(篇目/方目提纲) -> 02_加工数据/ 定位: 本件为**衣之镖《〈辅行诀五脏用药法要〉整订稿》之篇目/方目提纲**。 依据:`药性探真_第一章_原始_20260915.md` 首行目录作「附录一 《辅行诀五脏用药法要》整订稿 …… (221)」, 正文自述「依拙作《〈辅行诀五脏用药法要〉整订稿》(见附篇)五味五行互含名位推论」。 提纲只存篇题与方名(编号 1–101),无药味、无主治、无附篇页码正文 —— 属**篇目级**文献。 与底本(印刷本《辅行诀五脏用药法要》)关系:同一「五脏」传本系统(含金石方与附录二种)。 清理项(编号与本报告一致): A 版式残留:CRLF(\\r\\n)换行、行首行尾空格 B 断行/脱分隔:篇题被并入条目行 2 处(「辨肺脏病证文并方」并入 28. 条、「又心包病方」并入 12. 条) C 脱漏补正:编号脱顿号点 3 处(36/40/64) D 标点规范化:半角括号 -> 全角 8 处 E 存疑(原文不改):「散汤」命名体例、「又心包病方」篇题字面 F 版本差异/用字登记:「心包」vs 底本「心胞」、分卷体例、编号体例 自检(脚本自动): ① 编号 1–101 连续、无缺号无重号;② 每篇条目数 = 篇题所标「方N首」;③ 篇序与「散汤」条数 输出: 02_加工数据/整订稿_清洗版.md 02_加工数据/整订稿清洗报告.md 02_加工数据/整订稿结构_篇方.json 幂等:可重复运行,输出覆盖生成;关键改写均带原文锚校验,源文件被替换即报错。 """ import os, re, json, hashlib BASE = os.path.expanduser("~/Documents/ai_agent_scraper_study/data/辅行诀五脏用药法要-药性探真") SRC = os.path.join(BASE, "01_来源数据/《辅行诀五脏用药法要》整订稿.md") OUT_MD = os.path.join(BASE, "02_加工数据/整订稿_清洗版.md") OUT_REPORT = os.path.join(BASE, "02_加工数据/整订稿清洗报告.md") OUT_STRUCT = os.path.join(BASE, "02_加工数据/整订稿结构_篇方.json") VERSION = "衣之镖《〈辅行诀五脏用药法要〉整订稿》篇目/方目提纲(《药性探真》附篇 附录一,p.221 起)" # ── B 类:篇题被并入条目行末尾,须补回车界(原文锚全串唯一命中)────────── MERGED_HEADINGS = [ ("28.大补脾散汤辨肺脏病证文并方(论证四条,方八首)", "28.大补脾散汤", "辨肺脏病证文并方(论证四条,方八首)", "篇题「辨肺脏病证文并方(论证四条,方八首)」脱换行,被并入上一条(脾篇末条 28.大补脾散汤)之末"), ("12.大补心汤又心包病方(论证一条,方八首)", "12.大补心汤", "又心包病方(论证一条,方八首)", "篇题(心包篇)脱换行,被并入上一条(心篇末条 12.大补心汤)之末"), ] # ── C 类:编号脱顿号点(原文锚全串唯一命中)──────────────────────────── NUMBER_DOTS = ["36大补肺散汤", "40大泻肾散汤", "64救误大泻肾散汤"] # ── E 类:存疑(原文一律不改,登记待核)──────────────────────────────── SUSPECTS = [ ("散汤", "「X散汤」命名体例:底本《辅行诀五脏用药法要》各该方作「X汤散」(草木方+金石散同条," "如「小泻肝汤散」),本提纲将同一方拆为两项,草木方作「X汤」、金石方作「X散汤」,一分二计。" "二解待核:①整理者(衣之镖)整订体例,汤=草木方、散汤=其同构金石散方;" "②系「汤散」二字误倒(传抄/录排),实即底本「X汤散」。**须补《药性探真》附篇附录一原书正文" "(p.221 起)核对命名形制**。本阶段原文不改,只登记。"), ("又心包病方", "篇题字面作「又心包病方」(底本此篇篇题缺)。疑原书篇题作「辨心包病证文并方」" "(参校本·维基文库本作「辨心包络病证文并方」),本提纲作简式节题。待补附篇原文核对。"), ] # ── F 类:版本差异/用字登记(不校改)────────────────────────────────── VERSION_NOTES = [ ("用字·心包/心胞", "本提纲作「心包」(心包病方、小泻心(心包)汤);" "底本(印刷本)作「心胞」;参校本·维基文库本作「心包络」。" "本阶段异体用字不校,只登记;以本库底本用字为准。"), ("分卷体例", "本提纲**不分上下卷**,与底本(印刷本)同;" "本库 `02_加工数据/印刷本结构_上下卷.json` 的「上卷/下卷」标签系依古本原文传本编次所加," "非底本原有标识,关联时须留意。"), ("篇题与论证/方数标注", "本提纲于篇题后缀「(论证N条,方N首)」,底本无此标注。" "经逐篇核对(见 `03_关联融合/整订稿核对报告.md`),其论证条数、方首数与底本正文实况" "全部相符(方数按「汤」「散」分立计),可作底本缺题篇的编次依据。"), ("编号体例", "本提纲对**有方名之方**连续编号 1–101(编号贯通全书、跨篇连续);" "附录二种、篇内「附」方(启咽方、熨耳以通心气)及《汤液经法》六十一首方不编号。"), ("文献层级", "本件仅为**篇目/方目提纲**,无药味、无主治、无附篇页码正文;" "《药性探真》附篇「附录一 整订稿」原书正文(p.221 起)尚未入库,待补。"), ] # 篇题标准序(自检用) HEADINGS_ORDER = [ "辨肝脏病证文并方(论证四条,方八首)", "辨心脏病证文并方(论证五条,方四首)", "又心包病方(论证一条,方八首)", "辨脾脏病证文并方(论证四条,方八首)", "辨肺脏病证文并方(论证四条,方八首)", "辨肾脏病证文并方(论证四条,方八首)", "救诸病误治方(论证一首,方二十首)", "救诸劳损病方(论证二首,方二十首)", "检录伊尹《汤液经法》方(六十一首)", "诸药五味五行互含文", "心病诸药五行互含文", "五味补泻体用图", "外感天行病方(论二条,方十二首)", "治中恶卒死方(论一条,方五首)", "附:拟补心兼属土火金石补泻方四首", "附:从火论心草木金石小补泻汤散四首", ] NO_FANG = {"检录伊尹《汤液经法》方(六十一首)", "诸药五味五行互含文", "心病诸药五行互含文", "五味补泻体用图", "附:拟补心兼属土火金石补泻方四首", "附:从火论心草木金石小补泻汤散四首"} CN = {"一":1,"二":2,"三":3,"四":4,"五":5,"六":6,"七":7,"八":8,"九":9,"十":10, "十一":11,"十二":12,"十三":13,"十五":15,"二十":20,"六十":60,"六十一":61} def clean(raw): events = [] n_crlf = raw.count("\r\n") text = raw.replace("\r\n", "\n").replace("\r", "\n") raw_lines = text.split("\n") if n_crlf: events.append(dict(类型="A版式残留", 原=f"CRLF(\\r\\n)换行 {n_crlf} 处", 改="统一为 LF", 依据="Windows 记事本/导出残留,行末 CR 一律清除")) n_trail = sum(1 for l in raw_lines if l != l.rstrip()) if n_trail: events.append(dict(类型="A版式残留", 原=f"行尾空格 {n_trail} 行", 改="已删", 依据="版式残留,逐行 rstrip")) # 1) B 类:篇题拆行恢复(原文锚全串唯一命中) for key, head, heading, why in MERGED_HEADINGS: if text.count(key) != 1: raise SystemExit(f"篇题拆行锚校验失败(命中 {text.count(key)} 次):{key}") ln = next(i for i, l in enumerate(text.split("\n"), start=1) if key in l) text = text.replace(key, f"{head}\n\n## {heading}") events.append(dict(行=ln, 类型="B断行接合", 原=key, 改=f"{head} ∥ 篇题「{heading}」", 依据=why + ";据同书体例(各篇题独立成行、冠「## 」)恢复行界与标题级")) # 2) C 类:编号脱顿号点(原文锚全串唯一命中) for s in NUMBER_DOTS: if text.count(s) != 1: raise SystemExit(f"编号锚校验失败(命中 {text.count(s)} 次):{s}") m = re.match(r"^(\d+)", s) new = f"{m.group(1)}." + s[m.end():] ln = next(i for i, l in enumerate(text.split("\n"), start=1) if s in l) text = text.replace(s, new) events.append(dict(行=ln, 类型="C脱漏补正", 原=s, 改=new, 依据="本提纲方名编号体例作「NN.方名」(101 条中仅此 3 处脱顿号点)," "与前后条目对校可证")) # 3) B 类:篇内方目列表被录入断行(行末悬「;」),接合为一行 lines = text.split("\n") merged, i, n_merge = [], 0, 0 while i < len(lines): cur = lines[i] if cur.strip().endswith(";"): j = i + 1 while j < len(lines) and not lines[j].strip(): j += 1 if j < len(lines) and not lines[j].strip().startswith("#"): nxt = lines[j].strip() merged.append(cur.rstrip() + nxt) events.append(dict(行=i + 1, 类型="B断行接合", 原=f"{cur.strip()[-24:]} ⟂ {nxt[:24]}", 改=f"{cur.strip()[-24:]}{nxt[:24]}", 依据="行末悬顿号「;」而次行仍为同篇方目列表,显系录入断行" "(同书肝/心/心包/救误/劳损/外感/中恶诸篇之方目皆一行到底),据体例接合")) n_merge += 1 i = j + 1 continue merged.append(cur) i += 1 if n_merge != 3: raise SystemExit(f"篇内方目断行接合数 {n_merge},预期 3 处(脾、肺、肾篇)") text = "\n".join(merged) # 4) 逐行处理:D 类标点 + 组装(篇题前后空行) heads, out = [], [] for i, l in enumerate(text.split("\n"), start=1): s = l.strip() if not s: continue if s.startswith("## "): name = s[3:].strip() out += [f"## {name}", ""] heads.append(dict(篇=name, 提纲行=i)) continue if s.startswith("# "): out += [s, ""] continue t = s.replace("(", "(").replace(")", ")").replace(";", ";") if t != s: np_ = sum(1 for a, b in zip(s, t) if a == "(" and b == "(") ns_ = sum(1 for a, b in zip(s, t) if a == ";" and b == ";") events.append(dict(行=i, 类型="D标点", 原=s, 改=t, 依据=f"半角括号改全角({np_} 处)" + (f"、半角分号改全角({ns_} 处)" if ns_ else "") + ";标点规范化,与全书体例一致")) out += [t, ""] text_out = re.sub(r"\n{3,}", "\n\n", "\n".join(out)).strip() + "\n" return text_out, events, heads def parse_and_check(text_out, heads): """按清洗版重建 篇→方 归属(严格顺序遍历),并做自检。""" errs, oks = [], [] cur, fangs2 = None, [] for i, l in enumerate(text_out.split("\n"), start=1): s = l.strip() if s.startswith("## "): cur = s[3:].strip() continue if not s or s.startswith("#") or cur in NO_FANG: continue for item in s.split(";"): if not item.strip(): continue m = re.match(r"^(\d+)\.(.+)$", item.strip()) if not m: raise SystemExit(f"条目非预期格式(清洗版行 {i}):{item[:50]}") fangs2.append(dict(篇=cur, 编号=int(m.group(1)), 方名=m.group(2), 行=i)) for h in heads: fs = [f for f in fangs2 if f["篇"] == h["篇"]] h["方数"] = len(fs) m = re.search(r"方([一二三四五六七八九十]+)首", h["篇"]) h["篇题标称方数"] = CN.get(m.group(1)) if m else None nums = [f["编号"] for f in fangs2] if nums == list(range(1, len(nums) + 1)): oks.append(f"编号 1–{len(nums)} 连续、无缺号无重号") else: errs.append(f"编号异常:共 {len(nums)} 条,实得 {nums[:5]}…{nums[-5:]}") for h in heads: if h["篇题标称方数"] is not None and h["篇"] not in NO_FANG: if h["篇题标称方数"] != h["方数"]: errs.append(f"{h['篇']}:篇题标称 {h['篇题标称方数']} 首,实列 {h['方数']} 首") else: oks.append(f"{h['篇'].split('(')[0]}:篇题标称 {h['篇题标称方数']} 首 = 实列 {h['方数']} 首") order = [h["篇"] for h in heads] if order == HEADINGS_ORDER: oks.append(f"篇序 {len(order)} 篇与预期一致") else: errs.append(f"篇序与预期不符:{order}") n_san = sum(1 for f in fangs2 if f["方名"].endswith("散汤")) if n_san == 40: oks.append("「散汤」(金石散方)40 条 = 五脏 20 + 救误 10 + 劳损 10") else: errs.append(f"「散汤」条数 {n_san},预期 40") return fangs2, errs, oks def main(): raw = open(SRC, "rb").read().decode("utf-8") # 二进制读取:保留 CRLF 以登记版式残留 text0, events, heads = clean(raw) fangs, errs, oks = parse_and_check(text0, heads) def wf(p, c): os.makedirs(os.path.dirname(p), exist_ok=True) with open(p, "w", encoding="utf-8") as f: f.write(c); f.flush(); os.fsync(f.fileno()) wf(OUT_MD, text0) md5_src, md5_out = hashlib.md5(raw.encode()).hexdigest(), hashlib.md5(text0.encode()).hexdigest() struct = { "来源文件": os.path.basename(SRC), "来源文件md5": md5_src, "清洗版文件": os.path.basename(OUT_MD), "清洗版md5": md5_out, "文献定位": VERSION, "性质": "篇目/方目提纲(编号 1–101),无药味、无主治条文", "体例说明": "行号均指清洗版;「散汤」为整理者整订体例(对应底本「X汤散」条之金石散方)," "原文不改,见存疑登记", "篇": heads, "方": fangs, } wf(OUT_STRUCT, json.dumps(struct, ensure_ascii=False, indent=2) + "\n") c = lambda t: sum(1 for e in events if e["类型"].startswith(t)) rep = ["# 《辅行诀五脏用药法要》整订稿(篇目提纲)— 清洗报告", "", f"> 🧾 **文献定位**:{VERSION}", "> 依据 `01_来源数据/药性探真_第一章_原始_20260915.md` 首行目录行" "「附录一 《辅行诀五脏用药法要》整订稿 …… (221)」及正文自述(「依拙作《〈辅行诀五脏用药法要〉整订稿》" "(见附篇)五味五行互含名位推论」)判定:本件为衣之镖整订稿之**篇目/方目提纲**,编号 1–101。", "> 本件为**篇目级**文献(无药味、无主治、无页码正文);附篇原书正文待补。", "> 与底本关系:同一「五脏」传本系统(含每方同构金石方与附录二种);" "底本=`02_加工数据/辅行诀五脏用药法要_基础文本_清洗版.md`(用户 2026-09-16 定)。", "", "## 0 概览", "", f"- 来源:`01_来源数据/{os.path.basename(SRC)}`(md5 `{md5_src}`)", f"- 清洗版:`02_加工数据/{os.path.basename(OUT_MD)}`(md5 `{md5_out}`)", f"- 字符数:{len(raw)} → {len(text0)};行数:{raw.count(chr(10))+1} → {text0.count(chr(10))+1}", f"- 结构:{len(heads)} 篇 / {len(fangs)} 条方目(编号 1–{len(fangs)});" f"其中「散汤」(金石散方){sum(1 for f in fangs if f['方名'].endswith('散汤'))} 条", "", "| 清理类别 | 处数 | 说明 |", "|---------|------|------|", f"| A 版式残留 | {c('A')} | CRLF 换行、行尾空格 |", f"| B 断行接合 | {c('B')} | 篇题被并入条目行 2 处(脾→肺篇、心→心包篇),恢复行界 |", f"| C 脱漏补正 | {c('C')} | 编号脱顿号点 3 处(36/40/64) |", f"| D 标点规范化 | {c('D')} | 半角括号→全角 8 处 |", f"| E 存疑(未改原文) | {len(SUSPECTS)} | 「散汤」体例、「又心包病方」篇题字面 |", f"| F 版本差异/用字(未改) | {len(VERSION_NOTES)} | 心包/心胞、分卷体例等 |", "", "### 各篇方目一览", "", "| 篇 | 篇题标称 | 实列方目 | 编号区间 |", "|----|---------|---------|---------|"] for h in heads: fs = [f for f in fangs if f["篇"] == h["篇"]] rng = f"{fs[0]['编号']}–{fs[-1]['编号']}" if fs else "—" rep.append(f"| {h['篇']} | {h.get('篇题标称方数','—')} | {len(fs)} | {rng} |") rep += ["", "---", "", "## 1 逐条清理明细", "", "| 行 | 类别 | 原文 | 处理后 | 依据 |", "|----|------|------|--------|------|"] for e in events: rep.append(f"| {e.get('行','—')} | {e['类型']} | {e['原'].replace('|','|')} | " f"{e['改'].replace('|','|')} | {e['依据'].replace('|','|')} |") rep += ["", "---", "", "## 2 自检结果(脚本自动)", ""] for o in oks: rep.append(f"- ✅ {o}") for e in errs: rep.append(f"- ⚠ **{e}**") rep += ["", "> 与底本的逐条落位核对(方名归一、行号锚定、差异清单)见 " "`03_关联融合/整订稿-底本_方目对照表.md` 与 `03_关联融合/整订稿核对报告.md`。", "", "## 3 存疑登记(原文不改,待核附篇原文)", "", "| 片段 | 说明 |", "|------|------|"] for k, v in SUSPECTS: rep.append(f"| {k} | {v} |") rep += ["", "## 4 版本差异/体例登记(不校改)", "", "| 类别 | 说明 |", "|------|------|"] for k, v in VERSION_NOTES: rep.append(f"| {k} | {v} |") rep += ["", "## 5 篇段结构与方目(清洗版行号)", "", "| 编号 | 方名 | 篇 | 清洗版行 |", "|------|------|----|---------|"] for f in fangs: rep.append(f"| {f['编号']} | {f['方名']} | {f['篇']} | {f['行']} |") rep += ["", "---", "", "> 📅 2026-09-17 —— 建件:导入用户提供的《辅行诀五脏用药法要》整订稿(篇目提纲)并清洗;" f"结构 {len(heads)} 篇 / {len(fangs)} 方目;自检 {'通过' if not errs else '有告警'}。"] wf(OUT_REPORT, "\n".join(rep) + "\n") print("清洗版:", OUT_MD, os.path.getsize(OUT_MD), "bytes") print("结构 :", OUT_STRUCT, os.path.getsize(OUT_STRUCT), "bytes") print("报告 :", OUT_REPORT, os.path.getsize(OUT_REPORT), "bytes") print(f"A {c('A')} | B {c('B')} | C {c('C')} | D {c('D')} | 存疑 {len(SUSPECTS)} | 版本差异 {len(VERSION_NOTES)}") print(f"篇 {len(heads)} | 方目 {len(fangs)} | 自检错误 {len(errs)}") for o in oks: print(" ✅", o) for e in errs: print(" ⚠", e) if __name__ == "__main__": main()