第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变体的关系和适用场景。这张图我在团队内训时反复用过,新同学看完基本都能上手。

SMILES三种变体关系图 基础SMILES Canonical SMILES 用于去重与唯一标识 Isomeric SMILES 保留立体化学与同位素 SMARTS模式 子结构匹配与搜索 典型应用场景 • 化合物库去重 • 数据库索引键 • 分子唯一性验证 典型应用场景 • 手性分子建模 • QSAR/QSPR分析 • 药物活性预测 典型应用场景 • 功能团筛选 • 子结构搜索 • 虚拟筛选 三者不存在互斥关系,实际项目中常组合使用:先标准化去重,再保留立体信息,最后用SMARTS做子结构搜索

2.6 实际项目中的选择策略

说了这么多理论和语法,最后分享几条实操经验。我在几个不同的药化项目中总结出来的选择策略,希望对你有帮助。

  1. 数据入库阶段:优先使用Canonical SMILES建立主键索引。这一步能吞掉90%以上的冗余问题。
  2. 查询展示阶段:保留Isomeric SMILES的原始输入信息。尤其是涉及手性分子时,强型建议保留@和@@标记。
  3. 子结构搜索阶段:用SMARTS模式定义搜索。注意,SMARTS对原子属性的敏感度很高——写模式时要明确考虑芳香性、杂化和原子类型。
  4. 数据增强阶段:随机化SMILES生成(我们下一章会详细讲),但增强后一定要做标准化回写,否则模型学到的只是SMILES语法变体,而不是化学结构本身。

一个坑:我曾经在数据预处理时,先做了Canonical标准化,然后用标准化后的字符串去匹配SMARTS模式找羧基。结果发现很多应该被匹配的结构漏掉了——原因就在于标准化过程改变了原子顺序和分支结构,导致SMARTS模式中的原子连接方式跟实际结构对不上。解决办法是:用Mol对象做子结构匹配,而不是在字符串层面做模式匹配。

好了,SMILES的三种核心变体就聊到这儿。下回你拿到一批分子数据,心里应该清楚该用哪种SMILES来处理了吧?如果还有疑问,建议你先在RDKit里跑几个例子试试——实践出真知,不要光看文档。我就是靠着反复试验、踩坑、总结,才慢慢把这些东西吃透的。