4. PDB 文件写入与保存:改完了,怎么存回去?

解析 PDB 是一回事,改完了再写回去,那又是另一回事了。我刚开始做这行的时候,总觉得写文件嘛,不就是 open.write() 一下?

结果第一次跑完,拿新文件去跑分子对接,直接报错。

打开文件一看,原子名全串列了,残基编号对不齐……嗯,那时候我就明白,PDB 写入这件事,也有不少门道。

为什么写入比读取更「讲究」?

你想想看,读取的时候,程序容忍度可以很高。多一个空格?跳过。列没对齐?没事。可写入不一样——一串格式不对,下游工具可能直接报错,或者更糟,静默地算个错误结果给你。

我自己碰到过最折腾的一个 case:改了一个 B 因子字段,结果因为少写了一位小数,整个 ANISOU 记录格式就乱掉了。后来的对接结果全是废的,排查了一整天才发现是写入精度没控制好。

注意:PDB 格式是「固定列宽」格式,不是逗号分隔,也不是 JSON。每个字段写在哪一列、占多少字符,都有严格规定。

写入前,先做数据验证

我个人习惯,在写入之前做一遍「完整性检查」。说白了,就是看看你改完的结构,能不能通过格式的「体检」:

  • 原子序号:是否连续?有没有重复?
  • 原子名称:标准残基的原子名,是否有拼写错误?
  • 残基编号:插入码(iCode)有没有按格式写?
  • 坐标:有没有 NaN 或者超大值?
  • 链标识:是否为空?如果有多个链,是否区分清楚?

我曾经在项目里写过一个小脚本,专门做这事。后来发现其实大部分问题都能在写入前自动修掉,省了不少回头查 bug 的时间。

格式化策略:按字段位置逐列写入

PDB 每一类记录,都有它自己的「列地图」。举个最常用的 ATOM 记录:

字段 起始列 结束列 说明
记录名16"ATOM " 或 "HETATM"
原子序号711右对齐,整数
原子名称1316左对齐,注意空格
备用位置1717通常为空格或 'A','B'
残基名1820右对齐,标准三字母码
链标识2222单个字符
残基编号2326右对齐,整数
插入码2727可选
X坐标3138右对齐,8.3格式
Y坐标3946同上
Z坐标4754同上
占有率55606.2格式
温度因子61666.2格式
元素符号7778右对齐
原子电荷7980可选

你可能会问:这些列宽非得这么死板吗?对的,很多结构生物学的软件,就靠这些列号来定位字段。稍微偏一点,字段就会被读成别的值。

我在实际项目中写过一个 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 文件

这里是我亲测有效的大致步骤,你可以参考着用:

  1. 读入并备份原始结构,不要在原文件上直接修改。
  2. 校验数据完整性:检查有没有缺失的字段,有没有非法值。
  3. 按固定列宽格式化每条记录,原子记录尤其严格。
  4. 写入临时文件,不要覆盖原始文件,方便对比差异。
  5. 验证输出文件:用 pdb_validate.py 或在线验证工具检查格式。
  6. 可视化检查:用 PyMOL、ChimeraX 等打开新文件,看看结构有没有明显的偏移或断裂。

本章知识体系

下面这张图,我把从解析到写入全流程的关键步骤串起来了。你写代码的时候可以拿它当 checklist:

PDB 写入与保存核心流程 解析原始 PDB 数据验证与清洗 按列宽格式化 写入临时文件 格式校验 可视化检查 校验失败?回到数据验证环节重做

最后说一句:PDB 写入不是纯字符串拼接,它是有「纪律」的格式化输出。把格式控制好,下游的每一个工具都会感谢你。