You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 20:45:51