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 做关联查询。
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 声明是否一致,作为完整性检查。
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 个。
增量更新与状态管理
增量更新的核心是维护一张数据源状态表,记录每个源的最后成功时间、版本号、校验和和游标。每次运行管道时,先读状态表决定拉取范围,成功后更新状态,失败则保留原状态并告警。这样即使中途中断,下次运行也不会重复拉取或漏拉。
| 字段 | 示例值 | 作用 |
|---|---|---|
| source | uniprot | 数据源标识 |
| last_success | 2025-03-01T08:00:00Z | 时间戳增量起点 |
| release | 2025_02 | 版本比对依据 |
| checksum | a3f1...9c2e | 文件是否变化 |
| cursor | page=7 | 分页中断续拉 |
| status | ok / failed | 失败时阻止状态推进 |
可复现性的三个落点
第一,固定输入:靶点列表、查询语句、分页大小、时间窗口全部写入配置文件,不散落在代码里。第二,保留原始层:每次拉取的原始 JSON 或文件按 source/date/ 分区落盘,后续解析只读原始层,保证可回溯。第三,记录运行元数据:包括代码版本、配置哈希、开始结束时间、各源记录数。做到这三点,同一配置在任意机器上重跑,得到的 curated 层结果应当一致;若不一致,差异必然能定位到某个源的版本变化或某次失败重试,而不是“不知道哪里变了”。
实际管道中,建议把上述逻辑组织为三个可独立运行的阶段:fetch 负责按状态表拉取并写 raw 层,parse 负责把 raw 转成结构化 staging 表,curate 负责字段标准化、去重和主键对齐后写入 curated 层。每个阶段都可单独重跑,互不污染,这是靶点数据整合工程能够长期维护的基础。