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

698 lines
45 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
辅行诀五脏用药法要-药性探真 资料库 ETL 第 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()