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

334 lines
20 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 第 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()