关于 unichem-database
UniChem 是 EMBL-EBI 提供的化学结构交叉引用服务,面向需要整合公共化学数据库的研究人员。它利用 InChI 标识符,将化合物映射到 20 多个数据库的记录,并提供结构连接性检索。
主要功能
- 01按 InChIKey 查询跨库记录
- 02通过来源数据库 ID 查询
- 03检索结构连接性相关化合物
- 04列出可用数据源目录
- 05以 REST API 返回交叉引用
适用场景
- 01将 ChEMBL ID 转换为 PubChem CID
- 02核对多个数据库中的化合物身份
- 03查找立体异构体或盐形式的相关记录
- 04合并生物活性表与结构数据库标识符
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jaechang-hits/ sciagent-skills --skill unichem-database安装后在 Claude Code 中输入/unichem-database确认已加载。
详细介绍
UniChem 数据库
概述
UniChem 是 EMBL-EBI 提供的一项化学结构交叉引用服务,使用基于 InChI 的标识符关联 20 多个公共化学数据库中的化合物记录。 它会将单个化学实体映射到其在 ChEMBL、DrugBank、PubChem、ChEBI、PDB(RCSB 和 PDBe)、SureChEMBL、HMDB、DrugCentral、BindingDB 等数据库中的对应 ID。 可通过 https://www.ebi.ac.uk/unichem/api/v1/ 上的免费 REST API 访问,无需 API 密钥。 重要提示:所有交叉引用查询都通过带 JSON 正文的 POST 发送;只有目录端点 GET /sources 使用 GET 实现。
适用场景
- 将 ChEMBL 化合物 ID 转换为 PubChem CID、DrugBank 登录号或 ChEBI ID,以便进行跨数据库分析
- 根据 InChIKey 查找化合物出现在哪些数据库来源中
- 使用连通性搜索,在各数据库中查找所有结构相关化合物(连通性相同,但立体化学/盐形式不同)
- 在合并多个数据库的数据集之前,验证不同来源中的化合物身份
- 为药物发现项目构建化合物交叉引用表(将 ChEMBL 中的生物活性数据关联到 PDB 中的结构数据)
- 通过 InChIKey 检查合成化合物或供应商化合物是否存在于任何公共数据库中
- 如需完整的生物活性谱(IC50、Ki),请使用 chembl-database-bioactivity;UniChem 仅提供 ID 交叉引用,不提供实验数据
- 如需化合物性质预测或子结构搜索,请使用 pubchem-compound-search;UniChem 仅用于标识符转换
前提条件
- Python 包:requests、pandas、matplotlib
- 数据要求:化合物 InChIKey(标准 27 字符格式 XXXXXXXXXXXXXX-XXXXXXXXXX-X)、特定来源的 ID(例如 CHEMBL25),或作为起始点的 PubChem CID
- 环境:需要互联网连接;无需 API 密钥
- 速率限制:约 10 次请求/秒;在批量循环的请求之间添加 time.sleep(0.1);无每日配额
pip install requests pandas matplotlib
快速开始
UniChem /compounds 端点仅支持 POST——GET 会返回 405 Method Not Allowed。提交 JSON 正文 {type: inchikey, compound: KEY},并从 compounds[0][sources] 中读取各数据库的命中结果。每条来源记录都包含 id(数据库的数字 ID)和 compoundId(该数据库中的 ID)。
import requests
UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
def unichem_post(endpoint: str, body: dict) -> dict:
"""POST request to UniChem API; raise on HTTP errors."""
r = requests.post(f"{UNICHEM_API}/{endpoint}", json=body, timeout=20)
r.raise_for_status()
return r.json()
# Find all database sources for aspirin by InChIKey
inchikey = "BSYNRYMUTXBXSQ-UHFFFAOYSA-N" # aspirin
result = unichem_post("compounds", {"type": "inchikey", "compound": inchikey})
compounds = result.get("compounds", [])
print(f"Found {len(compounds)} compound record(s) for {inchikey}")
if compounds:
sources = compounds[0].get("sources", [])
print(f" Present in {len(sources)} database records")
seen = set()
for src in sources:
if src["id"] in seen:
continue
seen.add(src["id"])
print(f" source id={src['id']:>3} ({src['shortName']:>12}): {src['compoundId']}")
if len(seen) >= 5:
break
# Found 1 compound record(s) for BSYNRYMUTXBXSQ-UHFFFAOYSA-N
# Present in many database records
# source id= 1 ( chembl): CHEMBL25
# source id= 2 ( drugbank): DB00945
# source id= 3 ( rcsb_pdb): AIN
核心 API
查询 1:InChIKey 查询——所有来源
使用标准 InChIKey 搜索化合物并获取所有数据库记录。这是主要的交叉引用方法。端点为 POST /compounds;如果找到结果,响应会在 compounds 中包含一个条目,每个条目都有一个 sources 列表,其中的记录使用 id(来源数据库)和 compoundId(该数据库中的 ID)。
import requests, pandas as pd
UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
# Common source IDs (verify with the /sources endpoint - see Query 4)
SOURCE_NAMES = {
1: "ChEMBL", 2: "DrugBank", 3: "RCSB PDB", 4: "GtoPdb", 5: "PDBe",
7: "ChEBI", 14: "FDA SRS", 15: "SureChEMBL", 18: "HMDB", 22: "PubChem",
31: "BindingDB", 32: "CompTox", 33: "LIPID MAPS", 34: "DrugCentral",
37: "BRENDA", 38: "Rhea", 41: "SwissLipids", 49: "Probes-and-Drugs",
}
def lookup_by_inchikey(inchikey: str) -> pd.DataFrame:
"""Return all database cross-references for an InChIKey."""
r = requests.post(f"{UNICHEM_API}/compounds",
json={"type": "inchikey", "compound": inchikey}, timeout=20)
r.raise_for_status()
compounds = r.json().get("compounds", [])
if not compounds:
return pd.DataFrame()
rows = []
for src in compounds[0].get("sources", []):
rows.append({
"source_id": src["id"],
"source_name": SOURCE_NAMES.get(src["id"], src.get("shortName", "")),
"compound_id": src["compoundId"],
"url": src.get("url", ""),
})
return pd.DataFrame(rows).sort_values(["source_id", "compound_id"])
# Triclosan cross-references
df = lookup_by_inchikey("XEFQLINVKFYRCS-UHFFFAOYSA-N")
print(f"Triclosan found in {df['source_id'].nunique()} distinct databases ({len(df)} records):")
print(df[["source_name", "compound_id"]].head(8).to_string(index=False))
# Triclosan found in 16 distinct databases
# ChEMBL CHEMBL849
# DrugBank DB08604
# RCSB PDB TCL
# ChEBI CHEBI:164200
# Extract specific source IDs from cross-reference table
def get_id_for_source(inchikey: str, source_id: int) -> str | None:
"""Return the compound ID in a specific database, or None if not found."""
r = requests.post(f"{UNICHEM_API}/compounds",
json={"type": "inchikey", "compound": inchikey}, timeout=20)
r.raise_for_status()
compounds = r.json().get("compounds", [])
if not compounds:
return None
for src in compounds[0].get("sources", []):
if src["id"] == source_id:
return src["compoundId"]
return None
triclosan = "XEFQLINVKFYRCS-UHFFFAOYSA-N"
chembl_id = get_id_for_source(triclosan, source_id=1) # ChEMBL
pubchem_id = get_id_for_source(triclosan, source_id=22) # PubChem
drugbank_id = get_id_for_source(triclosan, source_id=2) # DrugBank
print(f"Triclosan: ChEMBL={chembl_id}, PubChem={pubchem_id}, DrugBank={drugbank_id}")
# Triclosan: ChEMBL=CHEMBL849, PubChem=5564, DrugBank=DB08604
查询 2:按特定来源 ID 查询化合物
已知特定来源数据库中的化合物 ID(例如 ChEMBL ID)时,可获取所有交叉引用。使用 type: sourceID,并在正文中同时提供化合物的来源 ID 和数字形式的 sourceID。返回的数据结构与 InChIKey 查询相同。
import requests
UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
def get_sources_for_compound(compound_id: str, source_id: int) -> list:
"""Get all database cross-references for a compound identified in a specific source.
Args:
compound_id: The ID in the source database (e.g., CHEMBL192)
source_id: UniChem source ID (1=ChEMBL, 2=DrugBank, 22=PubChem, 7=ChEBI)
"""
body = {"type": "sourceID", "compound": compound_id, "sourceID": source_id}
r = requests.post(f"{UNICHEM_API}/compounds", json=body, timeout=20)
r.raise_for_status()
compounds = r.json().get("compounds", [])
if not compounds:
return []
return compounds[0].get("sources", [])
# Sildenafil (Viagra): look up starting from ChEMBL ID
sources = get_sources_for_compound("CHEMBL192", source_id=1)
distinct_dbs = {s["id"] for s in sources}
print(f"Sildenafil (CHEMBL192): {len(sources)} source records across {len(distinct_dbs)} databases")
seen = set()
for s in sources:
if s["id"] in seen:
continue
seen.add(s["id"])
print(f" [{s['id']:>3}] {s['shortName']:>15}: {s['compoundId']}")
if len(seen) >= 8:
break
# Sildenafil (CHEMBL192): 272 source records across 19 databases
查询 3:连通性搜索——结构相关化合物
查找核心结构相同、但立体化学、盐形式、同位素标记或质子化状态不同的化合物。端点为 POST /connectivity,接受完整的标准 InChIKey(API 会对 14 字符片段返回 404 Not found)。UniChem 内部使用连通性层进行匹配,但返回的命中结果可能在立体化学/电荷/同位素层有所不同。
与 /compounds 不同,连通性响应会返回一个扁平的 sources 列表(包含所有相关化合物在各数据库中的命中结果,每个命中一条),以及查询到的 searchedCompound 和 totalCompounds/totalSources 汇总字段。每个命中中的 comparison 字典会显示哪些 InChI 层匹配。
import requests, pandas as pd
UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
def connectivity_search(inchikey: str) -> dict:
"""Find all compounds related by InChI connectivity (same skeleton, possibly different stereo/salts).
Pass a FULL standard InChIKey (27 chars). The server rejects 14-char fragments.
"""
body = {"type": "inchikey", "compound": inchikey}
r = requests.post(f"{UNICHEM_API}/connectivity", json=body, timeout=30)
r.raise_for_status()
return r.json()
# Warfarin: find all stereoforms, racemates, and salt forms
warfarin_inchikey = "PJVWKTKQMONHTI-UHFFFAOYSA-N"
data = connectivity_search(warfarin_inchikey)
print(f"Warfarin connectivity relatives: {data['totalCompounds']} unique compounds, "
f"{data['totalSources']} database records")
hits = data.get("sources", [])
by_source = {}
for h in hits:
by_source.setdefault(h["shortName"], []).append(h["compoundId"])
for name, ids in sorted(by_source.items(), key=lambda kv: -len(kv[1]))[:8]:
print(f" {name:>15}: {len(ids):>4} IDs (e.g. {ids[0]})")
# Warfarin connectivity relatives: 13 unique compounds, 353 database records
# Compare source coverage across connectivity relatives
def compare_coverage(inchikey: str) -> pd.DataFrame:
"""Show connectivity relatives split by their source-database coverage."""
data = connectivity_search(inchikey)
rows = []
for src in data.get("sources", []):
rows.append({
"source_id": src["id"],
"source_name": src["shortName"],
"compound_id": src["compoundId"],
"stereo_match": src["comparison"].get("stereoSp3", False),
"salt_match": src["comparison"].get("protonation", False),
})
return pd.DataFrame(rows)
df = compare_coverage("PJVWKTKQMONHTI-UHFFFAOYSA-N")
print(f"Total records: {len(df)}")
print(df.head(10).to_string(index=False))
print(f"Records with stereo mismatch (skeleton matches, stereo differs): {(~df['stereo_match']).sum()}")
查询 4:列出所有数据来源
获取 UniChem 数据来源的完整列表,包括其 ID、名称、描述和网站 URL。这是唯一通过 GET 提供的端点。每个来源条目中的响应字段使用 sourceID(大写 ID)。
import requests, pandas as pd
UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
def list_sources() -> pd.DataFrame:
"""Return all UniChem data sources as a DataFrame."""
r = requests.get(f"{UNICHEM_API}/sources", timeout=15)
r.raise_for_status()
sources = r.json().get("sources", [])
rows = []
for s in sources:
rows.append({
"source_id": s["sourceID"],
"name": s.get("nameLong") or s.get("nameLabel", ""),
"label": s.get("nameLabel", ""),
"short_name": s.get("name", ""),
"uci_count": s.get("UCICount"),
"url": s.get("baseIdUrl", ""),
})
return pd.DataFrame(rows).sort_values("source_id")
sources_df = list_sources()
print(f"Total UniChem sources: {len(sources_df)}")
print(sources_df[["source_id", "label", "uci_count"]].to_string(index=False))
# Total UniChem sources: 23
# 1 ChEMBL 2854815
# 2 DrugBank 14622
# 22 PubChem 123392679
查询 5:逐个化合物循环(无批量端点)
UniChem 不支持列表批量处理形式——/compounds POST 请求每次只能接受一个化合物。提交 {compounds: [...]} 或 {inchikeys: [...]} 会返回 400 illegal_argument_exception。对于多个输入,请逐个处理,并短暂休眠,以遵守约 10 req/s 的速率限制。
import requests, time, pandas as pd
UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
def batch_translate(inchikeys: list[str],
target_source_ids=(1, 2, 7, 22)) -> pd.DataFrame:
"""Translate a list of InChIKeys to IDs in multiple target databases.
Loops one POST per InChIKey (UniChem has no list-batch endpoint).
"""
SOURCE_NAMES = {1: "chembl", 2: "drugbank", 3: "pdb", 7: "chebi",
14: "fda_srs", 22: "pubchem", 34: "drugcentral"}
rows = []
for ik in inchikeys:
row = {"inchikey": ik}
for sid in target_source_ids:
row[SOURCE_NAMES.get(sid, f"src_{sid}")] = None
try:
r = requests.post(f"{UNICHEM_API}/compounds",
json={"type": "inchikey", "compound": ik}, timeout=20)
r.raise_for_status()
compounds = r.json().get("compounds", [])
if compounds:
for src in compounds[0].get("sources", []):
if src["id"] in target_source_ids:
col = SOURCE_NAMES.get(src["id"], f"src_{src['id']}")
if row[col] is None:
row[col] = src["compoundId"]
except requests.RequestException as e:
row["error"] = str(e)
rows.append(row)
time.sleep(0.1) # respect ~10 req/s rate limit
return pd.DataFrame(rows)
# Translate a set of NSAIDs by InChIKey
nsaid_inchikeys = [
"BSYNRYMUTXBXSQ-UHFFFAOYSA-N", # aspirin
"HEFNNWSXXWATRW-UHFFFAOYSA-N", # ibuprofen
"CMWTZPSULFXXJA-VIFPVBQESA-N", # naproxen
"DCOPUUMXTXDBNB-UHFFFAOYSA-N", # diclofenac (free acid)
]
df = batch_translate(nsaid_inchikeys, target_source_ids=[1, 2, 7, 22])
print(df.to_string(index=False))
df.to_csv("nsaid_xrefs.csv", index=False)
print(f"Saved nsaid_xrefs.csv ({len(df)} compounds)")
查询 6:使用来源 ID 输入逐个化合物循环
如果起始标识符不是 InChIKey,而是特定来源的 ID(例如来自生物活性表的一组 ChEMBL ID),请使用 type=sourceID 并逐个循环,同样每个 ID 发送一个 POST 请求。
import requests, time
UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
def translate_source_ids(ids: list[str], from_source: int,
to_sources=(2, 22, 34)) -> list[dict]:
"""Translate IDs in one source database to IDs in target databases.
Args:
ids: list of compound IDs in the from_source database
from_source: source ID of the input list (1=ChEMBL, 22=PubChem, ...)
to_sources: iterable of target source IDs to extract
"""
out = []
for cid in ids:
body = {"type": "sourceID", "compound": cid, "sourceID": from_source}
r = requests.post(f"{UNICHEM_API}/compounds", json=body, timeout=20)
row = {"input": cid}
if r.ok:
compounds = r.json().get("compounds", [])
if compounds:
row["inchikey"] = compounds[0].get("standardInchiKey")
hits = {s["id"]: s["compoundId"] for s in compounds[0].get("sources", [])}
for tsid in to_sources:
row[f"src_{tsid}"] = hits.get(tsid)
out.append(row)
time.sleep(0.1)
return out
# Three kinase inhibitors known by ChEMBL ID
chembl_inputs = ["CHEMBL535", "CHEMBL553", "CHEMBL941"] # nilotinib, dasatinib, imatinib
rows = translate_source_ids(chembl_inputs, from_source=1, to_sources=(2, 22, 34))
for row in rows:
ik = (row.get("inchikey") or "?")[:14]
print(f"{row['input']:>10} ik={ik}... "
f"DrugBank={row.get('src_2')}, PubChem={row.get('src_22')}, "
f"DrugCentral={row.get('src_34')}")
核心概念
InChI 与 InChIKey
UniChem 使用 InChI(IUPAC 国际化学标识符)及其哈希形式 InChIKey 作为化合物的标准标识。InChIKey 是由 27 个字符组成的字符串,分为三个部分:前 14 个字符编码连接层(重原子和键),接下来的 8 个字符编码立体化学和电荷,最后一个字符是版本标记。UniChem 通过要求标准 InChIKey 完全一致来交叉引用化合物,确保不同数据库中的化学实体相同。
来源 ID 参考表(已通过 /sources 实时验证)
| 来源 ID | 数据库 | 范围 |
|---|---|---|
| 1 | ChEMBL | 生物活性分子、药物发现 |
| 2 | DrugBank | 已批准药物、药理学 |
| 3 | RCSB PDB | 晶体结构中的配体(美国) |
| 4 | Guide to Pharmacology | 药理学靶标/配体 |
| 5 | PDBe | 晶体结构中的配体(欧洲) |
| 7 | ChEBI | 化学本体、代谢物 |
| 14 | FDA SRS | FDA 物质注册系统 |
| 15 | SureChEMBL | 专利化学 |
| 18 | HMDB | 人类代谢组数据库 |
| 22 | PubChem | 通用化合物库 |
| 31 | BindingDB | 结合亲和力数据 |
| 32 | CompTox | 环境毒理学仪表板 |
| 33 | LIPID MAPS | 脂质结构 |
| 34 | DrugCentral | 已批准药物 + 药理学 |
| 37 | BRENDA | 酶底物/产物 |
| 38 | Rhea | 生化反应 |
| 41 | SwissLipids | 脂质结构 |
| 49 | Probes-and-Drugs | 化学探针 |
字段命名:id 与 sourceID
这是编写 UniChem 脚本时最常见的错误。两个端点对来源数据库标识符使用不同的字段名:
- GET /sources 将数据库列为对象,并使用 sourceID 字段(ID 大写)。
- POST /compounds 和 POST /connectivity 会在 sources 列表中返回每个数据库的匹配项,其中来源标识符使用普通的 id 字段。这些匹配记录中没有 sourceID 或 sourceId 键。
遍历化合物或连接性响应时,始终使用 src["id"];遍历 /sources 目录时,使用 s["sourceID"]。
连接性匹配与标准 InChIKey 匹配
POST /compounds 返回精确的 InChIKey 匹配项(立体化学、盐、同位素均相同)。POST /connectivity 返回所有键拓扑相同的化合物——可用于查找外消旋体、立体异构体、游离酸/碱以及共晶搭档。连接性响应会为每个匹配项提供一个比较字典,指出哪些 InChI 层匹配(stereoSp3、protonation、isotope 等);可用它筛选骨架相同、仅立体化学不同的相关化合物。
常见工作流
工作流 1:药物化合物交叉引用报告
目标:给定一组药物名称(或 ChEMBL ID),解析出所有主要数据库 ID 并导出为 CSV。
import requests, time, pandas as pd
UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
CHEMBL_API = "https://www.ebi.ac.uk/chembl/api/data"
SOURCE_NAMES = {1: "chembl", 2: "drugbank", 3: "rcsb_pdb",
7: "chebi", 22: "pubchem", 14: "fda_srs", 34: "drugcentral"}
def chembl_to_inchikey(chembl_id: str) -> str | None:
"""Look up the standard InChIKey for a ChEMBL compound ID."""
r = requests.get(f"{CHEMBL_API}/molecule/{chembl_id}.json", timeout=15)
if r.status_code == 404:
return None
r.raise_for_status()
return r.json().get("molecule_structures", {}).get("standard_inchi_key")
def inchikey_to_sources(inchikey: str) -> dict:
"""Return source_id -> compound_id dict for an InChIKey (first hit per source)."""
r = requests.post(f"{UNICHEM_API}/compounds",
json={"type": "inchikey", "compound": inchikey}, timeout=20)
r.raise_for_status()
compounds = r.json().get("compounds", [])
if not compounds:
return {}
out = {}
for s in compounds[0].get("sources", []):
out.setdefault(s["id"], s["compoundId"])
return out
# Example: top cardiovascular drugs
drug_chembl_ids = {
"atorvastatin": "CHEMBL1487",
"lisinopril": "CHEMBL1237",
"metoprolol": "CHEMBL13",
"amlodipine": "CHEMBL1491",
"warfarin": "CHEMBL1464",
}
rows = []
for name, chembl_id in drug_chembl_ids.items():
ik = chembl_to_inchikey(chembl_id)
row = {"drug": name, "chembl_id": chembl_id, "inchikey": ik}
if ik:
srcs = inchikey_to_sources(ik)
for sid, col in SOURCE_NAMES.items():
row[col] = srcs.get(sid)
rows.append(row)
time.sleep(0.2)
df = pd.DataFrame(rows)
df.to_csv("drug_xrefs.csv", index=False)
print(df[["drug", "chembl", "drugbank", "pubchem", "chebi"]].to_string(index=False))
print(f"Saved drug_xrefs.csv ({len(df)} drugs)")
工作流 2:结构相关化合物发现与可视化
目标:查找某个化合物的所有结构相关化合物,汇总其数据库覆盖情况,并绘制条形图展示来源分布。
import requests, pandas as pd
import matplotlib.pyplot as plt
from collections import Counter
UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
SOURCE_NAMES = {1: "ChEMBL", 2: "DrugBank", 3: "RCSB PDB", 5: "PDBe",
7: "ChEBI", 14: "FDA SRS", 15: "SureChEMBL",
22: "PubChem", 31: "BindingDB", 34: "DrugCentral"}
# Aspirin connectivity relatives (covers acetylsalicylate salts)
query_inchikey = "BSYNRYMUTXBXSQ-UHFFFAOYSA-N"
r = requests.post(f"{UNICHEM_API}/connectivity",
json={"type": "inchikey", "compound": query_inchikey}, timeout=30)
r.raise_for_status()
data = r.json()
hits = data.get("sources", [])
print(f"Aspirin connectivity relatives: {data['totalCompounds']} unique compounds, "
f"{len(hits)} database records")
# Count how often each named database appears
source_counter = Counter()
for h in hits:
if h["id"] in SOURCE_NAMES:
source_counter[SOURCE_NAMES[h["id"]]] += 1
labels = [k for k, _ in source_counter.most_common()]
counts = [v for _, v in source_counter.most_common()]
fig, ax = plt.subplots(figsize=(9, 4))
bars = ax.bar(labels, counts, color="#2E86AB", edgecolor="white")
ax.bar_label(bars, padding=2)
ax.set_xlabel("Database")
ax.set_ylabel("Number of Source Records (relatives x hits)")
ax.set_title("UniChem Connectivity Records - Aspirin Skeleton")
plt.xticks(rotation=30, ha="right")
plt.tight_layout()
plt.savefig("unichem_connectivity_coverage.png", dpi=150, bbox_inches="tight")
print("Saved unichem_connectivity_coverage.png")
plt.close(fig)
# DataFrame summary by source
df = pd.DataFrame(source_counter.most_common(), columns=["database", "records"])
print(df.to_string(index=False))
工作流 3:将 ChEMBL 生物活性数据与 PubChem CID 合并
目标:为 ChEMBL 生物活性表补充 PubChem CID,以便在使用 PubChem 标识符的工具中进行后续分析。
import requests, time, pandas as pd
UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
def add_pubchem_cids(df: pd.DataFrame,
inchikey_col: str = "standard_inchi_key") -> pd.DataFrame:
"""Add pubchem_cid column to a DataFrame that has an InChIKey column."""
unique_keys = df[inchikey_col].dropna().unique()
mapping = {}
for ik in unique_keys:
try:
r = requests.post(f"{UNICHEM_API}/compounds",
json={"type": "inchikey", "compound": ik}, timeout=15)
r.raise_for_status()
compounds = r.json().get("compounds", [])
if compounds:
for src in compounds[0].get("sources", []):
if src["id"] == 22: # PubChem
mapping[ik] = src["compoundId"]
break
except requests.RequestException:
pass
time.sleep(0.1)
df = df.copy()
df["pubchem_cid"] = df[inchikey_col].map(mapping)
return df
# Simulate a small ChEMBL activity table
chembl_data = pd.DataFrame({
"compound_name": ["aspirin", "ibuprofen", "naproxen"],
"standard_inchi_key": [
"BSYNRYMUTXBXSQ-UHFFFAOYSA-N",
"HEFNNWSXXWATRW-UHFFFAOYSA-N",
"CMWTZPSULFXXJA-VIFPVBQESA-N",
],
"ic50_nm": [2500.0, 13000.0, 1600.0],
})
enriched = add_pubchem_cids(chembl_data)
print(enriched[["compound_name", "ic50_nm", "pubchem_cid"]].to_string(index=False))
enriched.to_csv("chembl_with_pubchem.csv", index=False)
print("Saved chembl_with_pubchem.csv")
关键参数
| 参数 | 端点 | 默认值 | 范围 / 选项 | 效果 |
|---|---|---|---|---|
| type(请求体) | POST /compounds, POST /connectivity | - | inchikey, sourceID | 选择 compound 中标识符的类型。inchikey 需要 27 个字符的标准 InChIKey;sourceID 还需要数值型 sourceID 字段。 |
| compound(请求体) | POST /compounds, POST /connectivity | - | 单个字符串(不能是列表) | 要查询的标识符。UniChem 不支持列表批量格式;每个 compound 循环发送一次 POST 请求。 |
| sourceID(请求体) | POST /compounds(type=sourceID 时) | - | 来自 /sources 的整数数据库 ID | 输入 compound 所属的源数据库(1=ChEMBL,2=DrugBank,22=PubChem,……)。 |
| 单条命中记录的 id(响应) | POST /compounds, POST /connectivity | - | 整数 | sources 中每条命中记录对应的源数据库 ID。这些单条命中记录请使用 src["id"],而不是 src["sourceID"]。 |
| sourceID(响应) | GET /sources | - | 整数 | 目录中每个数据库条目的数字 ID(大写 ID)。 |
| timeout | 所有请求 | 20s | 任意正整数 | 请求失败前等待的秒数;对常见骨架的 /connectivity 请求,可将其提高到 30s。 |
最佳实践
-
除 /sources 外一律使用 POST:GET /compounds 返回 405 Method Not Allowed;GET /compounds/{src}/{id} 和 GET /connectivity/{key} 返回 404。唯一的 GET 端点是 /sources。始终使用 JSON 请求体发送 POST 请求。
-
使用正确的字段 - 命中记录用 id,/sources 用 sourceID:在 POST /compounds 和 POST /connectivity 返回的 sources 列表中,每条记录的源数据库位于 id 键中。读取 src["sourceID"] 会引发 KeyError。大写 ID 字段 sourceID 仅出现在 GET /sources 返回的目录中。
-
使用标准 InChIKey(而非非标准 InChIKey):UniChem 对标准 InChIKey 建立了索引。非标准 InChIKey 不会返回结果。请使用以下代码验证:from rdkit.Chem.inchi import MolToInchiKey; MolToInchiKey(mol)。
-
精确匹配失败时,改用 connectivity 搜索:如果 DrugBank 中的 compound 是盐(例如盐酸盐),而你持有的是游离碱的 InChIKey,那么标准查询会找不到结果。可使用 connectivity 搜索作为药物交叉引用的备用方案。
-
向 /connectivity 传入完整的 InChIKey:该端点要求完整的 27 个字符的标准 InChIKey。只提交 14 个字符的连接性片段会返回 {response: Not found}。UniChem 会在内部去除立体化学 / 电荷层。
-
启动时缓存来源列表:调用一次 /sources 并构建 {sourceID: nameLabel} 字典,而不是将 ID 硬编码。
def load_source_map() -> dict:
r = requests.get(f"{UNICHEM_API}/sources", timeout=15)
r.raise_for_status()
return {s["sourceID"]: s.get("nameLabel", str(s["sourceID"]))
for s in r.json().get("sources", [])}
- 没有批量端点 - 循环请求并添加 sleep:提交 {compounds: [...]} 或 {inchikeys: [...]} 会返回 400 illegal_argument_exception。循环发送 POST 请求,并在调用之间执行 time.sleep(0.1)(约 10 req/s)。
常见用法
用法:将任意 Compound ID 解析为 InChIKey
适用场景:你有 PubChem CID 或 ChEBI ID,需要 InChIKey 来查询 UniChem 或其他服务。
import requests
UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
PUBCHEM_API = "https://pubchem.ncbi.nlm.nih.gov/rest/pug"
def pubchem_cid_to_inchikey(cid) -> str | None:
"""Resolve a PubChem CID to a standard InChIKey via PubChem."""
r = requests.get(f"{PUBCHEM_API}/compound/cid/{cid}/property/InChIKey/JSON", timeout=10)
if r.status_code == 404:
return None
r.raise_for_status()
props = r.json()["PropertyTable"]["Properties"]
return props[0]["InChIKey"] if props else None
def cid_to_all_sources(cid) -> list:
"""PubChem CID -> InChIKey -> UniChem cross-references."""
ik = pubchem_cid_to_inchikey(cid)
if not ik:
return []
r = requests.post(f"{UNICHEM_API}/compounds",
json={"type": "inchikey", "compound": ik}, timeout=20)
r.raise_for_status()
compounds = r.json().get("compounds", [])
return compounds[0].get("sources", []) if compounds else []
sources = cid_to_all_sources(2244) # PubChem CID for aspirin
distinct = {s["id"] for s in sources}
print(f"Aspirin (CID=2244) is in {len(distinct)} UniChem databases ({len(sources)} records)")
seen = set()
for s in sources:
if s["id"] in seen:
continue
seen.add(s["id"])
print(f" [{s['id']:>3}] {s['shortName']:>15}: {s['compoundId']}")
if len(seen) >= 6:
break
用法:检查 Compound 是否为已批准药物
适用场景:使用 InChIKey 快速标记 compound 是否出现在 DrugBank(来源 2)中。
import requests
UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
def is_approved_drug(inchikey: str) -> tuple[bool, str | None]:
"""Check if compound appears in DrugBank (id=2). Returns (is_drug, DrugBank_ID)."""
r = requests.post(f"{UNICHEM_API}/compounds",
json={"type": "inchikey", "compound": inchikey}, timeout=20)
r.raise_for_status()
compounds = r.json().get("compounds", [])
if not compounds:
return False, None
for src in compounds[0].get("sources", []):
if src["id"] == 2: # DrugBank
return True, src["compoundId"]
return False, None
# Test a few compounds
test_compounds = {
"aspirin": "BSYNRYMUTXBXSQ-UHFFFAOYSA-N",
"triclosan": "XEFQLINVKFYRCS-UHFFFAOYSA-N",
"sildenafil": "BNRNXUUZRGQAQC-UHFFFAOYSA-N",
}
for name, ik in test_compounds.items():
is_drug, db_id = is_approved_drug(ik)
status = f"DrugBank:{db_id}" if is_drug else "not in DrugBank"
print(f"{name:>12}: {status}")
用法:汇总一组 Compound 的来源覆盖情况
适用场景:审查哪些数据库收录了你的 compound 列表,这有助于决定后续分析使用哪个数据库。
import requests, time, pandas as pd
UNICHEM_API = "https://www.ebi.ac.uk/unichem/api/v1"
SOURCE_NAMES = {1: "ChEMBL", 2: "DrugBank", 3: "RCSB PDB", 7: "ChEBI",
14: "FDA SRS", 22: "PubChem", 34: "DrugCentral"}
def source_coverage_matrix(inchikeys: list[str]) -> pd.DataFrame:
"""Return a boolean matrix: rows=compounds, columns=databases."""
rows = []
for ik in inchikeys:
r = requests.post(f"{UNICHEM_API}/compounds",
json={"type": "inchikey", "compound": ik}, timeout=15)
row = {"inchikey": ik}
for sid, name in SOURCE_NAMES.items():
row[name] = False
if r.ok:
compounds = r.json().get("compounds", [])
if compounds:
ids_present = {s["id"] for s in compounds[0].get("sources", [])}
for sid, name in SOURCE_NAMES.items():
row[name] = sid in ids_present
rows.append(row)
time.sleep(0.1)
return pd.DataFrame(rows)
sample_keys = [
"BSYNRYMUTXBXSQ-UHFFFAOYSA-N", # aspirin
"HEFNNWSXXWATRW-UHFFFAOYSA-N", # ibuprofen
"XEFQLINVKFYRCS-UHFFFAOYSA-N", # triclosan
]
coverage = source_coverage_matrix(sample_keys)
print(coverage.to_string(index=False))
print("Coverage per database:")
for col in list(SOURCE_NAMES.values()):
print(f" {col}: {coverage[col].sum()}/{len(coverage)}")
故障排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 在 /compounds 上出现 405 Method Not Allowed | 使用 GET 请求访问端点 | UniChem /compounds 仅支持 POST。使用 requests.post(url, json={type:inchikey, compound:ik}) 发送请求。 |
| 每条命中记录出现 KeyError: sourceID(或 sourceId) | 读取了错误的字段名 | 对 compounds[0][sources] 中的命中项和 /connectivity 返回的来源列表,使用 src["id"]。sourceID 仅存在于 /sources 目录中。 |
| 出现 400 illegal_argument_exception field name is null or empty | 尝试使用 {compounds: [...]} 或 {inchikeys: [...]} 批量请求 | 不存在批量端点。请循环发送请求,并在 POST 请求之间调用 time.sleep(0.1)。 |
| /connectivity 返回 {response: Not found} | 仅传入了 14 个字符的片段 | 发送完整的 27 个字符的标准 InChIKey;UniChem 会在内部剥离立体化学层。 |
| 已知化合物的 compounds 列表为空 | InChIKey 非标准、盐形式不匹配,或缺少该化合物 | 使用 RDKit 验证 InChIKey;尝试 POST /connectivity,以捕获盐形式或立体化学变体。 |
| connectivity 返回的记录过多 | 同一骨架匹配到多个 SureChEMBL 专利 ID | 按 src["id"] 过滤以排除来源 15(SureChEMBL),或使用 comparison.stereoSp3 == True 过滤。 |
| requests.exceptions.Timeout | 负载较高时 API 响应缓慢 | 将 /connectivity 的超时时间延长至 30s;使用指数退避重试一次。 |
| 来源 URL 字段为空 | 并非所有来源都提供 URL 模板 | 使用 /sources 端点返回的 baseIdUrl 与 compoundId 手动构造链接。 |
相关技能
- chembl-database-bioactivity - 使用通过 UniChem 解析出的化合物 ID 查询 ChEMBL 的生物活性数据(IC50、Ki)
- pubchem-compound-search - 使用来自 UniChem 的 PubChem CID 查询完整的化合物属性和生物测定数据
- pdb-database - 使用通过 UniChem 解析出的 PDB 配体代码(来源 3 或 5)查找 3D 配体结构
- drugbank-database-access - 使用来自 UniChem 的 DrugBank ID 查询详细的药理学、ADMET 和药物相互作用数据
参考资料
- UniChem API 文档 - 官方 REST API 参考,包含所有端点说明
- UniChem 主页 - 交互式搜索界面和数据库列表
- Chambers 等,J Cheminform 2013 - 介绍基于 InChI 的交叉引用方法的 UniChem 原始论文
- InChI Trust - InChI 标准规范和算法文档