1. 项目缘起从“手动查”到“批量抓”的化学信息学刚需如果你在实验室里待过或者正在从事药物发现、材料化学、环境分析相关的工作一定对下面这个场景不陌生导师或项目负责人甩过来一个Excel表格里面密密麻麻列着几百个化合物的名称或CAS号然后轻描淡写地说一句“把这些化合物的结构、分子量、LogP值都查一下整理好发我。” 那一刻你看着满屏的“2,4-二氯苯氧乙酸”、“N-4-氯苯基乙酰胺”感觉手动打开浏览器、登录PubChem、一个个复制粘贴查询、再整理数据的过程足以消磨掉一整个下午甚至更久。这就是我们今天要解决的核心痛点如何高效、准确、自动化地从海量化学数据库中获取结构化信息。PubChem作为全球最大的免费化学信息数据库无疑是我们的首选“金矿”。而PubChemPy这个Python库就是我们手中的“自动化采矿机”。它不是什么高深莫测的黑科技而是一个将PubChem强大的API功能封装成简单Python调用的工具让化学工作者和程序员都能用几行代码完成过去需要重复劳动数小时的任务。这个项目的价值远不止于“省时间”。在AI驱动的药物设计、高通量虚拟筛选、化学信息学分析日益普及的今天能够程序化地获取和处理化合物数据已经成为一项基础且关键的能力。无论是构建自己的化合物属性本地数据库还是为机器学习模型准备特征数据批量下载都是第一步。网上虽然有很多零散的代码片段但往往缺乏对错误处理、速率限制、数据清洗等实际生产环节的深入讲解导致很多初学者照着抄完跑几个化合物没问题一到上百个就各种报错最终还得回头手动补漏。所以这篇内容不会只给你一个干巴巴的脚本。我会结合我多次处理成千上万个化合物数据的实战经验带你从环境搭建、核心原理、代码逐行解读一直讲到如何处理网络异常、解析复杂数据、以及将结果优雅地输出成科研人员最爱的Excel和SDF格式。我们不仅要“跑通”更要“跑稳”、“跑好”。2. PubChemPy 核心机制与数据边界剖析在动手写代码之前我们必须先理解工具的工作原理和它能做什么、不能做什么。这就像用望远镜前得先知道它的焦距和视场角否则你可能会用它来观察细菌那注定是徒劳的。2.1 PubChemPy 的本质一个友好的“传话员”PubChemPy本身并不存储任何化学数据。它所有的数据都来自美国国立卫生研究院NIH维护的PubChem数据库。这个库的核心工作是帮你把复杂的HTTP API请求包括查询参数构造、身份验证、错误码处理包装成简单的Python函数调用。例如当你调用get_compounds(‘Aspirin’, ‘name’)时PubChemPy在背后默默地做了以下几件事将化合物名称‘Aspirin’和搜索类型‘name’转换成PubChem REST API要求的URL格式。向https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/Aspirin/JSON发送一个HTTP GET请求。接收服务器返回的JSON格式的原始数据。将JSON数据反序列化并封装成自定义的Python对象如Compound对象方便你用compound.molecular_weight这样的属性来访问。这个过程对你来说是透明的你只需要关心函数名和参数。但理解这一点至关重要因为它意味着任何网络问题、API速率限制、服务器错误最终都会体现在你的程序运行中而不是被PubChemPy完全消化掉。2.2 你能获取哪些信息理解“属性”与“记录”PubChem中的化合物信息是分层、多维的。通过PubChemPy我们主要获取两种粒度的数据1. 化合物摘要信息 (Compound Records)这是最常用的。通过get_compounds()函数获取返回一个或多个Compound对象。每个对象包含数十个常用属性例如标识符CIDPubChem唯一标识符、InChI、InChI Key、SMILES规范和非规范、分子式。物理化学性质分子量、精确质量、拓扑极性表面积、可旋转键数、氢键供受体数。脂水分配系数XLogP预测的辛醇-水分配系数对数。化学描述IUPAC名称、同义词列表。这些属性对于大多数QSAR定量构效关系建模、初步的化合物筛选已经足够。你可以像访问对象属性一样直接获取cid compound.cid,mw compound.molecular_weight。2. 详细属性与数据表 (Properties Tables)有时你需要更专业、更庞大的数据。PubChemPy提供了get_properties()和get_synonyms()等函数。get_properties()可以一次性获取大量预定义或自定义的属性列表返回的是字典列表更适合批量导出为表格。例如你可以指定获取‘MolecularWeight’, ‘XLogP’, ‘TPSA’, ‘Complexity’等一系列属性。重要边界与常见误解并非所有属性都实时计算像XLogP、TPSA等是预测值或计算值存储在PubChem中。你不能通过这个库进行实时的量子化学计算。名称查询的模糊性与多结果用名称查询是最方便但也最不精确的。get_compounds(‘Glucose’, ‘name’)可能会返回多个同分异构体或不同来源的记录。库默认返回的是匹配度最高的第一个结果但这不一定是你想要的特定异构体如α-D-葡萄糖。对于关键任务使用CID、InChI Key或SMILES进行精确查询是更可靠的做法。数据完整性不是每个化合物都有全部属性。一些天然产物或复杂分子可能缺少XLogP或3D坐标。你的代码必须能优雅地处理缺失值返回None。理解这些边界能帮助你在设计批量下载流程时提前规避很多坑比如设置备用标识符、添加结果验证步骤等。3. 从零搭建批量下载流水线理论清楚了我们开始动手搭建一个健壮的、可用于生产环境的批量下载脚本。我们将分模块构建最终整合成一个完整的程序。3.1 环境准备与依赖安装首先确保你的Python环境是3.6及以上版本。打开终端或命令行安装PubChemPy库非常简单pip install pubchempy通常不需要其他额外依赖。但为了后续的数据处理和保存我们一般会同时安装pandas和openpyxl用于生成Excel文件pip install pandas openpyxl如果你还需要处理化学结构文件如SDFrdkit是一个强大的工具但安装稍复杂通常推荐使用Conda。本篇为保持环境简洁我们先以Excel输出为主。创建一个新的Python脚本文件比如batch_download_pubchem.py并导入必要的库import pubchempy as pcp import pandas as pd import time from typing import List, Optional, Dict, Any import logging3.2 核心下载函数健壮性高于一切这是整个流水线的心脏。一个健壮的下载函数必须处理查询、网络异常、未找到结果、数据解析和速率限制。def fetch_compound_info(query: str, query_type: str ‘name’, max_retries: int 3) - Optional[Dict[str, Any]]: 根据查询词和类型从PubChem获取单个化合物的核心信息。 参数: query: 查询字符串如化合物名称、CID、SMILES等。 query_type: 查询类型可选 ‘name’, ‘cid’, ‘smiles’, ‘inchi’, ‘inchikey’。默认为 ‘name’。 max_retries: 网络请求失败时的最大重试次数。 返回: 一个包含化合物信息的字典。如果查询失败或未找到返回None。 for attempt in range(max_retries): try: # 执行查询get_compounds返回一个列表 compounds pcp.get_compounds(query, namespacequery_type) if not compounds: logging.warning(f“未找到匹配的化合物: {query} (类型: {query_type})”) return None # 通常取第一个最佳匹配结果。对于精确标识符如CID列表长度为1。 compound compounds[0] # 构建信息字典。使用.get()方法避免属性缺失导致报错。 info { ‘query’: query, ‘query_type’: query_type, ‘cid’: compound.cid, ‘iupac_name’: compound.iupac_name, ‘molecular_formula’: compound.molecular_formula, ‘molecular_weight’: compound.molecular_weight, ‘canonical_smiles’: compound.canonical_smiles, ‘isomeric_smiles’: compound.isomeric_smiles if compound.isomeric_smiles else compound.canonical_smiles, ‘inchi’: compound.inchi, ‘inchikey’: compound.inchikey, ‘xlogp’: compound.xlogp, ‘tpsa’: compound.tpsa, ‘h_bond_donor_count’: compound.h_bond_donor_count, ‘h_bond_acceptor_count’: compound.h_bond_acceptor_count, ‘rotatable_bond_count’: compound.rotatable_bond_count, ‘complexity’: compound.complexity, } logging.info(f“成功获取化合物: {query} - CID: {compound.cid}”) return info except pcp.BadRequestError: # 通常是查询格式错误重试无意义 logging.error(f“请求格式错误可能无效的{query_type}: {query}”) return None except pcp.NotFoundError: # 明确未找到 logging.warning(f“在PubChem中未找到: {query}”) return None except (pcp.TimeoutError, pcp.ServerError, ConnectionError) as e: # 网络或服务器问题进行重试 wait_time (attempt 1) * 5 # 递增等待如5, 10, 15秒 logging.warning(f“第{attempt1}次尝试失败 ({e}) {wait_time}秒后重试...”) time.sleep(wait_time) except Exception as e: # 捕获其他未预见的异常 logging.error(f“获取化合物 {query} 时发生未知错误: {e}”) return None logging.error(f“经过{max_retries}次重试仍无法获取化合物: {query}”) return None关键设计解析与经验重试机制PubChem的API是公开的但有速率限制且可能遇到临时网络抖动。简单的time.sleep配合递增等待时间能大幅提高批量任务的成功率。这里采用了指数退避的变体线性递增。异常细分处理PubChemPy定义了多种异常。BadRequestError和NotFoundError通常意味着查询词有问题重试没用直接返回None。而TimeoutError和ServerError是临时性问题应该重试。属性安全获取使用compound.iupac_name直接访问如果该属性为None字典值就是None。对于isomeric_smiles我们做了一个小处理如果它不存在则回退到canonical_smiles确保总有SMILES信息。日志记录使用logging模块而非print可以方便地控制输出级别INFO, WARNING, ERROR并将日志保存到文件便于事后排查哪些化合物失败了。3.3 批量调度与速率控制有了单个化合物的下载函数批量下载的核心就是循环调用它。但这里有一个必须遵守的规则尊重PubChem的服务器控制请求频率。PubChem虽然没有严格的API Key制度但其 使用政策 要求避免过度请求通常建议每秒不超过5-10个请求。不遵守可能导致你的IP被暂时限制。def batch_fetch_compound_list(query_list: List[str], query_type: str ‘name’, delay: float 0.2) - List[Dict[str, Any]]: 批量下载化合物信息列表并自动添加请求延迟以避免速率限制。 参数: query_list: 查询字符串列表。 query_type: 查询类型。 delay: 每次请求之间的固定延迟秒。默认0.2秒即每秒约5个请求。 返回: 成功获取的化合物信息字典列表。 results [] total len(query_list) for idx, query in enumerate(query_list, 1): logging.info(f“正在处理 [{idx}/{total}]: {query}”) compound_info fetch_compound_info(query, query_type) if compound_info: results.append(compound_info) else: # 即使失败也记录一个包含查询词的空字典或占位符保持结果顺序 results.append({‘query’: query, ‘error’: ‘Not found or failed’}) # 添加延迟除非是最后一个化合物 if idx total: time.sleep(delay) return results经验之谈delay0.2是一个比较保守且安全的设置。如果你需要下载成千上万个化合物可以适当调整如0.1秒但务必观察是否有429Too Many Requests错误出现。我个人的经验是在办公网环境下0.15-0.25秒的延迟能稳定运行数小时。在循环内打印或记录进度非常重要尤其是对于长时间运行的任务你能知道程序卡在哪里。即使某个化合物查询失败我们也选择在结果列表中保留一个标记错误的记录而不是直接跳过。这样最终输出的表格能清晰反映出哪些输入条目有问题方便后续核对和补查。3.4 数据后处理与多格式导出获取到的数据是字典列表我们需要将其转化为更易分析和分享的格式。def save_results_to_file(results: List[Dict[str, Any]], output_prefix: str): 将结果列表保存为Excel和CSV文件。 参数: results: fetch_compound_info返回的字典列表。 output_prefix: 输出文件的前缀不包含扩展名。 if not results: logging.warning(“结果列表为空未生成文件。”) return df pd.DataFrame(results) # 重新排列列的顺序让关键信息靠前 preferred_order [‘query’, ‘cid’, ‘iupac_name’, ‘molecular_formula’, ‘molecular_weight’, ‘canonical_smiles’, ‘isomeric_smiles’, ‘xlogp’, ‘tpsa’] # 只调整存在的列 existing_columns [col for col in preferred_order if col in df.columns] other_columns [col for col in df.columns if col not in existing_columns] final_columns existing_columns other_columns df df[final_columns] # 保存为Excel (xlsx) excel_filename f“{output_prefix}_compounds.xlsx” try: df.to_excel(excel_filename, indexFalse, engine‘openpyxl’) logging.info(f“结果已保存至Excel文件: {excel_filename}”) except Exception as e: logging.error(f“保存Excel文件失败: {e}”) # 同时保存为CSV更通用兼容性更好 csv_filename f“{output_prefix}_compounds.csv” try: df.to_csv(csv_filename, indexFalse, encoding‘utf-8-sig’) # ‘utf-8-sig’解决Excel打开中文乱码 logging.info(f“结果已保存至CSV文件: {csv_filename}”) except Exception as e: logging.error(f“保存CSV文件失败: {e}”) def save_sdf_for_docking(results: List[Dict[str, Any]], output_sdf_path: str): 将包含SMILES的结果保存为SDF文件用于分子对接或可视化。 注意这是一个简化示例需要rdkit库支持。 try: from rdkit import Chem from rdkit.Chem import PandasTools except ImportError: logging.error(“未安装rdkit库无法生成SDF文件。请通过‘conda install -c conda-forge rdkit’安装。”) return df pd.DataFrame(results) # 确保数据框中有SMILES列和CID列 if ‘canonical_smiles’ not in df.columns or df[‘canonical_smiles’].isnull().all(): logging.warning(“没有有效的SMILES信息无法生成SDF。”) return # 使用rdkit从SMILES创建分子对象 df[‘ROMol’] df[‘canonical_smiles’].apply(lambda x: Chem.MolFromSmiles(x) if pd.notnull(x) else None) # 移除无法生成分子的行 df_valid df.dropna(subset[‘ROMol’]).copy() if df_valid.empty: logging.warning(“没有有效的分子可写入SDF。”) return # 使用PandasTools将分子和属性写入SDF PandasTools.WriteSDF(df_valid, output_sdf_path, molColName‘ROMol’, propertieslist(df_valid.columns.drop(‘ROMol’))) logging.info(f“分子结构已保存至SDF文件: {output_sdf_path}”)导出环节的实用技巧双格式备份总是同时生成Excel.xlsx和CSV.csv文件。Excel便于手动查看和简单分析而CSV是纯文本几乎被所有编程语言和数据库工具支持且版本控制友好。列顺序优化调整DataFrame的列顺序把最关心的信息如查询词、CID、分子式、分子量放在前面提升表格的可读性。UTF-8 with BOM在保存CSV时使用encoding‘utf-8-sig’这个“sig”BOM能帮助Windows系统下的Excel正确识别UTF-8编码避免中文或其他特殊字符乱码。这是很多教程里不会提但实际协作中经常遇到的坑。SDF生成save_sdf_for_docking函数展示了如何向分子对接等下游任务延伸。rdkit的PandasTools模块能非常方便地在DataFrame和SDF格式间转换。注意这里需要确保SMILES是有效的。4. 实战整合与高级场景应对现在我们把所有模块组装起来并模拟一个真实的、稍复杂的应用场景。4.1 完整脚本示例与主流程创建一个完整的脚本包含配置、执行和错误处理。# batch_download_pubchem.py import pubchempy as pcp import pandas as pd import time import logging from typing import List, Dict, Any, Optional from pathlib import Path # 配置日志 logging.basicConfig(levellogging.INFO, format‘%(asctime)s - %(levelname)s - %(message)s’, handlers[ logging.FileHandler(‘batch_download.log’), logging.StreamHandler() ]) # 此处插入上面定义过的三个函数fetch_compound_info, batch_fetch_compound_list, save_results_to_file def main(): 主函数演示完整流程 # 示例1从文本文件读取化合物名称列表 input_file ‘compound_names.txt’ # 每行一个化合物名称 output_prefix ‘my_compound_library’ try: with open(input_file, ‘r’, encoding‘utf-8’) as f: # 去除每行首尾空白并过滤掉空行 name_list [line.strip() for line in f if line.strip()] except FileNotFoundError: logging.error(f“输入文件未找到: {input_file}”) # 如果文件不存在使用一个示例列表 name_list [‘Aspirin’, ‘Caffeine’, ‘Paracetamol’, ‘Metformin’, ‘InvalidCompoundXYZ’, ‘Glucose’] logging.info(f“将使用内置示例列表: {name_list}”) if not name_list: logging.error(“化合物列表为空程序退出。”) return logging.info(f“开始批量处理 {len(name_list)} 个化合物...”) start_time time.time() # 执行批量下载设置0.25秒延迟每秒约4次请求 all_results batch_fetch_compound_list(name_list, query_type‘name’, delay0.25) end_time time.time() logging.info(f“批量下载完成耗时 {end_time - start_time:.2f} 秒。”) # 统计成功率 successful [r for r in all_results if r.get(‘cid’) is not None] logging.info(f“成功获取 {len(successful)} 个失败 {len(all_results) - len(successful)} 个。”) # 保存结果 save_results_to_file(all_results, output_prefix) # 可选生成SDF文件 if successful: save_sdf_for_docking(successful, f“{output_prefix}_structures.sdf”) logging.info(“所有任务处理完毕。请查看生成的 .xlsx, .csv 文件和 .log 日志。”) if __name__ ‘__main__’: main()如何使用将上述所有代码块整合到一个.py文件中。在同目录下创建一个compound_names.txt文件每行写一个化合物通用名或IUPAC名。在终端运行python batch_download_pubchem.py。程序会一边运行一边在屏幕和batch_download.log文件中输出进度。完成后你会得到my_compound_library_compounds.xlsx、.csv文件以及可能的结构文件.sdf。4.2 应对复杂查询与结果歧义在实际项目中你拿到的化合物列表可能不那么“干净”。除了常见的“查不到”还有更棘手的问题场景一同义词与商品名查询“Vitamin C”能成功返回抗坏血酸CID: 54670067。但查询“Tylenol”商品名可能直接失败。解决方案是构建一个同义词预处理步骤或者使用更宽容的查询方式。PubChemPy的get_compounds函数对名称的匹配其实已经包含了同义词搜索但不够智能。一个增强策略是如果精确名称查询失败可以尝试用pcp.get_synonyms(query)先获取该名称对应的CID再用CID查询。但注意get_synonyms也可能返回多个CID。场景二混合物与盐查询“Sodium chloride”会返回氯化钠CID: 5234。但很多药物是以盐的形式存在的比如“Metformin hydrochloride”。直接用这个名字查询返回的化合物记录可能仍然是盐酸二甲双胍的母核CID: 4091属性中可能会包含盐的信息也可能不完整。对于精确的物化性质计算最好查询其明确的母核CID或SMILES。场景三批量查询CID获取属性如果你已经有一批CID那么批量获取属性效率更高。pcp.get_properties()函数支持一次性传入多个CID的列表。但需要注意这个函数返回的是属性字典的列表而不是完整的Compound对象且属性名是固定的。def fetch_properties_by_cids(cid_list: List[int], properties: List[str]) - List[Dict]: 批量通过CID获取指定属性 try: # 注意这里一次性传入整个cid_listPubChem API可能有数量限制通常几千个 result pcp.get_properties(properties, cid_list, as_dataframeFalse) # 返回字典列表 return result except Exception as e: logging.error(f“批量获取属性失败: {e}”) return [] # 示例获取分子量和LogP # props fetch_properties_by_cids([2244, 1983], [‘MolecularWeight’, ‘XLogP’])4.3 性能优化与大规模处理当你的列表达到上万甚至十万级别时简单的顺序请求加延迟的方式可能耗时过长数小时到数天。此时需要考虑优化策略并发请求谨慎使用使用concurrent.futures库的ThreadPoolExecutor可以并发发送多个请求。但必须非常小心地控制并发数并严格遵守PubChem的服务条款。建议将并发数限制在3-5并且每个线程内部仍需保持一个基础延迟如0.1秒这样整体速率不会超标。from concurrent.futures import ThreadPoolExecutor, as_completed def batch_fetch_concurrent(query_list, max_workers3, delay_per_worker0.3): results [] with ThreadPoolExecutor(max_workersmax_workers) as executor: # 创建未来任务字典 future_to_query {executor.submit(fetch_compound_info, q, ‘name’): q for q in query_list} for future in as_completed(future_to_query): query future_to_query[future] try: result future.result() if result: results.append(result) except Exception as e: logging.error(f“处理 {query} 时发生异常: {e}”) time.sleep(delay_per_worker) # 在主线程控制总体频率 return results重要警告滥用并发请求可能导致你的IP被PubChem暂时封禁。仅在明确需要且能承担风险时使用并做好异常处理和日志记录。断点续传对于超大规模任务程序可能因网络或意外中断。一个健壮的设计是将已成功下载的CID或查询词实时保存到一个checkpoint.json文件中。程序重启时先加载这个文件跳过已处理的部分从断点处继续。本地缓存对于需要反复查询的化合物比如你经常分析一个固定的化合物库可以建立一个本地SQLite数据库或简单的JSON文件缓存。每次查询前先检查缓存命中则直接读取未命中再请求PubChem并更新缓存。这能极大减少对外部API的依赖和等待时间。5. 错误排查与数据清洗实战即使有了完善的代码在实际运行中你依然会遇到各种“意外”。这一部分分享几个我踩过的坑和对应的解决思路。问题一HTTPError: 503 Service Unavailable或TimeoutError这是最常见的网络相关错误。503表示PubChem服务器暂时过载或维护。应对我们的fetch_compound_info函数中的重试机制就是为此设计的。增加重试次数如5次和每次重试的等待时间。如果整个批次频繁出现503可能是你的请求速率还是太快了适当增加delay参数。问题二PubChemPy返回了结果但关键属性如xlogp是None这通常不是错误而是该化合物在PubChem中确实没有这个计算或实验值。应对在数据处理阶段进行清洗。使用Pandas可以方便地处理缺失值。# 在保存结果后用pandas进行数据分析 df pd.read_excel(‘output.xlsx’) # 检查缺失值 missing_xlogp df[df[‘xlogp’].isnull()][[‘query’, ‘cid’]] if not missing_xlogp.empty: print(“以下化合物缺少XLogP值:”) print(missing_xlogp) # 可以用中位数或平均值填充根据需求谨慎选择 # df[‘xlogp’].fillna(df[‘xlogp’].median(), inplaceTrue)问题三手性信息丢失通过canonical_smiles获取的是去手性的SMILES。如果你的研究涉及立体化学isomeric_smiles属性至关重要但它也可能为None。应对在查询时就像我们在fetch_compound_info函数里做的那样优先使用isomeric_smiles如果不存在再回退到canonical_smiles。并在结果中明确标注使用的是哪种SMILES。问题四查询词包含特殊字符或格式不一致例如输入列表中有“α-D-Glucose”PubChem可能无法识别希腊字母。应对在批量处理前对输入列表进行简单的清洗和标准化。比如将希腊字母转换为英文α-alpha, β-beta去除多余的空格和换行符。对于复杂的名称可以尝试先通过PubChem网站手动搜索确认其正确的查询词格式。问题五结果与预期不符用名称“Benzene”查询返回的CID是241但你可能期望是某个特定的实验数据源。应对PubChem会聚合来自多个数据源的信息。如果你需要追踪特定来源可以使用pcp.get_sources()函数查看化合物的数据来源。对于绝对精确的需求始终使用CID、InChI Key或SMILES作为查询依据而不是名称。最后所有这些问题都指向同一个核心建议永远不要完全信任自动化流程的原始输出。在将批量下载的数据用于关键分析或模型训练之前进行人工抽样检查比如随机检查20-30个结果是必不可少的一步。检查标识符是否正确、关键属性是否合理比如分子量是否在预期范围内。这个习惯能帮你避免后续因数据错误导致的数天甚至数周的工作白费。通过这样一套从原理到实践再到排错和优化的完整流程你就能建立起一个可靠、高效的化学信息批量获取工作流从容应对从几十到几万个化合物的数据准备任务把宝贵的时间留给更富创造性的科研思考。