Python批量获取化合物信息:PubChempy自动化数据采集实战
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为每个化合物存储了海量信息全部下载既不现实也没必要。常见的、在药物设计或初步分析中常用的属性包括标识类CIDPubChem唯一标识符IUPACNameCanonicalSMILES规范SMILES字符串InChIKey。基础物化性质MolecularWeightMolecularFormulaXLogP脂水分配系数TPSA拓扑极性表面积。药代动力学相关HydrogenBondDonorCountHydrogenBondAcceptorCountRotatableBondCount。复杂描述符有些更复杂的描述符可能需要通过其他方式计算但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(levellogging.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, indexFalse, encodingutf-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(..., encodingutf-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, retries2): 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第{attempt1}次获取{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_workers5): desired_properties [...] # 同上 all_results [] with ThreadPoolExecutor(max_workersmax_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个已知化合物的测试列表跑一遍脚本。这能帮你快速验证网络、环境、脚本逻辑是否全部正常避免在长时间运行后才发现一个低级错误导致全军覆没。数据处理工作尤其是这种网络爬取类任务稳健性和可重复性远比一时的速度更重要。把这个脚本打磨成你化学信息学工具箱里的一件可靠利器它能持续为你节省无数个小时的手动劳动。