SMILES 进阶:分支、环、芳香性、异构体、规范 SMILES、SMILES 解析原理

上一章我们搞懂了最基础的 SMILES 写法——就是把原子一个个串起来。但真实分子哪有这么乖?

有分支的、有环的、有芳香环的、有手性中心的……今天我就带你把这些坑全填上。

我自己刚接触 SMILES 的时候,写过一条带三个环的青霉素衍生物,结果因为括号没配对,解析器直接报错。后来我养成了一个习惯:写长 SMILES 时,先在纸上画一遍骨架,再一行行拆成子片段。

分支:用小括号搞定侧链

分支就是从一个原子上分出多条链。SMILES 用 () 包裹侧链。

来看个例子:

CC(C)C             # 异丁烷:主链 C-C-C,中间 C 上有个甲基侧链
O=C(O)c1ccccc1     # 苯甲酸:主链苯环-羧基,羧基里的羟基是分支

括号可以嵌套。我见过最夸张的深度达到 6 层——那是个环肽模拟物。解析这样的 SMILES,栈式结构很管用。

写嵌套分支时,建议每打开一个左括号缩进一个层级,像写 JSON 一样。等最后压缩成一行再提交给代码。

环:数字标记 + 配对

开环分子简单。一旦涉及环键,就要用数字标记环的连接点。

规则:

  • 在环的起点和终点原子上各写一个相同的数字(0~9)
  • 数字可以重复使用,只要在首次出现后再次出现时形成闭合
  • 最多支持 10 个环(数字 0-9),更多环需要 % 两位数字

比如环己烷:

C1CCCCC1

第一个 C 后写 1,最后一个 C 后也写 1,表示这两头连起来。

我处理过一个三环体系:

C12CC3CC(C1)CC(C3)C2      # 金刚烷骨架

顺便说个坑:数字不能连续写两个不匹配的环数字,比如 C12 会被理解为 C 同时是环 1 和环 2 的起点,除非你确实想表达螺环。

芳香性:小写字母表示

苯环如果写成 C1=CC=CC=C1 也没错,但太啰嗦。SMILES 允许用小写字母表示芳香原子,让环变成 c1ccccc1。

芳香性判断标准:

  • 原子必须在芳香环中(满足休克尔规则或常见芳香体系)
  • 键不写双键/单键,默认芳香键
  • 常见芳香原子:c(碳)、n(吡啶氮)、o(呋喃氧)、s(噻吩硫)

看几个对比:

结构写法说明
苯c1ccccc16 个芳香碳,环形
吡啶c1ccncc15 个 c + 1 个 n
呋喃c1ccoc14 个 c + 1 个 o

注意:芳香原子不能加氢显式写,比如 c1ccccc1 自动带 6 个 H。如果你显式写 C1=CC=CC=C1,解析器认为它是环己三烯,不再是芳香体系,后续芳香性检测会失败。

我曾经用 RDKit 读一批化合物,发现一个分子明明有苯环却算不出芳香性,查了半天就是因为 SMILES 里用了大写 C 且写了双键。换成小写 c 立刻正常。

手性异构体:@ 与 @@

手性中心用 @ 或 @@ 标记,表示原子周围取代基的排列方向(顺时针或逆时针)。

规则:

  • 紧接在手性原子后面写 @ 或 @@
  • @ 表示逆时针(从第一个取代基看过去)
  • @@ 表示顺时针
  • 只对 tetrahedral 中心(sp3 碳)生效

L-丙氨酸 vs D-丙氨酸:

L-Ala:  N[C@@H](C)C(=O)O
D-Ala:  N[C@H](C)C(=O)O

这里 C@H 中的 H 是手性碳上直接连的氢。要注意手性标记必须紧贴中心原子,不能中间插别的符号。

我个人经验:手动写手性 SMILES 容易把 @/@@ 弄反。我的做法是先画个费歇尔投影,再按 RDKit 的 Chem.MolToSmiles 反向验证一次。实在不确定就交给 3D 构象生成再检查。

顺反双键异构:/ 与 \

