Python自动发邮件实战定时发报表

Python 自动发邮件实战:每天 8 点把报表自动发到邮箱

摘要: 每天早上到公司第一件事:打开前一天的报表、写邮件、发老板?这件事完全可以交给 Python:本文用 标准库 smtplib + email 组装带 Excel 附件的邮件,用 schedule 做定时,每天 8 点自动投递到老板邮箱。更关键的是,本文提供一个 "零成本调试法"------用本地调试 SMTP 服务器把发信链路完整验证通,不用真邮箱、不用真实授权码就能把代码写对。全文代码均已实测通过。


标题


一、效果预览与整体思路

最终效果:脚本挂在后台上,每天 08:00 自动把《9 月销售汇总报表.xlsx》(系列第 1 篇的产出)作为附件发给老板,正文是一段简短的 HTML 说明。

整体结构是一张三层积木:

复制代码
① 组装邮件 build_message()   →   ② 发送邮件 send_mail()   →   ③ 定时调度 run_scheduler()
EmailMessage:正文+附件          smtplib:SMTP_SSL           schedule:每天 8 点触发

每一层都能单独测试------这是本篇最重要的工程思想:先保证"一封信能发出去",再保证"到点会触发"。

二、准备工作:拿到邮箱"授权码"

用代码发邮件,登录用的不是邮箱密码,而是"授权码"(第三方客户端专用密码):

  • QQ 邮箱 :设置 → 账号 → 开启 SMTP 服务 → 生成授权码;SMTP 服务器 smtp.qq.com,SSL 端口 465;
  • 163 邮箱 :设置 → POP3/SMTP/IMAP → 开启并生成授权码;服务器 smtp.163.com,SSL 端口 465。

企业邮箱找 IT 要 SMTP 地址和端口即可,逻辑完全一样。

安全提醒 :授权码相当于发信密码,不要硬编码进会外传的代码,更不要提交到 Git。本文为演示写在配置区,正式项目请用环境变量 os.environ["MAIL_AUTH_CODE"]。

三、组装邮件:EmailMessage 三步走

推荐用新式的 email.message.EmailMessage(标准库),比老式的 MIMEMultipart 一堆类拼装清爽得多:

python 复制代码
from email.message import EmailMessage
from pathlib import Path

def build_message(subject: str, html_body: str,
                  attachment: Path | None = None) -> EmailMessage:
    """组装一封"正文 + 附件"的邮件对象(与发送解耦,方便单独测试)"""
    msg = EmailMessage()
    msg["Subject"] = subject
    msg["From"] = SENDER
    msg["To"] = ", ".join(RECEIVERS)
    msg.set_content(html_body, subtype="html")          # 支持 HTML 正文

    if attachment is not None and attachment.exists():
        # filename 直接传中文,EmailMessage 会自动按标准编码,不会乱码
        msg.add_attachment(attachment.read_bytes(),
                           maintype="application", subtype="xlsx",
                           filename=attachment.name)
    return msg

四个值得记住的点:

  1. set_content(body, subtype="html"):邮件正文天然支持 HTML,加个颜色、加个签名链接,比纯文本体面得多;
  2. add_attachment() 传中文文件名不乱码 :它会自动按 RFC 2231 标准编码;老式 MIMEApplication 里那套 add_header("Content-Disposition", ..., filename=("gbk", "", name)) 的黑魔法可以扔了;
  3. 组装与发送分离 :build_message 只造信不发信,单元测试友好;
  4. 附件存在才附 :报表没生成时脚本不会崩,只是少个附件或跳过(后面 job() 里有检查)。

小科普:邮件里的中文主题在传输时会被编码成形如 =?utf-8?b?44CQ...?= 的样子,这是 RFC 2047 标准编码,收件端(Outlook/Foxmail/手机邮箱)都会自动还原成中文,不是乱码。

四、发送邮件:SMTP_SSL 十行搞定

python 复制代码
import smtplib

