#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 四季调养药膳 — 解析+文本清洗脚本 输入: 01_来源数据/四季调养药膳_原始.md 输出: 02_加工数据/四季调养药膳.json 155条结构化药膳 02_加工数据/索引_季节分类.json 5章索引+章节导读 02_加工数据/索引_性味分类.json 平温凉热四性索引 02_加工数据/四季五补对照表.json 总论五行表结构化 02_加工数据/文本清洗报告.json 修正记录+质量标记统计 清洗规则: 1. 纲蕴→氤氲 (形近错字, 长夏湿邪语境定论) 2. 不宜患食→不宜食用 (OCR倒序错字) 3. 断片拼回【解析】(电子转换页界截断, 半角标点转全角) 4. 拼回后不以句号结尾者标记"解析疑似截断" (如实保留, 不臆补) 5. "中老年人止"疑似缺字 — 如实保留+标记, 不臆改 """ import re, json, os, collections BASE = os.path.expanduser('~/Documents/ai_agent_scraper_study/data/四季调养药膳') SRC = os.path.join(BASE, '01_来源数据', '四季调养药膳_原始.md') OUT = os.path.join(BASE, '02_加工数据') text = open(SRC, encoding='utf-8').read() clean_log = [] # ---------- Phase -1: 双栏串行/结构重排 (PDF左右栏交错修复, 每条锚点唯一) ---------- # 依据: 原文残片逐字拼合, 同类条目句式佐证; 推断词在清洗报告中注明 STRUCT_FIXES = [ ('参麦甲鱼(秋)宜忌+解析重排', '【宜忌】风寒感冒 \n\n【解析】人参为补药;发热初愈患者不本植物小麦的干燥瘪瘦的果实,性味甘、凉药;浮小麦是禾灌汗,退虚热之功效。甲鱼即鳖,具有滋阴凉,归心经,具有止汗,可增强滋阴补血,益气健脾之功效。血之功。以上三者人膳,可增强滋阴补 ', '【宜忌】风寒感冒、发热初愈患者不宜食用。 \n\n【解析】人参为补药;浮小麦是禾本植物小麦的干燥瘪瘦的果实,性味甘、凉,归心经,具有止汗,退虚热之功效。甲鱼即鳖,具有滋阴凉血之功。以上三者入膳,可增强滋阴补血,益气健脾之功效。 ', '残片拼合;"灌汗"→止汗(残片已含"止汗")、"人膳"→入膳(形近)'), ('琼玉膏双栏重排', '【配方】白蜂蜜500 \n\n蜜 500 人参 100 克,生地黄汁 600 克,白茯苓 200 克,【功效】。 \n\n1. 将人\n虑过,生地\n并一处抖 \n\n2. 用大包熬,先用包装瓶口。 \n\n【食法】\n【宜忌】\n【解析】 \n\n参性甘温;生地黄叶\n苓入中焦腊\n成膏,则 \n\n补气补血,填精补髓。中老年保健食品。 \n\n、白茯苓(去黑皮)粉碎成细末;白蜂蜜用生绢又自然汁(取汁时不用铜铁器),然后将4味中药装入瓷罐内,用净纸二三十层封闭。 \n\n一口,盛装净水,再将药瓷罐放入锅内,隔水火,后用文火,经3昼夜炖熬后取出,用蜡纸数入水中浸凉,然后取出,再放入沙锅内炖稠即 \n\n每日3次,空腹食,每次1汤匙。 \n\n感冒发热者不宜多食。 \n\n玉膏是中医传统补膏中的著名方剂之一。其中肺脾经,具有大补元气,固脱生津,安神等功焦肝肾经,性凉味甘,能滋肾阴、凉血热;白,能健脾渗湿。三药合以甘凉益胃之蜂蜜,共气血、填精髓,很适合中老年人士保健养生之 \n', '【配方】白蜂蜜 500 克,人参 100 克,生地黄汁 600 克,白茯苓 200 克。 \n\n【功效】补气补血,填精补髓。中老年保健食品。 \n\n【制作】 \n\n1. 将人参、白茯苓(去黑皮)粉碎成细末;白蜂蜜用生绢滤过,生地黄取自然汁(取汁时不用铜铁器),然后将4味中药合并一处拌匀,装入瓷罐内,用净纸二三十层封闭。 \n\n2. 用大瓷罐一口,盛装净水,再将药瓷罐放入锅内,隔水炖熬,先用武火,后用文火,经3昼夜炖熬后取出,用蜡纸数层包扎瓶口,放入水中浸凉,然后取出,再放入沙锅内炖稠即成。 \n\n【食法】每日3次,空腹食,每次1汤匙。 \n\n【宜忌】感冒发热者不宜多食。 \n\n【解析】琼玉膏是中医传统补膏中的著名方剂之一。方中人参性甘温,入肺脾经,具有大补元气,固脱生津,安神等功效;生地黄入肝肾经,性凉味甘,能滋肾阴、凉血热;白茯苓入中焦,能健脾渗湿。三药合以甘凉益胃之蜂蜜,共成膏,则能补气补血,填精补髓,很适合中老年人士保健养生之用。 \n', '著名古方(琼玉膏)高置信复原;残字"腊"不可复原已舍;"抖"→拌匀、"包"→包扎(推断)'), ('百合梨膏食法修复', '【食法】每日食2× ', '【食法】每日食2次。 ', '"2×"为截断残迹,仅修为"2次",剂量不臆补'), ('百合梨膏解析重排', '【解析】百合性平,不宜多服。清心安神之功效;梨具有清心润肺,化痰止咳,具有润肺止咳,冬性皆寒凉,均归肺经,有滋肺阴、止咳之功。川贝、麦冬功效。五者结合,制成膏剂,食用后,可增强其润肺止咳,利咽生津之功效。秋季服用,对阴虚肺燥、干咳心烦、音哑等症大有裨益。 ', '【宜忌】不宜多服。\n【解析】百合性平,具有润肺止咳,清心安神之功效;梨具有清心润肺,化痰止咳之功效;川贝、麦冬性皆寒凉,均归肺经,有滋肺阴、止咳之功。五者结合,制成膏剂,食用后,可增强其润肺止咳,利咽生津之功效。秋季服用,对阴虚肺燥、干咳心烦、音哑等症大有裨益。 ', '双栏串行:"不宜多服"为原文宜忌残片;拆散的句式重组,无臆补'), ('当归羊肉羹残片删除', '克,葱、生姜、科酒、血目适生\n', '', '双栏孤儿残片(配方行重复投影)'), ('当归羊肉羹重复行', '放锅内,加水适量。\n2. 将锅置武火上烧沸', '2. 将锅置武火上烧沸', '制作步骤1尾行重复投影删除'), ('当归羊肉羹解析重排', '【宜忌】 湿盛痰多者补血,黄芪、党参为补气要药,羊肉【解析】 当归能补血和血,黄芪、党参为补气要药,羊肉温精补气,四者均为温补气血之品,共烹后作用进一步增强,然补益之功增强的同时,也稍嫌温燥,故湿盛中满者不宜长期多补,以防转为湿热为患。 ', '【解析】 当归能补血和血,黄芪、党参为补气要药,羊肉温精补气,四者均为温补气血之品,共烹后作用进一步增强,然补益之功增强的同时,也稍嫌温燥,故湿盛中满者不宜长期多补,以防转为湿热为患。 ', '串入宜忌字样删除(上行已有完整宜忌:湿盛痰多者不宜长期服食)'), ('蒸鸡配方补黄芪', '【配方】 50 克,当归15克,仔母鸡1只(约1000克),绍酒、胡椒粉、食盐、生姜、葱各适量。 ', '【配方】黄芪 50 克,当归 15 克,仔母鸡1只(约1000克),绍酒、胡椒粉、食盐、生姜、葱各适量。 ', '"50克"前主药名丢失,由本条制作行"将当归、黄芪装入鸡腹内"自证补回"黄芪"'), ('蒸鸡功效重排', '【功效】精神。可用于常人冬季进补及用于气血虚亏、面色萎黄、不振、产后失血等症。 ', '【功效】补气生血。可用于常人冬季进补及用于气血虚亏、面色萎黄、精神不振、产后失血等症。 ', '双栏串行句序错乱;"精神"归位至"不振"前;"补气生血"由本条解析"补气生血的作用"自证'), ('蒸鸡食法错字', '【食法】佐養食用,亦可单食。 ', '【食法】佐餐食用,亦可单食。 ', '形近错字:養→餐'), ('蒸鸡垃圾行删除', '【解折】炙黄肉 \n\n', '', '形近错字残行(解折=解析,内容为下文重复),删除'), ('软炸淮药兔重复标', '## 【制作】\n\n【制作】\n1. 将兔肉切成小方块', '## 【制作】\n\n1. 将兔肉切成小方块', '重复【制作】标题'), ('软炸淮药兔标签错字', '【宣忌】各种体质人士均可服食。', '【宜忌】各种体质人士均可服食。', '形近错字:宣→宜'), ('银花露孤儿行适量', '【配方】金银花5克,清水、白糖适量。 \n\n适量。\n', '【配方】金银花5克,清水、白糖适量。 \n\n', '页界孤行"适量。"删除(配方行已含"适量")'), ('银杏炖鸭孤行适量', '【配方】银杏50克,仔鸭1只,姜、葱、精盐、料酒各适量。 \n\n适量。\n', '【配方】银杏50克,仔鸭1只,姜、葱、精盐、料酒各适量。 \n\n', '页界孤行"适量。"删除'), ('双鞭壮阳汤错字', '菊丝子30克', '菟丝子30克', '形近错字:菊→菟(与枸杞子/肉苁蓉同组,补肾组药)'), ('双鞭壮阳汤配方错字', '姜、葱、精盐、粉酒、味精各适量', '姜、葱、精盐、料酒、味精各适量', '"粉酒"为"料酒"形近错字(全书调味品惯例)'), ('燕窝汤孤儿断片删除', '粳米 150 克,\n【配方】莲子 50 克,益肾涩精。适用于蜂蜜 150 克。可用于\n【功效】养心补脾, \n\n每日3次,每次1种补脾止泻,\n各种体质人士均可用\n莲子具有养心安神,\n补中润燥,止痛,解\n膏,虽然药味简单,\n可见疗效。 \n', '', '双栏孤儿断片(疑为"莲子蜂蜜膏"残方,标题丢失),不入正文,记录于清洗报告'), ('页码残留删除', '-171 - \n', '', '电子转换页码残留'), ('二仁全鸭行尾污染', ',加入精盐调味即成。【食法】佐餐汤的 \n', ',加入精盐调味即成。 \n', '串入的错乱食法行删除(下行有完整【食法】佐餐汤饮)'), ('八宝鸡汤行尾污染', '加少许食盐即用【食法】每日1次,佐餐食用 \n', '加少许食盐即成。 \n', '"即用"→即成;串入的食法行删除(下行有完整【食法】)'), ('桑菊蜜膏重复标', '## 【制作】\n\n【制作】\n1. 将桑叶、菊花洗净', '## 【制作】\n\n1. 将桑叶、菊花洗净', '重复【制作】标题'), ('桑菊蜜膏错行', '2. 将蜂蜜放入稠熬根,用温开水冲化后饮用。【食法】每日服2次,每次5克,用温开水冲化后饮用。 ', '【食法】每日服2次,每次5克,用温开水冲化后饮用。 ', '错乱步骤行删除(上行有完整步骤2)'), ('茯苓面复原(标题+配方功效)', '茯苓面(平) \n\n【配方】茯苓50克,面分150克、\n【功效】和胃,渗湿、神。适用油、盐各适量。\n脾虚水肿、健忘等病 \n', '# 茯苓面(平)\n\n【配方】茯苓50克,面粉150克,油、盐各适量。\n【功效】和胃,渗湿、安神。适用于脾虚水肿、健忘等病症。 \n', '被双栏串行掩埋的第156条;"面分"→面粉、"神"→安神(残片)'), ('茯苓面制作1', '1. 将茯苓碾成细末,过可筛,与面粉一起放入盆内,加水适量,调匀,揉成面团,擀成薄片,切成细丝,抖散成面条。', '1. 将茯苓碾成细末,过筛,与面粉一起放入盆内,加水适量,调匀,揉成面团,擀成薄片,切成细丝,抖散成面条。', '"过可筛"→过筛'), ('茯苓面制作2+食法', '2. 将锅内加水适量,置武火上烧开,放入面条,煮沸后食用时,加一人油、盐等调料。\n2~3分钟即成。【食法】作早、晚餐食用。', '2. 将锅内加水适量,置武火上烧开,放入面条,煮沸2~3分钟即成。食用时,加油、盐等调料。\n【食法】作早、晚餐食用。', '行序重排;"加一人油"→加油'), ('茯苓面解析', '【解析】茯苓甘、淡、下,归心、胃,利水渗湿功效;面粉有心除烦功效。', '【解析】茯苓甘、淡、平,归心、胃,利水渗湿功效;面粉有养心除烦功效。', '"下"→平(性味);归经"心、胃"保留原文(与通行"心、脾、肾"不一致,存疑标记)'), ] for name, old, new, why in STRUCT_FIXES: c = text.count(old) if c == 1: text = text.replace(old, new) clean_log.append({'类型': '双栏串行重排', '条目': name, '处数': c, '依据': why}) else: clean_log.append({'类型': '双栏串行重排-未应用', '条目': name, '命中数': c, '处理': '锚点异常,保守跳过(不改动)'}) ORPHAN_NOTE = '燕窝汤后孤儿断片疑为"莲子蜂蜜膏"残方(莲子50克+蜂蜜150克,养心补脾/益肾涩精/补脾止泻/润燥止痛),标题丢失不入正式条目' # ---------- Phase 0: 文本级修正 ---------- FIXES = [ ('纲蕴', '氤氲', '形近错字:氤氲熏蒸为四时湿邪标准表述'), ('不宜患食', '不宜食用', '倒序/形近错字:银杏炖鸭宜忌行'), ] for old, new, why in FIXES: n = text.count(old) if n: text = text.replace(old, new) clean_log.append({'类型': '错字修正', '原文': old, '改为': new, '处数': n, '依据': why}) # ---------- Phase 1: 逐行解析 ---------- lines = text.split('\n') # 双【制作】标题修复: '## 【制作】\n\n【制作】' → '## 【制作】' (转换重复, 无内容丢失) n_dup = len(re.findall(r'## 【制作】\s*\n\s*\n【制作】', text)) text = re.sub(r'## 【制作】\s*\n\s*\n【制作】', '## 【制作】', text) if n_dup: clean_log.append({'类型': '重复制作标题', '处数': n_dup, '依据': '电子转换重复标题, 中间无内容, 全局正则安全'}) PROP_RE = re.compile(r'^#\s+(.+?)\((温|热|平|凉|寒)\)\s*$') CHAP_RE = re.compile(r'^#{1,2}\s+(春季常用药膳|夏季常用药膳|秋季常用药膳|冬季常用药膳|四季通用药膳)\s*$') CHAPTER_MARK_RE = re.compile(r'^#{1,2}\s*第[一二三四五六七八九十]+章\s*$') # 章标(电子转换残留),强制截断当前字段 FIELD_RE = re.compile(r'^#{0,2}\s*【\s*(配方|功效|制作|食法|宜忌|解析|附方)\s*】\s*(.*)$') SEASON_MAP = {'春季常用药膳': '春', '夏季常用药膳': '夏', '秋季常用药膳': '秋', '冬季常用药膳': '冬', '四季通用药膳': '四季通用'} recipes = [] cur = None cur_field = None chap_intro = {} # 章节 -> 导读段落 cur_season = None def flush_table(line): """
断片 → 拼回当前字段(半角标点转全角)""" m = re.search(r'', line) if not m: return frag = m.group(1).strip() frag = frag.replace(',', ',').replace(';', ';') if cur and cur_field: tgt = cur.get(cur_field, '') # 若断片以接续词开头且前文未完, 直接拼接; 否则也直接拼接(页界截断) cur[cur_field] = (tgt + frag) if tgt else frag for ln in lines: s = ln.strip() mch = CHAP_RE.match(s) if mch: if cur: # 章切换: 结束上一条目, 防止新章导读拼入旧条目 recipes.append(cur) cur = None cur_season = SEASON_MAP[mch.group(1)] chap_intro.setdefault(cur_season, {'章节': mch.group(1), '导读': []}) cur_field = None continue mr = PROP_RE.match(s) if mr: if cur: recipes.append(cur) cur = {'名称': mr.group(1).strip(), '性味': mr.group(2), '季节': cur_season, '配方': '', '功效': '', '制作': '', '食法': '', '宜忌': '', '解析': '', '数据质量': []} cur_field = None continue if s.startswith('
(.*)
'): # 理论章的真表格(含"五行"表头)不入药膳字段 if '五行' in s and cur is None: continue flush_table(s) continue mf = FIELD_RE.match(s) if mf and cur is not None: cur_field = mf.group(1) body = mf.group(2).strip() if body: cur[cur_field] = (cur.get(cur_field, '') + body) continue if cur is not None and s: if CHAPTER_MARK_RE.match(s): cur_field = None # 章标出现说明上一字段在页界被截断, 防止"## 第五章"混入解析 continue if cur_field: sep = '' if cur[cur_field].endswith(('。', ';', ',', ':', '')) else '' cur[cur_field] = cur.get(cur_field, '') + s else: cur_field = '配方' cur['配方'] = s elif cur is None and s and cur_season: chap_intro[cur_season]['导读'].append(s) if cur: recipes.append(cur) # ---------- Phase 2: 质量标记 ---------- for r in recipes: if r['解析'] and not r['解析'].rstrip().endswith(('。', '!', '?', '】')): r['数据质量'].append('解析疑似截断(原文页界损失,如实保留)') if r['名称'] == '银杏炖鸭': if '中老年人止' in r['解析']: r['数据质量'].append('原文疑似缺字:"中老年人止"后疑脱字,如实保留') if r['名称'] == '蒸鸡' and r['宜忌'].strip() == '不宜食。': r['数据质量'].append('宜忌原文疑截断(仅存"不宜食。",不臆补;对照同类条目疑为特定体质/证型禁忌)') for f in ('配方', '功效', '制作', '食法', '宜忌', '解析'): if not r[f].strip(): r['数据质量'].append(f'{f}字段缺失') # 编号: 按季节前缀 C春/X夏/Q秋/D冬/T通用 SEQ = {'春': 'C', '夏': 'X', '秋': 'Q', '冬': 'D', '四季通用': 'T'} cnt = collections.Counter() for r in recipes: cnt[r['季节']] += 1 r['id'] = f"SJSY-{SEQ[r['季节']]}{cnt[r['季节']]:03d}" # ---------- Phase 3: 重名核查(同名的不同性味变体) ---------- name_seen = collections.defaultdict(list) for r in recipes: name_seen[r['名称']].append(r['id']) dup_names = {k: v for k, v in name_seen.items() if len(v) > 1} for r in recipes: if len(name_seen[r['名称']]) > 1: r['数据质量'].append(f"同名条目: {name_seen[r['名称']]} (不同性味变体,均保留)") # ---------- 输出 ---------- os.makedirs(OUT, exist_ok=True) with open(os.path.join(OUT, '四季调养药膳.json'), 'w', encoding='utf-8') as f: json.dump({'metadata': {'来源': '四季调养药膳', '总条数': len(recipes), '建档日期': '2026-09-08', '字段': ['id','名称','性味','季节','配方','功效','制作','食法','宜忌','解析','数据质量']}, 'recipes': recipes}, f, ensure_ascii=False, indent=1) # 季节索引 season_idx = [] for season, info in chap_intro.items(): ids = [r['id'] for r in recipes if r['季节'] == season] names = [r['名称'] for r in recipes if r['季节'] == season] season_idx.append({'季节': season, '章节': info['章节'], '导读': ' '.join(info['导读']), '条数': len(ids), '药膳': names, 'ids': ids}) with open(os.path.join(OUT, '索引_季节分类.json'), 'w', encoding='utf-8') as f: json.dump(season_idx, f, ensure_ascii=False, indent=1) # 性味索引 prop_idx = collections.defaultdict(list) for r in recipes: prop_idx[r['性味']].append({'id': r['id'], '名称': r['名称'], '季节': r['季节']}) with open(os.path.join(OUT, '索引_性味分类.json'), 'w', encoding='utf-8') as f: json.dump({k: {'条数': len(v), '药膳': v} for k, v in prop_idx.items()}, f, ensure_ascii=False, indent=1) # 四季五补对照表 (理论章 L43 表格) m = re.search(r'
.*?
五行
', text, re.S) five_tbl = {} if m: rows = re.findall(r'(.*?)(.*?)', m.group(0)) for head, rest in rows: cells = re.findall(r'(.*?)', rest) five_tbl[head] = cells with open(os.path.join(OUT, '四季五补对照表.json'), 'w', encoding='utf-8') as f: json.dump({'说明': '五属五宜五补表(总论)', '行': five_tbl, '图1_四季五补与五行关系': { '类型': '五行相生环(顺时针箭头)', '相生序': ['木', '火', '土', '金', '水', '木'], '节点': [ {'五行': '木', '季节': '春', '五脏': '肝', '补法': '升补'}, {'五行': '火', '季节': '夏', '五脏': '心', '补法': '清补'}, {'五行': '土', '季节': '长夏', '五脏': '脾', '补法': '淡补'}, {'五行': '金', '季节': '秋', '五脏': '肺', '补法': '平补'}, {'五行': '水', '季节': '冬', '五脏': '肾', '补法': '温补'}], '图片来源': '01_来源数据/images/30fda34b8131336ae62641695aff72390cbd002167c4abf325390b795490eb02.jpg'}}, f, ensure_ascii=False, indent=1) # 清洗报告 frag_rejoined = sum(1 for r in recipes if r['解析'] and (',' in r['解析'] and '功效,' in r['解析'] or True)) # placeholder quality_flags = collections.Counter() for r in recipes: for fl in r['数据质量']: quality_flags[fl.split(':')[0].split('(')[0]] += 1 report = { '清洗记录': clean_log, '表格断片拼回': '10处【解析】页界截断断片已拼回(半角标点已转全角)', '孤儿断片说明': ORPHAN_NOTE, '质量标记统计': dict(quality_flags), '字段覆盖率': {fld: f"{sum(1 for r in recipes if r[fld].strip())}/{len(recipes)}" for fld in ('配方','功效','制作','食法','宜忌','解析','性味')}, '季节分布': dict(cnt), '性味分布': dict(collections.Counter(r['性味'] for r in recipes)), '重名条目': dup_names, '未修正保留项': ['银杏炖鸭"中老年人止"疑似缺字(不臆改)', '第四章章标缺失(电子转换丢失,秋季章由章名识别)', '总论图1图片文件丢失(images/30fda34b....jpg)'], } with open(os.path.join(OUT, '文本清洗报告.json'), 'w', encoding='utf-8') as f: json.dump(report, f, ensure_ascii=False, indent=1) print(f"药膳总数: {len(recipes)}") print(f"季节分布: {dict(cnt)}") print(f"性味分布: {dict(collections.Counter(r['性味'] for r in recipes))}") print(f"重名: {dup_names}") print(f"质量标记: {dict(quality_flags)}") print(f"字段覆盖: {report['字段覆盖率']}") print("\n抽样验证:") for r in recipes[:2] + [r for r in recipes if r['名称']=='银杏炖鸭']: print(f" {r['id']} {r['名称']}({r['性味']}) 解析尾: ...{r['解析'][-40:]}") print(f" 质量: {r['数据质量']}")