PDB 文件解析基础

PDB 文件里面存的是啥?说白了,就是蛋白质或核酸里每个原子的三维坐标、残基序列、链编号,还有一堆实验元数据。你直接把文件当文本打开也能看,但人工一行一行读?那几千个原子会把你眼睛看花。所以我们要用工具来解析。

我最早做这行时,试过自己写正则表达式去匹配 ATOM 行。结果呢?一个小数点位数的差异就让脚本崩了。后来我学乖了,老老实实用 Biopython 的 PDBParser。嗯,这条弯路你们可以不用走。

1. PDBParser 的基本用法

Biopython 的 Bio.PDB.PDBParser 是专门用来吃 PDB 文件的东西。你给它一个文件路径,它吐出 SMCRA 结构对象。什么是 SMCRA?就是 Structure → Model → Chain → Residue → Atom,一个四层嵌套容器。

基本调用代码很简单:

from Bio.PDB import PDBParser

parser = PDBParser(QUIET=True)   # QUIET 这个参数我建议你一直开着
structure = parser.get_structure("1ubq", "1ubq.pdb")

QUIET=True 的意思是抑制解析过程中的警告信息。如果你不设这个,那些格式不规范但还能用的老旧 PDB 文件会刷屏,很烦人。

我的习惯: 拿到一个 PDB 文件后,先看一眼它的文件头。用 head -10 或 less 扫一下 REMARK 行,了解它是 X 射线还是 NMR 还是计算模型。不同来源的数据,清洗侧重点完全不同。

2. 解析后的结构遍历

东西解析出来以后,怎么取原子和残基?你得一层一层往下走:

for model in structure:
    for chain in model:
        for residue in chain:
            for atom in residue:
                print(atom.get_id(), atom.get_coord())

有人嫌这四层循环太深。Biopython 给了一个方便写法:

for atom in structure.get_atoms():
    print(atom.get_full_id(), atom.get_coord())

get_atoms() 会自动走完所有层级,直接返回原子对象。但如果你要按链或按残基做过滤,还是得用循环。

为什么我强调这个?有一次我接了一个蛋白质复合物的 PDB,里面有 12 条链,每条链的残基编号还是跳跃的。如果不用 get_atoms() 而自己写循环,很容易漏掉某些残基上的水分子标记。

层级 获取方法 说明
Structure parser.get_structure() 最顶层容器
Model structure[0] 大多数 X 射线只有一个 model
Chain model['A'] 按链 ID 索引
Residue chain[(' ', 10, ' ')] 按 (异标, 序号, 插入码) 索引
Atom residue['CA'] 按原子名称索引
注意: 残基的索引是一个三元组,不是简单的整数。格式是 (hetero_flag, sequence_number, insertion_code)。比如 ('W', 1, ' ') 表示水分子,(' ', 10, 'A') 表示有插入码的残基。我最初在这上面踩过坑,直接用整数去取残基,结果抛 KeyError,找了半天才发现是索引格式不对。

3. 用 MMCIFParser 处理 mmCIF 格式

现在 PDB 官方已经不太更新老式 PDB 格式了。新发布的蛋白质结构基本都是 mmCIF 格式,后缀是 .cif。

mmCIF 能存更多信息,比如完整的各向异性温度因子、非常规残基的化学图。你想,PDB 文件的列宽撑死 80 个字符,有些信息放不下。mmCIF 就没这个限制。

用法基本一样:

from Bio.PDB.MMCIFParser import MMCIFParser

parser = MMCIFParser(QUIET=True)
structure = parser.get_structure("7bq1", "7bq1.cif")

读取出来后,同样是 SMCRA 结构。你的下游代码不用改,只要换一个解析器就行。

要点: 如果你处理的 PDB 文件是 2020 年之后发布的,或者分辨率高于 2.0 Å,大概率是 mmCIF。建议统一用 MMCIFParser,这样字段更完整。我个人在项目中也会先用后缀判断格式,然后自动选择解析器。

4. 常见的异常和处理

PDB 文件是从真实实验来的,不可能像教科书一样规规矩矩。我总结了五个我见过最多的异常场景:

  1. 文件下载不全: 文件只有几百字节,长度不达标。
  2. 残基编号混乱: 比如负数编号、跳跃式编号、插入码混用。
  3. 原子名称不一致: 同样是 Cα,有的写 CA,有的写 CA 但前面多空格。
  4. 非标准残基: 比如磷酸化丝氨酸(SEP),被当作 HETATM 处理。
  5. 链 ID 冲突: 同一个链里既有水分子又有氨基酸链。

对应的处理方法:

from Bio.PDB.PDBExceptions import PDBConstructionWarning
import warnings

warnings.filterwarnings("ignore", category=PDBConstructionWarning)

try:
    structure = parser.get_structure("pdb", "broken.pdb")
except Exception as e:
    print(f"解析失败: {e}")
    # 做回退处理,比如跳过或用备选文件

有一个常见的坑:有些 PDB 文件里 REMARK 行的总残基数和你实际解析到的残基数不一致。这时候不要慌,以解析结果为准,因为 REMARK 行的数据有时是手工填的,有误差。

曾经我遇到一个情况: 一个来自同源建模的 PDB 文件,所有残基都是 HETATM 而不是 ATOM。Biopython 默认只把 ATOM 记录当常规残基。如果你不做过滤,整个结构里一个氨基酸都拿不到。后来我加上 is_amino_acid() 判断,再把 HETATM 里的已知氨基酸也纳入统计,才算搞定。

5. 解析核心逻辑图

下面这个图帮你理清解析流程和异常分支。你看一眼就明白:

PDB 文件解析核心流程 PDB 文件源 格式判断:PDB vs mmCIF PDBParser MMCIFParser SMCRA 结构对象 异常处理:过滤警告/回退 输出干净的 Structure 对象

6. 一个完整的工作流示例

我把前面讲的知识点整合成一个函数。你复制就能用:

from Bio.PDB import PDBParser
from Bio.PDB.MMCIFParser import MMCIFParser
from Bio.PDB.PDBExceptions import PDBConstructionWarning
import warnings, os

def safe_load_structure(filepath):
    """安全加载 PDB 或 mmCIF 文件,返回 Structure 对象或 None"""
    if not os.path.isfile(filepath):
        print(f"文件不存在: {filepath}")
        return None

    ext = os.path.splitext(filepath)[1].lower()
    warnings.filterwarnings("ignore", category=PDBConstructionWarning)

    try:
        if ext == ".pdb":
            parser = PDBParser(QUIET=True)
            struct = parser.get_structure("struct", filepath)
        elif ext == ".cif":
            parser = MMCIFParser(QUIET=True)
            struct = parser.get_structure("struct", filepath)
        else:
            print(f"不支持的格式: {ext}")
            return None
    except Exception as e:
        print(f"解析异常: {e}")
        return None

    # 校验:至少有 1 个 model 和 1 条链
    if len(list(struct.get_chains())) == 0:
        print("未解析到任何链,请检查文件")
        return None

    return struct

这个函数做了三件事:格式判断、异常捕获、空结构检查。我在处理批量结构时,就用这个函数做第一关筛选,把明显有问题的文件筛出去。

一点补充: 如果文件里只有 CA 原子(比如来自 AlphaFold 的部分输出),PDBParser 照样能解析,只是后续做全原子分析时会报 KeyError。你可以在解析后用 list(residue.get_atoms()) 检查原子数,提早发现这个问题。