|
@@ -585,6 +585,166 @@ def extract_pathogens_inline(text):
|
|
|
return results
|
|
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
|
|
'数据': food_rows
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ # 菌群检出详细列表
|
|
|
|
|
+ bacteria_tables = extract_bacteria_tables(pdf_path)
|
|
|
|
|
+ result['菌群检出详细列表'] = bacteria_tables
|
|
|
|
|
+
|
|
|
return result
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1114,6 +1278,38 @@ def main_csv():
|
|
|
w.writerow(row)
|
|
w.writerow(row)
|
|
|
print(f' -> CSV/推荐指数汇总.csv ({len(std_rows)} 行)')
|
|
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}/')
|
|
print(f'\n完成!所有CSV已输出到 {OUTDIR}/')
|
|
|
|
|
|
|
|
|
|
|