
1. 内容整体设计与思路拆解1.1 先搞清楚考勤生成的痛点在哪考勤处理这件事几乎每个公司都躲不开。月底人事要算工资考勤表就是最基础的数据来源。但很多人实际遇到的情况是打卡机导出来的原始记录乱到没法看一天两次打卡、漏卡补卡、外勤签到、请假调休全混在一起。要把这些原始记录变成一张能直接交给财务的月度考勤汇总表往往要耗费几个小时甚至一整天去手工整理。用Python来做考勤生成核心解决的就是“原始打卡记录 → 规范考勤汇总表”这一整条流水线。它不涉及任何高深算法本质上就是三件事读数据、算规则、写表格。但恰恰是这三件事手工做起来又烦又容易错。迟到一次的时间换算错、漏打卡没有自动标记、请假类型统计漏项都会直接影响工资结算回头再改非常被动。我刚开始做这个脚本的时候目标定得很朴素把整月的打卡明细扔进去一键输出两张表一张是每天每人出勤状态的明细表一张是月度汇总统计表。从实际效果看脚本跑完一整个部门三百多人的考勤从原始数据到格式化表格基本控制在十秒以内出错率比人工整理低得多。这个效率差异才是这类脚本真正的价值所在。1.2 为什么用Python只用pandas就够了考勤处理的难点不在于计算逻辑有多复杂而在于数据形态杂乱。打卡机导出的Excel文件通常会有多行表头、合并单元格、空行、日期和时间混在一列、上下班打卡顺序不固定等各种问题。Python生态里处理这种半结构化表格数据最顺手的工具就是pandas。有人可能会问要不要用openpyxl直接操作Excel单元格我的经验是openpyxl适合做格式化写入比如设置列宽、加边框、冻结窗格但不适合做数据计算和筛选。pandas把表格当成数据框来操作按条件筛选、分组聚合、列运算都是几行代码的事。两者配合pandas负责计算openpyxl负责美化分工明确。选型上还有一种方案是直接在Excel里写公式配合透视表来做汇总。对几十个人的小团队这个方法勉强可行。但一旦数据量上来Excel公式的维护成本会迅速膨胀尤其遇到跨月数据、多种班次规则并存的情况公式改起来非常痛苦。用Python脚本固化规则之后换月份只需要替换原始文件不用动任何逻辑这个优势在长期使用中会体现得特别明显。2. 环境准备与数据规范约定2.1 安装Python与依赖库做这类型脚本Python版本建议直接用3.9以上版本官方下载安装包的时候记得勾选“Add Python to PATH”选项否则后面在命令行里输入python会提示找不到命令。安装完成后打开命令行工具Windows下是CMD或PowerShellmacOS和Linux下是终端输入python --version确认版本号能正常显示。接下来安装pandas和openpyxl两个库。pandas负责数据处理openpyxl负责最终表格的样式写入。如果还想加一个图形界面后面再引入tkinter这类标准库不需要额外安装。安装命令如下pip install pandas openpyxl国内网络环境下如果pip下载速度很慢可以在命令后面加上清华镜像源pip install pandas openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple这里有一个细节值得留意pandas本身依赖numpypip安装时会自动一并装好不需要手动处理。openpyxl是用来读写xlsx文件的底层引擎pandas的read_excel和to_excel方法在背后调用它所以这两个库缺一不可。如果是在公司内网环境或没有管理员权限的电脑上操作可以用pip install --user参数装到当前用户目录这样不需要管理员权限也能正常使用。装完之后在Python交互环境里输入import pandas和import openpyxl不报错就说明环境没问题。2.2 Excel表结构约定先把地基打好脚本写得再好如果原始表结构乱七八糟后面处理起来还是会很痛苦。所以动手写代码之前一定要先和打卡机厂商或行政确认导出的字段格式。我经手的绝大多数考勤系统导出Excel的字段大同小异常见的有工号、姓名、部门、打卡日期、打卡时间、打卡设备、打卡类型上班/下班/外勤/加班。这里要特别强调一个原则不要在脚本里兼容无数种奇葩格式而是反过来在脚本开头做一个必要的格式检查和格式转换。如果发现原始表的列名和预期不一致直接报错并给出提示这样逼着使用者先把Excel整理成标准格式。表面上看起来多了一步实际上能避免大量莫名其妙的数据错误。比较稳妥的做法是维护一个标准字段映射表把不同打卡机导出的列名统一映射成脚本内部的固定名称。比如有的系统导出的列叫“刷卡时间”有的叫“考勤时间”都在映射表里统一成clock_time。这一步做扎实了后续的迟到早退判断、汇总统计都不会乱。除了字段名还有一个容易忽略的点日期和时间的格式。很多考勤系统导出的日期是“2025-01-06”这样的文本时间列是“09:03:25”文本但也有一些系统会把日期和时间合并成一个列中间用空格隔开。读取数据后第一件事就是把文本格式统一转成Python的datetime对象这一步不做后面所有比较运算都会出错。3. 核心代码实现一步一步跑通整条流水线3.1 设计代码结构把逻辑拆成独立模块写考勤脚本最忌讳的就是把所有逻辑堆在同一个脚本文件里那样后面维护会非常痛苦。我建议按功能拆成四个独立模块数据加载模块、规则计算模块、汇总统计模块、表格输出模块。数据加载模块负责读取原始Excel、做字段映射、清洗数据。规则计算模块负责判断每天的出勤状态包括正常、迟到、早退、缺卡、请假、出差等。汇总统计模块负责把明细数据聚合到人月级别算出勤天数、缺勤天数、迟到次数等指标。表格输出模块负责把计算结果写到新的Excel文件里同时调整样式。这四个模块之间通过数据框传递数据模块内部互不干扰。比如规则计算模块只需要一个规范化的数据框输入输出一个包含出勤状态的新数据框。后面想加新的考勤规则只需要改规则计算模块其他模块不需要动。这种结构看起来前期多花了一点时间设计但实际改动起来非常省力。我自己的代码里还加了一个config.py配置文件把上下班时间、迟到阈值、月度起止日期等规则参数都放到配置里。每家公司考勤规则不一样有的九点上班有的八点半有的迟到宽容五分钟。把这些参数独立出来换一个部门或者换一个项目组用只需要改配置不需要改代码。3.2 数据加载与预处理清洗比计算更费功夫数据加载这一步最常用的代码是pd.read_excel。这个函数参数比较多我用得最多的几个是sheet_name指定工作表、header指定表头行位置、skiprows跳过前面的杂行。打卡机导出的文件很多会在真正表头之前加上几行公司名称和导出时间这时候skiprows就派上用场了。import pandas as pd raw_df pd.read_excel( ioraw_data/2025年1月考勤明细.xlsx, sheet_name打卡记录, header0, skiprows2, dtype{工号: str} ) print(raw_df.head())读进来之后第一件事是看列名是否完整是否有全空列。我的做法是先用raw_df.columns.tolist()打印出所有列名确认哪些列是真正需要的然后做字段重命名。常见的做法是维护一个映射字典column_map { 工号: emp_id, 姓名: emp_name, 部门: dept, 日期: work_date, 时间: clock_time } raw_df raw_df.rename(columnscolumn_map) keep_cols [emp_id, emp_name, dept, work_date, clock_time] df raw_df[keep_cols].copy()这里有一个非常关键的操作工号必须按字符串读入不能按数字读入。有些公司的工号是“0123”这样带前导零的格式如果pandas读成数字前导零就丢了后面匹配员工信息就会失败。所以在read_excel里面指定dtype{工号: str}是非常有必要的。日期和时间处理是清洗的另一个重点。比如原始数据里work_date是“2025-01-06”文本clock_time是“09:03:25”文本我一般直接用pd.to_datetime合并成一个完整的时间戳列df[datetime] pd.to_datetime( df[work_date].astype(str) df[clock_time].astype(str) ) df[work_date] df[datetime].dt.date df[clock_time] df[datetime].dt.time清洗过程中还需要处理空值。打卡记录里最常见的空值是某些人某天漏打卡了导致时间列为空。这种情况不要直接删除而是保留记录但标记成缺卡后面统计的时候能区分“上班缺卡”和“下班缺卡”。我用dropna(subset[work_date])只删掉完全没有日期的脏数据时间为空的行单独处理。3.3 出勤状态判定迟到早退缺卡的规则实现出勤规则看起来简单但真实现起来会发现有很多边界情况。先说最常见的标准工时制上午九点上班下午六点下班。一天两次打卡记录一次上班卡一次下班卡。但实际数据里有的人一天打了四次卡中午出去吃饭刷了两次有的人只打了一次。所以不能简单地按记录数去判断而是要从当天全部打卡记录中选取最合理的上下班时间。我的判断逻辑是每个人每天的所有打卡时间取出来小于等于中午12点的记录作为上班卡候选取其中最早的一条作为上班时间大于12点的记录作为下班卡候选取其中最晚的一条作为下班时间。这个逻辑在绝大多数场景下是合理的一天打多次卡的人也能正确处理。def get_times(group): morning group[group[clock_time] pd.to_datetime(12:00:00).time()] afternoon group[group[clock_time] pd.to_datetime(12:00:00).time()] on_time morning[clock_time].min() if not morning.empty else None off_time afternoon[clock_time].max() if not afternoon.empty else None return pd.Series({on_time: on_time, off_time: off_time}) daily_status df.groupby([emp_id, emp_name, dept, work_date]).apply(get_times).reset_index()有了每个员工每天的上下班时间之后就可以和标准上下班时间做比较。配置里定义好start_time和end_time之后逐行判定的逻辑非常直接config { start_time: 09:00:00, end_time: 18:00:00, grace_minutes: 5 } def judge_status(row): start_limit pd.to_datetime(config[start_time]) pd.Timedelta(minutesconfig[grace_minutes]) if pd.isna(row[on_time]): return 上班缺卡 if row[on_time] start_limit: return 迟到 if pd.isna(row[off_time]): return 下班缺卡 if row[off_time] pd.to_datetime(config[end_time]): return 早退 return 正常这里我把宽容时间grace_minutes单独拎出来是因为实际考勤规则里很少有公司做到严格一分不差的给几分钟缓冲是常态。宽容时间设成5分钟意味着9点05分之前打卡都算正常这在实际落地中更贴合管理习惯。还有一种情况要考虑请假的员工当天可能完全没有打卡记录。这种情况需要先去请假记录表里匹配匹配上的标记为“请假”匹配不上的才是真正的“缺勤”。所以完整流程里还需要读入一张请假明细表用员工工号和日期做关联。关联好之后状态优先级是请假 缺卡 迟到早退 正常。3.4 汇总统计groupby聚合一键出结果明细状态表生成之后汇总就非常轻松了。用pandas的groupby按员工分组再统计各状态出现次数即可summary daily_status.groupby([emp_id, emp_name, dept]).agg( 出勤天数(status, lambda s: (s 正常).sum()), 迟到次数(status, lambda s: (s 迟到).sum()), 早退次数(status, lambda s: (s 早退).sum()), 缺卡次数(status, lambda s: ((s 上班缺卡) | (s 下班缺卡)).sum()), 请假天数(status, lambda s: (s 请假).sum()), 缺勤天数(status, lambda s: (s 缺勤).sum()), 实际出勤天数(status, lambda s: ((s ! 请假) (s ! 缺勤)).sum()) ).reset_index()这里我加了“实际出勤天数”这个指标它表示员工实际上在岗的天数等于总工作日减去请假和缺勤天数。做工资计算的时候这个数值是核心依据之一。还要注意聚合统计出来的结果默认是按工号排序的但实际查看的时候最好按部门分组排序。我习惯加上一个部门内部排序的逻辑让同一个部门的员工排在一起方便人事逐部门核对summary summary.sort_values([dept, emp_id]).reset_index(dropTrue)3.5 写入Excel既要数据对也要好看数据计算完了输出格式同样重要。如果生成的文件是一堆干巴巴的数字人事同事用起来还是不方便。我一般用pandas的ExcelWriter加上openpyxl引擎来写文件先写入数据再做样式美化。这里有一个踩过的坑值得提醒同一个Excel文件里面既要写入考勤明细表又要写入汇总统计表如果用默认的to_excel直接写两次后面写入的内容会把前面覆盖掉。正确处理方式是使用ExcelWriter的sheet添加模式out_path output/2025年1月考勤汇总.xlsx with pd.ExcelWriter(out_path, engineopenpyxl) as writer: daily_status.to_excel(writer, sheet_name每日明细, indexFalse) summary.to_excel(writer, sheet_name月度汇总, indexFalse)写入之后再用openpyxl打开文件调整样式。常见的调整包括设置列宽、标题行加粗并填充背景色、冻结首行、给表格加边框。这些操作我需要单独写一个函数来封装from openpyxl import load_workbook from openpyxl.styles import Font, PatternFill, Border, Side, Alignment def format_excel(path, sheet_names): wb load_workbook(path) thin_border Border( leftSide(stylethin), rightSide(stylethin), topSide(stylethin), bottomSide(stylethin) ) header_fill PatternFill(start_colorD9E1F2, end_colorD9E1F2, fill_typesolid) for sheet_name in sheet_names: ws wb[sheet_name] for cell in ws[1]: cell.font Font(boldTrue) cell.fill header_fill cell.alignment Alignment(horizontalcenter, verticalcenter) ws.freeze_panes A2 for col in ws.columns: max_length max(len(str(cell.value)) for cell in col if cell.value is not None) ws.column_dimensions[col[0].column_letter].width max_length 4 for row in ws.iter_rows(min_row2): for cell in row: cell.border thin_border cell.alignment Alignment(horizontalcenter, verticalcenter) wb.save(path)这里面的freeze_panes特别实用数据量一大往下滚动的时候看不见表头冻结之后表头始终固定在第一行查看体验会好很多。列宽自适应这个逻辑也值得保留我见过太多导出表格列宽挤在一起、内容都显示不全的情况用上面这段代码可以自动把列宽撑到最合适的大小。4. 常见问题与排查技巧实录4.1 日期列变成字符串是不定时炸弹pandas读Excel有时候日期列读进来是Timestamp对象有时候是字符串这取决于Excel单元格的原始格式。如果在清洗阶段没有统一转成日期格式后面所有比较大小、计算时间差的逻辑全部会出问题。我排查过的一个真实案例某个月的考勤明细中大部分日期是正常的日期格式但其中有一天因为打卡机服务器调整导出的日期列变成了文本格式。结果脚本用pd.to_datetime合并日期和时间列时直接报错报错信息是“Unknown string format”当时排查了很久才定位到是少数单元格格式异常导致的。应对办法是在读取之后立刻调用pd.to_datetime做强制转换并且把errors参数设为coerce让无法转换的值变成NaT然后再用isnull检查有多少行转换失败。如果失败行数超过某个阈值直接说明数据源有问题需要重新导出。4.2 汇总Sheet被覆盖是新手高频踩坑这个问题我刚刚提过再次强调一遍因为它太常见了。很多人第一次写多Sheet输出时直接连续调用两次to_excel结果发现第二次调用把第一次的结果冲掉了。原因在于to_excel默认会创建一个新的ExcelWriter对象前面的写入内容没有保存。正确的做法是用ExcelWriter的上下文管理器像我在3.5节写的示例那样把多个Sheet写在同一个with块里面。在Python 3.10以上版本还有一个需要注意的点ExcelWriter里面的if_sheet_exists参数在openpyxl引擎下可以设置为replace或overlay区别在于如果Sheet名已经存在replace是直接覆盖整个Sheetoverlay是在原Sheet基础上追加写入。我默认用replace除非有特殊需求需要保留多个版本。4.3 打卡时间跨天与全天空白的判断有些公司有夜班岗位晚上十点上班第二天早上六点下班。这种跨天打卡的情况如果按自然日来分组逻辑会对不上。处理跨天最常用的办法是引入班次属性给每个员工配置所属班次然后以班次起始时间作为日期分界线。夜班员工当天晚上十点之前的记录归入前一个工作日十点之后到次日早上六点的记录仍然归入前一个工作日。不过大多数做考勤脚本的场景都是标准白班跨天情况属于少数。我的建议是脚本里先不支持跨天把规则跑通以后再进行扩展。程序设计的通用原则是先解决80%的核心场景再逐步叠加特殊规则。一上来就把所有边界情况都塞进去代码会复杂到没人愿意维护。全天空白的情况也需要特别判断。如果某个员工某一天完全没有打卡记录分组之后on_time和off_time都是空值。这时候不要盲目标记为缺勤先检查一下当天是不是法定节假日、公司调休日或者员工请假。正确的处理顺序是先查节假日表再查请假表最后才标记缺勤。如果这三层都没有匹配上才是真正的无理由缺勤。4.4 中文与编码问题Windows下最容易翻车Windows环境下用Python处理Excel还有一个非常常见的坑文件路径中包含中文目录名或者文件名本身就包含中文字符在某些旧版本的pandas和openpyxl环境下会报编码错误。现代版本的pandas已经处理好了大部分情况但我依然建议在脚本开头声明文件编码或者直接用pathlib模块处理路径from pathlib import Path base_dir Path(__file__).parent raw_path base_dir / 原始数据 / 2025年1月考勤.xlsx out_path base_dir / 输出结果 / 2025年1月考勤汇总.xlsx另外还需要注意Excel单元格内容的编码统一用UTF-8。pandas读取Excel文件时会在后台处理好编码问题但如果通过CSV中间格式中转CSV的编码就非常关键了。Windows下面很多软件导出CSV默认是GBK编码pandas直接read_csv会乱码需要指定encodinggbk或者encodingutf-8-sig。我一般尽量避免CSV中转全程用xlsx格式传递数据编码问题就能最小化。5. 进阶玩法与应用扩展5.1 打包成exe不会Python的人也能用脚本写好了但公司里不是每个人都有Python环境。很多HR同事电脑上连Python都没装让他们打开命令行去跑脚本根本不现实。解决这个问题最直接的办法就是把脚本打包成可执行文件exe双击就能运行。Python生态里打包exe最常用的工具是PyInstaller。安装很简单pip install pyinstaller就行。打包命令也简洁pyinstaller -F -w attendance_generator.py-F表示打包成单个文件-w表示运行时隐藏控制台窗口。如果是带图形界面的脚本-w参数特别重要否则用户运行的时候会同时弹出一个黑底白字的命令行窗口看起来很吓人。打包过程中容易踩的坑是依赖库遗漏。PyInstaller有时候不能自动识别所有动态导入的模块尤其是pandas和openpyxl这种比较大的库。如果打包出来的exe运行时报ModuleNotFoundError可以手动在打包命令里加上--hidden-import参数指定缺失的模块。另外打包出来的exe文件体积通常会超过30MB这是正常的因为里面包含了整个Python运行时和依赖库。想让HR用起来更方便还可以用tkinter写一个极简的图形界面界面上只有一个“选择原始考勤文件”的按钮和一个“开始生成”的按钮。这样整个操作流程对不懂技术的人非常友好极大降低了使用门槛。5.2 从出勤统计延伸到人天工时计算考勤数据算清楚之后很多时候还需要进一步计算出人天工时。比如软件项目里面核算投入成本需要知道每个项目当月消耗了多少人天。出勤天数结合员工所属项目可以计算出每个项目的总人天投入。实现方式是在汇总表的基础上再维护一张员工与项目的对应表然后按部门或者项目做二次聚合。这个扩展逻辑很直接就是把出勤天数关联到项目维度再做一次groupbyproject_map pd.read_excel(config/员工项目映射.xlsx) summary_merged summary.merge(project_map, onemp_id, howleft) project_hours summary_merged.groupby([project_name, dept]).agg( 总人天(实际出勤天数, sum) ).reset_index()这种扩展几乎是零成本的因为底层的出勤明细已经算得足够干净。这也从侧面说明把前期的数据清洗和状态判定做扎实了后面的各种统计需求都会变得很轻松。5.3 多班次支持和自动邮件发送再往远处扩展多班次支持是考勤系统里绕不开的需求。早班、晚班、夜班每个班次的上下班时间不同。实现的思路是把班次信息作为配置项和员工绑定在判断状态之前先根据员工所属班次加载对应的时间阈值。判断逻辑本身不用做大幅改动只是需要根据员工班组动态读取config里的时间参数。自动邮件发送是另一个常见的需求。月底考勤汇总生成之后自动把结果发给相关部门负责人审批省去手动转发的麻烦。Python标准库里的smtplib和email库就能实现不用额外安装第三方包。配置好公司邮件服务器的地址和账号信息之后设置一个定时任务每月一号上午九点自动跑脚本、自动发邮件整个考勤流程就真正实现了无人值守。写在最后的实操体会这套考勤脚本我在自己部门跑了三个多月从最初的版本到现在经历了好几轮迭代最大的体会是考勤生成这件事真正的瓶颈不在于计算而在于对数据的理解和对规则的梳理。花一整天去研究原始表结构、搞明白每一列的业务含义远比写代码的时间更值。数据规范了规则清楚了Python写起来反而特别快。另外一个很深的感触是脚本越简单越好。能用pandas原生方法实现的功能就不要自定义复杂函数能用标准时间类型处理的数据就不要自己造字符串解析逻辑。一切尽量遵循Python数据处理的通用惯例这样代码出了问题在网上很容易搜到解决方案别人接手维护的时候也不会看得一头雾水。如果你半年前让我写这个脚本我可能也会想得特别复杂理论上支持各种特殊考勤规则结果写出来的代码大而全但漏洞百出。现在我的思路完全变了先跑通一个最核心的场景解决80%的问题剩下的边缘情况在真实使用中一个一个补充进来。从这个角度说用Python做考勤生成最有价值的不是一次写出来的代码而是在持续使用中积累下来的规则库和踩坑记录。