4. RdKit 分子对象操作:核心三板斧
好,我们正式进入实战环节。前面几章我们把图论基础过了一遍,也大致了解了分子图的抽象。但说到底,你得先把一个真实的分子弄进 Python 环境里,才能往下做图构建。RdKit 就是干这个的——它把 SMILES 字符串变成分子对象,反过来也行。
我个人最早接触 RdKit 时,觉得它像个瑞士军刀。随手一行代码,就能把结构式往里塞。但要注意,不是所有字符串都能被解析。我踩过的坑是 SMILES 格式不标准,比如少括号、缺手性标记,RdKit 直接返回 None,你连警告都没有。
4.1 Chem.MolFromSmiles:把字符串变分子
这是最常用的入口函数。你把一个 SMILES 字符串传进去,它给你返回一个 rdkit.Chem.rdmol.Mol 对象。如果 SMILES 非法,返回 None。
from rdkit import Chem
smiles = "CCO" # 乙醇
mol = Chem.MolFromSmiles(smiles)
print(type(mol)) # <class 'rdkit.Chem.rdmol.Mol'>
当然,真实场景往往没那么干净。我在项目中处理过百万级的化合物库,里面有大量带空格、带中文注释的 CSV 行。你记得先做清理:
# 常见坑:SMILES 里带了多余引号或括号不匹配
raw = '"CCO"'
clean = raw.replace('"', '')
mol = Chem.MolFromSmiles(clean)
if mol is None:
# 记录日志,不要直接跳过
print(f"不合法 SMILES: {clean}")
为什么会返回 None?我总结了几种情况:
| 原因 | 示例 | 提示 |
|---|---|---|
| 元素符号拼写错误 | CCCK(钾应写为 K 但大写?括号缺失) | 检查原子价态 |
| 括号不匹配 | C(CC | 用 rdkit.Chem.CanonSmiles 校验 |
| 手性标记乱用 | C[C@@H] 没有正确连接 | 参考 IUPAC 格式 |
| 太长的链式结构导致递归深层 | 超 10 万个原子 | 分段加载 |
对了,RdKit 内部还会做氢的自动补全。你得到的 Mol 对象默认带有隐式氢,但显式氢是不会自动加的。如果你做图神经网络需要显式氢,记得调用 Chem.AddHs(mol)。我个人习惯在特征提取前加一次,因为很多图卷积需要氢节点。
4.2 Chem.MolToSmiles:把分子变字符串
反向操作。很多时候你从 RDKit 操作完一个分子、修改完原子或键,需要把它导出为规范 SMILES 来存数据库或做对接。用 MolToSmiles 就行。
mol = Chem.MolFromSmiles("C(=O)O")
smiles_out = Chem.MolToSmiles(mol)
print(smiles_out) # "C(=O)O" 还是 "OC=O"?
嗯,这就有意思了。输出的 SMILES 不一定是输入的原样。RdKit 会按自己的规则重排——它会把原子编号按某种顺序写出来。如果你想要 canonical(规范)形式,默认就是。但如果你要保留手性或特定分支顺序,得加参数:
# 保留原有顺序(如果可能)
smiles_kek = Chem.MolToSmiles(mol, kekuleSmiles=True)
# 不带芳香性标记
smiles_all = Chem.MolToSmiles(mol, allBondsExplicit=True)
我记得有一次做分子指纹比对,发现两个完全相同结构的分子指纹不一致,查了半天发现是 SMILES 序列化方式不同。所以做指纹或图网络时,建议统一用 canonical SMILES,否则你的图结构看似一样,但节点的顺序变了,图匹配可能出问题。
str 处理就行。4.3 分子属性获取:比你想的丰富
一个 Mol 对象里藏着大量数据。除了原子数和键数,还有分子量、LogP、氢键供受体数、环数等等。这些属性在机器学习中直接当标量特征用也很常见。
先看最基础的:
mol = Chem.MolFromSmiles("CC(C)CC(C)C(=O)O")
print(mol.GetNumAtoms()) # 原子总数(包括H?默认不包括)
print(mol.GetNumHeavyAtoms()) # 重原子数(非H)
print(mol.GetNumBonds()) # 键数
print(mol.GetNumConformers()) # 构象数(默认0)
获取单个原子或键的信息,需要遍历:
for atom in mol.GetAtoms():
print(atom.GetAtomicNum(), atom.GetSymbol(), atom.GetFormalCharge())
for bond in mol.GetBonds():
print(bond.GetBeginAtomIdx(), bond.GetEndAtomIdx(), bond.GetBondType())
这里要注意:原子索引从 0 开始。我犯过的一个低级错误是索引越界——你可能会以为 GetNumAtoms() 返回的是最大索引+1,但小心有些操作会改变原子数(比如加氢),导致之前的索引失效。最好每次访问前重新获取。
如果你想用现有的分子描述符库,RdKit 内置了一堆:
from rdkit.Chem import Descriptors
mol = Chem.MolFromSmiles("CCO")
mw = Descriptors.MolWt(mol) # 分子量
logp = Descriptors.MolLogP(mol) # 脂水分配系数
hbd = Descriptors.NumHDonors(mol) # 氢键供体
hba = Descriptors.NumHAcceptors(mol) # 氢键受体
tpsa = Descriptors.TPSA(mol) # 极性表面积
print(mw, logp, hbd, hba, tpsa)
这些描述符在 QSAR 模型里简直是标配。但要注意,有些描述符计算很慢(比如超过 500 个原子的分子),你批量处理时建议加 cache。
4.4 子结构搜索:识别关键模式
子结构搜索是药物化学里最常用的功能之一。你想知道分子里有没有某个片段——比如羧基、苯环、酰胺键。RdKit 用 Mol.HasSubstructMatch(substructure) 判断存在性,用 GetSubstructMatch 获取匹配的原子索引。
mol = Chem.MolFromSmiles("CCO")
sub = Chem.MolFromSmiles("CO") # 甲醇片段
match = mol.HasSubstructMatch(sub)
print(match) # True,因为乙醇包含 C-O 键
但这里有个细节:子结构匹配默认不考虑立体化学。你如果要做精确匹配,需要设置参数。
实际项目中,我经常需要同时匹配多个片段:
mol = Chem.MolFromSmiles("C1=CC=C(C=C1)C(=O)O") # 苯甲酸
patterns = {
"苯环": "c1ccccc1",
"羧基": "C(=O)O",
"甲基": "C"
}
for name, smarts in patterns.items():
subst = Chem.MolFromSmarts(smarts) # 注意用 SMARTS
if mol.HasSubstructMatch(subst):
print(f"包含:{name}")
C=O 在 SMILES 里是羰基,但在 SMARTS 里你要写 [CX3]=[OX1] 才能精确匹配酮羰基。如果你要获取所有匹配的列表,用 GetSubstructMatches(返回二维列表,每个匹配是一组原子索引)。
mol = Chem.MolFromSmiles("C=CC=C")
sub = Chem.MolFromSmarts("[C]=[C]") # 任意双键碳
matches = mol.GetSubstructMatches(sub)
print(matches) # [(0,1), (2,3)] 两个匹配
这些索引可以直接用于后续子图提取。比如你想把子结构单独拿出来做分子图,可以配合 Chem.MolFromSmiles 新生成一个对象。
流程图:分子对象的生命周期
好,我们把上面四个操作串起来看。下面这张图展示了分子对象从 SMILES 到特征提取再到子结构搜索的完整流程。你可以把它理解为整个实战教程的第一张地图。
这张图里的每个节点你都要能用手里的代码调出来。MolToSmiles 那条虚线是来回用的——你改了分子,再转回字符串,看它是不是还能解析成本来的结构。我自己经常这么调试,确保没改坏东西。
避坑指南:我曾经被搞过的地方
- SMILES 里的芳香性:RdKit 会把芳香键自动识别并记录到分子里。如果你用
GetBondType()判断,BondType.AROMATIC和BondType.SINGLE是不同的。我在做图网络计算时,一开始用的bond_type编码没包含芳香性,结果模型精度一直上不去。 - 加氢与不加氢:默认不加氢,但图神经网络通常需要氢节点来区分共振结构。一定要在子结构搜索前统一加氢,否则可能漏匹配。我曾经因为忘记
AddHs,导致某个活性片段死活找不到,花了一个下午才排查出来。 - 子结构搜索的原子索引:
GetSubstructMatch返回的是原分子中的原子索引。如果你对分子做了一些操作(比如删原子),这些索引就会变。建议先备份一份原始分子对象。 - MolFromSmiles 返回 None 后的处理:不要直接拿
None去调用GetNumAtoms(),会崩溃。一定要先判断if mol is not None。
小结
这一章内容不多,但都是每天要用的基本功。你掌握了 MolFromSmiles 和 MolToSmiles 这对双向转换器,加上属性获取和子结构搜索,基本上就能完成 80% 的分子预处理工作。下一章我们会把这些操作组合起来,批量生成分子图的节点特征和边特征,正式为图神经网络准备数据。
HasSubstructMatch 检查它们是否含有特定的药效团(比如苯环+羰基)。顺手把 MolToSmiles 的输出和输入对比一下,看看 RdKit 是怎么重排原子的。你会发现有时候它比你自己写的更规范,有时候又会走火入魔(笑)。