告别邮件焦虑:用Python打造你的专属日报机器人
每天早晨,当你打开邮箱,看到几十封未读邮件,其中夹杂着各种报表、数据文件,那种感觉就像面对一座需要手动挖掘的信息矿山。你不得不一封封点开,下载附件,整理数据,生成汇总——这个过程不仅枯燥,而且极其消耗时间。有没有一种方法,能让这些重复性工作在你喝咖啡的间隙自动完成?今天,我想和你分享的,就是如何用Python构建一个智能邮件处理机器人,它能在5分钟内,帮你把杂乱无章的日报邮件,变成一份清晰、可用的统计报告。
这个方案的核心,是Python标准库中两个看似低调但功能强大的模块:imaplib和email。它们不像那些炫酷的AI框架引人注目,但在处理自动化流程上,却是不折不扣的“瑞士军刀”。我将从一个真实的办公场景出发,带你一步步搭建一个稳定、高效、可扩展的自动化脚本。我们不仅会实现基础功能,还会深入探讨如何让它更“聪明”——比如识别特定发件人、过滤垃圾邮件、处理异常格式,以及如何让它在你睡觉时默默工作。无论你是数据分析师、项目经理,还是需要处理大量邮件的运营人员,这套方案都能为你每天节省至少一小时。
1. 从零开始:搭建你的自动化邮件处理环境
在开始编写代码之前,我们需要先理清整个自动化流程的逻辑链条。想象一下,一个理想的邮件处理机器人应该具备哪些能力?首先,它需要能安全地登录你的邮箱;其次,它要能精准地找到那些包含日报的邮件;然后,它要能聪明地解析邮件内容,提取出我们需要的附件;最后,它要能处理附件数据,并生成我们想要的报告。整个过程,最好还能在后台静默运行,遇到错误能自己处理,而不是动不动就崩溃。
1.1 核心工具包:不止是imaplib和email
虽然imaplib和email是主力,但一个健壮的系统还需要一些“帮手”。下面这个表格列出了我们构建完整方案所需的核心库及其作用:
| 库名 | 主要用途 | 是否必需 | 备注 |
|---|---|---|---|
imaplib | 与邮件服务器建立IMAP连接,执行搜索、获取等操作 | 是 | Python标准库,无需安装 |
email | 解析复杂的邮件结构(MIME),提取正文、附件等 | 是 | Python标准库,无需安装 |
pandas | 读取和处理Excel、CSV等格式的附件数据 | 强烈推荐 | 数据处理事实标准,pip install pandas |
schedule | 实现轻量级的定时任务调度 | 可选 | 比time.sleep更优雅,pip install schedule |
python-dotenv | 安全地管理邮箱密码等敏感信息 | 推荐 | 避免密码硬编码在代码中,pip install python-dotenv |
chardet | 自动检测邮件内容的字符编码,避免乱码 | 可选 | 处理国际邮件时很有用,pip install chardet |
提示:对于
pandas,如果你只需要处理.xlsx文件,可能还需要安装openpyxl引擎:pip install openpyxl。处理.xls文件则需要xlrd。
安装好这些库后,我们还需要在邮箱端进行一项关键设置:开启IMAP/SMTP服务并获取授权码。这是大多数教程容易忽略,但实际操作中最容易卡住的一步。
-
以QQ邮箱为例:
- 登录网页版QQ邮箱,点击顶部“设置”。
- 选择“账户”选项卡,向下滚动到“POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务”部分。
- 找到“IMAP/SMTP服务”,点击“开启”。根据指引(可能需要短信验证)生成一个授权码。这个授权码将替代你的邮箱登录密码,在代码中使用。
- 记下IMAP服务器地址:
imap.qq.com,端口993(SSL)。
-
以网易163/126邮箱为例: 流程类似,在设置中开启“IMAP/SMTP服务”并设置授权密码。IMAP服务器为
imap.163.com或imap.126.com。 -
以Outlook/Hotmail为例: 服务器地址为
outlook.office365.com。你可能需要在微软账户的“安全设置”中开启“两步验证”,然后创建“应用密码”用于代码登录。
准备好这些信息后,我们就可以开始编写第一个核心功能:邮箱连接。
1.2 建立稳固的邮箱连接通道
连接邮箱是整个流程的起点,必须保证其稳定和安全。我们不能把密码直接写在代码里,而是使用环境变量。在项目根目录创建一个名为.env的文件(注意前面的点),内容如下:
EMAIL_USER=your_email@example.com
EMAIL_PASSWORD=your_authorization_code_here
EMAIL_IMAP_SERVER=imap.example.com
然后,我们编写一个健壮的连接函数。这个函数不仅要能连接,还要能优雅地处理连接失败、超时等异常情况。
import imaplib
import ssl
from dotenv import load_dotenv
import os
import logging
# 加载环境变量
load_dotenv()
# 配置日志,方便追踪运行状态
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def create_imap_connection(max_retries=3):
"""
创建并返回一个IMAP4_SSL连接对象。
包含重试机制和基本的错误处理。
Args:
max_retries (int): 连接失败时的最大重试次数。
Returns:
imaplib.IMAP4_SSL object or None: 连接成功返回对象,失败返回None。
"""
email_user = os.getenv('EMAIL_USER')
email_pass = os.getenv('EMAIL_PASSWORD')
imap_server = os.getenv('EMAIL_IMAP_SERVER')
if not all([email_user, email_pass, imap_server]):
logger.error("环境变量配置不完整,请检查 .env 文件。")
return None
for attempt in range(max_retries):
try:
logger.info(f"尝试连接邮箱服务器 ({attempt + 1}/{max_retries})...")
# 创建SSL上下文,增强连接安全性
context = ssl.create_default_context()
# 连接到IMAP服务器,端口默认为993
mail = imaplib.IMAP4_SSL(host=imap_server, port=993, ssl_context=context)
mail.login(email_user, email_pass)
logger.info("邮箱登录成功!")
return mail
except imaplib.IMAP4.error as auth_error:
logger.error(f"登录认证失败: {auth_error}。请检查用户名和授权码是否正确。")
break # 认证错误无需重试
except (ConnectionError, TimeoutError, ssl.SSLError) as conn_error:
logger.warning(f"网络连接失败 (尝试 {attempt + 1}): {conn_error}")
if attempt == max_retries - 1:
logger.error("达到最大重试次数,连接失败。")
return None
time.sleep(2) # 等待2秒后重试
except Exception as e:
logger.error(f"发生未知错误: {e}")
return None
return None
这个函数做了几件关键的事:使用环境变量管理密钥、加入了重试逻辑以应对网络波动、通过SSL上下文提升安全性,并用日志记录替代简单的print,便于后期维护和监控。这是构建可靠自动化脚本的第一步。
2. 精准捕获:智能搜索与邮件过滤策略
成功连接邮箱后,下一步是从海量邮件中精准找到我们需要的“日报邮件”。如果只是简单搜索“未读邮件”,很可能会把订阅的新闻、同事的闲聊一并抓取下来,造成干扰。我们需要更精细的搜索策略。
2.1 理解IMAP搜索语法:你的邮件“筛选器”
IMAP协议提供了一套强大的搜索命令,我们可以组合多个条件来缩小范围。以下是一些在日报处理场景中极其有用的搜索条件:
UNSEEN: 查找所有未读邮件。这是最基础的条件。FROM "daily-report@company.com": 只查找来自特定发件人地址的邮件。这是最精准的过滤方式。SUBJECT "每日数据报表": 查找主题中包含特定关键词的邮件。注意关键词要用双引号括起来。SINCE "01-DEC-2024": 查找指定日期之后(含当天)的邮件。日期格式必须是DD-MMM-YYYY。BEFORE "05-DEC-2024": 查找指定日期之前的邮件。- 组合条件: 可以用括号和逻辑关系组合多个条件。例如:
(UNSEEN FROM "report@system.com"): 来自特定系统的未读邮件。(SUBJECT "报表" SINCE "01-DEC-2024"): 12月1日以后主题含“报表”的邮件。
在我们的Python代码中,可以这样实现一个灵活的搜索函数:
def search_target_emails(mail, mailbox='INBOX', search_criteria=None):
"""
在指定邮箱文件夹中搜索邮件。
Args:
mail: 已登录的IMAP连接对象。
mailbox (str): 邮箱文件夹,如 'INBOX'(收件箱)、'Sent'(已发送)。
search_criteria (list): 搜索条件列表,如 ['UNSEEN', 'FROM "report@company.com"']。
Returns:
list: 匹配邮件的唯一ID列表。
"""
if search_criteria is None:
search_criteria = ['UNSEEN'] # 默认搜索未读邮件
try:
# 选择邮箱文件夹
status, _ = mail.select(mailbox, readonly=False) # readonly=False允许后续标记邮件
if status != 'OK':
logger.error(f"无法选择邮箱文件夹: {mailbox}")
return []
# 构建搜索字符串
criteria_str = ' '.join(search_criteria)
logger.info(f"搜索条件: {criteria_str}")
# 执行搜索
status, message_ids_bytes = mail.search(None, criteria_str)
if status != 'OK':
logger.error("邮件搜索失败。")
return []
# 将字节串ID转换为列表
if message_ids_bytes[0]:
id_list = message_ids_bytes[0].split()
logger.info(f"找到 {len(id_list)} 封匹配的邮件。")
return id_list
else:
logger.info("未找到匹配的邮件。")
return []
except Exception as e:
logger.error(f"搜索邮件过程中发生错误: {e}")
return []
2.2 进阶过滤:在解析后二次筛选
有时仅靠IMAP搜索条件还不够。例如,发件人可能相同,但附件格式或命名不符合要求。我们可以在获取邮件的基本信息后,进行更细致的二次过滤。
def filter_emails_by_content(mail, message_id_list, must_have_attachment=True, allowed_extensions=['.xlsx', '.csv']):
"""
根据邮件内容(如附件类型)对邮件ID列表进行二次过滤。
注意:此函数会实际获取并解析邮件头部和结构,有一定开销。
Args:
mail: IMAP连接对象。
message_id_list: 初步搜索得到的邮件ID列表。
must_have_attachment (bool): 是否必须包含附件。
allowed_extensions (list): 允许的附件后缀名列表。
Returns:
list: 经过内容过滤后的邮件ID列表。
"""
filtered_ids = []
for msg_id in message_id_list:
try:
# 仅获取邮件的信封信息或特定部分,以减轻负载
status, msg_data = mail.fetch(msg_id, '(BODY.PEEK[HEADER] BODYSTRUCTURE)')
if status != 'OK':
continue
# 这里可以解析BODYSTRUCTURE来初步判断是否有附件及其类型
# 这是一个简化的示例:我们直接获取完整邮件进行解析
status, full_msg_data = mail.fetch(msg_id, '(RFC822)')
raw_email = full_msg_data[0][1]
email_message = email.message_from_bytes(raw_email)
has_valid_attachment = False
for part in email_message.walk():
if part.get_content_disposition() == 'attachment':
filename = part.get_filename()
if filename and any(filename.endswith(ext) for ext in allowed_extensions):
has_valid_attachment = True
break
# 根据条件决定是否保留该邮件ID
if not must_have_attachment or has_valid_attachment:
filtered_ids.append(msg_id)
except Exception as e:
logger.warning(f"过滤邮件ID {msg_id} 时出错: {e}")
continue # 跳过这封邮件,继续处理下一封
logger.info(f"内容过滤后,剩余 {len(filtered_ids)} 封邮件。")
return filtered_ids
通过IMAP搜索初筛和内容解析精筛这两层过滤,我们的机器人就能像经验丰富的助理一样,准确地从收件箱里挑出那些真正需要处理的日报邮件,为后续的解析和处理打下坚实基础。
3. 庖丁解牛:深度解析邮件与附件处理
找到目标邮件后,接下来就是“拆解”它。一封邮件可能包含纯文本、HTML、图片、以及我们最关心的附件。email库的强大之处在于它能轻松处理这种复杂的MIME多部分结构。
3.1 安全解码与信息提取
邮件的主题、发件人名称可能使用多种编码(如UTF-8、GBK、Base64),直接解码可能导致乱码。我们需要一个健壮的解码函数。
from email.header import decode_header
import chardet
def decode_mime_header(header_value):
"""
安全地解码邮件头信息(如Subject、From)。
Args:
header_value: 原始的邮件头值,可能是字符串或字节。
Returns:
str: 解码后的字符串。
"""
if header_value is None:
return ""
decoded_parts = decode_header(header_value)
decoded_str = ""
for content, charset in decoded_parts:
try:
if isinstance(content, bytes):
if charset:
decoded_str += content.decode(charset, errors='ignore')
else:
# 自动检测编码
detected = chardet.detect(content)
encoding = detected['encoding'] if detected['encoding'] else 'utf-8'
decoded_str += content.decode(encoding, errors='ignore')
else:
decoded_str += str(content)
except Exception:
# 如果所有解码都失败,使用回退方案
decoded_str += str(content) if not isinstance(content, bytes) else content.decode('latin-1', errors='ignore')
return decoded_str.strip()
def extract_email_address(from_header):
"""
从 '发件人' 头信息中提取纯净的邮箱地址。
例如,从 '"张三" <zhangsan@company.com>' 中提取 'zhangsan@company.com'。
"""
import re
# 常见的邮箱地址正则表达式
pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
matches = re.findall(pattern, from_header)
return matches[0] if matches else from_header
利用这些工具,我们可以编写一个完整的邮件解析函数,它返回一个结构清晰的字典,包含所有我们需要的信息:
def parse_email_message(mail, message_id):
"""
解析单封邮件,提取关键信息和附件。
Args:
mail: IMAP连接对象。
message_id: 邮件的唯一ID。
Returns:
dict: 包含邮件元数据、正文和附件信息的字典。
"""
email_info = {
'id': message_id,
'subject': '',
'from': '',
'from_email': '',
'date': '',
'body_plain': '',
'body_html': '',
'attachments': []
}
try:
status, msg_data = mail.fetch(message_id, '(RFC822)')
if status != 'OK':
logger.error(f"获取邮件 {message_id} 内容失败。")
return email_info
raw_email = msg_data[0][1]
msg = email.message_from_bytes(raw_email)
# 解析头部信息
email_info['subject'] = decode_mime_header(msg.get('Subject'))
from_header = decode_mime_header(msg.get('From'))
email_info['from'] = from_header
email_info['from_email'] = extract_email_address(from_header)
email_info['date'] = msg.get('Date')
# 解析邮件正文和附件
email_info.update(_walk_message_parts(msg))
except Exception as e:
logger.error(f"解析邮件 {message_id} 时发生错误: {e}")
return email_info
def _walk_message_parts(msg):
"""
递归遍历邮件MIME结构,提取正文和附件。
这是一个内部辅助函数。
"""
result = {'body_plain': '', 'body_html': '', 'attachments': []}
if msg.is_multipart():
for part in msg.get_payload():
# 递归处理子部分
sub_result = _walk_message_parts(part)
result['body_plain'] += sub_result['body_plain']
result['body_html'] += sub_result['body_html']
result['attachments'].extend(sub_result['attachments'])
else:
content_type = msg.get_content_type()
content_disposition = msg.get_content_disposition()
filename = msg.get_filename()
# 处理附件
if content_disposition == 'attachment' or filename:
filename_decoded = decode_mime_header(filename) if filename else f"attachment_{hash(msg)}"
result['attachments'].append({
'filename': filename_decoded,
'content_type': content_type,
'payload': msg.get_payload(decode=True) # 解码后的二进制内容
})
# 处理纯文本正文
elif content_type == 'text/plain':
payload = msg.get_payload(decode=True)
charset = msg.get_content_charset() or 'utf-8'
try:
result['body_plain'] = payload.decode(charset, errors='ignore')
except:
result['body_plain'] = str(payload)
# 处理HTML正文
elif content_type == 'text/html':
payload = msg.get_payload(decode=True)
charset = msg.get_content_charset() or 'utf-8'
try:
result['body_html'] = payload.decode(charset, errors='ignore')
except:
result['body_html'] = str(payload)
return result
3.2 附件的保存与管理策略
附件提取出来后,我们需要一个有组织地保存它们。直接扔到一个文件夹里,时间久了就会混乱不堪。一个好的做法是按照日期和邮件主题建立目录结构。
import os
from datetime import datetime
def save_attachments(attachments_list, base_save_dir="downloads"):
"""
将附件列表保存到本地磁盘,并返回保存路径信息。
Args:
attachments_list: 由 _walk_message_parts 生成的附件信息列表。
base_save_dir (str): 附件保存的根目录。
Returns:
list: 包含每个附件保存详情的字典列表。
"""
saved_files = []
today_str = datetime.now().strftime("%Y-%m-%d")
for attach_info in attachments_list:
filename = attach_info['filename']
payload = attach_info['payload']
if not payload:
logger.warning(f"附件 '{filename}' 无有效内容,跳过。")
continue
# 创建按日期组织的目录
daily_dir = os.path.join(base_save_dir, today_str)
os.makedirs(daily_dir, exist_ok=True)
# 处理文件名冲突
filepath = os.path.join(daily_dir, filename)
counter = 1
while os.path.exists(filepath):
name, ext = os.path.splitext(filename)
filepath = os.path.join(daily_dir, f"{name}_{counter}{ext}")
counter += 1
# 保存文件
try:
with open(filepath, 'wb') as f:
f.write(payload)
logger.info(f"附件已保存: {filepath}")
saved_files.append({
'original_name': filename,
'saved_path': filepath,
'saved_name': os.path.basename(filepath),
'content_type': attach_info['content_type']
})
except IOError as e:
logger.error(f"保存附件 '{filename}' 到 '{filepath}' 失败: {e}")
return saved_files
现在,我们已经能够将邮件完整地“拆解”并分类存放。邮件正文可以用于日志记录或进一步分析(例如,提取邮件中的关键说明文字),而附件则被整齐地归档到以日期命名的文件夹中,为下一步的数据处理做好了准备。
4. 从数据到洞察:自动化报表生成与任务调度
邮件的解析和保存只是解决了“收”的问题,真正的价值在于“处理”。我们的目标是让机器人自动读取附件中的数据,生成一份直观的统计报告,甚至执行一些预定义的分析动作。
4.1 使用Pandas进行核心数据处理
假设我们每天收到的是一份名为 daily_sales.xlsx 的Excel报表,里面有一张“订单数据”表。我们的任务是统计当日订单总额、平均订单金额、前三大客户等。
import pandas as pd
import numpy as np
from openpyxl import load_workbook # 用于更复杂的Excel操作
def analyze_daily_sales(excel_filepath):
"""
分析每日销售报表Excel文件,生成关键指标。
Args:
excel_filepath (str): Excel文件的完整路径。
Returns:
dict: 包含分析结果的字典。
str: 生成的文本报告内容。
"""
analysis_result = {}
report_lines = []
try:
# 1. 读取数据
# 使用 openpyxl 引擎,可以读取 .xlsx 格式
df = pd.read_excel(excel_filepath, sheet_name='订单数据', engine='openpyxl')
report_lines.append(f"=== 每日销售数据分析报告 ===")
report_lines.append(f"数据文件: {os.path.basename(excel_filepath)}")
report_lines.append(f"分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
report_lines.append(f"总记录数: {len(df)}")
report_lines.append("")
# 2. 基础数据清洗与检查
# 确保金额列是数值类型
if '订单金额' in df.columns:
df['订单金额'] = pd.to_numeric(df['订单金额'], errors='coerce')
# 填充或删除空值
df_clean = df.dropna(subset=['订单金额'])
if len(df) != len(df_clean):
report_lines.append(f"警告: 发现 {len(df) - len(df_clean)} 条订单金额为空的记录,已排除。")
df = df_clean
# 3. 核心指标计算
if not df.empty and '订单金额' in df.columns:
total_sales = df['订单金额'].sum()
avg_order_value = df['订单金额'].mean()
max_order = df['订单金额'].max()
min_order = df['订单金额'].min()
analysis_result['total_sales'] = total_sales
analysis_result['avg_order_value'] = avg_order_value
analysis_result['order_count'] = len(df)
report_lines.append(f"**销售总额**: ¥{total_sales:,.2f}")
report_lines.append(f"**平均订单金额**: ¥{avg_order_value:,.2f}")
report_lines.append(f"**最高订单金额**: ¥{max_order:,.2f}")
report_lines.append(f"**最低订单金额**: ¥{min_order:,.2f}")
report_lines.append("")
# 4. 维度分析:按客户统计
if '客户名称' in df.columns and '订单金额' in df.columns:
report_lines.append(f"**按客户销售排名**:")
customer_sales = df.groupby('客户名称')['订单金额'].sum().sort_values(ascending=False)
analysis_result['top_customers'] = customer_sales.head(5).to_dict()
for i, (customer, sales) in enumerate(customer_sales.head(5).items(), 1):
report_lines.append(f" {i}. {customer}: ¥{sales:,.2f}")
report_lines.append("")
# 5. 维度分析:按产品/类别统计(如果存在相关列)
product_col = None
for col in ['产品名称', '产品类别', 'SKU']:
if col in df.columns:
product_col = col
break
if product_col and '订单金额' in df.columns:
report_lines.append(f"**按{product_col}销售排名**:")
product_sales = df.groupby(product_col)['订单金额'].sum().sort_values(ascending=False)
analysis_result['top_products'] = product_sales.head(5).to_dict()
for i, (product, sales) in enumerate(product_sales.head(5).items(), 1):
report_lines.append(f" {i}. {product}: ¥{sales:,.2f}")
report_lines.append("")
# 6. 趋势或异常检测(简单示例)
if '订单日期' in df.columns:
df['订单日期'] = pd.to_datetime(df['订单日期'], errors='coerce')
daily_trend = df.groupby(df['订单日期'].dt.date)['订单金额'].sum()
if len(daily_trend) > 1:
report_lines.append(f"**近期销售趋势** (最近{min(5, len(daily_trend))}天):")
for date, sales in daily_trend.tail(5).items():
report_lines.append(f" {date}: ¥{sales:,.2f}")
report_content = "\n".join(report_lines)
return analysis_result, report_content
except FileNotFoundError:
error_msg = f"错误: 未找到文件 {excel_filepath}"
logger.error(error_msg)
return {}, error_msg
except Exception as e:
error_msg = f"分析Excel文件时发生未知错误: {e}"
logger.error(error_msg)
return {}, error_msg
这个分析函数展示了从基础统计到多维分析的完整链条。你可以根据自己报表的实际字段,灵活调整和扩展这些分析逻辑。
4.2 让机器人“活”起来:定时任务与自动化执行
一个需要手动触发的脚本,其自动化程度减半。我们需要让脚本在预定时间自动运行。在Windows上,你可以使用“任务计划程序”;在macOS或Linux上,可以使用cron。但这里,我推荐一个纯Python的轻量级方案——schedule库,它能让你的脚本在自身内部管理定时逻辑,便于开发和调试。
import schedule
import time
import sys
def daily_report_job():
"""
每日报表处理任务的主函数。
整合了连接、搜索、解析、处理、分析的完整流程。
"""
logger.info("="*50)
logger.info("开始执行每日邮件报表处理任务...")
start_time = time.time()
# 1. 连接邮箱
mail = create_imap_connection()
if not mail:
logger.error("邮箱连接失败,任务终止。")
return
try:
# 2. 搜索邮件(示例:查找今天来自特定地址的未读邮件)
today = datetime.now().strftime("%d-%b-%Y") # 格式: 05-Dec-2024
search_terms = [
'UNSEEN',
f'FROM "noreply@bi-system.com"',
f'SINCE "{today}"'
]
target_msg_ids = search_target_emails(mail, search_criteria=search_terms)
if not target_msg_ids:
logger.info("今日没有符合条件的待处理报表邮件。")
mail.logout()
return
# 3. 解析并处理每一封邮件
for msg_id in target_msg_ids:
logger.info(f"\n处理邮件 ID: {msg_id}")
email_data = parse_email_message(mail, msg_id)
logger.info(f"主题: {email_data['subject']}")
logger.info(f"发件人: {email_data['from_email']}")
# 4. 保存附件
if email_data['attachments']:
saved_attachments = save_attachments(email_data['attachments'], base_save_dir="./日报附件")
# 5. 分析每一个Excel附件
for attach in saved_attachments:
if attach['saved_path'].endswith(('.xlsx', '.xls')):
logger.info(f"分析文件: {attach['saved_name']}")
analysis_result, report_text = analyze_daily_sales(attach['saved_path'])
# 6. 保存分析报告
report_filename = os.path.splitext(attach['saved_name'])[0] + '_分析报告.txt'
report_path = os.path.join(os.path.dirname(attach['saved_path']), report_filename)
with open(report_path, 'w', encoding='utf-8') as f:
f.write(report_text)
logger.info(f"分析报告已生成: {report_path}")
# 7. (可选)将关键结果通过其他方式通知,如打印到日志或发送到企业微信/钉钉
if analysis_result.get('total_sales'):
logger.info(f"关键指标 -> 销售总额: ¥{analysis_result['total_sales']:,.2f}, 订单数: {analysis_result.get('order_count', 0)}")
# 8. 标记邮件为已处理(已读)
mail.store(msg_id, '+FLAGS', '\\Seen')
logger.info(f"邮件 {msg_id} 已标记为已读。")
except Exception as e:
logger.exception(f"任务执行过程中发生严重错误: {e}")
finally:
# 确保连接被关闭
try:
mail.close()
mail.logout()
logger.info("邮箱连接已关闭。")
except:
pass
elapsed_time = time.time() - start_time
logger.info(f"每日任务执行完毕,耗时 {elapsed_time:.2f} 秒。")
logger.info("="*50)
def main_scheduler():
"""
主调度函数。设置定时任务并运行。
"""
# 设置定时规则
# 示例1:每天上午9点15分执行
schedule.every().day.at("09:15").do(daily_report_job)
# 示例2:每周一到周五上午8点执行
# schedule.every().monday.to(friday).at("08:00").do(daily_report_job)
# 示例3:每两小时执行一次(用于测试)
# schedule.every(2).hours.do(daily_report_job)
logger.info("邮件报表处理机器人已启动,等待执行定时任务...")
logger.info(f"下次任务执行时间: {schedule.next_run()}")
# 保持程序运行,并检查定时任务
try:
while True:
schedule.run_pending()
time.sleep(60) # 每分钟检查一次
except KeyboardInterrupt:
logger.info("程序被用户中断。")
sys.exit(0)
if __name__ == "__main__":
# 可以直接运行一次任务(用于测试)
# daily_report_job()
# 或者启动调度器,让程序持续运行
main_scheduler()
将上述所有代码模块整合到一个Python脚本中,你就拥有了一个功能完整的日报处理机器人。它会在每天指定的时间自动唤醒,登录邮箱,抓取报表,分析数据,生成报告,然后继续休眠。整个过程无需人工干预。
5. 从“能用”到“好用”:高级技巧与避坑指南
在基本流程跑通之后,我们可以进一步优化这个系统,让它更稳定、更智能、更符合生产环境的要求。
5.1 状态持久化与防重复处理
我们的脚本目前基于“未读”状态来搜索邮件。但如果脚本运行中途出错,或者同一封邮件被多次匹配,可能会导致附件被重复下载和分析。一个更健壮的做法是维护一个已处理邮件的记录。
import json
import hashlib
PROCESSED_LOG_FILE = "processed_emails.log"
def get_email_unique_id(email_data):
"""生成邮件的唯一标识符,用于去重。通常结合 Message-ID 和 日期。"""
unique_string = f"{email_data.get('from_email', '')}_{email_data.get('subject','')}_{email_data.get('date', '')}"
return hashlib.md5(unique_string.encode()).hexdigest()
def is_email_processed(email_uid):
"""检查邮件是否已被处理过。"""
if not os.path.exists(PROCESSED_LOG_FILE):
return False
try:
with open(PROCESSED_LOG_FILE, 'r') as f:
processed_ids = set(json.load(f))
return email_uid in processed_ids
except:
return False
def mark_email_as_processed(email_uid):
"""将邮件标记为已处理。"""
processed_ids = set()
if os.path.exists(PROCESSED_LOG_FILE):
try:
with open(PROCESSED_LOG_FILE, 'r') as f:
processed_ids = set(json.load(f))
except:
pass
processed_ids.add(email_uid)
with open(PROCESSED_LOG_FILE, 'w') as f:
json.dump(list(processed_ids), f)
在主处理循环中,在解析邮件后,先检查is_email_processed(email_uid),如果已处理则跳过。在所有处理完成后,调用mark_email_as_processed(email_uid)。
5.2 异常处理与邮件通知
自动化脚本在无人值守运行时,必须能妥善处理异常,并告知我们发生了什么。除了记录日志文件,我们还可以让脚本在失败时给自己发一封报警邮件(使用smtplib库),或者将错误信息发送到团队协作工具(如企业微信机器人、钉钉机器人)。
import smtplib
from email.mime.text import MIMEText
def send_alert_email(subject, body, to_addr):
"""通过SMTP发送错误警报邮件。"""
from_addr = os.getenv('EMAIL_USER') # 发件人
smtp_server = os.getenv('EMAIL_SMTP_SERVER', 'smtp.example.com')
smtp_port = int(os.getenv('EMAIL_SMTP_PORT', 587))
password = os.getenv('EMAIL_PASSWORD')
msg = MIMEText(body, 'plain', 'utf-8')
msg['Subject'] = subject
msg['From'] = from_addr
msg['To'] = to_addr
try:
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls() # 启用TLS加密
server.login(from_addr, password)
server.send_message(msg)
server.quit()
logger.info("警报邮件发送成功。")
except Exception as e:
logger.error(f"发送警报邮件失败: {e}")
# 在主任务函数 daily_report_job 的异常捕获块中调用
# except Exception as e:
# error_msg = f"日报处理机器人执行失败: {str(e)}"
# logger.exception(error_msg)
# send_alert_email("【自动化脚本警报】日报处理失败", error_msg, "your_alert@company.com")
5.3 性能考量与扩展思路
- 并发处理:如果需要处理数百封邮件,可以考虑使用
concurrent.futures模块的ThreadPoolExecutor来并发解析邮件,但要注意邮箱服务器的连接限制。 - 增量处理:对于非常大的邮箱,每次搜索“所有未读邮件”可能效率低下。可以改为搜索“今天以来的未读邮件”(
SINCE),或者记录上次处理的时间点。 - 与工作流集成:生成的分析报告,除了保存为文本,还可以自动写入数据库(如SQLite、MySQL)、更新到在线文档(通过API),或者作为附件自动回复给指定人员。
- 配置化:将搜索条件、分析规则、报告模板等抽离到配置文件(如YAML或JSON)中,使脚本无需修改代码就能适配不同的报表格式和处理需求。
将这套系统部署到一台始终开机的电脑或服务器上,它就会成为你最可靠的数字助理。我自己的脚本已经稳定运行了超过一年,每天早晨都能在喝第一杯咖啡前,在指定文件夹里看到整理好的数据和生成好的报告。它处理过附件名乱码、邮件格式异常、网络临时中断等各种小问题,但核心流程从未掉链子。最大的收获不是节省的时间,而是那种从重复劳动中解放出来,可以将精力专注于更有价值的数据分析和决策上的掌控感。如果你在实现过程中遇到任何问题,不妨从最核心的“连接-搜索-解析-保存”链路开始,确保每一步都稳固,再逐步添加更复杂的功能。
359



被折叠的 条评论
为什么被折叠?



