|
|
@@ -0,0 +1,942 @@
|
|
|
+"""
|
|
|
+肠道菌群健康检测报告 — 全指标提取脚本(v5)
|
|
|
+========================================
|
|
|
+基于PDF实际文本格式精确解析 + JSON输出模式。
|
|
|
+修复:
|
|
|
+1) 抗生素风险评估 inline 区域使用已知名称词表过滤
|
|
|
+2) 肠道屏障 SCFA/神经递质使用已知指标名精确匹配
|
|
|
+3) Inline 氨基酸/维生素/微量元素使用已知名称过滤
|
|
|
+
|
|
|
+用法:
|
|
|
+ python extract_full_report_v5.py # CSV 批量输出(原有模式)
|
|
|
+ python extract_full_report_v5.py -j <PDF路径> # JSON 单文件输出
|
|
|
+"""
|
|
|
+
|
|
|
+import sys, os, csv, re, json
|
|
|
+sys.stdout.reconfigure(encoding='utf-8')
|
|
|
+sys.stderr.reconfigure(encoding='utf-8')
|
|
|
+from PyPDF2 import PdfReader
|
|
|
+
|
|
|
+BASE = r'D:\workspace\cfc\docs\参考资料'
|
|
|
+OUTDIR = os.path.join(BASE, 'CSV')
|
|
|
+os.makedirs(OUTDIR, exist_ok=True)
|
|
|
+
|
|
|
+RADICAL_MAP = {
|
|
|
+ '\u2f18': '卜', '\u2f1f': '土', '\u2f24': '大', '\u2f26': '子',
|
|
|
+ '\u2f29': '小', '\u2f2d': '山', '\u2f32': '干', '\u2f3c': '心',
|
|
|
+ '\u2f42': '文', '\u2f46': '无', '\u2f4a': '木', '\u2f50': '比',
|
|
|
+ '\u2f54': '水', '\u2f55': '火', '\u2f5c': '牛', '\u2f5f': '玉',
|
|
|
+ '\u2f60': '瓜', '\u2f62': '甘', '\u2f63': '生', '\u2f64': '用',
|
|
|
+ '\u2f69': '白', '\u2f6a': '皮', '\u2f6c': '目', '\u2f6f': '石',
|
|
|
+ '\u2f75': '竹', '\u2f76': '米', '\u2f7a': '羊', '\u2f7b': '羽',
|
|
|
+ '\u2f7c': '老', '\u2f7f': '耳', '\u2f81': '肉', '\u2f90': '衣',
|
|
|
+ '\u2f95': '谷', '\u2f96': '豆', '\u2f9d': '身', '\u2fa6': '金',
|
|
|
+ '\u2faf': '面', '\u2fb2': '韭', '\u2fb9': '香', '\u2fca': '黑',
|
|
|
+ '\u2ec9': '贝', '\u2edd': '食', '\u2ee2': '马', '\u2ee5': '鱼',
|
|
|
+ '\u2ee8': '麦', '\u2ee9': '黄', '\u2ef0': '龙',
|
|
|
+}
|
|
|
+
|
|
|
+# ── 已知指标名词表 ──
|
|
|
+KNOWN_MACRO_NUTRIENTS = ['碳水化合物', '蛋白质', '脂肪', '纤维素', '乳制品']
|
|
|
+KNOWN_AMINO_ACIDS = sorted(['苏氨酸', '异亮氨酸', '亮氨酸', '赖氨酸', '蛋氨酸',
|
|
|
+ '胱氨酸', '苯丙氨酸', '酪氨酸', '缬氨酸', '组氨酸', '丙氨酸', '丝氨酸',
|
|
|
+ '甘氨酸', '脯氨酸', '谷氨酸', '天门冬氨酸', '天冬氨酸', '天冬酰胺',
|
|
|
+ '谷氨酰胺', '精氨酸', '色氨酸', '丙氨酸'])
|
|
|
+KNOWN_VITAMINS = ['维生素A', '维生素B1', '维生素B2', '维生素B5', '维生素B6',
|
|
|
+ '叶酸', '维生素B12', '维生素C', '维生素D', '维生素K2', '维生素E']
|
|
|
+KNOWN_TRACE = ['铁', '锌']
|
|
|
+KNOWN_ANTIBIOTICS = ['β-内酰胺酶类', '氨基糖苷类', '大环内酯类', '呋喃类',
|
|
|
+ '喹诺酮类', '磺胺类', '甲氧苄啶类', '氯霉素类', '四环素类']
|
|
|
+
|
|
|
+# 已知肠道屏障指标
|
|
|
+KNOWN_BARRIER = ['肠道炎症水平', '肠道产气', '肠道屏障', '脂多糖LPS',
|
|
|
+ '次级胆汁酸', '对甲酚(p-Cresol)', '吲哚', '苯酚', '腐胺',
|
|
|
+ '硫化氢', '尸胺']
|
|
|
+# 已知短链脂肪酸
|
|
|
+KNOWN_SCFA = ['丁酸盐(Butyrate)', '丙酸盐(Propionate)', '乙酸盐(Acetate)',
|
|
|
+ '异戊酸盐(Isovaleric)']
|
|
|
+# 已知神经递质及激素
|
|
|
+KNOWN_NEUROTRANSMITTER = ['血清素(5-HT)', 'γ-氨基丁酸(GABA)',
|
|
|
+ '谷氨酸(Glutamate)', '色氨酸(Tryptophan)', 'DOPAC',
|
|
|
+ '多巴胺', '组胺(Histamine)', '一氧化氮', '喹啉(Quinolinic)',
|
|
|
+ '维生素K2', '肌醇(Inositol)', '肾上腺素', '去甲肾上腺素',
|
|
|
+ '乙酰胆碱', '皮质醇']
|
|
|
+
|
|
|
+COLUMNS_FOOD = ['名称', '分类', '推荐指数', '能量KJ', '蛋白g', '脂肪g',
|
|
|
+ '碳水化合物g', '淀粉g', '总膳食纤维g', '胆固醇mg']
|
|
|
+KNOWN_CATS = ['主食', '乳制品', '干果', '坚果', '快餐', '水产品',
|
|
|
+ '水果', '汤', '肉类', '蔬菜', '豆类及豆制品', '蛋类', '饮料']
|
|
|
+FOOD_SKIP_TEXTS = [
|
|
|
+ '根据您的肠道菌群', '分值从-100', '食物推荐考虑', '食物推荐是综合',
|
|
|
+ '需要注意的是', '本饮食推荐', '该饮食推荐根据', '后续表格中的营养',
|
|
|
+ '16S 高通量测序', '基于机器学习和', '肠道菌群健康检测报告说明',
|
|
|
+ '检测方法及局限性', '数据分析及模型', '结果解读及使用',
|
|
|
+ '影响因素说明', '建议将检测结果', '营养建议说明',
|
|
|
+ '重要提示', '推荐食物清单', '实际食用时需结合', '如有特殊疾病',
|
|
|
+ '免责声明', '本检测报告仅供', '以上模型预测', '正常范围的定义',
|
|
|
+ '募极生物',
|
|
|
+]
|
|
|
+
|
|
|
+
|
|
|
+def norm(s):
|
|
|
+ return ''.join(RADICAL_MAP.get(c, c) for c in s)
|
|
|
+
|
|
|
+
|
|
|
+def clean_lines(text):
|
|
|
+ lines = []
|
|
|
+ for line in norm(text).split('\n'):
|
|
|
+ ls = line.strip()
|
|
|
+ if not ls or re.match(r'^\d+/\d+$', ls):
|
|
|
+ continue
|
|
|
+ lines.append(ls)
|
|
|
+ return lines
|
|
|
+
|
|
|
+
|
|
|
+def detect_format(pages_text):
|
|
|
+ for pt in pages_text:
|
|
|
+ t = norm(pt)
|
|
|
+ if '疾病风险评估' in t and '指标范围' in t:
|
|
|
+ for line in t.split('\n'):
|
|
|
+ ls = line.strip()
|
|
|
+ if not ls: continue
|
|
|
+ if re.search(r'[\u4e00-\u9fff]+\d+\.?\d*[\u4e00-\u9fff]+', ls):
|
|
|
+ return 'inline'
|
|
|
+ return 'triplet'
|
|
|
+ return 'triplet'
|
|
|
+
|
|
|
+
|
|
|
+# ── 三元组解析 ──
|
|
|
+def parse_triplet_until(lines, stop_markers):
|
|
|
+ results = []
|
|
|
+ i = 0
|
|
|
+ while i < len(lines):
|
|
|
+ if any(lines[i] == sm or lines[i].startswith(sm) for sm in stop_markers):
|
|
|
+ break
|
|
|
+ if lines[i] == '指标范围':
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ name = lines[i]
|
|
|
+ if i + 2 >= len(lines):
|
|
|
+ break
|
|
|
+ val = lines[i + 1]
|
|
|
+ status = lines[i + 2]
|
|
|
+ if re.match(r'^-?\d+\.?\d*$', val):
|
|
|
+ results.append({'名称': name, '数值': val, '状态': status})
|
|
|
+ i += 3
|
|
|
+ else:
|
|
|
+ i += 1
|
|
|
+ return results, lines[i:]
|
|
|
+
|
|
|
+
|
|
|
+# ── inline解析(v5:支持名称词表过滤) ──
|
|
|
+def parse_inline(text, name_whitelist=None):
|
|
|
+ """压缩同行格式解析。如果提供 name_whitelist,只返回在词表中的条目。"""
|
|
|
+ results = []
|
|
|
+ # inline 格式:中文名+数字+状态(中文或/)
|
|
|
+ pattern = r'([\u4e00-\u9fff()\u2014\-\u2212]+?)(\d+(?:\.\d+)?)([\u4e00-\u9fff/]+)'
|
|
|
+ for m in re.finditer(pattern, text):
|
|
|
+ name = m.group(1).strip()
|
|
|
+ val = m.group(2)
|
|
|
+ status = m.group(3).strip()
|
|
|
+ if len(name) <= 1:
|
|
|
+ continue
|
|
|
+ # 始终过滤掉非指标标记
|
|
|
+ if any(k in name for k in ['指标范围', '疾病风险', '营养状况', '抗生素风险',
|
|
|
+ '注:', '注:', '页', '分值', '正常',
|
|
|
+ '本次', '根据', '用户', '共检测',
|
|
|
+ '三项', '核心', '您的厚壁', '注意',
|
|
|
+ '异常', '健康', '肠道菌群',
|
|
|
+ '水平', '肠道炎症']):
|
|
|
+ continue
|
|
|
+ if any(k in status for k in ['指标范围', '疾病风险', '养分', '范例']):
|
|
|
+ continue
|
|
|
+ # 如果提供词表,只返回词表内的
|
|
|
+ if name_whitelist is not None:
|
|
|
+ if name not in name_whitelist:
|
|
|
+ continue
|
|
|
+ try:
|
|
|
+ fv = float(val)
|
|
|
+ if 0 <= fv <= 1000:
|
|
|
+ results.append({'名称': name, '数值': val, '状态': status})
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+ return results
|
|
|
+
|
|
|
+
|
|
|
+def parse_inline_region(text, start_marker, end_markers, name_whitelist=None):
|
|
|
+ sidx = text.find(start_marker)
|
|
|
+ if sidx == -1:
|
|
|
+ return [], text
|
|
|
+ end_pos = len(text)
|
|
|
+ for em in end_markers:
|
|
|
+ ei = text.find(em, sidx + len(start_marker))
|
|
|
+ if ei != -1 and ei < end_pos:
|
|
|
+ end_pos = ei
|
|
|
+ region = text[sidx:end_pos]
|
|
|
+ results = parse_inline(region, name_whitelist)
|
|
|
+ return results, region
|
|
|
+
|
|
|
+
|
|
|
+# ==========================================
|
|
|
+# 报告概述
|
|
|
+# ==========================================
|
|
|
+def extract_overview(lines_all, label):
|
|
|
+ r = {'姓名': label}
|
|
|
+ t = '\n'.join(lines_all)
|
|
|
+
|
|
|
+ m = re.search(r'编号\s*(\S+)', t)
|
|
|
+ if m: r['编号'] = m.group(1)
|
|
|
+ m = re.search(r'姓名\s*(\S+)', t)
|
|
|
+ if m: r['姓名'] = m.group(1)
|
|
|
+ for pat in ['年龄', '性别']:
|
|
|
+ m = re.search(rf'{pat}\s*(\S+)', t)
|
|
|
+ if m: r[pat] = m.group(1)
|
|
|
+ m = re.search(r'肠道预测年龄[:\s]*([\d.]+岁?)', t)
|
|
|
+ if m: r['肠道预测年龄'] = m.group(1)
|
|
|
+ m = re.search(r'肠型[:\s]*(\S+)', t)
|
|
|
+ if m: r['肠型'] = m.group(1)
|
|
|
+ for kw in ['肠道菌群平衡', '菌群多样性', '有益菌', '有害菌', '核心菌属']:
|
|
|
+ m = re.search(rf'{kw}\s*(\d+)', t)
|
|
|
+ if m: r[kw] = m.group(1)
|
|
|
+ m = re.search(r'健康总分\s*(\d+)', t)
|
|
|
+ if m: r['健康总分'] = m.group(1)
|
|
|
+ for kw in ['菌群健康', '慢病控制', '营养均衡']:
|
|
|
+ m = re.search(rf'{kw}\s*(\d+)', t)
|
|
|
+ if m: r[kw] = m.group(1)
|
|
|
+ return r
|
|
|
+
|
|
|
+
|
|
|
+# ==========================================
|
|
|
+# 三元组格式 - 按页提取各模块
|
|
|
+# ==========================================
|
|
|
+def extract_all_triplet(all_lines):
|
|
|
+ data_pages = []
|
|
|
+ for i, line in enumerate(all_lines):
|
|
|
+ if line == '指标范围':
|
|
|
+ data_pages.append(i)
|
|
|
+
|
|
|
+ result = {}
|
|
|
+
|
|
|
+ # 数据页1: 疾病风险评估
|
|
|
+ if len(data_pages) >= 1:
|
|
|
+ i = data_pages[0] + 1
|
|
|
+ if i < len(all_lines) and all_lines[i] == '疾病风险评估':
|
|
|
+ i += 1
|
|
|
+ rows, _ = parse_triplet_until(all_lines[i:], ['主要营养评估', '氨基酸评估', '维生素评估',
|
|
|
+ '微量元素评估', '主要消化道致病菌',
|
|
|
+ '抗生素风险评估', '抗生素耐药风险'])
|
|
|
+ result['疾病风险评估'] = [r for r in rows if '注:' not in r['名称'] and '注:' not in r['名称']]
|
|
|
+
|
|
|
+ # 数据页2: 营养状况评估
|
|
|
+ if len(data_pages) >= 2:
|
|
|
+ i = data_pages[1] + 1
|
|
|
+ if i < len(all_lines) and all_lines[i] == '营养状况评估':
|
|
|
+ i += 1
|
|
|
+ rows, remainder = parse_triplet_until(all_lines[i:], ['主要营养评估', '氨基酸评估'])
|
|
|
+ result['主要营养评估'] = rows[:5]
|
|
|
+ ama_rows, _ = parse_triplet_until(remainder, ['氨基酸评估'])
|
|
|
+ result['氨基酸评估_p2'] = ama_rows
|
|
|
+
|
|
|
+ # 数据页3: 剩余氨基酸
|
|
|
+ if len(data_pages) >= 3:
|
|
|
+ i = data_pages[2] + 1
|
|
|
+ if i < len(all_lines) and '氨基酸' in all_lines[i]:
|
|
|
+ while i < len(all_lines) and '氨基酸' in all_lines[i]:
|
|
|
+ i += 1
|
|
|
+ remaining_amino, _ = parse_triplet_until(all_lines[i:], ['氨基酸评估'])
|
|
|
+ result['氨基酸评估_p3'] = remaining_amino
|
|
|
+
|
|
|
+ # 数据页4: 维生素评估
|
|
|
+ if len(data_pages) >= 4:
|
|
|
+ i = data_pages[3] + 1
|
|
|
+ if i < len(all_lines) and all_lines[i] == '维生素评估':
|
|
|
+ i += 1
|
|
|
+ vit_rows, remainder = parse_triplet_until(all_lines[i:], ['微量元素评估', '主要消化道致病菌'])
|
|
|
+ vit_names = set(['维生素A', '维生素B1', '维生素B2', '维生素B5', '维生素B6',
|
|
|
+ '叶酸', '维生素B12', '维生素C', '维生素D'])
|
|
|
+ vit = []
|
|
|
+ trace = []
|
|
|
+ for r in vit_rows:
|
|
|
+ if r['名称'] in vit_names or '维生素' in r['名称']:
|
|
|
+ vit.append(r)
|
|
|
+ else:
|
|
|
+ trace.append(r)
|
|
|
+ result['维生素评估'] = vit
|
|
|
+ result['微量元素评估'] = trace
|
|
|
+
|
|
|
+ # 数据页5: 主要消化道致病菌
|
|
|
+ for i in range(len(all_lines)):
|
|
|
+ if all_lines[i] == '主要消化道致病菌':
|
|
|
+ j = i + 1
|
|
|
+ while j < len(all_lines) and all_lines[j] in ['致病菌', '丰度', '评估']:
|
|
|
+ j += 1
|
|
|
+ path_rows = []
|
|
|
+ while j < len(all_lines) and not all_lines[j].startswith('肠道屏障'):
|
|
|
+ name = all_lines[j]
|
|
|
+ if j + 2 < len(all_lines) and re.match(r'^\d+%$', all_lines[j+1]):
|
|
|
+ path_rows.append({'致病菌': name, '丰度': all_lines[j+1], '评估': all_lines[j+2]})
|
|
|
+ j += 3
|
|
|
+ else:
|
|
|
+ j += 1
|
|
|
+ result['主要消化道致病菌'] = path_rows
|
|
|
+ break
|
|
|
+
|
|
|
+ # 抗生素风险评估
|
|
|
+ for i in range(len(all_lines)):
|
|
|
+ if all_lines[i] == '抗生素风险评估':
|
|
|
+ j = i + 1
|
|
|
+ abx_rows, _ = parse_triplet_until(all_lines[j:], ['抗生素耐药风险', '个体化食物推荐表'])
|
|
|
+ result['抗生素风险评估'] = abx_rows
|
|
|
+ break
|
|
|
+
|
|
|
+ return result
|
|
|
+
|
|
|
+
|
|
|
+# ==========================================
|
|
|
+# 肠道屏障及代谢物 - 三元组格式(v5:使用已知指标名精确匹配)
|
|
|
+# ==========================================
|
|
|
+def extract_barrier_and_scfa_triplet(all_lines):
|
|
|
+ results = {'barrier': [], 'scfa': [], 'neurotransmitter': []}
|
|
|
+
|
|
|
+ current_section = None
|
|
|
+ i = 0
|
|
|
+ while i < len(all_lines):
|
|
|
+ line = all_lines[i]
|
|
|
+ if line == '肠道屏障及菌群代谢物':
|
|
|
+ current_section = 'barrier'
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ if '短链脂肪酸' in line:
|
|
|
+ current_section = 'scfa'
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ if '神经递质' in line:
|
|
|
+ current_section = 'neurotransmitter'
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ if current_section is None:
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 跳过头行
|
|
|
+ if line in ['名称', '评估值', '正常范围', '过量', '缺乏', '相关症状'] or \
|
|
|
+ '过量 /' in line or '缺乏' in line:
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 提前终止
|
|
|
+ if current_section == 'barrier' and line in ['短链脂肪酸', '抗生素风险', '抗生素耐药风险']:
|
|
|
+ break
|
|
|
+ if current_section == 'scfa' and line in ['神经递质', '神经递质及激素指标', '神经递质及激素', '抗生素风险']:
|
|
|
+ break
|
|
|
+ if current_section == 'neurotransmitter' and line in ['抗生素风险', '个体化食物推荐表']:
|
|
|
+ break
|
|
|
+
|
|
|
+ # 只处理已知指标名
|
|
|
+ known = {'barrier': KNOWN_BARRIER, 'scfa': KNOWN_SCFA, 'neurotransmitter': KNOWN_NEUROTRANSMITTER}
|
|
|
+ known_list = known[current_section]
|
|
|
+
|
|
|
+ # 检查当前行是否以已知指标名开头
|
|
|
+ matched_name = None
|
|
|
+ for kn in sorted(known_list, key=len, reverse=True):
|
|
|
+ if line.startswith(kn) or line == kn:
|
|
|
+ matched_name = kn
|
|
|
+ break
|
|
|
+ if matched_name is None:
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 尝试整行解析:名称+空格+数值+状态+范围+...
|
|
|
+ parts = line.split()
|
|
|
+ if len(parts) >= 2:
|
|
|
+ # 找数值
|
|
|
+ vi = None
|
|
|
+ for pi, p in enumerate(parts):
|
|
|
+ if re.match(r'^\d+$', p) and pi >= 1:
|
|
|
+ vi = pi
|
|
|
+ break
|
|
|
+ if vi:
|
|
|
+ name = matched_name
|
|
|
+ val = parts[vi]
|
|
|
+ status = ''
|
|
|
+ range_ = ''
|
|
|
+ for rp in parts[vi+1:]:
|
|
|
+ if rp in ['正常', '过多', '轻度产气', '过低', '不足', '缺乏', '不⾜']:
|
|
|
+ status = rp
|
|
|
+ elif re.match(r'^\d+-\d+$', rp):
|
|
|
+ range_ = rp
|
|
|
+ results[current_section].append({
|
|
|
+ '名称': name, '评估值': val, '健康状况': status, '正常范围': range_
|
|
|
+ })
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 模式2:下一行是数值
|
|
|
+ if i + 1 < len(all_lines) and re.match(r'^\d+$', all_lines[i+1]):
|
|
|
+ name = matched_name
|
|
|
+ val = all_lines[i+1]
|
|
|
+ status = ''
|
|
|
+ range_ = ''
|
|
|
+ for j in range(2, min(6, len(all_lines)-i)):
|
|
|
+ if all_lines[i+j] in ['正常', '过多', '轻度产气', '过低', '不足', '缺乏', '不⾜']:
|
|
|
+ status = all_lines[i+j]
|
|
|
+ elif re.match(r'^\d+-\d+$', all_lines[i+j]):
|
|
|
+ range_ = all_lines[i+j]
|
|
|
+ results[current_section].append({
|
|
|
+ '名称': name, '评估值': val, '健康状况': status, '正常范围': range_
|
|
|
+ })
|
|
|
+ i += 2
|
|
|
+ continue
|
|
|
+
|
|
|
+ i += 1
|
|
|
+
|
|
|
+ return results
|
|
|
+
|
|
|
+
|
|
|
+# ==========================================
|
|
|
+# inline 格式中各模块提取(v5:使用已知名称词表)
|
|
|
+# ==========================================
|
|
|
+def extract_inline_module(text, start_marker, end_markers, known_names):
|
|
|
+ """从 inline 文本中提取已知名称的指标(密集同行格式专用)
|
|
|
+
|
|
|
+ inline 格式示例:
|
|
|
+ 维生素A54正常维生素B183正常维生素B253正常
|
|
|
+ 或:β-内酰胺酶类76正常氨基糖苷类97注意
|
|
|
+
|
|
|
+ 已知名称在文本中是紧挨着的,没有空格分隔。
|
|
|
+ 找到名称后,紧跟着的数字就是数值,之后到下一个名称之间的文本就是状态。
|
|
|
+ """
|
|
|
+ results = []
|
|
|
+ sidx = text.find(start_marker)
|
|
|
+ if sidx == -1:
|
|
|
+ return results
|
|
|
+ end_pos = len(text)
|
|
|
+ for em in end_markers:
|
|
|
+ ei = text.find(em, sidx + len(start_marker))
|
|
|
+ if ei != -1 and ei < end_pos:
|
|
|
+ end_pos = ei
|
|
|
+ region = text[sidx:end_pos]
|
|
|
+
|
|
|
+ # 按名称在区域中的位置排序
|
|
|
+ name_positions = []
|
|
|
+ for kn in known_names:
|
|
|
+ idx = region.find(kn)
|
|
|
+ if idx != -1:
|
|
|
+ name_positions.append((idx, kn))
|
|
|
+ name_positions.sort()
|
|
|
+
|
|
|
+ for i, (idx, kn) in enumerate(name_positions):
|
|
|
+ start_after = idx + len(kn)
|
|
|
+ # 取数字
|
|
|
+ val_match = re.match(r'(\d+(?:\.\d+)?)', region[start_after:])
|
|
|
+ if not val_match:
|
|
|
+ continue
|
|
|
+ val = val_match.group(1)
|
|
|
+ val_end = start_after + len(val)
|
|
|
+ # 状态:从数值结束到下一个已知名称开始
|
|
|
+ if i + 1 < len(name_positions):
|
|
|
+ next_idx = name_positions[i + 1][0]
|
|
|
+ status = region[val_end:next_idx].strip()
|
|
|
+ else:
|
|
|
+ status = region[val_end:end_pos].strip()
|
|
|
+ # 剪掉状态中的后续标记(如"正常氨基酸评估"→"正常")
|
|
|
+ status = re.sub(r'注意大环内酯类|注意呋喃类|注意氯霉素类|注意喹诺酮类|注意磺胺类|注意甲氧苄啶类|注意四环素类|正常氨基酸评估|正常维生素评估|正常微量元素评估', '', status).strip()
|
|
|
+ results.append({'名称': kn, '数值': val, '状态': status})
|
|
|
+
|
|
|
+ return results
|
|
|
+
|
|
|
+
|
|
|
+# ==========================================
|
|
|
+# 食物推荐表
|
|
|
+# ==========================================
|
|
|
+def split_7_fields(s):
|
|
|
+ results = []
|
|
|
+ ranges = [(2, 4), (1, 2), (1, 2), (1, 2), (1, 2), (1, 2), (1, 4)]
|
|
|
+ def backtrack(pos, idx, nums):
|
|
|
+ if idx == 7:
|
|
|
+ if pos == len(s):
|
|
|
+ results.append(list(nums))
|
|
|
+ return
|
|
|
+ if pos >= len(s): return
|
|
|
+ lo, hi = ranges[idx]
|
|
|
+ for w in range(lo, min(hi + 1, len(s) - pos + 1)):
|
|
|
+ chunk = s[pos:pos + w]
|
|
|
+ if chunk.isdigit():
|
|
|
+ backtrack(pos + w, idx + 1, nums + [int(chunk)])
|
|
|
+ backtrack(0, 0, [])
|
|
|
+ return results
|
|
|
+
|
|
|
+
|
|
|
+def decode_compressed(name, num_str, ref_vals=None):
|
|
|
+ raw = num_str.lstrip('-')
|
|
|
+ has_neg = num_str.startswith('-')
|
|
|
+ candidates = []
|
|
|
+ for rec_len in range(1, 3):
|
|
|
+ if rec_len > len(raw): continue
|
|
|
+ rec = ('-' if has_neg else '') + raw[:rec_len]
|
|
|
+ try:
|
|
|
+ rec_val = int(rec)
|
|
|
+ if not (-100 <= rec_val <= 100): continue
|
|
|
+ except: continue
|
|
|
+ remain = raw[rec_len:]
|
|
|
+ for nums in split_7_fields(remain):
|
|
|
+ if ref_vals:
|
|
|
+ matches = sum(1 for i in range(7) if ref_vals[i] == nums[i])
|
|
|
+ if matches >= 6:
|
|
|
+ candidates.append([rec_val] + nums)
|
|
|
+ else:
|
|
|
+ candidates.append([rec_val] + nums)
|
|
|
+ if not candidates: return None
|
|
|
+ if ref_vals:
|
|
|
+ candidates.sort(key=lambda r: (sum(1 for i in range(7) if ref_vals[i] == r[1:][i]),
|
|
|
+ -len(str(abs(r[0])))), reverse=True)
|
|
|
+ if sum(1 for i in range(7) if ref_vals[i] == candidates[0][1:][i]) < 6:
|
|
|
+ return None
|
|
|
+ return candidates[0]
|
|
|
+
|
|
|
+
|
|
|
+def extract_food_table(pdf_path, ref_lookup=None):
|
|
|
+ reader = PdfReader(pdf_path)
|
|
|
+ food_start = None
|
|
|
+ for i, page in enumerate(reader.pages):
|
|
|
+ if '个体化食物推荐表' in page.extract_text():
|
|
|
+ food_start = i
|
|
|
+ break
|
|
|
+ if food_start is None:
|
|
|
+ return [], 'not_found'
|
|
|
+
|
|
|
+ first_text = norm(reader.pages[food_start + 1].extract_text())
|
|
|
+ lines = [l.strip() for l in first_text.split('\n') if l.strip() and not re.match(r'\d+/\d+', l)]
|
|
|
+ is_compressed = sum(1 for l in lines[:10] if len(l) > 100) >= 3
|
|
|
+
|
|
|
+ rows = []
|
|
|
+ if is_compressed:
|
|
|
+ fmt = 'compressed'
|
|
|
+ for i in range(food_start + 1, len(reader.pages)):
|
|
|
+ text = norm(reader.pages[i].extract_text())
|
|
|
+ text = re.sub(r'\d+/\d+', '', text)
|
|
|
+ header = '名称分类推荐指数能量KJ蛋白g脂肪g碳水化合物g淀粉g总膳食纤维g胆固醇mg'
|
|
|
+ text = text.replace(header, '')
|
|
|
+ for kw in FOOD_SKIP_TEXTS:
|
|
|
+ text = text.replace(kw, '')
|
|
|
+ while text:
|
|
|
+ best_cat, best_idx = None, len(text)
|
|
|
+ for cat in KNOWN_CATS:
|
|
|
+ idx = text.find(cat)
|
|
|
+ if idx != -1 and idx < best_idx:
|
|
|
+ best_idx, best_cat = idx, cat
|
|
|
+ if best_cat is None: break
|
|
|
+ name = text[:best_idx]
|
|
|
+ text = text[best_idx + len(best_cat):]
|
|
|
+ num_str = ''
|
|
|
+ while text and (text[0].isdigit() or text[0] in '-\u2212\u2014'):
|
|
|
+ c = '-' if text[0] in '\u2212\u2014' else text[0]
|
|
|
+ num_str += c
|
|
|
+ text = text[1:]
|
|
|
+ if not name or not num_str: continue
|
|
|
+ ref_vals = ref_lookup.get(name) if ref_lookup else None
|
|
|
+ decoded = decode_compressed(name, num_str, ref_vals)
|
|
|
+ if decoded:
|
|
|
+ rows.append(dict(zip(COLUMNS_FOOD, [name, best_cat] + [str(v) for v in decoded])))
|
|
|
+ else:
|
|
|
+ fmt = 'vertical'
|
|
|
+ all_lines = []
|
|
|
+ for i in range(food_start + 1, len(reader.pages)):
|
|
|
+ for line in norm(reader.pages[i].extract_text()).split('\n'):
|
|
|
+ lt = line.strip()
|
|
|
+ if not lt or re.match(r'\d+/\d+', lt) or lt in COLUMNS_FOOD:
|
|
|
+ continue
|
|
|
+ if len(lt) > 60 and any(k in lt for k in FOOD_SKIP_TEXTS):
|
|
|
+ continue
|
|
|
+ all_lines.append(lt)
|
|
|
+ i = 0
|
|
|
+ while i + 9 < len(all_lines):
|
|
|
+ name = all_lines[i].strip()
|
|
|
+ cat = all_lines[i + 1].strip()
|
|
|
+ if cat not in KNOWN_CATS:
|
|
|
+ i += 1
|
|
|
+ continue
|
|
|
+ nums = []
|
|
|
+ ok = True
|
|
|
+ for j in range(2, 10):
|
|
|
+ v = all_lines[i + j].replace('\u2212', '-').replace('\u2014', '-').strip()
|
|
|
+ try: int(v); nums.append(v)
|
|
|
+ except: ok = False; break
|
|
|
+ if ok and len(nums) == 8:
|
|
|
+ rows.append(dict(zip(COLUMNS_FOOD, [name, cat] + nums)))
|
|
|
+ i += 1
|
|
|
+ return rows, fmt
|
|
|
+
|
|
|
+
|
|
|
+# ==========================================
|
|
|
+# 单 PDF ➔ JSON 输出(v5 新增)
|
|
|
+# ==========================================
|
|
|
+def extract_pdf_to_json(pdf_path):
|
|
|
+ """输入单个PDF文件路径,输出JSON格式的全量指标内容"""
|
|
|
+ reader = PdfReader(pdf_path)
|
|
|
+ pages_text = [p.extract_text() for p in reader.pages]
|
|
|
+ fmt = detect_format(pages_text)
|
|
|
+ label = os.path.splitext(os.path.basename(pdf_path))[0]
|
|
|
+
|
|
|
+ all_full_text = '\n\n---PAGEBREAK---\n\n'.join(norm(p.extract_text()) for p in reader.pages)
|
|
|
+ all_lines = []
|
|
|
+ for pt in pages_text:
|
|
|
+ all_lines.extend(clean_lines(pt))
|
|
|
+
|
|
|
+ result = {
|
|
|
+ '文件名': label,
|
|
|
+ '格式': fmt,
|
|
|
+ '总页数': len(reader.pages),
|
|
|
+ }
|
|
|
+
|
|
|
+ # 1. 报告概述
|
|
|
+ overview = extract_overview(clean_lines(pages_text[0]), label)
|
|
|
+ # 如果第一页没找到关键信息,搜其他页
|
|
|
+ found_info = any(k in str(overview) for k in ['肠道预测年龄', '核心菌属'])
|
|
|
+ if not found_info:
|
|
|
+ for pt in pages_text:
|
|
|
+ if '基本信息' in norm(pt) and '肠道预测年龄' in norm(pt):
|
|
|
+ overview = extract_overview(clean_lines(pt), label)
|
|
|
+ break
|
|
|
+ result['报告概述'] = overview
|
|
|
+
|
|
|
+ if fmt == 'triplet':
|
|
|
+ parsed = extract_all_triplet(all_lines)
|
|
|
+
|
|
|
+ # 疾病风险评估
|
|
|
+ result['疾病风险评估'] = parsed.get('疾病风险评估', [])
|
|
|
+ # 主要营养评估
|
|
|
+ result['主要营养评估'] = parsed.get('主要营养评估', [])
|
|
|
+ # 氨基酸
|
|
|
+ amino = parsed.get('氨基酸评估_p2', []) + parsed.get('氨基酸评估_p3', [])
|
|
|
+ result['氨基酸评估'] = amino
|
|
|
+ # 维生素
|
|
|
+ result['维生素评估'] = parsed.get('维生素评估', [])
|
|
|
+ # 微量元素
|
|
|
+ result['微量元素评估'] = parsed.get('微量元素评估', [])
|
|
|
+ # 主要消化道致病菌
|
|
|
+ result['主要消化道致病菌'] = parsed.get('主要消化道致病菌', [])
|
|
|
+ # 抗生素风险评估
|
|
|
+ result['抗生素风险评估'] = parsed.get('抗生素风险评估', [])
|
|
|
+
|
|
|
+ # 肠道屏障 + 短链脂肪酸 + 神经递质
|
|
|
+ bs = extract_barrier_and_scfa_triplet(all_lines)
|
|
|
+ result['肠道屏障及代谢物'] = bs['barrier']
|
|
|
+ result['短链脂肪酸'] = bs['scfa']
|
|
|
+ result['神经递质及激素'] = bs['neurotransmitter']
|
|
|
+
|
|
|
+ else:
|
|
|
+ # inline 格式
|
|
|
+ result['疾病风险评估'] = parse_inline_region(all_full_text, '疾病风险评估',
|
|
|
+ ['主要营养评估', '主要消化道致病菌'])[0]
|
|
|
+
|
|
|
+ # 主要营养评估:使用精确区域+已知5项
|
|
|
+ result['主要营养评估'] = extract_inline_module(all_full_text, '营养状况评估',
|
|
|
+ ['主要营养评估', '氨基酸评估'], KNOWN_MACRO_NUTRIENTS)
|
|
|
+
|
|
|
+ # 氨基酸评估
|
|
|
+ result['氨基酸评估'] = extract_inline_module(all_full_text, '氨基酸评估',
|
|
|
+ ['维生素评估', '主要消化道致病菌'], KNOWN_AMINO_ACIDS)
|
|
|
+
|
|
|
+ # 维生素评估
|
|
|
+ result['维生素评估'] = extract_inline_module(all_full_text, '维生素评估',
|
|
|
+ ['微量元素评估', '主要消化道致病菌'], KNOWN_VITAMINS)
|
|
|
+
|
|
|
+ # 微量元素评估
|
|
|
+ result['微量元素评估'] = extract_inline_module(all_full_text, '微量元素评估',
|
|
|
+ ['主要消化道致病菌'], KNOWN_TRACE)
|
|
|
+
|
|
|
+ # 主要消化道致病菌
|
|
|
+ path_rows, _ = parse_inline_region(all_full_text, '主要消化道致病菌', ['肠道屏障'])
|
|
|
+ result['主要消化道致病菌'] = path_rows
|
|
|
+
|
|
|
+ # 抗生素风险评估(使用已知抗生素词表)
|
|
|
+ result['抗生素风险评估'] = extract_inline_module(all_full_text, '抗生素风险评估',
|
|
|
+ ['抗生素耐药风险', '个体化食物推荐表'], KNOWN_ANTIBIOTICS)
|
|
|
+
|
|
|
+ # 肠道屏障 - inline 使用精确词表
|
|
|
+ result['肠道屏障及代谢物'] = extract_inline_module(all_full_text, '肠道屏障及菌群代谢物',
|
|
|
+ ['短链脂肪酸', '神经递质', '抗生素风险'], KNOWN_BARRIER)
|
|
|
+
|
|
|
+ # 短链脂肪酸
|
|
|
+ result['短链脂肪酸'] = extract_inline_module(all_full_text, '短链脂肪酸',
|
|
|
+ ['神经递质', '抗生素风险'], KNOWN_SCFA)
|
|
|
+
|
|
|
+ # 神经递质
|
|
|
+ result['神经递质及激素'] = extract_inline_module(all_full_text, '神经递质',
|
|
|
+ ['抗生素风险', '个体化食物推荐表'], KNOWN_NEUROTRANSMITTER)
|
|
|
+
|
|
|
+ # 食物推荐表
|
|
|
+ food_rows, food_fmt = extract_food_table(pdf_path)
|
|
|
+ result['个体化食物推荐表'] = {
|
|
|
+ '格式': food_fmt,
|
|
|
+ '条目数': len(food_rows),
|
|
|
+ '数据': food_rows
|
|
|
+ }
|
|
|
+
|
|
|
+ return result
|
|
|
+
|
|
|
+
|
|
|
+# ==========================================
|
|
|
+# 主流程 - 批量 CSV 模式
|
|
|
+# ==========================================
|
|
|
+def main_csv():
|
|
|
+ pdf_files = sorted(f for f in os.listdir(BASE) if f.lower().endswith('.pdf'))
|
|
|
+ if not pdf_files:
|
|
|
+ print('未找到PDF文件')
|
|
|
+ return
|
|
|
+
|
|
|
+ all_data = {}
|
|
|
+ print('读取PDF文件...')
|
|
|
+ for pdf_file in pdf_files:
|
|
|
+ pdf_path = os.path.join(BASE, pdf_file)
|
|
|
+ label = pdf_file.replace('.pdf', '')
|
|
|
+ try:
|
|
|
+ reader = PdfReader(pdf_path)
|
|
|
+ pages_text = [p.extract_text() for p in reader.pages]
|
|
|
+ fmt = detect_format(pages_text)
|
|
|
+ all_data[label] = {'reader': reader, 'pages_text': pages_text, 'fmt': fmt}
|
|
|
+ print(f' {label}: {len(pages_text)} pages, format={fmt}')
|
|
|
+ except Exception as e:
|
|
|
+ print(f' {label}: ERROR - {e}')
|
|
|
+
|
|
|
+ if not all_data:
|
|
|
+ print('无可处理的PDF')
|
|
|
+ return
|
|
|
+
|
|
|
+ triplet_labels = [l for l, d in all_data.items() if d['fmt'] == 'triplet']
|
|
|
+ inline_labels = [l for l, d in all_data.items() if d['fmt'] == 'inline']
|
|
|
+ all_labels = list(all_data.keys())
|
|
|
+
|
|
|
+ print(f'\ntriplet: {triplet_labels}, inline: {inline_labels}')
|
|
|
+
|
|
|
+ all_full_text = {}
|
|
|
+ all_full_lines = {}
|
|
|
+ for label, d in all_data.items():
|
|
|
+ all_full_text[label] = '\n\n---PAGEBREAK---\n\n'.join(norm(p.extract_text()) for p in d['reader'].pages)
|
|
|
+ lines = []
|
|
|
+ for pt in d['pages_text']:
|
|
|
+ lines.extend(clean_lines(pt))
|
|
|
+ all_full_lines[label] = lines
|
|
|
+
|
|
|
+ # ── 1. 报告概述 ──
|
|
|
+ print('\n[1/11] 报告概述...')
|
|
|
+ overviews = []
|
|
|
+ for label, d in all_data.items():
|
|
|
+ lines = clean_lines(d['pages_text'][0])
|
|
|
+ found_info = any(k in '\n'.join(lines) for k in ['肠道预测年龄', '核心菌属'])
|
|
|
+ if not found_info:
|
|
|
+ for pt in d['pages_text']:
|
|
|
+ if '基本信息' in norm(pt) and '肠道预测年龄' in norm(pt):
|
|
|
+ lines = clean_lines(pt)
|
|
|
+ break
|
|
|
+ r = extract_overview(lines, label)
|
|
|
+ overviews.append(r)
|
|
|
+
|
|
|
+ overview_cols = ['姓名', '编号', '年龄', '性别', '肠道预测年龄', '肠型',
|
|
|
+ '肠道菌群平衡', '菌群多样性', '有益菌', '有害菌', '核心菌属',
|
|
|
+ '健康总分', '菌群健康', '慢病控制', '营养均衡']
|
|
|
+ with open(os.path.join(OUTDIR, '报告概述.csv'), 'w', newline='', encoding='utf-8-sig') as f:
|
|
|
+ w = csv.DictWriter(f, fieldnames=overview_cols, extrasaction='ignore')
|
|
|
+ w.writeheader()
|
|
|
+ w.writerows(overviews)
|
|
|
+ print(f' -> CSV/报告概述.csv ({len(overviews)} 行)')
|
|
|
+
|
|
|
+ # ── 2-9. 模块数据(v5:使用精确词表过滤 inline) ──
|
|
|
+ modules = [
|
|
|
+ ('疾病风险评估', '疾病', '疾病风险评估.csv', '疾病风险评估', ['主要营养评估', '主要消化道致病菌'], None),
|
|
|
+ ('主要营养评估', '指标', '主要营养评估.csv', '营养状况评估', ['主要营养评估', '氨基酸评估'], KNOWN_MACRO_NUTRIENTS),
|
|
|
+ ('氨基酸评估', '氨基酸', '氨基酸评估.csv', '氨基酸评估', ['维生素评估', '主要消化道致病菌'], KNOWN_AMINO_ACIDS),
|
|
|
+ ('维生素评估', '维生素', '维生素评估.csv', '维生素评估', ['微量元素评估', '主要消化道致病菌'], KNOWN_VITAMINS),
|
|
|
+ ('微量元素评估', '微量元素', '微量元素评估.csv', '微量元素评估', ['主要消化道致病菌'], KNOWN_TRACE),
|
|
|
+ ('抗生素风险评估', '抗生素', '抗生素风险评估.csv', '抗生素风险评估', ['抗生素耐药风险', '个体化食物推荐表'], KNOWN_ANTIBIOTICS),
|
|
|
+ ]
|
|
|
+
|
|
|
+ for mod_name, name_col, fname, start_mk, end_mks, whitelist in modules:
|
|
|
+ print(f' [{mod_name}]...')
|
|
|
+ data = {}
|
|
|
+
|
|
|
+ for label in triplet_labels:
|
|
|
+ parsed = extract_all_triplet(all_full_lines[label])
|
|
|
+ rows = parsed.get(mod_name, [])
|
|
|
+ if not rows and mod_name == '氨基酸评估':
|
|
|
+ rows = parsed.get('氨基酸评估_p2', []) + parsed.get('氨基酸评估_p3', [])
|
|
|
+ for r in rows:
|
|
|
+ data.setdefault(r['名称'], {})[label] = r['数值']
|
|
|
+
|
|
|
+ for label in inline_labels:
|
|
|
+ if whitelist:
|
|
|
+ rows = extract_inline_module(all_full_text[label], start_mk, end_mks, whitelist)
|
|
|
+ else:
|
|
|
+ rows, _ = parse_inline_region(all_full_text[label], start_mk, end_mks)
|
|
|
+ for r in rows:
|
|
|
+ data.setdefault(r['名称'], {})[label] = r['数值']
|
|
|
+
|
|
|
+ if data:
|
|
|
+ labels = all_labels
|
|
|
+ with open(os.path.join(OUTDIR, fname), 'w', newline='', encoding='utf-8-sig') as f:
|
|
|
+ w = csv.DictWriter(f, fieldnames=[name_col] + labels)
|
|
|
+ w.writeheader()
|
|
|
+ for name, vals in sorted(data.items()):
|
|
|
+ row = {name_col: name}
|
|
|
+ row.update(vals)
|
|
|
+ w.writerow(row)
|
|
|
+ print(f' -> CSV/{fname} ({len(data)} 指标)')
|
|
|
+ else:
|
|
|
+ print(f' (无数据)')
|
|
|
+
|
|
|
+ # ── 7. 主要消化道致病菌 ──
|
|
|
+ print('[7/11] 主要消化道致病菌...')
|
|
|
+ path_data = {}
|
|
|
+ for label in triplet_labels:
|
|
|
+ parsed = extract_all_triplet(all_full_lines[label])
|
|
|
+ for r in parsed.get('主要消化道致病菌', []):
|
|
|
+ path_data.setdefault(r['致病菌'], {})[label] = r['丰度']
|
|
|
+
|
|
|
+ for label in inline_labels:
|
|
|
+ rows, _ = parse_inline_region(all_full_text[label], '主要消化道致病菌', ['肠道屏障'])
|
|
|
+ for r in rows:
|
|
|
+ path_data.setdefault(r['名称'], {})[label] = r['数值'] if '%' in r['数值'] else r['数值'] + '%'
|
|
|
+
|
|
|
+ if path_data:
|
|
|
+ labels = all_labels
|
|
|
+ with open(os.path.join(OUTDIR, '主要消化道致病菌.csv'), 'w', newline='', encoding='utf-8-sig') as f:
|
|
|
+ w = csv.DictWriter(f, fieldnames=['致病菌'] + labels)
|
|
|
+ w.writeheader()
|
|
|
+ for name, vals in sorted(path_data.items()):
|
|
|
+ row = {'致病菌': name}
|
|
|
+ row.update(vals)
|
|
|
+ w.writerow(row)
|
|
|
+ print(f' -> CSV/主要消化道致病菌.csv ({len(path_data)} 菌种)')
|
|
|
+
|
|
|
+ # ── 8. 肠道屏障及代谢物 ──
|
|
|
+ print('[8/11] 肠道屏障及代谢物...')
|
|
|
+ with open(os.path.join(OUTDIR, '肠道屏障及代谢物.csv'), 'w', newline='', encoding='utf-8-sig') as f:
|
|
|
+ w = csv.writer(f)
|
|
|
+ w.writerow(['指标', '姓名', '评估值', '健康状况', '正常范围', '症状'])
|
|
|
+ total = 0
|
|
|
+
|
|
|
+ for label in triplet_labels:
|
|
|
+ bs = extract_barrier_and_scfa_triplet(all_full_lines[label])
|
|
|
+ for item in bs.get('barrier', []):
|
|
|
+ w.writerow([item['名称'], label, item.get('评估值',''), item.get('健康状况',''),
|
|
|
+ item.get('正常范围',''), item.get('症状','')])
|
|
|
+ total += 1
|
|
|
+
|
|
|
+ for label in inline_labels:
|
|
|
+ rows = extract_inline_module(all_full_text[label], '肠道屏障及菌群代谢物',
|
|
|
+ ['短链脂肪酸', '神经递质', '抗生素风险'], KNOWN_BARRIER)
|
|
|
+ for item in rows:
|
|
|
+ w.writerow([item['名称'], label, item.get('数值',''), item.get('状态',''), '', ''])
|
|
|
+ total += 1
|
|
|
+
|
|
|
+ print(f' -> CSV/肠道屏障及代谢物.csv ({total} 条)')
|
|
|
+
|
|
|
+ # 短链脂肪酸
|
|
|
+ print(' [短链脂肪酸]...')
|
|
|
+ scfa_total = 0
|
|
|
+ with open(os.path.join(OUTDIR, '肠道屏障及代谢物.csv'), 'a', newline='', encoding='utf-8-sig') as f:
|
|
|
+ w = csv.writer(f)
|
|
|
+ for label in triplet_labels:
|
|
|
+ bs = extract_barrier_and_scfa_triplet(all_full_lines[label])
|
|
|
+ for item in bs.get('scfa', []):
|
|
|
+ w.writerow([item['名称'], label, item.get('评估值',''), item.get('健康状况',''),
|
|
|
+ item.get('正常范围',''), item.get('症状','')])
|
|
|
+ scfa_total += 1
|
|
|
+ for label in inline_labels:
|
|
|
+ rows = extract_inline_module(all_full_text[label], '短链脂肪酸',
|
|
|
+ ['神经递质', '抗生素风险'], KNOWN_SCFA)
|
|
|
+ for item in rows:
|
|
|
+ w.writerow([item['名称'], label, item.get('数值',''), item.get('状态',''), '', ''])
|
|
|
+ scfa_total += 1
|
|
|
+ print(f' (短链脂肪酸 {scfa_total} 条, 已追加)')
|
|
|
+
|
|
|
+ # 神经递质
|
|
|
+ print(' [神经递质]...')
|
|
|
+ nt_total = 0
|
|
|
+ with open(os.path.join(OUTDIR, '肠道屏障及代谢物.csv'), 'a', newline='', encoding='utf-8-sig') as f:
|
|
|
+ w = csv.writer(f)
|
|
|
+ for label in triplet_labels:
|
|
|
+ bs = extract_barrier_and_scfa_triplet(all_full_lines[label])
|
|
|
+ for item in bs.get('neurotransmitter', []):
|
|
|
+ w.writerow([item['名称'], label, item.get('评估值',''), item.get('健康状况',''),
|
|
|
+ item.get('正常范围',''), item.get('症状','')])
|
|
|
+ nt_total += 1
|
|
|
+ for label in inline_labels:
|
|
|
+ rows = extract_inline_module(all_full_text[label], '神经递质',
|
|
|
+ ['抗生素风险', '个体化食物推荐表'], KNOWN_NEUROTRANSMITTER)
|
|
|
+ for item in rows:
|
|
|
+ w.writerow([item['名称'], label, item.get('数值',''), item.get('状态',''), '', ''])
|
|
|
+ nt_total += 1
|
|
|
+ print(f' (神经递质 {nt_total} 条, 已追加)')
|
|
|
+
|
|
|
+ # ── 10. 个体化食物推荐表 ──
|
|
|
+ print('[10/11] 个体化食物推荐表...')
|
|
|
+ ref_nutrition = {}
|
|
|
+ for label in triplet_labels:
|
|
|
+ if '侯' in label:
|
|
|
+ pdf_path = os.path.join(BASE, label + '.pdf')
|
|
|
+ ref_rows, _ = extract_food_table(pdf_path)
|
|
|
+ for r in ref_rows:
|
|
|
+ ref_nutrition[r['名称']] = [int(r[k]) for k in
|
|
|
+ ['能量KJ', '蛋白g', '脂肪g', '碳水化合物g', '淀粉g', '总膳食纤维g', '胆固醇mg']]
|
|
|
+ break
|
|
|
+
|
|
|
+ for label, d in all_data.items():
|
|
|
+ pdf_path = os.path.join(BASE, label + '.pdf')
|
|
|
+ rows, fmt = extract_food_table(pdf_path, ref_nutrition)
|
|
|
+ fname = f'{label}-个体化食物推荐表.csv'
|
|
|
+ with open(os.path.join(OUTDIR, fname), 'w', newline='', encoding='utf-8-sig') as f:
|
|
|
+ w = csv.DictWriter(f, fieldnames=COLUMNS_FOOD)
|
|
|
+ w.writeheader()
|
|
|
+ w.writerows(rows)
|
|
|
+ print(f' -> CSV/{fname} ({len(rows)} 条, {fmt})')
|
|
|
+
|
|
|
+ # ── 11. 推荐指数汇总 ──
|
|
|
+ print('[11/11] 推荐指数汇总...')
|
|
|
+ rec_all = {}
|
|
|
+ for label, d in all_data.items():
|
|
|
+ pdf_path = os.path.join(BASE, label + '.pdf')
|
|
|
+ rows, _ = extract_food_table(pdf_path, ref_nutrition)
|
|
|
+ rec_all[label] = {r['名称']: r['推荐指数'] for r in rows}
|
|
|
+
|
|
|
+ std_label = next((l for l in triplet_labels if '侯' in l), triplet_labels[0])
|
|
|
+ pdf_path = os.path.join(BASE, std_label + '.pdf')
|
|
|
+ std_rows, _ = extract_food_table(pdf_path)
|
|
|
+
|
|
|
+ sum_cols = ['名称', '分类'] + ['能量KJ', '蛋白g', '脂肪g', '碳水化合物g', '淀粉g',
|
|
|
+ '总膳食纤维g', '胆固醇mg'] + all_labels
|
|
|
+ with open(os.path.join(OUTDIR, '推荐指数汇总.csv'), 'w', newline='', encoding='utf-8-sig') as f:
|
|
|
+ w = csv.DictWriter(f, fieldnames=sum_cols)
|
|
|
+ w.writeheader()
|
|
|
+ for r in std_rows:
|
|
|
+ name = r['名称']
|
|
|
+ row = {'名称': name, '分类': r['分类']}
|
|
|
+ for k in ['能量KJ', '蛋白g', '脂肪g', '碳水化合物g', '淀粉g', '总膳食纤维g', '胆固醇mg']:
|
|
|
+ row[k] = r[k]
|
|
|
+ for la in all_labels:
|
|
|
+ row[la] = rec_all.get(la, {}).get(name, '')
|
|
|
+ w.writerow(row)
|
|
|
+ print(f' -> CSV/推荐指数汇总.csv ({len(std_rows)} 行)')
|
|
|
+
|
|
|
+ print(f'\n完成!所有CSV已输出到 {OUTDIR}/')
|
|
|
+
|
|
|
+
|
|
|
+# ==========================================
|
|
|
+# 主入口
|
|
|
+# ==========================================
|
|
|
+def main():
|
|
|
+ if len(sys.argv) >= 3 and sys.argv[1] == '-j':
|
|
|
+ # JSON 单文件输出模式
|
|
|
+ pdf_path = sys.argv[2]
|
|
|
+ if not os.path.isfile(pdf_path):
|
|
|
+ print(f'错误:找不到文件 {pdf_path}')
|
|
|
+ sys.exit(1)
|
|
|
+ result = extract_pdf_to_json(pdf_path)
|
|
|
+ print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
|
|
|
+ else:
|
|
|
+ main_csv()
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == '__main__':
|
|
|
+ main()
|