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 文件会刷屏,很烦人。
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'] | 按原子名称索引 |
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 结构。你的下游代码不用改,只要换一个解析器就行。
4. 常见的异常和处理
PDB 文件是从真实实验来的,不可能像教科书一样规规矩矩。我总结了五个我见过最多的异常场景:
- 文件下载不全: 文件只有几百字节,长度不达标。
- 残基编号混乱: 比如负数编号、跳跃式编号、插入码混用。
- 原子名称不一致: 同样是 Cα,有的写 CA,有的写 CA 但前面多空格。
- 非标准残基: 比如磷酸化丝氨酸(SEP),被当作 HETATM 处理。
- 链 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 行的数据有时是手工填的,有误差。
5. 解析核心逻辑图
下面这个图帮你理清解析流程和异常分支。你看一眼就明白:
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
这个函数做了三件事:格式判断、异常捕获、空结构检查。我在处理批量结构时,就用这个函数做第一关筛选,把明显有问题的文件筛出去。