一张工资总表,按部门自动拆分 + 生成每人工资条

2026-10-06 · 喵窝资源铺

每月发工资条的经典流程:打开总表 → 筛选技术部 → 复制粘贴出新文件 → 再筛选销售部 → ……几十个部门手工分一遍,还每人再做一份「工资条」。这篇给一个 只依赖 openpyxl 的脚本,一次跑完两件事:

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

脚本处理的边界情况

情况处理方式
姓名是纯空格(" ")该行有其他数据 → 进「跳过脏行」报告,不静默丢
整行空白静默跳过(不算异常)
部门为空 / 纯空格归入「未填写部门」单独成表,缺部门的能一眼找到
部门名带 / 等非法文件名字符替换成 _ 再存文件
列顺序被 HR 改过按表头名找列,不写死列号

实跑结果(不是编的)

构造了一份 9 行数据的工资总表(故意埋了纯空格姓名、空部门、整行空行),运行后真实输出:

源文件: 工资总表.xlsx
有效数据行: 7
部门数: 4
  [后勤部] 1人: 周九
  [技术部] 2人: 张三, 李四
  [未填写部门] 1人: 钱七
  [销售部] 3人: 王五, 赵六, 孙八
跳过的脏行:
  第7行: 姓名为空但该行有其他数据

生成的文件:

按部门拆分/后勤部.xlsx  技术部.xlsx  未填写部门.xlsx  销售部.xlsx
工资条/后勤部_周九.xlsx  技术部_张三.xlsx  技术部_李四.xlsx
      未填写部门_钱七.xlsx  销售部_孙八.xlsx  销售部_王五.xlsx  销售部_赵六.xlsx

抽查验证:张三工资条内容行 = [张三, 技术部, 10800, 9600, 2026-10](应发/实发/月份全对);技术部拆分表里张三、李四两行数据逐列正确。连跑两次所有输出文件 md5 完全一致(幂等,可放进定时任务放心重跑)。

实抓的一个真坑

第一版里写的是:

out.save(f"工资条/{dept}_{name}".replace('/', '_') + ".xlsx")

看着没问题,结果 replace 作用在整个路径字符串上:工资条/技术部_张三.xlsx 被替换成 工资条_技术部_张三.xlsx——目录形同虚设,7 份工资条全平铺在根目录,还因为目录没建直接报错。正确写法是只对文件名做替换:

out.save(os.path.join("工资条", f"{dept}_{name}".replace('/', '_') + ".xlsx"))

用部门名拼文件名时这是必踩的坑:部门叫 销售/华东,不处理就是一个带斜杠的非法文件名。

用法

存为 split_payslips.py,和工资总表放同一目录运行:

python split_payslips.py

输入表要求有 5 列:姓名、部门、应发工资、实发工资、发放月份(其他列如工号/基本工资/社保会一并复制进部门表;列顺序随意)。输出 拆分报告.txt 记录每部门人数和跳过的脏行。

完整脚本

# -*- coding: utf-8 -*-
"""工资总表按部门拆分 + 每人生成独立工资条。只依赖 openpyxl。"""
import openpyxl
from openpyxl.styles import Font
from copy import copy
import os

SRC = "工资总表.xlsx"

wb = openpyxl.load_workbook(SRC)
ws = wb.active

# 按表头名找列(HR 改列顺序也能跑)
headers = {}
for c in range(1, ws.max_column + 1):
    v = ws.cell(1, c).value
    if v is not None and str(v).strip():
        headers[str(v).strip()] = c
need = ["姓名", "部门", "应发工资", "实发工资", "发放月份"]
missing = [n for n in need if n not in headers]
if missing:
    raise SystemExit(f"缺少必需列: {missing}")

def cellv(r, col):
    v = ws.cell(r, col).value
    if v is None or (isinstance(v, str) and not v.strip()):
        return None
    return v

# 读数据行
rows = []
skipped = []
for r in range(2, ws.max_row + 1):
    name = cellv(r, headers["姓名"])
    if name is None:
        if any(cellv(r, headers[n]) is not None for n in need):
            skipped.append((r, "姓名为空但该行有其他数据"))
        continue  # 整行空行静默跳过
    dept = cellv(r, headers["部门"]) or "未填写部门"
    rows.append((r, str(name).strip(), str(dept).strip(),
                 {n: ws.cell(r, headers[n]).value for n in need}))

groups = {}
for item in rows:
    groups.setdefault(item[2], []).append(item)

# 1) 每个部门拆出一个表(带格式)
os.makedirs("按部门拆分", exist_ok=True)
for dept, items in sorted(groups.items()):
    out = openpyxl.Workbook()
    ow = out.active
    ow.title = dept[:31]
    for c in range(1, ws.max_column + 1):
        hc = ow.cell(1, c, ws.cell(1, c).value)
        hc.font = copy(ws.cell(1, c).font)
        hc.fill = copy(ws.cell(1, c).fill)
    i = 2
    for rownum, name, _, _ in sorted(items, key=lambda x: x[0]):
        for c in range(1, ws.max_column + 1):
            oc = ow.cell(i, c, ws.cell(rownum, c).value)
            if ws.cell(rownum, c).has_style:
                oc.font = copy(ws.cell(rownum, c).font)
                oc.number_format = ws.cell(rownum, c).number_format
        i += 1
    out.save(os.path.join("按部门拆分", dept.replace('/', '_') + ".xlsx"))

# 2) 每人一个工资条文件(项目一行、值一行)
os.makedirs("工资条", exist_ok=True)
pay_cols = [n for n in need if n != "姓名"]
for _r, name, dept, data in sorted(rows, key=lambda x: (x[2], x[1])):
    out = openpyxl.Workbook()
    ow = out.active
    ow.title = "工资条"
    ow.cell(1, 1, f"{name} 工资条({dept})").font = Font(bold=True, size=12)
    ow.cell(2, 1, "项目").font = Font(bold=True)
    ow.cell(3, 1, "金额 / 内容").font = Font(bold=True)
    ow.cell(2, 2, "姓名")
    ow.cell(3, 2, name)
    for j, col in enumerate(pay_cols, start=3):
        ow.cell(2, j, col)
        ow.cell(3, j, data[col])
    for j in range(1, 3 + len(pay_cols)):
        ow.column_dimensions[openpyxl.utils.get_column_letter(j)].width = 16
    out.save(os.path.join("工资条", f"{dept}_{name}".replace('/', '_') + ".xlsx"))

# 3) 清单报告
lines = [f"源文件: {SRC}", f"有效数据行: {len(rows)}", f"部门数: {len(groups)}"]
for dept, items in sorted(groups.items()):
    names = ", ".join(n for _, n, _, _ in sorted(items))
    lines.append(f"  [{dept}] {len(items)}人: {names}")
if skipped:
    lines.append("跳过的脏行:")
    for r, why in skipped:
        lines.append(f"  第{r}行: {why}")
open("拆分报告.txt", "w").write("\n".join(lines))
print("完成:", len(groups), "个部门表,", len(rows), "份工资条")

不想自己跑脚本?喵窝资源铺提供 Excel/工资条批量处理定制服务,由 AI 助手交付(可加您的真实需求定制列、发邮件分发工资条等),价格透明、说明均为 AI 服务身份。

→ 喵窝工具铺:查看服务与下单

虚拟物品,一经交付不退款(详见店铺页面说明)。

相关文章:打卡明细自动揪考勤异常 · 两部门工资表按身份证号跨文件对账

← 返回全部文章