4. 数据获取与API工程

靶点发现的第一步不是建模,而是把分散在 UniProt、Ensembl、ChEMBL、Open Targets、PDB、Reactome 等公共资源中的靶点信息稳定、可追溯地取回来。手工下载几十个页面无法支撑后续分析,真正需要的是可复现的数据管道:给定同一份配置和同一时间窗口,任何人都能重新得到同一批数据。本节围绕 REST API、FTP、GraphQL 与增量更新策略,说明如何把“拉数据”工程化。

先判断接口形态,再决定拉取方式

公共数据库通常同时提供多种访问通道,选择依据是数据粒度、更新频率和查询灵活性。REST API 以资源路径为中心,适合按标识符精确取单条记录,例如 https://rest.uniprot.org/uniprotkb/P00533.json 返回 EGFR 的完整条目;FTP/HTTP 文件服务提供全量或按物种切分的压缩文件,适合一次性建立本地基线库;GraphQL 允许一次请求组合多个实体和字段,适合 Open Targets 这类以“靶点—疾病—证据”关系为核心的平台。三者不是替代关系,而是分层配合:FTP 建基线,REST 补细节,GraphQL 做关联查询。

靶点数据获取管道:接口分层与增量更新 公共数据源 UniProt / Ensembl ChEMBL / PDB Open Targets / Reactome 版本号 + 发布日期 访问接口 REST:按 ID 精确取 FTP:全量基线文件 GraphQL:关系组合查询 限速 / 分页 / 重试 本地处理 解析 JSON / TSV 字段映射与去重 写入 Parquet / SQLite 保留原始响应快照 靶点库 raw/ staging/ curated/ 带时间戳分区 增量更新策略:三种触发方式与状态记录 ① 时间戳增量 记录 last_run 时间 只取该时间后更新记录 适用:UniProt 检索 API 风险:时钟漂移需留缓冲 ② 版本比对增量 比对 release 版本号 版本变化才重新拉取 适用:ChEMBL / Ensembl 风险:需解析版本元数据 ③ 校验和增量 下载前取 MD5 / ETag 与本地记录比对 适用:FTP 全量文件 风险:首次仍需全量下载

REST API:分页、限速与重试是基本要求

以 UniProt 检索 API 为例,批量获取一批靶点(如 50 个基因名)时,不应逐个发请求,而应使用查询语法一次检索多个:https://rest.uniprot.org/uniprotkb/search?query=gene_exact:EGFR+OR+gene_exact:ERBB2&format=json&size=500。返回结果通过 Link 响应头给出下一页地址,必须循环跟随直到没有 next。分页大小建议 200~500,过大容易触发超时,过小则请求数激增。

限速方面,UniProt 对匿名请求通常允许每秒数个请求,工程上应设置固定间隔或令牌桶。重试必须区分错误类型:HTTP 429 和 5xx 可指数退避重试(如 1s、2s、4s、8s),400 和 404 属于请求本身错误,重试无意义。每次请求都应记录 URL、状态码、耗时和响应哈希,写入日志表,这是可复现性的最低要求。

import requests, time, hashlib, json

def fetch_uniprot(genes, out):
    url = "https://rest.uniprot.org/uniprotkb/search"
    params = {"query": " OR ".join(f"gene_exact:{g}" for g in genes),
              "format": "json", "size": 500}
    while url:
        for attempt in range(4):
            r = requests.get(url, params=params, timeout=30)
            if r.status_code == 200:
                break
            time.sleep(2 ** attempt)          # 指数退避
        data = r.json()
        for item in data["results"]:
            out.write(json.dumps(item) + "\n")
        url = r.links.get("next", {}).get("url")
        params = None                          # 下一页 URL 已含参数
        time.sleep(0.5)                        # 固定限速间隔

FTP 与批量文件:先建基线,再谈增量

ChEMBL 通过 FTP 提供 chembl_34_chemreps.txt.gz 等文件,Ensembl 提供按物种划分的 GTF 和 FASTA。这类文件动辄数百 MB 到数 GB,正确做法是:首次全量下载并记录文件名、大小、MD5;后续运行先读取远端目录列表,比对 MD5 或修改时间,只有变化才重新下载。下载过程应支持断点续传(curl -C - 或 wget -c),并校验解压后行数与官方 README 声明是否一致,作为完整性检查。

FTP 目录中的 CHECKSUM 或 md5sum.txt 文件是判断文件是否更新的可靠依据,比单纯比较修改时间更稳健,因为部分镜像会重写时间戳但内容未变。

GraphQL:一次请求拿到靶点—疾病—证据链

Open Targets 的 GraphQL 端点允许在一个查询中同时取靶点基本信息、关联疾病和证据评分。相比 REST 需要多次往返,GraphQL 的优势在于按需取字段、减少冗余传输。下面示例查询 EGFR 的已知药物和关联疾病:

query {
  target(ensemblId: "ENSG00000146648") {
    approvedSymbol
    knownDrugs(size: 5) {
      rows { drug { name } phase mechanismOfAction }
    }
    associatedDiseases(page: {index: 0, size: 5}) {
      rows { disease { name } score }
    }
  }
}

GraphQL 的工程注意点与 REST 不同:它通常只有一个端点,但查询复杂度受限,深层嵌套可能被拒绝;错误以 200 状态码返回在 errors 字段中,必须显式检查;分页通过 page 参数而非链接头。批量靶点查询应使用别名(alias)把多个 target 查询合并为一次请求,但要注意单次查询的复杂度上限,必要时拆成每批 10~20 个。

增量更新与状态管理

增量更新的核心是维护一张数据源状态表,记录每个源的最后成功时间、版本号、校验和和游标。每次运行管道时,先读状态表决定拉取范围,成功后更新状态,失败则保留原状态并告警。这样即使中途中断,下次运行也不会重复拉取或漏拉。

字段示例值作用
sourceuniprot数据源标识
last_success2025-03-01T08:00:00Z时间戳增量起点
release2025_02版本比对依据
checksuma3f1...9c2e文件是否变化
cursorpage=7分页中断续拉
statusok / failed失败时阻止状态推进
不要把“请求成功”等同于“数据可用”。部分 API 在限速时会返回 200 但内容为空或截断,必须校验返回记录数、关键字段非空率和主键唯一性,再决定是否推进状态表。

可复现性的三个落点

第一,固定输入:靶点列表、查询语句、分页大小、时间窗口全部写入配置文件,不散落在代码里。第二,保留原始层:每次拉取的原始 JSON 或文件按 source/date/ 分区落盘,后续解析只读原始层,保证可回溯。第三,记录运行元数据:包括代码版本、配置哈希、开始结束时间、各源记录数。做到这三点,同一配置在任意机器上重跑,得到的 curated 层结果应当一致;若不一致,差异必然能定位到某个源的版本变化或某次失败重试,而不是“不知道哪里变了”。

实际管道中,建议把上述逻辑组织为三个可独立运行的阶段:fetch 负责按状态表拉取并写 raw 层,parse 负责把 raw 转成结构化 staging 表,curate 负责字段标准化、去重和主键对齐后写入 curated 层。每个阶段都可单独重跑,互不污染,这是靶点数据整合工程能够长期维护的基础。