1. 项目概述:从化合物名称到结构化数据的自动化桥梁
在药物研发、材料科学或者化学信息学领域工作的朋友,大概率都遇到过这个场景:手头有一长串化合物的名称,可能是从文献里摘录的,也可能是高通量筛选实验出来的候选列表。你需要获取这些化合物的详细信息,比如分子式、分子量、SMILES结构式、LogP、氢键供受体数等等。如果只有几个化合物,手动上PubChem网站搜一下,复制粘贴也就罢了。但一旦这个列表膨胀到几十、几百甚至上千个,手动操作就成了一场噩梦,不仅效率低下,还极易出错。
“利用化合物名称从PubChempy中批量下载化合物信息”这个项目,就是为了解决这个痛点。它的核心思路很简单:将重复、繁琐的人工查询工作,转化为由脚本驱动的自动化流程。PubChempy是一个优秀的Python库,它封装了访问PubChem这座全球最大免费化学数据库的API。我们通过它,用代码“告诉”PubChem我们想要哪些化合物,以及需要它们的哪些属性,然后程序会自动完成查询、解析、整理数据并保存的全过程。
这不仅仅是节省时间。想象一下,你需要比较上百个类似物的物化性质,手动操作可能导致数据格式不统一、来源记录缺失。而自动化脚本能确保每次获取的数据字段、格式都完全一致,极大提升了后续数据分析的可靠性和可重复性。无论是用于构建QSAR模型的特征矩阵,还是为虚拟筛选准备配体库,这个自动化流程都是基础且关键的一步。
2. 核心工具与原理:PubChempy与PubChem PUG REST API
在动手写代码之前,我们必须先理解背后的“引擎”是如何工作的。整个项目的基石是两个部分:PubChem数据库和PubChempy这个Python封装库。
2.1 PubChem PUG REST API:数据之源
PubChem提供了多种访问方式,其中PUG REST API是最适合程序化访问的接口。你可以把它想象成一个专门为机器设计的“问询处”。我们向一个特定的网址(URL)发送一个请求,这个请求里包含了我们的问题(比如“请告诉我阿司匹林的信息”),然后API就会返回一份结构化的答案(通常是XML或JSON格式)。
一个典型的请求URL长这样:https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/aspirin/property/MolecularWeight,CanonicalSMILES/JSON我们来拆解一下这个链接:
https://pubchem.ncbi.nlm.nih.gov/rest/pug/是API的根地址。compound表示我们要查询的是化合物。name表示我们使用化合物名称进行查询(还可以用cid用ID查,用smiles用结构查)。aspirin就是我们要查询的具体名称。property/后面跟着我们想要获取的属性列表,这里用逗号分隔。JSON指定返回数据的格式为JSON,便于程序解析。
手动在浏览器里输入这个链接,你就能直接看到返回的数据。而我们的任务,就是用代码自动拼接这样的URL,发送请求,并处理返回的结果。
2.2 PubChempy库:优雅的封装器
虽然我们可以直接用Python的requests库去直接调用上述API,但PubChempy库的存在让这件事变得异常简单和“Pythonic”。它帮我们处理了诸多底层细节:
- URL拼接:你不需要自己记住复杂的URL结构,只需要调用像
get_compounds(‘aspirin’, ‘name’)这样的函数。 - 错误处理:网络超时、化合物未找到、服务器错误等常见问题,库提供了相对友好的异常机制。
- 数据解析:它将返回的JSON或XML数据自动解析成Python对象(如
Compound对象),你可以通过compound.molecular_weight这样的属性直接访问数据,而不需要自己去解析复杂的JSON结构。 - 批量操作:库本身对批量查询有一定支持,虽然我们还需要一些额外的逻辑来构建健壮的流程。
简单来说,PubChempy在强大的PUG REST API之上,为我们铺了一层平整的柏油路,让我们能更专注于业务逻辑,而不是HTTP请求和数据解析的坑洼。
2.3 关键属性选择:你需要下载什么?
在批量下载前,必须想清楚你需要哪些信息。PubChem为每个化合物存储了海量信息,全部下载既不现实也没必要。常见的、在药物设计或初步分析中常用的属性包括:
- 标识类:
CID(PubChem唯一标识符),IUPACName,CanonicalSMILES(规范SMILES字符串),InChIKey。 - 基础物化性质:
MolecularWeight,MolecularFormula,XLogP(脂水分配系数),TPSA(拓扑极性表面积)。 - 药代动力学相关:
HydrogenBondDonorCount,HydrogenBondAcceptorCount,RotatableBondCount。 - 复杂描述符:有些更复杂的描述符可能需要通过其他方式计算,但PubChem也提供了一些,如
Complexity(分子复杂度评分)。
在脚本中,我们会定义一个property_list,明确列出这些需要的属性字段。这步规划很重要,它决定了最终生成的数据表的结构和实用性。
3. 脚本设计与核心代码拆解
接下来,我们进入实战环节,一步步构建这个批量下载脚本。我会先给出一个完整的、可运行的脚本框架,然后逐一拆解其中的关键部分。
3.1 完整脚本框架
import pandas as pd from pubchempy import get_compounds, Compound import time import logging from typing import List, Optional # 配置日志,方便追踪运行过程和排查错误 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) def fetch_compound_info(compound_name: str, properties: List[str]) -> Optional[dict]: """ 根据化合物名称获取指定属性信息。 Args: compound_name (str): 化合物名称。 properties (List[str]): 需要获取的属性列表。 Returns: Optional[dict]: 包含属性信息的字典,如果查询失败或未找到则返回None。 """ try: # 使用名称查询,返回一个化合物列表 compounds = get_compounds(compound_name, 'name') if not compounds: logger.warning(f"未找到化合物: {compound_name}") return None # 通常取第一个结果(最相关)。注意:名称查询可能有歧义! compound = compounds[0] result = {'Query_Name': compound_name} # 遍历所需属性,尝试从compound对象中获取 for prop in properties: # 使用getattr安全获取属性,避免因属性不存在而崩溃 value = getattr(compound, prop, None) result[prop] = value # 额外记录CID,作为唯一标识 result['CID'] = compound.cid logger.info(f"成功获取: {compound_name} (CID: {compound.cid})") return result except Exception as e: # 捕获所有异常,如网络错误、解析错误等 logger.error(f"查询化合物 '{compound_name}' 时发生错误: {e}") return None def batch_download_from_names(name_list: List[str], output_file: str = 'compound_data.csv'): """ 批量下载化合物信息主函数。 Args: name_list (List[str]): 化合物名称列表。 output_file (str): 输出CSV文件名。 """ # 定义需要下载的属性字段 desired_properties = [ 'iupac_name', # IUPAC名称 'canonical_smiles', # 规范SMILES 'molecular_formula', # 分子式 'molecular_weight', # 分子量 'xlogp', # 脂水分配系数 'tpsa', # 拓扑极性表面积 'h_bond_donor_count', # 氢键供体数 'h_bond_acceptor_count', # 氢键受体数 'rotatable_bond_count' # 可旋转键数 ] all_results = [] total = len(name_list) for idx, name in enumerate(name_list, 1): logger.info(f"正在处理 ({idx}/{total}): {name}") data = fetch_compound_info(name, desired_properties) if data: all_results.append(data) else: # 即使失败,也记录一个只有查询名的空行,保证输出行数一致 all_results.append({'Query_Name': name}) # 礼貌性延时,避免对PubChem服务器请求过快 time.sleep(0.2) # 将结果列表转换为DataFrame df = pd.DataFrame(all_results) # 重排列顺序,将Query_Name和CID放在前面 column_order = ['Query_Name', 'CID'] + [p for p in desired_properties if p in df.columns] # 确保只排列存在的列 df = df.reindex(columns=[col for col in column_order if col in df.columns]) # 保存到CSV文件 df.to_csv(output_file, index=False, encoding='utf-8-sig') # 使用utf-8-sig支持Excel中文 logger.info(f"批量下载完成!共处理{total}个化合物,成功获取{len(df[df['CID'].notna()])}个。数据已保存至: {output_file}") # 打印简要统计 failed = df[df['CID'].isna()] if not failed.empty: logger.warning(f"以下化合物未能成功获取信息:{list(failed['Query_Name'])}") # 使用示例 if __name__ == '__main__': # 这里替换成你的化合物名称列表 my_compound_names = [ 'aspirin', 'paracetamol', 'caffeine', 'ibuprofen', 'metformin', 'ThisIsANotExistCompound' # 用于测试错误处理 ] batch_download_from_names(my_compound_names, 'my_compounds_info.csv')3.2 核心函数fetch_compound_info深度解析
这个函数是数据获取的原子操作单元,其健壮性决定了整个批量任务的成败。
关键点1:查询与结果处理compounds = get_compounds(compound_name, 'name')这行代码是核心。它返回一个列表,因为一个名称可能对应多个同分异构体或不同数据库条目。我们的策略是取第一个(compounds[0]),这通常是PubChem认为最匹配、最常见的形式。但这隐含了一个重要风险:名称歧义。比如“D-glucose”和“L-glucose”是不同的化合物,但如果你只查询“glucose”,返回的第一个结果可能只是其中一种。对于精确性要求高的场景,需要使用更精确的标识符(如InChIKey)或包含立体化学信息的名称。
关键点2:安全属性获取我们使用getattr(compound, prop, None)来获取属性。getattr是Python的内置函数,它尝试从compound对象获取名为prop的属性,如果该属性不存在,则返回我们指定的默认值None。这比直接使用compound.prop要安全得多,因为并非所有化合物都拥有我们列表中的每一个属性(例如,某些属性计算失败可能为null)。直接访问不存在的属性会引发AttributeError并导致程序崩溃。
关键点3:错误隔离整个函数被包裹在try...except块中。PubChempy在底层会发起网络请求,可能遇到各种问题:网络连接失败、请求超时、PubChem服务暂时不可用、返回的数据格式意外等。将这些错误捕获在单个化合物的处理环节,并记录日志、返回None,可以保证即使某个化合物查询失败,整个批量流程也不会中断,能够继续处理列表中的下一个化合物。这是批量处理脚本必须具备的容错能力。
3.3 批量控制与延时策略
在batch_download_from_names主函数中,有两个细节对长期稳定运行至关重要。
循环与进度反馈:for idx, name in enumerate(name_list, 1):这里的enumerate带了一个起始值1,让打印的进度从“1/总数”开始,更符合阅读习惯。清晰的日志(正在处理 (idx/total): name)能让你在运行一个长达数小时的任务时,随时了解进度,心里有底。
请求延时(time.sleep(0.2)): 这行代码至关重要。PubChem是一个公共的、免费的资源,其服务器有负载限制。如果我们以极快的速度(比如每秒几十次)连续发送请求,很可能触发服务器的速率限制(rate limiting),导致IP地址被暂时封禁,后续所有请求都会失败。time.sleep(0.2)意味着在每个请求后暂停0.2秒,相当于将请求频率限制在每秒5次左右。这是一个比较保守且礼貌的间隔,能有效避免被封。对于成百上千的批量任务,耐心一点是值得的。你也可以根据实际情况调整这个值,但绝不建议低于0.1秒。
3.4 数据整理与输出
我们使用pandas的DataFrame来整理数据,这是Python数据分析的事实标准,非常方便。
- 处理缺失数据:即使某个化合物查询失败,我们仍会向
all_results列表中添加一个只包含‘Query_Name’的字典。这保证了最终输出的CSV文件行数与输入列表完全一致,便于后续对照检查。失败的行中,除Query_Name外的列均为空(NaN)。 - 列顺序整理:通过
reindex方法,我们将Query_Name和CID这两列最关键的标识信息放在表格最前面,方便查看。 - 编码选择:
df.to_csv(..., encoding='utf-8-sig')。utf-8-sig编码会在文件开头添加一个特殊的字节顺序标记(BOM)。对于Windows系统下的Excel,这个标记能帮助其正确识别UTF-8编码,避免打开CSV时出现中文乱码。如果你的数据全是英文,使用utf-8也可以。
4. 高级技巧与实战优化方案
基础的脚本跑起来后,我们会发现一些可以优化和深入的地方。下面分享几个从实战中总结出来的进阶技巧。
4.1 应对查询失败与结果验证
基础脚本已经处理了网络错误和“未找到”的情况。但在实际使用中,还有两类常见问题:
1. 查询到错误化合物(名称歧义)这是最隐蔽的问题。脚本显示“成功获取”,但下载下来的SMILES可能不是你想要的分子。如何验证?
- 手动抽查:对于关键化合物,务必用下载到的
CanonicalSMILES或CID,去PubChem网站反查一下,看看结构是否正确。 - 质量过滤器:可以在脚本中增加逻辑,对结果进行初步筛选。例如,检查获取到的
molecular_weight是否在合理范围内(比如不是0或None),或者canonical_smiles是否包含你期望的特定子结构(这需要更复杂的化学信息学库,如RDKit)。
2. 服务器返回不完整或异常数据有时由于服务器负载或化合物本身数据问题,某些属性可能返回None,即使这个属性理论上应该存在。
- 二次重试机制:可以修改
fetch_compound_info函数,当发现核心属性(如cid,canonical_smiles)为None时,自动重试1-2次。重试前可以加一个稍长的延时(如1秒)。
def fetch_compound_info_with_retry(compound_name, properties, retries=2): for attempt in range(retries): data = fetch_compound_info(compound_name, properties) if data and data.get('CID') is not None: return data logger.warning(f"第{attempt+1}次获取{compound_name}的CID失败,准备重试...") time.sleep(1) # 重试前等待更久 logger.error(f"化合物{compound_name}在{retries}次重试后仍失败。") return {'Query_Name': compound_name}4.2 输入列表的预处理与清洗
你的化合物名称列表来源可能很杂:Excel里复制出来的、PDF里提取的、手打的。里面常常包含各种“噪音”,直接查询必然失败。
必须进行的清洗步骤:
- 去除首尾空格:
name.strip() - 处理换行符:
name.replace(‘\n’, ‘’).replace(‘\r’, ‘’) - 统一大小写:对于PubChem,名称查询通常是大小写不敏感的,但为了统一,可以全部转为小写
name.lower()。但要注意,有些名称的大小写是有化学意义的(如DOPA和dopa),需谨慎。 - 处理特殊字符和括号:有时名称里会包含
*,,,(,)等。这些字符在URL中可能需要编码。PubChempy内部会处理一部分,但最稳妥的办法是在传入列表前,就用简单的字符串方法清理掉非必要字符。不过,括号经常是名称的一部分(如(S)-ibuprofen),不能简单删除。
一个建议的流程是:先对原始列表进行基础的空白字符清理,然后运行脚本。对于失败的条目,单独拿出来进行人工检查或更复杂的清洗(如使用正则表达式),而不是一开始就进行可能破坏信息的强力清洗。
4.3 性能优化与大规模处理
当你的列表有上万条时,简单的串行循环加上0.2秒延时,总耗时将非常可观(2000条就需要近7分钟)。此时可以考虑以下优化:
1. 并发请求(谨慎使用)通过concurrent.futures库使用线程池,可以同时发起多个请求,大幅缩短总时间。
from concurrent.futures import ThreadPoolExecutor, as_completed def batch_download_parallel(name_list, output_file, max_workers=5): desired_properties = [...] # 同上 all_results = [] with ThreadPoolExecutor(max_workers=max_workers) as executor: # 提交所有任务 future_to_name = {executor.submit(fetch_compound_info, name, desired_properties): name for name in name_list} for future in as_completed(future_to_name): name = future_to_name[future] try: data = future.result() if data: all_results.append(data) else: all_results.append({'Query_Name': name}) except Exception as e: logger.error(f"处理{name}时发生未捕获异常: {e}") all_results.append({'Query_Name': name}) # 注意:并行时,延时控制变得复杂,依赖线程池本身的调度和服务器端限制。 # 后续保存DataFrame的代码同上 ...重要警告:并发虽快,但对PubChem服务器不友好,极易触发速率限制导致全体失败。
max_workers务必设置得非常小(建议3-5),并且最好在请求间保留少量延时(可以在fetch_compound_info函数内部添加)。滥用并发可能导致你的IP被暂时封禁。对于公共免费API,保持礼貌比追求速度更重要。
2. 断点续传处理超大列表时,网络中断或程序崩溃可能导致前功尽弃。可以实现一个简单的断点续传逻辑:在循环开始前,先尝试加载已存在的输出文件,读取已成功获取到CID的化合物名称集合。在循环处理每个名称时,先检查它是否已存在于这个集合中,如果存在则跳过。这样,即使程序中途停止,重新运行时会自动跳过已处理的部分。
4.4 结果数据的后处理与应用
拿到compound_data.csv后,工作才刚刚开始。Pandas提供了强大的数据清洗和分析能力:
- 处理缺失值:
df.dropna(subset=[‘CID’])可以过滤掉所有完全失败的记录。df.fillna(...)可以用特定值(如0或‘N/A’)填充某些属性的缺失值。 - 数据类型转换:从PubChem获取的数字可能是字符串格式,需要转换:
df[‘molecular_weight’] = pd.to_numeric(df[‘molecular_weight’], errors=‘coerce’)。 - 描述性统计:快速查看物化性质的分布:
df[[‘molecular_weight’, ‘xlogp’, ‘tpsa’]].describe()。 - 筛选化合物:例如,筛选出符合“类药五原则”(Lipinski’s Rule of Five)的化合物:
rule_of_five_df = df[ (df[‘molecular_weight’] <= 500) & (df[‘xlogp’] <= 5) & (df[‘h_bond_donor_count’] <= 5) & (df[‘h_bond_acceptor_count’] <= 10) ].copy()5. 常见问题与排查技巧实录
在实际操作中,你肯定会遇到各种报错和意外情况。下面是我踩过的一些坑以及解决办法。
5.1 网络与连接问题
问题现象:脚本运行中突然卡住,随后报错requests.exceptions.ConnectionError或Timeout。
- 排查1:检查网络连接。尝试在浏览器中访问
https://pubchem.ncbi.nlm.nih.gov,看是否正常。 - 排查2:降低请求频率。这是最常见的原因。立即将
time.sleep的间隔加大,比如从0.2秒增加到0.5秒或1秒。你的IP可能已经被临时限流。 - 排查3:使用代理(仅适用于有合理科研网络配置的情况)。在某些网络环境下,直接访问国外API可能不稳定。你可以在代码中为
requests库(PubChempy底层使用它)配置代理,但这需要你拥有合法、稳定的代理资源。注意:这里讨论的代理是用于科研网络访问的常规HTTP代理,与任何违规网络工具无关。 - 解决:在脚本中加入更强大的重试机制和更长的休眠时间。可以使用
tenacity或retrying库来优雅地实现带指数退避的重试。
5.2 化合物匹配与数据缺失问题
问题现象:日志显示“成功获取”,但CSV中该化合物的许多属性是空值。
- 排查1:属性名拼写错误。确保
desired_properties列表中的字符串与PubChempy的属性名完全一致。属性名是小写加下划线的风格(如molecular_weight),而不是PUG API中的驼峰风格(如MolecularWeight)。查看PubChempy文档确认。 - 排查2:该属性确实不存在。不是所有化合物都计算了所有属性。例如,
xlogp(计算得到的LogP)对于一些超大或特殊分子可能为空。这是正常现象。 - 排查3:查询结果不精确。用获取到的
CID或CanonicalSMILES到PubChem网站核对,看是否真的是你想要的化合物。可能名称有歧义,匹配到了同分异构体或类似物。 - 解决:对于关键化合物,采用更精确的查询方式。如果知道化合物的SMILES或InChIKey,使用
get_compounds(smiles, ‘smiles’)或get_compounds(inchikey, ‘inchikey’)进行查询,准确率几乎是100%。
5.3 编码与文件格式问题
问题现象:用Excel打开CSV文件,中文注释或某些特殊字符显示为乱码。
- 排查与解决:确保在
df.to_csv()时使用了encoding=‘utf-8-sig’参数。如果问题依旧,可以尝试用纯文本编辑器(如VS Code、Notepad++)打开CSV文件,查看其实际编码。也可以尝试encoding=‘gb18030’(一种中文编码),但utf-8-sig是跨平台兼容性最好的选择。
5.4 脚本运行效率低下
问题现象:处理几百个化合物就耗时极长。
- 排查1:单次请求延时是否过长?确认
time.sleep参数,0.2秒是平衡礼貌与效率的推荐值,可以微调。 - 排查2:是否开启了日志输出到控制台?大量日志打印(尤其是
print语句)会拖慢速度。使用Python的logging模块并设置合适的级别(如logging.WARNING)在生产运行时可以减少输出。 - 排查3:网络延迟。你的网络连接到NCBI服务器的速度可能较慢。可以在脚本开始和结束时打时间戳,计算平均每个化合物的处理时间。如果远高于“请求延时+数据处理时间”,那就是网络问题。
- 解决:对于超大规模任务(>5000),考虑将任务列表分割成多个小文件,分多次、在不同时间段运行。这是最稳妥、最不容易被封IP的方式。
5.5 PubChempy版本与依赖问题
问题现象:导入pubchempy失败,或运行时报AttributeError。
- 排查1:确认安装。在终端运行
pip show pubchempy查看是否安装及版本。 - 排查2:安装/更新。使用
pip install pubchempy --upgrade安装最新版。 - 排查3:依赖冲突。确保
requests库也已正确安装。通常pip install pubchempy会自动安装其依赖。
最后,分享一个我个人的习惯:在运行任何批量下载任务,尤其是大型任务之前,先用一个极小的、包含3-5个已知化合物的测试列表跑一遍脚本。这能帮你快速验证网络、环境、脚本逻辑是否全部正常,避免在长时间运行后才发现一个低级错误导致全军覆没。数据处理工作,尤其是这种网络爬取类任务,稳健性和可重复性远比一时的速度更重要。把这个脚本打磨成你化学信息学工具箱里的一件可靠利器,它能持续为你节省无数个小时的手动劳动。