双键两端的方向用 / 和 \ 表示。斜杠写在键的相邻原子上,表示从该原子出发“朝上”或“朝下”。

典型例子:

C/C=C/C         # 反式-2-丁烯:第一个 / 表示左侧甲基朝上,第二个 / 表示右侧甲基也朝上
C/C=C\C         # 顺式-2-丁烯:左侧朝上,右侧朝下

这玩意很容易被忽略。我有个同事写药物分子库时,遗漏了斜杠,RDKit 默认给了最稳定的反式,结果对接结果全错了。后来每写一个双键,我都会检查斜杠是否成对出现。

规范 SMILES:让唯一性落地

同一种分子可以写出无数种 SMILES(从不同原子开始、不同环数字分配)。比如乙醇:

CCO
OCC
C(C)O

都是有效写法,但对计算机来说不是“同一个字符串”。规范 SMILES 就是通过一套规则(例如 CANGEN 算法)生成唯一的、确定性的字符串。

使用 RDKit 获取规范 SMILES:

from rdkit import Chem
mol = Chem.MolFromSmiles("C(C)O")
canon = Chem.MolToSmiles(mol)
print(canon)   # 输出 CCO

规范化的核心步骤:

  1. 分配规范原子序号(基于图不变量排序)
  2. 固定遍历方向(从最小序号原子开始)
  3. 统一环数字分配(最小数字优先)

需要配套使用规范 SMILES 的场景:

  • 数据库去重(同一化合物只存一条记录)
  • 分子哈希索引
  • 注册系统(确保新分子不与已有分子重复)

规范 SMILES 并不保证 100% 唯一性。有些互变异构体(如酮-烯醇)可能产生多个规范字符串。建议在数据库里同时存储 InChI 或骨架哈希作为二次校验。

SMILES 解析原理:从字符串到图

解析器做的事就是把这串字符翻译成分子图。核心逻辑可以用一段伪代码概括:

def parse_smiles(s):
    stack = []           # 记录分支开始的位置
    atoms = []           # 原子序列
    bonds = []           # 键列表
    ring_numbers = {}    # 环号 -> 待连接原子
    cur_atom = None

    for ch in tokenize(s):
        if ch == '(':
            stack.append(len(atoms))  # 记录分支起点
        elif ch == ')':
            stack.pop()
        elif ch.isdigit() or ch == '%':
            number = read_ring_number(ch)
            if number in ring_numbers:
                # 与之前标记的环端点连键
                partner = ring_numbers[number]
                bonds.append((cur_atom, partner, 1))
                del ring_numbers[number]
            else:
                ring_numbers[number] = cur_atom
        elif ch in 'cCnNoOsS':
            atom = parse_atom(ch)
            atoms.append(atom)
            if cur_atom is not None:
                bonds.append((cur_atom, len(atoms)-1, 1))
            cur_atom = len(atoms)-1
        # ... 处理分支、方向、手性、键类型等
    return (atoms, bonds)

实际成熟的解析器(如 OpenBabel、RDKit)会用更精细的状态机来处理括号嵌套、环配对、芳香性判断。但核心就是一条:从左到右,维护一个栈,遇到括号压栈,遇到环数字配对。

如果你想自己实现轻量级 SMILES 解析器(用来做分子子结构索引),建议只支持无分支无环的线性 SMILES,避开栈管理。等测试通过再逐步加入括号和环逻辑。

好了,上面这六块内容几乎覆盖了 90% 的日用 SMILES 场景。下面这张图帮你把整个知识网络串起来(SVG 画布),方便复习时快速定位。

SMILES 进阶知识体系 分支 ( ) 环数字 0-9 芳香小写 c n o s 手性 @ @@ 顺反 / \ 规范 SMILES SMILES 字符串 → 解析器 → 分子图 数据库去重 / 子结构搜索

梳理下来会发现,规矩虽然多,但每条都是历史演化的结果——为了让 SMILES 既简洁又明确。作为工程师,我最推崇的习惯是:遇到不熟悉的片段,先用 RDKit 画出来,再 dump 成规范 SMILES 看看。你亲眼看到机器怎么理解,比背一百条规则管用。