考勤 Excel 和排班表对不上?Python 交叉核对揪出迟到/缺卡/旷工

2026-10-06 · 喵窝资源铺

场景:月底算工资,HR 拿着打卡机导出的流水(几百行「姓名,日期,时间」)和排班表,一行行人工对——谁迟到了、谁缺了下班卡、谁压根没来。这篇给一个纯标准库的 Python 脚本(kaoqin_check.py):输入排班表(姓名,日期,班次)+ 打卡流水两个 CSV,交叉核对后输出异常清单 CSV(迟到/早退/缺卡/旷工,宽限 5 分钟可调)。文中代码实跑验证,真实输出贴在下面。

实跑:7 条排班 → 5 条异常全揪出

造了 7 条排班、11 条打卡,覆盖正常/迟到/早退/缺卡/旷工/跨天夜班六种情况:

姓名,日期,班次,应上班,应下班,实际上班,实际下卡,结论,备注
张伟,2026-10-01,早班,08:00,16:00,07:55,16:02,正常,
张伟,2026-10-02,早班,08:00,16:00,08:18,16:00,迟到18分钟,
李娜,2026-10-01,中班,12:00,20:00,11:52,20:05,正常,
李娜,2026-10-02,晚班,16:00,24:00,15:58,00:10(次日),正常,
王强,2026-10-01,早班,08:00,16:00,07:58,,缺下班卡,
王强,2026-10-02,早班,08:00,16:00,,,,旷工,无任何打卡记录
刘洋,2026-10-01,中班,12:00,20:00,12:00,19:30,早退30分钟,

连跑两次输出 md5 一致(幂等)。运行日志:核对 7 条排班,输出 5 条异常。

坑一(最阴险):跨天夜班的 24:00,跨天判断静默失效

晚班 16:00-24:00,李娜次日 00:10 打下班卡,第一版把她判成「缺下班卡」——明明卡就在那里。

排查发现:把 24:00 统一转成 1440 分钟后,e_m <= s_m(下班≤上班即跨天)这个判断永远不成立了,1440 > 960。于是找下班卡时只在当天找,次日的 00:10 根本不在搜索范围里。正解是显式补上:

cross_day = e_m <= s_m or e == '24:00'

教训:「把特殊值归一化」之后,原来依赖特殊值的判断会静默失效。归一化和判断逻辑必须一起改,而且要用一条含跨天夜班的用例当验收测试——不含这个 case 的话这 bug 永远发现不了,直到某个月底工资算错。

坑二:下班卡的搜索窗口,寧大勿小

「00:10 下班卡算不算 16:00-24:00 班次的下班卡」——如果把搜索条件写成「时间在班次时段附近」,跨天后的 00:10 换算成 1450 分钟,必须先把「次日的卡 +24h 偏移」再比较。

正解:找下班卡时遍历当天+(跨天时)次日两天的打卡,次日的卡统一加 1440 分钟偏移后与 e_m 比较,窗口放宽到 e_m - 120 以后的第一张卡。同理上班卡窗口是 [s_m-120, s_m+180]——有人提前两小时到岗先打个卡很正常,窗口太窄会把正常卡漏掉造成误报「缺上班卡」。

坑三:宽限时间不写进逻辑,工资就算不平

打卡机 08:00:03 打卡算不算迟到?各公司规矩不同。脚本把 GRACE = 5(分钟)做成常量:迟到/早退判定都要超过宽限值才记异常,否则每天几十条 1-2 分钟的「迟到」会把异常表刷爆,HR 反而不看了。宁可少报几条真实边缘 case,也要保证报出来的每条都值得处理——和上篇聊天记录提取的「宁漏不误」是同一条设计哲学。

核心代码(纯标准库,~60 行)

#!/usr/bin/env python3
"""考勤交叉核对:排班表(姓名,日期,班次) × 打卡记录(姓名,日期,时间) → 异常清单CSV"""
import csv, datetime

SHIFTS = {'早班': ('08:00','16:00'), '中班': ('12:00','20:00'), '晚班': ('16:00','24:00')}
GRACE = 5

def to_min(t):
    h, m = map(int, t.split(':'))
    return h*60 + m

def check(rows, punches):
    by_punch = {}
    for n, d, t in punches:
        by_punch.setdefault((n, d), []).append(to_min(t))
    out = []
    for name, date, shift in rows:
        s, e = SHIFTS[shift]
        s_m, e_m = to_min(s), 24*60 if e == '24:00' else to_min(e)
        cross_day = e_m <= s_m or e == '24:00'
        next_day = (datetime.date.fromisoformat(date) + datetime.timedelta(days=1)).isoformat()
        days = [date, next_day] if cross_day else [date]
        tin = tout = None
        for p in sorted(by_punch.get((name, date), [])):
            if s_m - 120 <= p <= s_m + 180 and tin is None:
                tin = p
        for dd in days:
            off = 0 if dd == date else 24*60
            for p in sorted(by_punch.get((name, dd), [])):
                if p + off >= e_m - 120 and tout is None:
                    tout = (dd, p)
        if tin is None and tout is None:
            out.append([name, date, shift, s, e, '', '', '旷工', '无任何打卡记录'])
            continue
        status = []
        if tin is None:
            status.append('缺上班卡')
        elif tin - s_m > GRACE:
            status.append(f'迟到{tin-s_m}分钟')
        if tout is None:
            status.append('缺下班卡')
        elif (tout[1] + (0 if tout[0] == date else 24*60)) < e_m - GRACE:
            status.append(f'早退{e_m-(tout[1]+(0 if tout[0]==date else 24*60))}分钟')
        tin_s = f'{tin//60:02d}:{tin%60:02d}' if tin is not None else ''
        tout_s = (f'{tout[1]//60:02d}:{tout[1]%60:02d}' + ('(次日)' if tout[0] != date else '')) if tout else ''
        out.append([name, date, shift, s, e, tin_s, tout_s, ';'.join(status) or '正常', ''])
    return out

if __name__ == '__main__':
    rows = list(csv.reader(open('kaoqin_shifts.csv', encoding='utf-8')))[1:]
    punches = list(csv.reader(open('kaoqin_punches.csv', encoding='utf-8')))[1:]
    result = check(rows, punches)
    with open('kaoqin_check_out.csv', 'w', newline='', encoding='utf-8') as f:
        w = csv.writer(f)
        w.writerow(['姓名','日期','班次','应上班','应下班','实际上班','实际下卡','结论','备注'])
        w.writerows(result)
    print(f'核对 {len(rows)} 条排班,输出 {sum(1 for r in result if r[7] != "正常")} 条异常')

喵窝资源铺提供代跑脚本服务:把排班表和打卡流水两个 CSV 发过来,跑完把异常清单(CSV)发回你,¥5 起,虚拟物品不退款(跑不通不收费)。班次时间不同可说明,脚本按你的班次定制。脚本本身免费自取。

📖 相邻文章:聊天记录批量提取待办+承诺日期 · 考勤时间列清洗成工时 · 两张表人名模糊匹配合并 · 博客目录

异常清单仅供参考,追责/扣薪请以原始打卡数据为准(输出保留实际上下班时间便于核对)。