def send_mail(msg: EmailMessage, host: str = SMTP_HOST, port: int = SMTP_PORT,
              debug_server: str | None = None):
    """发送邮件;debug_server 形如 'localhost:1025',走本地调试服务器"""
    if debug_server:
        d_host, d_port = debug_server.rsplit(":", 1)
        with smtplib.SMTP(d_host, int(d_port), timeout=10) as smtp:
            smtp.send_message(msg)
        print(f"[调试] 邮件已投递到本地调试服务器 {debug_server}")
        return

    with smtplib.SMTP_SSL(host, port, timeout=15) as smtp:   # 465 端口走 SSL
        smtp.login(SENDER, AUTH_CODE)
        smtp.send_message(msg)
    print(f"邮件已发送:{msg['Subject']} -> {msg['To']}")

注意 465 端口要用 SMTP_SSL (连接即加密);如果用 587 端口,则是 SMTP() + smtp.starttls()。两者别混。

五、零成本调试:本地 SMTP 服务器大法

学这篇最大的障碍通常是:没有授权码/不好意思用真邮箱反复测试。解法是在本机跑一个调试 SMTP 服务器,它不真发信,只把收到的邮件打印出来------

bash 复制代码
pip install aiosmtpd
python -m aiosmtpd -n -l localhost:1025

保持这个窗口开着,另开一个窗口运行脚本时加上 --debug-server localhost:1025(见第七节完整代码),邮件就"发"给了本地服务器,终端会完整打印出协议交互和邮件内容:

text 复制代码
---------- MESSAGE FOLLOWS ----------
Subject: =?utf-8?b?44CQ5rWL6K+V44CRUHl0aG9uIOiHquWKqOaKpeihqOmCruS7tg==?=
From: your_account@qq.com
To: boss@example.com
Content-Type: multipart/mixed; ...

Content-Disposition: attachment;
 filename*0*=utf-8''9%E6%9C%88%E9%94%80%E5%94%AE%E6%B1%87%E6%80%BB%E6%8A%A5;
 filename*1*=%E8%A1%A8.xlsx

从这里可以直接确认:主题编码正确、附件文件名 9月销售汇总报表.xlsx 编码正确、正文是 HTML。链路验证通过后,把配置换成真实邮箱,一次性切换成功率极高。

六、定时调度:schedule 让脚本每天自动跑

定时用 schedule(轻量,几行上手):

bash 复制代码
pip install schedule
python 复制代码
import schedule
import time

def job(debug_server: str | None = None):
    """到点要做的事:组装并发送报表邮件"""
    if not ATTACHMENT.exists():
        print(f"[跳过] 找不到报表文件:{ATTACHMENT}")
        return
    body = ("<p>早上好,昨日销售报表见附件。</p>"
            "<p style='color:#888'>本邮件由 Python 脚本自动发送</p>")
    send_mail(build_message("【自动】9 月销售汇总报表", body, ATTACHMENT),
              debug_server=debug_server)

def run_scheduler(test_seconds: int | None = None,
                  debug_server: str | None = None):
    if test_seconds:
        # 测试模式:每 test_seconds 秒触发一次,验证调度链路
        schedule.every(test_seconds).seconds.do(job, debug_server=debug_server)
        print(f"[测试] 每 {test_seconds} 秒触发一次,Ctrl+C 退出")
    else:
        schedule.every().day.at("08:00").do(job, debug_server=debug_server)
        print("定时任务已注册:每天 08:00 发送报表,Ctrl+C 退出")

    while True:
        try:
            schedule.run_pending()
        except Exception as e:
            # 单次任务失败(网络抖动等)不能把整个调度循环搞挂
            print(f"[任务异常] {e}")
        time.sleep(1)

三个实战细节:

  1. schedule.every().day.at("08:00") :几点几分触发一目了然,还支持 .monday、.hour、.seconds 等各种粒度;
  2. do(job, debug_server=...) 可以带参数 ,同一份 job() 既能正式运行也能接调试服务器;
  3. 调度循环里的 try/except 是保命符:某天网络抖动发信失败,抛了异常,若不接住,整个调度循环直接死掉,你第二天才发现老板没收到报表。单次失败只打日志,循环永续。

七、完整代码

保存为 mail_report.py:

