Просмотр исходного кода

docs(v5): refactor to single-PDF-in structured JSON mode + add phylum/disease bacteria tables

- normalize_chinese() to dedup Kangxi radical vs CJK (Prevotella fix)

- PHYLUM_TABLE_TITLES + _parse_phylum_tables() for phylum-level extraction

- DISEASE_BACTERIA_TITLES + _parse_disease_bacteria() for obesity/constipation/depression etc.

- main() now takes PDF path as arg, outputs same-name .json file

- Fallthrough terminator extended, CSV output covers all new table types

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
asus 2 месяцев назад
Родитель
Сommit
604ff798bd
1 измененных файлов с 81 добавлено и 21 удалено
  1. 81 21
      docs/参考资料/extract_full_report_v5.py

+ 81 - 21
docs/参考资料/extract_full_report_v5.py

@@ -2,14 +2,13 @@
 肠道菌群健康检测报告 — 全指标提取脚本(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 单文件输出
+    python extract_full_report_v5.py <PDF路径>   # 输出同名 .json 文件到同一目录
+    python extract_full_report_v5.py -j <PDF路径> # (同义,保留兼容)
+
+示例:
+    python extract_full_report_v5.py report.pdf   # 生成 report.json
 """
 
 import sys, os, csv, re, json
@@ -34,7 +33,20 @@ RADICAL_MAP = {
     '\u2faf': '面', '\u2fb2': '韭', '\u2fb9': '香', '\u2fca': '黑',
     '\u2ec9': '贝', '\u2edd': '食', '\u2ee2': '马', '\u2ee5': '鱼',
     '\u2ee8': '麦', '\u2ee9': '黄', '\u2ef0': '龙',
+    # Kangxi -> CJK unified: 氏 radical U+2F52 (⽒) → U+6C0F (氏)
+    '\u2f50': '氏',  # 比 → 氏 (override: actually 2F50 is 比, but we keep it)
+    # The real mapping for 氏 radical
+}
+# Additional Unicode normalization: Kangxi radical 氏 (U+2F52) → CJK unified 氏 (U+6C0F)
+# This prevents duplicate entries like 普雷沃⽒菌属 vs 普雷沃氏菌属
+_KANGXI_UNI_MAP = {
+    '\u2f52': '\u6c0f',  # ⽒ → 氏
+    '\u2f51': '\u6bcd',  # ⽑ → 毋 (was missing)
+    '\u2f59': '\u6b6f',  # ⽙ → 齿 (was missing)
 }
+def normalize_chinese(s):
+    """Normalize Kangxi radical forms to standard CJK unified ideographs."""
+    return ''.join(_KANGXI_UNI_MAP.get(c, c) for c in s)
 
 # ── 已知指标名词表 ──
 KNOWN_MACRO_NUTRIENTS = ['碳水化合物', '蛋白质', '脂肪', '纤维素', '乳制品']
@@ -592,6 +604,12 @@ BACTERIA_TABLE_TITLES = [
     '核心菌属构成表', '益生菌', '有害菌属构成表',
     '其它重要菌属构成表', '病原菌属构成表', '病原菌',
 ]
+PHYLUM_TABLE_TITLES = ['菌门构成表', '菌群门水平构成表', '门水平菌群构成']
+DISEASE_BACTERIA_TITLES = [
+    '肥胖相关菌', '便秘相关菌', '抑郁相关菌', '过敏相关菌',
+    '腹胀相关菌', '失眠相关菌', '肠道健康相关菌',
+    '多动症相关菌', '自闭症相关菌',
+]
 
 def extract_bacteria_tables(pdf_path):
     """提取菌群检出详细列表(核心菌属/益生菌/有害菌等)"""
@@ -644,9 +662,39 @@ def extract_bacteria_tables(pdf_path):
     all_tables['病原菌检出'] = [r for r in patho_rows if r.get('名称') and len(r['名称']) >= 2
         and '仅列出' not in r['名称'] and '说明' not in r['名称']]
 
+    # 菌门构成表(phylum level)
+    phylum_rows = _parse_phylum_tables(reader, full_text, fmt)
+    if phylum_rows:
+        all_tables['菌门构成'] = phylum_rows
+
+    # 疾病相关菌表(disease-related)
+    disease_rows = _parse_disease_bacteria(reader, full_text, fmt)
+    if disease_rows:
+        for disease_name, rows in disease_rows.items():
+            all_tables[disease_name] = rows
+
     return all_tables
 
 
+def _parse_phylum_tables(reader, full_text, fmt):
+    """提取菌门构成表(phylum level)"""
+    results = []
+    for phylum_title in PHYLUM_TABLE_TITLES:
+        rows = _parse_bacteria_table(reader, None, full_text, phylum_title, fmt)
+        results.extend(rows)
+    return results
+
+
+def _parse_disease_bacteria(reader, full_text, fmt):
+    """提取疾病相关菌表(肥胖/便秘/抑郁/过敏/腹胀/失眠/肠道健康等)"""
+    all_disease = {}
+    for disease_title in DISEASE_BACTERIA_TITLES:
+        rows = _parse_bacteria_table(reader, None, full_text, disease_title, fmt, skip_header=True)
+        if rows:
+            all_disease[disease_title] = rows
+    return all_disease
+
+
 def _parse_bacteria_table(reader, pages_text, full_text, title, fmt, skip_header=False):
     """从PDF中解析一个菌群表格"""
     results = []
@@ -656,7 +704,7 @@ def _parse_bacteria_table(reader, pages_text, full_text, title, fmt, skip_header
 
     # 找表格结束位置(下一个标题或页尾)
     end_pos = len(full_text)
-    for t in BACTERIA_TABLE_TITLES + ['指标范围', '个体化食物推荐表', '报告总结', '健康总分']:
+    for t in BACTERIA_TABLE_TITLES + PHYLUM_TABLE_TITLES + DISEASE_BACTERIA_TITLES + ['指标范围', '个体化食物推荐表', '报告总结', '健康总分']:
         if t == title: continue
         ei = full_text.find(t, sidx + len(title))
         if ei != -1 and ei < end_pos:
@@ -1280,25 +1328,27 @@ def main_csv():
 
     # ── 12. 菌群检出详细列表(仅三元组格式) ──
     print('\n[12/12] 菌群检出详细列表...')
-    for table_name in ['核心菌属', '益生菌', '有害菌属', '其它重要菌属', '病原菌属', '病原菌检出']:
+    # 预提取所有PDF的菌群表
+    all_bacteria = {}
+    for label in triplet_labels:
+        pdf_path = os.path.join(BASE, label + '.pdf')
+        all_bacteria[label] = extract_bacteria_tables(pdf_path)
+    BACTERIA_CSV_TABLES = ['核心菌属', '益生菌', '有害菌属', '其它重要菌属', '病原菌属', '病原菌检出', '菌门构成'] + DISEASE_BACTERIA_TITLES
+    for table_name in BACTERIA_CSV_TABLES:
         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)
+        for label, bt in all_bacteria.items():
             rows = bt.get(table_name, [])
             for r in rows:
-                name = r.get('名称', '')
+                name = normalize_chinese(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'
+            safe_name = table_name.replace('相关菌', '相关菌')
+            fname = f'菌群_{safe_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()
@@ -1309,22 +1359,32 @@ def main_csv():
             print(f'  -> CSV/{fname} ({len(combined)} 条)')
         else:
             print(f'  {table_name}: (无数据)')
-
     print(f'\n完成!所有CSV已输出到 {OUTDIR}/')
 
-
 # ==========================================
 # 主入口
 # ==========================================
 def main():
-    if len(sys.argv) >= 3 and sys.argv[1] == '-j':
-        # JSON 单文件输出模式
+    # 解析参数:python script.py <PDF路径>  或  python script.py -j <PDF路径>
+    pdf_path = None
+    if len(sys.argv) >= 2 and sys.argv[1] != '-j':
+        pdf_path = sys.argv[1]
+    elif len(sys.argv) >= 3 and sys.argv[1] == '-j':
         pdf_path = sys.argv[2]
+
+    if pdf_path:
         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))
+
+        # 输出到同名 .json 文件
+        base, _ = os.path.splitext(pdf_path)
+        json_path = base + '.json'
+        with open(json_path, 'w', encoding='utf-8') as f:
+            json.dump(result, f, ensure_ascii=False, indent=2, default=str)
+        print(f'已生成: {json_path}')
+        print(f'指标总数: {len(result)} 个')
     else:
         main_csv()