Kaynağa Gözat

feat(parser): 北京菌群报告解析集成——指纹检测+表格提取+统一输出

- 自动检测北京菌群报告格式(GMHI/高通量测序等指纹)
- pdfplumber表格提取SCFA/核心菌属/营养代谢/抗生素/毒性物质
- 统一输出结构:两种报告类型产出相同字段集
- 标准报告北京特有字段保持空默认值
liaoxg 1 ay önce
ebeveyn
işleme
aee87e6670
1 değiştirilmiş dosya ile 712 ekleme ve 0 silme
  1. 712 0
      cfc-langgraph/app/parsers/pdf_parser.py

+ 712 - 0
cfc-langgraph/app/parsers/pdf_parser.py

@@ -159,6 +159,12 @@ def parse_triplet_until(lines, stop_markers):
 def parse_report_pdf(file_path: str) -> dict:
 def parse_report_pdf(file_path: str) -> dict:
     """主函数:解析 PDF 返回结构化数据"""
     """主函数:解析 PDF 返回结构化数据"""
     lines = extract_text(file_path)
     lines = extract_text(file_path)
+    full_text = '\n'.join(lines)
+
+    # 指纹检测:北京菌群报告
+    if detect_beijing_format(full_text):
+        return _parse_beijing_report(file_path)
+
     fmt = detect_format(lines)
     fmt = detect_format(lines)
     result = {'format': fmt, 'overview': parse_overview(lines)}
     result = {'format': fmt, 'overview': parse_overview(lines)}
 
 
@@ -204,6 +210,21 @@ def parse_report_pdf(file_path: str) -> dict:
         '数据': food_rows
         '数据': food_rows
     }
     }
 
 
+    # 统一结构:北京报告特有字段(标准报告为空)
+    result['肠道微生物健康指数'] = {}
+    result['菌群多样性'] = {}
+    result['肠道黏膜屏障'] = {}
+    result['菌群表型评估'] = {}
+    result['短链脂肪酸合成能力'] = {}
+    result['肠道菌群精准分布'] = []
+    result['营养物质及营养素代谢评估'] = {}
+    result['抗生素风险评估'] = {}
+    result['毒性物质清除能力评估'] = {}
+    result['趣味肠菌评估'] = {}
+    result['胃肠道感染病原体'] = []
+    result['健康整体评估'] = {}
+    result['肠道菌群主要检测结果'] = {}
+
     return result
     return result
 
 
 
 
@@ -751,3 +772,694 @@ def _extract_food_rows(pdf_path):
                 continue
                 continue
     food_rows, food_fmt = extract_food_table(pdf_path, ref_nutrition or None)
     food_rows, food_fmt = extract_food_table(pdf_path, ref_nutrition or None)
     return food_rows, food_fmt
     return food_rows, food_fmt
