第4章 解析PDB文件内容——把原子坐标读进内存
说到解析PDB文件,我觉得这是整个质控体系的基础。你要是连文件内容都读不准,后面什么验证、修复、统计都是空中楼阁。
我刚开始做结构解析那阵子,曾试着用一行Python脚本来读PDB。结果发现,这些看起来规规矩矩的文本文件,实际解析起来充满陷阱。
今天咱们就深入看看,怎么正确地把PDB文件里那些数据扒出来。
4.1 PDB格式的骨架
PDB文件本质上是个固定宽度的文本格式。每一行80个字符,每个字段的位置都是定死的。说白了,就是个纯文本的数据库表。
我个人习惯把PDB的记录类型分成三组:
| 记录类别 | 典型记录 | 作用 |
|---|---|---|
| 头信息 | HEADER, TITLE, KEYWDS, EXPDTA | 描述实验信息、来源、作者等元数据 |
| 坐标核心 | ATOM, HETATM, TER, END | 原子坐标、残基链、B因子等结构数据 |
| 附属记录 | CRYST1, REMARK, SSBOND, SITE | 晶胞、注释、化学键、活性位点等额外信息 |
你在解析的时候,必须清楚每种记录的字段偏移。少一个空格,读出来的数据就全乱了。
处理PDB文件之前,先确认它的版本。PDB v3.0开始对字段格式做了调整,特别是REMARK记录。我习惯在解析前用
grep "^HEADER"看一眼版本号,再决定用哪个解析模板。
4.2 坐标核心部分——占80%的工作量
ATOM记录是整个解析的重头戏。每行都代表一个原子,字段排列是这样的:
记录类型: ATOM
序列号: 1-6 (右对齐)
原子名称: 13-16 (左对齐)
残基名称: 18-20 (左对齐)
链ID: 22 (单字符)
残基号: 23-26 (右对齐)
插入码: 27 (可选项)
X坐标: 31-38 (右对齐, 三位小数)
Y坐标: 39-46
Z坐标: 47-54
占有率: 55-60 (一位小数)
B因子: 61-66 (两位小数)
元素: 77-78 (右对齐)
电荷: 79-80 (可选项)
你想想看,这就意味着你不能用简单的空格分割来解析。如果你的代码是line.split(),那十有八九会读错那些带空格的原子名。我在项目中遇到过好几次," CA "和"CA "被当成同一个东西,导致后续的残基匹配全崩了。
4.3 关键附属记录
除了坐标,有几个记录对质控特别重要。我挑几个重点说说。
4.3.1 CRYST1——晶胞参数
这条记录告诉你晶体的对称性和晶胞尺寸。很多验证和重计算都得靠它。
CRYST1 41.810 41.810 38.000 90.00 90.00 120.00 P 31 2 1 6
字段解析:
- a, b, c:晶胞边长,6-15列,右对齐
- α, β, γ:晶胞角度,16-25列
- 空间群:26-40列,例:P 31 2 1
不同版本的PDB文件在CRYST1记录里,空间群的格式并不统一。有些写
P 31 2 1,有些写P3121。我建议在加载完成后,统一用标准空间群符号映射表做一次标准化。
4.3.2 REMARK——自由文本的沼泽
REMARK记录是给人类看的,不是给机器看的。但有些质控指标会藏在里面,比如分辨率和R因子。
我见过最麻烦的解析任务是提取REMARK 200里分辨率的信息。它经常写成这样的格式:
REMARK 2 RESOLUTION. 2.50 ANGSTROMS.
你能直接取数字吗?不行。有些文件写成2.5,有些写2.50,甚至前面还会有杂乱的注释。我常用的办法是用正则表达式去抓(\d+\.?\d*)\s*ANGSTROMS,然后做容错转换。
4.4 解析流程——别自己造轮子
我强烈建议不要从零写PDB解析器。直接复用BioPython的PDB解析模块,再封装自己的质控逻辑。这样稳定,还省事。
不过,自己写一套轻量级解析方案,对理解格式本身帮助很大。下面是一段我自己项目里用过的简易解析代码:
def parse_atom_record(line):
"""从固定宽度的ATOM记录中提取字段"""
if not line.startswith(('ATOM', 'HETATM')):
return None
record = {}
record['type'] = line[0:6].strip()
record['serial'] = int(line[6:11].strip())
record['name'] = line[12:16].strip()
record['resname'] = line[17:20].strip()
record['chain'] = line[21].strip()
record['resnum'] = int(line[22:26].strip())
record['x'] = float(line[30:38].strip())
record['y'] = float(line[38:46].strip())
record['z'] = float(line[46:54].strip())
record['occupancy'] = float(line[54:60].strip())
record['bfactor'] = float(line[60:66].strip())
return record
这段代码能处理大部分标准的PDB文件。但我曾经遇到过一批老旧文件,字段不到80列,或者有异常的填充字符。这时候你就要加点容错逻辑,比如用.strip()配合if len(line) > 30的检查。
4.5 常见坑和实战建议
- 空行和注释行:解析时记得过滤掉空行以及以
ANISOU开头的各向异性温度因子记录,除非你关注这些数据。 - 链ID缺失:有些旧PDB文件链ID列是空格。我建议在加载时自动赋
'A'作为默认链。 - 残基编号重复:同一链里出现两个
1号残基的情况,在NMR模型里很常见。这时候得用insertion_code区分,或者启用model_index。 - B因子异常:我见过B因子是负数或者超过100的情况。解析时标记这类异常,但不要直接抛弃,留到后续质控再判断。
我曾经负责一个批次的PDB文件清洗任务,300个文件里有23个的ATOM记录不是80列。原因是一些旧软件在写入时用了混合长度。解法很简单:在解析前先对每行做
line.ljust(80)补齐。
好,你知道了怎么解析PDB文件,还了解了常见坑。下一步就是把这些解析出来的数据,套进质控规则里做自动化检测。不过那是后面章节的事了,今天先到这里。