Python办公自动化实战:批量处理Excel、Word与自动发送邮件

纸上星河
纸上星河
发布时间:2026-11-11| 3人浏览过
原创

针对日常办公中频繁遇到的数据提取、文档套打与定制发信场景,本文通过完整的企业考评通知单案例,详解如何使用Python协同openpyxl、python-docx与smtplib实现批量化处理与自动分发。

在行政、人事与财务日常业务中,经常需要把 Excel 中的表格明细拆分出来,按固定排版套入独立的 Word 模板,再将生成的文档作为附件逐一发送给对应的员工或客户。本文通过一个标准的“月度绩效考评通知单”综合实战案例,讲解如何使用 openpyxl 批量读取与清洗表格数据、使用 python-docx 精确映射占位符批量生成定制 Word 文档,最后通过 Python 标准库 smtplib 与 email 自动完成安全加密发信与附件分发。
Python办公自动化实战:批量处理Excel、Word与自动发送邮件
图 1:Python 办公自动化全流程架构总览(数据流转与模块关系)

一、实战需求与技术链路拆解

为了让整套自动化流程贴合真实办公场景,我们设定一个具体的业务目标:人事部门有一份名为 staff_eval.xlsx 的员工考核表,里面包含员工姓名、部门、考评得分、绩效奖金、考评评语以及个人公司邮箱。

人工处理该业务的痛点在于:若部门有 100 名员工,工作人员必须打开 Word 模板复制粘贴 100 次、保存为 100 个文件,并手动撰写 100 封邮件且逐个上传附件,整个过程繁琐且极易混淆名单或发错附件。通过 Python 脚本,我们可以将该流程抽象为四个标准化阶段:

  • 输入阶段:使用 openpyxl 打开 Excel 工作簿,提取每一行考评数据,并完成空值校验与邮箱格式合法性过滤。
  • 处理阶段:读取统一的 template.docx 模板文件,利用占位符替换机制,将字段填入指定的段落与表格中,另存为个性化命名的通知单文档。
  • 输出阶段:利用 email.mime 组装包含 HTML 正文与 Word 附件的 MIME 复合邮件对象。
  • 分发阶段:通过 smtplib.SMTP_SSL 连接企业邮件服务器,完成安全鉴权并按频率限制逐封发送,记录成功与失败日志。
Python办公自动化实战:批量处理Excel、Word与自动发送邮件
图 2:办公自动化项目实操四步法(从环境配置到投递回执)

二、运行环境与工作目录规范

在开始编写代码前,我们需要安装用于处理 Office 文件的第三方核心库。Python 内置了邮件收发所需的 smtplib 与 email 模块,因此只需要安装 openpyxl 和 python-docx:

pip install openpyxl python-docx
项目推荐目录结构:
为了保证代码中相对路径清晰统一,建议按如下结构规划项目文件夹:
auto_office_project/
├── data/
│   └── staff_eval.xlsx       # 员工考评源数据表格
├── templates/
│   └── template.docx          # Word 官方模板文件(含 {{name}} 等占位符)
├── output_docs/               # 批量生成的 Word 成果存放目录(由代码自动创建)
├── logs/                      # 发送日志与异常记录
└── main.py                    # 核心自动化串联脚本

三、Excel 数据的批量提取与规范化清洗

Excel 文件在实际业务流转中往往存在空单元格、浮点数精度多位、空格以及格式错乱等问题。如果直接将原始读取值填充至 Word 中,会导致最终版面极不美观,若邮箱格式有误更会导致后续邮件发送直接崩溃。因此,读取过程必须包含数据规整逻辑。

我们使用 openpyxl 的 iter_rows(values_only=True) 遍历数据,将有效记录转换为字典列表,具体实现如下:

import re
from openpyxl import load_workbook

