ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python+pandas考勤工时自动化:从打卡流水到月度报表的实现

2026/10/2 3:59:56 拓冰建站 浏览量
Python+pandas考勤工时自动化:从打卡流水到月度报表的实现 每月月底那几天我都在跟考勤表较劲。几十号人的上下班打卡时间一个个复制到Excel里算工时眼睛都快看瞎了稍不留神还会算错。后来我直接用Python写了一个月报考勤工时计算脚本把这件事彻底交给代码现在每次跑完只要三分钟而且不会再出现人为纰漏。今天就把这套思路完整写出来从需求拆解到核心代码实现再到各种边界场景和踩坑实录希望能给同样被考勤表折磨的兄弟们一点参考。这套方案本质上是用pandas做数据清洗和聚合计算把考勤机导出的原始打卡流水读进来按人员和日期归并成上下班记录再结合班次规则算出正常工时、加班工时、迟到早退天数最后生成一张可以直接发给领导的月度汇总表。整个过程全部自动化原始数据一变结果跟着刷新完全不用重新整理Excel。对于正在学Python的朋友来说这也是一个非常好的练手项目它涵盖了pandas最常用的几个操作——数据读取、列清洗、groupby聚合、时间计算、多Sheet写入Excel难度适中做完会非常有成就感。人力资源、行政岗位和需要自己统计工时的部门负责人都可以直接拿这套代码改一改落地使用。1. 项目究竟在解决什么痛点需求拆解与计算规则设计1.1 考勤原始数据长什么样在我接手之前月报全靠手工做。考勤机导出的Excel表格长这样一个Sheet里存着全员的打卡流水每一行是一条记录核心字段大概有工号、姓名、打卡日期、打卡时间、打卡设备编号。麻烦的是每个人每天会打很多次卡——早上来一次中午下楼吃饭回来一次下午出去办事回来一次晚上下班一次有时候晚上加班又打一次。同一个人的同一天在表里可能躺着五六行。这种长表格式对人的眼睛极不友好但对pandas来说反而是最舒服的数据结构。因为它天然适合“按人按日期分组后再聚合”的操作。我们要做的第一步就是把这些散乱的流水记录整理成“每人每天一条上下班记录”。这一步如果靠手工做不但要反复筛选、肉眼找最早和最晚时间还很容易把中午外出办事的打卡当成上下班边界算出来的工时就会失真。1.2 工时计算规则从“打卡时间”到“有效工时”做系统之前必须先把计算规则订死。我调研过不少公司工时计算的主流口径可以分为三层标准工时从上班打卡到下班打卡的总时长减去午休时间。比如9:00上班、18:00下班、午休1小时标准工时就是8小时。实际工时基于真实的上下班打卡计算同样扣午休。比如你9:30才到但19:30才走实际工时是19:30-9:30-1小时9小时。加班工时实际工时减去标准工时的差额通常只算正差额。但很多公司要求超过某个阈值才算加班比如满30分钟才计一次不满就四舍五入。另外迟到和早退不能光靠“看时间”判断要跟每人对应的班次对比。比如该9点上班9:05打卡算不算迟到有的公司有5分钟宽限期有的公司严格按点。这种差异不能写死在代码里应该做成可配置项——这也是我一开始就踩过的坑规则没定清楚代码改来改去。我最终定的规则表长这样场景计算方式备注正常上下班下班时间 - 上班时间 - 午休时长结果为当日实际工时跨天班次下班时间小于上班时间时下班时间加一天再计算晚班、夜班场景缺卡缺上班卡或下班卡按异常记录输出不自动补时间留待人工确认加班实际工时 标准工时取差值阈值和取整规则可配置迟到上班时间 班次上班时间 宽限分钟允许公司自定义宽限早退下班时间 班次下班时间 - 宽限分钟允许公司自定义宽限1.3 为什么用Python而不是Excel公式我知道有人会问Excel函数也能算啊为什么要绕一圈用Python我的回答是Excel公式在人数少、规则简单的时候确实够用但只要人数超过二十个或者班次超过两三种公式写起来就没边了。你要同时处理迟到早退判断、迟到宽限、午休扣除、跨天班次、节假日调休一大串IF嵌套下来自己看得懂同事完全不敢动万一哪个月加一条规则整个表格都得重构。Pythonpandas的好处在于代码和数据是分离的。数据文件更新了结果重新跑一遍就出来了规则变了只改配置项或者改一个函数就行。而且代码天然可以应对几百人、上万条打卡流水的情况Excel在这量级下已经会开始卡了。再加上Python能生成格式化的Excel报表自动标注异常直接发给财务做工资整个流程的可追溯性和稳定性完全是两回事。2. 搭建可复用的工时计算框架核心数据结构与代码实现2.1 配置驱动把规则从代码里剥离出来我这个脚本能一直用到现在最重要的设计原则就是“配置驱动”。考勤规则这种业务逻辑永远不能写死在代码里。比如公司从下个月开始午休改成1.5小时或者迟到宽限从5分钟改成10分钟如果改完代码还要重新部署那这套东西就不算成功。我用一个单独的JSON文件来存所有可调参数包括{ 午休时长_分钟: 60, 标准工时_小时: 8.0, 迟到宽限_分钟: 5, 早退宽限_分钟: 5, 加班起算阈值_分钟: 30, 加班取整规则: round, # round / ceil / floor 班次定义: { 常白班: {上班: 09:00, 下班: 18:00}, 晚班: {上班: 20:00, 下班: 08:00} }, 员工班次映射: { A001: 常白班, A002: 晚班 }, 节假日: [2025-05-01, 2025-05-02, 2025-05-05] }代码里读一下这个文件所有规则直接生效。每次调整规则不用碰代码改几行JSON重跑一遍就齐活。而且把配置放出来业务同事也能看懂减少沟通成本。2.2 打卡流水清洗与上下班归档拿到考勤机导出的原始Excel后第一步不是马上计算而是先把数据清洗干净。考勤机导出的表经常有乱七八糟的东西列名带空格、时间列是文本格式、日期列混着“2025/5/6”和“2025-05-06”两种写法甚至偶尔混入考勤机的测试记录。下面是读取和清洗的核心代码我会逐步解释每一步的作用import pandas as pd import numpy as np from datetime import datetime, timedelta import json # 读取原始考勤流水 df pd.read_excel(考勤流水.xlsx, sheet_name原始记录) # 统一列名 col_map { 工号: emp_id, 姓名: emp_name, 打卡日期: work_date, 打卡时间: punch_time, 设备: device } df df.rename(columnscol_map) # 清洗去掉完全空白的行 df df.dropna(subset[emp_id, work_date, punch_time], howall) # 把日期和时间拼成真正的datetime类型 df[work_date] pd.to_datetime(df[work_date]).dt.date df[punch_dt] pd.to_datetime( df[work_date].astype(str) df[punch_time].astype(str) ) # 去掉明显异常的时间考勤机半夜测试数据等 df df[(df[punch_dt].dt.hour 5) | (df[punch_dt].dt.hour 4)]这里有个关键点时间列从Excel读出来有时候是datetime.time对象有时候是字符串“18:30:00”pandas都能用to_datetime兜底处理。还有那个过滤条件的说明正常考勤时间基本不会出现在凌晨4点到5点之间这条经验值过滤能非常好地清除设备测试产生的脏数据。如果公司有真实的凌晨班次这个过滤条件就要相应放宽否则会把真实打卡误删配置化才是长久之计。接下来做“每人每天最早和最晚打卡”的聚合# 按人日期分组取出最早和最晚打卡时间作为上下班边界 grouped df.groupby([emp_id, emp_name, work_date], as_indexFalse)[punch_dt] result grouped.agg( first_punch(punch_dt, min), last_punch(punch_dt, max) ) result[first_time] result[first_punch].dt.time result[last_time] result[last_punch].dt.time这个聚合逻辑非常实用。它默认“最早打卡上班最晚打卡下班”。对绝大多数坐班岗位这个假设成立而且能自动跳过中午外出吃饭的中间打卡。最晚打卡时间在很大程度上也包含了加班时长因为加班到20:30打卡最后一笔就是20:30。2.3 核心工时计算逻辑清洗完数据接下来就是重头戏把上下班时间和班次规则结合起来算出每日工时。def calculate_daily_hours(row, config): emp_id row[emp_id] shift_name config[员工班次映射].get(emp_id, 常白班) shift config[班次定义][shift_name] work_date row[work_date] first_punch row[first_punch] last_punch row[last_punch] # 班次上下班时间转成当天datetime shift_start datetime.combine(work_date, datetime.strptime(shift[上班], %H:%M).time()) shift_end datetime.combine(work_date, datetime.strptime(shift[下班], %H:%M).time()) # 处理跨天班次如果班次下班时间早于上班时间说明是次日下班 if shift_end shift_start: shift_end timedelta(days1) # 判断是否跨天最晚打卡时间如果是凌晨需要加一天才能正常计算 if last_punch first_punch: last_punch timedelta(days1) # 实际工时 下班 - 上班 - 午休 lunch_minutes config[午休时长_分钟] actual_hours (last_punch - first_punch).total_seconds() / 3600 - lunch_minutes / 60 actual_hours max(actual_hours, 0) # 标准工时 班次下班 - 班次上班 - 午休 standard_hours (shift_end - shift_start).total_seconds() / 3600 - lunch_minutes / 60 # 加班 实际工时 - 标准工时只算正数 overtime_hours max(actual_hours - standard_hours, 0) # 是否迟到最晚上班时间与班次上班时间对比 # 注意判断迟到要用第一次打卡但如果第一次打卡晚于班次上班宽限则迟到 shift_start_dt datetime.combine(work_date, datetime.strptime(shift[上班], %H:%M).time()) late_minutes 0 if first_punch shift_start_dt timedelta(minutesconfig[迟到宽限_分钟]): late_minutes int((first_punch - shift_start_dt).total_seconds() / 60) # 早退最后一次打卡是否早于班次下班时间 - 宽限 early_leave_minutes 0 shift_end_that_day shift_end # 白班场景下shift_end_that_day就是当天 if last_punch shift_end_that_day - timedelta(minutesconfig[早退宽限_分钟]): early_leave_minutes int((shift_end_that_day - last_punch).total_seconds() / 60) return actual_hours, overtime_hours, late_minutes, early_leave_minutes这段逻辑的几个关键点需要特别说明一是跨天判断。晚班20:00上班、次日08:00下班的人他的下班打卡时间其实是第二天的凌晨。如果直接拿来减上班时间会得到一个负数。所以这里做了两处跨天补偿班次定义里下班早于上班的给班次的shift_end加一天实际打卡里last_punch小于first_punch的也给last_punch加一天。这个双保险看起来很笨但能兜住很多意外情况。二是迟到判断要用第一次打卡而加班计算只跟最晚打卡有关。这两个完全不同的问题不能混在一起算。有人上午迟到但晚上加班到很晚结果算下来工时反而是正的这是正常的——他的实际劳动时长确实覆盖了迟到的部分。在月报里迟到记录和加班时长是两个独立指标不能互相抵消。2.4 输出月度报表计算完每天的记录之后最后一步就是把结果整理成漂亮的月度报表。我的做法是往同一个Excel文件里写三个Sheet原始明细、月度汇总、异常说明。# 给每个员工载入每天的工时结果 daily_rows [] for _, row in result.iterrows(): actual_hours, overtime_hours, late_minutes, early_leave_minutes \ calculate_daily_hours(row, config) daily_rows.append({ 工号: row[emp_id], 姓名: row[emp_name], 日期: row[work_date], 上班打卡: row[first_time], 下班打卡: row[last_time], 实际工时: round(actual_hours, 2), 加班工时: round(overtime_hours, 2), 迟到分钟: late_minutes, 早退分钟: early_leave_minutes, }) daily_df pd.DataFrame(daily_rows) # 月度汇总按员工聚合 summary daily_df.groupby([工号, 姓名], as_indexFalse).agg( 出勤天数(日期, count), 累计工时(实际工时, sum), 累计加班(加班工时, sum), 迟到次数(迟到分钟, lambda x: (x 0).sum()), 早退次数(早退分钟, lambda x: (x 0).sum()), ) # 写出Excel with pd.ExcelWriter(月度考勤工时报表.xlsx, engineopenpyxl) as writer: daily_df.to_excel(writer, sheet_name每日明细, indexFalse) summary.to_excel(writer, sheet_name月度汇总, indexFalse)月度汇总这里有一个非常实用的点groupby之后用命名聚合同时算好几个统计量比之前把每个字段分开算再merge要高效得多。这段代码生成的报表已经可以直接交差了但如果公司需要更漂亮的格式——比如把迟到的行标红、给加班超过20小时的员工加底色——用openpyxl在写出之后做一轮格式美化就行这属于锦上添花的部分。3. 边界场景处理跨天班次、请假缺卡与节假日3.1 跨天班次怎么判断更稳妥前面提到的跨天判断我在实际使用中又碰到过更复杂的场景。有些员工的常白班时间是“08:30-17:30”但他某天加班到凌晨2点系统里留下的最后一条打卡记录是次日凌晨02:10。这时候last_punch first_punch的判断会触发系统会认为他跨天了把下班时间加一天。这样处理是合理的。但反过来如果某天员工白天没来晚上来加了会儿班比如23:50打卡进来次日00:10打卡走这样的数据就会被判定为“跨天班次”的20点班次然后得到“只工作20分钟”这种结果。我后来用一条更细的规则来解决如果班次是“常白班”下班时间大于上班时间但员工的第一次打卡晚于18:00这通常是异常数据应该输出待人工确认标记而不是硬算工时。shift_start_dt datetime.combine(work_date, ...) shift_end_dt datetime.combine(work_date, ...) # 白班员工却只在夜间打卡视为异常 if shift_end_dt shift_start_dt and first_punch.hour 20: row[abnormal] 夜间打卡但班次为白班这种“异常标记”思路非常重要。考勤数据不能只追求“算出一个数”更要帮助HR识别哪些数据是有问题的。把模糊的情况明明白白地摊开让人看远远好过让算法偷偷猜出一个人均工时。3.2 缺卡、请假与补卡怎么处理缺卡是考勤计算里最大的坑几乎每个月都会遇到。有的员工早上忘记打卡有的下班走得急没刷上还有的出差一天根本没打卡记录。如果脚本强行按“最早打卡”和“最晚打卡”计算缺卡的人会被算成0小时一天这是完全错误的。我的做法是分两种缺卡场景处理完全无记录这一天在考勤流水里就不存在该员工的记录那么当日不能自动算工时应该在异常表里输出提示“当日无打卡记录”由HR确认是否为请假或出差。只缺上班卡或只缺下班卡这时只能用半天工时来标记或根据请假单填充。更完善的方法是引入请假数据表把请假日期拉进来判断是全天假还是半天假。请假数据通常也是Excel列结构大概是工号、开始日期、结束日期、请假类型事假/病假/年假、请假时长全天/上午/下午。把这些数据读进来在最终报表里加一列“请假状态”有请假记录的日期就不再参与工时计算。leave_df pd.read_excel(请假记录.xlsx) leave_map {} for _, row in leave_df.iterrows(): key (row[工号], row[开始日期]) leave_map[key] row[请假类型] str(row[请假时长])然后在生成明细表的时候检查一下这个映射命中就标记为请假工时时长改记0出勤天数也不计。这样做的好处是月底对数据的时候请假的人不会被当成“旷工”输出减少跟员工扯皮的概率。3.3 节假日与调休配置节假日这块最好不要用第三方库硬算我们国家的放假安排每年都可能有调整再高级的库也没法自动知道今年春节法定假日到底是哪几天。最简单可靠的做法维护一张“节假日表”把元旦、春节、清明等所有需要特殊处理的日子列出来每年年初更新一次。holidays set(pd.to_datetime(config[节假日]).date) weekend {5, 6} # 周六周日取决于日历 def is_workday(d): if d in holidays: return False # 法定节假日不算工作日 if d.weekday() in weekend: return False # 周末默认不算工作日 return True但如果遇到调休补班规则就要做小步调整了。端午前的那个周日可能要求上班一个普通周六也可能被调成工作日。我后来直接在配置里加了一个“调休上班日”字段把这些周六周日补班的日期也写进去然后is_workday的优先级变成节假日 调休上班日 常规周末。这样逻辑就完整了。在汇总表里我还会给出勤天数做一列标准基数比如本月应出勤21天实际出勤20天。这个基数就是从is_workday函数逐天数出来的。有了应出勤天数和实际出勤天数考勤异常率、缺勤天数这些管理维度就能顺带出一堆。4. 实操踩坑实录这些问题我全都遇到过4.1 Excel日期时间变成了小数考勤机导出的Excel里时间这一列最常踩的坑是Excel时间格式被pandas读成了小数。Excel内部把时间存储为0到1之间的分数比如18:00:00实际上是0.75因为它是中午12点后的0.75天。如果用openpyxl直接读单元格拿到的就是这么个小数直接强制转字符串会得到“0.75”这种鬼东西。我的解决方式是读取后统一交给pandas的to_datetime做转换。如果sheet是用pd.read_excel读的pandas会基于Excel的格式信息自动解析基本不会出问题。但如果从别的系统导出的CSV带时间列就很容易出现纯文本“0.75”或者“18:00:00”混在一起的情况这时要写一个转换函数兜底def excel_time_to_hour(t): if isinstance(t, (int, float)): # Excel的小数时间换算成时分 total_minutes int(round(t * 24 * 60)) return f{total_minutes // 60:02d}:{total_minutes % 60:02d} return str(t)这个转换函数还可以用来统一不同设备的输出格式非常实用。4.2 中文列名与编码问题考勤机的导出文件经常直接用“工号”“姓名”这种中文列名。pandas用中文列名本身没问题但有个隐蔽的坑——同一个文件有时候列名叫“姓名 ”带个尾随空格有时候叫“姓 名”中间多个空格直接df[姓名]就会KeyError。我的做法是读进来之后先做一轮列名清洗把列名里的空格、全角字符、换行符全部剥掉统一替换成标准英文名。这个清洗动作是为整个脚本的健壮性打底非常值得花几分钟写好。df.columns df.columns.str.strip().str.replace( , ).str.replace(\n, ) col_map {工号: emp_id, 姓名: emp_name} df df.rename(columnscol_map)另外CSV文件如果直接读取中文内容常遇到乱码。解决方式很简单pd.read_csv(考勤.csv, encodingutf-8-sig)注意不是utf-8而是utf-8-sig它自带BOM头Excel用起来不会出乱码。如果用Excel模板存CSV导出有时候是GBK编码那就要用encodinggbk读。写代码时建议先try两种编码或者直接用刚才那种转换函数能省不少时间。4.3 数值精度与浮点误差工时计算里最容易忽略的是浮点数精度问题。比如19:30下班、09:00上班、午休60分钟实际工时是9.5小时。但在Python里(19.5 - 9.0) - 1.0得到的结果是9.5吗表面上没问题但如果经过多次运算浮点误差会累积最后汇总出来的数字出现9.499999999这种结果。我最初跑出来的月度报表里有几个人的累计工时出现了58.999999998这种数吓了我一跳。解决方式有两个一是最终展示前用round(x, 2)统一四舍五入二是更稳妥的办法把单位从“小时”换算成“分钟”做整数运算只在最终结果里转换回小时。比如把时间差记成570分钟而不是9.5小时存进DataFrame里用整数计算输出时除以60保留两位小数。这个做法可以彻底避免浮点误差带来的脏数据。4.4 考勤机时间不准带来的连锁反应考勤机设备的时间偶尔会偏差一两分钟虽然系统对过了时间但打卡流水里的时间是错的。这种问题如果直接算会导致某些人明明按时上班却被打成迟到。我的容错方案是对第一次打卡时间做一个“缓冲区间”处理。如果第一次打卡时间比班次上班时间早很多比如提前了一个半小时这大概率不是真正的上班打卡很可能是员工前一晚忘记打卡早上来补打或者是设备时间误差。我只会取“班次上班时间之前90分钟以后”的有效打卡作为当天上班时间。valid_start shift_start_dt - timedelta(minutes90) first_punch_final first_punch if first_punch valid_start else shift_start_dt同理下班打卡如果比班次下班时间晚太多比如晚班次20:00上班次日09:00才打卡走这种超过班次时间七八个小时的记录也很可疑。我会在明细表里加一列“备注”提醒HR人工复核而不是直接算成十几个小时的班。5. 从月报到工时看板这个脚本还能怎么扩展5.1 日报自动推送月度报表做出来之后我很快发现还有更迫切的需求月底一次性看到所有问题已经晚了最好每天都能看到谁没打卡、谁迟到。所以我基于这套脚本加了一个“每日快报”模式只取当天的打卡数据跑一遍同样的清洗和计算逻辑输出今天有异常的人员名单。每天下班后用系统计划任务定时跑一次脚本生成日报Excel再配合企业内部通讯工具的机器人把汇总数据直接发给考勤管理员。这个扩展只用了几行代码——因为核心的数据清洗、班次映射、迟到判断完全复用只是把过滤条件从“整个月”改成“当天”。5.2 部门维度统计与特别提醒如果你的数据里有“部门”字段那么汇总层还可以做更多维度的分析按部门统计迟到次数、平均加班时长、缺卡率等等。我后来就在报表里加了一个“部门月度异常概览”专门帮助项目主管快速了解自己团队这个月的考勤健康度。这里有个容易被忽略的小细节不同部门的班次往往不一样不能用一个统一上下班时间去判断迟到。如果销售团队是弹性工时用固定的“09:00上班”去判断迟到整个团队都会被打成迟到非常不准确。所以配置里要按“员工班次映射”配置到人而不是按部门或全公司统一赋值。弹性工作制的团队甚至可以给每人配置独立的上下班时间。5.3 自动化运行与邮件分发整套脚本完全可以在无人值守的情况下运行。我用系统计划任务每天凌晨跑一次重算随时保证结果和考勤机数据同步。月末汇总出完之后直接用smtplib把生成的Excel文件作为附件发给HR并且把汇总表的关键指标写在邮件正文里比如“本月应出勤21天实际平均出勤20.3天迟到人数3人”一封邮件就可以把关键信息全部传达。做自动化的另一个好处是日志留痕。我每次运行都会写一份日志记录读了多少行流水、过滤掉多少条脏数据、多少人完全无打卡记录。这些日志在复盘的时候非常有用——比如你想知道“3月为什么缺卡率很高”翻查当日运行日志很快就能定位到是考勤机故障还是大量员工出差这比对着Excel手工追查高效太多。这套脚本我从最初的十几行代码一路迭代到现在最大的体会是考勤计算这种项目难点根本不在于“写代码”而在于把业务规则彻底搞清楚。你只有真的把迟到宽限、午休扣除、跨天班次、节假日调休、缺卡补卡这些场景一一列全了写的脚本才是能用的。否则每个月都有新情况代码就会陷入无休止的打补丁循环。如果看完这篇你也打算动手写一套自己的考勤计算脚本我建议先做一件事把你上个月的考勤流水导出来手工拿Excel算清楚三个人的工时和异常把计算规则吃透再开始写第一行代码。规则清楚了代码只是顺水推舟的事。