使用RDKit读取SDF文件:从开箱到提取
SDF格式,说白了就是化学数据的通用包装盒。每个分子自带一个结构,还能挂一堆自定义属性。我个人习惯不管项目多急,第一步永远是先把文件读进来,看看里面到底装了什么。
RDKit 提供了两个核心读取器:Chem.SDMolSupplier 和 Chem.SDMolSupplier 的变体。今天我们就啃透它。
核心函数:Chem.SDMolSupplier
你只需要一行代码就能创建读取器:
from rdkit import Chem
supplier = Chem.SDMolSupplier('ligands.sdf')
嗯,这里要注意。Chem.SDMolSupplier 返回的不是列表,而是一个迭代器。说白了,它是懒加载的——只有当你循环到某个分子时,它才去解析那一条数据。我在项目中遇到过同事直接 list(supplier) 把整个文件塞进内存,结果一个 5 万分子的文件吃掉了 4GB 内存。没必要。
遍历分子,提取属性
最常见的姿势如下:
for mol in supplier:
if mol is None:
continue
print(mol.GetNumAtoms(), mol.GetProp('_Name'))
为什么会有一个 if mol is None?
因为 SDF 文件里可能存在解析失败的条目。比如坏键、非法原子类型。RDKit 拿它没办法,就返回 None。不跳过的话,下一行代码就会崩。这招是我在早期踩坑学来的。
要提取 SDF 中的自定义属性(就是 > <field_name> 里的那些),用 mol.GetProp():
mol_id = mol.GetProp('ID')
ic50 = float(mol.GetProp('IC50_nM'))
smiles = mol.GetProp('SMILES')
如果属性不存在,RDKit 会抛 KeyError。所以要么先用 mol.HasProp('key') 检查一下,要么用 GetPropsAsDict() 一把抓。
我个人更喜欢后者:
props = mol.GetPropsAsDict()
# props = {'ID': 'CHEM-001', 'IC50_nM': '12.5', 'SMILES': 'CC(=O)Oc1ccccc1C(=O)O'}
这样返回的是一个普通字典,键都是字符串,值可能是字符串、数字或者列表(如果 SDF 里同一个字段出现多次的话)。你放心,有列表的情况很少,但处理管线里加上类型判断总是好的。
跨平台兼容性
你可能在不同操作系统间传文件。Windows 下的 SDF 换行符是 \r\n,Linux 下是 \n。RDKit 内部做了处理,但我自己仍然习惯先统一一下:
with open('ligands.sdf', 'r') as f:
raw = f.read()
raw = raw.replace('\r\n', '\n').replace('\r', '\n')
supplier = Chem.SDMolSupplier()
open() 再传给 ForwardSDMolSupplier:
from rdkit.Chem import ForwardSDMolSupplier
with open('配体库.sdf', 'rb') as f:
supplier = ForwardSDMolSupplier(f)
性能与内存管理
SDF 文件动辄几百 MB。我曾处理过一个 2.3 GB 的化合物库。如果一口气 list(supplier),内存直接爆炸。正确的做法是分批处理:
def batch_process(supplier, batch_size=1000):
batch = []
for idx, mol in enumerate(supplier):
if mol is None:
continue
batch.append(mol)
if (idx + 1) % batch_size == 0:
yield batch
batch = []
if batch:
yield batch
for batch in batch_process(supplier, 2000):
# 插入数据库 / 写入文件
pass
这样每处理完一批,内存就能被回收。你想想看,如果你直接加载全量,Python 根本撑不住。
避坑指南
- 属性不存在:用
mol.GetProp('IC50')前先mol.HasProp('IC50'),否则 KeyError 会直接中断循环。 - 字符串转数值:SDF 所有属性都是字符串。
float('1.23e-9')没问题,但遇到空字符串''会抛 ValueError。我在项目中一度被这个坑惨了。 - 多值字段:同一属性名在 SDF 中出现多次时,
GetProp()返回最后一个,GetPropsAsDict()返回列表。规范文件通常不会有,但你不是总能控制上游数据。
实战:一个完整的读取流程
把上面所有东西串起来,写个稳健的读取器:
import numpy as np
from rdkit import Chem
from rdkit.Chem import Descriptors
def safe_read_sdf(path):
supplier = Chem.SDMolSupplier(path)
data = []
for mol in supplier:
if mol is None:
continue
# 提取属性
props = mol.GetPropsAsDict()
# 清洗数值字段
ic50 = props.get('IC50_nM', None)
if ic50 is not None:
try:
ic50 = float(ic50)
except ValueError:
ic50 = None
# 计算衍生描述符
mw = Descriptors.MolWt(mol)
logp = Descriptors.MolLogP(mol)
data.append({
'mol': mol,
'ic50': ic50,
'mw': mw,
'logp': logp,
'props': props
})
return data
# 使用
records = safe_read_sdf('compound_library.sdf')
print(f"成功读取 {len(records)} 个有效分子")
这里我特意用 props.get() 而不是 GetProp(),就是为了避免键不存在时的崩溃。数值转换也套了 try-except——你永远不知道上游会塞进来什么。
嗯,读完文件只是第一步。拿到分子对象之后,你就可以做清洗、过滤、描述符计算,或者直接扔进机器学习模型了。但说到底,读文件这个动作是整个流程的地基。地基没打稳,上面盖什么都白搭。