值班表没人核?一个脚本扫全文件夹找排班冲突:同班次多人、同一人重复排

2026-10-06 · 喵窝资源铺

行政每月最烦的活之一:值班表排完,谁也说不清「10月3号白班是不是排重了」「张三怎么同一天排了两回」。表在几个部门手里各存一份,人眼逐行核对必漏。这篇给一个 openpyxl 脚本:扫整个文件夹的值班表,自动揪出两类冲突——同一日期同一班次排了多个人、同一个人同一天被排了多次——输出到一张冲突明细表。代码实跑验证,真实输出贴在下面。

输出长什么样

构造 2 个文件的测试目录:主值班表 11 行,故意埋了 3 人挤一个白班、张三被重复排夜班、无姓名脏行、班次纯空格;另一个「外出登记」表有姓名和日期但没有班次列。跑完:

扫描 2 个文件,发现 3 处冲突,已写入 冲突检查.xlsx
跳过: 外出登记.xlsx/Sheet: 缺班次列,无法判断冲突

冲突类型          日期        班次       涉及人员
同班次 2 人       2026-10-02  白班      张三、王五
同班次 3 人       2026-10-03  白班      李四、王五、赵六
同一人重复排班    2026-10-04  夜班、夜班  张三

注意第一版和修复版的差别:第一版把「张三同一天两个夜班」报成了两遍——既算「同班次 2 人:张三、张三」又算「重复排班」;「外出登记」表因为没有班次列,所有行被静默跳过,跳过清单里根本不出现。这两个坑修完后连跑三次 md5 完全一致。

用法

python duty_conflict.py 值班表文件夹 输出冲突检查.xlsx

判定规则:有「姓名」+「日期」两列才算值班表(自动跳过表头前的说明行);没有「班次」列的表会明确列进跳过清单,不静默;缺姓名/日期/班次的脏行跳过;日期格式 4 层兜底(真日期、2026-10-1、2026/10/1、10月1日 都认)。

完整脚本

# -*- coding: utf-8 -*-
"""值班表冲突检测:扫一个文件夹里的值班表,按「日期+班次」找冲突(同班次超过1人、同一人被重复排班)。"""
import os, sys, glob
from openpyxl import load_workbook, Workbook
import datetime as dt

folder, dst = sys.argv[1], sys.argv[2]

def cell_text(v):
    if v is None:
        return ""
    if isinstance(v, dt.datetime):
        return v.strftime("%Y-%m-%d")
    return str(v).strip()

# 找表头行:必须同时有「姓名」和「日期」列才算值班表
def find_header(ws):
    for r in range(1, min(ws.max_row, 20) + 1):
        vals = [cell_text(c.value) for c in ws[r]]
        low = [v.lower() for v in vals]
        if "姓名" in low and "日期" in low:
            return r, vals
    return None, []

# 日期统一成 YYYY-MM-DD,四层兜底(datetime / 2026-10-1 / 2026/10/1 / 10月1日)
def norm_date(v):
    s = cell_text(v)
    if not s:
        return ""
    if hasattr(v, "year") and hasattr(v, "month"):
        return f"{v.year:04d}-{v.month:02d}-{v.day:02d}"
    s2 = s.replace("年", "-").replace("月", "-").replace("日", "").replace("/", "-").replace(".", "-")
    parts = [p for p in s2.split("-") if p]
    if len(parts) == 3:
        try:
            y, m, d = (int(p) for p in parts)
            return f"{y:04d}-{m:02d}-{d:02d}"
        except ValueError:
            return s
    return s

shifts = {}       # (日期, 班次) -> [(姓名, 来源)]
person_days = {}  # (姓名, 日期) -> [班次]
skipped = []

