4. 化学信息学工具:RDKit安装与基本使用、分子读取与操作

聊到分子指纹,RDKit 是绕不过去的工具。说实话,我当初刚接触化学信息学的时候,第一个装的库就是它。RDKit 在药物筛选圈子里几乎是标配——处理分子、算指纹、做相似性比较,它都能搞定。我们这一章就先把 RDKit 装好,然后上手读写分子文件,再熟悉一些基础的分子操作方法。

4.1 安装 RDKit

安装 RDKit 我推荐直接用 conda。它的底层依赖(比如 Boost)比较麻烦,用 conda 能省掉一堆编译的麻烦。我个人习惯是专门为 RDKit 建一个环境,避免和其他库打架。

conda create -n rdkit_env python=3.9
conda activate rdkit_env
conda install -c conda-forge rdkit

等一下,如果你是 Windows 用户,装完后最好跑一下这个命令验证是否成功:

python -c "from rdkit import Chem; print(Chem.__version__)"

如果能打出版本号,恭喜你,环境搭好了。我后来碰到过一个坑:在 macOS 上如果用了 M1 芯片,conda 的默认 channel 可能会装到 x86 版本,效率大打折扣。建议指定 osx-arm64 的 channel,或者直接用 miniforge。

小提示:如果你在公司内网或代理环境下安装,conda 容易超时。试试 conda config --set remote_read_timeout_secs 300,把超时时间拉长。

4.2 读取与写入分子文件

RDKit 支持的分子格式挺多的,日常最常用的就是 SDF 和 SMILES。我刚开始做项目时,手头有一批供应商的 SDF 文件,里面某些分子的原子坐标是乱的。嗯,之后被「坑」多了,就慢慢练出手感了。

4.2.1 从 SMILES 构建分子

from rdkit import Chem

# 从 SMILES 读取单个分子
smiles = "c1ccccc1C(=O)O"
mol = Chem.MolFromSmiles(smiles)
if mol is not None:
    print("分子读取成功,原子数:", mol.GetNumAtoms())
else:
    print("SMILES 格式有误,请检查")

这里要注意,Chem.MolFromSmiles 如果解析失败会返回 None。我见过不少新手直接拿 None 往下算指纹,结果程序直接崩溃。所以你想想看,加个判空处理能省很多事。

4.2.2 从 SDF 文件读取

SDF 常用于存储 3D 构象或带属性的分子。RDKit 提供了批量读取的接口:

supplier = Chem.SDMolSupplier("ligands.sdf")
for i, mol in enumerate(supplier):
    if mol is None:
        continue
    print(f"分子 {i}: {mol.GetNumAtoms()} 个原子")

SDMolSupplier 是个生成器,懒加载,不会一下子把所有分子读进内存。这一点对于处理上百万个分子的大文件特别友好。不过,SDF 里有些分子格式不规范,比如氢原子数写错,会导致 mol 为 None。我建议加上跳过逻辑,避免只因为一个坏分子整个作业崩掉。

4.2.3 写入分子到文件

把处理好的分子写出去也是家常便饭:

# 写 SDF
writer = Chem.SDWriter("output.sdf")
for mol in mol_list:
    writer.write(mol)
writer.close()

# 写 SMILES
with open("output.smi", "w") as f:
    for mol in mol_list:
        smi = Chem.MolToSmiles(mol)
        f.write(smi + "\n")

写 SDF 时记得调用 close(),不然文件可能不完整。我在一次批量虚拟筛选后就吃过这个亏,凌晨跑完一看文件是空的,气得我直接加了 with 语句管理上下文。

4.3 分子基础操作

拿到分子对象后,我们常需要做一些操作,比如加氢、删氢、计算属性、子结构搜索。下面是我在项目中用得最多的几个。

4.3.1 显式氢与隐式氢

RDKit 默认的分子对象里氢原子是隐式的。如果需要做 3D 优化或对接,一般先要把氢显式化:

mol = Chem.MolFromSmiles("CCO")
mol_with_H = Chem.AddHs(mol)
print("隐式氢模型原子数:", mol.GetNumAtoms())      # 3
print("显式氢模型原子数:", mol_with_H.GetNumAtoms()) # 9

