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