PDB 文件格式解析:原子记录、坐标格式与常见错误
PDB 文件格式,说白了就是一套用纯文本拼出来的表格。你随便找个 .pdb 文件打开看,满屏都是固定长度的字符串。
我个人习惯在处理前先用 head -20 瞄一眼文件头。为什么?因为格式是 Fortran 77 时代定下来的,列必须对齐。错一个空格,解析就全乱套。
嗯,先看最核心的部分——原子记录。
原子记录(ATOM / HETATM)
这两种记录标识了分子中的所有原子。区别其实很简单:ATOM 记录的是标准氨基酸/核苷酸原子;HETATM 记录配体、水分子、金属离子等“非标准”实体。
它们的格式完全一致,都是 80 列定长记录。我刚开始接触时觉得这很死板,后来发现这种固定格式反而让解析变得极其稳定——只要按列切就行。
标准列分布(我通常用一个字典来记)
| 列位置 | 字段 | 数据类型 | 说明 |
|---|---|---|---|
| 1-6 | 记录类型 | 字符串 | ATOM 或 HETATM |
| 7-11 | 原子序号 | 整数 | 全局唯一编号 |
| 13-16 | 原子名称 | 字符串 | 如 CA ,两端带空格 |
| 17 | 交替位置 | 字符 | 构象不确定时标 A/B/C |
| 18-20 | 残基名称 | 字符串 | 如 ALA |
| 22 | 链标识符 | 字符 | 单字母链 ID |
| 23-26 | 残基序号 | 整数 | 残基在链中的位置 |
| 27 | 插入码 | 字符 | 序列插入标记 |
| 31-54 | 坐标 X, Y, Z | 浮点 | 单位:Å |
| 55-60 | 占有率 | 浮点 | 0.00 - 1.00 |
| 61-66 | 温度因子 | 浮点 | B 因子值 |
核心要点:列 13-16 的原子名称是右对齐的,列 18-20 的残基名称是左对齐的。很多人直接在 Python 里用 line.split(),遇到有空格的原子名(如 CA 是 " CA ",CA2 是 "CA2 " )就会直接错位。
坐标格式与精度陷阱
坐标占了 31-54 列,共 24 个字符,每个坐标固定 8 位宽。格式是 %8.3f,也就是说小数点后固定 3 位。
你想想看,3 位小数对于大部分 X 射线结构来说勉强够用,但如果你做的是高精度分子动力学模拟,这个精度可能会有问题。
我曾经犯过的错:直接使用 float(line[30:38]) 提取 X 坐标,但列索引从 0 开始的话应该是 [30:38],而不是 [31:38]。多一个少一个字符,整个分子的坐标漂移到几万 Å 之外。
一个标准的 ATOM 记录长这样:
ATOM 27 CA ALA A 4 22.343 11.632 -5.390 1.00 45.82 C
注意看原子名 CA 前后的空格。正因如此,我要求团队写解析代码时一律按列切片 + 手动 strip(),绝对不能用 split()。
常见错误归纳总结
我在项目中遇到过的 PDB 格式问题,总结起来无非以下几类:
- 残基跳跃 / 缺失原子:序列不连续,解析时出现
999或-1等占位序号。 - 交替构象:列 17 出现
A/B,但大多数人只取A或占有率最高的那个。我建议在补全缺失原子之前,先把交替构象降维处理掉。 - 链 ID 混乱:使用小写字母
a、b甚至空格。嗯,空格在严格模式下是不允许的,但很多老旧 PDB 文件就这么写。 - REMARK 中的分辨率信息:解析
REMARK 2 RESOLUTION. 2.30 Å时要注意单位前缀,不同来源格式有差异。
一个小经验:拿到 PDB 文件后,先跑一个统计脚本,打印出所有记录类型、链 ID、残基序号范围。这一步能过滤掉半数以上的“脏数据”。
解析流程可视化
下面这张图是我做缺失补全前最常走的流程。注意看,我先做“交替构象处理”,再做“坐标提取”,这是有顺序逻辑的。
解析代码模板(Python)
我一般直接用 line[0:6] 做模式匹配,然后用 line[30:38] 这种切片语法提取坐标。虽然看起来繁琐,但出错的概率最低。
def parse_atom_line(line: str) -> dict | None:
if len(line) < 54:
return None
rec_type = line[0:6].strip()
if rec_type not in ("ATOM", "HETATM"):
return None
atom_num = int(line[6:11].strip())
atom_name = line[12:16].strip()
alt_loc = line[16:17].strip()
res_name = line[17:20].strip()
chain_id = line[21:22].strip()
res_num = int(line[22:26].strip())
icode = line[26:27].strip()
try:
x = float(line[30:38])
y = float(line[38:46])
z = float(line[46:54])
except ValueError:
return None
occupancy = line[54:60].strip()
b_factor = line[60:66].strip()
return {
"type": rec_type,
"atom_num": atom_num,
"atom_name": atom_name,
"alt_loc": alt_loc,
"res_name": res_name,
"chain_id": chain_id,
"res_num": res_num,
"icode": icode,
"x": x, "y": y, "z": z,
"occupancy": occupancy,
"b_factor": b_factor
}
关于抗噪:我习惯在 try-except 块里做坐标转换。有些 PDB 在列 31-54 里混入了 ****** 或空值。没必要让一行坏数据搞崩整个 pipeline。
解析完原子记录,下一步就是从这些信息里判断哪些残基是“残残缺的”、哪些是“可以补的”。这部分内容,我们留在后面具体展开。