第2章:PDB文件解析——用BioPython搞定原子与链信息
解析PDB文件,是PDB预处理的第一步。我早年刚接触结构生物学时,面对动辄几十MB的PDB文件,手动翻看文本简直是噩梦。后来发现BioPython,才算是真正找到了趁手的工具。这一章,我们就手把手把PDB文件拆开,看看里面到底藏着什么。
2.1 为什么选BioPython?
PDB文件本质上是文本,但直接字符串解析极易出错——行宽、字段偏移、可选注释,坑太多。BioPython的 Bio.PDB 模块把这些全封装好了。你想想看,它能把PDB文件转成一个结构化的对象树:Structure → Model → Chain → Residue → Atom。每一层都能直接访问属性,省去你手撸解析器的工夫。
核心对象层级:
- Structure — 一个PDB文件,可能包含多个模型(NMR)
- Model — 一个构象,通常只有一个(X射线)
- Chain — 一条链,比如A、B
- Residue — 一个残基(氨基酸或核酸)
- Atom — 单个原子,包含坐标、元素、B因子等
我最早用的时候,还闹过笑话:直接读一个NMR的PDB文件,结果 get_list() 返回了好几个模型,我愣是以为代码写错了。嗯,这里要注意:如果是X射线晶体结构,通常只有一个模型;但NMR会有多个。
2.2 快速上手:读取PDB文件
先装包:
pip install biopython
然后是最基本的读取脚本。我个人习惯用 PDBParser,它严格按格式解析,遇到问题会报错。如果你想宽松一点,可以用 MMCIFParser 读mmCIF格式。
from Bio.PDB import PDBParser
parser = PDBParser(QUIET=True) # QUIET=True 关闭警告
structure = parser.get_structure("1ubq", "1ubq.pdb")
print("结构ID:", structure.get_id())
print("模型数量:", len(structure))
这段代码输出结构ID和模型数。我遇到过一件事:某次处理用户提供的PDB文件,文件名和内部ID不一致,get_structure 第一个参数其实是给内存中结构命名的,不是文件路径。别搞混了。
2.3 提取链信息
拿到模型后,下一步就是看链。每个模型包含若干链,链ID通常是单个字母。下面这段代码可以列出所有链及其包含的残基数:
for model in structure:
for chain in model:
print(f"链ID: {chain.get_id()}, 残基数: {len(chain)}")
很多PDB文件里,链ID不一定是'A'、'B',也可能是数字。我曾经处理过一个多蛋白复合物,链ID是'0'、'1'、'2',当时还以为是PDB文件坏了。其实只要按接口取就行,get_id() 返回的是字符串。
2.4 原子信息提取
这才是重头戏。常见的需求是:提取所有原子的坐标、元素、B因子、残基名称等。看代码:
for model in structure:
for chain in model:
for residue in chain:
# 跳过水分子和异质原子
if residue.get_id()[0] != ' ':
continue
for atom in residue:
print(
f"Atom: {atom.get_name():4s} | "
f"Res: {residue.get_resname():3s} {residue.get_id()[1]:4d} | "
f"Chain: {chain.get_id()} | "
f"Coord: {atom.get_vector():.3f} | "
f"BFactor: {atom.get_bfactor():5.2f}"
)
这里有个细节:residue.get_id() 返回一个元组 (hetero_flag, residue_number, insertion_code)。hetero_flag 为空格表示标准氨基酸,'H_' 开头表示异质原子(如配体、水)。我曾在项目中漏掉这个检查,结果把水分子也当氨基酸处理了,后面建模全乱套。避坑指南:一定要跳过 hetero_flag != ' ' 的残基。
2.5 常用属性一览
我把最常用的Atom和Residue属性整理成一个表格,方便你快速查阅:
| 对象 | 方法/属性 | 说明 | 返回值类型 |
|---|---|---|---|
| Atom | get_name() | 原子名,如 CA、CB、N | str |
| Atom | get_vector() | 坐标向量(x,y,z) | Vector |
| Atom | get_bfactor() | 温度因子(B因子) | float |
| Atom | get_occupancy() | 占有率 | float |
| Atom | get_element() | 元素符号,如 C、N、O | str |
| Residue | get_resname() | 残基名称,如 ALA、LYS | str |
| Residue | get_id() | 残基标识 (flag, num, insert) | tuple |
| Residue | child_list | 该残基下的所有Atom对象 | list |
| Chain | get_id() | 链ID,如 'A' | str |
小技巧:如果你只需要蛋白质主链原子,可以用 atom.is_metal() 或直接判断 atom.get_name() in ['N','CA','C','O']。我写过一次把 'OXT'(末端氧)漏掉的情况,后来特意加了检查。
2.6 关于模型索引的坑
大多数X射线PDB文件只有一个模型,但NMR文件可能有几十个。当你遍历 structure 时,默认会遍历所有模型。如果只想要第一个模型,用 structure[0] 即可。但注意:bioPython的 Structure 对象用字典存储模型,structure[0] 取的是模型编号(通常为0),不是Python列表索引。正确写法是:
model = structure[0] # 模型编号0
我曾经在这里栽过跟头:用 structure.get_list()[0],结果在某些PDB文件里模型编号不是从0开始,导致下标越界。所以直接用整数索引反而更安全。
2.7 完整的提取函数
下面是我常用模板的一个片段,把原子信息提取成字典列表,方便后续写入CSV或直接分析:
def extract_pdb_atoms(pdb_file):
parser = PDBParser(QUIET=True)
structure = parser.get_structure("tmp", pdb_file)
records = []
for model in structure:
for chain in model:
for residue in chain:
if residue.get_id()[0] != ' ':
continue
for atom in residue:
vec = atom.get_vector()
records.append({
"atom_name": atom.get_name().strip(),
"res_name": residue.get_resname().strip(),
"res_num": residue.get_id()[1],
"chain_id": chain.get_id(),
"x": vec[0], "y": vec[1], "z": vec[2],
"b_factor": atom.get_bfactor(),
"element": atom.get_element(),
})
return records
注意:PDB文件中某些原子名含有空格(如 " CA "),atom.get_name() 返回的字符串可能带空格,如果不 strip,后续做键匹配时会失败。你可以在存入前调用 .strip() 统一去除。
2.8 核心流程一览
为了让思路更清晰,我画了一张流程图,把PDB文件解析与信息提取的完整路径展示出来:
经验之谈:解析完成后,我通常第一时间检查链数量和残基数量是否与PDB文件头部的信息一致。如果不一致,大概率是过滤异质原子时出了疏漏。写一个简单的断言快速验证,能省去后期排查的大量时间。
这一章内容其实不多,但非常基础。BioPython的 Bio.PDB 模块把繁琐的行解析变成了优雅的对象调用。你只要记住对象层级和几个关键方法,大部分PDB解析工作都能顺手完成。后面几章我们会在这个基础上做更复杂的预处理,比如补全缺失原子、重命名链、生成坐标矩阵等——但根子都在这里。