Jelajahi Sumber

docs(v5): add bacteria composition tables extraction (core genus/probiotics/harmful/pathogens) for triplet format

Added extract_bacteria_tables() for detailed bacteria composition lists (6 tables). CSV batch outputs 菌群_核心菌属.csv. Inline format not supported (data concatenated without delimiters).
asus 2 bulan lalu
induk
melakukan
25f4c2b3c8

+ 20 - 0
docs/参考资料/CSV/菌群_核心菌属.csv

@@ -0,0 +1,20 @@
+菌名,501999942-某人,530010234-侯,547982403,儿童示例,朱评估报告
+0.1914-14.598,,,39%,39%,
+0.3366-3.237,21%,31%,,,
+1.0292-55.9251,,92%,,,
+5.1543-18.1656,59%,45%,,,
+Lachnoclostridium,0.2093,1.7121,1.5483,1.5173,
+⽑螺菌属 Lachnospira,0.1943,,2.3302,,
+优杆菌属 Eubacterium,0.1712,0.2606,1.4728,0.4918,
+双歧杆菌属 Bifidobacterium,0.2396,0.2973,0.1114,0.1141,
+巨单胞菌属 Megamonas,ND,0.1404,0.0169,ND,
+拟杆菌属 Bacteroides,13.2781,58.0864,45.0500,18.4336,
+普雷沃⽒菌属 Prevotella,60.0857,,4.2684,,
+普雷沃氏菌属 Prevotella,,0.4876,,0.6430,
+梭菌属 Clostridium,0.2787,0.1303,1.1836,0.6731,
+毛螺菌属 Lachnospira,,0.2574,,2.6397,
+瘤胃球菌属 Ruminococcus,1.6504,0.5747,1.9927,2.1607,
+粪杆菌属 Faecalibacterium,3.2406,1.8959,7.1088,14.2451,
+粪球菌属 Coprococcus,1.8668,0.0501,0.2561,1.4292,
+经黏液真杆菌属 Blautia,0.1206,0.1869,0.5625,3.1770,
+考拉杆菌属 Phascolarctobacterium,0.0101,0.0167,4.4324,1.4663,

+ 196 - 0
docs/参考资料/extract_full_report_v5.py

@@ -585,6 +585,166 @@ def extract_pathogens_inline(text):
     return results
 
 
