pmid转pmcid实践

大规模生物医学文献ID映射系统的设计与实现

前言

在生物医学研究领域,PubMed和PMC(PubMed Central)是两个重要的文献数据库。PubMed提供文献摘要,而PMC提供全文内容。在实际应用中,经常需要将PubMed ID(PMID)映射到对应的PMC ID,以获取完整的文献信息。本文将详细介绍一个处理2000万条PMID记录的大规模映射系统的设计与实现过程。

一、需求分析与拆解

1.1 业务需求

  • 数据规模:处理约2000万条PMID记录
  • 处理方式:一次性批量处理,非增量更新
  • 数据源:JSON格式的PMID文件,按编号序列存储(0001.json - 9999.json)
  • 输出要求:PMID与PMC ID的完整映射关系
  • 性能要求:在合理时间内完成处理,确保数据完整性

1.2 技术需求拆解

  1. API选择:需要选择合适的PubMed API进行ID转换
  2. 数据处理:大文件分片处理,批量API调用
  3. 错误处理:网络异常、API限制、无效PMID等异常情况
  4. 进度监控:实时处理进度展示和任务状态管理
  5. 数据存储:高效的数据库存储方案
  6. 系统稳定性:长时间运行的稳定性保障

二、方案设计

2.1 技术选型

API选择对比分析

经过调研,发现两个主要的PubMed API方案:

特性ELink APIPMC ID Converter API
专门化程度通用链接API专门的ID转换API
批处理能力复杂的小批次处理支持200个ID/批
响应格式复杂的嵌套XML/JSON简洁的JSON数组
解析复杂度需要300+行代码仅需50行代码
错误处理需要人工验证映射关系天然的一对一对应

最终选择:PMC ID Converter API,理由如下:

  • 更高的批处理效率(40倍提升:5→200 IDs/批)
  • 更简洁的响应格式,降低解析复杂度
  • 内置的一对一映射验证机制
  • 更好的错误处理支持
架构设计
┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Controller    │    │     Service      │    │   Data Layer    │
│                 │    │                  │    │                 │
│ - 任务管理       │───▶│ - 文件处理        │───▶│ - 映射结果存储   │
│ - 进度查询       │    │ - 批量API调用     │    │ - 任务状态管理   │
│ - 状态监控       │    │ - 错误处理        │    │ - 缺失文件记录   │
└─────────────────┘    └──────────────────┘    └─────────────────┘
                              │
                              ▼
                    ┌──────────────────┐
                    │   PubMed API     │
                    │   Client         │
                    │                  │
                    │ - HTTP客户端     │
                    │ - 响应解析       │
                    │ - 错误重试       │
                    └──────────────────┘

2.2 数据处理流程设计

文件范围任务启动
      │
      ▼
┌─────────────────┐
│  文件遍历处理    │
│  (0001-9999)   │
└─────────────────┘
      │
      ▼
┌─────────────────┐
│  单文件处理      │
│  - 读取JSON     │
│  - 解析PMID     │
│  - 分批处理     │
└─────────────────┘
      │
      ▼
┌─────────────────┐
│  API批量调用     │
│  - 200个/批     │
│  - 错误处理     │
│  - 结果解析     │
└─────────────────┘
      │
      ▼
┌─────────────────┐
│  数据库存储      │
│  - 批量插入     │
│  - 状态更新     │
│  - 进度统计     │
└─────────────────┘

三、核心编码实现

3.1 API客户端设计

关键特性

  • 支持200个PMID的批量处理
  • 完善的错误检测和处理机制
  • 统一的响应解析逻辑
public class PubMedApiClient {
    private static final String PMC_CONVERTER_URL = 
        "https://www.ncbi.nlm.nih.gov/pmc/utils/idconv/v1.0/";
    
    public Map<Long, String> getPmcIds(List<Long> pmids) {
        // 构建请求参数
        String ids = pmids.stream()
            .map(String::valueOf)
            .collect(Collectors.joining(","));
        
        String url = PMC_CONVERTER_URL + "?ids=" + ids + 
                    "&format=json&email=" + email;
        
        // HTTP请求处理
        String response = restTemplate.getForObject(url, String.class);
        
        // 响应解析与错误处理
        return parseResponse(response, pmids);
    }
    
