Python办公自动化实战:批量处理Excel、Word与自动发送邮件
针对日常办公中频繁遇到的数据提取、文档套打与定制发信场景,本文通过完整的企业考评通知单案例,详解如何使用Python协同openpyxl、python-docx与smtplib实现批量化处理与自动分发。
openpyxl 批量读取与清洗表格数据、使用 python-docx 精确映射占位符批量生成定制 Word 文档,最后通过 Python 标准库 smtplib 与 email 自动完成安全加密发信与附件分发。
一、实战需求与技术链路拆解
为了让整套自动化流程贴合真实办公场景,我们设定一个具体的业务目标:人事部门有一份名为 staff_eval.xlsx 的员工考核表,里面包含员工姓名、部门、考评得分、绩效奖金、考评评语以及个人公司邮箱。
人工处理该业务的痛点在于:若部门有 100 名员工,工作人员必须打开 Word 模板复制粘贴 100 次、保存为 100 个文件,并手动撰写 100 封邮件且逐个上传附件,整个过程繁琐且极易混淆名单或发错附件。通过 Python 脚本,我们可以将该流程抽象为四个标准化阶段:
- 输入阶段:使用
openpyxl打开 Excel 工作簿,提取每一行考评数据,并完成空值校验与邮箱格式合法性过滤。 - 处理阶段:读取统一的
template.docx模板文件,利用占位符替换机制,将字段填入指定的段落与表格中,另存为个性化命名的通知单文档。 - 输出阶段:利用
email.mime组装包含 HTML 正文与 Word 附件的 MIME 复合邮件对象。 - 分发阶段:通过
smtplib.SMTP_SSL连接企业邮件服务器,完成安全鉴权并按频率限制逐封发送,记录成功与失败日志。
二、运行环境与工作目录规范
在开始编写代码前,我们需要安装用于处理 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-docx 占位符断裂的核心难点
许多初学者在直接调用 paragraph.text.replace(...) 时会遇到两个严重问题:
- 直接对
paragraph.text进行赋值会破坏段落内既有的字体、字号、颜色和加粗等格式。 - 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 标准库自带完整的网络与邮件协议支持,无需安装第三方客户端即可实现端到端自动化。
邮件协议核心概念与鉴权要求
在实际编写发信代码前,有以下三项前置条件需要明确:
- 必须使用授权码而非登录密码:主流邮箱服务商(如 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()
七、生产运行与异常防御检查
将脚本投入正式生产运行前,必须建立健全的防御策略,防止因为网络中断、脏数据或邮箱服务商策略导致批量任务中途夭折。
高频疑难问题与解决策略
| 异常场景 | 典型错误提示 | 产生根源与解决方案 |
|---|---|---|
| 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 排版与邮件排版完全符合预期后,再切入正式生产名单。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