def load_and_validate_excel(file_path):
    """
    加载 Excel 数据表并清洗校验
    返回: list[dict] 标准化记录列表
    """
    wb = load_workbook(filename=file_path, data_only=True)
    sheet = wb.active

    records = []
    # 邮箱基础校验正则
    email_regex = re.compile(r"^[\w\.-]+@[\w\.-]+\.\w+$")

    # 获取所有行数据
    rows = list(sheet.iter_rows(values_only=True))
    if not rows or len(rows) < 2:
        print("[提示] Excel 数据为空或未包含数据行")
        return []

    # 第一行为表头
    headers = [str(h).strip() if h is not None else "" for h in rows[0]]

    # 从第二行开始处理实际数据
    for row_idx, row in enumerate(rows[1:], start=2):
        row_dict = dict(zip(headers, row))
        
        # 提取核心字段并清理空白字符
        name = str(row_dict.get("姓名") or "").strip()
        dept = str(row_dict.get("部门") or "").strip()
        email = str(row_dict.get("邮箱") or "").strip()
        
        # 基础数据空值校验
        if not name or not email:
            print(f"[跳过] 第 {row_idx} 行姓名或邮箱缺失,已忽略")
            continue
        
        # 邮箱格式验证
        if not email_regex.match(email):
            print(f"[警告] 第 {row_idx} 行员工 {name} 邮箱格式无效: {email}")
            continue

        # 数值清洗:得分转浮点数并保留1位小数,奖金转浮点数并格式化千分位
        try:
            score = float(row_dict.get("考核得分") or 0.0)
            score_str = f"{score:.1f}"
        except (ValueError, TypeError):
            score_str = "0.0"

        try:
            bonus = float(row_dict.get("绩效奖金") or 0.0)
            bonus_str = f"¥{bonus:,.2f}"
        except (ValueError, TypeError):
            bonus_str = "¥0.00"

        comment = str(row_dict.get("评语") or "考核合格,请保持良好工作状态。").strip()

        records.append({
            "name": name,
            "dept": dept,
            "email": email,
            "score": score_str,
            "bonus": bonus_str,
            "comment": comment
        })

    wb.close()
    return records
关键参数说明:
load_workbook 的 data_only=True 参数至关重要。如果 Excel 单元格中包含公式(例如 =SUM(...) 或 =VLOOKUP(...)),不加该参数读取出来的将是公式文本字符串而非计算结果,指定为 True 则能直接读取到最近一次保存的数值缓存。

四、Word 模板的占位符设计与跨 Run 渲染

在批量生成文档环节,最稳健的做法是事先制作一个样式完整的 Word 母版 template.docx。在文档中的动态位置写上约定的占位标记,例如 {{name}}、{{dept}}、{{score}}、{{bonus}} 和 {{comment}}。

Python办公自动化实战:批量处理Excel、Word与自动发送邮件
图 3:Excel 数据向 Word 模板流转与 Run 分片重构机制

解决 python-docx 占位符断裂的核心难点

许多初学者在直接调用 paragraph.text.replace(...) 时会遇到两个严重问题:

  1. 直接对 paragraph.text 进行赋值会破坏段落内既有的字体、字号、颜色和加粗等格式。
  2. Word 在底层 XML 中将段落拆分为若干个连续的 Run 元素。当用户编辑过模板文本后,原本连贯的 {{name}} 可能会被拆散在两个甚至三个不同的 Run 中(例如 Run 1 为 {,Run 2 为 {name,Run 3 为 }})。此时若仅遍历单个 Run 进行替换,将完全匹配不到占位符。

针对这一问题,严谨的解决方案是:先判断段落或单元格的整体文本是否包含占位符;若包含,则在保留第一个 Run 格式的前提下进行统一字符串替换,并将多余的后续 Run 内容清空。对于普通业务通知单,这种处理方式既能保证版面格式不变,又能确保占位符 100% 被替换成功。

import os
from docx import Document

def replace_text_in_paragraph(paragraph, data_dict):
    """
    安全替换段落内的占位符,保持段落首个有效 Run 的排版格式
    """
    full_text = paragraph.text
    needs_replace = False
    
    # 检查是否包含任一目标字段
    for key in data_dict.keys():
        placeholder = f"{{{{{key}}}}}"
        if placeholder in full_text:
            needs_replace = True
            break
            
    if not needs_replace:
        return

    # 替换完整文本
    for key, val in data_dict.items():
        placeholder = f"{{{{{key}}}}}"
        full_text = full_text.replace(placeholder, str(val))

    # 若段落包含 runs,将替换后的文本赋给第一个 run,后续清空
    if paragraph.runs:
        paragraph.runs[0].text = full_text
        for r in paragraph.runs[1:]:
            r.text = ""
    else:
        paragraph.text = full_text

def generate_word_document(template_path, output_path, data_dict):
    """
    基于模板填充数据并生成独立 docx 文档
    """
    doc = Document(template_path)

    # 1. 扫描文档正文段落
    for paragraph in doc.paragraphs:
        replace_text_in_paragraph(paragraph, data_dict)

    # 2. 扫描文档内所有表格中的单元格段落
    for table in doc.tables:
        for row in table.rows:
            for cell in row.cells:
                for paragraph in cell.paragraphs:
                    replace_text_in_paragraph(paragraph, data_dict)

    # 保存为新文件
    doc.save(output_path)

