第二章: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

我来拆开说。每个字段的起始和结束列是固定的:

字段起始列结束列说明
记录类型16固定为"ATOM ",注意末尾有空格
原子序号711从1开始顺序编号,整数右对齐
原子名称1316比如"N"、"CA"、"CB",有4列空间
残基名称1820三种字母缩写,如THR、ALA
链标识2222单个字符,通常是A、B、C或空格
残基序号2326整数,可以是负数!这点常被忽略
插入码2727可选字符,用于插入的残基
X坐标31388列宽,3位小数
Y坐标3946同上
Z坐标4754同上
占据率5560通常在0到1之间
温度因子6166也叫B因子,反映原子位移
元素符号7778右对齐,比如" 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 知识结构总览

下面这张图帮你把原子记录、链、残基三者的关系串起来。我自己做质控培训时,经常拿这张图开场:

PDB文件核心数据骨架 ATOM记录 链标识 (1列, 单字符) 残基 (3字母码) 重复判定:同链+同残基号 构造(chain, resnum)键 标准 vs 非标准判定 水/配体/修饰残基分类 质控检验点 链数量是否合理?残基覆盖率?非标准残基是否备案? 识别陷阱:空格链、插入码、负残基号 跳过这些,你的解析器迟早要出问题

这张图要说明什么呢?从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 常见的质控校验点

基于本章的内容,落地到质控时,你需要至少检查这几项:

  1. 链标识完整性:是否有空字符串链?多条链是否用了相同ID?
  2. 残基序号连续性:是否存在残留的insertion code?序列跳跃是否超过预期?
  3. 标准残基覆盖率:分子中理论应有的氨基酸数量和实际构建的残基数比对。
  4. 非标准残基备案:所有出现过的非标准残基名称是否在预设的允许列表中?
  5. 原子名称一致性:同一残基类型的原子名称是否标准化?比如有没有把"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做自动化,只要理解了这些规则,你就能在第一时间识别出文件里的异常。