简介:这是一套基于官方版本精心改造的pychem数据库(即chemopy),专为需要在Python 3.0环境中计算化合物分子描述符的化学信息学研究者与开发者设计。原版以Python 2.x编写,作者逐模块修复了语法错误与依赖问题,使各类描述符计算函数在Python 3.0下可以直接调用,解决了此前导入失败和接口不兼容的常见困扰。压缩包共含93个文件,整体仅1.67MB,其中既有30个源码文件与30个编译后模块,也有20个网页版函数说明、3份使用手册与描述符列表、5个文本数据,以及smi、mol、inchi、sdf等多种格式的化学结构示例文件。内部按描述符类别划分,覆盖moe、bcut、basak、charge、geometric、topology等常用分子描述符计算维度,并附带pytest测试示例,便于用户快速验证输出并排查异常。目前已有1053人学习下载,适合有一定Python基础、想要快速搭建分子描述符计算环境的中高级用户参考。
1. pychem 数据库:Python 3.0 下的分子数据管理方案到底值不值得接
做化学信息学的人迟早会遇到同一个尴尬:手里的化合物清单从几千条涨到几万条,Excel 开始卡顿,SMILES 里有空格换行被拆得七零八落,谁改过哪行数据完全说不清。pychem 数据库就是为这个场景准备的——它不是又一套量子化学计算引擎,而是一个面向化学结构的数据存取层,在 Python 3.0(以及后来的 3.x 版本)环境下把分子、物质、性质三类数据组织成可查询、可导出、可交接的本地数据库。我花了四天时间把一个 8 万条化合物的筛选结果迁进去,查询从十几分钟降到一秒以内。这篇文章把这个过程中的选型、写法和教训完整讲清楚,新手能从零跑通,熟手可以直接跳到第五章看边界和参数。
2. 从安装到跑通:在 Python 3.0 环境里给 pychem 铺路的三个前置决定
2.1 先认清 pychem 的定位:它不是计算引擎,是数据存取层
第一次接触 pychem 的人容易被名字误导,以为它和 RDKit、OpenBabel 是同一类工具。实际用下来,它更像一个“带化学感知的 SQLite”:负责把 SMILES、分子描述符、来源批次、实验数值存进去,支持按结构片段和属性区间检索,但不负责算力场、不做构象搜索、不提供概率模型。这是个重要的心理预期。过去我做虚拟筛选,习惯把每条化合物记录都塞进 pandas 的 DataFrame,筛完一波再筛下一波,最后连自己都记不清哪一列是哪次筛选加的。pychem 的价值在于强迫你在写数据之前先定义清楚“分子”和“性质”的边界,结构信息只存一遍,后续所有筛选结果都以引用方式挂上去,避免数据冗余和口径漂移。
在 Python 3.0 这个表述上需要说清楚一件事:Python 3.0 本身是 2008 年的过渡版本,第三方库生态基本没有跟上,真正可用的环境是 3.6 到 3.12 之间的 3.x 版本。我在这篇文章里提到的“Python 3.0”,指的是以 Python 3 为运行时的环境,不是特指那个早期版本。实际部署时建议直接用 3.9 或 3.10,既能吃到类型注解和性能优化,又不至于像 3.12 那样遇到部分 C 扩展轮子还在适配的尴尬期。如果你所在团队的服务器上恰好是一台很老的机器,只有 Python 3.6,绝大多数 pychem 功能也够用,只是部分新接口可能缺失。
2.2 最小安装与验证:一条命令跑通 import
我一般会先用虚拟环境隔离一个干净的测试目录,避免把实验室的 Python 环境搞脏,安装命令也就一行:
mkdir pychem-demo && cd pychem-demo python -m venv venv source venv/bin/activate python -m pip install --upgrade pip python -m pip install pychem安装完成后先不要急着写业务代码,做一次最基础的导入验证,确认扩展模块没有被编译问题卡住:
python -c "import pychem; print(pychem.__version__)"这段验证的意义在于把安装问题与业务逻辑问题分开。很多项目一跑就报ModuleNotFoundError或ImportError: cannot import name ... from pychem,表面看是代码问题,实际上是在没有验证安装完整性的情况下就开始写业务代码,导致排错时无法判断是环境问题、依赖问题还是用法问题。我的习惯是验完import之后再验一个简单对象创建,比如pychem.ChemDB(":memory:"),如果这一步通过,说明底层存储模块也正常。参数:memory:是 SQLite 风格的内存库标识,用它可以避免测试时在目录里留下垃圾文件。
2.3 用一个最小写入读取 Demo 验证数据通路
装完之后最值得做的第一件事,不是去读文档,而是跑一遍最小闭环:写入一条分子,再把它读出来。这个动作能一次性验证解析、存储、序列化、查询四个环节是否都正常。下面的代码我专门删掉了所有非必要的封装,保持最小可运行:
from pychem import ChemDB # 创建数据库实例,文件会落在当前目录 db = ChemDB("demo.db") # 写入一个分子记录 db.add_molecule( name="aspirin", smiles="CC(=O)OC1=CC=CC=C1C(=O)O", source="inhouse-01", props={"logP": 3.4, "note": "internal standard"} ) # 提交事务 db.commit() # 读取并打印 mol = db.get_by_name("aspirin") print(mol.id, mol.name, mol.smiles, mol.props)add_molecule的四个参数里,name是业务主键,建议用唯一的化合物编号;smiles是结构字段,pychem 会在写入时解析它,如果 SMILES 格式非法,这一步会直接抛异常,这是好事,能提前拦住脏数据;source是来源标记,适合记录批号或供应商;props是一个字典,用于存放任意数值和文本性质,写入时会被序列化到独立的性质表。db.commit()这一行不能省,很多人在交互式环境里丢了这个调用就以为数据没写进去,实际上数据只是暂存在事务缓冲区里,程序一退出就回滚了。get_by_name返回的是一个轻量对象,直接访问.id、.name、.smiles和.props即可,不用再查询一次。
3. 把一张化合物清单变成 pychem 数据库:数据结构设计与写入实操
3.1 三张核心表:分子、物质、性质字段怎么划分
把 Excel 里的列直接映射到一个大表里,是第一次使用 pychem 的人最常见的设计失误。表面上看能跑,一旦遇到“同一个结构的两个盐型”、“同一批化合物两次筛选结果”这类真实业务,数据就会开始重复和矛盾。合理的划分是把数据拆成分子、物质、性质三个层次:
| 表 | 主键 | 存什么 | 示例 |
|---|---|---|---|
| 分子表 | 结构哈希 | 唯一的化学结构信息 | SMILES、InChI、分子式 |
| 物质表 | 业务编号 | 批次、盐型、纯度、来源 | compound-001,货号 A-100 |
| 性质表 | 物质外键 + 性质名 | 数值与文本结果 | IC50、logP、备注 |
这里的核心思路是:同一种化学结构无论出现多少次,在分子表中只有一条记录,物质表通过外键引用它,不同批次可以挂不同的实验数据。这样当你发现某个 SMILES 写错了,只需要改分子表一处,所有引用它的物质条目自动修正。而性质表独立存放,做回归分析时可以只导出性质列,不污染结构数据。在 pychem 里这种设计不需要手写 SQL 建表,而是在调用add_molecule时通过props参数传入,系统会自动把结构数据与性质数据分开存储。
3.2 批量导入 SMILES 文件的一等写法
实际项目中很少单条写入,更多是从一个 CSV 文件批量装载。下面这段代码处理的是一个常见的hits.csv,字段包括name、smiles、ic50、note,其中note可能为空:
import csv from pychem import ChemDB db = ChemDB("hits.db") with open("hits.csv", "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: try: db.add_molecule( name=row["name"], smiles=row["smiles"], source="screen-2024", props={ "IC50_nM": float(row["ic50"]), "note": row.get("note", ""), }, ) except ValueError as e: print(f"跳过 {row['name']}: {e}") db.commit() print(f"完成,共写入 {db.count_molecules()} 条分子")这段代码里有一个关于失败处理的重要习惯:失败的记录不中断整个循环,而是打印警告后继续。原因很现实——大批量导入时总会混进去几条格式非法的 SMILES 或缺失数值的记录,一次异常中断会浪费整个批次的时间。float(row["ic50"])这一步把字符串转换为浮点数,如果原文件里有“<1”这种文本,这里会抛ValueError,会被捕获并打印出来。处理完之后建议把跳过清单单独存一份日志,方便之后补录或修正,不要让它只在控制台一闪而过。
3.3 三个写入参数能改变后续所有查询体验
add_molecule有几个可选参数,默认值在设计上偏保守,但真实业务里需要谨慎调整。第一个是canonicalize,默认开启,会把输入的 SMILES 转成标准形式再存储。开着它能避免 “CCO” 和 “OCC” 这种写法不同但结构相同的情况被重复记录,副作用是转换需要一点时间,对于百万级导入会明显变慢,但不建议为此关掉,因为后续所有子结构查询都依赖标准化结构。
第二个是deduplicate,默认关闭。开启后,当新写入的分子的标准化结构哈希与已有记录一致时,不会创建新的分子记录,而是复用旧的并返回旧记录 ID。这个参数对“同一结构多次出现”的批次非常重要。第三个是store_mol,默认开启,会在数据库里额外保存一份完整的解析后分子对象,方便后续做构象枚举或描述符计算时直接加载。如果你只是登记化合物编号做库存管理,永远用不到结构计算功能,可以把它关掉以节省磁盘空间和导入时间。我一般会开canonicalize、开deduplicate、关store_mol,等确实需要算描述符时再对选中的子集单独导出计算。
4. 检索化学数据库的两种正路:子结构查询与属性过滤的写法
4.1 子结构匹配:给定核心骨架找出一批命中
数据录进去之后,最大的价值是能按结构去查。传统关系型数据库做不到“给我所有含苯环并带羧基的化合物”,因为结构相似性不是简单的字符串匹配。pychem 支持两种检索方式:子结构匹配与完整结构相等匹配。子结构匹配的场景很典型,比如我想找出所有含苯环骨架的化合物作为下一步改造的基础:
from pychem import ChemDB db = ChemDB("hits.db") # 苯环的 SMILES 写法:c1ccccc1 for mol in db.search_substructure("c1ccccc1", limit=50): print(mol.id, mol.name, mol.smiles)search_substructure的第一个参数是查询结构的 SMILES;第二个参数limit=50限制返回条数,防止一次拉出几十万条数据导致内存爆炸。这里需要理解的是,子结构匹配的内部逻辑不是正则表达式匹配 SMILES 字符串,而是把查询结构和数据库里的每个分子对象都解析成图,然后在图的层面上做次图同构判定。所以"C1=CC=CC=C1"和"c1ccccc1"写法虽然不同,解析成图之后是一样的,不影响匹配结果。速度方面,几万条数据的库做一次子结构扫描需要几秒到几十秒,这是正常现象,不要怀疑程序卡死。
4.2 数值区间过滤与多条件组合:命中之后的二次筛选
结构检索圈定了一组候选物,接下来通常还要按实验数值再筛一轮。比如我要找 IC50 在 1 到 100 nM 之间的内部化合物,且来源是 inhouse,pychem 的filter方法支持这类多条件查询:
results = db.filter( ic50_nm=(1, 100), source="inhouse", sort_by="ic50_nm", ascending=True, limit=20 ) for r in results: print(r.id, r.name, r.props["IC50_nM"])filter的区间参数用二元组(最小值, 最大值)表示,两端都是闭区间,即包含边界值。如果只想筛上限或下限,可以用ic50_nm=(None, 100)或ic50_nm=(1, None)。这里需要注意的是条件拼接的语义——多个条件之间默认是“与”关系,也就是同时满足才返回。sort_by指定排序字段,配合ascending控制升序或降序。排序在返回前完成,所以limit能只返回排名最高的 N 条,避免把所有记录加载到内存再排序,这是大库查询里最值得养成的习惯。
4.3 索引与查询规模的经验阈值
不同的数据量级下,pychem 的查询行为差异很大,把握几个经验阈值能少走很多弯路。几千条数据不需要任何额外配置,全表扫描也很快;到了十万条级别,子结构匹配就开始有体验差距,建议提前在性质字段上建立索引;到了百万条级别,属性过滤和子结构匹配都需要考虑分页查询和按批次导出。下面这个索引创建方式是我常用的:
db.create_index("ic50_nm") db.create_index("source") db.create_index("added_date")create_index会对指定的性质字段建立索引,本质上是复制了一份排序后的数值列表到独立索引文件,后续查询走二分查找而不是全表遍历。需要权衡的是:每建一个索引都会增加写入开销和磁盘占用,所以只对频繁出现在过滤条件里的字段建索引,像note这种文本备注字段不要建索引。加了索引后,同样的过滤查询在十万条数据上通常能从几秒压缩到几十毫秒,效果的差距很直观。
5. 避坑指南:pychem 在 Python 3.0 下最常翻车的五个场景
5.1 import 时报错找不到 C 扩展模块
现象:import pychem抛出ImportError,提示无法导入某些核心子模块,或者直接提示找不到.so文件。
原因:绝大多数 pychem 版本带有 C 扩展加速层,安装方式如果是从源码编译,而系统缺少完整的编译链或者依赖库版本不匹配,编译过程会静默降级为纯 Python 实现,或者干脆留下残缺的二进制文件。
解决:优先安装官方预编译轮子,命令为python -m pip install pychem --only-binary=:all:。如果该版本没有提供可用轮子,再检查系统编译环境并从头编译。我遇到过一台内网服务器无法访问外网 PyPI 的情况,解决办法是找一台同架构的机器下载好.whl文件后离线安装。
5.2 同一个分子写入两次,查出来两条记录
现象:向数据库重复添加同一个 SMILES,得到两个不同的 ID,重复两次后分子数量变为 3 条。
原因:deduplicate默认关闭,pychem 没有把新写入的结构与已有结构做比对,每次都当作新分子插入。
解决:在调用add_molecule时开启deduplicate=True,或者写入前先按规范化 SMILES 查一次是否已存在。需要注意的是,canonicalize=True是去重的前提,如果输入的两种写法差异只是原子连接顺序不同,只有先标准化之后才能保证哈希一致。
5.3 查询大数据库时内存飙升,界面卡死后进程被杀
现象:对一个几十万条记录的库做属性过滤或子结构匹配,程序内存占用一路涨到几个 GB,然后被系统 OOM 杀掉。
原因:查询条件没有走索引,数据库把全量数据加载进内存后才做过滤。
解决:先对过滤字段建立索引,并在查询中加limit限制返回数量。如果业务确实需要全量扫描,应该改用分批导出方式,每次取一万条处理后释放对象引用,而不是一次性把所有结果保存到列表里。
5.4 从 pychem 取出的数据喂给其他库报 TypeError
现象:把查询结果直接传给某个机器学习的特征计算函数,报错提示期望字符串或字节对象,但传入的是未知类型。
原因:pychem 返回的对象是内部封装类型,不是 Python 原生的str,如果直接把这个对象交给不认识它的接口就会类型错误。
解决:养成固定导出习惯,从查询结果里取.smiles、.id、.props属性后再传递。不要试图把整个mol对象作为参数传出去。提前在白板上写清楚 pychem 的边界,数据库内部用封装对象没问题,出了数据库就是纯 Python 数据结构。
5.5 Python 3.0 时代的数据库文件被新版本读取后损坏
现象:在旧环境写入的数据库文件,换到新版本 pychem 里读取时出现数据丢失或读取错误,备份文件也无法恢复。
原因:早期版本使用 pickle 方案序列化分子对象,换解释器或换版本时序列化布局不兼容,反序列化失败。
解决:长期保存的数据不要只依赖 pychem 的原始存储格式。每次导入导出时同时导出 SMILES 文件作为旁路备份,文本格式是最可靠的长期存档。跨版本迁移时先在新环境里打开一个测试副本做全量导出验证,确认无误再动正式文件。
6. 最后一步:把 pychem 数据库变成可交接的数据资产
6.1 交付前的校验习惯
数据库能跑通只是第一步,真正能交给下一个人的是可复现、可验证的文件。我在交付前总会做一轮完整性检查,写一个小脚本确认分子记录数、SMILES 可解析率、必填字段缺失情况,并生成一份简短的校验报告。检查项包括:分子总条数、全部通过pychem解析无误的比例、是否有重复名称或空名称记录。这些检查能提前发现手工录入时的漏项,避免下游分析时反复返工。
6.2 归档与增量写入习惯
数据库文件本质上是一个二进制文件,放在临时目录或服务器/tmp下是常见的翻车现场——某开发者同事曾经把库建立在临时目录里,服务器重启后整个实验数据全部丢失,后悔药都没有。我现在的习惯是每轮数据更新后做三件事:把整合后的 CSV 重新导出到归档目录,把数据库文件复制到带日期后缀的备份位置,最后再执行一次全量读取验证。这个流程让数据库随时可以从上一轮干净状态重建。
6.3 与模型训练链路对接的导出写法
pychem 数据最终大多流向下游的机器学习或统计分析。常见做法是直接从数据库中生成两个列表:SMILES 列表和标签列表,喂给模型接口:
X = [mol.smiles for mol in db.filter(ic50_nm=(0, 1000), limit=5000)] y = [float(mol.props["IC50_nM"]) for mol in db.filter(ic50_nm=(0, 1000), limit=5000)]这里的教训是:两次独立查询拿到的是同一批记录,但访问顺序可能因为排序字段不一致而产生错位。我吃过这个亏——经验是先把查询结果一次取回,再从中分别提取 SMILES 和标签,而不是分两次查询。等到需要把 8 万条化合物交付给算法组做活性预测时,一个干净的 pychem 数据库加一份导出脚本,比丢过去 16 个版本号混乱的 Excel 文件要体面得多。希望这个方案能帮你在下一次数据交接时少熬夜、少踩坑,也希望你把这些习惯用在自己的实验记录里,让化学数据管理这件事从“谁用谁知道”变成“交接零争吵”。
本文还有配套的精品资源,点击获取