初始归档:健康管理资料库(大医网/药膳/体质监测/中医理论等12个资料集,7712个文件)
This commit is contained in:
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()
|
||||
Executable
+122
@@ -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())
|
||||
Reference in new issue
Block a user