4、使用Python解析PDB文件:读取原子坐标、链、残基信息

我个人经验告诉我,解析PDB文件这事,确实是个基本功。搞RMSD计算,第一步就是把结构文件里的坐标、链信息、残基编号这些数据给提出来。别看PDB文件就是个文本文件,里面的门道还是不少的。

做RMSD评估,说白了就是对比两组原子坐标。但你想想看,坐标从哪来?结构里是不是只有A链?水分子要不要保留?这些都得在解析这一步处理好。我在项目里遇到过,同一个晶体结构,不同实验室修出来的版本,链标识和残基编号可能都不一样——嗯,这里要注意。

PDB文件长什么样

一个标准PDB文件是一行一行的文本记录。每行以特定的记录类型开头,比如ATOM、HETATM、TER、END。我们最关心的就是ATOM记录。

典型的ATOM记录格式(固定列宽,注意是列列列,不是逗号分隔):

ATOM      1  N   ALA A   1       8.342  16.044  22.408  1.00  0.00           N  
ATOM      2  CA  ALA A   1       9.062  15.309  21.326  1.00  0.00           C  
ATOM      3  C   ALA A   1       8.162  15.028  20.129  1.00  0.00           C  
ATOM      4  O   ALA A   1       7.314  14.175  20.228  1.00  0.00           O

每一列对应什么?我习惯按起始位置来记(从1开始数):

起始列 结束列 字段 说明
1 6 记录类型 ATOM 或 HETATM
7 11 原子序号 整型,从1递增
13 16 原子名称 如 CA、N、O
17 17 备选位置标识 A/B 或空格
18 20 残基名称 如 ALA、ILE
22 22 链标识 如 A、B
23 26 残基序号 整型
31 38 x 坐标 单位:埃
39 46 y 坐标 单位:埃
47 54 z 坐标 单位:埃
小心索引陷阱! 我曾经就因为记错了起始列,读出来的坐标全错了。PDB格式的列宽是固定宽度,不是空格分割。直接用 split() 会裂开——你试试看,原子名称 "CA" 和 "C" 混在一起,结果必定乱套。

用Python自写解析器

解析PDB文件这事儿,我推荐两个路子。一个是直接用Biopython,省事。另一个是自己写个简单的解析器,轻量可控。

先看自己写的版本。我个人偏爱这个方式,因为不用多装一个包,而且逻辑完全在掌控之下。

def parse_pdb(filepath):
    atoms = []
    with open(filepath, 'r') as f:
        for line in f:
            if line.startswith(('ATOM', 'HETATM')):
                record_type = line[0:6].strip()
                serial = int(line[6:11].strip())
                atom_name = line[12:16].strip()
                alt_loc = line[16:17].strip()
                res_name = line[17:20].strip()
                chain = line[21:22].strip()
                res_seq = int(line[22:26].strip())
                x = float(line[30:38].strip())
                y = float(line[38:46].strip())
                z = float(line[46:54].strip())
                atoms.append({
                    'serial': serial,
                    'atom_name': atom_name,
                    'res_name': res_name,
                    'chain': chain,
                    'res_seq': res_seq,
                    'x': x,
                    'y': y,
                    'z': z
                })
    return atoms
小技巧: 切片是从0开始的,但PDB格式的列号是从1开始计。所以第13到16列对应切片 [12:16]。这个对照关系最好写个注释,防止以后自己忘了。

用Biopython解析

如果你项目里已经装了Biopython,那解析起来就轻松多了。我从Biopython开始接触结构生物信息,确实方便。

from Bio.PDB import PDBParser

parser = PDBParser(QUIET=True)  # QUIET=True 可以少吐一堆警告
structure = parser.get_structure('my_protein', 'example.pdb')

for model in structure:
    for chain in model:
        print(f"Chain: {chain.get_id()}")
        for residue in chain:
            print(f"  Residue: {residue.get_resname()} {residue.get_id()[1]}")
            for atom in residue:
                coord = atom.get_vector()
                print(f"    Atom: {atom.get_name()} ({coord[0]:.3f}, {coord[1]:.3f}, {coord[2]:.3f})")

同样的解析,用Biopython做出来的是一个嵌套结构:Structure → Model → Chain → Residue → Atom。我一般通过这个层级来提取坐标。

方法 优点 缺点
自写解析器 无依赖、代码可控、轻量 需要自己处理边界情况
Biopython 功能完整、处理紊乱更稳健 额外依赖、封装较厚
我的选择: 在做RMSD评估的教学工具时,我选自写解析器,因为场景固定、不需要处理所有PDB变体。但如果你在做一个通用工具,用Biopython更省心。

提取链和残基信息

很多时候我们不只要原子坐标,还需要按链和残基来组织数据。比如做RMSD计算时,通常是对齐两个结构的Cα原子,而且只对齐主链,还要排除水分子。

下面这个函数,提取指定链、指定残基类型(只取骨架原子)的坐标:

def extract_ca_coords(atoms, chain_id='A', exclude_water=True):
    coords = []
    for atom in atoms:
        if atom['chain'] != chain_id:
            continue
        if exclude_water and atom['res_name'] == 'HOH':
            continue
        if atom['atom_name'] == 'CA':
            coords.append((atom['x'], atom['y'], atom['z']))
    return coords

这里有个坑:PDB里的水分子残基名是 HOH,有时也写成 WAT。我在项目里碰到过一个PDB混合了两种写法。所以做RMSD之前,一定先去确认数据结构。你的对比对象里是不是混进了溶剂?

解析流程全景图

下面这张SVG图,是我做解析时脑中过的一遍整体流程。从PDB文件开始,按行读入,筛选ATOM记录,再分别提取链、残基、原子坐标,最后按RMSD计算需求做二次筛选。

PDB文件解析核心流程 读取PDB文件 筛选 "ATOM" / "HETATM" 按固定列宽提取: 链 / 残基 / 原子名 / xyz坐标 按链标识筛选 例如 chain = 'A' 按残基/原子筛选 例如只取 CA 原子 排除水/配体 丢弃 HOH 等残基 得到 RMSD 计算的坐标集 ⚠ 注意 坐标索引从 1 开始

避坑指南 & 实战经验

  • 多模型PDB: 有些NMR结构包含多个模型。Biopython里会返回多个Model对象,你得指定取第一套坐标。自写解析器就要小心了,MODEL记录要正确处理。
  • 备选位置: 同一个原子可能有A和B两种构象。RMSD计算时通常只取A或主占位位置。我一般会过滤 alt_loc == ' ' 或依次取A、B。
  • 残基插入码: 某些残基序号后面带了插入码(比如 10A、10B),残基序号字段不是纯整数了。解析时建议读成形式。
  • 文件编码: 大部分PDB是ASCII,但偶尔遇到UTF-8 BOM头,open 的时候用 encoding='utf-8' 比较稳。
一个小习惯: 我每次写完解析代码,都会找一个自己熟悉的PDB文件跑一遍,打印出前几行坐标和残基信息,看看是不是和VMD或PyMOL里显示的一致。这一步花不了10秒,但能避免后续RMSD算出来一头雾水。