第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属性整理成一个表格,方便你快速查阅:

对象方法/属性说明返回值类型
Atomget_name()原子名,如 CA、CB、Nstr
Atomget_vector()坐标向量(x,y,z)Vector
Atomget_bfactor()温度因子(B因子)float
Atomget_occupancy()占有率float
Atomget_element()元素符号,如 C、N、Ostr
Residueget_resname()残基名称,如 ALA、LYSstr
Residueget_id()残基标识 (flag, num, insert)tuple
Residuechild_list该残基下的所有Atom对象list
Chainget_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文件解析核心流程 PDB文件(.pdb) PDBParser → Structure对象 遍历 Model(通常只1个) 遍历 Chain → 获取链ID 遍历 Residue → 过滤异质原子 (hetero_flag == ' ') 遍历 Atom → 提取名字、坐标、B因子 输出:字典/CSV/DataFrame 常见坑点 • 水分子残基 hetero_flag='W' • 配体如 ATP 需单独处理 • NMR多模型需指定编号 • 原子名可能带空格

经验之谈:解析完成后,我通常第一时间检查链数量和残基数量是否与PDB文件头部的信息一致。如果不一致,大概率是过滤异质原子时出了疏漏。写一个简单的断言快速验证,能省去后期排查的大量时间。

这一章内容其实不多,但非常基础。BioPython的 Bio.PDB 模块把繁琐的行解析变成了优雅的对象调用。你只要记住对象层级和几个关键方法,大部分PDB解析工作都能顺手完成。后面几章我们会在这个基础上做更复杂的预处理,比如补全缺失原子、重命名链、生成坐标矩阵等——但根子都在这里。