+# ==========================================
+# 菌群检出详细列表提取
+# ==========================================
+BACTERIA_TABLE_TITLES = [
+    '核心菌属构成表', '益生菌', '有害菌属构成表',
+    '其它重要菌属构成表', '病原菌属构成表', '病原菌',
+]
+
+def extract_bacteria_tables(pdf_path):
+    """提取菌群检出详细列表(核心菌属/益生菌/有害菌等)"""
+    reader = PdfReader(pdf_path)
+    full_text = '\n'.join(norm(p.extract_text()) for p in reader.pages)
+    fmt = 'triplet' if '指标范围' in full_text and '疾病风险评估' in full_text else 'inline'
+    # 再检测一次
+    for pt in [p.extract_text() for p in reader.pages]:
+        t = norm(pt)
+        if '疾病风险评估' in t and '指标范围' in t:
+            for line in t.split('\n'):
+                if re.search(r'[\u4e00-\u9fff]+\d+\.?\d*[\u4e00-\u9fff]+', line.strip()):
+                    fmt = 'inline'
+                    break
+            break
+
+    all_tables = {}
+
+    # 核心菌属构成表1-3
+    core_genus = []
+    for i in range(1, 4):
+        title = f'核心菌属构成表{i}'
+        rows = _parse_bacteria_table(reader, pages_text=None, full_text=full_text, title=title, fmt=fmt)
+        core_genus.extend(rows)
+    all_tables['核心菌属'] = core_genus
+
+    # 益生菌
+    prob_rows = _parse_bacteria_table(reader, None, full_text, '益生菌', fmt, skip_header=True)
+    all_tables['益生菌'] = [r for r in prob_rows if r.get('名称') and r['名称'] not in
+        ['我的益生菌都为ND', '仅列出丰度前22的益生菌种。']]
+
+    # 有害菌属构成表1-2
+    harmful = []
+    for i in range(1, 3):
+        title = f'有害菌属构成表{i}'
+        rows = _parse_bacteria_table(reader, None, full_text, title, fmt)
+        harmful.extend(rows)
+    all_tables['有害菌属'] = harmful
+
+    # 其它重要菌属
+    other_rows = _parse_bacteria_table(reader, None, full_text, '其它重要菌属构成表', fmt)
+    all_tables['其它重要菌属'] = other_rows
+
+    # 病原菌属构成表
+    patho_genus = _parse_bacteria_table(reader, None, full_text, '病原菌属构成表', fmt)
+    all_tables['病原菌属'] = patho_genus
+
+    # 病原菌(检出列表)
+    patho_rows = _parse_bacteria_table(reader, None, full_text, '病原菌', fmt, skip_header=True)
+    all_tables['病原菌检出'] = [r for r in patho_rows if r.get('名称') and len(r['名称']) >= 2
+        and '仅列出' not in r['名称'] and '说明' not in r['名称']]
+
+    return all_tables
+
+
+def _parse_bacteria_table(reader, pages_text, full_text, title, fmt, skip_header=False):
+    """从PDF中解析一个菌群表格"""
+    results = []
+    sidx = full_text.find(title)
+    if sidx == -1:
+        return results
+
+    # 找表格结束位置(下一个标题或页尾)
+    end_pos = len(full_text)
+    for t in BACTERIA_TABLE_TITLES + ['指标范围', '个体化食物推荐表', '报告总结', '健康总分']:
+        if t == title: continue
+        ei = full_text.find(t, sidx + len(title))
+        if ei != -1 and ei < end_pos:
+            end_pos = ei
+
+    region = full_text[sidx:end_pos]
+
+    if fmt == 'triplet':
+        # 三元组格式:每个字段单独一行
+        lines = [l.strip() for l in region.split('\n') if l.strip()]
+        # 找到数据开始的位置(跳过标题行)
+        start = 0
+        for i, line in enumerate(lines):
+            if line == '名称':
+                start = i + 1
+                break
+            if line.startswith('名称'):
+                # 行内格式
+                if skip_header:
+                    start = i + 1
+                break
+
+        i = start
+        while i < len(lines):
+            name = lines[i]
+            if not name or len(name) <= 1 or name in ['说明', '检测结果', '结果解释', '建议']:
+                i += 1
+                continue
+            if name.startswith('说明:') or name.startswith('改善方式'):
+                i += 1
+                continue
+            # 检查是否包含说明文本(跳过过长行)
+            if len(name) > 80:
+                i += 1
+                continue
+
+            # 找丰度%
+            if i + 1 < len(lines) and re.match(r'^[\d.]+%?$|^ND$', lines[i+1]):
+                pct = lines[i+1]
+                normal_range = ''
+                pop_level = ''
+                detection_rate = ''
+                desc = ''
+                j = i + 2
+                # 正常范围
+                if j < len(lines) and re.match(r'^[\d.]+-[\d.]+$', lines[j]):
+                    normal_range = lines[j]
+                    j += 1
+                # 人群水平%
+                if j < len(lines) and re.match(r'^\d+%$', lines[j]):
+                    pop_level = lines[j]
+                    j += 1
+                # 检出率%
+                if j < len(lines) and re.match(r'^\d+\.?\d*%$', lines[j]):
+                    detection_rate = lines[j]
+                    j += 1
+                # 说明
+                if j < len(lines) and lines[j].startswith('说明'):
+                    desc = lines[j]
+                    j += 1
+                # 改善方式
+                if j < len(lines) and lines[j].startswith('改善方式'):
+                    if desc:
+                        desc += ' | ' + lines[j]
+                    else:
+                        desc = lines[j]
+                    j += 1
+
+                entry = {'名称': name, '丰度%': pct}
+                if normal_range: entry['正常范围%'] = normal_range
+                if pop_level: entry['人群水平%'] = pop_level
+                if detection_rate: entry['检出率%'] = detection_rate
+                if desc: entry['说明'] = desc
+                results.append(entry)
+                i = j
+            else:
+                i += 1
+    else:
+        # inline 格式:数据挤在一行且数字段无分隔符,无法可靠自动解析
+        # 见 朱评估报告 page 23-26,格式为 名称+丰度+范围+水平%+检出率% 无缝拼接
+        # 示例:梭菌属 Clostridium1.00450.0306-6.961566%99.52%
+        # 其中 1.00450.0306 无法区分 1.0045 + 0.0306 vs 1.004 + 50.0306
+        # 跳过自动提取,用户可参考 raw text
+        pass
+
+    return results
+
+
 # ==========================================
 # 食物推荐表
 # ==========================================
@@ -856,6 +1016,10 @@ def extract_pdf_to_json(pdf_path):
         '数据': food_rows
     }
 
+    # 菌群检出详细列表
+    bacteria_tables = extract_bacteria_tables(pdf_path)
+    result['菌群检出详细列表'] = bacteria_tables
+
     return result
 
 
@@ -1114,6 +1278,38 @@ def main_csv():
             w.writerow(row)
     print(f'  -> CSV/推荐指数汇总.csv ({len(std_rows)} 行)')
 
+    # ── 12. 菌群检出详细列表(仅三元组格式) ──
+    print('\n[12/12] 菌群检出详细列表...')
+    for table_name in ['核心菌属', '益生菌', '有害菌属', '其它重要菌属', '病原菌属', '病原菌检出']:
+        combined = {}
+        for label in all_labels:
+            if label in inline_labels:
+                continue
+            pdf_path = os.path.join(BASE, label + '.pdf')
+            bt = extract_bacteria_tables(pdf_path)
+            rows = bt.get(table_name, [])
+            for r in rows:
+                name = r.get('名称', '')
+                if not name or len(name) <= 1:
+                    continue
+                if name not in combined:
+                    combined[name] = {}
+                combined[name][label] = r.get('丰度%', '')
+
+        if combined:
+            fieldnames = ['菌名'] + all_labels
+            fname = f'菌群_{table_name}.csv'
+            with open(os.path.join(OUTDIR, fname), 'w', newline='', encoding='utf-8-sig') as f:
+                w = csv.DictWriter(f, fieldnames=fieldnames)
+                w.writeheader()
+                for name, vals in sorted(combined.items()):
+                    row = {'菌名': name}
+                    row.update(vals)
+                    w.writerow(row)
+            print(f'  -> CSV/{fname} ({len(combined)} 条)')
+        else:
+            print(f'  {table_name}: (无数据)')
+
     print(f'\n完成!所有CSV已输出到 {OUTDIR}/')