反过来,做完 3D 工作后,一般要删掉显式氢再存盘或算指纹:

mol_no_H = Chem.RemoveHs(mol_with_H)
当心:AddHs 和 RemoveHs 的操作不是完全可逆的。有些磺酸基、季铵盐在加氢删氢过程中可能会丢掉正确的质子化状态。我建议你在加氢前先把分子的电荷信息写进属性里,后面好做校验。

4.3.2 计算分子属性

RDKit 自带一系列快速计算分子描述符的方法。这里列几个我在筛选 pipeline 里常用的:

属性 函数 说明
分子量 Descriptors.MolWt 精确质量,常用于 ADMET 初筛
LogP Descriptors.MolLogP 脂水分配系数,过高容易毒性大
氢键供体/受体 Descriptors.NumHDonors / NumHAcceptors Lipinski 五规则的核心指标
可旋转键 Descriptors.NumRotatableBonds 影响分子柔性,口服候选一般 ≤ 10
TPSA Descriptors.TPSA 极性表面积,反映透膜能力
from rdkit.Chem import Descriptors

mol = Chem.MolFromSmiles("c1ccccc1C(=O)O")
mw = Descriptors.MolWt(mol)
logp = Descriptors.MolLogP(mol)
hbd = Descriptors.NumHDonors(mol)
hba = Descriptors.NumHAcceptors(mol)
print(f"MW: {mw:.1f}  LogP: {logp:.2f}  HBD: {hbd}  HBA: {hba}")

4.3.3 子结构搜索与匹配

药物筛选里常要判断分子是否包含某个药效团或警报结构(比如 PAINS)。RDKit 提供了子结构匹配:

pattern = Chem.MolFromSmarts("[$(C(=O)O)]")  # 羧基
if mol.HasSubstructMatch(pattern):
    print("分子中包含羧基")
    match_atoms = mol.GetSubstructMatch(pattern)
    print("匹配的原子索引:", match_atoms)

这里用的是 SMARTS 模式,比 SMILES 灵活很多。我常常用它来批量标记分子里的「问题片段」。比如 PAINS 过滤,把几十个警报 SMARTS 写成一个列表,一行代码扫过去,能做到秒级过滤。

4.4 核心流程与知识结构

下面这张 SVG 图展示了本章的 RDKit 操作流程,从读取分子到输出结果的完整路径:

RDKit 分子读取与操作流程图 SMILES / SDF / SMI MolFromSmiles / SDMolSupplier RDKit Mol 对象 AddHs / RemoveHs Descriptors.MolWt / LogP / TPSA … HasSubstructMatch / SMARTS 写出 SDF / SMILES / 属性表 每次读取后都要判空,防止异常分子往下游传递

4.5 实践中容易踩的坑

最后聊几个我实际经历过的教训,希望对你有帮助:

  • 芳香性感知不一致:RDKit 默认的芳香性模型可能和第三方工具(比如 Open Babel)不同。同一根键可能在 RDKit 里是芳香键,在其他工具里是单双键交替。如果你需要跨工具交换数据,最好统一用 Kekulize 后的形式。
  • 立体化学丢失:从 SDF 读取时,如果原子坐标不够精确或缺少顺反标记,RDKit 可能无法正确识别手性。我曾经遇到过一组手性分子,跑完筛选发现有一半的 R/S 反了,检查才发现源文件里已经丢掉了立体信息。我建议在读入后打印一下 Mol.GetStereo 做快速验证。
  • 分子无效返回 None 但不报错:SDMolSupplier 对格式有问题的分子只是跳过,不会抛异常。我曾经有 5 万分子跑完筛选后,猛然发现实际只处理了 4.8 万。后来我加了一段计数代码:
total = 0
good = 0
for mol in supplier:
    total += 1
    if mol is not None:
        good += 1
print(f"总分子数:{total},成功读取:{good},丢失:{total - good}")

这么一查,就发现了那个带坏头的 SDF 文件。嗯,从那以后,我的 pipeline 里一直保留着这段日志。