4. PDB 文件基本操作:读写、坐标修改与残基增删
PDB 文件这东西,说白了就是蛋白质结构的「身份证」。里面记录着每个原子的名字、坐标、所属残基,甚至温度因子。你拿到一个 PDB,第一件事是什么?我个人习惯是先瞄一眼文件头部的 HEADER 和 REMARK,看看这是晶体结构还是 NMR 模型,分辨率多少。这步跳过的话,后面容易踩坑。
但真正干活的时候,你不能老用文本编辑器手动改。几百个残基,几万个原子,手改要改到猴年马月去。所以我一般直接用 Biopython 的 PDB 模块,写几行脚本就搞定了。下面我把最常用的三个功能拆开来聊:读写、改坐标、删加残基。
核心思路:用 Biopython 的 PDBParser 把文本解析成对象,操作完再用 PDBIO 写回去。整个过程不要碰原始文本行。
4.1 读取与写入 PDB 文件
读取其实就两步:建一个解析器,喂进去文件名。我刚开始用的时候,老忘记设置 PERMISSIVE=1,导致那些格式不标准的 PDB 直接报错。后来我就记住了,非标准结构多的是,别太死板。
from Bio.PDB import PDBParser, PDBIO
parser = PDBParser(PERMISSIVE=1) # 1 表示容错模式
structure = parser.get_structure("1ake", "1ake.pdb")
# 看一眼结构里有什么
for model in structure:
for chain in model:
print(f"Chain {chain.id}: {len(chain)} residues")
写入更简单。你改完结构之后,直接拿 PDBIO 的 save 方法输出就行。不过我建议输出前调用一下 io.set_structure(),不然有时候会丢链信息。嗯,这里要注意:千万别用 io.save("output.pdb") 连着写,容易覆盖掉你没保存的中间版本。
io = PDBIO()
io.set_structure(structure)
io.save("cleaned.pdb")
4.2 修改原子坐标
为什么要改坐标?最常见的情况是:你从 PDB 里拿到的是粗粒度模型,或者是对接结果,需要微调侧链位置。我曾经有个项目,需要把所有 ALA 的 CB 原子平移 0.5 Å,用肉眼挑简直是自虐。
看看怎么用代码搞定:
from Bio.PDB.vectors import Vector
for model in structure:
for chain in model:
for residue in chain:
if residue.get_resname() == "ALA":
if "CB" in residue:
atom = residue["CB"]
old_coord = atom.get_vector()
offset = Vector(0.5, 0, 0)
new_coord = old_coord + offset
atom.set_coord(new_coord.get_array())
这里我用了 Vector 做向量运算,比手写坐标加减清晰得多。你可能会问:为什么只平移 CB?因为 ALA 的侧链就一个甲基,平移 CB 就等于整个侧链动了。做设计的时候,这种定点操作特别常用。
坐标单位:PDB 默认坐标单位是 Ångström(埃)。平移 0.5 就是 0.5 Å,别搞成纳米了。我一同事曾经写过 offset = Vector(5, 0, 0),结果原子直接飞出去了——那是 5 Å,够跨半个残基了。
4.3 删除或添加残基
删残基和加残基,看起来是两个方向,但底层逻辑一样:操作 Chain 对象里的 child_list。我习惯先把链里的残基按 ID 排好序,再动手。
4.3.1 删除指定残基
比如我要删掉 1ake 结构里所有的水分子。水分子在 PDB 里通常残基名是 HOH,链 ID 可能不统一。我一般这么写:
for model in structure:
for chain in model:
to_detach = []
for residue in chain:
if residue.get_resname() == "HOH":
to_detach.append(residue.id)
for res_id in to_detach:
chain.detach_child(res_id)
注意:先收集要删的 ID,再统一删。直接在遍历中删会报 RuntimeError: dictionary changed size during iteration,我踩过这个坑,差点以为 Biopython 有 bug。
4.3.2 添加新残基
添加残基稍微麻烦点,因为你要自己构造 Residue 对象和它的原子。我做过一个模拟磷酸化修饰的脚本,需要在某个 SER 旁边加一个磷酸根(PO4)。
from Bio.PDB.Residue import Residue
from Bio.PDB.Atom import Atom
import numpy as np
# 假设要在链 A 的残基 100 之后插入一个磷酸根
chain = structure[0]["A"]
new_res = Residue((" ", 101, " "), "PO4", "")
# 加一个 P 原子
p_atom = Atom("P", np.array([10.0, 20.0, 30.0]), 1.0, 1.0, " ", "P", "P")
new_res.add(p_atom)
# 插入到链中
chain.add(new_res)
这里 Residue 的第一个参数是三元组 (hetero_flag, sequence_number, insertion_code),我一般用 (" ", 101, " ") 表示标准残基。如果你加的是水或配体,hetero_flag 要改成 "W" 或 "H_"。
个人小技巧:添加残基之后,最好调用一下 structure.internal_coords() 重建内部坐标,否则有些第三方工具读你的 PDB 可能会报错。
4.4 整体流程:从读到改到存
把上面三个操作串起来,就是一个标准的 PDB 处理流水线。我画了张图,概括了整个流程:
4.5 PDB 文件格式那些事儿
操作之前,你得清楚 PDB 每一列什么意思。我把它列成了一张速查表,这表每次写脚本前我都会瞟一眼。
| 列范围 | 字段 | 说明 |
|---|---|---|
| 1-6 | 记录名 | ATOM / HETATM / TER / END |
| 7-11 | 序列号 | 原子在文件中的编号 |
| 13-16 | 原子名 | 如 CA、CB、N、O |
| 18-20 | 残基名 | ALA、HOH、PO4 等三字符代码 |
| 22 | 链 ID | A、B、C 或空 |
| 23-26 | 残基序号 | 整数,可带插入码 |
| 31-54 | 坐标 | x, y, z 各占 8 列,右对齐 |
| 55-60 | 占有率 | 一般 1.0 |
| 61-66 | 温度因子 | B 因子,反映原子位移 |
改坐标或者加残基的时候,尤其要注意:PDB 格式对列的宽度要求很严格。你如果自己构造字符串,必须保证每一个字段都落在正确的列区间内。好在 Biopython 的 Atom 和 Residue 会自动帮你排版,不用手算列位置。
版本问题:现在主流 PDB 文件既有 v3.0 格式(列更宽松),也有 v2.0 格式(严格固定列宽)。如果用的是高版本 Biopython(>= 1.80),默认输出就是 v3.0,与老软件可能不兼容。我建议你在 PDBIO 初始化时指定 PDBIO(use_v3=False) 来切换回 v2.0 格式。
4.6 一个完整的实战脚本
最后,我把今天讲的内容整合成一个五六行就能跑通的脚本。这个脚本干的事:读入 PDB,删掉所有水,把所有 CYS 的 SG 原子向上平移 1 Å,然后保存为新文件。
from Bio.PDB import PDBParser, PDBIO
from Bio.PDB.vectors import Vector
parser = PDBParser(PERMISSIVE=1)
structure = parser.get_structure("target", "input.pdb")
for model in structure:
for chain in model:
# 删水
for residue in list(chain):
if residue.get_resname() == "HOH":
chain.detach_child(residue.id)
# 改 CYS 的 SG 坐标
for residue in chain:
if residue.get_resname() == "CYS" and "SG" in residue:
atom = residue["SG"]
atom.set_coord( (atom.get_vector() + Vector(0, 0, 1)).get_array() )
io = PDBIO()
io.set_structure(structure)
io.save("modified.pdb")
print("搞定。")
你试试跑一下,改完之后可以拿 PyMOL 或者 VMD 打开看看。能看到硫原子全都往上飘了一截,水分子消失得干干净净。嗯,这就是 PDB 文件操作的日常——只要掌握了解析器、向量运算和增删方法,剩下的无非是重复组合而已。