第2章:SMILES规范与变体——从Canonical到SMARTS
说起SMILES,大家第一反应可能是那串看着像乱码的字母和符号。嗯,我刚入行时也觉得这东西很玄乎,不就是把分子结构写成一行字符串吗?但真正用起来才发现,里面的门道可不少。今天我就把自己踩过的坑和积攒的经验,一次性掏出来跟你聊聊。
2.1 什么是SMILES?
SMILES全称是Simplified Molecular Input Line Entry System。说白了,就是一套用ASCII字符表示分子结构的规则。它并不像InChI那样追求绝对的唯一性,而是强调简洁和可读性——至少对机器来说是这样。
举个例子,乙醇可以写成CCO,就这么简单。碳原子用C,氧原子用O,单键省略不写。你可能会问:那双键、三键怎么表示?环结构呢?别急,我们一样一样拆开看。
| 结构 | SMILES表达 | 说明 |
|---|---|---|
| 单键 | - |
通常省略,如CC表示乙烷 |
| 双键 | = |
如C=C表示乙烯 |
| 三键 | # |
如C#C表示乙炔 |
| 环结构 | 数字标记 | 如C1CCCCC1表示环己烷 |
| 分支 | () |
如CC(C)C表示异丁烷 |
小技巧:我个人习惯在SMILES字符串里显式写出单键符号,虽然按规范可以省略。原因很简单:在做字符串匹配和解析时,显式单键能减少很多歧义。你想想看,C-C和CC在语义上完全等价的,但有些解析器在处理后者时可能会出幺蛾子。
2.2 Canonical SMILES:一个分子只有一个身份
同一个分子,理论上可以有无数种SMILES写法。比如乙醇,可以写成CCO、OCC,甚至C(O)C。这在做数据库去重或分子唯一性比对时简直是灾难。我记得第一次建数据集时,因为用了非标准化的SMILES,结果同一个化合物在库里出现了七八次……那个洗数据的周末,我至今记忆犹新。
Canonical SMILES就是为了解决这个问题而生的。它通过一套确定的算法,为每个分子生成一个唯一的、标准化的SMILES。无论你用哪种方式输入分子结构,生成的Canonical SMILES都是一样的。
RDKit中生成Canonical SMILES的方式如下:
from rdkit import Chem
smiles_list = ["CCO", "OCC", "C(O)C"]
for s in smiles_list:
mol = Chem.MolFromSmiles(s)
canonical = Chem.MolToSmiles(mol)
print(f"{s} -> {canonical}")
输出结果:
CCO -> CCO
OCC -> CCO
C(O)C -> CCO
关键点:Canonical SMILES的生成过程跟解析器的实现有关。不同工具包(RDKit、Open Babel、CDK)生成的Canonical SMILES可能不同。所以你在做跨平台数据交换时,最好统一用同一套工具来做标准化处理。我曾经在项目里混合使用RDKit和Open Babel做标准化,结果同样的分子输出完全不同,排查了大半天才发现是工具链不一致的问题。
2.3 Isomeric SMILES:保留立体化学信息
现实世界中的分子不只有二维骨架,立体化学往往是决定生物活性的关键。比如手性药物,(R)-构型和(S)-构型的药效可能天差地别。你想想看,如果SMILES丢失了立体信息,做出来的QSAR模型基本就是睁眼瞎。
Isomeric SMILES就是专门保留这些立体信息的变体。它用特殊标记法来表示手性、双键构型(E/Z)和同位素信息。
常见的手性标记:
| 标记 | 含义 | 示例 |
|---|---|---|
@ |
反时针(相对于原子顺序) | CC@H |
@@ |
顺时针(相对于原子顺序) | CC@H |
/ |
顺式双键 | C/C=C/C |
\ |
反式双键 | C\C=C\C |
RDKit中生成Isomeric SMILES的方法:
from rdkit import Chem
mol = Chem.MolFromSmiles("C[C@H](Cl)Br")
isomeric_smiles = Chem.MolToSmiles(mol, isomericSmiles=True)
canonical_smiles = Chem.MolToSmiles(mol, isomericSmiles=False)
print(f"Isomeric: {isomeric_smiles}")
print(f"Canonical: {canonical_smiles}")
Isomeric: C[C@H](Cl)Br
Canonical: CC(Cl)Br
注意:在使用Isomeric SMILES做分子指纹计算或相似性搜索时,一定要想清楚你的目标是什么。如果只关注分子骨架,那就用Canonical SMILES;如果立体化学很重要,那就一定要保留Isomeric信息。我曾经见过有人把所有SMILES都转成Canonical,结果Ames毒性预测模型完全失效——原来活性跟手性中心密切相关。
2.4 SMARTS模式入门:描述子结构
SMARTS是SMILES的扩展语法,专门用来描述分子中的子结构模式。简单来说,SMILES是描述一个具体的分子,而SMARTS是描述一类结构特征。你想想看,如果你要在百万级化合物库中筛选出所有含羧基的化合物,总不能一个个写SMILES比对吧?
SMARTS的核心语法:
- [元素符号]:匹配特定原子,如
[C]匹配任意碳原子 - [#原子序数]:按原子序数匹配,如
[#6]也是碳 - [符号]:逻辑操作符,如
[C,c]匹配脂肪碳或芳香碳 - [!符号]:非操作,如
[!C]匹配非碳原子 - [*]:匹配任何原子(通配符)
常用SMARTS模式示例:
| 模式 | 含义 |
|---|---|
[CX3](=[OX1])[OX2H1] |
羧基 |
[NX3;H2;+0] |
伯氨基 |
c1ccccc1 |
苯环 |
[OH] |
羟基 |
[$(C(=O)N)] |
酰胺键 |
RDKit中SMARTS匹配示例:
from rdkit import Chem
from rdkit.Chem import Draw
mol = Chem.MolFromSmiles("CC(=O)OCC(=O)O")
smarts = "[CX3](=[OX1])[OX2H1]" # 羧基模式
matches = mol.GetSubstructMatches(Chem.MolFromSmarts(smarts))
print(f"匹配到 {len(matches)} 个羧基")
for match in matches:
print(f"原子索引: {match}")
匹配到 1 个羧基
原子索引: (3, 4, 5)
经验谈:写SMARTS模式时,我建议你尽量明确原子属性。比如匹配芳香原子时用a而不是c,因为c只匹配芳香碳,而a匹配所有芳香原子(包括氮、氧等)。还有,[C]和c的区别——前者匹配任何碳(不管是否芳香),后者只匹配芳香碳。这在杂环匹配时特别容易出错。
2.5 三种变体的关系
讲了这么多,我们用一张图来理清这三种SMILES变体的关系和适用场景。这张图我在团队内训时反复用过,新同学看完基本都能上手。
2.6 实际项目中的选择策略
说了这么多理论和语法,最后分享几条实操经验。我在几个不同的药化项目中总结出来的选择策略,希望对你有帮助。
- 数据入库阶段:优先使用Canonical SMILES建立主键索引。这一步能吞掉90%以上的冗余问题。
- 查询展示阶段:保留Isomeric SMILES的原始输入信息。尤其是涉及手性分子时,强型建议保留@和@@标记。
- 子结构搜索阶段:用SMARTS模式定义搜索。注意,SMARTS对原子属性的敏感度很高——写模式时要明确考虑芳香性、杂化和原子类型。
- 数据增强阶段:随机化SMILES生成(我们下一章会详细讲),但增强后一定要做标准化回写,否则模型学到的只是SMILES语法变体,而不是化学结构本身。
一个坑:我曾经在数据预处理时,先做了Canonical标准化,然后用标准化后的字符串去匹配SMARTS模式找羧基。结果发现很多应该被匹配的结构漏掉了——原因就在于标准化过程改变了原子顺序和分支结构,导致SMARTS模式中的原子连接方式跟实际结构对不上。解决办法是:用Mol对象做子结构匹配,而不是在字符串层面做模式匹配。
好了,SMILES的三种核心变体就聊到这儿。下回你拿到一批分子数据,心里应该清楚该用哪种SMILES来处理了吧?如果还有疑问,建议你先在RDKit里跑几个例子试试——实践出真知,不要光看文档。我就是靠着反复试验、踩坑、总结,才慢慢把这些东西吃透的。