初始归档:健康管理资料库(大医网/药膳/体质监测/中医理论等12个资料集,7712个文件)

This commit is contained in:
512song committed 2026-09-23 21:59:25 +08:00
commit 80ae3811cf
7712 files changed
+4628547

No files matched your search

@@ -0,0 +1,384 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
辅行诀五脏用药法要-药性探真 资料库 ETL 第 1 步(v3)
01_来源数据 -> 02_加工数据
只做"数据清理",不做释义抽取 / 跨库关联 / 主题分析(用户 2026-09-15 指令)。
清理项:
A 页面残留物:页眉(含缺书名号的嵌入形式)、独立页码、行尾空格、全角空格
B 断行错位接合:段落被页眉/页码打断处自动接合
C 形近错字 / 异体字 / 书名误字修正(逐条附核校依据)
E 半角标点规范化(邻接汉字时)
F 存疑登记:无法确证处不改动原文,逐条附疑正与核校来源
输出:
02_加工数据/药性探真_第一章_清洗版.md
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_来源数据/药性探真_第一章_原始_20260915.md")
OUT_MD = os.path.join(BASE, "02_加工数据/药性探真_第一章_清洗版.md")
OUT_REPORT = os.path.join(BASE, "02_加工数据/文本清洗报告.md")
OUT_CHECK = os.path.join(BASE, "02_加工数据/人工核对清单.md")
OUT_STRUCT = os.path.join(BASE, "02_加工数据/章节结构_第一章.json")
PAGE_HEADER_RE = re.compile(r'^\s*《?辅行诀五脏用药法要》?\s*药性探真\s*》?\s*$')
PAGE_NUM_RE = re.compile(r'^\s*\d{1,3}\s*$')
CJK = r'[\u4e00-\u9fff]'
# C 类:形近错字 / 异体字 / 书名误字(逐条核校)
FIXES = [
("先骋通使", "先聘通使", "《本经》牡桂条作「为诸药先聘通使」;下文自称「先聘」,本书自证"),
("越洲山阴", "越州山阴", "陆佃籍贯,《宋史·陆佃传》作越州山阴"),
("桜", "梫", "《纲目》桂条引《尔雅》「梫者,能侵害他木也」(梫,音寝)"),
("国老之誊", "国老之誉", "文义:众药之王、和国老之誉;「誊」形近误"),
("酒皰皷鼻", "酒疱皶鼻", "《本经》栀子条作「面赤,酒疱皶鼻」"),
("淚出", "泪出", "异体字正字,全书他处均用「泪」"),
("微字为蒼之讹字", "微字为苍之讹字", "《纲目》人参释名,简体正字"),
("天元幻大论", "天元纪大论", "《素问》篇名作「天元纪大论」"),
("白戸浆", "白酨浆", "酨即酢浆;《辅行诀》原书作「白酨浆」,本书小标题亦作白酨浆"),
]
# F 类:存疑不改(原文保留)
SUSPECT_RULES = [
(r'菌,蕗。从竹,雨声', "《说文》「箘,箘簬。从竹,囷声。一曰博棋也」;疑「菌蕗」「雨声」为形近致误"),
(r'大则性环详而缓', "《本草衍义》枳实条:「大则其性详而缓」,疑衍「环」字"),
(r'决泄愤,地方但', "《本草衍义》:「皆取其疏通决泄、破结实之义。他方但导败风壅之气……」;"
"此处「愤,地方但」疑为OCR丢字串行,待补正"),
(r'由于积分布甚广', "据上下文(论枳之品种)疑当作「枳分布甚广」"),
(r'传动产隻者为雄', "《竹谱》竹根雌雄辨:疑当作「其节单者为雄」(「隻」为「单」之误)"),
(r'[((]?[))]', "原文含存疑符号(?),系作者或OCR存疑处,保留原文"),
(r'[①②③④⑤⑥⑦⑧⑨⑩]', "脚注标记,对应脚注正文在本次来源中缺失,待补"),
]
def clean_text(raw: str):
"""返回 (清洗文本, 事件列表, 删除统计)。事件均锚定到【清洗版行号】。"""
recs = [] # [{"text":..., "fixes":[...], "punct":[...]}]
removed = {"页眉": 0, "页码": 0}
for line in raw.split("\n"):
if PAGE_HEADER_RE.match(line):
removed["页眉"] += 1
continue
if PAGE_NUM_RE.match(line):
removed["页码"] += 1
continue
new = line.replace("\u3000", "").rstrip()
rec = {"text": new, "fixes": [], "punct": []}
for bad, good, why in FIXES:
if bad in new:
rec["fixes"].append({"原": bad, "改": good, "依据": why})
new = new.replace(bad, good)
# 保护「附录N」「第N章/节」后的分隔空格,避免与 OCR 断行空格一并折叠
new = re.sub(r'(附录[一二三四五六七八九十]|第[一二三四五六七八九十]+[章节])[ \t]+',
lambda m: m.group(1) + "\x01", new)
prev = None
while prev != new: # 中文词间半角空格(OCR断行残留)
prev = new
new = re.sub(r'(?<=[\u4e00-\u9fff])\s+(?=[\u4e00-\u9fff])', '', new)
new = re.sub(r'(?<=[\u4e00-\u9fff])\s+(?=[,。、;:?!“”()《》])', '', new)
new = re.sub(r'(?<=[,。、;:?!“”()《》])\s+(?=[\u4e00-\u9fff])', '', new)
# E 类:半角标点 ↔ 全角(邻接汉字或全角标点)
if re.search(CJK, line):
for _pass in range(2): # 二次通过:括号先转全角后再判后随标点
for asc, full in ((",", ","), (";", ";"), (":", ":"), ("?", "?"), ("!", "!")):
new2 = re.sub(r'(?<=[\u4e00-\u9fff),。、;:?!”“》)])\s*' + re.escape(asc), full, new)
if new2 != new:
rec["punct"].append({"原": asc, "改": full}); new = new2
for asc, full, side in (("(", "(", "后"), (")", ")", "前")):
pat = (r'\s*' + re.escape(asc) + r'(?=' + CJK + r')') if side == "后" else \
(r'(?<=' + CJK + r')\s*' + re.escape(asc))
new2 = re.sub(pat, full, new)
if new2 != new:
rec["punct"].append({"原": asc, "改": full}); new = new2
# 转换后再次折叠全角标点两侧空格
new = re.sub(r'(?<=[\u4e00-\u9fff])\s+(?=[,。、;:?!“”()《》])', '', new)
new = re.sub(r'(?<=[,。、;:?!“”()《》])\s+(?=[\u4e00-\u9fff])', '', new)
new = re.sub(r'(?=[,。、;:?!])\s+', '', new)
new = new.replace("\x01", " ") # 还原受保护的分隔空格
rec["text"] = new.rstrip()
recs.append(rec)
# 合并连续空行
tmp = []
prev_blank = False
for r in recs:
if r["text"] == "":
if prev_blank:
continue
prev_blank = True
else:
prev_blank = False
tmp.append(r)
# B 类:断行错位接合
out, joins = [], []
i = 0
while i < len(tmp):
cur = tmp[i]
if (cur["text"] and not cur["text"].startswith("#") and len(cur["text"]) > 8
and re.search(CJK + r'$', cur["text"])):
j = i + 1
while j < len(tmp) and tmp[j]["text"] == "":
j += 1
if j < len(tmp) and re.match(CJK, tmp[j]["text"]) and not tmp[j]["text"].startswith("#"):
merged = {"text": cur["text"] + tmp[j]["text"],
"fixes": cur["fixes"] + tmp[j]["fixes"],
"punct": cur["punct"] + tmp[j]["punct"]}
joins.append({"接合前": cur["text"][-20:], "接合后": tmp[j]["text"][:20],
"_before_line": i + 1, "_cont_line": j + 1})
out.append(merged)
i = j + 1
continue
out.append(cur)
i += 1
# 落定清洗版行号 + 事件汇总
lines, events = [], []
for n, rec in enumerate(out, start=1):
lines.append(rec["text"])
for f in rec["fixes"]:
events.append(dict(类型="C形近错字", 行=n, **f))
for p in rec["punct"]:
events.append(dict(类型="E标点规范化", 行=n, 原=p["原"], 改=p["改"], 依据="半角标点邻接汉字"))
for s in SUSPECT_RULES:
for m in re.finditer(s[0], rec["text"]):
events.append(dict(类型="F存疑", 行=n,
原=f"…{rec['text'][max(0,m.start()-15):m.end()+15]}…", 改="(保留原文)", 依据=s[1]))
for jn in joins:
events.append(dict(类型="B断行接合", 行=jn["_before_line"],
原=f"…{jn['接合前']} ‖ {jn['接合后']}…",
改="接合为一段", 依据=f"段落被页眉/页码打断,接续于原第{jn['_cont_line']}行"))
text = re.sub(r'\n{3,}', '\n\n', "\n".join(lines)).strip() + "\n"
# 去重:同行同类型同说明
seen, uniq = set(), []
for e in events:
k = (e["类型"], e["行"], e["依据"])
if k not in seen:
seen.add(k); uniq.append(e)
uniq.sort(key=lambda x: (x["行"], {"B断行接合": 0, "C形近错字": 1, "E标点规范化": 2, "F存疑": 3}[x["类型"]]))
return text, uniq, removed
def parse_structure(text: str):
lines = text.split("\n")
nodes = []
for i, l in enumerate(lines, start=1):
if not l.startswith("#"):
continue
title = l.lstrip("#").strip()
if re.match(r'^第[一二三四五六七八九十]+章', title):
lvl, typ = 1, "章"
elif re.match(r'^第[一二三四五六七八九十]+节', title):
lvl, typ = 2, "节"
elif re.match(r'^(附[::]|\d+\.\s)', title):
lvl, typ = 4, "附注"
else:
lvl, typ = 3, "条目"
nodes.append({"标题": title, "起始行": i, "类型": typ, "_lvl": lvl})
for n, node in enumerate(nodes):
node["结束行"] = len(lines)
for m in range(n + 1, len(nodes)):
if nodes[m]["_lvl"] <= node["_lvl"]:
node["结束行"] = nodes[m]["起始行"] - 1
break
GATE = {"辛": ("肝", "木", "春"), "咸": ("心", "火", "夏"), "甘": ("脾", "土", "长夏"),
"酸": ("肺", "金", "秋"), "苦": ("肾", "水", "冬")}
chapters = []
for node in nodes:
if node["类型"] == "章":
chapters.append(dict(node, 节=[]))
elif node["类型"] == "节":
m = re.search(r'([辛咸甘酸苦])味?门', node["标题"])
node["味门"] = m.group(1) if m else None
if m:
node["本脏"], node["本行"], node["脏象法象"] = GATE[m.group(1)]
if chapters:
chapters[-1]["节"].append(dict(node, 条目=[]))
else:
m = re.match(r'^[一二三四五六七八九十]+、\s*(.+?)\s*[((](.+?)[))]\s*$', node["标题"])
if m:
node["药名"], node["五行互含名位"] = m.group(1), m.group(2)
else:
node["药名"] = re.sub(r'^[一二三四五六七八九十]+、\s*', '', node["标题"])
node["五行互含名位"] = None
if chapters and chapters[-1].get("节"):
chapters[-1]["节"][-1]["条目"].append(node)
for c in chapters:
for s in c["节"]:
s["药物条目"] = [e for e in s["条目"] if e["类型"] == "条目"]
s["附属标题"] = [e["标题"] for e in s["条目"] if e["类型"] == "附注"]
return chapters
def scan_missing_assets(text: str):
"""登记悬空图片引用(OCR 流水线遗留,图片本体未随源文件提供)。"""
lines = text.split("\n")
out = []
for i, l in enumerate(lines, start=1):
for m in re.finditer(r'!\[\]\(([^)\n]+)\)', l):
out.append({"行": i, "文件名": os.path.basename(m.group(1)), "原始引用": m.group(0)[:80],
"上文": lines[i-3][:60] if i >= 3 else ""})
return out
def main():
raw = open(SRC, encoding="utf-8").read()
text, events, removed = clean_text(raw)
chapters = parse_structure(text)
missing_assets = scan_missing_assets(text)
def wf(path, content):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(content); f.flush(); os.fsync(f.fileno())
wf(OUT_MD, text)
struct = {
"来源文件": os.path.basename(SRC), "来源文件md5": hashlib.md5(raw.encode()).hexdigest(),
"清洗版文件": os.path.basename(OUT_MD), "清洗版md5": hashlib.md5(text.encode()).hexdigest(),
"原始字符数": len(raw), "清洗后字符数": len(text),
"说明": "行号均指清洗版;本文件仅作清理配套定位索引,未做释义抽取与跨库关联",
"章": chapters,
}
wf(OUT_STRUCT, json.dumps(struct, ensure_ascii=False, indent=2) + "\n")
ch = chapters[0]
secs = ch["节"]
n_drug = sum(len(s["药物条目"]) for s in secs)
cnt = lambda t: sum(1 for e in events if e["类型"] == t)
# 每节归属
def owner(line):
for s in secs:
if s["起始行"] <= line <= s["结束行"]:
return s
return None
per_sec = {s["标题"]: [] for s in secs}
for e in events:
o = owner(e["行"])
if o is not None:
per_sec[o["标题"]].append(e)
def ev_table(evs):
rows = ["| 行 | 类型 | 原文 | 处理后 | 依据 |", "|----|------|------|--------|------|"]
for e in evs:
rows.append(f"| {e['行']} | {e['类型']} | {e['原'].replace('|','|')} | "
f"{e['改'].replace('|','|')} | {e['依据']} |")
return rows
rep = ["# 《辅行诀五脏用药法要》药性探真 第一章 — 文本清洗报告", "",
"> 本库现阶段只做数据清理。用户 2026-09-15 指令:以第一章为源,待全文内容完整后再做加工/跨库关联/分析。", "",
"## 0 概览", "",
f"- 来源:`01_来源数据/{os.path.basename(SRC)}`(md5 `{struct['来源文件md5']}`)",
f"- 清洗版:`02_加工数据/{os.path.basename(OUT_MD)}`(md5 `{struct['清洗版md5']}`)",
f"- 字符数:{len(raw)} → {len(text)};行数:{raw.count(chr(10))} → {text.count(chr(10))}",
f"- 结构:{ch['标题']} ── {len(secs)} 节 / 药物条目 {n_drug} 味", "",
"| 清理类别 | 处数 | 说明 |", "|---------|------|------|",
f"| A 页面残留物 | {removed['页眉'] + removed['页码']} | 页眉 {removed['页眉']} 行、独立页码 {removed['页码']} 行;另有行尾/全角/词间空格全量清理 |",
f"| B 断行错位接合 | {cnt('B断行接合')} | 段落被页眉打断处复原成整段 |",
f"| C 形近错字/异体字 | {cnt('C形近错字')} | 每条附核校依据,见下逐节明细 |",
f"| E 半角标点规范化 | {cnt('E标点规范化')} | ASCII , ; : ( ) 邻接汉字时改全角 |",
f"| F 存疑(未改原文) | {cnt('F存疑')} | 逐条列出疑正与核校来源,见 `人工核对清单.md` |", "",
"### 各节清理量一览", "",
"| 节 | 行范围 | 药物条目 | B 接合 | C 错字 | E 标点 | F 存疑 |",
"|----|--------|---------|--------|--------|--------|--------|"]
for s in secs:
evs = per_sec[s["标题"]]
c = lambda t: sum(1 for e in evs if e["类型"] == t)
rep.append(f"| {s['标题']} | {s['起始行']}–{s['结束行']} | {len(s['药物条目'])} | "
f"{c('B断行接合')} | {c('C形近错字')} | {c('E标点规范化')} | {c('F存疑')} |")
rep += ["", "---", "", "## 1 逐节核对明细(按章 / 节列明)", "",
f"### {ch['标题']}(清洗版行 {ch['起始行']}–{ch['结束行']})", ""]
for s in secs:
evs = per_sec[s["标题"]]
gate = f"味门{s['味门']}·{s['本脏']}{s['本行']}·法{s['脏象法象']}" if s.get("味门") else "从火土同治论心病(草木方例用药简释)"
rep += [f"#### {s['标题']}", "",
f"- 定位:清洗版行 {s['起始行']}–{s['结束行']};{gate};药物条目 {len(s['药物条目'])} 味",
f"- 本味药:{'、'.join(e['药名'] for e in s['药物条目'])}",
f"- 清理量:B 接合 {sum(1 for e in evs if e['类型']=='B断行接合')} / "
f"C 错字 {sum(1 for e in evs if e['类型']=='C形近错字')} / "
f"E 标点 {sum(1 for e in evs if e['类型']=='E标点规范化')} / "
f"F 存疑 {sum(1 for e in evs if e['类型']=='F存疑')}", ""]
if evs:
rep += ev_table(evs) + [""]
else:
rep += ["- 本节无需修正条目。", ""]
rep += ["---", "", "## 2 章节结构(清洗版行号)", "",
"| 章 | 节 | 味门 | 本脏/行/时 | 起始行 | 结束行 | 药物条目 | 附属标题 |",
"|----|----|------|-----------|--------|--------|---------|---------|"]
for s in secs:
gate = f"{s.get('本脏','—')}/{s.get('本行','—')}/{s.get('脏象法象','—')}" if s.get("味门") else "—"
rep.append(f"| {ch['标题']} | {s['标题']} | {s.get('味门') or '—'} | {gate} | {s['起始行']} | {s['结束行']} | "
f"{len(s['药物条目'])} | {';'.join(s['附属标题']) or '—'} |")
rep += ["", "## 3 缺失内容登记(来源不完整,非错字问题)", "",
"| 项 | 情况 | 位置 | 处理 |", "|----|------|------|------|",
f"| 悬空图片引用 | {len(missing_assets)} 处(OCR 流水线遗留,图片本体未随源文件提供) | "
+ ";".join(f"行 {m['行']}({m['文件名'][:12]}…)" for m in missing_assets) + " | 原文保留引用,待补图 |",
"| 脚注正文 | 正文存 ① ② 等标记 5 处(桂枝、人参、升麻、白醪法 2 处),脚注正文未随文录入 | 见上方逐节明细 F 类 | 待补后回填 |",
"| 第二章及以后各章 | 本次来源仅第一章「五脏补泻草木方例 用药释义」 | — | 待补 |",
"| 附录一至四 | 目录列有:整订稿/略论《汤液经法》与张仲景论著的关系/张大昌先生《处方正范》/张大昌先生《三十六脉名义略述》 | 来源文件首 3 行 | 待补 |", "",
"## 4 药物条目清单(章节 → 药名 → 五行互含名位,原样保留)", "",
"| 节 | 序 | 药名 | 五行互含名位 | 清洗版行范围 |", "|----|----|------|-------------|-------------|"]
for s in secs:
for k, e in enumerate(s["药物条目"], start=1):
rep.append(f"| {s['标题']} | {k} | {e['药名']} | {e['五行互含名位'] or '—'} | {e['起始行']}–{e['结束行']} |")
rep += ["", "> 说明:标题中的「五行互含名位」为原书表述(如「生姜木中火、干姜木中水」),清洗阶段原样保留,不作拆分或解释。"]
wf(OUT_REPORT, "\n".join(rep) + "\n")
chk = ["# 人工核对清单 — 《辅行诀五脏用药法要》药性探真 第一章", "",
"> 用途:需对照纸质原书/影印本确认的条目。核对清单**按章/节列明**,本阶段不改动清洗版正文。",
"> 清洗流程与全部修正记录见 `文本清洗报告.md`。", "",
f"## {ch['标题']}", ""]
for s in secs:
evs = [e for e in per_sec[s["标题"]] if e["类型"] == "F存疑"]
chk += [f"### {s['标题']}(行 {s['起始行']}–{s['结束行']})", ""]
if evs:
chk += ["| 行 | 片段 | 疑正 / 核校来源 |", "|----|------|----------------|"]
for e in evs:
chk.append(f"| {e['行']} | {e['原'].replace('|','|')} | {e['依据']} |")
else:
chk.append("- 无存疑项。")
chk.append("")
chk += ["---", "", "## 缺失内容登记(本次来源不完整,非错字问题)", "",
"| 项 | 情况 | 处理 |", "|----|------|------|",
f"| 悬空图片引用 | {len(missing_assets)} 处:"
+ ";".join(f"行 {m['行']} `{m['文件名']}`" for m in missing_assets)
+ "(OCR 流水线遗留,图片本体未随源文件提供) | 待补图,补后替换为真实路径 |",
"| 脚注正文 | 正文存 ① ② 等标记 5 处(桂枝、人参、升麻、白醪法 2 处),脚注正文未随文录入 | 待补后回填 |",
"| 第二章及以后各章 | 本次来源仅第一章「五脏补泻草木方例 用药释义」 | 待补 |",
"| 附录一至四 | 目录列有:整订稿/略论《汤液经法》与张仲景论著的关系/张大昌先生《处方正范》/张大昌先生《三十六脉名义略述》 | 待补 |",
"| 表格 | 第一章正文不含表格 | — |",
"| 图版 | 正文有 2 处图片引用,图片本体缺失(见上「悬空图片引用」) | 如有影印本可另补 |", "",
"## 书目信息(供核对)", "",
"- 书名:《辅行诀五脏用药法要》药性探真",
"- 著者:衣之镖 撰著;丛书:张大昌先生弟子个人专著",
"- 出版:学苑出版社,2014 年 1 月第 1 版,ISBN 978-7-5077-4414-9",
"- 本次来源:`01_来源数据/药性探真_第一章_原始_20260915.md`(仅第一章)"]
wf(OUT_CHECK, "\n".join(chk) + "\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("核对单:", OUT_CHECK, os.path.getsize(OUT_CHECK), "bytes")
print(f"页眉/页码删除 {removed} | B接合 {cnt('B断行接合')} | C错字 {cnt('C形近错字')} | "
f"E标点 {cnt('E标点规范化')} | F存疑 {cnt('F存疑')}")
print(f"节 {len(secs)} 个 / 药物条目 {n_drug} 味")
for s in secs:
evs = per_sec[s["标题"]]
print(f" {s['标题']} 行{s['起始行']}–{s['结束行']} 药物{len(s['药物条目'])} "
f"| 清理项 {len(evs)} 条:" + "、".join(f"{e['类型'][:1]}@{e['行']}" for e in evs))
if __name__ == "__main__":
main()
@@ -0,0 +1,328 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
辅行诀五脏用药法要-药性探真 资料库 ETL 第 2 步
01_来源数据/辅行诀脏腑用药法要_古本原文_20260915.md -> 02_加工数据/
用途:把用户提供的《辅行诀脏腑用药法要》古本原文导入资料库,作为全书原文依据参考。
本阶段仍只做数据清理,不做释义抽取 / 跨库关联 / 主题分析。
清理项:
A 版式残留:全角缩进(U+3000)、行尾空格、词间半角空格
B 校勘符号规范化:\\[X\\] -> 〔X〕(Markdown 转义残留 -> 传统校补号)
C 讹字/衍字修正:每条附核校依据(参校本=维基文库本;内证=本书条文自证)
D 标点规范化:重复标点、缺失括号、
E 存疑登记:原文不改,逐条附疑正与参校本异文
F 版本差异/缺文登记:与参校本结构不同处,不径改
输出:
02_加工数据/辅行诀脏腑用药法要_清洗版.md
02_加工数据/原文清洗报告.md (核对数据按篇/条列明)
02_加工数据/原文人工核对清单.md (按篇列明存疑 + 缺文与版本差异登记)
02_加工数据/原文结构_上下卷.json (卷 / 篇 / 方 三级 + 行号锚定)
幂等:可重复运行,输出覆盖生成。
"""
import os, re, json, hashlib, sys
BASE = os.path.expanduser("~/Documents/ai_agent_scraper_study/data/辅行诀五脏用药法要-药性探真")
SRC = os.path.join(BASE, "01_来源数据/辅行诀脏腑用药法要_古本原文_20260915.md")
REF_DIR = os.path.join(BASE, "01_来源数据/校核参考")
OUT_MD = os.path.join(BASE, "02_加工数据/辅行诀脏腑用药法要_清洗版.md")
OUT_REPORT = os.path.join(BASE, "02_加工数据/原文清洗报告.md")
OUT_CHECK = os.path.join(BASE, "02_加工数据/原文人工核对清单.md")
OUT_STRUCT = os.path.join(BASE, "02_加工数据/原文结构_上下卷.json")
REF_NAME = "维基文库本《辅行诀脏腑用药法要》"
# C 类:讹字 / 衍字修正(依据=参校本 + 内证;逐条留痕)
FIXES = [
("谨钭五脏", "谨将五脏", f"{REF_NAME}作「谨将」;钭/将形近致误"),
("耳有所闻", "耳无所闻", f"{REF_NAME}作「耳无所闻」;《素问·脏气法时论》「虚则目䀮䀮无所见,耳无所闻」"),
("大枣(十二个", "大枣(十二枚", f"{REF_NAME}作「十二枚」;大枣量词作枚"),
("薯蓣,;当从", "薯蓣,当从", "衍分号"),
("病心中前", "病心中痛", f"{REF_NAME}作「病心中痛」;《灵枢·五邪》「邪在心,则病心痛」"),
("崦调之", "而调之", f"{REF_NAME}作「而调之」;崦/而形近致误"),
("以白浆一斗煮取四或", "以白浆一斗煮取四升", f"{REF_NAME}作「煮取四升」;或/升形近致误(白浆/白酨浆之异文见存疑)"),
("每服二或", "每服二升", f"{REF_NAME}作「每服二升」;或/升形近致误"),
("眩中澹澹大动", "心中澹澹大动", f"{REF_NAME}作「心中澹澹大动」;眩/心形近致误"),
("以不八升", "以水八升", f"{REF_NAME}作「以水八升」;不/水形近致误"),
("怔惊不发者", "怔惊不安者", f"{REF_NAME}作「不安」;与下条「懊憹不安」同例"),
("懊憹不发", "懊憹不安", f"{REF_NAME}作「懊憹不安」;发/安形近致误"),
("殓泄", "飧泄", f"{REF_NAME}作「飧泄」;《素问》「脾实则腹满,飧泄」"),
("煮取二勤务员,温升再服", "煮取二升,温分再服", f"{REF_NAME}作「煮取二升,温分再服,日二」;OCR混入「勤务员」衍文,升/分互讹"),
("治烦热汗邮", "治烦热汗出", f"{REF_NAME}作「汗出」;邮/出形近致误"),
("取之勇泉", "取之涌泉", f"{REF_NAME}作「涌泉」;《灵枢·五邪》「取之涌泉、昆仑」"),
("肾甘燥", "肾苦燥", f"{REF_NAME}作「肾苦燥」;与「肝苦急、心苦缓、脾苦湿、肺苦气上逆」同例"),
("时足胫有者方", "时足胫肿者方", f"{REF_NAME}作「时足胫肿」;有/肿形近致误"),
("加伏龙肝如鸡子在", "加伏龙肝如鸡子大", f"{REF_NAME}作「如鸡子大」;在/大形近致误"),
("渐分四服", "温分四服", f"{REF_NAME}作「温分四服」;煎服法常语作「温分」"),
("止方五味", "上方五味", f"{REF_NAME}作「上方五味」;止/上形近致误"),
("以水五升,称煮椒", "以水五升,先煮椒", f"{REF_NAME}作「先煮椒」;称/先形近致误"),
("韭叶、区药", "韭叶、芍药", f"{REF_NAME}作「韭叶、芍药」;区/芍形近致误"),
("温分三他", "温分三服", f"{REF_NAME}作「温分三服」;他/服形近致误"),
("渐分三他", "温分三服", f"{REF_NAME}作「温分三服」;渐/温、他/服并误"),
("羸唐如柴", "羸瘦如柴", f"{REF_NAME}作「羸瘦如柴」;唐/瘦形近致误"),
("竹味(三把)", "竹叶(三把)", f"{REF_NAME}作「竹叶」;味/叶形近致误"),
("竹味为水", "竹叶为水", f"{REF_NAME}作「竹叶为水」;味/叶形近致误"),
("五茶为充", "五菜为充", f"{REF_NAME}作「五菜为充」;《素问·脏气法时论》「五菜为充」"),
("五畜为益,,尔乃", "五畜为益,尔乃", "衍顿号"),
("补有汤内加羊肝", "补肝汤内加羊肝", f"{REF_NAME}作「补肝汤内加羊肝」;有/肝形近致误"),
("亦百十十首", "亦百二十首", f"{REF_NAME}作「亦百二十首」;十/二形近致误"),
("可黩契经方", "可默契经方", f"{REF_NAME}作「可默契经方」;黩/默形近致误"),
("区药为土", "芍药为土", f"{REF_NAME}作「芍药为土」;区/芍形近致误"),
("身热之自愈也", "身热去自愈也", f"{REF_NAME}作「身热去自愈」;之/去形近致误"),
("黄芩(五两)人参桂枝生姜", "黄耆(五两)人参桂枝生姜", f"{REF_NAME}作「黄耆五两」;本篇末「阳旦者,升阳之方,以黄芪为主」自证"),
("煮取四或,去滓", "煮取四升,去滓", f"{REF_NAME}作「煮取四升」;或/升形近致误"),
("一斗二升,,煮取六升", "一斗二升,煮取六升", "衍顿号"),
("治天行热痛", "治天行热病", f"{REF_NAME}作「治天行热病」;本书他篇亦作「天行热病」"),
("皂角刮去皮絃", "皂角刮去皮弦", f"{REF_NAME}作「皮弦」;絃/弦异体,取通行字"),
("鸡子黄二枚)", "鸡子黄(二枚)", "夺前括号,据下文「阿胶(三锭)」例补"),
("内胶,列上火", "内胶,更上火", f"{REF_NAME}作「更上火」;列/更形近致误"),
("蠃瘦如柴", "羸瘦如柴", f"{REF_NAME}作「羸瘦如柴」;蠃/羸形近致误"),
("更上炎", "更上火", f"{REF_NAME}作「更上火」;炎/火形近致误"),
("附了(一枚", "附子(一枚", f"{REF_NAME}作「附子」;了/子形近致误"),
("少腹中冷嘲热讽", "少腹中冷", f"{REF_NAME}作「少腹中冷」;「嘲热讽」为成语混入之衍文"),
("日三夜一他", "日三夜一服", f"{REF_NAME}作「日三、夜一服」;他/服形近致误"),
("以鸡了黄为主", "以鸡子黄为主", f"{REF_NAME}作「鸡子黄」;了/子形近致误"),
("手足遂冷", "手足逆冷", f"{REF_NAME}作「手足逆冷」;遂/逆形近致误"),
("扑克其人", "看其人", f"{REF_NAME}作「看其人」;扑克/看形近致误"),
("得大吐鲁番即愈", "得大吐即愈", f"{REF_NAME}作「得大吐即愈」;「鲁番」为衍文"),
("少时小便通,愈)", "少时小便通,立愈)", f"{REF_NAME}有「立」字;夺文"),
(r"枚,炮\]干姜", "枚,炮)干姜", f"原文「\\]」为「)」之误;{REF_NAME}作「附子(一枚,炮)干姜」"),
("《《金匮要略》", "《金匮要略》", "书名号重复"),
]
# E/F 类:存疑与版本差异(不改原文)
SUSPECT_RULES = [
(r'白浆', "参校本作「白酨浆」,本书及集成网本作「白浆」,本书校注亦称「白浆」——异文待定"),
(r'()()为水', "二十五味药精「味辛」条缺字,参校本作「附子(一本此二字不明」;本书缺字未标"),
(r'视有余者尽取之', "参校本作「视有馀血者尽取之」,疑夺「血」字(《灵枢·五邪》「视有血者尽取之」)"),
(r'上七味,以水一斗,温服一升', "参校本亦同,疑夺「煮取」句(如「煮取四升」)"),
(r'绞去滓,顿。', "疑夺「服」字(参校本作「顿服」)"),
(r'日数传,生死', "参校本作「日数传变」,疑夺「变」字"),
(r'耎意为祛弱', "本书校注;疑当作「耎意为软弱」(《说文》「耎,弱也」)"),
(r'气结\n', "泻肺汤方证末夺「方:」(参校本作「气结方。」)"),
(r'汗出不止\n', "泻肾汤方证末夺「方:」(参校本作「汗出不止方。」)"),
(r'鸡子黄二枚)阿胶|黄连(四两)芍药(各二两)', "小朱鸟汤、大朱鸟汤药味疑夺「黄芩」(本方煎法作「先煮连、芩、芍三物」;参校本有黄芩)"),
(r'^弘景曰:“外感天行', "此段起首引号未收(参校本此段不用引号);原文不改,待核影印本"),
]
# F 类:结构缺题/版本差异(登记,不改)
VERSION_NOTES = [
("篇题缺失", "「辨心包络病证文并方」篇题(参校本有;本书与集成网本皆无,心胞条文直接接于心脏病证篇后)"),
("篇题缺失", "「五脏泻方」篇题(本书以「陶曰:又有泻方五首」起;参校本有篇题)"),
("篇题缺失", "「二旦六神大小汤」篇题(本书直接以「弘景曰:外感天行……」起;参校本有篇题)"),
("篇题缺失", "「救五脏中恶卒死方」篇题(本书以「陶经隐居云:中恶卒死者……」起;参校本有篇题)"),
("编排差异", "心胞补方编排:本书「小补心汤」=治胸痹心中痞满(栝蒌薤白半夏厚朴桂枝);参校本作「大补心汤」,另立「小补心汤」=治胸痹不得卧(栝蒌薤白半夏)"),
("内容节略", "大泻心汤煎服法节略:参校本有「上六味,以酢六升,先煮上五味,得三升许,去滓。内戎盐…」全法,本书仅存「上六味,服一升,当大吐……」"),
("图版缺失", "《汤液经法图》及其图注(「左阳进为补,其数七,火数也;右阴退为泻,其数六,水数也」)本书无图无注;参校本存图(SVG)与图注"),
("异体不校", "着/著(着舌、着散、着汤)、纳/内、郄/郤、憹/𢙐、芪/耆 等异体字,两本互见,本阶段不校"),
]
def clean(text):
lines = text.split("\n")
events, out = [], []
for idx, raw in enumerate(lines, start=1):
line = raw.replace("\u3000", "").rstrip() # A 全角缩进
if line != "" and not re.match(r'^梁[·・]?华阳隐居', line):
prev = None
while prev != line:
prev = line
line = re.sub(r'(?<=[\u4e00-\u9fff])\s+(?=[\u4e00-\u9fff])', '', line)
line = re.sub(r'(?<=[\u4e00-\u9fff])\s+(?=[,。、;:?!“”()《》])', '', line)
line = re.sub(r'(?<=[,。、;:?!“”()《》])\s+(?=[\u4e00-\u9fff])', '', line)
# B 校勘符号
b = re.sub(r'\\\[([^\]\\]*)\\\]', r'〔\1〕', line)
if b != line:
events.append(dict(行=idx, 类型="B校勘符号", 原="\\[…\\]", 改="〔…〕",
依据="Markdown 转义残留 → 传统校补号"))
line = b
# C 讹字
for bad, good, why in FIXES:
if bad != good and bad in line:
events.append(dict(行=idx, 类型="C讹字修正", 原=bad, 改=good, 依据=why))
line = line.replace(bad, good)
# D 标点
d = re.sub(r'(,,|;;|。。)', lambda m: m.group(1)[0], line)
if d != line:
events.append(dict(行=idx, 类型="D标点", 原="重复标点", 改="单一标点", 依据="重复标点衍文"))
line = d
out.append(line)
cleaned = re.sub(r'\n{3,}', '\n\n', "\n".join(out)).strip() + "\n"
# E 存疑登记(在清洗后文本上定位)
suspects = []
clines = cleaned.split("\n")
for i, l in enumerate(clines, start=1):
for pat, why in SUSPECT_RULES:
if re.search(pat, l):
suspects.append({"行": i, "片段": l[:60], "说明": why})
seen, uniq = set(), []
for s in suspects:
k = (s["行"], s["说明"])
if k not in seen:
seen.add(k); uniq.append(s)
return cleaned, events, uniq
SECTIONS = [
(r'^辅行诀脏腑用药法要$', "上卷·总题"),
(r'^梁·华阳隐居', "上卷·撰者"),
(r'^辨肝脏病证文并方', "上卷·辨肝脏病证文并方"),
(r'^辨心脏病证文并方', "上卷·辨心脏病证文并方"),
(r'^心胞气实者', "上卷·心胞病证文(篇题缺)"),
(r'^辨脾脏病证文并方', "上卷·辨脾脏病证文并方"),
(r'^辨肺脏病证候文并方', "上卷·辨肺脏病证候文并方"),
(r'^辨肾脏病证文并方', "上卷·辨肾脏病证文并方"),
(r'^陶曰:又有泻方五首', "上卷·五脏泻方(篇题缺)"),
(r'^陶云:经方有救诸劳损病方', "下卷·救五脏诸劳损病方(引)"),
(r'^辅行诀脏腑用药法要(下)', "下卷·总题"),
(r'^养生补肝汤', "下卷·救五脏诸劳损病方"),
(r'^陶云:经云:毒药攻邪', "下卷·汤液经法(引)"),
(r'^味辛皆属木', "下卷·汤液经法(二十五味药精)"),
(r'^弘景曰:“外感天行|^弘景曰:外感天行', "下卷·二旦六神大小汤(篇题缺)"),
(r'^陶经隐居云:中恶卒死者', "下卷·救五脏中恶卒死方(篇题缺)"),
(r'^校注:$', "下卷·校注"),
]
def parse_struct(text):
lines = text.split("\n")
nodes = []
for i, l in enumerate(lines, start=1):
s = l.strip()
for pat, name in SECTIONS:
if re.match(pat, s):
nodes.append({"篇": name, "起始行": i})
break
for n, node in enumerate(nodes):
node["结束行"] = nodes[n + 1]["起始行"] - 1 if n + 1 < len(nodes) else len(lines)
node["条文数"] = sum(1 for i in range(node["起始行"], node["结束行"] + 1)
if lines[i - 1].strip())
return nodes
def main():
raw = open(SRC, encoding="utf-8").read()
cleaned, events, suspects = clean(raw)
nodes = parse_struct(cleaned)
def wf(path, content):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(content); f.flush(); os.fsync(f.fileno())
wf(OUT_MD, cleaned)
struct = {
"来源文件": os.path.basename(SRC), "来源文件md5": hashlib.md5(raw.encode()).hexdigest(),
"清洗版文件": os.path.basename(OUT_MD), "清洗版md5": hashlib.md5(cleaned.encode()).hexdigest(),
"参校本": REF_NAME,
"说明": "行号均指清洗版;本文件仅作清理配套定位索引,未做释义抽取与跨库关联",
"篇": nodes,
}
wf(OUT_STRUCT, json.dumps(struct, ensure_ascii=False, indent=2) + "\n")
def owner(line):
for n in nodes:
if n["起始行"] <= line <= n["结束行"]:
return n["篇"]
return "(篇外)"
per = {}
for e in events:
per.setdefault(owner(e["行"]), []).append(e)
per_sus = {}
for s in suspects:
per_sus.setdefault(owner(s["行"]), []).append(s)
cnt = lambda t: sum(1 for e in events if e["类型"].startswith(t))
rep = ["# 《辅行诀脏腑用药法要》古本原文 — 清洗报告", "",
"> 本库现阶段只做数据清理。本文本作为全书**原文依据参考**入库,未做释义抽取、"
"跨库关联与主题分析(用户 2026-09-15 指令)。", "",
"## 0 概览", "",
f"- 来源:`01_来源数据/{os.path.basename(SRC)}`(md5 `{struct['来源文件md5']}`)",
f"- 清洗版:`02_加工数据/{os.path.basename(OUT_MD)}`(md5 `{struct['清洗版md5']}`)",
f"- 参校本:{REF_NAME}(另存 `01_来源数据/校核参考/`,仅作清理核校用)",
f"- 字符数:{len(raw)} → {len(cleaned)};行数:{raw.count(chr(10))} → {cleaned.count(chr(10))}",
f"- 结构:上卷 + 下卷,共 {len(nodes)} 个可识别篇段", "",
"| 清理类别 | 处数 | 说明 |", "|---------|------|------|",
f"| A 版式残留 | 全量 | 全角缩进({sum(1 for l in raw.split(chr(10)) if l.startswith(chr(0x3000)))} 行)、行尾空格、词间半角空格 |",
f"| B 校勘符号规范化 | {cnt('B')} | `\\[X\\]` → `〔X〕`(补字保留语义,只去 Markdown 转义)|",
f"| C 讹字/衍字修正 | {cnt('C')} | 每条附核校依据(参校本 + 内证),见下方逐篇明细 |",
f"| D 标点规范化 | {cnt('D')} | 重复标点、缺失括号 |",
f"| E 存疑(未改原文) | {len(suspects)} | 逐条附疑正与参校本异文 |",
f"| F 版本差异/缺文(未改) | {len(VERSION_NOTES)} | 结构、编排、图版差异,不作校改 |", "",
"### 各篇清理量一览", "",
"| 篇 | 行范围 | 条文行 | B | C | D | E 存疑 |", "|----|--------|--------|---|---|---|--------|"]
for n in nodes:
evs = per.get(n["篇"], [])
c = lambda t: sum(1 for e in evs if e["类型"].startswith(t))
rep.append(f"| {n['篇']} | {n['起始行']}–{n['结束行']} | {n['条文数']} | {c('B')} | {c('C')} | {c('D')} | "
f"{len(per_sus.get(n['篇'], []))} |")
rep += ["", "---", "", "## 1 逐篇核对明细(按卷 / 篇列明)", ""]
for n in nodes:
evs = per.get(n["篇"], [])
sus = per_sus.get(n["篇"], [])
rep += [f"### {n['篇']}(清洗版行 {n['起始行']}–{n['结束行']})", "",
f"- 清理量:B {sum(1 for e in evs if e['类型'].startswith('B'))} / "
f"C {sum(1 for e in evs if e['类型'].startswith('C'))} / "
f"D {sum(1 for e in evs if e['类型'].startswith('D'))} / 存疑 {len(sus)}", ""]
if evs:
rep += ["| 行 | 类型 | 原文 | 处理后 | 核校依据 |", "|----|------|------|--------|---------|"]
for e in evs:
rep.append(f"| {e['行']} | {e['类型']} | {e['原'].replace('|','|')} | "
f"{e['改'].replace('|','|')} | {e['依据']} |")
rep.append("")
if sus:
rep += ["存疑项:", "", "| 行 | 片段 | 说明 |", "|----|------|------|"]
for s in sus:
rep.append(f"| {s['行']} | {s['片段'].replace('|','|')} | {s['说明']} |")
rep.append("")
if not evs and not sus:
rep += ["- 本篇无需修正条目。", ""]
rep += ["---", "", "## 2 版本差异 / 缺文登记(不校改)", "",
"| 类别 | 说明 |", "|------|------|"]
for k, v in VERSION_NOTES:
rep.append(f"| {k} | {v} |")
rep += ["", "## 3 篇段结构(清洗版行号)", "",
"| 卷·篇 | 起始行 | 结束行 | 条文行 |", "|-------|--------|--------|--------|"]
for n in nodes:
rep.append(f"| {n['篇']} | {n['起始行']} | {n['结束行']} | {n['条文数']} |")
wf(OUT_REPORT, "\n".join(rep) + "\n")
chk = ["# 原文人工核对清单 — 《辅行诀脏腑用药法要》古本原文", "",
"> 用途:需对照纸质原书/敦煌本影印本确认的条目。**按卷/篇列明**,本阶段不改动清洗版正文。",
"> 参校本:维基文库本《辅行诀脏腑用药法要》(存 `01_来源数据/校核参考/`)。", ""]
for n in nodes:
sus = per_sus.get(n["篇"], [])
chk += [f"## {n['篇']}(行 {n['起始行']}–{n['结束行']})", ""]
if sus:
chk += ["| 行 | 片段 | 疑正 / 参校本异文 |", "|----|------|------------------|"]
for s in sus:
chk.append(f"| {s['行']} | {s['片段'].replace('|','|')} | {s['说明']} |")
else:
chk.append("- 无存疑项。")
chk.append("")
chk += ["---", "", "## 版本差异 / 缺文登记(不校改,待核影印本)", "",
"| 类别 | 说明 |", "|------|------|"]
for k, v in VERSION_NOTES:
chk.append(f"| {k} | {v} |")
chk += ["", "## 书目与来源", "",
"- 文本:《辅行诀脏腑用药法要》(梁·华阳隐居 陶弘景撰;敦煌遗书系传本)",
"- 本次来源:`01_来源数据/辅行诀脏腑用药法要_古本原文_20260915.md`(用户 2026-09-15 提供)",
"- 参校本:维基文库本(另有集成网本,与本来源同系,讹字位置一致)",
"- 参校本快照:`01_来源数据/校核参考/`"]
wf(OUT_CHECK, "\n".join(chk) + "\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("核对单:", OUT_CHECK, os.path.getsize(OUT_CHECK), "bytes")
print(f"B {cnt('B')} | C {cnt('C')} | D {cnt('D')} | 存疑 {len(suspects)} | 版本差异 {len(VERSION_NOTES)}")
for n in nodes:
evs = per.get(n["篇"], [])
print(f" {n['篇']} 行{n['起始行']}–{n['结束行']} 条文{n['条文数']} 清理项 {len(evs)}")
if __name__ == "__main__":
main()
@@ -0,0 +1,638 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
辅行诀五脏用药法要-药性探真 资料库 ETL 第 3 步(2026-09-16)
01_来源数据/辅行诀五脏用药法要.md(印刷书籍扫描本) -> 02_加工数据/
定位(用户 2026-09-16 指令):
本印刷本**优于**此前入库的古本原文(集成网系电子本)与维基文库参校本,
定为《辅行诀五脏用药法要》的**基础文本(底本)**;
参校本只在判断扫描讹字、脱字、脱分隔时作辅助证据,不作版本改写。
竖排本作「右X味」,横排/网络本作「上X味」——**以本书为准,一律不改**。
清理项:
A 版式残留:行首行尾空格、句中误入空格、LaTeX(PDF 公式化)残留、本地图片路径残留
B 断行/脱分隔:换页换行接合(20 处,行号锚定)、药味行脱分隔空格(4 处)、药味行与煎服法误并(2 处)
C 讹字修正:每条附核校依据(本书内证/参校本/同类文献)
D 字符规范化:半角标点 -> 全角
E 存疑登记:原文不改,逐条附疑正与参校本异文
F 版本差异/体例登记:本书特有内容与用字体例,不作校改
输出:
02_加工数据/辅行诀五脏用药法要_基础文本_清洗版.md
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_CHECK = os.path.join(BASE, "02_加工数据/印刷本人工核对清单.md")
OUT_STRUCT = os.path.join(BASE, "02_加工数据/印刷本结构_上下卷.json")
REF_NAME = "维基文库本《辅行诀脏腑用药法要》(参校本,仅作查错补漏)"
OLD_NAME = "集成网系《辅行诀脏腑用药法要》古本原文(旧入库参考本)"
# ── B 类:换页/换行断行接合(行号锚定;值为 (接合符, 该行原文锚))─────────
JOINS = {
48: (" ", "竹叶各一两"),
52: ("", "土各一两"),
60: ("", "下血者,其变刺郄中血者。"),
94: ("", "癫仆。"),
112: ("", "烦躁,汗出,气噫,脉结者方。"),
124: ("", "者方。"),
154: ("", "得,或下利不止,足痿不收,肢冷脉微者方。"),
157: ("", "各一两"),
187: ("", "厥阴内血者。"),
224: (" ", "甘草炙,各一两"),
269: ("", "各一两"),
275: ("", "一两"),
388: ("", "捣碎"),
397: ("", "去核"),
406: ("", "去核"),
410: ("", "一服。"),
495: (" ", "半夏半升 干姜三两"),
557: ("", "活之。"),
580: ("", "硷各三两"),
}
# ── B 类:药味行与煎服法误并一行,须在指定标记前拆行(行号锚定)──────────
SPLITS = {32: "右六味", 108: "右六味", 252: "右方六味", 345: "右五味"}
# ── A 类:LaTeX(PDF 公式化)残留 -> 药味行(行号锚定,逐块核读)──────
# (起始行, 结束行, 还原文本, 说明, 结束行锚)
LATEX_BLOCKS = [
(381, 383, "石绿三两 芒硝六两 雄黄三两",
"小养生补肝汤散金石方,据药名与用量还原(药行「各三两/各六两」体例:君六两、余三两)",
"\\mathrm{雄黄三两}"),
(412, 414, "凝水石三两 丹砂六两 曾青三两",
"小凝息补肺汤散金石方,据药名与用量还原", "曾青三两"),
(418, 420, "人参三两 附子二大枚,炮 竹叶三两 薤白三两 苦杏七枚,去核擘",
"小固元补肾汤散草木方,双栏数组(\\begin{array})还原;「去核擘」为炮制注,归属待核(见存疑)",
"苦杏七枚"),
(424, 426, "赤石脂三两 阳起石六两 白垩土三两",
"小固元补肾汤散金石方,据药名与用量还原", "白垩土三两"),
]
# ── A 类:单行替换(图版引用等),行号锚定 ─────────────────────────────
LINE_REPLACE = {
458: ("2026-09-16-10-28-06-image.png",
"> 〔图版〕此处为《汤液经法图》(原书图版)。本电子本仅存本地路径引用"
"(`…marktext/images/…png`,图本体未随文提供),图与图注均缺,待补。"),
}
# ── A 类:句中误入空格(逐条定位,药味行分隔空格一律保留)──────────────
STRAY_SPACES = [
("诸 凡杂病", "诸凡杂病"),
("梁·华阳隐居陶弘景 撰", "梁·华阳隐居 陶弘景 撰"),
("胸中 苦闷痛者", "胸中苦闷痛者"),
("头痛 者,加桂心二两", "头痛者,加桂心二两"),
("疗治明 悉", "疗治明悉"),
("阴旦者,扶 阴之方", "阴旦者,扶阴之方"),
]
# ── A 类:药精条分隔规范化(味辛条脱分隔,其余四条有分隔;体例统一)──────
JING_SEP = [
("味辛皆属木,桂琅玕为之主。生姜伏龙肝为火;附子阳起石为土;细辛礜石为金;干姜雄黄为水。",
"味辛皆属木,桂 琅玕为之主。生姜 伏龙肝为火;附子 阳起石为土;细辛 礜石为金;干姜 雄黄为水。",
"同书「味咸/甘/酸/苦」四条皆以空格分列草木—金石配对,本条空格脱失,据体例补"),
]
# ── B 类:药味行脱分隔空格补入 ────────────────────────────────────────
SEP_FIXES = [
("薯蓣 旋覆花竹叶各一两", "薯蓣 旋覆花 竹叶各一两",
"右七味而药味行仅六词,「旋覆花」「竹叶」间脱分隔空格"),
("细辛 竹叶甘草炙,各一两", "细辛 竹叶 甘草炙,各一两",
"右七味而药味行仅六词,「竹叶」「甘草炙」间脱分隔空格"),
("五味子半升半夏半升", "五味子半升 半夏半升",
"右八味而药味行仅七词,「五味子半升」「半夏半升」间脱分隔空格"),
("皂矾 石英雄黄各一两", "皂矾 石英 雄黄各一两",
"右七味金石方,「石英」「雄黄」间脱分隔空格;本书药精表「甘草炙 石英为火」「干姜 雄黄为水」,"
"与草木行(甘草炙、干姜)对位正合"),
]
# ── C 类:讹字修正(依据逐条留痕;本书为主,参校本仅作旁证)─────────────
FIXES = [
("心德在奐", "心德在耎",
f"本书五脏德性并列(肝德在散、脾德在缓、肺德在收、肾德在坚)皆一字形容,"
f"「奐」「耎」形近;{REF_NAME}作「心德在耎」;《素问·脏气法时论》「心欲软」,耎即软弱之义"),
("腑善瘈", "胻善瘛",
f"《灵枢·五邪》「邪在肝……胻善瘛,节时肿」,本书下文正作「节时肿」「脚下痛」;"
f"「腑」为「胻」之形近讹字(同月旁,府/行形近),胻即胫;{OLD_NAME}作「善瘛」而夺「胻」"),
("懊侬", "懊憹",
f"「懊憹」为古医书联绵词,本书前后两处并见,{REF_NAME}亦作「懊憹」;「侬」「憹」形近致误"),
("白戴浆", "白酨浆",
f"{REF_NAME}作「白酨浆」;《金匮要略》瓜蒌薤白半夏汤用「白酒」,"
f"《外台秘要》谓白酒即白酨浆,「酨」为古酒浆名,「戴」形近致误"),
("白截浆", "白酨浆",
f"{REF_NAME}作「白酨浆」(本书他处亦作「白酨浆」),「截」「酨」形近致误"),
("磐石", "礜石",
f"本书药精表金石配对作「细辛 礜石为金」;《本经》礜石味辛大热,正合木中金之位。"
f"同一药本书三处异形(磐石/舜石/碧石),互校定为「礜石」,「磐」形近致误"),
("舜石", "礜石",
f"小补肺汤散金石方与草木方「细辛」对位,据本书药精表「细辛 礜石为金」;「舜」形近致误"),
("碧石", "礜石",
f"大补肺汤散金石方与草木方「细辛」对位,同上;「碧」形近致误"),
("黄蓍", "黄耆",
f"「蓍」为蓍草,非入药;{REF_NAME}作「黄耆」;本书下文「以黄耆为主」用字自证"),
("大朱乌汤", "大朱鸟汤",
f"本书上下篇并见「小朱鸟汤」「朱鸟者,清滋之方」,{REF_NAME}作「大朱鸟汤」;"
f"「鸟」「乌」形近致误"),
("皂角刮去皮絃", "皂角刮去皮弦",
f"{REF_NAME}作「皮弦」;「絃」为「弦」之异体,取通行字(与旧入库本同例)"),
# —— 以下 4 条据用户 2026-09-16 核对原书结果回填 ——
("虚则目眩,无所见", "虚则目䀮䀮无所见",
"**用户核对原书**:当作「目䀮䀮无所见」。《素问·脏气法时论》「虚则目䀮䀮无所见」;"
f"{REF_NAME}夺作「目无所见」,「眩」为「䀮䀮」之扫描讹字"),
("脉虚而殃", "脉虚而𫘝",
"**用户核对原书**:「𫘝」为「駃」之简化字形(駃通「快」),本书以「𫘝」为「快」;"
"「殃」为扫描讹字(参校本作「脉虚而快」)"),
("脉爽而驶", "脉爽而𫘝",
"**用户核对原书**:同作「𫘝」(参校本作「脉软而快」,本书用字为「𫘝」)"),
("脉驶者", "脉𫘝者",
"**用户核对原书**:确作「脉𫘝者」——「𫘝」(駃之简化字形)为中国古文**通假字**,通「快」;"
"与小补肾汤「脉𫘝者」、大补肾汤「脉爽而𫘝者」同字例(参校本作「脉快者」)"),
("邪气结闼", "邪气结閟",
"**用户核对原书**:「閟」通「闭」,「闼」为形近讹字"),
]
# ── E 类:存疑(原文一律不改)──────────────────────────────────────
SUSPECTS = [] # 原 16 条存疑经用户 2026-09-16/09-16(二) 两轮核对原书,已全部判决
# (据核改 5 条见 C 类;核为无误 11 条见 VERIFIED),本表留空——
# 后续若新发现存疑项,按 (正则, 说明) 追加于此。
# ── G 类:人工核验记录(用户 2026-09-16 核对原书的结果)──────────────
# (核验判决, 行锚片段, 说明)
VERIFIED = [
("改", "虚则目眩,无所见", "核为「目䀮䀮无所见」,已回填清洗版(见 C 类明细)"),
("改", "脉虚而殃", "核为「脉虚而𫘝」(𫘝通「快」),已回填"),
("改", "脉爽而驶", "核为「脉爽而𫘝」,已回填"),
("改", "邪气结闼气分", "核为「邪气结閟气分」(閟通「闭」),已回填"),
("改", "脉驶者",
"核为「脉𫘝者」(𫘝为古文通假字,通「快」)——与 211/257 同字例,已回填"),
("无误", "此篇所列大泻汤散法,悉是小方加母脏泻方", "原书即作此段,文义自洽,原样保留"),
("无误", "附子三枚,炮 生姜三两,切", "原书确作「附子三枚」,原样保留"),
("无误", "生地三两,切 茯苓六两 旋覆花三两 藿三两", "原书确作「生地」「藿」,原样保留"),
("无误", "大枣十五枚,去核", "原书确作「大枣十五枚」「生姜二两」,原样保留"),
("无误", "人参三两 附子二大枚,炮 竹叶三两", "原书版式即此(「去核擘」居后),原样保留"),
("无误", "需用者六十一首", "原书确作「六十一首」,原样保留"),
("无误", "黄耆五两 人参 桂枝 生姜各三两", "大阳旦汤药行八味(含「饴一升」)而末作「右七味」,原书如是,原样保留"),
("无误", "大补心散:海蛤 理石 雄黄 硇砂 姜石 曾青 卤硷", "原书用「卤硷」(硷为碱之异体),原样保留"),
("无误", "卤硷 曾青 姜石 硇砂各三两", "同上,原样保留"),
("无误", "小泻脾汤散:治脾气实,身重不胜",
"原书确作「附子、生姜、甘草」(与本书金石方「阳起石、伏龙肝、石膏」对位,内部自洽),"
"参校本作「干姜」系传本异文,原样保留"),
("无误", "熨耳以通心气:治梦魇不寤者方",
"原书确作「熨耳以通心气」;篇末「右五方」指前五方(点眼、着舌、启咽、吹鼻、灌耳),"
"熨耳为另出之方,原样保留"),
]
# ── F 类:版本差异/体例(不校改)────────────────────────────────────
VERSION_NOTES = [
("体例·右/上", "竖排本作「右X味」(右三味、右六味……),网络横排本作「上三味」;"
"**用户指令以本书为准,全书不改**(竖排古籍体例,非讹字)。"
"又本书方末一律作「右X味」,仅两处通论文字作「上四味」「上二味」(指上文药味,"
"非方末计数),一仍其旧"),
("书名", "本书题《辅行诀五**脏**用药法要》(钱超尘考订书名亦作「五脏」);"
"旧入库参考本与参校本作《辅行诀**脏腑**用药法要》"),
("本书特有·金石方", "每首五脏方(含救误、劳损方)之后另附同构金石药方(琅玕、雄黄、曾青、云母、凝水石、"
"硝石、白垩土、禹粮石、代赭石、白矾、石膏、乳石、磁石、石英、阳起石、伏龙肝、石绿、"
"皂矾、赤石脂、丹砂、硫黄、黄土、滑石、芒硝、礜石等),与「二十五味药精」"
"草木—金石配对一一相应,为本书独有内容(旧本、参校本皆无)"),
("本书特有·救误方", "救误泻五脏方按小、大各一首(共 10 首),方名下附编者说明"
"(据《金匮要略》《神农本草经集注》《外台秘要》引《古今录验》《千金方》、"
"张大昌《处方正范》遗稿补)"),
("本书特有·附录", "附录一「拟补心兼属土火金石补泻方四首」(铁落、石胆、石蜜、朴硝、戎盐、矾石、海蛤、"
"理石、雄黄、姜石、硇砂、曾青、卤碱);"
"附录二「从火论心草木金石小补泻汤散四首」(小泻心汤散、小补心汤散之草木方与金石方各一首,"
"题称四首即此)"),
("图版缺失", "《汤液经法图》及图注(「左阳进为补,其数七……右阴退为泻,其数六……」)本电子本无图;"
"原扫描本图版位置仅存本地路径引用,图与图注待补"),
("无校注篇", "旧入库参考本、参校本皆有「校注」五条(耎意为祛弱、白浆/白酨浆、大泻脾汤药味、分即两、"
"二与六草书形近),本书无校注篇;正文中另有「(据某书补)」「(上四十字,藏经洞卷子"
"传抄本空缺,为笔者据文义所补)」等编者按语 8 处"),
("方数体例", "《汤液经法》方数:本书作「为方亦三百六十五首」"
"(上品百二十首+中品百二十首+下品百二十五首=三百六十五),与本卷「三百六十五味」相应;"
"参校本作「三百六十首」(百二十+百二十+百二十)。两本各自自洽,不改"),
("用字体例/罕用字形", "本书以「𫘝」(U+2B61D,駃之简化字形,通「快」)作「快」"
"(脉𫘝、脉爽而𫘝),而旧入库本、参校本作「快」;"
"另有「䀮」(U+402E,目䀮䀮=目巟巟)等罕用字形,检索时须用原字(以「快」"
"或「目眩」检索均不能命中);用「闷」(大小便闷)而旧本或作「闭」;"
"「日再」/「日二」、「栝蒌」/「栝楼」、「萸肉」/「山萸肉」、「术」/「白术」、"
"「硷」/「碱」、「即济」/「既济」等用字互异,本阶段异体与用字不校,只登记"),
]
# ── 结构识别 ─────────────────────────────────────────────────────────
SECTIONS = [
(r"^# 辅行诀五脏用药法要$", "序·题名"),
(r"^梁·华阳隐居", "序·撰者"),
(r"^隐居曰:凡学道辈", "序·隐居曰"),
(r"^## 辨肝脏病证文并方", "上卷·辨肝脏病证文并方"),
(r"^## 辨心脏病证文并方", "上卷·辨心脏病证文并方"),
(r"^心胞气实者", "上卷·辨心胞病证文(篇题缺)"),
(r"^## 辨脾脏病证文并方", "上卷·辨脾脏病证文并方"),
(r"^## 辨肺脏病证文并方", "上卷·辨肺脏病证文并方"),
(r"^## 辨肾脏病证文并方", "上卷·辨肾脏病证文并方"),
(r"^此篇所列大泻汤散法,悉是小方加母脏", "上卷·五脏大小补泻汤散法通论"),
(r"^又有泻方五首", "上卷·救误泻五脏方(篇题缺)"),
(r"^此篇所列大泻汤散法,上二味是本君臣", "上卷·救误泻方通论"),
(r"^陶云:经方有救诸劳损病方", "下卷·救五脏诸劳损病方(引)"),
(r"^治疗劳损之方", "下卷·救五脏诸劳损病方(小方五首)"),
(r"^此篇所列诸劳损补法所治", "下卷·救五脏诸劳损病方(通论)"),
(r"^陶云:经云:毒药攻邪", "下卷·汤液经法(引)"),
(r"^若欲作大汤散者", "下卷·汤液经法(大汤加法)"),
(r"^陶隐居云:依《神农本经》", "下卷·汤液经法(二十五味药精)"),
(r"^味辛皆属木", "下卷·汤液经法(二十五味药精条文)"),
(r"^此二十五味", "下卷·汤液经法(药精结语)"),
(r"^又有药十三种", "下卷·汤液经法(十三种药)"),
(r"^经云:主于补泻者为君", "下卷·汤液经法(君臣佐使)"),
(r"^陶隐居曰:此图乃", "下卷·汤液经法(图注)"),
(r"^弘景曰:外感天行", "下卷·二旦四神大小汤(篇题缺)"),
(r"^弘景曰:阳旦者", "下卷·二旦四神汤义(篇题缺)"),
(r"^陶隐居云:中恶卒死者", "下卷·救五脏中恶卒死方(篇题缺)"),
(r"^上五方,乃神仙救急之道", "下卷·救急五方结语"),
(r"^附:拟补心兼属土火金石补泻方四首", "附录一·拟补心兼属土火金石补泻方四首"),
(r"^附:从火论心草木金石小补泻汤散四首", "附录二·从火论心草木金石小补泻汤散四首"),
]
FANG_RE = re.compile(r"^(?:救误|养生|调中|建中|凝息|固元)?(?:小|大)?[\u4e00-\u9fff()、]{2,12}?(?:汤散?|散|方)[::]")
OPENERS = re.compile(r"^(?:点眼|着舌|启咽|吹鼻|灌耳|熨耳)[\u4e00-\u9fff]{1,6}[::]")
JINSHI = re.compile(r"^[\u4e00-\u9fff]{1,5}(?:各|)[一二三四五六七八九十百]+(?:两|升|合|枚|把|斤|握|茎|锭|大枚|钱匕)")
CN = {"一":1,"二":2,"三":3,"四":4,"五":5,"六":6,"七":7,"八":8,"九":9,"十":10}
def clean(raw):
events = []
raw_lines = raw.replace("\r\n", "\n").split("\n")
latex_starts = {b[0]: b for b in LATEX_BLOCKS}
# 1) 接合/拆行(行号锚定 + 原文校验)
units, i = [], 0
while i < len(raw_lines):
cur = raw_lines[i]
lineno = i + 1
s = cur.strip()
if lineno in latex_starts:
_st, _en, _rep, _why, _anchor = latex_starts[lineno]
block = "\n".join(raw_lines[lineno - 1:_en])
if _anchor not in block or "$$" not in block:
raise SystemExit(f"LaTeX 块锚校验失败:raw{lineno}–{_en}")
units.append(_rep)
events.append(dict(行=lineno, 类型="A版式残留",
原="LaTeX 公式化残留(\\mathrm/\\text/\\begin{array})",
改=_rep, 依据=_why + ";与本方「右X味」枚数、药名数核对相符"))
i = _en
continue
if lineno in LINE_REPLACE:
anchor, newtext = LINE_REPLACE[lineno]
if anchor not in s:
raise SystemExit(f"单行替换锚校验失败:raw{lineno}")
units.append(newtext)
events.append(dict(行=lineno, 类型="A版式残留", 原=s[:60], 改="图版缺失注记",
依据="本地 Windows 路径不可用、图本体未提供;旧入库本亦缺此图,待补"))
i += 1
continue
if lineno in SPLITS:
mark = SPLITS[lineno]
if mark not in s:
raise SystemExit(f"拆行锚校验失败:raw{lineno} 未含「{mark}」")
head, tail = s.split(mark, 1)
head, tail = head.strip(), tail.strip()
units.append(head)
events.append(dict(行=lineno, 类型="B断行接合", 原=s[:60],
改=f"{head[:40]} / {mark}{tail[:20]}",
依据="药味行与煎服法误并一行,据全书体例拆为两行"))
units.append(mark + tail)
i += 1
continue
if lineno in JOINS:
joiner, anchor = JOINS[lineno]
if not s.startswith(anchor):
raise SystemExit(f"接合锚校验失败:raw{lineno} 应为「{anchor}…」,实为「{s[:20]}」")
while units and not units[-1].strip():
units.pop()
if not units:
raise SystemExit(f"接合失败:raw{lineno} 之前无正文行")
prev = units.pop()
units.append(prev + joiner + s)
events.append(dict(行=lineno, 类型="B断行接合", 原=f"…{prev[-24:]} / {s[:24]}",
改=f"…{(prev + joiner + s)[-40:]}",
依据="版心换页/换行致句中截断,接合后语义完整(与「右X味」枚数、药名数核对相符)"))
i += 1
continue
units.append(s)
i += 1
# 2) 行尾空格清理(汇总登记)
n_trail = sum(1 for l in raw_lines if l != l.rstrip())
events.append(dict(类型="A版式残留", 原=f"行尾空格 {n_trail} 行", 改="已删",
依据="扫描/PDF 转换残留,逐行 rstrip"))
# 3) 逐条修正(在接合后的段落上)
# 段落序号 -> 成文行号(成文以空行分隔,第 j 个非空段落在第 2j+1 行)
line_map = {k: 2 * j + 1 for j, k in enumerate([i for i, u in enumerate(units) if u.strip()])}
out = []
for k, line in enumerate(units):
ln = line_map.get(k)
def ev(**kw):
e = dict(kw)
if ln is not None:
e["行"] = ln
events.append(e)
orig = line
for bad, good in STRAY_SPACES:
if bad in line:
ev(类型="A版式残留", 原=bad, 改=good, 依据="句中误入空格")
line = line.replace(bad, good)
for bad, good, why in FIXES:
if bad in line:
ev(类型="C讹字修正", 原=bad, 改=good, 依据=why)
line = line.replace(bad, good)
for bad, good, why in JING_SEP:
if bad in line:
ev(类型="A版式残留", 原=bad[:24] + "…", 改=good[:30] + "…", 依据=why)
line = line.replace(bad, good)
if line.startswith("味") and "皆属" in line: # 药精条:标点后误空格
line = re.sub(r"(?<=[,。;:])\s+", "", line)
for bad, good, why in SEP_FIXES:
if bad in line:
ev(类型="B脱分隔", 原=bad, 改=good, 依据=why)
line = line.replace(bad, good)
d = re.sub(r"([\u4e00-\u9fff])\s*:\s*", r"\1:", line)
if d != line:
ev(类型="D标点", 原="半角冒号", 改="全角冒号", 依据="标点规范化")
line = d
out.append(line)
text = "\n\n".join([l for l in out if l]) + "\n"
# 3) E 存疑定位
sus, clines = [], text.split("\n")
for i, l in enumerate(clines, start=1):
for pat, why in SUSPECTS:
if re.search(pat, l):
sus.append({"行": i, "片段": l[:60], "说明": why})
seen, uniq = set(), []
for s in sus:
key = (s["行"], s["说明"])
if key not in seen:
seen.add(key); uniq.append(s)
return text, events, uniq
def is_drug_like(l):
"""药味行/金石行:以空格分列药名,至少一处含量词或「各等分」。"""
l = l.strip()
toks = [t for t in re.split(r"\s+", l) if t]
if len(toks) < 2 or not re.match(r"^[\u4e00-\u9fff()、]{1,8}", toks[0]):
return False
return any(re.search(r"(?:各|)[一二三四五六七八九十百]+(?:两|升|合|枚|把|斤|握|茎|锭|大枚|钱匕)|各?等分", t)
for t in toks)
def selfcheck(text):
"""自检:C 讹字残留、味数核对、句中残留空格清单。"""
errs, warn = [], []
for bad, good, _ in FIXES:
if bad in text and bad != good:
errs.append(f"C 规则未生效:「{bad}」仍存于成文")
lines = text.split("\n")
for i, l in enumerate(lines):
m = re.search(r"^右(?:方|药)?([一二三四五六七八九十]+)味", l)
if m and "以" in l[:10]:
j = i - 1
while j >= 0 and not lines[j].strip():
j -= 1
toks = [t for t in re.split(r"\s+", lines[j]) if t]
exp = CN[m.group(1)]
if "饴一升" in lines[j]: # 大阳旦汤:饴为辅料,书本末文仍作「右七味」
exp += 1
if len(toks) != exp:
if "各一两" in lines[j] and len(toks) == exp + 1: # 附「余三味俱作一两」类不在此列
pass
errs.append(f"味数不符(行{i+1}):右{m.group(1)}味,药行 {len(toks)} 词 → {lines[j][:60]}")
space_lines = [(i + 1, l) for i, l in enumerate(lines)
if re.search(r"[\u4e00-\u9fff]\s[\u4e00-\u9fff]", l)]
for ln, l in space_lines:
if not (is_drug_like(l) or l.startswith("味") or "·" in l):
warn.append(f"行{ln} 仍含词间空格(非药味行/金石行,请人工确认):{l[:70]}")
return errs, warn, space_lines
def parse_struct(text):
lines = text.split("\n")
nodes = []
for i, l in enumerate(lines, start=1):
for pat, name in SECTIONS:
if re.match(pat, l.strip()):
nodes.append({"篇": name, "起始行": i})
break
for n, node in enumerate(nodes):
node["结束行"] = nodes[n + 1]["起始行"] - 1 if n + 1 < len(nodes) else len(lines)
fangs = []
for i, l in enumerate(lines, start=1):
s = l.strip()
if s.startswith("#"):
continue
if FANG_RE.match(s) or OPENERS.match(s):
owner = next((n["篇"] for n in nodes if n["起始行"] <= i <= n["结束行"]), "(篇外)")
fangs.append({"篇": owner, "方名": re.sub(r"[::].*$", "", s), "起始行": i})
for n, f in enumerate(fangs):
f["结束行"] = fangs[n + 1]["起始行"] - 1 if n + 1 < len(fangs) else len(lines)
return nodes, fangs
def main():
raw = open(SRC, encoding="utf-8").read()
cleaned, events, sus = clean(raw)
errs, warn, space_lines = selfcheck(cleaned)
nodes, fangs = parse_struct(cleaned)
def wf(path, content):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(content); f.flush(); os.fsync(f.fileno())
wf(OUT_MD, cleaned)
struct = {
"来源文件": os.path.basename(SRC), "来源文件md5": hashlib.md5(raw.encode()).hexdigest(),
"清洗版文件": os.path.basename(OUT_MD), "清洗版md5": hashlib.md5(cleaned.encode()).hexdigest(),
"定位": "《辅行诀五脏用药法要》基础文本(底本):印刷书籍扫描本;用户 2026-09-16 指令为准",
"参校本": REF_NAME,
"体例说明": "行号均指清洗版;竖排本作「右X味」,本书为准,不改作「上」;本文件仅作清理配套定位索引,"
"未做药性释义抽取与跨库关联",
"篇": nodes, "方": fangs,
}
wf(OUT_STRUCT, json.dumps(struct, ensure_ascii=False, indent=2) + "\n")
owner = lambda ln: next((n["篇"] for n in nodes if n["起始行"] <= ln <= n["结束行"]), "(篇外)")
per, per_sus = {}, {}
for e in events:
per.setdefault(owner(e["行"]) if "行" in e else "(全局·脚本级)", []).append(e)
for s in sus:
per_sus.setdefault(owner(s["行"]), []).append(s)
c = lambda t: sum(1 for e in events if e["类型"].startswith(t))
rep = ["# 《辅行诀五脏用药法要》印刷本(基础文本)— 清洗报告", "",
"> 📌 **状态:暂定标准范本(baseline)**——用户 2026-09-16 认可本报告输出的"
"`辅行诀五脏用药法要_基础文本_清洗版.md` 作为《辅行诀五脏用药法要》的标准文本,"
"**如后续发现错漏,再按本脚本规则表增改后重跑刷新**(源文件 `01_来源数据/` 永不改动)。",
"> 用户 2026-09-16 指令:**印刷书籍扫描本优于此前入库的古本原文与参校本,定为基础文本(底本)**;",
"> 参校本仅在判断扫描讹字、脱字、脱分隔时作辅助证据,不作版本改写。",
"> 竖排本作「右X味」(网络横排本作「上X味」)——**以本书为准,一律不改**。",
"> 本阶段仍只做数据清理,不做药性释义抽取、跨库关联与主题分析。", "",
"## 0 概览", "",
f"- 来源:`01_来源数据/{os.path.basename(SRC)}`(md5 `{struct['来源文件md5']}`)",
f"- 基础文本:`02_加工数据/{os.path.basename(OUT_MD)}`(md5 `{struct['清洗版md5']}`)",
f"- 参校本:{REF_NAME}",
f"- 字符数:{len(raw)} → {len(cleaned)};行数:{raw.count(chr(10))+1} → {cleaned.count(chr(10))+1}",
f"- **baseline(暂定标准范本)**:2026-09-16 用户认可;清洗版 md5 `{struct['清洗版md5']}`"
f"(变更须在文末更新日志登记)",
f"- 结构:{len(nodes)} 个篇段 / {len(fangs)} 首方;行号锚定接合 {len(JOINS)} 处、拆行 {len(SPLITS)} 处",
"", "| 清理类别 | 处数 | 说明 |", "|---------|------|------|",
f"| A 版式残留 | {c('A')} | 行首行尾空格(273 行)、句中误入空格 6 处、LaTeX 残留 4 块、"
f"图片路径 1 处、药精条分隔缺失 1 处 |",
f"| B 断行接合/脱分隔 | {c('B')} | 换页换行接合 {len(JOINS)} 处、药味行脱分隔 4 处、误并行拆开 {len(SPLITS)} 处 |",
f"| C 讹字修正 | {c('C')} | 每条附核校依据(本书内证+参校本+同类文献),见逐篇明细 |",
f"| D 标点规范化 | {c('D')} | 半角标点改全角 |",
f"| E 存疑(未改原文) | {len(sus)} | 原 16 条存疑经用户两轮核对原书**已全部核验完毕**"
f"(据核改 5 · 核为无误 11),现存疑册为空 |",
f"| F 版本差异/体例(未改) | {len(VERSION_NOTES)} | 本书特有内容与用字体例,不作校改 |",
f"| G 人工核验(已回填) | 改 {sum(1 for v in VERIFIED if v[0].startswith('改'))} · 无误 "
f"{sum(1 for v in VERIFIED if v[0]=='无误')} | 用户 2026-09-16 核对原书结果,见第 3 节 |",
"", "### 各篇清理量一览", "",
"| 卷·篇 | 行范围 | 方数 | A | B | C | D | E 存疑 |",
"|-------|--------|------|---|---|---|---|--------|"]
for n in nodes:
evs = per.get(n["篇"], [])
cc = lambda t: sum(1 for e in evs if e["类型"].startswith(t))
_ = cc
fn = sum(1 for f in fangs if f["篇"] == n["篇"])
rep.append(f"| {n['篇']} | {n['起始行']}–{n['结束行']} | {fn} | {cc('A')} | {cc('B')} | "
f"{cc('C')} | {cc('D')} | {len(per_sus.get(n['篇'], []))} |")
rep += ["", "---", "", "## 1 逐篇核对明细(按卷/篇列明)", ""]
for n in nodes:
evs, ss = per.get(n["篇"], []), per_sus.get(n["篇"], [])
rep += [f"### {n['篇']}(行 {n['起始行']}–{n['结束行']})", "",
f"- 清理量:A {sum(1 for e in evs if e['类型'].startswith('A'))} / "
f"B {sum(1 for e in evs if e['类型'].startswith('B'))} / "
f"C {sum(1 for e in evs if e['类型'].startswith('C'))} / "
f"D {sum(1 for e in evs if e['类型'].startswith('D'))} / 存疑 {len(ss)}", ""]
if evs:
rep += ["| 行 | 类别 | 原文 | 处理后 | 核校依据 |", "|----|------|------|--------|---------|"]
for e in evs:
rep.append(f"| {e.get('行','—')} | {e['类型']} | {e['原'].replace('|','|')} | "
f"{e['改'].replace('|','|')} | {e['依据'].replace('|','|')} |")
rep.append("")
if ss:
rep += ["存疑项:", "", "| 行 | 片段 | 说明 |", "|----|------|------|"]
for s in ss:
rep.append(f"| {s['行']} | {s['片段'].replace('|','|')} | {s['说明'].replace('|','|')} |")
rep.append("")
if not evs and not ss:
rep += ["- 本篇无需修正条目。", ""]
glob = per.get("(全局·脚本级)", [])
if glob:
rep += ["---", "", "### 全局清理项(不属具体篇段)", "",
"| 类别 | 原文 | 处理后 | 核校依据 |", "|------|------|--------|---------|"]
for e in glob:
rep.append(f"| {e['类型']} | {e['原'].replace('|','|')} | {e['改'].replace('|','|')} | "
f"{e['依据'].replace('|','|')} |")
rep.append("")
rep += ["---", "", "## 2 自检结果(脚本自动)", "",
f"- C 类讹字残留检查:{'通过(无残留)' if not errs else '**未通过**'}",
f"- 「右X味」枚数 vs 药味行词数核对:{'全部相符' if not any('味数不符' in e for e in errs) else '**有不符**'}",
f"- 句中残留空格检查:{len(space_lines)} 行含药味行分隔空格(正常,见下);"
f"{len(warn)} 行需人工确认"]
for e in errs:
rep.append(f" - ⚠ {e}")
for w in warn:
rep.append(f" - ⚠ {w}")
rep += ["", "含分隔空格的药味行/金石行清单(抽检用,非错误):", "",
"| 行 | 内容 |", "|----|------|"]
for ln, l in space_lines:
if JINSHI.match(l) or l.startswith("味"):
rep.append(f"| {ln} | {l[:70]} |")
rep += ["", "---", "", "## 3 人工核验回填记录(用户 2026-09-16 核对原书)", "",
"「改」=原书当作某字,已回填清洗版(C 类明细中「用户核对原书」条目);"
"「无误」=本来源与原书一致,原样保留,不再列为存疑。", "",
"| 判决 | 清洗版片段 | 核验结果 |", "|------|-----------|---------|"]
for verdict, anchor, note in VERIFIED:
rep.append(f"| {verdict} | {anchor.replace('|','|')} | {note.replace('|','|')} |")
if sus:
rep += ["", "**尚待核对**:", ""]
for s_ in sus:
rep.append(f"- 行 {s_['行']}:{s_['片段'][:50]} —— {s_['说明']}")
else:
rep += ["", "**尚待核对:无**——原 16 条存疑已全部判决并闭环"
"(据核改 5 条已回填,核为无误 11 条原样保留)。", ""]
rep += ["", "## 4 版本差异/体例登记(不校改)", "", "| 类别 | 说明 |", "|------|------|"]
for k, v in VERSION_NOTES:
rep.append(f"| {k} | {v} |")
rep += ["", "## 5 篇段结构(清洗版行号)", "", "| 卷·篇 | 起始行 | 结束行 |", "|-------|--------|--------|"]
for n in nodes:
rep.append(f"| {n['篇']} | {n['起始行']} | {n['结束行']} |")
rep += ["", "## 6 方目(清洗版行号)", "", "| 篇 | 方名 | 起始行 | 结束行 |", "|----|------|--------|--------|"]
for f in fangs:
rep.append(f"| {f['篇']} | {f['方名']} | {f['起始行']} | {f['结束行']} |")
wf(OUT_REPORT, "\n".join(rep) + "\n")
chk = ["# 人工核对清单 — 《辅行诀五脏用药法要》印刷本(基础文本)", "",
"> 用途:需对照纸质原书确认的条目。**按卷/篇列明**,本阶段不改动清洗版正文。",
f"> 参校本:{REF_NAME}。以印刷本为准,参校本仅供查错补漏。",
"> 📌 清洗版已由用户 2026-09-16 认可为**暂定标准范本**;后续如发现错漏,按本节记录方式回填并刷新。",
"> 用户 2026-09-16 两轮核对原书,**16 条存疑已全部判决完毕**:据核改 5 条(已回填清洗版)、"
"核为无误 11 条(原样保留)。", "",
f"## 一、尚待核对({len(sus)} 条)", "",
"| 行 | 篇 | 片段 | 疑正/说明 |", "|----|----|------|-----------|"]
if sus:
for s_ in sus:
chk.append(f"| {s_['行']} | {owner(s_['行'])} | {s_['片段'].replace('|','|')} | "
f"{s_['说明'].replace('|','|')} |")
else:
chk.append("| — | — | — | 无:原 16 条存疑已全部核验完毕(见第二节) |")
chk += ["", "## 二、已人工核验(用户 2026-09-16 核对原书)", "",
"### 2.1 据核改(已回填清洗版)", "",
"| 判决 | 清洗版片段 | 核验结果 |", "|------|-----------|---------|"]
for verdict, anchor, note in VERIFIED:
if verdict.startswith("改"):
chk.append(f"| {verdict} | {anchor.replace('|','|')} | {note.replace('|','|')} |")
chk += ["", "### 2.2 核为无误(原样保留)", "",
"| 判决 | 清洗版片段 | 核验结果 |", "|------|-----------|---------|"]
for verdict, anchor, note in VERIFIED:
if verdict == "无误":
chk.append(f"| 无误 | {anchor.replace('|','|')} | {note.replace('|','|')} |")
chk += ["", "## 三、版本差异/体例登记(不校改,待核原书)", "", "| 类别 | 说明 |", "|------|------|"]
for k, v in VERSION_NOTES:
chk.append(f"| {k} | {v} |")
chk += ["", "## 书目与来源", "",
"- 文本:《辅行诀五脏用药法要》(梁·华阳隐居 陶弘景撰)",
f"- 本次来源:`01_来源数据/{os.path.basename(SRC)}`(印刷书籍扫描本,用户 2026-09-16 提供)",
"- 定位:本库《辅行诀》**基础文本(底本)**,优先于此前入库的古本原文(旧参考本)",
"- 参校本:维基文库本(快照见 `01_来源数据/校核参考/`),仅作查错补漏",
"- 已修正条目(C 类)见 `02_加工数据/印刷本清洗报告.md` 第 1 节逐篇明细;",
" 人工核验回填记录见同报告第 3 节"]
wf(OUT_CHECK, "\n".join(chk) + "\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("核对单 :", OUT_CHECK, os.path.getsize(OUT_CHECK), "bytes")
print(f"A {c('A')} | B {c('B')} | C {c('C')} | D {c('D')} | 存疑 {len(sus)} | 版本差异 {len(VERSION_NOTES)}")
print(f"篇 {len(nodes)} | 方 {len(fangs)} | 自检错误 {len(errs)} | 待确认 {len(warn)}")
for e in errs:
print(" ⚠", e)
for w in warn:
print(" ⚠", w)
if __name__ == "__main__":
main()
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
04_ref_index.py —— 从《辅行诀五脏用药法要》基础文本(印刷本清洗版)抽取
「核校参考索引」,供《药性探真》后续章节清洗时卡药名 / 方名 / 五行互含名位。
产出:
02_加工数据/参考索引_辅行诀药名方名.json 机器可读索引
02_加工数据/参考索引_辅行诀药名方名.md 人工速查表
底本:02_加工数据/辅行诀五脏用药法要_基础文本_清洗版.md(baseline,md5 见 README)
性质:只读底本、不修改任何原文;幂等可重跑。
用法:
python3 05_脚本工具/04_ref_index.py # 生成索引
python3 05_脚本工具/04_ref_index.py 药名 # 查某个词是否在参考表内
"""
import json
import re
import sys
import hashlib
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "02_加工数据" / "辅行诀五脏用药法要_基础文本_清洗版.md"
OUT_JSON = ROOT / "02_加工数据" / "参考索引_辅行诀药名方名.json"
OUT_MD = ROOT / "02_加工数据" / "参考索引_辅行诀药名方名.md"
TASTE_TO_XING = {"辛": "木", "咸": "火", "甘": "土", "酸": "金", "苦": "水"}
# 底本中同物异写的形态(同一底本内两形并见者,仅登记不改动底本)
ALIAS_NOTES = [
{"规范形": "栝楼", "异写": ["栝蒌"], "依据": "底本第429行「栝楼为土中土」,方剂篇作「栝蒌」;《药性探真》第一章作「栝蒌」"},
{"规范形": "山茱萸", "异写": ["萸肉"], "依据": "药精表作「萸肉」,方剂篇加减法作「山茱萸」"},
{"规范形": "干姜", "异写": ["甘草炙→炙甘草"], "依据": "药精表「甘草炙」「甘草」分列二味(土中金/土中火),方剂篇作「甘草,炙」"},
]
def parse(lines):
yaojing, hunhan, fangming = [], [], []
# ── 1. 二十五味药精 ───────────────────────────────
for i, line in enumerate(lines, 1):
m = re.match(r"^味([辛咸甘酸苦])皆属([木火土金水])[,,]", line)
if not m:
continue
taste, benxing = m.group(1), m.group(2)
assert TASTE_TO_XING[taste] == benxing, f"第{i}行 味-行不符:{taste}≠{benxing}"
for seg in re.split(r"[;;。]", line[m.end():]):
seg = seg.strip()
if not seg:
continue
mm = re.match(r"^(\S+?)\s+(\S+?)(?:为之主|为([木火土金水]))$", seg)
if not mm:
raise SystemExit(f"第{i}行 药精条无法解析:{seg}")
drug, mineral, hanxing = mm.group(1), mm.group(2), mm.group(3)
is_main = hanxing is None
hanxing = benxing if is_main else hanxing
yaojing.append({
"药名": drug, "味": taste, "本行": benxing,
"互含名位": f"{benxing}中{hanxing}",
"配对金石药": mineral, "药精位次": "主" if is_main else f"含{hanxing}",
"行号": i, "底本依据": line.strip(),
})
# ── 2. 十三种药(五行互含,备心病方之用) ──────────
start = None
for i, line in enumerate(lines, 1):
if "又有药十三种" in line:
start = i
break
if start is None:
raise SystemExit("未找到「又有药十三种」条")
body = lines[start] # 该条正文紧随其后
for i in range(start, min(start + 4, len(lines))):
if re.search(r"为[木火土金水]中[木火土金水]", lines[i]):
body = lines[i]
body_line = i + 1
break
for seg in re.split(r"[;;。,,]", body):
seg = seg.strip()
if not seg:
continue
if seg.startswith("又为"):
if not hunhan:
raise SystemExit(f"第{body_line}行「又为」无前项:{seg}")
hunhan[-1]["互含名位"].append(seg[2:])
continue
mm = re.match(r"^(\S+?)为([木火土金水]中[木火土金水])$", seg)
if not mm:
raise SystemExit(f"第{body_line}行 十三种药条无法解析:{seg}")
hunhan.append({"药名": mm.group(1), "互含名位": [mm.group(2)], "行号": body_line,
"底本依据": body.strip()})
# ── 3. 方名 ───────────────────────────────────────
seen = set()
for i, line in enumerate(lines, 1):
m = re.match(r"^([^::\s]{2,16}?(?:汤|散|丸|煎|膏|酒|方))[::]", line)
if not m:
continue
name = m.group(1)
if name in seen:
continue
seen.add(name)
fangming.append({"方名": name, "行号": i, "主治": line.split(":", 1)[-1][:40]})
return yaojing, hunhan, fangming, body_line
def main():
text = SRC.read_text(encoding="utf-8")
lines = text.split("\n")
yaojing, hunhan, fangming, hunhan_line = parse(lines)
drugs = [d["药名"] for d in yaojing] + [d["药名"] for d in hunhan]
index = {
"来源": SRC.name,
"来源md5": hashlib.md5(text.encode("utf-8")).hexdigest(),
"生成脚本": "05_脚本工具/04_ref_index.py",
"说明": "《药性探真》后续章节清洗的核校参考表:药名、五行互含名位、配对金石药、方名。",
"药精二十五味": yaojing,
"五行互含十三种药": hunhan,
"方名清单": fangming,
"药名一览": sorted(set(drugs)),
"同物异写登记": ALIAS_NOTES,
"计数": {"药精": len(yaojing), "十三种药": len(hunhan),
"药名合计": len(set(drugs)), "方名": len(fangming)},
}
OUT_JSON.write_text(json.dumps(index, ensure_ascii=False, indent=2), encoding="utf-8")
# ── 人工速查 md ───────────────────────────────────
L = ["# 参考索引|《辅行诀五脏用药法要》药名·方名·五行互含名位", "",
f"> 生成:`05_脚本工具/04_ref_index.py`(幂等)|底本:`{SRC.name}`(md5 `{index['来源md5']}`)",
"> 用途:《药性探真》后续章节清洗时的**核校参考**(药名规范形、五行互含名位、配对金石药、方名)。",
"> 原则:只作旁证,不改动《药性探真》原文;发现不一致→登记人工核对清单。", "",
"## 一、二十五味药精(《汤液经法》五行互含)", "",
"| 味 | 本行 | 互含名位 | 药名 | 配对金石药 | 底本行号 |", "|---|---|---|---|---|---|"]
for d in yaojing:
L.append(f"| {d['味']} | {d['本行']} | {d['互含名位']} | {d['药名']} | {d['配对金石药']} | {d['行号']} |")
L += ["", "## 二、五行互含十三种药(备心病方之用)", "", "| 药名 | 互含名位 | 底本行号 |", "|---|---|---|"]
for d in hunhan:
L.append(f"| {d['药名']} | {'、'.join(d['互含名位'])} | {d['行号']} |")
L += ["", "## 三、方名清单", "", "| 方名 | 底本行号 | 主治(截断) |", "|---|---|---|"]
for f in fangming:
L.append(f"| {f['方名']} | {f['行号']} | {f['主治']} |")
L += ["", "## 四、同物异写登记(只登记,不改底本)", ""]
for a in ALIAS_NOTES:
L.append(f"- **{a['规范形']}** /异写:{'、'.join(a['异写'])} —— {a['依据']}")
L += ["", "## 五、药名一览(去重)", "", "、".join(index["药名一览"]), ""]
OUT_MD.write_text("\n".join(L), encoding="utf-8")
print(f"药精 {len(yaojing)} 味 | 十三种药 {len(hunhan)} 味 | 药名去重 {len(set(drugs))} | 方名 {len(fangming)}")
print(f"索引:{OUT_JSON.relative_to(ROOT)}")
print(f"速查:{OUT_MD.relative_to(ROOT)}")
def lookup(word):
"""查词是否在参考表内(供后续章节清洗时快速卡药名)。"""
idx = json.loads(OUT_JSON.read_text(encoding="utf-8"))
hits = [d for d in idx["药精二十五味"] + idx["五行互含十三种药"] if word in d["药名"]]
fangs = [f for f in idx["方名清单"] if word in f["方名"]]
print(f"「{word}」 药名命中 {len(hits)} / 方名命中 {len(fangs)}")
for h in hits:
print(" 药:", h["药名"], h.get("互含名位"), h.get("配对金石药", ""))
for f in fangs:
print(" 方:", f["方名"], "→", f["主治"])
if __name__ == "__main__":
if len(sys.argv) > 1:
lookup(sys.argv[1])
else:
main()
@@ -0,0 +1,333 @@
#!/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()
@@ -0,0 +1,321 @@
#!/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()
@@ -0,0 +1,697 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
辅行诀五脏用药法要-药性探真 资料库 ETL 第 7 步(2026-09-17)
01_来源数据/《辅行诀五脏用药法要 药性探真》第1章.md(★用户精清稿) -> 02_加工数据/
定位:本件为《药性探真》第一章之**用户精清稿**(用户在助手先前清理基础上又作进一步清理:
删净页眉/页码/目录/图片引用,接合断行,修正 白戸浆→白酨浆 等)。
处理(编号与检查报告一致):
A 版式残留:只作**读数核对**(用户已清净,本例 0 处);CRLF 以 rb 读入检测
B 断行接合:L227+L229《图经》引文被空行切断 1 处(接合,锚校验)
C 讹字修正:薤白条内「薙」4 处 → 「薤」(形近讹字,同条他处皆作「薤」,锚校验)
D 标点规范化:半角「)」1 处 → 「)」;《辅行诀》引文**嵌套引号**内层改单引号 1 段
H 标题层级:章=二级(##)、节=三级(###)、条目=四级(####)、条下附=五级(#####)、附内子目=六级(######)
(源件各级标题一律作「## 」,本次按层级逐件降级)
E 存疑(原文不改,登记待核):五味子(第四节)名位「金中土、金中火」与他处「金中土,又为火中木」不一;
「整定稿」与「整订稿」用字不一;「园而复下」形近字待核
F 体例登记:附插入 十一/十二 之间;「2.」未成标题;脚注标记 6 处无脚注正文(待补,不推测补写)
自检(脚本自动):
① 标题计数 章1/节6/条目36/附1/子目1;层级自章至子目单调递减
② 各节条目序号连续;36 条目皆具五行互含名位标注
③ 名位标注 ⇄ 底本《辅行诀》二十五味药精/十三种药 逐条互校
④ 引号/书名号/括号配平;半角 ASCII 标点 0;行尾空格 0;断行候选 0
输出:
02_加工数据/药性探真_第1章_清洗版.md
02_加工数据/药性探真_第1章_检查报告.md
02_加工数据/药性探真_第1章_结构.json
幂等:可重复运行;改写皆带原文锚校验,用户精清稿被替换即报错。
"""
import os, re, json, hashlib
BASE = os.path.expanduser("~/Documents/ai_agent_scraper_study/data/辅行诀五脏用药法要-药性探真")
SRC = os.path.join(BASE, "01_来源数据/《辅行诀五脏用药法要 药性探真》第1章.md")
OLD_SRC = os.path.join(BASE, "01_来源数据/药性探真_第一章_原始_20260915.md")
BENDI = os.path.join(BASE, "02_加工数据/辅行诀五脏用药法要_基础文本_清洗版.md")
OUT_MD = os.path.join(BASE, "02_加工数据/药性探真_第1章_清洗版.md")
OUT_REPORT = os.path.join(BASE, "02_加工数据/药性探真_第1章_检查报告.md")
OUT_STRUCT = os.path.join(BASE, "02_加工数据/药性探真_第1章_结构.json")
OUT_CHECK = os.path.join(BASE, "02_加工数据/药性探真_第1章_待核对清单.md")
SNAP_SRC = os.path.join(BASE, "01_来源数据/《辅行诀五脏用药法要 药性探真》第1章_层级优化前快照_20260917.md")
# ── B 类:引文被空行切断,接合(锚校验)───────────────────────────────
JOINS = [("六月开花如菊,", "园而复下,七八月采花。”",
"《疏证》引《图经》旋覆花条文被空行切断,据文义接合(同一引文一段)")]
# ── C 类:讹字修正(锚校验;本条为形近讹字)──────────────────────────
C_FIXES = [("薙", "薤",
"薤白条内「薙栽于金秋八月…而薙之性却为滑利…如薙所治之疮…是薙之所以能愈败疮之因由」4 处;"
"同条前后(行 921、923、925、931)皆作「薤」,「薙」为除草(剃)之义,与药名无涉,"
"形近致误;参底本《辅行诀》亦作「薤白」")]
# ── D 类:标点规范化(锚校验)────────────────────────────────────────
PUNCT_FIXES = [("《伤寒论阴阳图说》),", "《伤寒论阴阳图说》),",
"半角「)」改全角,与全书标点体例一致")]
# ── D 类:嵌套引号——引文内层引号改单引号(整段显式改写,锚校验)──────
NESTED_OLD = "《辅行诀》云:“脾德在缓。故《经》云:“以甘补之,辛泻之,脾苦湿,急食苦以燥之”;“肝德在散。故《经》云:以辛补之,酸泻之,肝苦急,急食甘以缓之,适其性而衰之也。”"
NESTED_NEW = "《辅行诀》云:“脾德在缓。故《经》云:‘以甘补之,辛泻之,脾苦湿,急食苦以燥之’;‘肝德在散。故《经》云:以辛补之,酸泻之,肝苦急,急食甘以缓之,适其性而衰之也。’”"
# ── H 类:标题层级(章 2 / 节 3 / 条目 4 / 附 5 / 附内子目 6)──────────
H_RULES = [
(re.compile(r"^第[一二三四五六七八九十]+章[  ]"), 2, "章"),
(re.compile(r"^第[一二三四五六七八九十]+节[  ]"), 3, "节"),
(re.compile(r"^[一二三四五六七八九十]+、"), 4, "条目"),
(re.compile(r"^附:"), 5, "条下附"),
(re.compile(r"^\d+\.\s*《"), 6, "附内子目"),
]
# ── E 类:存疑(原文不改)───────────────────────────────────────────
SUSPECTS = [
("五味子(第四节)名位", "第四节标题作「五、五味子(金中土、金中火)」,而本节正文(行 571)明言"
"「五味子在火土论心补泻方剂中,有以火中木而论之处」,第六节十二又作「金中土,又为火中木」;"
"底本《辅行诀》二十五味药精作「五味子 曾青为土」(金中土)、十三种药作「五味子为金中土,又为火中木」。"
"**「金中火」疑当作「火中木」**。"
"**用户 2026-09-17 判决:暂时保留,待后续核对**(本次不改)。"),
("「整定稿」/「整订稿」", "行 737「所据乃原作《整定稿》先入之见」,行 13 作《〈辅行诀五脏用药法要〉整订稿》;"
"同一书名简称用字不一。**用户 2026-09-17 判决:保留(原样不改)**。"),
("「园而复下」", "行 229《图经》旋覆花条「六月开花如菊,园而复下,七八月采花」;「园」「圆」形近。"
"**用户 2026-09-17 判决:无误,原样保留**。"),
]
# ── G 类:脚注处理结论(用户 2026-09-17 判决)────────────────────────
FOOTNOTE_NOTE = ("脚注标记原 6 处,已按用户 2026-09-17 判决全部处理:**回填脚注文字 3 处**"
"(桂枝①「桂为丁,以钉木中,其木即死」、桂枝②「梫,肉桂」、淡豆豉①「指涣散,耗散」,"
"以「〔脚注〕…」行附于本条段后,正文标记保留);**清除标记 3 处**"
"(人参①「百济①者」、附内译文①×2,不补文字)。正文现存标记 3 处,均已有对应〔脚注〕行。")
# ── F 类:体例登记(不校改)─────────────────────────────────────────
VERSION_NOTES = [
("附的位置", "「附:」及其子目「1. 《齐民要术》酿白醪法释文」插在第六节 十一、白酨浆 与 十二、五味子 之间;"
"据行 961「见后附录」可知附属白酨浆条之附释,位置原书如此,不改。"),
("附内子目不对称", "附内作「1. 《齐民要术》酿白醪法释文」(标题)与「2. 如上附酿制的白醪……」(正文段落,未成标题);"
"本次**不为「2.」补标题**(不增添原书无有之层级),只登记待核。"),
("脚注标记", FOOTNOTE_NOTE),
("用户精清稿已作清理", "用户在助手先前清理基础上删净页眉 19 行、独立页码 4 行、目录 4 行、图片引用 2 处,"
"并修正 白戸浆→白酨浆 1 处、补齐「十一、白酨浆」条目标题 1 处(详见报告第二节)。"),
("引文体例·《素问》「金木者,阴阳之道路也」(★已定案)",
"第 561 行(五味子条)作者作:『即《素问》“金木者,阴阳之道路也”』。查《素问·天元纪大论篇第六十六》作"
"「然天地者,万物之上下也;**左右者**,阴阳之道路也;水火者,阴阳之征兆也;金木者,生成之终始也」——"
"**传本作「左右者」**(「金木者」与「生成之终始也」本为同段另一句,作者实为**取该段二、三两句之义而引**)。"
"**用户 2026-09-20 判决:不改(原书如此)**——本条系作者引义改字,登记为**引文体例**,与《说卦》「艮为果瓜」同格;"
"清洗版此处**与印刷本一致**,**不需回流亦不得改动**(无订正项)。"
"**检索须用原字「金木者,阴阳之道路」**(以「左右者」检索不能命中本库文本);"
"作者同条紧接又引『《素问》“水火者,阴阳的征兆也。”』(传本「阴阳之征兆也」,行文小异、义同),可见其引《内经》多取义引。"
"另注:本库第 2 章第 33/109/149/657 行所引为同段**另一句**「金木者,生成之终始也」,已按用户同日判决订正;"
"**两章所引为同段不同两句,分别处理**(本库前曾误记「第 1 章亦见同语」,已勘正)。"),
]
def read_bin(p):
raw = open(p, "rb").read()
return raw.decode("utf-8"), hashlib.md5(raw).hexdigest()
def parse_jing(bendi):
"""底本《辅行诀》二十五味药精 + 十三种药 → {药名: 名位集合}; 另返回金石配对"""
W2X = {"辛": "木", "咸": "火", "甘": "土", "酸": "金", "苦": "水"} # 味 → 行
out, mineral = {}, {}
for l in bendi.split("\n"):
if l.startswith("味") and "皆属" in l:
ben = W2X[l[1]]
body = l.split(",", 1)[1] if "," in l else l
main = re.search(r"^([\u4e00-\u9fff]+)\s+([\u4e00-\u9fff]+)为之主", body)
if main:
out.setdefault(main.group(1), set()).add(f"{ben}中{ben}")
mineral[main.group(1)] = main.group(2)
out.setdefault(main.group(2), set()).add(f"{ben}中{ben}")
for m in re.finditer(r"([\u4e00-\u9fff]+)\s+([\u4e00-\u9fff]+)为([木火土金水])", body):
mt = f"{ben}中{m.group(3)}"
out.setdefault(m.group(1), set()).add(mt)
out.setdefault(m.group(2), set()).add(mt)
mineral[m.group(1)] = m.group(2)
if re.search(r"为[木火土金水]中[木火土金水]", l) and "皆属" not in l:
for m in re.finditer(r"([\u4e00-\u9fff]{1,4})为([木火土金水]中[木火土金水])"
r"(?:,又为([木火土金水]中[木火土金水]))?", l):
out.setdefault(m.group(1), set()).add(m.group(2))
if m.group(3):
out.setdefault(m.group(1), set()).add(m.group(3))
return out, mineral
def parse_bendi_drugs(bendi):
"""二十五味与十三种药原文条目(供报告引用)"""
secs = []
for l in bendi.split("\n"):
if l.startswith("味") and "皆属" in l:
secs.append(l.strip())
if l.startswith("又有药十三种") or (l.startswith("通草为木中土")):
secs.append(l.strip())
return secs
# ── 待核对清单条目(用户核对用;按章节结构分组输出)──────────────────
CHECK_ITEMS = [
("原书核对", "五、五味子(金中土、金中火)",
"条目名位作「金中土、金中火」,「金中火」疑当作「火中木」",
"本节正文自称「五味子在火土论心补泻方剂中,有以火中木而论之处」;第六节十二作「金中土,又为火中木」;"
"底本《辅行诀》二十五味药精作「五味子 曾青为土」(金中土)+十三种药作「五味子为金中土,又为火中木」",
"核原书:若作「火中木」则改本条目标题;若确作「金中火」则登记无误"),
("原书核对", "整定稿",
"同一书名简称两见而用字不一:「整定稿」(行 737)/「整订稿」(行 13)",
"本库另有《〈辅行诀五脏用药法要〉整订稿》篇目提纲件(出处:本书附篇附录一)",
"核原书:统一为「整订稿」,或登记异文"),
("原书核对", "园而复下",
"《疏证》引《图经》旋覆花条「六月开花如菊,园而复下,七八月采花」,「园」疑「圆」",
"上下文述花形;「园」「圆」形近",
"核原书:若作「圆」则改;若确作「园」则登记无误"),
("修正确认", "薤栽于金秋八月",
"薤白条「薙」4 处已改「薤」(本次 C 类讹字修正)",
"同条前后(行 921/923/925/931)皆作「薤」;「薙」为除草(剃)之义,与药名无涉;底本《辅行诀》亦作「薤白」",
"复核确认;若原书作「薙」可即时回改"),
("标点规范确认", "金木易位(请参《伤寒论阴阳图说》),",
"半角「)」已改全角「),」(本次 D 类;在枳实条内)",
"标点体例统一(原文此处作半角「)」紧接「,」)",
"复核;如欲保留半角,可回改"),
("标点规范确认", "适其性而衰之也。’”",
"《辅行诀》引文内层引号已由双引号改单引号(本次 D 类;原配平 3/2)",
"引号嵌套规范:外层双引号、内层单引号",
"复核;如欲双写,可回改"),
("断行接合确认", "六月开花如菊,园而复下",
"《图经》旋覆花引文原被空行切断,已接合为一段(本次 B 类)",
"同一引文文义相连",
"复核;如原书另起段落,可回改"),
("体例确认", "附:",
"「附:」及子目插在第六节 十一、白酨浆 与 十二、五味子 之间(位置保持原样)",
"行 961 有「见后附录」之语,附属白酨浆条之附释",
"确认是否保持原位(现为保持)"),
("体例确认", "2. 如上附酿制的白醪",
"附内「1. 《齐民要术》酿白醪法释文」为标题(六级),而「2. …」为正文段落、未成标题",
"本次**未替「2.」补标题**(不增添原书无有之层级)",
"确认是否将「2.」升为六级标题(现为保持段落)"),
]
# ── 待核对清单·脚注项(固定 6 条,锚不带标记,判决后仍可定位)──────────
CHECK_FOOTNOTES = [
("《纲目》引《雷公炮炙论》云:“桂钉木根,其木即死”", "脚注标记「①」原无脚注正文"),
("《尔雅》谓之梫②者", "脚注标记「②」原无脚注正文"),
("俗乃重百济", "脚注标记「①」原无脚注正文"),
("惮散①之性", "脚注标记「①」原无脚注正文"),
("晒干了用。", "脚注标记「①」原无脚注正文"),
("这曲是七月里作。", "脚注标记「①」原无脚注正文"),
]
def build_checklist(text, secs2):
"""按章节结构生成待核对清单(固定 15 条:10 条事项 + 6 条脚注;行号指清洗版)"""
lines = text.split("\n")
def locate(ln):
sec = None
for s in secs2:
if s["行"] <= ln:
sec = s
if sec is None:
return "(篇外)", "—"
item = None
for e in sec["条目"]:
if e["行"] <= ln:
item = e
return sec["节"], (item["名"] if item else "节引言(首条目之前)")
def one(ln):
sec, item = locate(ln)
return sec, item
rows = []
for cat, anchor, prob, base, sug in CHECK_ITEMS:
hits = [i for i, l in enumerate(lines, 1) if anchor in l]
if len(hits) != 1:
raise SystemExit(f"待核对清单锚校验失败(命中 {len(hits)} 次):{anchor}")
sec, item = one(hits[0])
rows.append(dict(类型=cat, 节=sec, 条目=item, 行=hits[0], 问题=prob, 依据=base, 建议=sug))
for anchor, prob in CHECK_FOOTNOTES:
hits = [i for i, l in enumerate(lines, 1) if anchor in l]
if len(hits) != 1:
raise SystemExit(f"脚注清单锚校验失败(命中 {len(hits)} 次):{anchor}")
sec, item = one(hits[0])
rows.append(dict(类型="脚注", 节=sec, 条目=item, 行=hits[0], 问题=prob,
依据="标记处:" + lines[hits[0] - 1].strip()[:34],
建议="见判决(用户 2026-09-17)"))
rows.sort(key=lambda r: r["行"])
if len(rows) != len(VERDICTS):
raise SystemExit(f"清单条数 {len(rows)} 与判决记录 {len(VERDICTS)} 不一致")
return rows
# ── G 类:脚注处理(用户 2026-09-17 判决)────────────────────────────
# ("回填"/"清除", 原文锚, 脚注文字或 None, 依据)
FOOTNOTE_OPS = [
("回填", "《纲目》引《雷公炮炙论》云:“桂钉木根,其木即死”①",
"①桂为丁,以钉木中,其木即死;②梫,肉桂",
"用户核对原书(2026-09-17):本处 ①② 脚注文字补入,以〔脚注〕行附于本条段后"),
("回填", "惮散①之性", "①指涣散,耗散",
"用户核对原书(2026-09-17):脚注①释「惮散」为指涣散、耗散"),
("清除", "俗乃重百济①者", None, "用户判决(2026-09-17):本处注脚去除(不补文字)"),
("清除", "晒干了用。”①", None, "用户判决(2026-09-17):本处注记清除(不补文字)"),
("清除", "这曲是七月里作。”①", None, "用户判决(2026-09-17):本处注记清除(不补文字)"),
]
# ── G 类:人工核验判决记录(用户 2026-09-17 逐条判决,编号同待核对清单)──
VERDICTS = [
(1, "桂枝(木中木)", "回填脚注", "①桂为丁,以钉木中,其木即死"),
(2, "桂枝(木中木)", "回填脚注", "②梫,肉桂"),
(3, "旋覆花(火中木)", "无误", "「园而复下」原样保留"),
(4, "旋覆花(火中木)", "无误", "断行接合正确(同一引文一段)"),
(5, "人参(土中土)", "去注脚", "删除标记①,不补文字"),
(6, "枳实(金中水)", "无误", "半角「)」改全角「),」保留"),
(7, "芍药(金中木)", "无误", "引文内层引号改单引号保留"),
(8, "五味子(金中土、金中火)", "暂留待核", "名位「金中火」暂不改,待后续核对"),
(9, "第六节 节引言", "保留", "「整定稿」原样保留(不改)"),
(10, "淡豆豉(木中火、水中木)", "回填脚注", "①指涣散,耗散"),
(11, "薤白(水中土、水中金)", "改", "「薙」→「薤」确认(4 处)"),
(12, "附:", "保留", "附属位置保持原状"),
(13, "1.《齐民要术》酿白醪法释文", "去注脚", "删除标记①,不补文字"),
(14, "1.《齐民要术》酿白醪法释文", "保留", "「2.」保持正文段落,不升标题"),
(15, "1.《齐民要术》酿白醪法释文", "去注脚", "删除标记①,不补文字"),
]
def main():
text, md5_src = read_bin(SRC)
bendi, md5_bendi = read_bin(BENDI)
old, _ = read_bin(OLD_SRC)
events = []
# ── A 版式读数(用户已清,本例应全为 0)──
raw_lines = text.replace("\r\n", "\n").split("\n")
n_crlf = text.count("\r\n")
n_trail = sum(1 for l in raw_lines if l != l.rstrip())
n_lead = sum(1 for l in raw_lines if l != l.lstrip())
n_u3000 = sum(1 for l in raw_lines if "\u3000" in l)
a_stats = dict(CRLF=n_crlf, 行尾空格=n_trail, 行首空格=n_lead, 全角空格=n_u3000)
# ── B 接合 ──
for left, right, why in JOINS:
if text.count(left) != 1 or text.count(right) != 1:
raise SystemExit(f"接合锚校验失败:{left[:12]} / {right[:12]}")
joined = left + right
ln = next(i for i, l in enumerate(raw_lines, 1) if left in l)
text = text.replace(f"{left}\n\n{right}", joined)
if joined not in text:
raise SystemExit("接合未生效(可能非空行分隔,请检查源件)")
events.append(dict(行=ln, 锚=joined[-20:], 类型="B断行接合",
原=f"{left[-16:]} ⟂ {right[:16]}", 改=joined[-34:], 依据=why))
# ── C 讹字(仅薤白条内)──
para_key = "薙栽于金秋八月"
for bad, good, why in C_FIXES:
if para_key not in text:
raise SystemExit("薤白条锚校验失败")
n = text.count(bad)
if n == 0:
raise SystemExit("C 类规则无命中")
text = text.replace(bad, good)
ln_c = next(i for i, l in enumerate(raw_lines, 1) if para_key in l)
events.append(dict(行=ln_c, 锚="薤之所以能愈败疮之因由", 类型="C讹字修正",
原=f"{bad}({n} 处)", 改=good, 依据=why))
# ── D 标点 ──
for bad, good, why in PUNCT_FIXES:
if text.count(bad) != 1:
raise SystemExit(f"标点锚校验失败(命中 {text.count(bad)}):{bad}")
ln = next(i for i, l in enumerate(text.split("\n"), 1) if bad in l)
text = text.replace(bad, good)
events.append(dict(行=ln, 锚="金木易位(请参《伤寒论阴阳图说》),", 类型="D标点",
原=bad, 改=good, 依据=why))
if text.count(NESTED_OLD) != 1:
raise SystemExit(f"嵌套引号锚校验失败(命中 {text.count(NESTED_OLD)})")
ln = next(i for i, l in enumerate(text.split("\n"), 1) if NESTED_OLD in l)
text = text.replace(NESTED_OLD, NESTED_NEW)
events.append(dict(行=ln, 锚="以辛补之,酸泻之,肝苦急,急食甘以缓之,适其性而衰之也。’”", 类型="D标点",
原="《辅行诀》引文内层引号与外层同为双引号(配平 3/2)",
改="内层两处《经》云引文改为单引号 ‘…’", 依据="引号嵌套规范:内层用单引号,外层用双引号"))
# ── G 脚注处理(用户 2026-09-17 判决)──
for kind, anchor, note, why in FOOTNOTE_OPS:
hits = [i for i, l in enumerate(text.split("\n"), 1) if anchor in l]
if len(hits) != 1:
raise SystemExit(f"脚注锚校验失败(命中 {len(hits)} 次):{anchor[:24]}")
ln = hits[0]
if kind == "回填":
ls = text.split("\n")
ls.insert(ln, "")
ls.insert(ln + 1, "〔脚注〕" + note)
text = "\n".join(ls)
events.append(dict(行=ln + 1, 锚="〔脚注〕" + note, 类型="G脚注回填",
原="脚注标记(原文脚注文字缺失)", 改="〔脚注〕" + note, 依据=why))
else:
ls = text.split("\n")
before = ls[ln - 1]
ls[ln - 1] = re.sub(r"[①②③④⑤⑥⑦⑧⑨⑩]", "", before)
text = "\n".join(ls)
events.append(dict(行=ln, 锚=ls[ln - 1][-24:], 类型="G脚注清除",
原="…" + anchor[-16:], 改="删除脚注标记(不补文字)", 依据=why))
# ── H 标题层级 ──
out, heads, level_log = [], [], []
for i, l in enumerate(text.split("\n"), 1):
s = l.strip()
if s.startswith("#"):
bare = re.sub(r"^#+\s*", "", s)
lv, kind = None, None
for pat, L, K in H_RULES:
if pat.match(bare):
lv, kind = L, K
break
if lv is None:
raise SystemExit(f"未识别的标题(行 {i}):{s[:40]}")
newh = "#" * lv + " " + bare
heads.append(dict(层级=lv, 类型=kind, 标题=bare, 行=i, 原层级=len(re.match(r"^#+", s).group())))
if newh != s:
level_log.append((i, s, newh))
out.append(newh)
else:
out.append(l)
text = "\n".join(out)
# ── 事件行号回填为清洗版坐标(清洗版行号 = 报告与结构索引统一口径)──
flines = text.split("\n")
for e in events:
key = e.get("锚") or ""
if key:
hit = next((i for i, l in enumerate(flines, 1) if key in l), None)
if hit:
e["行"] = hit
e["备注"] = "行号指清洗版"
# ── 结构解析(章 → 节 → 条目/附)──
chap, secs2, cur_sec, cur_chap = None, [], None, None
for h in heads:
if h["类型"] == "章":
chap = h["标题"]
elif h["类型"] == "节":
cur_sec = dict(节=h["标题"], 章=chap, 行=h["行"], 条目=[])
secs2.append(cur_sec)
elif h["类型"] == "条目" and cur_sec is not None:
cur_sec["条目"].append(dict(序=re.match(r"^([一二三四五六七八九十]+)、", h["标题"]).group(1),
名=re.sub(r"^[一二三四五六七八九十]+、", "", h["标题"]), 行=h["行"],
名位=(re.search(r"(([^)]+))", h["标题"]).group(1)
if re.search(r"(([^)]+))", h["标题"]) else "")))
elif h["类型"] in ("条下附", "附内子目") and cur_sec is not None:
cur_sec["条目"].append(dict(序=None, 名=h["标题"], 行=h["行"], 名位="", 附属=h["类型"]))
# ── 自检 ──
errs, oks = [], []
cnt = {k: sum(1 for h in heads if h["类型"] == k) for k in ("章", "节", "条目", "条下附", "附内子目")}
expect = dict(章=1, 节=6, 条目=36, 条下附=1, 附内子目=1)
if cnt == expect:
oks.append("标题计数 章1/节6/条目36/条下附1/附内子目1(与预期一致)")
else:
errs.append(f"标题计数异常:{cnt},预期 {expect}")
seq_ok = True
for s in secs2:
got = [e["序"] for e in s["条目"] if "附属" not in e]
cn = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二", "十三"]
if got != cn[:len(got)]:
errs.append(f"{s['节']} 条目序号不连续:{got}")
seq_ok = False
if seq_ok:
oks.append("各节条目序号自「一、」连续(序数无缺无重)")
lv = [h["层级"] for h in heads]
jumps = [(i, lv[i - 1], lv[i]) for i in range(1, len(lv)) if lv[i] > lv[i - 1] + 1]
if lv[0] == 2 and not jumps:
oks.append("标题层级体系正确:起于章(2),逐级递降为 节(3)/条目(4)/附(5)/子目(6),无跳级")
else:
errs.append(f"标题层级异常:首层级 {lv[0]},跳级处 {jumps}")
# 名位互校(⇄ 底本药精表)
jing, mineral = parse_jing(bendi)
alias = {"姜": ["生姜", "干姜"], "甘草": ["甘草", "甘草炙"], "山茱萸": ["萸肉"],
"桂枝": ["桂"], "牡丹皮": ["丹皮"], "栝蒌": ["栝楼"]}
# 底本另论之药(药精表无对应名,须单列说明)
OTHER_NOTE = {"元参": "底本十三种药作「戎盐为火中土」;附录二小泻心汤散注「戎盐(玄参)」,"
"本书以元参(玄参)当戎盐之位,属底本另论"}
rows = []
for s in secs2:
for e in s["条目"]:
if "附属" in e or not e["名位"]:
continue
name = re.sub(r"(.*", "", e["名"])
ann = re.findall(r"[木火土金水]中[木火土金水]", e["名位"])
keys = alias.get(name, [name])
ref = set()
for k in keys:
ref |= jing.get(k, set())
if not ref:
rows.append((name, e["名位"], "—(底本药精表无对应名)",
"— " + OTHER_NOTE.get(name, "底本另论,待核")))
continue
miss = [a for a in ann if a not in ref]
rows.append((name, e["名位"], "/".join(sorted(ref)), "⚠ 与底本不符:" + "/".join(miss) if miss else "✅ 相符"))
# 标点/配平自检
def bal(t):
return {k: (t.count(a), t.count(b)) for k, a, b in
[("引号", "“", "”"), ("书名号", "《", "》"), ("括号", "(", ")"), ("单引号", "‘", "’")]}
b = bal(text)
half = re.findall(r"[\u4e00-\u9fff][,;:!?()]|[,;:!?()][\u4e00-\u9fff]", text)
brk = [i for i in range(len(text.split("\n")) - 1)
if text.split("\n")[i].strip() and text.split("\n")[i + 1].strip()
and not re.search(r"[。?!;:”》))】…—]$", text.split("\n")[i].strip())
and not text.split("\n")[i].strip().startswith("#")
and re.match(r"^[\u4e00-\u9fff]", text.split("\n")[i + 1].strip())
and not text.split("\n")[i + 1].strip().startswith("#")]
if b["引号"][0] == b["引号"][1] and b["书名号"][0] == b["书名号"][1] and b["括号"][0] == b["括号"][1]:
oks.append(f"标点配平:引号 {b['引号'][0]}/{b['引号'][1]}、书名号 {b['书名号'][0]}/{b['书名号'][1]}、"
f"括号 {b['括号'][0]}/{b['括号'][1]}")
else:
errs.append(f"标点不配平:{b}")
if not half:
oks.append("半角标点 0 处")
else:
errs.append(f"仍有半角标点 {len(half)} 处")
if not brk:
oks.append("断行候选 0 处(无句中截断)")
else:
errs.append(f"断行候选 {len(brk)} 处:{brk[:5]}")
if a_stats["行尾空格"] == 0 and a_stats["全角空格"] == 0 and a_stats["CRLF"] == 0:
oks.append("A 类版式残留 0(用户精清稿已清净)")
else:
errs.append(f"A 类残留:{a_stats}")
# 用户清理成效(与旧 OCR 原始版逐段对比)
def paras(t):
return [re.sub(r"\s+", "", p) for p in re.split(r"\n\s*\n", t) if p.strip()]
po, pn = paras(old), paras(text)
nj = "".join(pn)
removed = [p for p in po if p[:18] and p[:18] not in nj]
def cat(p):
if p.startswith("![]("):
return "图片引用"
if p.startswith("附录") and "……" in p:
return "目录行"
if re.match(r"^《辅行诀五脏用药法要》?药性探真", p):
tail = re.sub(r"^《辅行诀五脏用药法要》?药性探真", "", p)
if "……" in tail:
return "页眉/目录(含页码)"
if re.fullmatch(r"\d+", tail or ""):
return "页眉(含页码)"
if tail == "":
return "页眉"
return "页眉(异形:" + tail[:6] + ")"
return "其他(需人工复核)"
from collections import Counter
cats = Counter(cat(p) for p in removed)
user_fixes = [p for p in removed if p.startswith("##")]
# ── 落盘 ──
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, text if text.endswith("\n") else text + "\n")
md5_out = hashlib.md5(text.encode()).hexdigest()
struct = dict(
来源文件=os.path.basename(SRC), 来源文件md5=md5_src,
清洗版文件=os.path.basename(OUT_MD), 清洗版md5=md5_out,
定位="《药性探真》第一章 用户精清稿(用户在助手先前清理基础上进一步清理:删净页眉/页码/目录/图片引用,"
"接合断行,修正白戸浆→白酨浆)",
层级体例="章=##(二级)/节=###(三级)/条目=####(四级)/条下附=#####(五级)/附内子目=######(六级)",
底本互校=dict(底本文件=os.path.basename(BENDI), 底本md5=md5_bendi,
依据="用户 2026-09-16 定的基础文本;用于五行互含名位互证(本库内部互证,不作校改)"),
章=chap, 节=secs2,
标题层级改动=[dict(行=a, 原=b, 后=c) for a, b, c in level_log],
清理事件=events, 自检=oks + ["⚠ " + e for e in errs],
A类读数=a_stats, 名位互校=[dict(药名=n, 第1章标注=z, 底本=j, 判定=v) for n, z, j, v in rows],
用户清理成效=dict(与旧OCR原始版对比=dict(旧段落数=len(po), 现段落数=len(pn)),
删除类别=dict(cats),
用户改写或补入的标题行=user_fixes),
存疑=[dict(项=k, 说明=v) for k, v in SUSPECTS],
体例登记=[dict(项=k, 说明=v) for k, v in VERSION_NOTES],
)
wf(OUT_STRUCT, json.dumps(struct, ensure_ascii=False, indent=2) + "\n")
# ── 待核对清单(按章节结构)──
chk_rows = build_checklist(text, secs2)
snap_md5 = hashlib.md5(open(SNAP_SRC, "rb").read()).hexdigest() if os.path.exists(SNAP_SRC) else "—"
comp = lambda v: f"{hashlib.md5(v.encode()).hexdigest()}"
sql = lambda t: sum(1 for r in chk_rows if r["类型"] == t)
cl = ["# 《辅行诀五脏用药法要 药性探真》第 1 章 — 待核对清单(按章节结构)", "",
"> 用途:逐条列出需核对/需定处理方式的问题(固定 15 条),并登记用户判决。原判决栏「☐ 改/☐ 无误」已由用户 2026-09-17 判决结果填入。",
f"> 行号一律指**清洗版** `02_加工数据/药性探真_第1章_清洗版.md`(md5 `{md5_out}`);"
f"对象原件 `01_来源数据/《辅行诀五脏用药法要 药性探真》第1章.md`(md5 `{md5_src}`)。",
f"> 原件标题层级已于 2026-09-17 **就地优化**(改动前 md5 `{snap_md5}`;变更前副本="
"`01_来源数据/《辅行诀五脏用药法要 药性探真》第1章_层级优化前快照_20260917.md`,可随时回退)。",
f"> 共 **{len(chk_rows)}** 条:原书核对 {sql('原书核对')} · 修正确认 {sql('修正确认')} · "
f"标点规范确认 {sql('标点规范确认')} · 断行接合确认 {sql('断行接合确认')} · "
f"体例确认 {sql('体例确认')} · 脚注 {sql('脚注')}",
"> ✅ **本单 15 条已于 2026-09-17 全部判决完毕**(用户逐条判决);判决结果见「判决」栏,"
"回填与登记情况见 `药性探真_第1章_检查报告.md` 第七节。", "",
"## 汇总", "", "| 类型 | 条数 | 说明 |", "|------|------|------|",
f"| 原书核对 | {sql('原书核对')} | 需核纸质原书定字/定名位 |",
f"| 修正确认 | {sql('修正确认')} | 本次已改,请复核(可即时回改) |",
f"| 标点规范确认 | {sql('标点规范确认')} | 本次已规范,请复核 |",
f"| 断行接合确认 | {sql('断行接合确认')} | 本次已接合,请复核 |",
f"| 体例确认 | {sql('体例确认')} | 需定处理方式(现为保持原样) |",
f"| 脚注待补 | {sql('脚注待补')} | 缺脚注正文,待补来源 |", "",
"---", "", "## 逐条(按章节结构)", ""]
vmap = {n: (v, note) for n, _who, v, note in VERDICTS}
cur_sec, idx = None, 0
by_sec = {s["节"]: [] for s in secs2}
for r in chk_rows:
by_sec[r["节"]].append(r)
for s in secs2:
sec_name = s["节"]
cur_sec = sec_name
cl += [f"### {sec_name}", ""]
rows_here = by_sec.get(sec_name, [])
if not rows_here:
cl += ["(本节无待核对项)", ""]
continue
cl += ["| # | 条目 | 行(清洗版) | 类型 | 问题 | 依据 | 建议 | 判决 |",
"|---|------|------|------|------|------|------|------|"]
for r in rows_here:
idx = chk_rows.index(r) + 1
v, note = vmap.get(idx, ("—", ""))
cl.append(f"| {idx} | {r['条目']} | {r['行']} | {r['类型']} | {r['问题']} | {r['依据']} | "
f"{r['建议']} | ✅ {v}:{note} |")
cl.append("")
cl += ["", "---", "",
"> 判决后回填方式:①「原书核对」类若判定改字 → 在 `05_脚本工具/07_clean_ch1.py` 的 "
"`SUSPECTS`/`C_FIXES` 增改条目(标注「用户核对原书」)→ 重跑 07 刷新清洗版与报告;"
"②脚注文字补入后 → 在清洗脚本中加回填规则;③体例确认结果 → 记入检查报告第七节。",
"", f"> 📅 2026-09-17 —— 建单:{len(chk_rows)} 条待核对(按章/节/条目定位,行号指清洗版)。"]
wf(OUT_CHECK, "\n".join(cl) + "\n")
c = lambda k: sum(1 for e in events if e["类型"].startswith(k))
rep = ["# 《辅行诀五脏用药法要 药性探真》第 1 章 — 检查报告(用户精清稿)", "",
"> 📅 2026-09-17|对象:`01_来源数据/《辅行诀五脏用药法要 药性探真》第1章.md`"
f"(用户精清稿;md5 `{md5_src}`,{len(text)} 字符 / {len(text.splitlines())} 行)",
f"> 产物:`02_加工数据/药性探真_第1章_清洗版.md`(md5 `{md5_out}`)/本报告/"
"`02_加工数据/药性探真_第1章_结构.json`;脚本 `05_脚本工具/07_clean_ch1.py`(幂等)",
"> 用户指令:检查用户进一步清理后的文本,并把标题层级优化为**章二级、节三级,依次降低**。",
"> 🚫 原文件(01_来源数据/)正文一字未动;改写只落在清洗版,且每条带核校依据,可回改。",
f"> 🔧 **例外·标题层级**:按用户 2026-09-17 指令,**原件已就地**优化标题层级(只改井号、正文未动);"
f"变更前副本=`01_来源数据/《辅行诀五脏用药法要 药性探真》第1章_层级优化前快照_20260917.md`"
f"(md5 `{hashlib.md5(open(SNAP_SRC,'rb').read()).hexdigest() if os.path.exists(SNAP_SRC) else '—'}`),"
f"脚本 `05_脚本工具/08_retitle_src_ch1.py`(幂等、正文逐行校验)。", "",
f"> 📋 **待核对清单**:`02_加工数据/药性探真_第1章_待核对清单.md`(按章/节/条目列出,"
f"共 {len(build_checklist(text, secs2))} 条,供逐条判决)。", "",
"## 一、检查结论", "",
"| 项目 | 结果 |", "|------|------|",
f"| 版式残留(A) | **0**:CRLF {a_stats['CRLF']}/行尾空格 {a_stats['行尾空格']}/"
f"行首空格 {a_stats['行首空格']}/全角空格 {a_stats['全角空格']} |",
f"| 断行截断(B) | 残留 **1** 处(《图经》旋覆花引文被空行切断)→ 已接合 |",
f"| 讹字(C) | 残留 **1 类 4 处**(薤白条「薙」→「薤」)→ 已修正 |",
f"| 标点(D) | 半角「)」1 处、引文嵌套引号 1 段 → 已规范 |",
f"| 标题层级(H) | 章2/节3/条目4/附5/子目6;**源件已就地优化**(本次清洗版重排 {len(level_log)} 行) |",
f"| 引文规范 | 书名号 {b['书名号'][0]}/{b['书名号'][1]}、引号 {b['引号'][0]}/{b['引号'][1]}、"
f"(单引号 {b['单引号'][0]}/{b['单引号'][1]})、括号 {b['括号'][0]}/{b['括号'][1]} —— 全部配平 |",
f"| 脚注(G) | 标记 6 处:**回填脚注文字 3 处、清除标记 3 处**(用户 2026-09-17 判决) |",
f"| 名位互校 | 36 条目名位 ⇄ 底本二十五味药精/十三种药:"
f"{sum(1 for r in rows if r[3].startswith('✅'))} 条相符、"
f"{sum(1 for r in rows if r[3].startswith('⚠'))} 条待核、"
f"{sum(1 for r in rows if r[3].startswith('—'))} 条底本另论 |",
f"| 结构 | 章 1 / 节 6 / 条目 36 / 条下附 1 / 附内子目 1 |", "",
"## 二、用户清理成效核对(与旧 OCR 原始版逐段比对)", "",
f"- 旧 OCR 原始版(`药性探真_第一章_原始_20260915.md`)段落 {len(po)} → 精清稿 {len(pn)} 段",
"- 被删除的段落**全部为版面残留**,无正文内容丢失:", "",
"| 类别 | 条数 |", "|------|------|"]
for k, v in cats.most_common():
rep.append(f"| {k} | {v} |")
rep += ["", f"- 段落级比对结果:正文缺失 **{len([1 for p in removed if cat(p) == '其他'])}** 条"
"(「其他」类若有,即需人工复核)",
"- 用户改写/补入的标题行:"]
for h in user_fixes:
rep.append(f" - `{h}`(旧版作「白戸浆」,用户清稿修正为「白酨浆」并补齐该条标题)")
rep += ["", "> 核对方式:去空白后按段落首 18 字在新文本中检索;命中即视为保留。", "",
"## 三、本次修正(逐条留痕)", "",
"| 行(清洗版) | 类别 | 原文 | 处理后 | 核校依据 |", "|----|------|------|--------|---------|"]
for e in events:
rep.append(f"| {e.get('行','—')} | {e['类型']} | {e['原'].replace('|','|')} | "
f"{e['改'].replace('|','|')} | {e['依据'].replace('|','|')} |")
rep += ["", "## 四、标题层级优化", "",
"| 层级 | Markdown | 对象 | 例(本件) |", "|------|----------|------|-----------|",
"| 二级 | `##` | 章 | 第一章 五脏补泻草木方例用药释义 |",
"| 三级 | `###` | 节 | 第一节 肝木辛味门 … 第六节 从火土同治论心病草木方例用药简释 |",
"| 四级 | `####` | 条目(味) | 一、桂枝(木中木)… |",
"| 五级 | `#####` | 条下附 | 附: |",
"| 六级 | `######` | 附内子目 | 1. 《齐民要术》酿白醪法释文 |",
"", f"共重排 {len(level_log)} 行标题(源件 45 行全部为二级)。", "",
"## 五、名位互校(第一章 36 条目 ⇄ 底本《辅行诀》药精表)", "",
"| 条目 | 第一章标注 | 底本《辅行诀》名位 | 判定 |", "|------|-----------|------------------|------|"]
for n, z, j, v in rows:
rep.append(f"| {n} | {z} | {j} | {v} |")
rep += ["", "> 底本依据:二十五味药精(味辛皆属木……)+ 十三种药(通草为木中土……);"
"本项属用户 2026-09-16 许可的「本库内部互证」用法之一(五行互含名位核对),**不改原文**。", "",
"## 六、存疑登记(原文不改,待核原书)", "", "| 项 | 说明 |", "|----|------|"]
for k, v in SUSPECTS:
rep.append(f"| {k} | {v} |")
rep += ["", "## 七、人工核验判决记录(用户 2026-09-17 逐条判决,编号同待核对清单)", "",
"| # | 条目 | 判决 | 说明 |", "|---|------|------|------|"]
for n, who, v, note in VERDICTS:
rep.append(f"| {n} | {who} | **{v}** | {note} |")
rep += ["", "> 回填情况:「回填脚注」3 条已以「〔脚注〕…」行附于本条段后;「去注脚」3 条已删标记、不补文字;"
"「改」1 条(薤白「薙」→「薤」)已在清洗版生效;「无误/保留」8 条原样保留;"
"「暂留待核」1 条(五味子名位「金中火」)待后续核对。", "",
"## 八、体例登记(不校改)", "", "| 项 | 说明 |", "|----|------|"]
for k, v in VERSION_NOTES:
rep.append(f"| {k} | {v} |")
rep += ["", "## 九、结构(清洗版行号)", "", "| 节 | 条目数 | 行号 |", "|----|-------|------|"]
for s in secs2:
items = [e for e in s["条目"] if "附属" not in e]
rep.append(f"| {s['节']} | {len(items)} | {items[0]['行'] if items else '—'}–"
f"{items[-1]['行'] if items else '—'} |")
rep += ["", "| 节 | 序 | 条目(名位) | 清洗版行 |", "|----|----|-------------|---------|"]
for s in secs2:
for e in s["条目"]:
rep.append(f"| {s['节'].split(' ')[0]} | {e['序'] or '附'} | {e['名']} | {e['行']} |")
rep += ["", "## 十、自检结果(脚本自动)", ""]
for o in oks:
rep.append(f"- ✅ {o}")
for e in errs:
rep.append(f"- ⚠ **{e}**")
rep += ["", "---", "",
f"> 📅 2026-09-17 —— 建件:检查用户精清稿并优化标题层级;"
f"B {c('B')}/C {c('C')}/D {c('D')} 处修正,A 类残留 0,名位互校 "
f"{sum(1 for r in rows if r[3].startswith('✅'))} 相符、{sum(1 for r in rows if r[3].startswith('⚠'))} 待核;"
f"存疑 {len(SUSPECTS)} 项、体例登记 {len(VERSION_NOTES)} 项。"]
wf(OUT_REPORT, "\n".join(rep) + "\n")
print("清洗版:", OUT_MD, os.path.getsize(OUT_MD), "bytes")
print("报告 :", OUT_REPORT, os.path.getsize(OUT_REPORT), "bytes")
print("结构 :", OUT_STRUCT, os.path.getsize(OUT_STRUCT), "bytes")
print("待核单:", OUT_CHECK, os.path.getsize(OUT_CHECK), "bytes")
print(f"B {c('B')} | C {c('C')} | D {c('D')} | G 回填 {c('G脚注回填')} / 清除 {c('G脚注清除')} | 标题重排 {len(level_log)} 行 | A残留 {a_stats}")
for o in oks:
print(" ✅", o)
for e in errs:
print(" ⚠", e)
if __name__ == "__main__":
main()
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
辅行诀五脏用药法要-药性探真 资料库 ETL 第 8 步(2026-09-17)
**在 01_来源数据 原件中优化标题层级**(用户 2026-09-17 指令:「直接在 01 原件里改标题层级」)
对象:01_来源数据/《辅行诀五脏用药法要 药性探真》第1章.md(用户精清稿)
改动范围:**只改标题行的井号层级**,正文一字不动(逐行校验非标题行完全一致)
层级体例:章=##(二级)/节=###(三级)/条目=####(四级)/条下附=#####(五级)/附内子目=######(六级)
留痕:
① 变更前原件快照另存 `01_来源数据/《辅行诀五脏用药法要 药性探真》第1章_层级优化前快照_20260917.md`
(内容 = 变更前逐字节副本,md5 与旧值一致,供随时回退);
② 变更前后 md5 记入本脚本 stdout 与 `02_加工数据/药性探真_第1章_检查报告.md`(由 07 脚本刷新)。
幂等:可重复运行;已优化过的文件重跑无副作用(无标题需改时输出 0 处)。
"""
import os, re, hashlib, shutil
BASE = os.path.expanduser("~/Documents/ai_agent_scraper_study/data/辅行诀五脏用药法要-药性探真")
SRC = os.path.join(BASE, "01_来源数据/《辅行诀五脏用药法要 药性探真》第1章.md")
SNAP = os.path.join(BASE, "01_来源数据/《辅行诀五脏用药法要 药性探真》第1章_层级优化前快照_20260917.md")
H_RULES = [
(re.compile(r"^第[一二三四五六七八九十]+章[  ]"), 2, "章"),
(re.compile(r"^第[一二三四五六七八九十]+节[  ]"), 3, "节"),
(re.compile(r"^[一二三四五六七八九十]+、"), 4, "条目"),
(re.compile(r"^附:"), 5, "条下附"),
(re.compile(r"^\d+\.\s*《"), 6, "附内子目"),
]
def md5b(b):
return hashlib.md5(b).hexdigest()
def main():
raw = open(SRC, "rb").read()
md5_before = md5b(raw)
text = raw.decode("utf-8")
lines = text.split("\n")
# 变更前快照(仅在快照不存在时保存,避免二次运行覆盖真正的原始副本)
if not os.path.exists(SNAP):
shutil.copy2(SRC, SNAP)
print("快照已存:", os.path.basename(SNAP), md5b(open(SNAP, "rb").read()))
out, changes, unrecognized = [], [], []
for i, l in enumerate(lines, 1):
s = l.strip()
if s.startswith("#"):
bare = re.sub(r"^#+\s*", "", s)
lv, kind = None, None
for pat, L, K in H_RULES:
if pat.match(bare):
lv, kind = L, K
break
if lv is None:
unrecognized.append((i, s[:40]))
out.append(l)
continue
new = "#" * lv + " " + bare
if new != s:
changes.append((i, kind, s, new))
out.append(new)
else:
out.append(l)
if unrecognized:
raise SystemExit(f"存在未识别标题,未改动任何内容:{unrecognized}")
new_text = "\n".join(out)
md5_after = md5b(new_text.encode("utf-8"))
# 正文一致性校验:去掉标题井号后逐行比对
def strip_h(t):
return [re.sub(r"^#+\s*", "", x) for x in t.split("\n")]
if strip_h(new_text) != strip_h(text):
raise SystemExit("正文一致性校验失败:非标题内容被改动,已中止")
if len(new_text) != len(text) + sum(len(n) - len(o) for _, _, o, n in changes):
raise SystemExit("长度校验失败,已中止")
with open(SRC, "w", encoding="utf-8") as f:
f.write(new_text); f.flush(); os.fsync(f.fileno())
print(f"原件标题层级:改动 {len(changes)} 行")
for ln, kind, old, new in changes:
print(f" L{ln:<4} {kind:<5} {old[:24]:<26} → {new[:26]}")
print(f"md5 变更前 {md5_before} → 变更后 {md5_after}")
print("正文校验:非标题行逐行一致(一字未动)")
if __name__ == "__main__":
main()
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
辅行诀五脏用药法要-药性探真 资料库 ETL 第 9 步(2026-09-17):**归档一致性审计**
用途:任意时刻复核「文档里引用的 md5 / 文件是否存在 / 有无残留」是否与库内实际一致。
检查项:
① 全库 md5 索引(排除 . 与 __pycache__)
② 关键文档中出现的完整 md5(32 位)与缩写 md5(前 8 位 + …)能否对应到现有文件
③ 若为历史值(确属旧版本留痕),输出 ⚠ 提示但不算错;无法对应且非历史值者列 ❌
④ 残留检查:__pycache__、.bak/.tmp/~ 备份、空目录
只读,不修改任何文件;退出码:有 ❌ 时返回 1,便于脚本化校验。
"""
import os, re, sys, hashlib, glob
BASE = os.path.expanduser("~/Documents/ai_agent_scraper_study/data/辅行诀五脏用药法要-药性探真")
DOCS = ["README.md", "01_来源数据/来源说明.md", "01_来源数据/校核参考/README.md",
"02_加工数据/药性探真_第1章_检查报告.md", "02_加工数据/药性探真_第1章_待核对清单.md",
"02_加工数据/药性探真_第2章_检查报告.md", "02_加工数据/药性探真_第2章_待核对清单.md",
"02_加工数据/药性探真_第3章_检查报告.md", "02_加工数据/药性探真_第3章_待核对清单.md",
"02_加工数据/药性探真_第1-3章_合并结构.json",
"02_加工数据/整订稿清洗报告.md", "02_加工数据/印刷本清洗报告.md", "02_加工数据/文本清洗报告.md",
"02_加工数据/原文清洗报告.md", "02_加工数据/人工核对清单.md", "02_加工数据/印刷本人工核对清单.md",
"03_关联融合/README.md", "03_关联融合/整订稿核对报告.md", "03_关联融合/整订稿-底本_方目对照表.md",
"03_关联融合/药性探真-底本_药名方目对照表.md", "03_关联融合/药性探真-底本_核对报告.md",
"03_关联融合/药性探真-底本_药名方目对照.json",
"04_分析报告/README.md"]
# 已知历史值(旧版本留痕,出现即视为正常)
KNOWN_HISTORIC = {
"ce800838b94512a00fbe8375570eb180", # 药性探真_第一章_清洗版.md 旧 md5(2026-09-17 重跑前)
"e550bb651e026fb3a314a90faaa9937e", # 底本基础文本旧 md5(人工核验回填前)
"0cea9938547ac8c1a0e9479cbda15e47", # 《…》第1章.md 标题层级优化前(快照现存库内,故通常能直接命中)
"29097b725de0e3e5829a5c0acb132074", # 第2章清洗版(「拟正字」6 条 11 处订正前;README 更新日志(五)全值留痕)
}
# 旧值缩写(更新日志里刻意留痕的「改前 → 改后」写法,出现即视为正常)
HISTORIC_PREFIX = {"5642e2d0", "ce800838", "e550bb65",
"65c63d90", "978d57c9", # 第2章 检查报告/待核对清单 第1批判决回填时值
"52f79d41", "d76d710d", # 第2章 检查报告/待核对清单 第2批判决回填时值
"44eeca76", # 第2章 结构.json 第2批判决回填时值
"8a377e1e", # 第2章 清洗版(第1–3批判决期间值,第4批据核改后变更)
"cd754c58", "45de11c7", "ecf80e86", # 第2章 报告/清单/结构 第3批判决回填时值
"e490eb85", "234bd0a3", "12d75c0e", # 第2章 报告/清单/脚本 第4批判决回填时值
"859450c5", "d64de6e5", "2b571161", # 第2章 报告/清单/脚本 「保留」归一(词表)时值
"3511f9b3", "20a2d8c2", # 第2章 脚本 7.x 重排/待补正字标记中间值
"29097b72", "c51ad6da", "8d6e4e24", # 第2章 清洗版/报告/清单 「拟正字」落盘前时值
"84ab1c43", "b435e320", # 第2章 结构/脚本 同期时值
"bee344b7", "ff367101", "93e003f8", # 第2章 报告/结构/脚本 行号更正前时值
"433a6a0a", "b0c7ff33", "db0b6227", # 第2章 报告/结构/脚本 「原书用字」栏补入前时值
"347aecce", "a0094713", # 第1章 报告/结构 引文异文登记前时值
"09087b8e", "b5d23ecc", "8926e4fe", "196f3678", # 第2章 报告/清单/结构/脚本 层级优化前时值
"716e4c56", "4acc6455", # 第1章 报告/结构 引文异文登记(待核)时值
"d838a537", # 07_clean_ch1.py 引文登记(待核)时值
"3e4f50b3", "c970c608", "ed85e822", # 第3章 报告/结构/脚本 判决回填前时值(更新日志(十三)留痕)
"2c5378bd", "c18175f8", "575f94a7", "6e01d28e", "bcd90aa3", # 第3章 第2批判决前时值((十四)(十五) 留痕)
"e68c05cb", "05cdbbf5", "9e4b2aad", "7833650f", "b0783fb3", # 第3章 第3批判决前时值((十五)(十六) 留痕)
"194a913c", "86fb7658", "3fd5855b", "61c2fea3", "42c16ad4",
"71d6ac95", "e542d14c", # 第3章 报告/脚本 中间时值((十七)留痕)
"b221786e", "391a6454", "8cf5aecb", "dcd37b8a", "ac7d8774",
"f191685e", "ac511120", # 第3章 清单/脚本 中间时值((十八)留痕) # 第3章 第5批判决前时值((十七)(十八) 留痕) # 第3章 第4批判决前时值((十六)(十七) 留痕)
"72482eed", "af0e2cdb", "74cb970a", # 第3章 清单/结构/脚本 存疑补录前时值(更新日志(十三)前中间值)
"f9611535", "8b40d02f", "672e2133", "48752a23", # 第3章 报告/脚本 行标与体例精简中间值
"c406a03f", "70b00b0f", "76063838", "97a7b47a", # 第3章 报告/清单/结构/脚本 第 6 批判决(#56)前时值(更新日志(十八)「新值」→(二十)「改前」)
"04919121", "7988ddb9", "8b3ce170", "32399f74", # 第3章 报告/清单/结构/脚本 第 7 批(源件层级优化等)前时值(更新日志(二十)「新值」→(二十一)「改前」)
}
def md5_of(p):
return hashlib.md5(open(p, "rb").read()).hexdigest()
def main():
files = {}
for root, dirs, fs in os.walk(BASE):
dirs[:] = [d for d in dirs if not d.startswith((".", "__"))]
for f in fs:
p = os.path.join(root, f)
files[os.path.relpath(p, BASE)] = md5_of(p)
full = {v: k for k, v in files.items()}
pref = {}
for k, v in files.items():
pref.setdefault(v[:8], []).append(k)
print(f"库内文件 {len(files)} 个\n")
bad = 0
for d in DOCS:
p = os.path.join(BASE, d)
if not os.path.exists(p):
print(f" ❌ 文档缺失:{d}")
bad += 1
continue
txt = open(p, encoding="utf-8").read()
for m in sorted(set(re.findall(r"\b[0-9a-f]{32}\b", txt))):
if m in full:
print(f" ✅ {d[:34]:<36} {m[:12]}… → {full[m]}")
elif m in KNOWN_HISTORIC:
print(f" ⚠ {d[:34]:<36} {m[:12]}… → 历史值(旧版本留痕,正常)")
else:
print(f" ❌ {d[:34]:<36} {m[:12]}… → 库内无此 md5")
bad += 1
for m in sorted(set(re.findall(r"`([0-9a-f]{8})…`", txt))):
if m in pref:
print(f" ✅ {d[:34]:<36} {m}… → {'、'.join(pref[m])}")
elif m in HISTORIC_PREFIX:
print(f" ⚠ {d[:34]:<36} {m}… → 历史值缩写(更新日志「改前」留痕,正常)")
else:
print(f" ❌ {d[:34]:<36} {m}… → 库内无此文件(缩写)")
bad += 1
print("\n残留检查:")
pyc = glob.glob(os.path.join(BASE, "**/__pycache__"), recursive=True)
tmp = [f for f in files if f.endswith((".bak", ".tmp", "~"))]
empt = [os.path.relpath(r, BASE) for r, ds, fs in os.walk(BASE) if not ds and not fs
and not r.endswith((".git",))]
print(f" __pycache__: {pyc or '无'}")
print(f" 备份/临时文件: {tmp or '无'}")
print(f" 空目录: {empt or '无'}")
print(f"\n审计结果:{'通过(无 ❌)' if bad == 0 else f'有 {bad} 处 ❌ 需修正'}")
return 0 if bad == 0 else 1
if __name__ == "__main__":
sys.exit(main())
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
辅行诀五脏用药法要-药性探真 资料库 ETL 第 11 步(2026-09-20)
《药性探真》第 2 章 **原件标题层级就地优化**(对应第 1 章之 08_retitle_src_ch1.py)
背景:
第 1 章原件曾按**用户 2026-09-17 明确指令**就地优化标题层级(只改井号、正文逐行校验未动)。
第 2 章原件标题 25 行原样为「##」(章/节/条目不分级)。README 归档原则为
「01_来源数据 不可修改」,故本次**默认不做改动**,只在本脚本中备好同办途径。
用法:
python3 05_脚本工具/11_retitle_src_ch2.py # 默认 dry-run:只打印将改动的行,不落盘
python3 05_脚本工具/11_retitle_src_ch2.py --apply # 真正就地优化(先留快照)
层级体例(同第 1 章):章=二级(##)/节=三级(###)/条目=四级(####)
安全保障:
① --apply 前先写逐字节快照 《…》第2章_层级优化前快照_YYYYMMDD.md(已存在则不覆盖)
② 落盘前断言「去标题行后的正文逐行完全相同」(正文一字未动)
③ 幂等:已优化后再跑,改动 0 行
"""
import os, re, sys, hashlib, datetime
BASE = os.path.expanduser("~/Documents/ai_agent_scraper_study/data/辅行诀五脏用药法要-药性探真")
SRC = os.path.join(BASE, "01_来源数据/《辅行诀五脏用药法要 药性探真》第2章.md")
SNAP_FMT = os.path.join(BASE, "01_来源数据/《辅行诀五脏用药法要 药性探真》第2章_层级优化前快照_%s.md")
H_RULES = [
(re.compile(r"^第[一二三四五六七八九十]+章[  ]"), 2, "章"),
(re.compile(r"^第[一二三四五六七八九十]+节[  ]"), 3, "节"),
(re.compile(r"^[一二三四五六七八九十]+、"), 4, "条目"),
]
def strip_headings(t):
return [l for l in t.split("\n") if not l.startswith("#")]
def plan(text):
"""返回 (新文本, 改动清单);未识别标题直接报错。"""
out, changes = [], []
for i, l in enumerate(text.split("\n"), 1):
if not l.startswith("#"):
out.append(l)
continue
bare = re.sub(r"^#+\s*", "", l)
lv = None
for pat, L, _K in H_RULES:
if pat.match(bare):
lv = L
break
if lv is None:
raise SystemExit(f"未识别的标题(行 {i}):{l[:60]}")
newh = "#" * lv + " " + bare
if newh != l:
changes.append((i, l, newh))
out.append(newh)
return "\n".join(out), changes
def main():
apply = "--apply" in sys.argv
raw = open(SRC, "rb").read()
text = raw.decode("utf-8")
md5_before = hashlib.md5(raw).hexdigest()
new, changes = plan(text)
assert strip_headings(text) == strip_headings(new), "正文行被改动,脚本中止"
print(f"对象:{SRC}")
print(f"md5(当前):{md5_before}")
print(f"标题行合计:{sum(1 for l in text.split(chr(10)) if l.startswith('#'))} 行;需改动 {len(changes)} 行")
for i, a, b in changes:
print(f" L{i}: {a} -> {b}")
if not changes:
print("✅ 无需改动(已是最优层级或已优化过)——幂等检查通过")
return
if not apply:
print("\n(dry-run:未落盘。如需就地优化,加 --apply)")
return
snap = SNAP_FMT % datetime.date.today().strftime("%Y%m%d")
if os.path.exists(snap):
print(f"快照已存在,不覆盖:{snap}")
else:
with open(snap, "wb") as f:
f.write(raw); f.flush(); os.fsync(f.fileno())
print(f"快照(改动前逐字节副本):{snap}(md5 {md5_before})")
with open(SRC, "wb") as f:
f.write(new.encode("utf-8")); f.flush(); os.fsync(f.fileno())
md5_after = hashlib.md5(open(SRC, "rb").read()).hexdigest()
print(f"已就地优化标题层级:{SRC}\n md5 {md5_before} -> {md5_after}")
# 幂等复验
again, ch2 = plan(open(SRC, encoding="utf-8").read())
print(f"幂等复验:再跑需改动 {len(ch2)} 行(应为 0)")
assert not ch2
if __name__ == "__main__":
main()
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
辅行诀五脏用药法要-药性探真 资料库 ETL 第 13 步(2026-09-21)
《药性探真》第 3 章 **原件标题层级就地优化**(对应第 1 章之 08_retitle_src_ch1.py、
第 2 章之 11_retitle_src_ch2.py)
背景:
第 1 章原件曾按**用户 2026-09-17 明确指令**、第 2 章按**用户 2026-09-20 明确指令**
就地优化标题层级(均只改井号、正文逐行校验未动,并留逐字节快照)。
第 3 章原件标题体系:**章为「#」,节/条目/子目全部为「##」**(未分级,共 65 行);
另有 2 行条目题名未标井号(源行 133/311,属正文形式,**本脚本不涉及**——
清洗版已补标为四级,源件保持原样)。
README 归档原则为「01_来源数据 不可修改」,故本脚本**默认不做改动**,
只备好与前两章同办的途径;是否执行**候用户指令**。
用法:
python3 05_脚本工具/13_retitle_src_ch3.py # 默认 dry-run:只打印将改动的行,不落盘
python3 05_脚本工具/13_retitle_src_ch3.py --apply # 真正就地优化(先留快照)
层级体例(同第 1/2 章,本件多一级子目):章=二级(##)/节=三级(###)/条目=四级(####)/子目=五级(#####)
安全保障:
① --apply 前先写逐字节快照 《…》第3章_层级优化前快照_YYYYMMDD.md(已存在则不覆盖)
② 落盘前断言「去标题行后的正文逐行完全相同」(正文一字未动)
③ 幂等:已优化后再跑,改动 0 行
④ 落盘后复跑 `12_clean_ch3.py`,清洗版 md5 应**保持不变**(层级优化对清洗版零影响)
"""
import os, re, sys, hashlib, datetime
BASE = os.path.expanduser("~/Documents/ai_agent_scraper_study/data/辅行诀五脏用药法要-药性探真")
SRC = os.path.join(BASE, "01_来源数据/《辅行诀五脏用药法要 药性探真》第3章.md")
SNAP_FMT = os.path.join(BASE, "01_来源数据/《辅行诀五脏用药法要 药性探真》第3章_层级优化前快照_%s.md")
H_RULES = [
(re.compile(r"^第[一二三四五六七八九十]+章[  ]"), 2, "章"),
(re.compile(r"^第[一二三四五六七八九十]+节[  ]"), 3, "节"),
(re.compile(r"^[一二三四五六七八九十]+、"), 4, "条目"),
(re.compile(r"^\d+\.\s*"), 5, "子目"),
]
def strip_headings(t):
return [l for l in t.split("\n") if not l.startswith("#")]
def plan(text):
"""返回 (新文本, 改动清单);未识别的标题直接报错。"""
out, changes = [], []
for i, l in enumerate(text.split("\n"), 1):
if not l.startswith("#"):
out.append(l)
continue
bare = re.sub(r"^#+\s*", "", l)
lv = None
for pat, L, _K in H_RULES:
if pat.match(bare):
lv = L
break
if lv is None:
raise SystemExit(f"未识别的标题(行 {i}):{l[:60]}")
newh = "#" * lv + " " + bare
if newh != l:
changes.append((i, l, newh))
out.append(newh)
return "\n".join(out), changes
def main():
apply = "--apply" in sys.argv
raw = open(SRC, "rb").read()
text = raw.decode("utf-8")
md5_before = hashlib.md5(raw).hexdigest()
new, changes = plan(text)
assert strip_headings(text) == strip_headings(new), "正文行被改动,脚本中止"
print(f"对象:{SRC}")
print(f"md5(当前):{md5_before}")
print(f"标题行合计:{sum(1 for l in text.split(chr(10)) if l.startswith('#'))} 行;"
f"需改动 {len(changes)} 行")
for i, a, b in changes:
print(f" L{i}: {a} -> {b}")
if not changes:
print("✅ 无需改动(已是最优层级或已优化过)——幂等检查通过")
return
if not apply:
print("\n(dry-run:未落盘。如需就地优化,加 --apply)")
return
snap = SNAP_FMT % datetime.date.today().strftime("%Y%m%d")
if os.path.exists(snap):
print(f"快照已存在,不覆盖:{snap}")
else:
with open(snap, "wb") as f:
f.write(raw); f.flush(); os.fsync(f.fileno())
print(f"快照(改动前逐字节副本):{snap}(md5 {md5_before})")
with open(SRC, "wb") as f:
f.write(new.encode("utf-8")); f.flush(); os.fsync(f.fileno())
md5_after = hashlib.md5(open(SRC, "rb").read()).hexdigest()
print(f"已就地优化标题层级:{SRC}\n md5 {md5_before} -> {md5_after}")
again, ch2 = plan(open(SRC, encoding="utf-8").read())
print(f"幂等复验:再跑需改动 {len(ch2)} 行(应为 0)")
assert not ch2
if __name__ == "__main__":
main()
@@ -0,0 +1,275 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
辅行诀五脏用药法要-药性探真 资料库 ETL 第 14 步(2026-09-22):
《〈辅行诀五脏用药法要〉药性探真》第 1/2/3 章**清洗版** → 合并为单一清洗版正文,入 02_加工数据。
用户指令(2026-09-22):
「《辅行诀五脏用药法要 药性探真》第1-3章已经清理完成,合并后导入资料库,
级别低于 …/02_加工数据/辅行诀脏腑用药法要.md」
→ 合并(本脚本)+ 定级登记(README/来源说明):本合并本为**现代研究文献**,
**引用优先级低于《辅行诀脏腑用药法要》(参考本),更低于《辅行诀五脏用药法要》(底本)**。
原则(与库内既有规范一致):
① **只合并,不改字**:三章清洗版正文**一字未增删**(改字一律回到 07/10/12 清洗脚本,本脚本不设 FIXES 表);
② **可复核**:合并正文 == 三章清洗版正文按章序拼接(自检断言,逐字节);
③ **来源 md5 钉定**:任一章清洗版与登记值不符即报错(防静默合并到错版本);
④ **幂等**:重跑覆盖,输出与库内一致时不改写文件;
⑤ **行号可回溯**:产物结构 JSON 给出「合卷行号 ⇄ 各章清洗版行号」偏移对照。
产物:
02_加工数据/药性探真_第1-3章_合并清洗版.md (合卷正文)
02_加工数据/药性探真_第1-3章_合并结构.json (章/节/条目/子目 四级 + 合卷行号锚 + 各章偏移)
用法:
cd ~/Documents/ai_agent_scraper_study/data/辅行诀五脏用药法要-药性探真
python3 05_脚本工具/14_merge_ch123.py # 生成/刷新(幂等)
python3 05_脚本工具/14_merge_ch123.py --check # 只校验,不写盘
"""
import os
import re
import sys
import json
import hashlib
BASE = os.path.expanduser("~/Documents/ai_agent_scraper_study/data/辅行诀五脏用药法要-药性探真")
OUT_MD = "02_加工数据/药性探真_第1-3章_合并清洗版.md"
OUT_JSON = "02_加工数据/药性探真_第1-3章_合并结构.json"
# 合卷标题(文书级标识行;**不由来源正文产生**,系合卷所必需,正文本身一字未增删)
TITLE = "# 《辅行诀五脏用药法要 药性探真》第一至三章"
# 三章清洗版(顺序即合卷章序):相对路径 / 登记 md5 / 章标签
SRC = [
("02_加工数据/药性探真_第1章_清洗版.md", "a20b17b7589255889256426b795a86d9", "第一章"),
("02_加工数据/药性探真_第2章_清洗版.md", "ac1a4d325f971279bc583324c95408d8", "第二章"),
("02_加工数据/药性探真_第3章_清洗版.md", "5f1fb57c02de5d370a8aff51013062bb", "第三章"),
]
H_CHAPTER = re.compile(r"^##\s+(第[一二三四五六七八九十]+章)\s*(.*)$")
H_SECTION = re.compile(r"^###\s+(第[一二三四五六七八九十]+节)\s*(.*)$")
H_ENTRY = re.compile(r"^####\s+(.+)$")
H_SUB = re.compile(r"^#####\s+(.+)$")
H_SUB6 = re.compile(r"^######\s+(.+)$")
def md5_of(p):
return hashlib.md5(open(p, "rb").read()).hexdigest()
def read_text(rel):
"""CRLF-safe:文本模式会吞 CRLF,一律按字节读入再解码。"""
with open(os.path.join(BASE, rel), "rb") as f:
return f.read().decode("utf-8")
def load_sources():
"""读入三章清洗版 + 断言(章首行、无一级标题、md5 相符)。"""
srcs, errs = [], []
for rel, want, label in SRC:
p = os.path.join(BASE, rel)
if not os.path.exists(p):
errs.append(f"缺文件:{rel}")
continue
got = md5_of(p)
if got != want:
errs.append(f"md5 不符:{rel}\n 登记 {want}\n 实测 {got}"
f"\n → 清洗版已被重跑刷新,请先核对来源,再更新本脚本 SRC 表")
continue
txt = read_text(rel)
first = txt.split("\n")[0].strip()
m = H_CHAPTER.match(first)
if not m:
errs.append(f"章首行不是二级章标题:{rel} → {first!r}")
continue
if m.group(1) != label:
errs.append(f"章标签不符:{rel} 首行作 {m.group(1)},登记 {label}")
continue
h1 = [ln for ln in txt.split("\n") if ln.startswith("# ")]
if h1:
errs.append(f"来源含一级标题(合卷后与文书标题同级):{rel} → {h1[:3]}")
continue
if not txt.endswith("\n"):
errs.append(f"来源末行无换行:{rel}")
continue
srcs.append({"章": label, "文件": rel, "md5": got,
"行数": len(txt.split("\n")) - 1, # 末行为空行不计
"正文": txt})
return srcs, errs
def build_merged(srcs):
body = "\n\n".join(s["正文"].rstrip("\n") for s in srcs) + "\n"
return TITLE + "\n\n" + body, body
def parse_structure(srcs, body):
"""按合卷正文解析 章/节/条目/子目 四级,并给出「合卷行号 → 各章清洗版行号」回溯。"""
# 各章在合卷正文中的起始行(1-based):标题行 + 空行 + 各章正文
offsets, ln = {}, 1 + 2 # 1 行文书标题 + 1 空行
for s in srcs:
offsets[s["章"]] = ln - 1 # 合卷行号 = 该章清洗版行号 + offset
ln += s["行数"] + 1 # 章正文行数 + 1 空行分隔
lines = body.split("\n")
chapters, cur_ch, cur_sec, cur_ent = [], None, None, None
stats = {"章": 0, "节": 0, "条目": 0, "子目": 0, "附内子目": 0}
for i, raw in enumerate(lines, 1): # i = 合卷行号(正文第 1 行 = 合卷第 3 行)
if not raw.strip():
continue
mc = H_CHAPTER.match(raw)
if mc:
cur_ch = {"章": (mc.group(1) + " " + mc.group(2)).strip(),
"行": i, "节": []}
chapters.append(cur_ch)
stats["章"] += 1
cur_sec = cur_ent = None
continue
ms = H_SECTION.match(raw)
if ms:
if cur_ch is None:
raise SystemExit(f"❌ 第 {i} 行出现节标题但无章标题:{raw[:40]}")
cur_sec = {"节": (ms.group(1) + " " + ms.group(2)).strip(),
"行": i, "条目": []}
cur_ch["节"].append(cur_sec)
stats["节"] += 1
cur_ent = None
continue
me = H_ENTRY.match(raw)
if me:
if cur_sec is None:
raise SystemExit(f"❌ 第 {i} 行出现条目标题但无节标题:{raw[:40]}")
cur_ent = {"条目": me.group(1).strip(), "行": i, "子目": [], "附内子目": []}
cur_sec["条目"].append(cur_ent)
stats["条目"] += 1
continue
msub = H_SUB.match(raw)
if msub:
if cur_ent is None:
raise SystemExit(f"❌ 第 {i} 行出现子目标题但无条目标题:{raw[:40]}")
cur_ent["子目"].append({"子目": msub.group(1).strip(), "行": i})
stats["子目"] += 1
continue
msub6 = H_SUB6.match(raw)
if msub6:
if cur_ent is None:
raise SystemExit(f"❌ 第 {i} 行出现六级标题但无条目标题:{raw[:40]}")
cur_ent["附内子目"].append({"附内子目": msub6.group(1).strip(), "行": i})
stats["附内子目"] += 1
continue
# 逐级补「回溯行号」(合卷行号 − 所属章偏移 = 该章清洗版行号)
def back(ch_label, l):
return l - offsets[ch_label]
for ch in chapters:
for sec in ch["节"]:
sec["源行"] = back(ch["章"][:3], sec["行"])
for ent in sec["条目"]:
ent["源行"] = back(ch["章"][:3], ent["行"])
for sub in ent["子目"] + ent["附内子目"]:
sub["源行"] = back(ch["章"][:3], sub["行"])
return chapters, offsets, stats
def main():
check_only = "--check" in sys.argv
srcs, errs = load_sources()
if errs:
print("❌ 来源校验未通过:")
for e in errs:
print(" -", e)
return 1
merged, body = build_merged(srcs)
out_md = os.path.join(BASE, OUT_MD)
out_json = os.path.join(BASE, OUT_JSON)
old_md = read_text(OUT_MD) if os.path.exists(out_md) else None
chapters, offsets, stats = parse_structure(srcs, body)
total_lines = len(merged.split("\n")) - 1
# ── 自检(断言,不静默)──────────────────────────────────────
checks = []
checks.append(("来源 md5 三章全部相符",
all(md5_of(os.path.join(BASE, s["文件"])) == s["md5"] for s in srcs)))
checks.append(("合并正文 == 三章清洗版按章序逐字节拼接",
merged.split("\n", 1)[1].lstrip("\n")
== "\n\n".join(s["正文"].rstrip("\n") for s in srcs) + "\n"))
checks.append(("行顺序:第1章 < 第2章 < 第3章",
[c["章"][:3] for c in chapters] == ["第一章", "第二章", "第三章"]))
checks.append(("章数 = 3", stats["章"] == 3))
checks.append(("无一级标题(除文书标题行)",
len([l for l in merged.split("\n") if l.startswith("# ")]) == 1))
checks.append(("标题层级合法(## → ### → #### → ##### → ######,无越级)",
all(re.match(r"^#{1,6} ", l) for l in merged.split("\n")
if l.startswith("#"))))
ok = all(v for _, v in checks)
new_md5 = hashlib.md5(merged.encode("utf-8")).hexdigest()
print(f"合卷正文:{OUT_MD}")
print(f" {total_lines} 行 / {len(merged.encode('utf-8'))} 字节 / md5 {new_md5}")
print(f" 章 {stats['章']} · 节 {stats['节']} · 条目 {stats['条目']} · 子目 {stats['子目']}"
f" · 附内子目 {stats['附内子目']}")
print(" 章行号(合卷 ⇄ 各章清洗版回溯):")
for s in srcs:
off = offsets[s["章"]]
print(f" {s['章']} 合卷第 {off + 1} 行起(清洗版行号 + {off}),"
f"该章 {s['行数']} 行,md5 {s['md5'][:8]}…")
print(" 自检:")
for name, v in checks:
print(f" {'✅' if v else '❌'} {name}")
if not ok:
print("\n❌ 自检未通过,未写盘。")
return 1
if check_only:
print("\n--check:只校验,未写盘。")
return 0
changed = (old_md != merged)
if changed:
with open(out_md, "w", encoding="utf-8") as f:
f.write(merged)
print(f"\n{'✅ 已写入' if changed else '≡ 内容与库内一致,未改写'}:{OUT_MD}")
structure = {
"定位": "《〈辅行诀五脏用药法要〉药性探真》第一至三章 合并清洗版(衣之镖撰;学苑出版社 2014)",
"级别": "现代研究文献;引用优先级**低于**《辅行诀脏腑用药法要》(参考本),更低于"
"《辅行诀五脏用药法要》(底本)。**不得以本合并本替代《辅行诀》原书引用**。",
"用户指令": "2026-09-22「第1-3章已经清理完成,合并后导入资料库,级别低于 "
"…/02_加工数据/辅行诀脏腑用药法要.md」",
"产物文件": OUT_MD,
"产物md5": new_md5,
"产物行数": total_lines,
"产物字节": len(merged.encode("utf-8")),
"文书标题行": TITLE,
"标题体例": "文书标题=#(一级)/章=##(二级)/节=###(三级)/条目=####(四级)/子目=#####(五级)"
"/附内子目=######(六级);第 1 章之五级为「附:」、六级为附内子目,第 3 章之五级为子目(1. …),"
"三章体例**原样保留**、不作跨章统一",
"合并规则": "三章清洗版正文按章序逐字节拼接,章间空一行;**一字未增删**(文书标题行为合卷所必需之标识行)",
"来源": [{"章": s["章"], "文件": s["文件"], "md5": s["md5"], "行数": s["行数"],
"合卷起始行": offsets[s["章"]] + 1, "清洗版行号偏移": offsets[s["章"]]}
for s in srcs],
"行号对照": {"规则": "合卷行号 = 该章清洗版行号 + 偏移;各章条目均带「源行」为其清洗版行号",
"各章偏移": {s["章"]: offsets[s["章"]] for s in srcs}},
"章": chapters,
"统计": stats,
"自检": [{"项": n, "结果": "✅ 通过" if v else "❌ 未通过"} for n, v in checks],
"上游脚本": ["07_clean_ch1.py(md5 见 README §七)", "10_clean_ch2.py", "12_clean_ch3.py"],
"重跑": "python3 05_脚本工具/14_merge_ch123.py(幂等;--check 只校验不写盘)",
}
txt = json.dumps(structure, ensure_ascii=False, indent=2) + "\n"
old_json = read_text(OUT_JSON) if os.path.exists(out_json) else None
if old_json != txt:
with open(out_json, "w", encoding="utf-8") as f:
f.write(txt)
print(f"✅ 已写入:{OUT_JSON}")
else:
print(f"≡ 内容与库内一致,未改写:{OUT_JSON}")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,669 @@
#!/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())