python 复制代码
# -*- coding: utf-8 -*-
"""
自动发邮件实战:每天 8 点把销售报表自动发到邮箱
依赖:pip install schedule
本地联调(可选):pip install aiosmtpd

用法:
  python mail_report.py --test-mail            # 通过本地调试服务器发一封测试邮件
  python mail_report.py --test-schedule        # 定时器按"每 3 秒"触发,验证调度逻辑
  python mail_report.py                        # 正式模式:每天 08:00 发送(需先改配置)
"""
import argparse
import smtplib
import time
from email.message import EmailMessage
from pathlib import Path

import schedule

# ============ 邮箱配置(正式使用前改成你自己的)============
SMTP_HOST = "smtp.qq.com"          # QQ 邮箱 SMTP;163 邮箱为 smtp.163.com
SMTP_PORT = 465                    # SSL 端口
SENDER = "your_account@qq.com"
AUTH_CODE = "你的授权码"            # 不是登录密码!在邮箱设置里生成
RECEIVERS = ["boss@example.com"]   # 支持多个收件人

ATTACHMENT = Path(__file__).parent / "9月销售汇总报表.xlsx"   # 要发的报表


def build_message(subject: str, html_body: str,
                  attachment: Path | None = None) -> EmailMessage:
    """组装一封"正文 + 附件"的邮件对象(与发送解耦,方便单独测试)"""
    msg = EmailMessage()
    msg["Subject"] = subject
    msg["From"] = SENDER
    msg["To"] = ", ".join(RECEIVERS)
    msg.set_content(html_body, subtype="html")          # 支持 HTML 正文

    if attachment is not None and attachment.exists():
        msg.add_attachment(attachment.read_bytes(),
                           maintype="application", subtype="xlsx",
                           filename=attachment.name)
    return msg


def send_mail(msg: EmailMessage, host: str = SMTP_HOST, port: int = SMTP_PORT,
              debug_server: str | None = None):
    """发送邮件;debug_server 形如 'localhost:1025',走本地调试服务器"""
    if debug_server:
        d_host, d_port = debug_server.rsplit(":", 1)
        with smtplib.SMTP(d_host, int(d_port), timeout=10) as smtp:
            smtp.send_message(msg)
        print(f"[调试] 邮件已投递到本地调试服务器 {debug_server}")
        return

    with smtplib.SMTP_SSL(host, port, timeout=15) as smtp:   # 465 端口走 SSL
        smtp.login(SENDER, AUTH_CODE)
        smtp.send_message(msg)
    print(f"邮件已发送:{msg['Subject']} -> {msg['To']}")


# ============ 定时任务 ============
def job(debug_server: str | None = None):
    """到点要做的事:组装并发送报表邮件"""
    if not ATTACHMENT.exists():
        print(f"[跳过] 找不到报表文件:{ATTACHMENT}")
        return
    body = ("<p>早上好,昨日销售报表见附件。</p>"
            "<p style='color:#888'>本邮件由 Python 脚本自动发送</p>")
    send_mail(build_message("【自动】9 月销售汇总报表", body, ATTACHMENT),
              debug_server=debug_server)


def run_scheduler(test_seconds: int | None = None,
                  debug_server: str | None = None):
    if test_seconds:
        schedule.every(test_seconds).seconds.do(job, debug_server=debug_server)
        print(f"[测试] 每 {test_seconds} 秒触发一次,Ctrl+C 退出")
    else:
        schedule.every().day.at("08:00").do(job, debug_server=debug_server)
        print("定时任务已注册:每天 08:00 发送报表,Ctrl+C 退出")

    while True:
        try:
            schedule.run_pending()
        except Exception as e:
            print(f"[任务异常] {e}")
        time.sleep(1)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="定时发送销售报表邮件")
    parser.add_argument("--test-mail", action="store_true",
                        help="通过本地调试服务器发一封测试邮件")
    parser.add_argument("--test-schedule", action="store_true",
                        help="调度器按秒触发,快速验证定时逻辑")
    parser.add_argument("--debug-server", default="localhost:1025",
                        help="本地调试 SMTP 地址(默认 localhost:1025)")
    args = parser.parse_args()

    if args.test_mail:
        send_mail(build_message("【测试】Python 自动报表邮件",
                                "<p>这是一封测试邮件</p>", ATTACHMENT),
                  debug_server=args.debug_server)
    elif args.test_schedule:
        run_scheduler(test_seconds=3, debug_server=args.debug_server)
    else:
        run_scheduler()

八、运行结果

