Python操作Outlook存附件脚本运行耗时过长问题排查与优化
Python Outlook邮件匹配脚本性能问题排查与优化
脚本预期实现功能
- 读取指定路径的Excel文件
- 遍历指定Outlook共享邮箱收件箱内的所有邮件,提取邮件主题前9位,与Excel表A列的单元格值逐一匹配
- 匹配成功时,将对应邮件的附件保存到当前路径下的
Orders输出文件夹 - 匹配失败则跳过当前对象继续遍历
- 脚本运行结束后输出总运行耗时
现存性能问题
脚本在仅含9封邮件的测试文件夹中可正常运行,但面对实际业务量级数据时表现极差:
- 匹配2539个Excel订单值时,运行1小时仍未完成
- 仅匹配10个Excel值时,运行40分钟仅处理完成4个值
原问题代码
from pathlib import Path import win32com.client import os from datetime import datetime import time import openpyxl #name of the folder created for output output_dir = Path.cwd() / "Orders" outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI") folder = outlook.Folders.Item("Shared Mailbox Name") inbox = folder.Folders.Item("Inbox") messages = inbox.Items wb = openpyxl.load_workbook(r"C:\Users\TEST\Path-to-excel\FolderName\ExcelName.xlsx") sheet = wb['Sheet1'] names=sheet['A'] for cellObj in names: ordno = str(cellObj.value) print(ordno) for message in messages: subject = message.Subject body = message.body attachments = message.Attachments if str(subject)[:9] == ordno: output_dir.mkdir(parents=True, exist_ok=True) for attachment in attachments: attachment.SaveAsFile(output_dir / str(attachment)) else: pass start = time() print(f'Time taken to run: {time() - start} seconds')
运行缓慢核心原因
- 嵌套循环导致计算量爆炸:当前写法外层遍历2539个Excel订单号,内层每处理一个订单号就重新完整遍历32700封邮件,总循环次数达到2539*32700≈8300万次。且Outlook的COM接口调用开销远大于普通Python本地循环,这是性能差的核心原因。
- 冗余IO操作浪费资源:循环内读取了完全没用到的
message.body属性,Outlook拉取、解析邮件正文是极重的IO操作,白白消耗大量时间。 - 重复执行无效操作:
output_dir.mkdir放在匹配成功的分支内,每保存一封邮件就执行一次文件夹存在校验,该操作仅需在脚本启动时执行一次即可。 - 计时逻辑完全错误:
start = time()写在了所有业务逻辑执行完成之后,最终打印的耗时永远接近0,根本无法统计真实运行时间,且直接调用time()本身就不符合time模块的调用语法,运行时会直接报错。 - 无提前终止逻辑:一个订单号匹配到对应邮件后,没有跳出内层循环,还会继续遍历剩余所有邮件做无效判断。
- 附件保存逻辑有bug:直接将COM对象转字符串作为文件名,会出现文件名乱码、保存失败的问题。
优化方案
核心优化思路
- 反转遍历逻辑:仅遍历一次所有邮件,提前把Excel内的所有订单号存入哈希集合(查询时间复杂度O(1)),遍历邮件时直接判断主题前9位是否在集合内即可,总循环次数直接降到32700次,性能提升数千倍。
- 删除所有无用的属性读取,仅加载业务逻辑必须的字段。
- 提前创建输出文件夹,修正计时逻辑。
- 匹配成功的订单号直接从待匹配集合中移除,集合为空时提前终止邮件遍历,不需要扫完所有邮件。
- 配置Outlook条目只读快速访问参数,降低COM调用开销。
- 修复原代码的语法、逻辑bug。
优化后可直接运行的代码
from pathlib import Path import win32com.client import time import openpyxl def main(): # 初始化计时器 start_time = time.time() # 提前创建输出文件夹,仅执行一次 output_dir = Path.cwd() / "Orders" output_dir.mkdir(parents=True, exist_ok=True) # 加载Excel订单号,存入哈希集合做O(1)速度的匹配查询 wb = openpyxl.load_workbook( r"C:\Users\TEST\Path-to-excel\FolderName\ExcelName.xlsx", read_only=True, data_only=True ) sheet = wb['Sheet1'] order_set = set() for cell in sheet['A']: val = cell.value if val is not None: order_set.add(str(val).strip()) wb.close() print(f"待匹配订单号总数:{len(order_set)}") # 连接Outlook邮箱 outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI") folder = outlook.Folders.Item("Shared Mailbox Name") inbox = folder.Folders.Item("Inbox") messages = inbox.Items # 按收件时间倒序排序,开启快速遍历 messages.Sort("[ReceivedTime]", True) processed_count = 0 # 仅遍历一次邮件 for idx, message in enumerate(messages, 1): # 所有订单匹配完成直接退出,不用遍历剩余邮件 if not order_set: print("所有订单已匹配完成,提前终止遍历") break # 每处理1000封邮件打印一次进度 if idx % 1000 == 0: print(f"已扫描邮件数:{idx},已匹配订单数:{processed_count},剩余待匹配:{len(order_set)}") # 跳过无权限、损坏的异常邮件 try: subject = str(message.Subject) except Exception: continue subject_prefix = subject[:9] if subject_prefix in order_set: # 保存匹配成功邮件的所有附件 attachments = message.Attachments for att in attachments: att.SaveAsFile(str(output_dir / att.FileName)) # 从待匹配集合移除已完成的订单号 order_set.remove(subject_prefix) processed_count += 1 total_time = time.time() - start_time print(f"运行完成,总耗时:{round(total_time, 2)}秒,共匹配处理订单:{processed_count}个,未匹配到的订单数:{len(order_set)}") if __name__ == "__main__": main()
额外提速建议
- 确保Outlook使用本地缓存模式,收件箱已完全同步到本地,在线模式下COM接口拉取邮件的速度会比本地模式慢10倍以上。
- 如果待匹配订单号对应的邮件都集中在某个时间区间,可以使用
Items.Restrict方法先按时间过滤邮件,进一步减少需要遍历的邮件总量。
内容的提问来源于stack exchange,提问作者Robert Cenusa
相关产品推荐
相关产品推荐