五、基于 SMTP 的自动化邮件分发与附件挂载

生成独立文件后,需要将文件作为附件发给员工。Python 标准库自带完整的网络与邮件协议支持,无需安装第三方客户端即可实现端到端自动化。

Python办公自动化实战:批量处理Excel、Word与自动发送邮件
图 4:SMTP 安全通信与 MIME 多部件邮件组装流图

邮件协议核心概念与鉴权要求

在实际编写发信代码前,有以下三项前置条件需要明确:

  • 必须使用授权码而非登录密码:主流邮箱服务商(如 QQ 邮箱、163 邮箱、企业微信邮箱、Office 365 等)为了账户安全,在第三方客户端通过 SMTP 登录时,必须在网页端“账户设置 - POP3/IMAP/SMTP/Exchange 服务”中开启服务并生成专用的“授权码 / 应用密码”。直接使用网页登录密码会触发 535 Error: authentication failed。
  • MIME 复合报文封装:一封包含 HTML 格式正文和 .docx 附件的邮件,顶层容器必须是 MIMEMultipart("mixed")。正文使用 MIMEText(..., "html", "utf-8") 构造,附件则使用 MIMEApplication 挂载。
  • 附件中文文件名防乱码:为了避免邮件客户端将中文文件名显示为乱码或 bin 文件,必须使用 email.header.Header 对 Content-Disposition 中的文件名进行 RFC 2231 / Base64 编码。
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.header import Header

def create_email_message(sender_email, receiver_email, subject, html_content, attachment_path):
    """
    构造包含 HTML 富文本正文与 Word 附件的标准 MIME 邮件对象
    """
    msg = MIMEMultipart("mixed")
    msg["From"] = Header(f"人力资源部 <{sender_email}>", "utf-8")
    msg["To"] = Header(receiver_email, "utf-8")
    msg["Subject"] = Header(subject, "utf-8")

    # 1. 挂载 HTML 正文
    html_part = MIMEText(html_content, "html", "utf-8")
    msg.attach(html_part)

    # 2. 挂载 Word 附件
    if attachment_path and os.path.exists(attachment_path):
        filename = os.path.basename(attachment_path)
        with open(attachment_path, "rb") as f:
            attachment_data = f.read()
            
        app_part = MIMEApplication(attachment_data)
        # 对中文文件名进行编码封装,兼容主流邮件客户端
        app_part.add_header(
            "Content-Disposition", 
            "attachment", 
            filename=("utf-8", "", filename)
        )
        msg.attach(app_part)

    return msg

def send_single_mail(smtp_server, smtp_port, sender_email, auth_code, receiver_email, msg):
    """
    通过 SSL 加密通道建立连接并投递邮件
    """
    server = None
    try:
        # 使用 465 端口建立 SSL 加密会话
        server = smtplib.SMTP_SSL(smtp_server, smtp_port, timeout=15)
        server.login(sender_email, auth_code)
        server.sendmail(sender_email, [receiver_email], msg.as_string())
        return True, "发送成功"
    except Exception as e:
        return False, str(e)
    finally:
        if server:
            try:
                server.quit()
            except Exception:
                pass

六、端到端一体化自动化实战脚本

将上述模块连接在一起,我们便可以编写出完整的批量自动化处理入口程序。脚本会自动遍历 Excel 表格、创建目标文件夹、批量生成专属 Word 通知书,并在发送邮件后加入 2 秒的频控保护,防止因短时间内并发请求过高而被邮件服务器阻断。

import os
import time
import logging

# 配置日志记录
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)