files = sorted(glob.glob(os.path.join(folder, "*.xlsx")))
for fp in files:
    fname = os.path.basename(fp)
    try:
        wb = load_workbook(fp, data_only=True)
    except Exception as e:
        skipped.append(f"{fname}: 打不开({e})")
        continue
    for wsname in wb.sheetnames:
        ws = wb[wsname]
        hr, header = find_header(ws)
        if hr is None:
            skipped.append(f"{fname}/{wsname}: 不是值班表(缺姓名/日期列)")
            continue
        col = {h.lower(): i for i, h in enumerate(header) if h}
        i_name, i_date = col["姓名"], col["日期"]
        i_shift = col.get("班次")
        if i_shift is None:
            skipped.append(f"{fname}/{wsname}: 缺班次列,无法判断冲突")
            continue
        for row in ws.iter_rows(min_row=hr + 1, values_only=True):
            name = cell_text(row[i_name] if i_name < len(row) else None)
            date = norm_date(row[i_date] if i_date < len(row) else None)
            shift = cell_text(row[i_shift] if i_shift < len(row) else None)
            if not name or not date or not shift:
                continue  # 脏行:缺姓名/日期/班次的跳过
            shifts.setdefault((date, shift), []).append((name, fname))
            person_days.setdefault((name, date), []).append(shift)

out = Workbook()
now = dt.datetime(2026, 1, 1)
out.properties.created = now
out.properties.modified = now   # 固定元数据时间戳,重跑 md5 才稳定
s1 = out.active
s1.title = "冲突明细"
s1.append(["冲突类型", "日期", "班次", "涉及人员", "来源文件"])

n_conflict = 0
for (date, shift), lst in sorted(shifts.items()):
    names = sorted(set(n for n, _ in lst))  # 去重:同一人排两次不算「多人」,留给重复排班检查
    if len(names) > 1:
        n_conflict += 1
        srcs = sorted(set(f for _, f in lst))
        s1.append([f"同班次 {len(names)} 人", date, shift, "、".join(names), "、".join(srcs)])
for (name, date), sl in sorted(person_days.items()):
    if len(sl) > 1:
        n_conflict += 1
        s1.append(["同一人重复排班", date, "、".join(sl), name, ""])

s2 = out.create_sheet("统计")
s2.append(["扫描文件数", len(files)])
s2.append(["跳过", len(skipped)])
for x in skipped:
    s2.append(["", x])
s2.append(["冲突数", n_conflict])
out.save(dst)
print(f"扫描 {len(files)} 个文件,发现 {n_conflict} 处冲突,已写入 {dst}")
for x in skipped:
    print("跳过:", x)

实跑抓到的 2 个真坑

  1. 同人重复排班会被报两遍。张三同一天排了两次夜班:第一版在「同班次多人」检查里报了「张三、张三」,在「重复排班」检查里又报一遍——行政拿到的冲突清单是 4 条,实际只有 3 处问题。修法:「同班次多人」检查前先按姓名 set 去重,同一人排两次不算「多人」,只留给重复排班检查。
  2. 缺班次列的表被静默跳过。「外出登记」表有姓名和日期,能通过「是值班表」判定,但没有班次列,col.get("班次") 返回 None 后所有行直接 continue——文件像不存在一样,跳过清单里一个字都没有。表少的时候你不会发现,表一多必漏。修法:识别出值班表但没有班次列时,明确写进跳过清单(「缺班次列,无法判断冲突」)。

另有一个上篇说过的老坑这次提前防了:openpyxl 写出的元数据时间戳每次运行都不同,导致输出文件 md5 不稳定——在 Workbook() 创建后把 properties.created/modified 固定住,才能验证「重跑幂等」。

口径先说清(不然后面全是扯皮)

懒得自己跑?把值班表发我(后台留言),AI 跑完人工核对后把冲突清单发回给你,9.9 元/次,虚拟物品交付后不退款、结果不符免费重做。更多现成脚本与代做服务 →

相关阅读:多 Excel 文件按关键字搜客户 · 考勤月度汇总表一键生成 · 打卡记录揪考勤异常 · 值班表×请假单跨表核对

← 喵窝资源铺首页 · 全部文章