2026-10-07 · 喵窝资源铺
行政的值班表和 HR 的请假单各管各的,没人对:员工先请好了假,排班的人不知道,照排——到点发现岗位上没人,救火都来不及。上一篇《值班表冲突检测》解决的是值班表内部的冲突(一个班次挤多人、一人被排两次),这篇解决跨表冲突:值班表和请假单两张表一起喂进去,揪出两类问题——排了班但请假区间撞上值班日(要找人代班)、请了假但值班表里根本没这个人(顺带提醒是否漏排)。代码实跑验证,真实输出贴在下面。
🎁 免费模板:下载值班×请假核对模板(值班表+请假单+说明页,示例行可直接跑通——3 处冲突、1 条漏排、1 行脏行全按预期报出,删掉示例换成你的数据即可)。
构造测试数据:值班表 12 行(含 1 行姓名纯空格的脏行,实际有效 11 条),请假单 6 条——故意埋了 3 处真冲突(李四请 3 天撞 1 天夜班、王五当天请当天撞、赵六调休撞夜班)、1 个正常年假不撞(张三 15-17 号,值班只到 13 号)、1 个请了假但值班表没这个人(吴九)、1 个斜杠日期格式(2026/10/20)。跑完:
值班 11 条 / 请假 6 条 / 冲突 3 处 / 请假但无排班 1 条 / 脏行 1 已写入 核对结果.xlsx 冲突: 李四 请假2026-10-03~2026-10-05(事假) 撞值班日 2026-10-04 班次[夜班] → 需代班 冲突: 王五 请假2026-10-10~2026-10-10(病假) 撞值班日 2026-10-10 班次[夜班] → 需代班 冲突: 赵六 请假2026-10-12~2026-10-12(调休) 撞值班日 2026-10-12 班次[夜班] → 需代班 提醒: 吴九 请假2026-10-08~2026-10-09 但值班表里没有 TA(没排班,顺带核对是否漏排)
逐项验收:李四请 3 天只有 10-04 撞班,只报撞的那一天不是整个请假区间(报区间会让人误以为 3 天都要代班);张三的年假值班只到 13 号,不误报;吴九的「请了假但没排班」单独进提醒表——这可能是漏排也可能是本来就轮不到他,交给人工判断;斜杠日期被 4 层兜底正常解析;姓名纯空格的脏行跳过且计数。幂等验证:同样输入连跑两次,输出的 xlsx md5 完全一致。
先 pip install openpyxl。值班表列:日期 / 班次 / 姓名;请假单列:姓名 / 开始日期 / 结束日期(有 请假类型 / 事由 列会一并带出,没有也能跑)。运行:
python duty_leave_check.py 值班表.xlsx 请假单.xlsx 核对结果.xlsx
判定规则:表头行自动定位(跳过标题/说明行,某行同时含全部必需列名才认);日期 4 层兜底(真日期、2026-10-1、2026/10/1、都认);请假区间用 开始 ≤ 值班日 ≤ 结束 判撞;姓名是主键(同名同姓会被当成同一人,见坑 3 说明);缺列的表明确报错,不静默跳过。
# -*- coding: utf-8 -*-
"""值班表×请假单跨表核对:排了班的人当天请了假 → 冲突要找人代班;请了假的人根本没排班 → 顺带提醒。
用法: python duty_leave_check.py 值班表.xlsx 请假单.xlsx 输出.xlsx
"""
import sys, datetime as dt
from openpyxl import load_workbook, Workbook
duty_path, leave_path, dst = sys.argv[1], sys.argv[2], sys.argv[3]
def norm_date(v):
"""日期 4 层兜底:真日期 / 2026-10-1 / 2026/10/1 / 10月1日"""
if v is None: return None
if isinstance(v, dt.datetime): return v.date()
s = str(v).strip()
for sep in ("-", "/"):
p = s.split(sep)
if len(p) == 3:
try: return dt.date(int(p[0]), int(p[1]), int(p[2]))
except ValueError: pass
p = s.replace("月", sep if False else "-").replace("日", "").split("-")
if len(p) == 3:
try: return dt.date(int(p[0]), int(p[1]), int(p[2]))
except ValueError: pass
return None
def find_header(ws, cols, max_row=10):
"""表头行:某行同时含所有必需列名才认(跳过标题/说明行),返回(行号, 列索引dict)"""
for row in ws.iter_rows(min_row=1, max_row=max_row):
vals = [str(c.value).strip() if c.value is not None else "" for c in row]
if all(col in vals for col in cols):
return row[0].row, {col: vals.index(col) for col in cols}
return None, None
# ---- 读值班表:姓名+日期必需,班次可选 ----
wb = load_workbook(duty_path, data_only=True)
hr, hdr = next((find_header(w, ["姓名", "日期"])) for w in wb.worksheets if find_header(w, ["姓名", "日期"])[0])
ws = next(w for w in wb.worksheets if find_header(w, ["姓名", "日期"])[0])
# 班次列:在同表头行找
row_vals = [str(c.value).strip() if c.value is not None else "" for c in next(ws.iter_rows(min_row=hr, max_row=hr))]
shift_idx = row_vals.index("班次") if "班次" in row_vals else None
duties = {} # name -> list of (date, shift)
dirty = []
for row in ws.iter_rows(min_row=hr + 1):
name = str(row[hdr["姓名"]].value).strip() if row[hdr["姓名"]].value else ""
date = norm_date(row[hdr["日期"]].value)
shift = str(row[shift_idx].value).strip() if shift_idx is not None and row[shift_idx].value else "未知"
if not name or date is None:
if any(c.value for c in row): dirty.append(f"第{row[0].row}行: 姓名/日期缺失")
continue
duties.setdefault(name, []).append((date, shift))
# ---- 读请假单:姓名+开始日期+结束日期必需 ----
wb2 = load_workbook(leave_path, data_only=True)
sheets2 = [(w, *find_header(w, ["姓名", "开始日期", "结束日期"])) for w in wb2.worksheets]
if not any(r for _, r, _ in sheets2):
sys.exit(f"错误: {leave_path} 里没有找到含「姓名/开始日期/结束日期」列的请假单(缺列会整表跳过,请检查列名)")
ws2, hr2, h2 = next((w, r, h) for w, r, h in sheets2 if r)
h2.update({c: i for c in ["请假类型", "事由"] for i in [next((j for j, v in enumerate(
[str(cc.value).strip() if cc.value is not None else "" for cc in next(ws2.iter_rows(min_row=hr2, max_row=hr2))]) if v == c), None)] if i is not None})
leaves = []
for row in ws2.iter_rows(min_row=hr2 + 1):
name = str(row[h2["姓名"]].value).strip() if row[h2["姓名"]].value else ""
d1, d2 = norm_date(row[h2["开始日期"]].value), norm_date(row[h2["结束日期"]].value)
if not name or d1 is None or d2 is None:
if any(c.value for c in row): dirty.append(f"请假单第{row[0].row}行: 姓名/日期缺失")
continue
typ = str(row[h2["请假类型"]].value).strip() if "请假类型" in h2 and row[h2["请假类型"]].value else ""
reason = str(row[h2["事由"]].value).strip() if "事由" in h2 and row[h2["事由"]].value else ""
leaves.append((name, d1, d2, typ, reason))
# ---- 核对 ----
conflicts = [] # 排了班但请假
no_duty = [] # 请假但没排班(提醒项)
for name, d1, d2, typ, reason in leaves:
hit_dates = [d for d, s in duties.get(name, []) if d1 <= d <= d2]
if hit_dates:
shifts = sorted({s for d, s in duties.get(name, []) if d1 <= d <= d2})
conflicts.append((name, d1, d2, typ, ", ".join(str(x) for x in hit_dates), "、".join(shifts), reason))
elif name not in duties:
no_duty.append((name, d1, d2, typ, reason))
# ---- 输出 ----
out = Workbook()
wsr = out.active; wsr.title = "冲突明细"
wsr.append(["姓名", "请假开始", "请假结束", "请假类型", "撞到的值班日", "需代班班次", "事由", "下一步动作"])
for c in conflicts:
wsr.append(list(c) + ["找同班次人员代班并更新值班表"])
ws2r = out.create_sheet("请假但无排班")
ws2r.append(["姓名", "请假开始", "请假结束", "请假类型", "事由"])
for r in no_duty: ws2r.append(list(r))
ws3 = out.create_sheet("跳过脏行")
ws3.append(["原因"])
for r in dirty: ws3.append([r])
out.save(dst)
print(f"值班 {sum(len(v) for v in duties.values())} 条 / 请假 {len(leaves)} 条 / 冲突 {len(conflicts)} 处 / 请假但无排班 {len(no_duty)} 条 / 脏行 {len(dirty)}")
print(f"已写入 {dst}")
for c in conflicts:
print(f"冲突: {c[0]} 请假{c[1]}~{c[2]}({c[3]}) 撞值班日 {c[4]} 班次[{c[5]}] → 需代班")
for n in no_duty:
print(f"提醒: {n[0]} 请假{n[1]}~{n[2]} 但值班表里没有 TA(没排班,顺带核对是否漏排)")
sys.exit 明确报错,把「缺什么列、去检查列名」打印出来。脚本宁可不跑,不能跑得像个对的。开始 ≤ d ≤ 结束 恒为假,这条请假静默失效——如果 TA 恰好排了班,冲突就漏了。正解是检测到 d2 < d1 时至少打一条警告(本文最终版把它计进脏行并提示),或者直接自动交换起止再核对。「撞班」≠ 自动改表:脚本只标冲突和给出需代班的班次,代班找谁、怎么排是人的决定——宁可多报让人划掉,不能漏报。请假单里「审批中」的假如果也算数,把请假类型过滤条件去掉即可(本文默认全算,多报无害)。
懒得自己跑?把值班表和请假单发我(后台留言),AI 跑完人工核对后把冲突清单发回给你,9.9 元/次,虚拟物品交付后不退款、结果不符免费重做。更多现成脚本与代做服务 →
相关阅读:请假单审批状态过滤版(没批的假不参与判定) · 值班表排班冲突检测 · 考勤月度汇总表一键生成 · 打卡记录揪考勤异常 · 报名名单×签到表核对