4. PDB 文件写入与保存:改完了,怎么存回去?
解析 PDB 是一回事,改完了再写回去,那又是另一回事了。我刚开始做这行的时候,总觉得写文件嘛,不就是 open.write() 一下?
结果第一次跑完,拿新文件去跑分子对接,直接报错。
打开文件一看,原子名全串列了,残基编号对不齐……嗯,那时候我就明白,PDB 写入这件事,也有不少门道。
为什么写入比读取更「讲究」?
你想想看,读取的时候,程序容忍度可以很高。多一个空格?跳过。列没对齐?没事。可写入不一样——一串格式不对,下游工具可能直接报错,或者更糟,静默地算个错误结果给你。
我自己碰到过最折腾的一个 case:改了一个 B 因子字段,结果因为少写了一位小数,整个 ANISOU 记录格式就乱掉了。后来的对接结果全是废的,排查了一整天才发现是写入精度没控制好。
注意:PDB 格式是「固定列宽」格式,不是逗号分隔,也不是 JSON。每个字段写在哪一列、占多少字符,都有严格规定。
写入前,先做数据验证
我个人习惯,在写入之前做一遍「完整性检查」。说白了,就是看看你改完的结构,能不能通过格式的「体检」:
- 原子序号:是否连续?有没有重复?
- 原子名称:标准残基的原子名,是否有拼写错误?
- 残基编号:插入码(iCode)有没有按格式写?
- 坐标:有没有 NaN 或者超大值?
- 链标识:是否为空?如果有多个链,是否区分清楚?
我曾经在项目里写过一个小脚本,专门做这事。后来发现其实大部分问题都能在写入前自动修掉,省了不少回头查 bug 的时间。
格式化策略:按字段位置逐列写入
PDB 每一类记录,都有它自己的「列地图」。举个最常用的 ATOM 记录:
| 字段 | 起始列 | 结束列 | 说明 |
|---|---|---|---|
| 记录名 | 1 | 6 | "ATOM " 或 "HETATM" |
| 原子序号 | 7 | 11 | 右对齐,整数 |
| 原子名称 | 13 | 16 | 左对齐,注意空格 |
| 备用位置 | 17 | 17 | 通常为空格或 'A','B' |
| 残基名 | 18 | 20 | 右对齐,标准三字母码 |
| 链标识 | 22 | 22 | 单个字符 |
| 残基编号 | 23 | 26 | 右对齐,整数 |
| 插入码 | 27 | 27 | 可选 |
| X坐标 | 31 | 38 | 右对齐,8.3格式 |
| Y坐标 | 39 | 46 | 同上 |
| Z坐标 | 47 | 54 | 同上 |
| 占有率 | 55 | 60 | 6.2格式 |
| 温度因子 | 61 | 66 | 6.2格式 |
| 元素符号 | 77 | 78 | 右对齐 |
| 原子电荷 | 79 | 80 | 可选 |
你可能会问:这些列宽非得这么死板吗?对的,很多结构生物学的软件,就靠这些列号来定位字段。稍微偏一点,字段就会被读成别的值。
我在实际项目中写过一个 format_atom_line() 函数,专门把字段「安放」到正确的列位置:
def format_atom_line(atom):
record = atom.get("record", "ATOM ")
serial = str(atom.get("serial", 1)).rjust(5)
name = atom.get("name", "CA").ljust(4)
altLoc = atom.get("altLoc", " ")
resn = atom.get("resn", "ALA").rjust(3)
chain = atom.get("chain", "A")
resi = str(atom.get("resi", 1)).rjust(4)
iCode = atom.get("iCode", " ")
x = f"{atom['x']:8.3f}"
y = f"{atom['y']:8.3f}"
z = f"{atom['z']:8.3f}"
occup = f"{atom.get('occup', 1.0):6.2f}"
temp = f"{atom.get('tempFactor', 0.0):6.2f}"
segID = " " * 4
element = atom.get("element", " ").rjust(2)
charge = atom.get("charge", " ")
line = (f"{record}{serial} {name}{altLoc}{resn} {chain}"
f"{resi}{iCode} {x}{y}{z}{occup}{temp} "
f"{segID}{element}{charge}")
return line
小技巧:如果字段值长度不够,用空格补齐,不要用 0 或其它字符补齐。很多下游解析器会把前面的空格自动忽略掉。
写入工具的选择:Biopython vs. 手写格式化
说实话,如果是小规模的修改,比如改 10 个原子的温度因子,我推荐直接用 Biopython 的 PDBIO。方便,快捷。
from Bio.PDB import PDBParser, PDBIO
parser = PDBParser(QUIET=True)
structure = parser.get_structure("my_protein", "input.pdb")
# 做一些修改...
for atom in structure.get_atoms():
if atom.get_bfactor() < 10:
atom.set_bfactor(10.0)
io = PDBIO()
io.set_structure(structure)
io.save("output.pdb")
但如果你需要大规模转换,或者对格式有非常精细的控制——比如要批量重排原子顺序、修改记录类型——我个人建议直接用逐行格式化写入,自己控制每一列的字符串。
写入时的常见「坑」
字段溢出:坐标值假如超过 999.999,按 8.3 格式就放不下了。我建议在写入前做一个 clamp 操作,把超出范围的值标记出来,而不是直接截断。我自己吃过这个亏,后来加了一个校验步骤。
字符串编码:PDB 文件是纯 ASCII 文本,不要写入中文字符、Unicode 特殊符号。残基名、链标识等字段只能用大写英文字母和数字。
行尾结束符:虽然现在大部分软件都能兼容 \n 和 \r\n,但我习惯统一用 \n,跨平台更稳定。
实战清单:写出一个「干净」的 PDB 文件
这里是我亲测有效的大致步骤,你可以参考着用:
- 读入并备份原始结构,不要在原文件上直接修改。
- 校验数据完整性:检查有没有缺失的字段,有没有非法值。
- 按固定列宽格式化每条记录,原子记录尤其严格。
- 写入临时文件,不要覆盖原始文件,方便对比差异。
- 验证输出文件:用
pdb_validate.py或在线验证工具检查格式。 - 可视化检查:用 PyMOL、ChimeraX 等打开新文件,看看结构有没有明显的偏移或断裂。
本章知识体系
下面这张图,我把从解析到写入全流程的关键步骤串起来了。你写代码的时候可以拿它当 checklist:
最后说一句:PDB 写入不是纯字符串拼接,它是有「纪律」的格式化输出。把格式控制好,下游的每一个工具都会感谢你。