    private Map<Long, String> parseResponse(String responseBody, List<Long> requestPmids) {
        Map<Long, String> resultMap = new HashMap<>();
        
        JsonArray records = JsonParser.parseString(responseBody)
            .getAsJsonObject().getAsJsonArray("records");
            
        for (JsonElement recordElement : records) {
            JsonObject record = recordElement.getAsJsonObject();
            Long pmid = Long.valueOf(record.get("pmid").getAsString());
            
            // 错误检测
            if (record.has("status") && "error".equals(record.get("status").getAsString())) {
                String errorMsg = record.has("errmsg") ? 
                    record.get("errmsg").getAsString() : "未知错误";
                log.debug("PMID {} 存在错误: {}", pmid, errorMsg);
                resultMap.put(pmid, null);
            } else {
                // 正常映射处理
                if (record.has("pmcid")) {
                    String pmcid = record.get("pmcid").getAsString();
                    resultMap.put(pmid, pmcid);
                } else {
                    resultMap.put(pmid, null);
                }
            }
        }
        
        return resultMap;
    }
}

3.2 服务层设计

核心功能

  • 文件分片处理
  • 批量数据处理
  • 进度跟踪管理
  • 异常恢复机制
@Service
public class FileBasedPmidMappingService {
    private static final int FILE_BATCH_SIZE = 3000;
    private static final int API_BATCH_SIZE = 200;
    
    public Long startFileRangeTask(String taskName, int startIndex, int endIndex) {
        // 创建任务记录
        FileProcessTask task = new FileProcessTask()
            .setTaskName(taskName)
            .setStartIndex(startIndex)
            .setEndIndex(endIndex)
            .setStatus(FileProcessTask.Status.PROCESSING);
            
        fileProcessTaskMapper.insert(task);
        
        // 异步处理
        CompletableFuture.runAsync(() -> processFileRangeAsync(task.getId()), executorService);
        
        return task.getId();
    }
    
    private void processFileRangeAsync(Long taskId) {
        FileProcessTask task = fileProcessTaskMapper.selectById(taskId);
        
        for (int fileIndex = task.getStartIndex(); fileIndex <= task.getEndIndex(); fileIndex++) {
            String fileName = String.format("%04d.json", fileIndex);
            
            try {
                // 处理单个文件
                processSingleFile(task, fileName, fileIndex);
                
                // 更新进度
                updateTaskProgress(task, 1, 0);
                
            } catch (Exception e) {
                log.error("处理文件失败: {}", fileName, e);
                recordMissingFile(task.getId(), fileName, e.getMessage());
                updateTaskProgress(task, 0, 1);
            }
        }
        
        // 标记任务完成
        task.setStatus(FileProcessTask.Status.COMPLETED);
        fileProcessTaskMapper.updateById(task);
    }
}

3.3 并发优化设计

线程池配置

public class ThreadPoolConfig {
    public FileBasedPmidMappingService() {
        ThreadFactory threadFactory = new ThreadFactoryBuilder()
            .setNameFormat("pmid-mapping-pool-%d")
            .build();

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            2, 2,  // 核心和最大线程数
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1000),
            threadFactory, 
            new AbortPolicy()
        );

        threadPoolExecutor.prestartAllCoreThreads();
        this.executorService = threadPoolExecutor;
    }
}

3.4 进度监控与日志优化

日志分层设计

// 文件级别进度
log.info("📄 处理文件 {} 第 {}/{} 批 ({}个PMID)", 
    fileName, currentBatch, totalBatches, pmids.size());

// API调用详情(Debug级别)
log.debug("  🔗 API调用 {}/{} - {}个PMID", 
    apiIndex + 1, apiBatches.size(), apiBatch.size());

// 完成统计
log.info("✅ 完成文件 {} 第 {}/{} 批 - 成功: {}, 无PMC: {}, 失败: {}", 
    fileName, currentBatch, totalBatches, successCount, noPmcCount, failedCount);

四、数据库设计

4.1 核心表结构

映射结果表

