#!/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()