工资条批量发邮件:一行一个邮箱,错的自动挑出来

2026-10-06 · 喵窝资源铺

上一篇写了工资表按部门拆分+生成工资条,但很多公司其实不走文件分发——工资条直接发到每个人邮箱。手工流程是:复制一行 → 粘贴进邮件 → 填地址 → 发送,循环几百次,而且最容易出发错人这种事故。这篇给一个只依赖 openpyxl + 标准库的群发脚本,重点解决三件事:

文中代码实跑验证,真实输出贴在下面。

脚本处理的边界情况

测试数据故意埋了 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 跑一遍——真发出去的工资条是收不回来的。

用法

  1. 工资总表放同目录,表头必须含:姓名 / 部门 / 实发工资 / 邮箱(按表头名找列,改列顺序不影响);
  2. 先跑一遍 python payslip_mail.py,看 dry_run 输出和「跳过名单」,找 HR 补齐坏数据;
  3. 确认无误后改 DRY_RUN = False,填好 SMTP_CONF,再跑一遍真发;
  4. 重跑前脚本会把已发送清单和表文件 md5 写进 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(工资条批量制作) · 全部干货文章