334 lines
20 KiB
Python
334 lines
20 KiB
Python
#!/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()
|