分子预处理:标准化、去重、去除盐离子、质子化状态调整

好,咱们进入正题。分子预处理,说白了就是把原始数据里的“毛刺”都剃干净。你想想看,从公开库、实验室、合作方拿到的分子结构,常常格式混乱、重复、带着乱七八糟的离子……直接扔进虚拟筛选,结果就是一锅粥。我刚开始做这块时,就吃过这个亏——跑了三天的大规模对接,最后发现一半分子根本就是重复的,另一半因为质子化状态不对,结合能全算偏了。嗯,从那以后,我再也不敢跳过预处理了。

下面我会按四个关键步骤展开:标准化、去重、去除盐离子、质子化状态调整。每一步我都给你讲清楚为什么做、怎么做,以及常见坑。

4.1 标准化

标准化的目标只有一个:让所有分子用同一种语言说话。比如硝基,有的文件写 N(=O)=O,有的写 N(=O)O,还有的写成 [N+](=O)[O-]……不同表示方式的硝基,在对接打分函数里会得到完全不同的结果。标准化就是把这些差异统一掉。

我个人习惯用 RDKit 的 SanitizeMol 或者 Open Babel 的 –canonical 选项。下面给个简单的 Python 示例:

from rdkit import Chem

def standardize_smiles(smiles):
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return None
    # 标准化:包含芳香性、价键、电荷、同位素等
    Chem.SanitizeMol(mol)
    # 输出标准 SMILES
    return Chem.MolToSmiles(mol)

# 示例
print(standardize_smiles("c1ccccc1"))   # 苯
print(standardize_smiles("N(=O)=O"))   # 硝基
标准化不只是 SMILES 格式统一,还包括:
  • 芳香性表示:Kekulé vs 交替单双键
  • 显式/隐式氢:比如 CH3 与 C(显式 H)
  • 立体化学标记:@ 与 @@ 的规范
  • 同位素:有些库会把碳-12 写成 [12C],有些写 [C]
如果忽视这些,后面的去重和对接都会出问题。

4.2 去重

去重听起来简单,但实际操作时很坑。你以为 SMILES 一样就是同一个分子?不一定。我遇到过同一个化合物,因为不同供应商给的 SMILES 用不同价键方式,导致 RDKit 解析后互相不认为相等。

推荐做法:先用标准化,再用 InChI 或规范 SMILES 做键值去重。 InChI 是一种更稳健的分子标识符,对互变异构体也有处理。

from rdkit import Chem

def deduplicate_mols(mol_list):
    seen = set()
    unique = []
    for mol in mol_list:
        inchi = Chem.MolToInchi(mol)
        if inchi and inchi not in seen:
            seen.add(inchi)
            unique.append(mol)
    return unique
小提示:如果你的分子库非常大(比如几百万),用 Python 列表去重会很慢。我通常用 pandas 的 drop_duplicates 配合 InChI 列,或者直接对 SDF 文件用 obabel 做预处理:obabel input.sdf -O output.sdf -d(-d 表示去重)。

4.3 去除盐离子

虚拟筛选的靶点通常只结合中性有机分子。如果一个 SDF 文件里写了“CCO.[Na+]”,对接时对接口袋里嵌着一个钠离子,这个打分能靠谱吗?

所以,必须去掉盐离子、溶剂分子、金属离子。常见做法是用 RDKit 的 SaltRemover,或者手动定义黑名单。

from rdkit.Chem import SaltRemover

remover = SaltRemover.SaltRemover()
mol = Chem.MolFromSmiles("CCO.[Na+]")
cleaned_mol = remover.StripMol(mol)
print(Chem.MolToSmiles(cleaned_mol))  # 输出:CCO

我个人习惯用 Open Babel 做批量处理,特别是处理上百万条记录时:

obabel huge.sdf -O stripped.sdf --remove salt
注意:去除盐离子后,原来配合的阴离子(如 Cl⁻、NO₃⁻)也会被去掉。如果你的分子本身就带负电(比如羧酸根),千万不要把因电荷平衡而存在的钠盐去掉后,还保留羧酸根的形式——应当先做质子化调整(见 4.4),让分子在生理 pH 下保持正确电荷,再去掉多余的反离子。

4.4 质子化状态调整

这个步骤我认为是预处理里最容易翻车的一环。同一个分子,在中性 pH 和酸性 pH 下,谷氨酸可能带负电(去质子化),组氨酸可能带正电(双质子化)……对接打分函数是基于固定 pH 的力场参数,如果电荷状态错了,结合能可以差好几个数量级。

我建议使用 计算 pKa 的方法来确定每个可电离基团在目标 pH(一般设为 7.4)下的主要形式。常用工具包括:

工具 原理 适合场景
RDKit + pKa 模型 基于局部结构经验规则 快速粗筛,几百万分子
Open Babel –p pH 调用内置 pKa 数据库 中等规模,命令行友好
MoKa / ChemAxon 高精度 QSPR 模型 精细筛选,几百到几千分子
Dimorphite-DL 深度学习 pKa 预测 新兴方法,无商业许可问题

下面是用 Open Babel 做质子化调整的命令示例(pH 7.4):

obabel input.sdf -O protonated.sdf --correct \
    --addpolarhydrogen --ph 7.4

如果用 RDKit + Dimorphite-DL,可以这样:

from dimorphite_dl import DimorphiteDL
from rdkit import Chem

def adjust_protonation(smiles, ph=7.4):
    dl = DimorphiteDL()
    result = dl.protonate(smiles, ph=ph)
    mol = Chem.MolFromSmiles(result[0])
    return mol
别忘了终检查和可视化:处理完后,我习惯随机抽样 100~200 个分子,用 PyMOL 或 RDKit 的 Draw.MolToImage 看一眼质子化形式是否正确。特别是那些含组氨酸、半胱氨酸、巯基的特殊分子。

本章核心流程图

下面这张图总结了分子预处理的标准流程,以及各步骤的主要工具和输出:

分子预处理核心流程 分子预处理流程 原始分子库(SDF/SMILES) 标准化(SanitizeMol / obabel --canonical) 去重(InChI / Canonical SMILES) 去除盐离子(SaltRemover / --remove salt) 质子化调整(--ph 7.4 / Dimorphite-DL) 预处理后分子库(可用于对接/筛选)
实用流水线建议:如果分子量超过一千万,我推荐直接写一个 Bash 脚本串联 Open Babel 命令,配合 parallel 并行处理。Python 脚本虽然灵活,但在 IO 和内存管理上不如原生 C++ 工具快。我曾经用 RDKit 处理 500 万分子,内存直接爆了;换成 obabel 分批处理,只用了 3 小时。

总结

这四个步骤——标准化、去重、去除盐离子、质子化调整——是大规模虚拟筛选绝对不能跳过的预处理环节。跳过任何一个,后面的对接结果都可能被污染。我个人经验是,花在预处理上的时间,至少要占整个 pipeline 开发时间的 20%。

最后再啰嗦一句:预处理完一定要做质控。至少跑一次小规模验证——拿几百个分子对接,检查结果是否符合化学直觉。如果发现离谱的得分,大概率是前面某个步骤出了问题。