#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 药膳全书库 四季标识补标 (参照《四季调养药膳》资料库方法论) ============================================================= 参照库: data/四季调养药膳/ (156条, 季节五分法: 春/夏/秋/冬/四季通用, 性味标注 平59/温54/凉39/热4, 季节×性味交叉验证) 本库现状: 两配方集仅有 category (功效类目), 其中仅61条带"春/夏/秋/冬季养生药膳" 类目, 其余576条无任何四季信息。 补标规则 (每条记录型, 如实保留不臆改): R1 书源类目优先: category 含 春/夏/秋/冬季养生药膳 → 直接标注该季节 (依据=书源) 其余类目 → "四季通用" (依据=书源非季节类目, 无季节限定) R2 性味聚合: 收集条目全部成分的"性"(中药286种+食物226种功能表), 归类: 有热→热; 温>凉且温≥2→温; 凉>温且凉≥2→凉; 其余→平; 无成分数据→None R3 交叉验证: 季节类目条目的聚合性味与《四季调养药膳》实测基准比对 (夏凉≥50% / 冬温热≥70% / 秋平≥40% / 春不设卡), 偏离仅注记不改标 R4 通用条目若聚合性味为纯温/热 → 备注"性味倾向冬补", 供人工复核 (不改标) 输出: 02_加工数据/四季标注_recipes.json 301条逐条标注 02_加工数据/四季标注_调理药膳.json 336条逐条标注 02_加工数据/索引_四季分类.json 春夏秋冬通用5组索引(与四季库同构) 03_关联融合/药膳全书-四季库_关联条目季节对照.json 04_分析报告/四季补标报告.md 幂等可重跑。 """ import json, os, re, collections BASE = os.path.dirname(os.path.abspath(__file__)) SRC = os.path.join(BASE, '01_来源数据') PRO = os.path.join(BASE, '02_加工数据') LNK = os.path.join(BASE, '03_关联融合') RPT = os.path.join(BASE, '04_分析报告') # ---------------- 性味解析 ---------------- XI = {'寒': '寒', '热': '热', '温': '温', '凉': '凉', '平': '平'} MOD = {'微': '', '大': '', '偏': ''} # 微寒计入寒, 大热计入热 _re_main = re.compile(r'([微大偏]?)([寒热温凉平])') _re_any = re.compile(r'[寒热温凉平]') def parse_xing(text): """从性味归经文本解析"性"; 返回 寒/热/温/凉/平 或 None""" t = text or '' m = _re_main.search(t) if m: return XI[m.group(2)] # 兜底: 无"性X"句式(如"苦,寒;入…经"/"性味甘、平"/"性辛、凉")取首个性字 m = _re_any.search(t) return XI[m.group(0)] if m else None # ---------------- 功能表性味字典 ---------------- def load(path): d = json.load(open(path, encoding='utf-8')) return d if isinstance(d, list) else next(v for v in d.values() if isinstance(v, list)) herbs = load(os.path.join(SRC, '药膳常用中药功能表.json')) foods = load(os.path.join(SRC, '药膳常用食物功能表.json')) xing_by_id = {} for x in herbs + foods: xing_by_id[x['id']] = parse_xing(x.get('性味归经', '')) xing_by_name = {x['名称']: xing_by_id[x['id']] for x in herbs + foods} def aggregate(chengfen_xings): """成分性列表 → 聚合性 (规则R2) 标尺对齐四季库四级制(平/温/凉/热): 寒并入凉向(寒凉同向, 寒>凉仅程度差)""" if not chengfen_xings: return None norm = ['凉' if x == '寒' else x for x in chengfen_xings] c = collections.Counter(norm) if c['热']: return '热' if c['温'] > c['凉'] and c['温'] >= 2: return '温' if c['凉'] > c['温'] and c['凉'] >= 2: return '凉' return '平' # ---------------- 逐条提取成分性 ---------------- recipes = load(os.path.join(PRO, '药膳配方库_recipes_富关联.json')) tiaoli = load(os.path.join(PRO, '调理药膳_知识图谱.json')) for r in recipes: xs = [] for c in r.get('食材成分', []) + r.get('药材成分', []): v = parse_xing(c.get('性味归经', '')) if v: xs.append(v) r['_xing_list'] = xs for r in tiaoli: xs = [] for c in r.get('关联成分明细', []): v = xing_by_id.get(c['id']) if v: xs.append(v) r['_xing_list'] = xs # ---------------- 补标 (规则R1/R3/R4) ---------------- SEASON_PAT = [('春季养生药膳', '春'), ('夏季养生药膳', '夏'), ('秋季养生药膳', '秋'), ('冬季养生药膳', '冬')] # 《四季调养药膳》实测基准 (建档报告: 季节×性味交叉表) VERIFY = {'夏': lambda x: x == '凉', '冬': lambda x: x in ('温', '热'), '秋': lambda x: x == '平', '春': lambda x: True} def tag(items, src_name): out = [] for it in items: cat = it.get('category', '') or '' season, basis, cred = '四季通用', None, '高' for pat, s in SEASON_PAT: if pat in cat: season, basis, cred = s, f'书源类目[{cat}]', '高' break if basis is None: # 书源未明示季节: 类目为功效类目, 标注"四季通用"为保守回退推断 basis = f'书源类目[{cat or "无"}]为功效类目, 无季节限定' cred = '中(回退推断, 书源未明示季节)' agg = aggregate(it['_xing_list']) notes, dev = [], [] if season != '四季通用': # R3 交叉验证 if agg and not VERIFY[season](agg): dev.append({'聚合性味': agg, '注记': f'{season}季类目但成分聚合性味为{agg}, 与四季库季节×性味基准偏离, 仅注记不改标'}) notes.append('性味偏离注记') elif agg in ('温', '热'): # R4 通用条目性味提示 notes.append(f'性味倾向冬补({agg}性), 供人工复核') out.append({ 'id': it['id'], '名称': it.get('name') or it.get('名称'), 'category': cat, '来源集': src_name, '四季标注': season, '标注依据': basis, '可信度': cred, '聚合性味': agg, '性味成分数': len(it['_xing_list']), '成分性分布': dict(collections.Counter(it['_xing_list'])) if it['_xing_list'] else {}, '数据质量': notes, '性味偏离注记': dev, }) return out tag_recipes = tag(recipes, '药膳配方库') tag_tiaoli = tag(tiaoli, '调理药膳') # ---------------- 五组索引 (与四季库 索引_季节分类.json 同构) ---------------- GUIDE = { '春': {'五补': '升补', '脏腑': '肝', '五行': '木'}, '夏': {'五补': '清补', '脏腑': '心', '五行': '火'}, '秋': {'五补': '平补', '脏腑': '肺', '五行': '金'}, '冬': {'五补': '温补', '脏腑': '肾', '五行': '水'}, '四季通用': {'五补': '平补为主', '脏腑': '—', '五行': '—'}, } index = [] all_tags = tag_recipes + tag_tiaoli for s in ['春', '夏', '秋', '冬', '四季通用']: grp = [t for t in all_tags if t['四季标注'] == s] index.append({ '季节': s, '五补原则': GUIDE[s]['五补'], '应脏腑': GUIDE[s]['脏腑'], '五行': GUIDE[s]['五行'], '条数': len(grp), '条目': [{'id': t['id'], '名称': t['名称'], '来源集': t['来源集'], 'category': t['category']} for t in grp], }) # ---------------- 与四季库关联条目季节对照 ---------------- XREF = os.path.join(BASE, '..', '四季调养药膳', '03_关联融合', '四季调养-药膳全书_交叉关联.json') compare = {'metadata': {'说明': '既有10条名称关联(见 药膳全书-四季库_交叉关联.json)的两侧季节/性味对照', '参照库': '四季调养药膳(156条,季节为书源章节)', '生成脚本': '库根 season_tag.py'}, '对照': []} if os.path.exists(XREF): x = json.load(open(XREF, encoding='utf-8')) tiaoli_name = {t['名称']: t for t in all_tags if t['来源集'] == '调理药膳'} recipes_name = {t['名称']: t for t in all_tags if t['来源集'] == '药膳配方库'} sj = json.load(open(os.path.join(BASE, '..', '四季调养药膳', '02_加工数据', '四季调养药膳.json'), encoding='utf-8')) sj = sj if isinstance(sj, list) else next(v for v in sj.values() if isinstance(v, list)) sj_by_id = {r['id']: r for r in sj} for ref in x.get('cross_references', []): sjr = sj_by_id[ref['本库id']] for a in ref['关联']: pool = tiaoli_name if a['对方库'] == '调理药膳' else recipes_name hit = pool.get(a['对方名称']) if hit: compare['对照'].append({ '四季库id': ref['本库id'], '四季库名称': ref['本库名称'], '四季库季节': sjr['季节'], '四季库性味': sjr['性味'], '药膳全书集': a['对方库'], '药膳全书id': a['对方id'], '药膳全书名称': a['对方名称'], '补标季节': hit['四季标注'], '补标依据': hit['标注依据'], '聚合性味': hit['聚合性味'], '季节一致': hit['四季标注'] == sjr['季节'] or sjr['季节'] == '四季通用' or hit['四季标注'] == '四季通用', }) # ---------------- 写盘 ---------------- os.makedirs(RPT, exist_ok=True) def dump(obj, path): json.dump(obj, open(path, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) dump(tag_recipes, os.path.join(PRO, '四季标注_recipes.json')) dump(tag_tiaoli, os.path.join(PRO, '四季标注_调理药膳.json')) dump(index, os.path.join(PRO, '索引_四季分类.json')) dump(compare, os.path.join(LNK, '药膳全书-四季库_关联条目季节对照.json')) # ---------------- 统计 ---------------- def stat(tags): c = collections.Counter(t['四季标注'] for t in tags) xing_by_season = collections.defaultdict(collections.Counter) for t in tags: if t['聚合性味']: xing_by_season[t['四季标注']][t['聚合性味']] += 1 return c, xing_by_season lines = ['# 药膳全书库 四季标识补标报告', '', '> 参照: 《四季调养药膳》资料库方法论 (季节五分法 + 性味聚合 + 季节×性味交叉验证)', '> 规则: R1书源类目优先 R2性味聚合 R3交叉验证(仅注记) R4通用条目性味提示', ''] for name, tags in [('药膳配方库(301条)', tag_recipes), ('调理药膳(336条)', tag_tiaoli)]: c, xbs = stat(tags) lines += [f'## {name}', '', '| 四季标注 | 条数 | 占比 |', '|---|---|---|'] tot = len(tags) for s in ['春', '夏', '秋', '冬', '四季通用']: n = c.get(s, 0) lines.append(f'| {s} | {n} | {n/tot:.0%} |') lines += ['', '标注依据分布: ' + '; '.join( f"{k[:12]}…={v}" if len(k) > 14 else f"{k}={v}" for k, v in collections.Counter(t['标注依据'].split('[')[0] for t in tags).most_common()), ''] lines += ['季节×聚合性味交叉:', '', '| 季节 | 平 | 温 | 凉 | 热 | 无成分数据 |', '|---|---|---|---|---|---|'] for s in ['春', '夏', '秋', '冬', '四季通用']: xb = xbs.get(s, collections.Counter()) none_n = c.get(s, 0) - sum(xb.values()) lines.append(f"| {s} | {xb.get('平',0)} | {xb.get('温',0)} | {xb.get('凉',0)} | {xb.get('热',0)} | {none_n} |") lines.append('') # 偏离清单 dev_all = [(t['来源集'], t['id'], t['名称'], t['四季标注'], d['聚合性味']) for t in all_tags for d in t['性味偏离注记']] lines += ['## 性味偏离注记清单 (R3, 不改标)', ''] if dev_all: lines += ['| 来源集 | id | 名称 | 标注 | 聚合性味 |', '|---|---|---|---|---|'] lines += [f'| {a} | {b} | {c} | {d} | {e} |' for a, b, c, d, e in dev_all] else: lines += ['无偏离条目 — 书源季节类目条目的成分聚合性味全部符合四季库季节×性味基准。'] lines.append('') r4 = [(t['来源集'], t['id'], t['名称'], t['聚合性味']) for t in all_tags if t['四季标注'] == '四季通用' and t['聚合性味'] in ('温', '热')] lines += ['## 通用条目性味倾向提示 (R4, 供人工复核)', '', f'共 {len(r4)} 条功效类目条目成分聚合为纯温/热性:', ''] lines += [f'- {a} {b} {c} (聚合{e}性)' for a, b, c, e in r4[:30]] if len(r4) > 30: lines.append(f'- …其余 {len(r4)-30} 条见 四季标注_*.json 数据质量字段') lines += ['', '## 与四季库关联条目季节对照', '', f"既有10条名称关联的两侧季节一致性: " f"{sum(1 for x in compare['对照'] if x['季节一致'])}/{len(compare['对照'])}", ''] lines += ['| 四季库 | 库内季节 | 药膳全书 | 补标季节 | 一致 |', '|---|---|---|---|---|'] for x in compare['对照']: lines.append(f"| {x['四季库名称']} | {x['四季库季节']}({x['四季库性味']}) | " f"{x['药膳全书名称']} | {x['补标季节']} | {'✓' if x['季节一致'] else '✗'} |") open(os.path.join(RPT, '四季补标报告.md'), 'w', encoding='utf-8').write('\n'.join(lines) + '\n') print('=== 四季补标完成 ===') print('recipes:', dict(stat(tag_recipes)[0])) print('调理药膳:', dict(stat(tag_tiaoli)[0])) print('偏离注记:', len(dev_all), '| R4通用温热提示:', len(r4), '| 关联对照:', len(compare['对照']), '条, 一致', sum(1 for x in compare['对照'] if x['季节一致']))