第二章:PDB文件格式详解——原子记录、链与残基
说实话,很多人觉得PDB文件就是个数据仓库,需要的时候读一下就行。但做质控不一样,你得把这文件里里外外看透。因为一个字段错了,整批结构判定就跟着歪了。
我见过不少新手直接拿Biopython读文件,根本不看原始记录长什么样。结果呢?解析出来的链总数不对,残基编号乱序,连水分子都混在配体列表里。嗯,今天我们就把PDB文件格式的关键部分捋一遍。
核心观点:质控的底层基础设施不是PDB文件头,而是原子记录。你必须能手工定位ATOM记录,解析链标识,识别标准残基。否则一切批量化校验都是空中楼阁。
2.1 一条ATOM记录到底怎么读
PDB格式是固定列宽的文本文件。每行80个字符,字段位置固定。这一点和CSV、JSON完全不同。为什么?因为这是上世纪70年代的设计,当时最宽的纸带和行式打印机就是80列。
我们看一个典型的ATOM记录:
ATOM 1 N THR A 1 6.340 9.890 3.350 1.00 13.25 N
ATOM 2 CA THR A 1 7.540 10.310 2.720 1.00 14.10 C
ATOM 3 C THR A 1 8.070 11.690 3.070 1.00 11.60 C
ATOM 4 O THR A 1 7.290 12.640 3.040 1.00 11.90 O
ATOM 5 CB THR A 1 7.360 9.990 1.210 1.00 13.85 C
我来拆开说。每个字段的起始和结束列是固定的:
| 字段 | 起始列 | 结束列 | 说明 |
|---|---|---|---|
| 记录类型 | 1 | 6 | 固定为"ATOM ",注意末尾有空格 |
| 原子序号 | 7 | 11 | 从1开始顺序编号,整数右对齐 |
| 原子名称 | 13 | 16 | 比如"N"、"CA"、"CB",有4列空间 |
| 残基名称 | 18 | 20 | 三种字母缩写,如THR、ALA |
| 链标识 | 22 | 22 | 单个字符,通常是A、B、C或空格 |
| 残基序号 | 23 | 26 | 整数,可以是负数!这点常被忽略 |
| 插入码 | 27 | 27 | 可选字符,用于插入的残基 |
| X坐标 | 31 | 38 | 8列宽,3位小数 |
| Y坐标 | 39 | 46 | 同上 |
| Z坐标 | 47 | 54 | 同上 |
| 占据率 | 55 | 60 | 通常在0到1之间 |
| 温度因子 | 61 | 66 | 也叫B因子,反映原子位移 |
| 元素符号 | 77 | 78 | 右对齐,比如" N"、" C" |
经验之谈:我自己写PDB解析器时,最常踩的坑是“原子名称”的列位置。有的PDB文件把钙离子写为"CA "(右对齐),有的写为" CA "(左对齐),还有的写为" CA"(居左但少一空格)。解析器如果按固定列读,很容易在原子名称前多吞一个空格。
2.2 链标识的“潜规则”
链标识(chain ID)只有一个字符。看起来很省事,对吧?但在实际质控中,这方面出的问题最多。
- 空格链:很多旧版PDB文件没有显式写链ID,列22就是空格。这不代表没有链,而是默认链为空字符串。
- 数字链:有时链标识是数字"0"、"1"、"2"。字符串比较时,"0" != 0,别用整数转换去碰链ID。
- 多聚体陷阱:同一条链里的残基序号可能不连续。比如链A有残基1-100,中间跳过了50-60,因为那个区域电子密度差,没建上模型。这不是文件错误,而是结构本身的问题。
质控时我习惯先收集所有出现的链标识,做一次去重和排序:
grep "^ATOM " input.pdb | cut -c22-22 | sort -u
这一行命令扫过去,文件里有多少条链、每条链用了什么ID,一秒就知道。
2.3 残基——标准与非标准
PDB里残基用三位字母表示。标准的20种氨基酸很容易判断。但真正的质控难点在于“非标准残基”。
非标准残基包括:
- 水分子:残基名为"HOH"或"H2O"
- 配体/辅因子:如"HEM"(血红素)、"FAD"(黄素腺嘌呤二核苷酸)
- 修饰氨基酸:如"SEP"(磷酸化丝氨酸)、"MLY"(甲基化赖氨酸)
- 金属离子:如"ZN"、"MG"、"CA"——这里的"CA"不是Cα原子,而是钙离子残基
注意歧义:"CA"在原子名称里是Cα,在残基名称里是钙离子。解析时必须同时考察记录类型和残基名称两列,才能正确判断。我曾经在一次批量校验脚本里只检查了残基名称列,结果把丝氨酸Cα的B因子全当成钙离子的来处理。那次数据全废了,想起来都心疼。
判断一个残基是不是标准氨基酸,有一个简单的方法:维护一个字典或set,放入20种标准氨基酸的三字母码。
STANDARD_RESIDUES = {
'ALA','ARG','ASN','ASP','CYS','GLN','GLU','GLY',
'HIS','ILE','LEU','LYS','MET','PHE','PRO','SER',
'THR','TRP','TYR','VAL'
}
def is_standard_residue(resname):
return resname.strip() in STANDARD_RESIDUES
对于批次的质控,我们要区分三类残基:
| 类别 | 例子 | 质控关注点 |
|---|---|---|
| 标准氨基酸残基 | ALA, GLY, THR | 数量、覆盖率、冲突检测 |
| 修饰残基 | SEP, PTR, CSO | 是否在修饰数据库中备案 |
| 配体/水/离子 | HOH, HEM, ZN | 占据率、B因子范围、异常接触 |
2.4 知识结构总览
下面这张图帮你把原子记录、链、残基三者的关系串起来。我自己做质控培训时,经常拿这张图开场:
这张图要说明什么呢?从ATOM记录出发,我们同时拿到链ID和残基信息。然后做两件独立的事情:第一,确认链+残基号组合是否唯一,有没有重叠的原子;第二,判断残基属于标准氨基酸、修饰残基还是配体。两路结果最终汇入质控检验。
2.5 实战:用Python快速解析ATOM记录
不上代码的教程就是耍流氓。我给你一个最小可用的解析片段。它不依赖Biopython,纯Python处理固定列宽。
def parse_atom_line(line):
"""解析一条ATOM记录,返回字典。"""
fields = {}
fields['record'] = line[0:6].strip()
fields['serial'] = int(line[6:11].strip())
fields['name'] = line[12:16].strip()
fields['resname'] = line[17:20].strip()
fields['chain'] = line[21:22].strip()
fields['resnum'] = int(line[22:26].strip())
fields['icode'] = line[26:27].strip()
fields['x'] = float(line[30:38].strip())
fields['y'] = float(line[38:46].strip())
fields['z'] = float(line[46:54].strip())
fields['occupancy'] = float(line[54:60].strip())
fields['bfactor'] = float(line[60:66].strip())
fields['element'] = line[76:78].strip()
return fields
with open('input.pdb', 'r') as f:
for line in f:
if line.startswith('ATOM') or line.startswith('HETATM'):
atom = parse_atom_line(line)
# 此时atom['chain']和atom['resnum']就是链+残基键
print(f"{atom['chain']}:{atom['resnum']} {atom['name']}")
又一个坑:HETATM记录的结构和ATOM完全一样,但HETATM代表非标准残基的原子。如果你只想统计蛋白质原子,只过滤line.startswith('ATOM '),别把水分子配体也扫进来。我早期一个脚本就没区分,统计氨基酸原子数时把配体也算进去了,导致覆盖率报告全错。
2.6 常见的质控校验点
基于本章的内容,落地到质控时,你需要至少检查这几项:
- 链标识完整性:是否有空字符串链?多条链是否用了相同ID?
- 残基序号连续性:是否存在残留的
insertion code?序列跳跃是否超过预期? - 标准残基覆盖率:分子中理论应有的氨基酸数量和实际构建的残基数比对。
- 非标准残基备案:所有出现过的非标准残基名称是否在预设的允许列表中?
- 原子名称一致性:同一残基类型的原子名称是否标准化?比如有没有把"Cα"写成" CA "(四个字符)但多了一个空格?
我建议把上面的校验规则做成一个函数,在批次处理时逐文件跑一遍。输出的报告应该精确到文件和行号,这样排查时才有头绪。
# 校验示例:检查是否有空格链
def check_empty_chain(pdb_path):
issues = []
with open(pdb_path, 'r') as f:
for i, line in enumerate(f, 1):
if line.startswith('ATOM') or line.startswith('HETATM'):
chain = line[21:22]
if chain == ' ':
issues.append(f"Line {i}: empty chain ID")
return issues
我个人习惯是写一个ValidatePDB类,把所有校验规则封装进去。解析、校验、报告三阶段分开。这样后期加规则或换输出格式时,不需要重写核心逻辑。
好了,到此我们彻底刨开了PDB文件的原子记录结构。你知道了固定列宽怎么读,链标识怎么处理,标准和非标准残基怎么区分。这些知识是后续所有批次质控脚本的基础。不管你用Python、Perl还是awk做自动化,只要理解了这些规则,你就能在第一时间识别出文件里的异常。