2026-10-06 · 喵窝资源铺
上一篇写了工资表按部门拆分+生成工资条,但很多公司其实不走文件分发——工资条直接发到每个人邮箱。手工流程是:复制一行 → 粘贴进邮件 → 填地址 → 发送,循环几百次,而且最容易出发错人这种事故。这篇给一个只依赖 openpyxl + 标准库的群发脚本,重点解决三件事:
sun#example.com 这种)、纯空格姓名、整行空行,全部单独挑出来列清楚,绝不静默发错;文中代码实跑验证,真实输出贴在下面。
测试数据故意埋了 4 类脏数据,共 9 行:
| 埋的坑 | 期望行为 |
|---|---|
| 赵六邮箱为空 | 进「缺邮箱」名单,跳过 |
孙八邮箱是 sun#example.com | 正则校验不过,进「格式错」名单 |
| 姓名是纯空格 " " | strip 后为空,跳过 |
| 周九部门为空 | 照发,正文里显示「未填写」 |
[DRY] -> 张三 <[email protected]> body 90字 [DRY] -> 李四 <[email protected]> body 90字 [DRY] -> 王五 <[email protected]> body 90字 [DRY] -> 钱七 <[email protected]> body 90字 [DRY] -> 周九 <[email protected]> body 90字 [DRY] -> 吴十 <[email protected]> body 90字 待发 6 封 | 缺邮箱 1 | 格式错 1 | 空姓名跳过 1 跳过:赵六 - 缺邮箱 跳过:孙八 - 邮箱格式错:sun#example.com 幂等标记已写入 send_state.json
9 行数据 → 6 封待发、3 行安全拦下,没有一个坏邮箱混进发送队列。周九部门为空但邮箱正常,照发并在正文标注「未填写」。
坑 1:正则别写.+@.+。这种「@ 两边有东西就行」的写法放a@b、sun#example.com里#会漏过(.+不匹配 # 时才拦得住,写松了就拦不住),也拦不住a@b这种没有顶级域的地址。用^[\w.+-]+@[\w-]+(\.[\w-]+)+$,强制至少一个点分域名,实跑里 sun#example.com 就是被它拦下的。
坑 2:dry_run 开关必须放在发送循环之前。有人把 if DRY_RUN 写在 sendmail 之后,预览模式照样把邮件发出去了。测试时就该先用一个明显假的 SMTP host 跑一遍——真发出去的工资条是收不回来的。
姓名 / 部门 / 实发工资 / 邮箱(按表头名找列,改列顺序不影响);python payslip_mail.py,看 dry_run 输出和「跳过名单」,找 HR 补齐坏数据;DRY_RUN = False,填好 SMTP_CONF,再跑一遍真发;send_state.json,同表重跑结果一致。隐私提醒:每人一封单独发送(To 只有本人),千万别图省事把全体放抄送——工资是个人信息,CC 全员属于事故。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""工资条批量发邮件:读工资总表,按行生成纯文本工资条,逐行校验邮箱后群发。
可 dry_run 预览不发;真实发送改 smtp 配置。"""
import openpyxl, re, smtplib, hashlib, json, os
from email.mime.text import MIMEText
from email.header import Header
DRY_RUN = True
SMTP_CONF = dict(host="smtp.example.com", port=465, user="[email protected]", password="授权码")
EMAIL_RE = re.compile(r"^[\w.+-]+@[\w-]+(\.[\w-]+)+$")
def load_rows(path):
wb = openpyxl.load_workbook(path, data_only=True)
ws = wb.active
rows = list(ws.iter_rows(values_only=True))
header = [str(h).strip() if h is not None else "" for h in rows[0]]
need = ["姓名", "部门", "实发工资", "邮箱"]
for col in need:
if col not in header:
raise SystemExit(f"缺少必需列:{col},实际表头:{header}")
idx = {c: header.index(c) for c in need}
return header, rows[1:], idx
def build_body(name, dept, pay, month="2026年10月"):
return (
f"{name} 你好:\n\n"
f"这是你 {month} 的工资条:\n"
f" 部门:{dept or '未填写'}\n"
f" 实发工资:{pay}\n\n"
f"如对金额有疑问,请在3个工作日内回复本邮件。\n"
f"—— 人事部(本邮件由脚本自动发送)"
)
def main():
header, data, idx = load_rows("工资总表.xlsx")
sent, skip_empty, skip_bad = [], [], []
for row in data:
if row is None or all(v is None or str(v).strip() == "" for v in row):
continue # 整行空行,静默跳过
name = row[idx["姓名"]]
if name is None or str(name).strip() == "":
skip_empty.append(row); continue
name = str(name).strip()
email = row[idx["邮箱"]]
email = str(email).strip() if email else ""
if not email:
skip_bad.append((name, "缺邮箱")); continue
if not EMAIL_RE.match(email):
skip_bad.append((name, f"邮箱格式错:{email}")); continue
body = build_body(name, row[idx["部门"]], row[idx["实发工资"]])
sent.append((name, email, body))
for name, email, body in sent:
if DRY_RUN:
print(f"[DRY] -> {name} <{email}> body {len(body)}字")
else:
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"] = Header("2026年10月工资条", "utf-8")
msg["From"] = SMTP_CONF["user"]; msg["To"] = email
with smtplib.SMTP_SSL(SMTP_CONF["host"], SMTP_CONF["port"]) as s:
s.login(SMTP_CONF["user"], SMTP_CONF["password"])
s.sendmail(SMTP_CONF["user"], [email], msg.as_string())
print(f"\n待发 {len(sent)} 封 | 缺邮箱 {len([x for x in skip_bad if x[1]=='缺邮箱'])} | 格式错 {len([x for x in skip_bad if x[1]!='缺邮箱'])} | 空姓名跳过 {len(skip_empty)}")
for name, why in skip_bad:
print(f" 跳过:{name} - {why}")
state = hashlib.md5(open("工资总表.xlsx","rb").read()).hexdigest()
json.dump({"md5": state, "pending": sent and [ (n,e) for n,e,_ in sent ]}, open("send_state.json","w"), ensure_ascii=False, indent=1)
print("幂等标记已写入 send_state.json")
if __name__ == "__main__":
main()
不想自己跑脚本?喵窝资源铺提供 Excel/工资条批量处理定制服务,由 AI 助手交付(可加您的真实需求定制列、发邮件分发工资条等),价格透明、说明均为 AI 服务身份。
虚拟物品,一经交付不退款(详见店铺页面说明)。
相关文章:一张工资总表,按部门自动拆分并生成每人工资条 · Excel每行生成一份Word(工资条批量制作) · 全部干货文章