CREATE TABLE pmid_pmc_mapping_simple (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    pmid BIGINT NOT NULL COMMENT 'PubMed ID',
    pmc_id VARCHAR(20) COMMENT 'PMC ID,可能为空',
    status TINYINT DEFAULT 1 COMMENT '状态:1-成功,2-无PMC,3-失败',
    file_name VARCHAR(100) COMMENT '来源文件名',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    KEY idx_pmid (pmid),
    KEY idx_file_name (file_name)
);

任务管理表

CREATE TABLE file_process_task (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    task_name VARCHAR(100) NOT NULL,
    start_index INT NOT NULL,
    end_index INT NOT NULL,
    processed_files INT DEFAULT 0,
    total_files INT DEFAULT 0,
    missing_files INT DEFAULT 0,
    total_pmids BIGINT DEFAULT 0,
    success_pmids BIGINT DEFAULT 0,
    failed_pmids BIGINT DEFAULT 0,
    status TINYINT DEFAULT 1,
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP
);

五、难点解析

5.1 API选型的技术难点

问题:ELink API响应结构复杂,需要复杂的解析逻辑

// ELink API复杂响应
{
  "linksets": [{
    "ids": ["12345"],
    "linksetdbs": [{
      "dbfrom": "pubmed",
      "linkname": "pubmed_pmc",
      "links": [{
        "id": "12345"
      }]
    }]
  }]
}

解决方案:迁移到PMC ID Converter API

// PMC ID Converter简洁响应
{
  "records": [
    {"pmid": "12345", "pmcid": "PMC123456"},
    {"pmid": "67890", "status": "error", "errmsg": "invalid article id"}
  ]
}

5.2 大规模数据处理的性能优化

挑战

  • 2000万条记录的处理时间
  • API调用频率限制
  • 内存使用优化

解决策略

  1. 批量处理:将5个ID/批提升到200个ID/批,40倍效率提升
  2. 分片处理:按文件分片,避免大文件内存占用
  3. 异步处理:使用线程池进行并发处理
  4. 进度持久化:支持断点续传

5.3 错误处理的完善性

复杂场景

  • 网络连接异常
  • API返回错误状态
  • 无效的PMID
  • 文件不存在

处理机制

// 多层错误处理
try {
    Map<Long, String> pmcIdMap = pubMedApiClient.getPmcIds(apiBatch);
    // 正常处理逻辑
} catch (Exception e) {
    log.error("API调用失败,批次大小: {}", apiBatch.size(), e);
    
    // 标记整批为失败
    for (Long pmid : apiBatch) {
        PmidPmcMappingSimple mapping = new PmidPmcMappingSimple()
            .setPmid(pmid)
            .setStatus(PmidPmcMappingSimple.Status.FAILED)
            .setFileName(fileName);
        allMappings.add(mapping);
    }
}

5.4 监控与运维的可观测性

挑战:长时间运行任务的监控需求

解决方案

  1. 分层日志:INFO级别显示关键进度,DEBUG级别显示技术细节
  2. 进度统计:文件级和PMID级的双重进度跟踪
  3. 异常记录:详细的错误日志和缺失文件记录
  4. 实时状态:RESTful API支持实时查询任务状态

六、性能与效果总结

6.1 性能提升数据

指标优化前优化后提升倍数
API批处理大小5个PMID200个PMID40x
代码复杂度300+行150行50%减少
错误处理能力手动验证自动检测质的提升
并发处理单线程2线程2x

6.2 系统稳定性提升

  1. 错误恢复:支持任务断点续传和异常恢复
  2. 资源管理:优化的线程池和内存使用
  3. 监控完善:完整的进度跟踪和异常记录
  4. 运维友好:清晰的日志分层和状态查询接口

6.3 技术债务清理

在项目重构过程中,清理了大量过时代码:

  • 删除9个过时的源代码文件
  • 简化了项目结构
  • 统一了技术方案
  • 完善了文档体系

七、总结与展望

7.1 项目成果

本项目成功构建了一个高效、稳定、可扩展的大规模生物医学文献ID映射系统,具备以下特点:

  1. 高性能:通过API优化和批处理提升,实现了40倍的处理效率提升
  2. 高可靠性:完善的错误处理和恢复机制,确保数据处理的完整性
  3. 高可维护性:清晰的架构设计和代码结构,便于后续维护和扩展
  4. 高可观测性:完整的监控和日志体系,支持实时运维管理

