|
@@ -0,0 +1,574 @@
|
|
|
|
|
+package com.etotem.cfc.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.dto.ParsedDiseaseRisk;
|
|
|
|
|
+import com.etotem.cfc.dto.ParsedGutFlora;
|
|
|
|
|
+import com.etotem.cfc.dto.ParsedIndicator;
|
|
|
|
|
+import com.etotem.cfc.dto.ParsedReportResult;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.apache.pdfbox.pdmodel.PDDocument;
|
|
|
|
|
+import org.apache.pdfbox.text.PDFTextStripper;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.io.InputStream;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.regex.Matcher;
|
|
|
|
|
+import java.util.regex.Pattern;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 募极生物 (Muji Bio) 肠道菌群报告 PDF 解析服务
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+public class PdfParseService {
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析 PDF 输入流,返回结构化的报告数据
|
|
|
|
|
+ */
|
|
|
|
|
+ public ParsedReportResult parse(InputStream inputStream) throws IOException {
|
|
|
|
|
+ try (PDDocument document = PDDocument.load(inputStream)) {
|
|
|
|
|
+ PDFTextStripper stripper = new PDFTextStripper();
|
|
|
|
|
+ stripper.setSortByPosition(true);
|
|
|
|
|
+ String fullText = stripper.getText(document);
|
|
|
|
|
+ return parseText(fullText);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从纯文本中解析报告数据
|
|
|
|
|
+ */
|
|
|
|
|
+ public ParsedReportResult parseText(String fullText) {
|
|
|
|
|
+ ParsedReportResult result = new ParsedReportResult();
|
|
|
|
|
+
|
|
|
|
|
+ // 按行分割
|
|
|
|
|
+ String[] lines = fullText.split("\\r?\\n");
|
|
|
|
|
+ List<String> lineList = new ArrayList<>();
|
|
|
|
|
+ for (String line : lines) {
|
|
|
|
|
+ String trimmed = line.trim();
|
|
|
|
|
+ if (!trimmed.isEmpty()) {
|
|
|
|
|
+ // 去除PDF提取中的特殊Unicode箭头字符(如 )
|
|
|
|
|
+ trimmed = trimmed.replaceAll("[\\uF000-\\uFFFF]", "").trim();
|
|
|
|
|
+ if (!trimmed.isEmpty()) {
|
|
|
|
|
+ lineList.add(trimmed);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析各部分
|
|
|
|
|
+ List<ParsedDiseaseRisk> diseaseRisks = new ArrayList<>();
|
|
|
|
|
+ List<ParsedGutFlora> gutFlora = new ArrayList<>();
|
|
|
|
|
+ List<ParsedGutFlora> probioticSpecies = new ArrayList<>();
|
|
|
|
|
+ List<ParsedIndicator> indicators = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 收集summary页面中的所有数据
|
|
|
|
|
+ parseSummary(lineList, result);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析疾病风险评估
|
|
|
|
|
+ parseDiseaseRisks(lineList, diseaseRisks);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析各类指标(营养、氨基酸、维生素、微量元素)
|
|
|
|
|
+ parseIndicatorSection(lineList, "主要营养评估", indicators);
|
|
|
|
|
+ parseIndicatorSection(lineList, "氨基酸评估", indicators);
|
|
|
|
|
+ parseIndicatorSection(lineList, "维生素评估", indicators);
|
|
|
|
|
+ parseIndicatorSection(lineList, "微量元素评估", indicators);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析需要"正常范围"列的区域(肠道屏障、短链脂肪酸、神经递质)
|
|
|
|
|
+ parseIndicatorWithRange(lineList, "肠道屏障及菌群代谢物", "肠道屏障功能", indicators);
|
|
|
|
|
+ parseIndicatorWithRange(lineList, "短链脂肪酸", "短链脂肪酸", indicators);
|
|
|
|
|
+ parseIndicatorWithRange(lineList, "神经递质及激素指标", "神经递质与激素", indicators);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析抗生素风险评估(3行一组)
|
|
|
|
|
+ parseSimpleIndicator(lineList, "抗生素风险评估", "抗生素耐药", indicators);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析核心菌属构成表
|
|
|
|
|
+ parseGutFloraTables(lineList, "核心菌属构成表", "核心菌属", gutFlora);
|
|
|
|
|
+ // 解析有害菌属构成表
|
|
|
|
|
+ parseGutFloraTables(lineList, "有害菌属构成表", "有害菌", gutFlora);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析益生菌
|
|
|
|
|
+ parseProbioticSection(lineList, probioticSpecies);
|
|
|
|
|
+
|
|
|
|
|
+ result.setDiseaseRisks(diseaseRisks);
|
|
|
|
|
+ result.setGutFlora(gutFlora);
|
|
|
|
|
+ result.setProbioticSpecies(probioticSpecies);
|
|
|
|
|
+ result.setIndicators(indicators);
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ======================== 解析页面1:汇总 ========================
|
|
|
|
|
+
|
|
|
|
|
+ private void parseSummary(List<String> lines, ParsedReportResult result) {
|
|
|
|
|
+ // 健康总分
|
|
|
|
|
+ Integer overallScore = findScoreAfter(lines, "健康总分");
|
|
|
|
|
+ if (overallScore != null) result.setOverallScore(overallScore);
|
|
|
|
|
+
|
|
|
|
|
+ // 菌群健康
|
|
|
|
|
+ Integer gutHealth = findScoreAfter(lines, "菌群健康");
|
|
|
|
|
+ if (gutHealth != null) result.setGutHealthScore(gutHealth);
|
|
|
|
|
+
|
|
|
|
|
+ // 慢病控制
|
|
|
|
|
+ Integer chronic = findScoreAfter(lines, "慢病控制");
|
|
|
|
|
+ if (chronic != null) result.setChronicDiseaseScore(chronic);
|
|
|
|
|
+
|
|
|
|
|
+ // 营养均衡
|
|
|
|
|
+ Integer nutrition = findScoreAfter(lines, "营养均衡");
|
|
|
|
|
+ if (nutrition != null) result.setNutritionScore(nutrition);
|
|
|
|
|
+
|
|
|
|
|
+ // 肠道菌群平衡
|
|
|
|
|
+ Integer balance = findScoreAfter(lines, "肠道菌群平衡");
|
|
|
|
|
+ if (balance != null) result.setBalanceScore(balance);
|
|
|
|
|
+
|
|
|
|
|
+ // 菌群多样性
|
|
|
|
|
+ Integer diversity = findScoreAfter(lines, "菌群多样性");
|
|
|
|
|
+ if (diversity != null) result.setDiversityScore(diversity);
|
|
|
|
|
+
|
|
|
|
|
+ // 有益菌 — 只取数字评分(跳过后面章节中的文字)
|
|
|
|
|
+ Integer beneficial = findScoreAfter(lines, "有益菌");
|
|
|
|
|
+ if (beneficial != null) result.setBeneficialScore(beneficial);
|
|
|
|
|
+
|
|
|
|
|
+ // 有害菌
|
|
|
|
|
+ Integer harmful = findScoreAfter(lines, "有害菌");
|
|
|
|
|
+ if (harmful != null) result.setHarmfulScore(harmful);
|
|
|
|
|
+
|
|
|
|
|
+ // 核心菌属
|
|
|
|
|
+ Integer coreGenus = findScoreAfter(lines, "核心菌属");
|
|
|
|
|
+ if (coreGenus != null) result.setCoreGenusScore(coreGenus);
|
|
|
|
|
+
|
|
|
|
|
+ // 肠道预测年龄
|
|
|
|
|
+ result.setGutAge(findTextAfter(lines, "肠道预测年龄:"));
|
|
|
|
|
+
|
|
|
|
|
+ // 肠型
|
|
|
|
|
+ result.setGutType(findTextAfter(lines, "肠型:"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 在lines中查找关键词,取下一行作为数字评分
|
|
|
|
|
+ */
|
|
|
|
|
+ private Integer findScoreAfter(List<String> lines, String keyword) {
|
|
|
|
|
+ for (int i = 0; i < lines.size() - 1; i++) {
|
|
|
|
|
+ if (lines.get(i).equals(keyword)) {
|
|
|
|
|
+ String nextLine = lines.get(i + 1).replaceAll("[^0-9.-]", "").trim();
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 只取纯数字部分
|
|
|
|
|
+ String numStr = nextLine.replaceAll("[^\\d]", "").trim();
|
|
|
|
|
+ if (!numStr.isEmpty()) {
|
|
|
|
|
+ return Integer.parseInt(numStr);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ // 忽略
|
|
|
|
|
+ }
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 在lines中查找关键词,取下一行文本
|
|
|
|
|
+ */
|
|
|
|
|
+ private String findTextAfter(List<String> lines, String keyword) {
|
|
|
|
|
+ for (int i = 0; i < lines.size() - 1; i++) {
|
|
|
|
|
+ if (lines.get(i).equals(keyword)) {
|
|
|
|
|
+ return lines.get(i + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ======================== 疾病风险评估 ========================
|
|
|
|
|
+
|
|
|
|
|
+ private void parseDiseaseRisks(List<String> lines, List<ParsedDiseaseRisk> result) {
|
|
|
|
|
+ int startIdx = -1;
|
|
|
|
|
+ for (int i = 0; i < lines.size(); i++) {
|
|
|
|
|
+ String line = lines.get(i);
|
|
|
|
|
+ if (line.contains("疾病风险评估") && !line.contains("注:")) {
|
|
|
|
|
+ startIdx = i + 1;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (startIdx < 0) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 从 startIdx 开始,每3行一组: name, value, level
|
|
|
|
|
+ for (int i = startIdx; i < lines.size() - 2; i += 3) {
|
|
|
|
|
+ String name = lines.get(i);
|
|
|
|
|
+ String value = lines.get(i + 1);
|
|
|
|
|
+ String level = lines.get(i + 2);
|
|
|
|
|
+
|
|
|
|
|
+ // 跳过非疾病风险行
|
|
|
|
|
+ if (name.contains("指标范围") || name.contains("注:") || name.contains("本检测")
|
|
|
|
|
+ || name.contains("健康评估") || name.contains("疾病认定")) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 验证value是数字格式
|
|
|
|
|
+ if (isNumeric(value)) {
|
|
|
|
|
+ ParsedDiseaseRisk risk = new ParsedDiseaseRisk();
|
|
|
|
|
+ risk.setDiseaseName(name);
|
|
|
|
|
+ risk.setRiskValue(value);
|
|
|
|
|
+ risk.setRiskLevel(level);
|
|
|
|
|
+ result.add(risk);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果不是数字,可能是下一页内容,停止
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ======================== 指标解析(3行一组) ========================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析 名称 / 数值 / 状态 三行一组的指标区域
|
|
|
|
|
+ */
|
|
|
|
|
+ private void parseSimpleIndicator(List<String> lines, String sectionHeader, String category,
|
|
|
|
|
+ List<ParsedIndicator> result) {
|
|
|
|
|
+ int startIdx = findSectionStart(lines, sectionHeader);
|
|
|
|
|
+ if (startIdx < 0) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 跳过表头行
|
|
|
|
|
+ int dataStart = startIdx;
|
|
|
|
|
+ // 跳过可能的空行和表头
|
|
|
|
|
+ while (dataStart < lines.size() && !isNumeric(lines.get(dataStart))) {
|
|
|
|
|
+ String line = lines.get(dataStart);
|
|
|
|
|
+ // 如果看起来像表头或者section标题,跳过
|
|
|
|
|
+ dataStart++;
|
|
|
|
|
+ }
|
|
|
|
|
+ dataStart--; // 回到可能的数据开始位置
|
|
|
|
|
+ if (dataStart < startIdx) dataStart = startIdx;
|
|
|
|
|
+
|
|
|
|
|
+ // 从 dataStart 开始扫描,每3行一组: name, value, status
|
|
|
|
|
+ for (int i = dataStart; i < lines.size() - 2; i++) {
|
|
|
|
|
+ String name = lines.get(i);
|
|
|
|
|
+ // 跳过明显的非数据行
|
|
|
|
|
+ if (name.contains("指标范围") || name.contains("抗生素风险评估") || name.contains("超出范围")) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ String valueLine = lines.get(i + 1);
|
|
|
|
|
+ String statusLine = lines.get(i + 2);
|
|
|
|
|
+
|
|
|
|
|
+ // 验证第二行是数字
|
|
|
|
|
+ if (isNumeric(valueLine) && !isNumeric(name)) {
|
|
|
|
|
+ ParsedIndicator ind = new ParsedIndicator();
|
|
|
|
|
+ ind.setCategory(category);
|
|
|
|
|
+ ind.setIndicatorName(name);
|
|
|
|
|
+ ind.setIndicatorValue(valueLine);
|
|
|
|
|
+ ind.setStatus(statusLine);
|
|
|
|
|
+ result.add(ind);
|
|
|
|
|
+ i += 2; // 跳过已处理的行
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ======================== 指标解析(名称 / value status / range / desc) ========================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析带参考范围的指标区域(肠道屏障、短链脂肪酸、神经递质等)
|
|
|
|
|
+ */
|
|
|
|
|
+ private void parseIndicatorWithRange(List<String> lines, String sectionHeader, String category,
|
|
|
|
|
+ List<ParsedIndicator> result) {
|
|
|
|
|
+ int startIdx = findSectionStart(lines, sectionHeader);
|
|
|
|
|
+ if (startIdx < 0) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 跳过表头行
|
|
|
|
|
+ int idx = startIdx;
|
|
|
|
|
+ while (idx < lines.size()) {
|
|
|
|
|
+ String line = lines.get(idx);
|
|
|
|
|
+ // 跳过表头:名称、评估值、正常范围等
|
|
|
|
|
+ if (line.contains("名称") || line.contains("评估值") || line.contains("正常范围")
|
|
|
|
|
+ || line.contains("过量") || line.contains("缺乏")) {
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 数据行:名称
|
|
|
|
|
+ String name = line;
|
|
|
|
|
+ if (name.contains("指标范围") || name.contains(sectionHeader)) {
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (idx + 1 >= lines.size()) break;
|
|
|
|
|
+
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ String valueStatus = lines.get(idx); // "16 正常" 或 "14 不足"
|
|
|
|
|
+
|
|
|
|
|
+ // 解析value和status
|
|
|
|
|
+ String value = valueStatus;
|
|
|
|
|
+ String status = "";
|
|
|
|
|
+ // 尝试从 valueStatus 中分离出数字和文本
|
|
|
|
|
+ String[] parts = valueStatus.split("\\s+");
|
|
|
|
|
+ if (parts.length >= 2 && isNumeric(parts[0])) {
|
|
|
|
|
+ value = parts[0];
|
|
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
|
|
+ for (int j = 1; j < parts.length; j++) {
|
|
|
|
|
+ if (sb.length() > 0) sb.append(" ");
|
|
|
|
|
+ sb.append(parts[j]);
|
|
|
|
|
+ }
|
|
|
|
|
+ status = sb.toString();
|
|
|
|
|
+ } else if (parts.length >= 1 && isNumeric(parts[0])) {
|
|
|
|
|
+ value = parts[0];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 下一行是参考范围
|
|
|
|
|
+ if (idx + 1 < lines.size()) {
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ String range = lines.get(idx);
|
|
|
|
|
+
|
|
|
|
|
+ // 下一行是描述/说明(跳过)
|
|
|
|
|
+ if (idx + 1 < lines.size()) {
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ String desc = lines.get(idx); // 只是跳过
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ParsedIndicator ind = new ParsedIndicator();
|
|
|
|
|
+ ind.setCategory(category);
|
|
|
|
|
+ ind.setIndicatorName(name);
|
|
|
|
|
+ ind.setIndicatorValue(value);
|
|
|
|
|
+ ind.setStatus(status);
|
|
|
|
|
+ ind.setRefRange(range);
|
|
|
|
|
+ result.add(ind);
|
|
|
|
|
+ }
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ======================== 营养/氨基酸/维生素/微量元素(3行一组) ========================
|
|
|
|
|
+
|
|
|
|
|
+ private void parseIndicatorSection(List<String> lines, String sectionHeader, List<ParsedIndicator> result) {
|
|
|
|
|
+ int startIdx = findSectionStart(lines, sectionHeader);
|
|
|
|
|
+ if (startIdx < 0) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 从startIdx开始,跳过表头行
|
|
|
|
|
+ int idx = startIdx;
|
|
|
|
|
+ while (idx < lines.size() && !isNumeric(lines.get(idx))) {
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ }
|
|
|
|
|
+ idx -= 2; // 回退到数据开始前
|
|
|
|
|
+ if (idx < startIdx) idx = startIdx;
|
|
|
|
|
+
|
|
|
|
|
+ // 3行一组: name, value, status
|
|
|
|
|
+ for (int i = idx; i < lines.size() - 2; i++) {
|
|
|
|
|
+ String name = lines.get(i);
|
|
|
|
|
+ String valLine = lines.get(i + 1);
|
|
|
|
|
+ String stLine = lines.get(i + 2);
|
|
|
|
|
+
|
|
|
|
|
+ // 检测是否为有效数据行
|
|
|
|
|
+ if (isNumeric(valLine) && !isNumeric(name) && !stLine.contains("评估")) {
|
|
|
|
|
+ // 确定category
|
|
|
|
|
+ String category = resolveCategory(sectionHeader);
|
|
|
|
|
+ ParsedIndicator ind = new ParsedIndicator();
|
|
|
|
|
+ ind.setCategory(category);
|
|
|
|
|
+ ind.setIndicatorName(name);
|
|
|
|
|
+ ind.setIndicatorValue(valLine);
|
|
|
|
|
+ ind.setStatus(stLine);
|
|
|
|
|
+ result.add(ind);
|
|
|
|
|
+ i += 2;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String resolveCategory(String sectionHeader) {
|
|
|
|
|
+ if (sectionHeader.contains("主要营养")) return "主要营养评估";
|
|
|
|
|
+ if (sectionHeader.contains("氨基酸")) return "氨基酸评估";
|
|
|
|
|
+ if (sectionHeader.contains("维生素")) return "维生素评估";
|
|
|
|
|
+ if (sectionHeader.contains("微量元素")) return "微量元素评估";
|
|
|
|
|
+ return sectionHeader;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ======================== 菌群表解析(核心菌属/有害菌属) ========================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析菌属构成表
|
|
|
|
|
+ * 格式: 名称, 丰度%, 正常范围%, 处于人群%水平, %正常人有检出, [可选说明]
|
|
|
|
|
+ */
|
|
|
|
|
+ private void parseGutFloraTables(List<String> lines, String tableHeader, String category,
|
|
|
|
|
+ List<ParsedGutFlora> result) {
|
|
|
|
|
+ int tableNum = 1;
|
|
|
|
|
+ while (true) {
|
|
|
|
|
+ String header = tableHeader + tableNum;
|
|
|
|
|
+ int startIdx = findSectionStart(lines, header);
|
|
|
|
|
+ if (startIdx < 0) {
|
|
|
|
|
+ // 可能表头没有序号,尝试直接搜索
|
|
|
|
|
+ if (tableNum == 1) {
|
|
|
|
|
+ startIdx = findSectionStart(lines, tableHeader);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (startIdx < 0) break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 跳过表头行(名称, 丰度%, 正常范围%, 处于人群%水平, %正常人有检出)
|
|
|
|
|
+ int idx = startIdx;
|
|
|
|
|
+ while (idx < lines.size()) {
|
|
|
|
|
+ String line = lines.get(idx);
|
|
|
|
|
+ if (line.contains("名称") || line.contains("丰度") || line.contains("正常范围")
|
|
|
|
|
+ || line.contains("处于人群") || line.contains("检出")) {
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析数据行:5-6行一组
|
|
|
|
|
+ while (idx < lines.size()) {
|
|
|
|
|
+ String nameLine = lines.get(idx);
|
|
|
|
|
+ // 判断是否到达表尾
|
|
|
|
|
+ if (nameLine.contains(tableHeader) || nameLine.contains("核心菌属")
|
|
|
|
|
+ || nameLine.contains("有害菌属") || nameLine.contains("其他重要菌属")
|
|
|
|
|
+ || nameLine.contains("病原菌属") || nameLine.contains("其他菌属")
|
|
|
|
|
+ || nameLine.contains("---")) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 跳过说明行和空行
|
|
|
|
|
+ if (nameLine.startsWith("说明:") || nameLine.startsWith("改善方式")) {
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (idx + 4 >= lines.size()) break;
|
|
|
|
|
+
|
|
|
|
|
+ String abundance = lines.get(idx + 1);
|
|
|
|
|
+ String normalRange = lines.get(idx + 2);
|
|
|
|
|
+ String popLevel = lines.get(idx + 3);
|
|
|
|
|
+ String detRate = lines.get(idx + 4);
|
|
|
|
|
+
|
|
|
|
|
+ // 验证丰度值格式
|
|
|
|
|
+ if (isAbundanceValue(abundance) && (normalRange.contains("-") || normalRange.contains("~"))) {
|
|
|
|
|
+ ParsedGutFlora flora = new ParsedGutFlora();
|
|
|
|
|
+ flora.setBacteriaName(nameLine);
|
|
|
|
|
+ flora.setBacteriaValue(cleanNumericValue(abundance));
|
|
|
|
|
+ flora.setNormalRange(normalRange);
|
|
|
|
|
+ flora.setPopulationLevel(popLevel.endsWith("%") ? popLevel : popLevel + "%");
|
|
|
|
|
+ flora.setDetectionRate(detRate.endsWith("%") ? detRate : detRate + "%");
|
|
|
|
|
+ flora.setCategory(category);
|
|
|
|
|
+ flora.setLevel("GENUS");
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否有说明行(第6行)
|
|
|
|
|
+ if (idx + 5 < lines.size() && lines.get(idx + 5).startsWith("说明:")) {
|
|
|
|
|
+ flora.setDescription(lines.get(idx + 5).replace("说明:", "").trim());
|
|
|
|
|
+ idx += 6;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ idx += 5;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result.add(flora);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tableNum++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ======================== 益生菌解析 ========================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析益生菌列表
|
|
|
|
|
+ * 格式: 名称, 丰度%, ⼈群⽔平%, %⼈检出
|
|
|
|
|
+ */
|
|
|
|
|
+ private void parseProbioticSection(List<String> lines, List<ParsedGutFlora> result) {
|
|
|
|
|
+ int startIdx = -1;
|
|
|
|
|
+ // 查找"益生菌"标题(注意不是"有益菌")
|
|
|
|
|
+ for (int i = 0; i < lines.size() - 1; i++) {
|
|
|
|
|
+ String line = lines.get(i);
|
|
|
|
|
+ if (line.equals("益生菌") && i > 0) {
|
|
|
|
|
+ // 确认前一行不是副标题相关内容
|
|
|
|
|
+ startIdx = i + 1;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (startIdx < 0) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 跳过表头:名称, 丰度%, ⼈群⽔平%, %⼈检出
|
|
|
|
|
+ int idx = startIdx;
|
|
|
|
|
+ while (idx < lines.size() && (lines.get(idx).contains("名称") || lines.get(idx).contains("丰度")
|
|
|
|
|
+ || lines.get(idx).contains("人群") || lines.get(idx).contains("检出"))) {
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4行一组: name, abundance, popLevel, detRate
|
|
|
|
|
+ while (idx + 3 < lines.size()) {
|
|
|
|
|
+ String name = lines.get(idx);
|
|
|
|
|
+ String value = lines.get(idx + 1);
|
|
|
|
|
+ String popLevel = lines.get(idx + 2);
|
|
|
|
|
+ String detRate = lines.get(idx + 3);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否仍然是益生菌数据(丰度值含有%或ND)
|
|
|
|
|
+ if (name.contains("益生菌") || name.contains("我的益生菌") || name.contains("建议")
|
|
|
|
|
+ || name.contains("双歧杆菌") && !value.contains("%") && !value.equals("ND")) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 跳过说明行
|
|
|
|
|
+ if (name.startsWith("仅列出") || name.startsWith("双歧杆菌") || name.startsWith("乳杆菌")) {
|
|
|
|
|
+ // 这是表头后的第一行实际数据(属名)
|
|
|
|
|
+ if (name.contains("双歧杆菌属") || name.contains("乳杆菌属")) {
|
|
|
|
|
+ // 这是实际数据,继续
|
|
|
|
|
+ } else {
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 验证value格式
|
|
|
|
|
+ if (value.contains("%") || value.equals("ND") || isNumeric(value.replaceAll("[^0-9.]", ""))) {
|
|
|
|
|
+ ParsedGutFlora flora = new ParsedGutFlora();
|
|
|
|
|
+ flora.setBacteriaName(name);
|
|
|
|
|
+ flora.setBacteriaValue(cleanNumericValue(value));
|
|
|
|
|
+ flora.setPopulationLevel(popLevel);
|
|
|
|
|
+ flora.setDetectionRate(detRate);
|
|
|
|
|
+ flora.setCategory("益生菌");
|
|
|
|
|
+ flora.setLevel("SPECIES");
|
|
|
|
|
+ result.add(flora);
|
|
|
|
|
+ idx += 4;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ======================== 工具方法 ========================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查找section标题所在行
|
|
|
|
|
+ */
|
|
|
|
|
+ private int findSectionStart(List<String> lines, String header) {
|
|
|
|
|
+ for (int i = 0; i < lines.size(); i++) {
|
|
|
|
|
+ if (lines.get(i).contains(header)) {
|
|
|
|
|
+ return i;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return -1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isNumeric(String str) {
|
|
|
|
|
+ if (str == null || str.isEmpty()) return false;
|
|
|
|
|
+ String cleaned = str.replaceAll("[,\\s%]", "").trim();
|
|
|
|
|
+ if (cleaned.isEmpty()) return false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ Double.parseDouble(cleaned);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 判断是否为丰度值格式(如 "0.1712", "ND", "76.721%")
|
|
|
|
|
+ */
|
|
|
|
|
+ private boolean isAbundanceValue(String str) {
|
|
|
|
|
+ if (str == null || str.isEmpty()) return false;
|
|
|
|
|
+ if (str.equals("ND")) return true;
|
|
|
|
|
+ String cleaned = str.replaceAll("[^0-9.]", "").trim();
|
|
|
|
|
+ return !cleaned.isEmpty() && cleaned.matches("\\d+\\.?\\d*");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 清理数值中的特殊字符
|
|
|
|
|
+ */
|
|
|
|
|
+ private String cleanNumericValue(String str) {
|
|
|
|
|
+ if (str == null) return null;
|
|
|
|
|
+ if (str.equals("ND")) return "ND";
|
|
|
|
|
+ // 保留百分比,"ND"
|
|
|
|
|
+ return str.replaceAll("[\\uF000-\\uFFFF]", "").trim();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|