def run_automation_pipeline():
    # 配置基础路径
    base_dir = os.path.abspath(os.path.dirname(__file__))
    excel_path = os.path.join(base_dir, "data", "staff_eval.xlsx")
    template_path = os.path.join(base_dir, "templates", "template.docx")
    output_dir = os.path.join(base_dir, "output_docs")

    # 确保输出目录存在
    os.makedirs(output_dir, exist_ok=True)

    # 邮件服务配置(推荐通过环境变量获取敏感凭据)
    smtp_server = os.environ.get("SMTP_SERVER", "smtp.corp-mail.com")
    smtp_port = int(os.environ.get("SMTP_PORT", 465))
    sender_email = os.environ.get("SENDER_EMAIL", "hr_notice@corp-mail.com")
    auth_code = os.environ.get("AUTH_CODE", "YOUR_AUTHORIZATION_CODE")

    logging.info(">>> 第一步:读取并清洗 Excel 考评数据")
    records = load_and_validate_excel(excel_path)
    total_count = len(records)
    logging.info(f"成功提取有效记录共计 {total_count} 条")

    if total_count == 0:
        logging.warning("没有需要处理的数据,程序退出。")
        return

    success_count = 0
    fail_count = 0

    logging.info(">>> 第二步:开始批量生成 Word 文档并执行个性化邮件分发")
    for index, item in enumerate(records, start=1):
        name = item["name"]
        receiver_email = item["email"]
        logging.info(f"正在处理 [{index}/{total_count}]: {name} ({receiver_email})")

        # 1. 生成独立的 Word 文档
        doc_filename = f"月度考评通知单_{name}.docx"
        doc_output_path = os.path.join(output_dir, doc_filename)
        
        try:
            generate_word_document(template_path, doc_output_path, item)
        except Exception as e:
            logging.error(f"为 {name} 生成 Word 失败: {e}")
            fail_count += 1
            continue

        # 2. 组装邮件内容
        mail_subject = f"【考评结果通知】{item['dept']} - {name} 同事月度绩效核算通知"
        mail_html = f"""
        <div style="font-family: sans-serif; line-height: 1.6; color: #333;">
            <h3 style="color: #2c3e50;">尊敬的 {name} 同事:</h3>
            <p>您本月的绩效考评已经完成核算,综合得分:<strong style="color: #27ae60;">{item['score']}</strong>。</p>
            <p>核算绩效奖金为:<strong style="color: #e67e22;">{item['bonus']}</strong>。</p>
            <p>具体评语与考核细则详见随信附带的 Word 附件通知单,请查收。</p>
            <hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
            <p style="font-size: 12px; color: #7f8c8d;">此邮件由 HR 办公自动化系统自动发出,如有异议请在三个工作日内联系人力资源部。</p>
        </div>
        """

        msg = create_email_message(
            sender_email=sender_email,
            receiver_email=receiver_email,
            subject=mail_subject,
            html_content=mail_html,
            attachment_path=doc_output_path
        )

        # 3. 投递邮件
        success, info = send_single_mail(
            smtp_server=smtp_server,
            smtp_port=smtp_port,
            sender_email=sender_email,
            auth_code=auth_code,
            receiver_email=receiver_email,
            msg=msg
        )

        if success:
            logging.info(f"[{name}] 邮件发送成功!")
            success_count += 1
        else:
            logging.error(f"[{name}] 邮件发送失败: {info}")
            fail_count += 1

        # 频控休眠:避免触发 SMTP 服务商防滥发保护机制
        time.sleep(1.5)

    logging.info("=" * 50)
    logging.info(f"自动化全流程执行完毕!总计: {total_count}, 成功: {success_count}, 失败: {fail_count}")

if __name__ == "__main__":
    run_automation_pipeline()

七、生产运行与异常防御检查

将脚本投入正式生产运行前,必须建立健全的防御策略,防止因为网络中断、脏数据或邮箱服务商策略导致批量任务中途夭折。

Python办公自动化实战:批量处理Excel、Word与自动发送邮件
图 5:办公自动化运维与异常防御四大防线总览

高频疑难问题与解决策略

异常场景 典型错误提示 产生根源与解决方案
SMTP 535 认证报错 535 Error: authentication failed 使用了邮箱网页端普通登录密码。必须登录邮箱管理后台生成专用的“客户端授权码”填入 auth_code。
SMTP 554 垃圾邮件拦截 554 DT:SPM / Message rejected 正文缺少明确收发件人、发信频次过高或正文包含营销高频词。解决方案:降低发送速率(sleep 2~3s),在正文与主题中规范公司真实抬头。
Word 占位符未替换 文档生成后仍显示 {{name}} Word 排版时将大括号分割到不同的 Run 块中。使用本文第四节的“段落级合并替换”函数,先清空后续 Run 再统一赋值。
中文附件名变为 .bin 收件方附件变为 noname 或乱码 未按 RFC 2231 标准声明文件名。必须在 Content-Disposition 中使用 ("utf-8", "", filename) 三元组包装。
中途断网导致重复发送 网络波动中断导致无法确认已发名单 可在 Excel 中新增一列“发送状态”,每成功发送一行即回写“已发送”,二次执行时优先跳过已成功的记录。
安全提示与保密规范:
考评通知书与薪资绩效属于企业敏感数据。绝不能将带有 auth_code 授权码的脚本上传至任何公开代码仓库;在批量外发前,建议先将 Excel 内所有邮箱临时修改为测试邮箱,完成 1~3 条记录的全流程抽样联调,确认 Word 排版与邮件排版完全符合预期后,再切入正式生产名单。

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。