第一步,调试服务器先跑起来(窗口 A):

bash 复制代码
pip install aiosmtpd
python -m aiosmtpd -n -l localhost:1025

第二步,发一封测试邮件(窗口 B):

text 复制代码
$ python mail_report.py --test-mail
[调试] 邮件已投递到本地调试服务器 localhost:1025

窗口 A 立刻打印出收到的完整邮件(主题、HTML 正文、附件俱在)------发信链路验证通过。

第三步,验证定时调度(每 3 秒触发一次,实测):

text 复制代码
$ python mail_report.py --test-schedule
[测试] 每 3 秒触发一次,Ctrl+C 退出
[调试] 邮件已投递到本地调试服务器 localhost:1025
[调试] 邮件已投递到本地调试服务器 localhost:1025

每 3 秒准时投递一封,调度链路也验证通过。

第四步,正式上线 :把配置区改成真实邮箱和授权码,直接 python mail_report.py,注册的任务是"每天 08:00",到点自动发信。

九、生产环境部署建议与常见坑

部署方式二选一:

  • 方案 A:脚本内建调度 (本文做法)。电脑/服务器上挂着 python mail_report.py 不退出即可;Windows 可用"任务计划程序"设为开机自启,Linux 用 systemd 或 nohup 守护;
  • 方案 B:系统级调度 。脚本改成"跑一次就退出"(去掉 while 循环),交给 Windows 任务计划程序每天 8:00 触发,或 Linux crontab:0 8 * * * python /path/mail_report.py。系统级调度更稳(不怕脚本意外退出),推荐生产使用。

常见坑:

现象 原因 解法
(535, b'Error: authentication failed') 用了登录密码而非授权码 去邮箱设置重新生成授权码
连接超时 端口/加密方式不匹配 465 用 SMTP_SSL;587 用 SMTP+starttls
收件人收到乱码主题 手动拼邮件头 用 EmailMessage,让它自动编码
附件名乱码 老式 API 手写 filename 头 add_attachment(..., filename=中文名)
定时脚本"失踪" 循环里异常没接住,进程已死 见第六节的 try/except

壁纸分享

十、总结

复盘本篇的三层积木和两个法宝:

  1. 组装 :EmailMessage + set_content(html) + add_attachment(中文名);
  2. 发送 :SMTP_SSL + login(授权码) + send_message;
  3. 调度 :schedule.every().day.at("08:00") + 异常保护循环;
  4. 法宝一:本地调试服务器(aiosmtpd),零成本把发信链路调通;
  5. 法宝二:按秒测试模式,不用等到早上 8 点才知道定时灵不灵。

在这个基础上再叠加第 1 篇的"自动合并 Excel",你就得到了一条完整的无人值守流水线:凌晨自动合并 → 早上 8 点自动发。

如果本文对你有帮助,点赞 + 收藏 + 关注三连支持一下~ 你想让脚本定时发什么?评论区聊聊。下一篇我们写《Python 批量处理 Word/PDF 实战:python-docx + PyMuPDF》。


相关推荐
王中阳Go2 小时前
5.27 分钟干完后端和全部测试:一场三小时笔试的五个失分点,四个不在代码上
大数据·数据库·人工智能·后端·大语言模型
Wang's Blog2 小时前
Java 项目实战:交友项目 - 用户体系集成总结
java·开发语言·交友
演艺大赏2 小时前
Paimon 1.3.1 报 Proxy user is not supported:Kyuubi + Kerberos 集成排查与源码级定位
大数据
中科三方2 小时前
IPv6检测常见问题:AAAA记录检测不通过是什么原因?
开发语言·php
财迅通Ai2 小时前
远信储能(9621.HK)招股开启,AI+储能跑出新范式
大数据·人工智能·远信储能
卡提西亚3 小时前
FastAPI基础入门
python·fastapi
fanruitian3 小时前
python 机器学习 ML LinearRegression KNN KMeans
python·机器学习·kmeans
一只鹿鹿鹿3 小时前
智算算力、AI-芯片与产品解析(PPT文件)
大数据·运维·网络·人工智能·物联网·1024程序员节
鱼宵3 小时前
取集合第一个元素,Java 为什么要写三种写法?
java·windows·python·java21