+
+
+# ============================================================
+# 北京菌群报告 — 指纹检测
+# ============================================================
+
+FINGERPRINTS_BEIJING = [
+    "肠道微生物健康指数",
+    "GMHI",
+    "肠道菌群主要检测结果",
+    "核心菌属综合评分",
+    "有益菌综合评分",
+    "有害菌综合评分",
+    "肠道菌群精准分布",
+    "营养物质及营养素代谢评估",
+    "趣味肠菌评估",
+    "高通量测序",
+]
+
+
+def detect_beijing_format(text):
+    """检测是否为北京菌群报告格式"""
+    beijing_matches = sum(1 for f in FINGERPRINTS_BEIJING if f in text)
+    if beijing_matches >= 3:
+        return True
+    return False
+
+
+# ============================================================
+# 北京菌群报告 — 章节提取(PyPDF2 版)
+# ============================================================
+
+# 疾病风险关键词(北京报告特有)
+DISEASE_KEYWORDS_BJ = [
+    "溃疡性结肠炎", "克罗恩病", "腹泻型肠易激综合征",
+    "硬化性胆管炎", "非酒精性脂肪性肝病", "肝硬化",
+    "便秘", "IgA肾病", "肾结石", "格雷夫斯病",
+    "慢性淋巴细胞性甲状腺炎", "强直性脊柱炎",
+    "精神分裂症", "痛经",
+    "结直肠癌", "胃癌", "甲状腺癌", "肺癌", "乳腺癌",
+    "高血压", "冠心病", "2型糖尿病", "高脂血症",
+    "食管癌", "胆结石", "哮喘", "心脏病",
+    "膜性肾病", "帕金森病", "类风湿关节炎",
+]
+
+# 抗生素分类(北京报告格式)
+ANTIBIOTIC_CATEGORIES_BJ = [
+    "β内酰胺类", "氨基糖苷类", "大环内酯类", "四环素类",
+    "氯霉素类", "磺酰胺类", "喹诺酮类", "万古霉素类",
+    "头孢菌素类", "磷霉素类", "甲氧苄氨嘧啶类",
+    "N-乙酰基转移酶类", "O-磷酸转移酶类", "16S甲基转移酶",
+]
+
+# 核心菌属 22 种(北京报告)
+CORE_GENERA_BJ = [
+    ("阿克曼菌属", "Akkermansia"), ("另枝菌属", "Alistipes"),
+    ("拟杆菌属", "Bacteroides"), ("双歧杆菌属", "Bifidobacterium"),
+    ("布劳特氏菌属", "Blautia"), ("梭菌属", "Clostridium"),
+    ("粪球菌属", "Coprococcus"), ("戴阿利斯特杆菌属", "Dialister"),
+    ("多尔氏菌属", "Dorea"), ("真杆菌属", "Eubacterium"),
+    ("粪杆菌属", "Faecalibacterium"), ("Lachnoclostridium属", "Lachnoclostridium"),
+    ("毛螺菌属", "Lachnospira"), ("乳杆菌属", "Lactobacillus"),
+    ("巨单胞菌属", "Megamonas"), ("颤螺旋菌属", "Oscillospira"),
+    ("副拟杆菌属", "Parabacteroides"), ("考拉杆菌属", "Phascolarctobacterium"),
+    ("普雷沃氏菌属", "Prevotella"), ("罗氏菌属", "Roseburia"),
+    ("瘤胃球菌属", "Ruminococcus"), ("萨特氏菌属", "Sutterella"),
+]
+
+# 病原体列表(北京报告)
+PATHOGEN_LIST_BJ = [
+    "气单胞菌属", "蜡样芽孢杆菌", "唐菖蒲伯克霍尔德氏菌",
+    "弯曲杆菌属", "艰难梭菌", "肉毒梭菌", "产气荚膜梭菌",
+    "幽门螺杆菌", "肺炎克雷伯菌", "类志贺邻单胞菌",
+    "沙门氏菌属", "志贺氏菌属", "金黄色葡萄球菌",
+    "肺炎链球菌", "霍乱弧菌", "拟态弧菌", "副溶血弧菌",
+    "小肠结肠炎耶尔森氏菌", "假结核耶尔森氏菌",
+]
+
+
+def _find_content_after_toc(text):
+    """找到北京报告中 TOC 之后的内容起始位置"""
+    # 找"第一部分 健康整体评估"后的内容,或者"健康整体评估"正文
+    markers = ['第一部分 健康整体评估', '菌群得分总览', '您属于肠']
+    for m in markers:
+        idx = text.find(m)
+        if idx != -1:
+            return idx
+    return 0
+
+
+def _find_last_section(text, section_title):
+    """找到章节标题的最后一次出现(实际内容,非目录)"""
+    last = -1
+    start = 0
+    while True:
+        idx = text.find(section_title, start)
+        if idx == -1:
+            break
+        last = idx
+        start = idx + 1
+    return last
+
+
+def _parse_bj_gmhi(text):
+    """提取 GMHI 肠道微生物健康指数(从内容区提取)"""
+    gmhi = {}
+    # 找最后一次出现的 GMHI 值(内容区,非目录)
+    content_start = _find_content_after_toc(text)
+    content = text[content_start:]
+
+    m = re.search(r'GMHI[)\)]?[为是](\d+)', content)
+    if m: gmhi['GMHI'] = float(m.group(1))
+    m = re.search(r'肠道微生物健康指数[为](\d+)', content)
+    if m: gmhi['肠道微生物健康指数'] = float(m.group(1))
+    m = re.search(r'肠道菌群状态[为::\s]*(\S+?)[。,\n]', content)
+    if m: gmhi['肠道菌群状态'] = m.group(1)
+
+    # 健康状态推断
+    if gmhi.get('肠道微生物健康指数') or gmhi.get('GMHI'):
+        score = gmhi.get('肠道微生物健康指数') or gmhi.get('GMHI', 0)
+        if score >= 90: gmhi['健康状态'] = '健康'
+        elif score >= 80: gmhi['健康状态'] = '健康倾向'
+        elif score >= 40: gmhi['健康状态'] = '亚健康'
+        elif score >= 20: gmhi['健康状态'] = '不健康倾向'
+        else: gmhi['健康状态'] = '不健康'
+    return gmhi
+
+
+def _parse_bj_gut_type(text):
+    """提取肠型"""
+    content_start = _find_content_after_toc(text)
+    content = text[content_start:]
+    m = re.search(r'属于\s*肠[型道]\s*([IVX]+)', content)
+    if m: return f'肠型{m.group(1)}'
+    m = re.search(r'属于\s*肠[型道]\s*(\S+?)[,。,.\n]', content)
+    if m: return m.group(1).strip()
+    return None
+
+
+def _parse_bj_balance(text):
+    """提取肠道菌群平衡评估"""
+    m = re.search(r'(I{1,3}度失衡)', text)
+    if m: return m.group(1)
+    return None
+
+
+def _parse_bj_barrier(text):
+    """提取肠道黏膜屏障/BE比值"""
+    barrier = {}
+    # 在内容区找表格
+    content_start = _find_content_after_toc(text)
+    content = text[content_start:]
+    # 找 BE比值 表格行
+    m = re.search(r'BE比[值例][::\s]*(\d+\.?\d*)', text)
+    if m: barrier['BE比值'] = float(m.group(1))
+    m = re.search(r'双歧杆菌[属菌][::\s]*(\d+\.?\d*)', text)
+    if m: barrier['双歧杆菌属'] = float(m.group(1))
+    m = re.search(r'肠杆菌[科属][::\s]*(\d+\.?\d*)', text)
+    if m: barrier['肠杆菌科'] = float(m.group(1))
+    # 综合评估在"BE比值"附近找
+    be_idx = content.find('BE比')
+    if be_idx != -1:
+        chunk = content[be_idx:be_idx + 300]
+        m = re.search(r'(正常|轻度损伤|中度损伤|重度损伤|损伤)', chunk)
+        if m: barrier['综合评估'] = m.group(1)
+    return barrier
+
+
+def _parse_bj_diversity(text):
+    """提取菌群多样性"""
+    div = {}
+    content_start = _find_content_after_toc(text)
+    content = text[content_start:]
+    m = re.search(r'多样性指数[为](\d+\.?\d*)', content)
+    if m: div['Shannon指数'] = float(m.group(1))
+    m = re.search(r'高于[::\s]*(\d+)%', content)
+    if m: div['人群百分位'] = f'高于{m.group(1)}%'
+    m = re.search(r'菌种总数[::\s]*(\d+)', content)
+    if m: div['菌种总数'] = int(m.group(1))
+    return div
+
+
+def _parse_bj_phenotype(text):
+    """提取菌群表型评估"""
+    pheno = {}
+    # 找表型评估表格区域
+    last_idx = _find_last_section(text, '菌群表型评估')
+    if last_idx == -1:
+        last_idx = _find_last_section(text, '肠道菌群表型评估')
+    if last_idx == -1:
+        return pheno
+    # 取表格区域
+    region = text[last_idx:last_idx + 1500]
+    # 解析表格行: 指标名 评估值 结果
+    for label, kw in [('革兰氏阳性菌', '阳性菌'), ('革兰氏阴性菌', '阴性菌'),
+                       ('生物膜合成', '生物膜'), ('好氧菌', '好氧'),
+                       ('厌氧菌', '厌氧'), ('兼性厌氧菌', '兼性'),
+                       ('氧化胁迫耐受', '氧化'), ('致病潜力', '致病')]:
+        # 找该行: 值在指标名附近
+        idx = region.find(kw)
+        if idx == -1:
+            continue
+        chunk = region[idx:idx + 100]
+        val_m = re.search(r'(\d+\.?\d*)', chunk)
+        if val_m:
+            entry = {'评估值': float(val_m.group(1))}
+            for status in ['正常', '异常']:
+                if status in chunk:
+                    entry['结果'] = status
+                    break
+            pheno[label] = entry
+    return pheno
+
+
+def _parse_bj_scfa(text):
+    """提取短链脂肪酸合成能力"""
+    scfa = {}
+    # 找 SCFA 表格区域(最后一次出现)
+    last_idx = _find_last_section(text, '短链脂肪酸合成能力')
+    if last_idx == -1:
+        last_idx = _find_last_section(text, '短链脂肪酸')
+    if last_idx == -1:
+        return scfa
+    region = text[last_idx:last_idx + 800]
+    for acid in ['甲酸', '乙酸', '丙酸', '丁酸', '戊酸', '己酸']:
+        m = re.search(rf'{acid}[^a-zA-Z]*?(\d+)', region)
+        if m:
+            scfa[acid] = int(m.group(1))
+    return scfa
+
+
+def _parse_bj_core_bacteria(text):
+    """提取核心菌属精准分布"""
+    bacteria = []
+    for cn_name, lat_name in CORE_GENERA_BJ:
+        idx = text.find(lat_name)
+        if idx == -1: idx = text.find(cn_name)
+        if idx == -1: continue
+        chunk = text[max(0, idx - 50):idx + 200]
+        val_m = re.search(r'(\d+\.?\d*)', chunk)
+        status_m = re.search(r'(缺失|严重超标|超标|偏低|正常)', chunk)
+        pct_m = re.search(r'(\d+\.?\d*)%', chunk)
+        range_m = re.search(r'([\d.]+-[\d.]+)', chunk)
+        entry = {
+            '拉丁名': lat_name,
+            '中文名': cn_name,
+            '检测结果': float(val_m.group(1)) if val_m else None,
+            '状态': status_m.group(1) if status_m else '未知',
+            '人群百分位': float(pct_m.group(1)) if pct_m else None,
+            '参考范围': range_m.group(1) if range_m else None,
+        }
+        bacteria.append(entry)
+    return bacteria
+
+
+def _parse_bj_disease_risks(text):
+    """提取疾病风险评估"""
+    diseases = []
+    for keyword in DISEASE_KEYWORDS_BJ:
+        idx = text.find(keyword)
+        if idx == -1: continue
+        chunk = text[idx:idx + 200]
+        val_m = re.search(r'(\d+\.\d+)', chunk)
+        level_m = re.search(r'(低风险|较低风险|中度风险|较高风险|高风险)', chunk)
+        if val_m:
+            diseases.append({
+                '疾病': keyword,
+                '风险指数': float(val_m.group(1)),
+                '风险等级': level_m.group(1) if level_m else '',
+            })
+    return diseases
+
+
+def _parse_bj_nutrient_metabolism(text):
+    """提取营养物质及营养素代谢评估"""
+    metabolism = {}
+    # 主要营养物质
+    for n in ['碳水化合物', '蛋白质', '脂肪', '矿物质']:
+        m = re.search(rf'{n}[^估]*?(\d+)', text)
+        if m:
+            metabolism.setdefault('主要营养物质代谢', {})[n] = int(m.group(1))
+    # 糖类
+    for item in ['葡萄糖(糖酵解途径)', '葡萄糖(磷酸戊糖途径)', '果糖和甘露糖',
+                 '半乳糖', '淀粉和蔗糖', '氨基糖和核苷酸糖', '丙酮酸']:
+        m = re.search(rf'{item[:4]}.*?(\d+)', text)
+        if m:
+            metabolism.setdefault('糖类代谢', {})[item] = int(m.group(1))
+    # 脂类
+    for item in ['甘油酯', '甘油磷脂', '甘油三酯', '鞘脂类', '脂肪酸',
+                 '硬脂酸', '棕榈酸', '花生四烯酸', '二十二碳六烯酸',
+                 'Omega-3', 'Omega-6']:
+        m = re.search(rf'{item}.*?(\d+)', text)
+        if m:
+            metabolism.setdefault('脂类代谢', {})[item] = int(m.group(1))
+    # 维生素
+    for item in ['维生素A', '维生素B1', '维生素B2', '维生素B3', '维生素B5',
+                 '维生素B6', '维生素B7', '维生素B12', '维生素C', '维生素D',
+                 '维生素E', '维生素K1', '维生素K2', '维生素K3', '叶酸']:
+        m = re.search(rf'{item}.*?(\d+)', text)
+        if m:
+            metabolism.setdefault('维生素', {})[item] = int(m.group(1))
+    # 微量元素
+    for item in ['铁', '锌', '钙', '镁', '硒', '锰', '铜', '钴', '镍', '钼', '铬', '钒']:
+        m = re.search(rf'{item}[^估]*?(\d+)', text)
+        if m:
+            metabolism.setdefault('微量元素', {})[item] = int(m.group(1))
+    # 氨基酸
+    for item in ['赖氨酸', '丝氨酸', '亮氨酸', '色氨酸', '苯丙氨酸',
+                 '缬氨酸', '组氨酸', '半胱氨酸', '酪氨酸', '丙氨酸',
+                 '脯氨酸', '苏氨酸', '谷氨酸', '异亮氨酸', '精氨酸',
+                 '蛋氨酸', '甘氨酸', '天冬氨酸', '牛磺酸']:
+        m = re.search(rf'{item}[^估]*?(\d+)', text)
+        if m:
+            metabolism.setdefault('氨基酸', {})[item] = int(m.group(1))
+    # 神经递质
+    for item in ['5-羟色胺', 'γ-氨基丁酸', '多巴胺', '乙酰胆碱', '组胺', '去甲肾上腺素']:
+        m = re.search(rf'{item}.*?(\d+)', text)
+        if m:
+            metabolism.setdefault('神经递质', {})[item] = int(m.group(1))
+    # 其他单项
+    for pattern, key in [
+        (r'嘌呤代谢.*?(\d+)', '嘌呤代谢'),
+        (r'谷胱甘肽.*?(\d+)', '三肽(谷胱甘肽)'),
+        (r'胆汁酸.*?(\d+)', '胆汁酸代谢'),
+        (r'硫辛酸.*?(\d+)', '抗自由基(硫辛酸)'),
+        (r'辅酶Q.*?(\d+)', '抗自由基(辅酶Q)'),
+    ]:
+        m = re.search(pattern, text)
+        if m:
+            metabolism[key] = int(m.group(1))
+    return metabolism
+
+
+def _parse_bj_antibiotics(text):
+    """提取抗生素风险评估"""
+    ab = {}
+    for cat in ANTIBIOTIC_CATEGORIES_BJ:
+        m = re.search(rf'{cat}[::\s]*(\d+)', text)
+        if m:
+            ab[cat] = int(m.group(1))
+    return ab
+
+
+def _parse_bj_toxins(text):
+    """提取毒性物质清除能力评估"""
+    toxins = {}
+    toxin_items = [
+        "苯甲酸酯", "对氨基苯甲酸乙酯", "对氟苯甲酸乙酯",
+        "氯烷烃和氯烯烃", "氯代环己烷", "氯苯",
+        "甲苯", "二甲苯", "硝基甲苯", "乙苯", "苯乙烯",
+        "阿特拉津", "己内酰胺", "双酚", "二恶英",
+        "萘", "多环芳烃",
+    ]
+    for item in toxin_items:
+        m = re.search(rf'{item}.*?(\d+)', text)
+        if m:
+            toxins[item] = int(m.group(1))
+    return toxins
+
+
+def _parse_bj_pathogens(text):
+    """提取胃肠道感染病原体评估"""
+    pathogens = []
+    for p in PATHOGEN_LIST_BJ:
+        idx = text.find(p)
+        if idx == -1: continue
+        chunk = text[idx:idx + 150]
+        val_m = re.search(r'(\d+\.?\d*)', chunk)
+        status_m = re.search(r'(检出|超标|未检出|正常)', chunk)
+        if val_m:
+            entry = {'名称': p, '检测值': float(val_m.group(1)) if '.' in val_m.group(1) else int(val_m.group(1))}
+            if status_m: entry['状态'] = status_m.group(1)
+            pathogens.append(entry)
+    return pathogens
+
+
+def _parse_bj_psych_eval(text, find_table_val=None):
+    """提取趣味肠菌评估"""
+    psych = {}
+    m = re.search(r'最高概率[::\s]*(\S+)', text)
+    if m: psych['人格特征'] = {'最高概率': m.group(1)}
+    # 认知功能(从表格)
+    if find_table_val:
+        cog = find_table_val('认知能力', val_col=1)
+        if cog:
+            psych['认知功能'] = {'认知能力评分': int(cog)}
+    return psych
+
+
+def _parse_bj_scfa_from_tables(find_table_rows):
+    """从表格提取短链脂肪酸合成能力"""
+    scfa = {}
+    for acid in ['甲酸', '乙酸', '丙酸', '丁酸', '戊酸', '己酸']:
+        rows = find_table_rows(acid)
+        for name, val in rows:
+            if acid in name:
+                scfa[acid] = int(val)
+                break
+    return scfa
+
+
+def _parse_bj_core_bacteria_from_tables(all_tables):
+    """从表格提取核心菌属精准分布"""
+    bacteria = []
+    for cn_name, lat_name in CORE_GENERA_BJ:
+        for table in all_tables:
+            for row in table:
+                row_str = ' '.join(str(c) for c in row if c)
+                if lat_name in row_str or cn_name in row_str:
+                    vals = [str(c).strip() for c in row if c and str(c).strip()]
+                    entry = {'拉丁名': lat_name, '中文名': cn_name}
+                    for v in vals:
+                        v_clean = v.replace('"', '').replace('#', '').strip()
+                        # 判断是否为范围值(如 0.0041-0.0339)
+                        if '-' in v_clean and v_clean.replace('.','').replace('-',' ').replace(' ','').isdigit():
+                            entry['参考范围'] = v_clean
+                        elif v_clean == 'ND':
+                            if '检测结果' not in entry:
+                                entry['检测结果'] = 0.0
+                        elif v_clean.replace('.','').replace('-','').isdigit() and v_clean != '':
+                            val = float(v_clean)
+                            if '检测结果' not in entry:
+                                entry['检测结果'] = val
+                            elif '人群百分位' not in entry:
+                                entry['人群百分位'] = val
+                    if '检测结果' in entry:
+                        bacteria.append(entry)
+                    break
+            if bacteria and bacteria[-1].get('拉丁名') == lat_name:
+                break
+    return bacteria
+
+
+def _parse_bj_nutrient_from_tables(find_table_rows, find_table_val):
+    """从表格提取营养物质及营养素代谢评估"""
+    metabolism = {}
+    # 主要营养物质
+    for n in ['碳水化合物', '蛋白质', '脂肪', '矿物质']:
+        rows = find_table_rows(n)
+        if rows:
+            for name, val in rows:
+                if n in name:
+                    metabolism.setdefault('主要营养物质代谢', {})[n] = int(val)
+    # 糖类
+    sugar_items = ['葡萄糖(糖酵解途径)', '葡萄糖(磷酸戊糖途径)', '果糖和甘露糖',
+                   '半乳糖', '淀粉和蔗糖', '氨基糖和核苷酸糖', '丙酮酸']
+    for item in sugar_items:
+        rows = find_table_rows(item[:3])
+        if rows:
+            for name, val in rows:
+                metabolism.setdefault('糖类代谢', {})[item] = int(val)
+    # 脂类
+    for item in ['甘油酯', '甘油磷脂', '甘油三酯', '鞘脂类', '脂肪酸',
+                 '硬脂酸', '棕榈酸', '花生四烯酸', '二十二碳六烯酸',
+                 'Omega-3', 'Omega-6']:
+        rows = find_table_rows(item[:3])
+        if rows:
+            for name, val in rows:
+                metabolism.setdefault('脂类代谢', {})[item] = int(val)
+    # 维生素
+    for item in ['维生素A', '维生素B1', '维生素B2', '维生素B3', '维生素B5',
+                 '维生素B6', '维生素B7', '维生素B12', '维生素C', '维生素D',
+                 '维生素E', '维生素K1', '维生素K2', '维生素K3', '叶酸']:
+        rows = find_table_rows(item)
+        if rows:
+            for name, val in rows:
+                metabolism.setdefault('维生素', {})[item] = int(val)
+    # 微量元素
+    for item in ['铁', '锌', '钙', '镁', '硒', '锰', '铜', '钴', '镍', '钼', '铬', '钒']:
+        rows = find_table_rows(item)
+        if rows:
+            for name, val in rows:
+                if name == item:
+                    metabolism.setdefault('微量元素', {})[item] = int(val)
+    # 氨基酸
+    for item in ['赖氨酸', '丝氨酸', '亮氨酸', '色氨酸', '苯丙氨酸',
+                 '缬氨酸', '组氨酸', '半胱氨酸', '酪氨酸', '丙氨酸',
+                 '脯氨酸', '苏氨酸', '谷氨酸', '异亮氨酸', '精氨酸',
+                 '蛋氨酸', '甘氨酸', '天冬氨酸', '牛磺酸']:
+        rows = find_table_rows(item[:2])
+        if rows:
+            for name, val in rows:
+                metabolism.setdefault('氨基酸', {})[item] = int(val)
+    # 神经递质
+    for item in ['5-羟色胺', 'γ-氨基丁酸', '多巴胺', '乙酰胆碱', '组胺', '去甲肾上腺素']:
+        rows = find_table_rows(item[:3])
+        if rows:
+            for name, val in rows:
+                metabolism.setdefault('神经递质', {})[item] = int(val)
+    # 其他单项
+    for item, key in [('嘌呤代谢', '嘌呤代谢'), ('谷胱甘肽', '三肽(谷胱甘肽)'),
+                       ('胆汁酸', '胆汁酸代谢'), ('硫辛酸', '抗自由基(硫辛酸)'),
+                       ('辅酶Q', '抗自由基(辅酶Q)')]:
+        v = find_table_val(item)
+        if v: metabolism[key] = int(v)
+    return metabolism
+
+
+def _parse_bj_antibiotics_from_tables(find_table_rows):
+    """从表格提取抗生素风险评估"""
+    ab = {}
+    for cat in ANTIBIOTIC_CATEGORIES_BJ:
+        rows = find_table_rows(cat, val_col=2)
+        for name, val in rows:
+            ab[cat] = int(val)
+    return ab
+
+
+def _parse_bj_toxins_from_tables(all_tables, find_table_rows):
+    """从表格提取毒性物质清除能力评估"""
+    toxins = {}
+    toxin_items = [
+        "苯甲酸酯", "对氨基苯甲酸乙酯", "对氟苯甲酸乙酯",
+        "氯烷烃和氯烯烃", "氯代环己烷", "氯苯",
+        "甲苯", "二甲苯", "硝基甲苯", "乙苯", "苯乙烯",
+        "阿特拉津", "己内酰胺", "双酚", "二恶英",
+        "萘", "多环芳烃",
+    ]
+    for item in toxin_items:
+        rows = find_table_rows(item[:3], name_col=1, val_col=2)
+        for name, val in rows:
+            toxins[item] = int(val)
+    return toxins
+
+
+def _parse_bj_overview(text):
+    """提取北京报告概述(统一到标准报告 overview 结构)"""
+    r = {}
+    m = re.search(r'检测编号[::\s]*(\S+)', text)
+    if m: r['report_number'] = m.group(1)
+    m = re.search(r'姓\s*名[::\s]*(\S+)', text)
+    if m: r['person_name'] = m.group(1)
+    m = re.search(r'年\s*龄[::\s]*(\S+)', text)
+    if m: r['age'] = m.group(1)
+    m = re.search(r'性\s*别[::\s]*(\S+)', text)
+    if m: r['gender'] = 'male' if m.group(1) in ('男', 'M') else 'female'
+    # 肠型
+    gut_type = _parse_bj_gut_type(text)
+    if gut_type: r['gut_type'] = gut_type
+    # GMHI → 健康总分
+    m = re.search(r'肠道微生物健康指数[::\s]*(\d+)', text)
+    if m: r['overallScore'] = int(m.group(1))
+    return r
+
+
+def _parse_beijing_report(file_path: str) -> dict:
+    """解析北京菌群报告 PDF,输出统一结构(pdfplumber 表格+文本混合提取)"""
+    try:
+        import pdfplumber
+        with pdfplumber.open(file_path) as pdf:
+            text = '\n'.join(page.extract_text() or '' for page in pdf.pages)
+            # 提取所有表格
+            all_tables = []
+            for page in pdf.pages:
+                tables = page.extract_tables()
+                for table in tables:
+                    if table and len(table) >= 2:
+                        all_tables.append(table)
+    except Exception as e:
+        from PyPDF2 import PdfReader
+        reader = PdfReader(file_path)
+        text = '\n'.join((page.extract_text() or '') for page in reader.pages)
+        all_tables = []
+
+    def find_table_rows(keyword, val_col=1, name_col=0, try_all=False):
+        """从表格中找指定关键词的数据行"""
+        rows = []
+        for table in all_tables:
+            for row in table:
+                row_str = ' '.join(str(c) for c in row if c)
+                if keyword in row_str:
+                    if name_col < len(row) and val_col < len(row):
+                        name = str(row[name_col] or '').strip()
+                        val = str(row[val_col] or '').strip()
+                        if name and val and val.replace('.','').replace('-','').isdigit():
+                            rows.append((name, val))
+            if rows and not try_all:
+                break
+        return rows
+
+    def find_table_val(keyword, val_col=1, name_col=0):
+        """从表格中找单个值"""
+        for table in all_tables:
+            for row in table:
+                row_str = ' '.join(str(c) for c in row if c)
+                if keyword in row_str:
+                    if val_col < len(row):
+                        val = str(row[val_col] or '').strip()
+                        if val and val.replace('.','').isdigit():
+                            return val
+        return None
+
+    result = {
+        'format': 'beijing',
+        'overview': _parse_bj_overview(text),
+        'disease_risks': [],
+        'nutrition': [],
+        'amino_acids': [],
+        'vitamins': [],
+        'trace_elements': [],
+        '菌群检出详细列表': {},
+        '个体化食物推荐表': {'格式': 'not_found', '条目数': 0, '数据': []},
+        # 北京报告特有指标
+        '肠道微生物健康指数': _parse_bj_gmhi(text),
+        '菌群多样性': _parse_bj_diversity(text),
+        '肠道黏膜屏障': _parse_bj_barrier(text),
+        '菌群表型评估': _parse_bj_phenotype(text),
+        '短链脂肪酸合成能力': _parse_bj_scfa_from_tables(find_table_rows),
+        '肠道菌群精准分布': _parse_bj_core_bacteria_from_tables(all_tables),
+        '营养物质及营养素代谢评估': _parse_bj_nutrient_from_tables(find_table_rows, find_table_val),
+        '抗生素风险评估': _parse_bj_antibiotics_from_tables(find_table_rows),
+        '毒性物质清除能力评估': _parse_bj_toxins_from_tables(all_tables, find_table_rows),
+        '趣味肠菌评估': _parse_bj_psych_eval(text, find_table_val),
+        '胃肠道感染病原体': [],
+        '健康整体评估': {},
+        '肠道菌群主要检测结果': {},
+    }
+
+    # 从表格更新 GMHI/抗炎/免疫/纤维/多样性/屏障
+    gmhi_val = find_table_val('肠道微生物健康指数', val_col=1)
+    if gmhi_val:
+        result['肠道微生物健康指数']['肠道微生物健康指数'] = float(gmhi_val)
+        score = float(gmhi_val)
+        if score >= 90: result['肠道微生物健康指数']['健康状态'] = '健康'
+        elif score >= 80: result['肠道微生物健康指数']['健康状态'] = '健康倾向'
+        elif score >= 40: result['肠道微生物健康指数']['健康状态'] = '亚健康'
+        elif score >= 20: result['肠道微生物健康指数']['健康状态'] = '不健康倾向'
+        else: result['肠道微生物健康指数']['健康状态'] = '不健康'
+    for attr, kw in [('肠道抗炎能力', '抗炎能力'), ('肠道免疫力', '免疫力'),
+                     ('肠道膳食纤维需求', '膳食纤维需求')]:
+        v = find_table_val(kw)
+        if v:
+            result.setdefault('肠道菌群主要检测结果', {})[attr] = int(v)
+            if '抗炎' in kw: result['overview']['inflammationScore'] = int(v)
+            elif '免疫' in kw: result['overview']['immunityScore'] = int(v)
+    # 多样性
+    div_val = find_table_val('肠道微生物多样性', val_col=1)
+    if div_val:
+        result['菌群多样性']['Shannon指数'] = float(div_val)
+    # BE比值
+    be_val = find_table_val('B/E比值', val_col=1)
+    if be_val:
+        result['肠道黏膜屏障']['BE比值'] = float(be_val)
+    bi_val = find_table_val('双歧杆菌属', val_col=1)
+    if bi_val:
+        result['肠道黏膜屏障']['双歧杆菌属'] = float(bi_val)
+    ent_val = find_table_val('肠杆菌科', val_col=1)
+    if ent_val:
+        result['肠道黏膜屏障']['肠杆菌科'] = float(ent_val)
+    # 肠型
+    gut_type = _parse_bj_gut_type(text)
+    balance = _parse_bj_balance(text)
+    if gut_type or balance:
+        result['健康整体评估'] = {}
+        if gut_type: result['健康整体评估']['肠型'] = gut_type
+        if balance: result['健康整体评估']['菌群平衡评估'] = balance
+
+    # 疾病风险评估(文本提取)
+    disease_risks = _parse_bj_disease_risks(text)
+    if disease_risks:
+        result['disease_risks'] = disease_risks
+
+    # 病原体
+    pathogens = _parse_bj_pathogens(text)
+    if pathogens:
+        result['胃肠道感染病原体'] = pathogens
+
+    # 营养物质 → 标准字段映射
+    metabolism = result.get('营养物质及营养素代谢评估', {})
+    if metabolism.get('主要营养物质代谢'):
+        result['nutrition'] = [
+            {'name': k, 'value': str(v), 'status': '正常' if v >= 60 else '偏低'}
+            for k, v in metabolism['主要营养物质代谢'].items()
+        ]
+    if metabolism.get('氨基酸'):
+        result['amino_acids'] = [
+            {'name': k, 'value': str(v), 'status': '正常' if v >= 60 else '偏低'}
+            for k, v in metabolism['氨基酸'].items()
+        ]
+    if metabolism.get('维生素'):
+        result['vitamins'] = [
+            {'name': k, 'value': str(v), 'status': '正常' if v >= 60 else '偏低'}
+            for k, v in metabolism['维生素'].items()
+        ]
+    if metabolism.get('微量元素'):
+        result['trace_elements'] = [
+            {'name': k, 'value': str(v), 'status': '正常' if v >= 60 else '偏低'}
+            for k, v in metabolism['微量元素'].items()
+        ]
+
+    return result