7.2 技术价值

  1. API选型方法论:通过对比分析,展示了技术选型的重要性和方法
  2. 大规模数据处理模式:提供了处理千万级数据的完整解决方案
  3. 系统架构最佳实践:分层架构、错误处理、并发优化等方面的实践经验
  4. 运维监控体系:构建了完整的可观测性体系

7.3 后续优化方向

  1. 性能进一步优化

    • 引入缓存机制减少重复API调用
    • 优化数据库批量操作性能
    • 考虑分布式处理架构
  2. 功能扩展

    • 支持增量更新模式
    • 添加数据质量检查功能
    • 提供数据导出和统计分析功能
  3. 系统集成

    • 与现有科研数据平台集成
    • 提供标准化的API接口
    • 支持多种数据源和格式

通过本项目的实施,不仅解决了大规模文献ID映射的业务需求,更重要的是积累了宝贵的技术经验和最佳实践,为后续类似项目提供了可复用的技术方案和实施经验。


本文详细介绍了大规模生物医学文献ID映射系统的完整实现过程,从需求分析到系统设计,从核心编码到难点解析,希望能为相关领域的技术实践提供参考和借鉴。

rm(list = ls()) ############################################### ## Basic Code ## ## Author: Luo Huaichao ## ## Version: v1.0 (2025-10-20) ## # Acknowledgments: We gratefully acknowledge the multidisciplinary collaboration provided by the Intelligent Clinlabomics Research Elites (iCARE) consortium. # cited Luo H, et al. Signal Transduct Target Ther. 2022 Oct 10;7(1):348. doi: 10.1038/s41392-022-01169-7. PMID: 36210387; PMCID: PMC9548502. # cited Wen X, et al. Clinlabomics: leveraging clinical laboratory data by data mining strategies. BMC Bioinformatics. 2022 Sep 24;23(1):387. doi: 10.1186/s12859-022-04926-1. PMID: 36153474; PMCID: PMC9509545. # cited Kawakami E, et al. Application of artificial intelligence for preoperative diagnostic and prognostic prediction in epithelial ovarian cancer based on blood biomarkers. Clin Cancer Res 2019; 25: 3006–15. # This is basic code, methods from STTT PMID: 36210387, data from Clinical Chemistry PMID: 38431275. ############################################### ## Preparation - Create Demo Data # Step 1: Create folder structure dir.create("Medical_Lab_Tests", showWarnings = FALSE) dir.create("Medical_Lab_Tests/Lab_Data", showWarnings = FALSE) # Step 2: Generate simulated lab test data library(writexl) # For writing Excel files # Lab test reference ranges test_items <- data.frame( Test_Item = c("WBC", "RBC", "Hemoglobin", "Platelet"), Reference_Lower = c(4, 3.5, 120, 100), Reference_Upper = c(10, 5.5, 160, 300), Unit = c("10^9/L", "10^12/L", "g/L", "10^9/L") ) # Generate 30 simulated reports (3 days × 10 reports/day) set.seed(123) dates <- rep(c("2024-01-15", "2024-01-16", "2024-01-17"), each = 10) test_types <- rep(c("CBC", "Biochemistry", "Immunology"), length.out = 30) patients <- paste0("Patient", sprintf("%03d", 1:30)) for (i in 1:30) { # Generate random test results report <- data.frame( Test_Item = test_items$Test_Item, Result_Value = c( runif(1, 3, 12), # WBC runif(1, 3, 6), # RBC runif(1, 110, 170), # Hemoglobin runif(1, 80, 320) # Platelet ), Reference_Lower = test_items$Reference_Lower, Reference_Upper = test_items$Reference_Upper, Unit = test_items$Unit ) # Save file filename <- paste0("Medical_Lab_Tests/Lab_Data/", dates[i], "_", test_types[i], "_", patients[i], "_A", sprintf("%03d", i), ".xlsx") write_xlsx(report, filename) } print("✓ Created 30 simulated lab reports")
10-26
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值