2026-10-06 · 喵窝资源铺
上一篇写了把异常行揪出来,但很多公司月底要交的是另一种表:考勤月度汇总——一人一行,列上出勤天数、迟到几次、缺卡几次、总工时,老板签字用。这种表最烦的不是算,是口径:缺卡的算不算出勤?时间填反的行算不算迟到?这篇给一个 openpyxl 脚本把这些口径问题一次讲清,代码实跑验证,真实输出贴在下面。
| 列 | 口径(代码里一行可改) |
|---|---|
| 出勤天数 | 有打卡记录的天数;单边缺卡也算出勤(人来了/走了),全天缺勤不算 |
| 迟到次数 | 上班晚于 9:30 的天数(只数有完整上下班的行) |
| 缺卡次数 | 单边缺卡次数 |
| 全天缺勤 | 上下班都没记录(含空格、空字符串) |
| 时间填反 | 下班早于上班的行,单独一列,不混进迟到也不算工时 |
| 总工时 | 只累计下班 > 上班的行 |
构造 11 行数据,故意埋了空格单元格、字符串时间 "09:00"、时间填反、姓名为空的脏行、整行空行:
?: 出勤1天 迟到0 缺卡0 全天缺勤0 填反0 工时9.00h 张三: 出勤4天 迟到1 缺卡2 全天缺勤0 填反0 工时17.33h 李四: 出勤2天 迟到0 缺卡1 全天缺勤1 填反0 工时15.92h 王五: 出勤0天 迟到0 缺卡1 全天缺勤1 填反1 工时0.00h
核对几个关键点:李四 9:10 上班没有被算迟到(阈值 9:30,9:10 在内);王五 18:00→8:30 的填反行进了「时间填反」列而不是迟到列,工时 0 没被污染;连跑三次输出文件 md5 完全一致。
python kq_month.py 考勤明细.xlsx 考勤月度汇总.xlsx
输入表要求 4 列:姓名、日期、上班打卡、下班打卡,按表头名找列,列顺序随意。
# -*- coding: utf-8 -*-
"""考勤月度汇总:按人统计出勤天数、迟到次数、缺卡、总工时,输出一人一行的月度表。"""
from openpyxl import load_workbook, Workbook
import sys, datetime as dt
src, dst = sys.argv[1], sys.argv[2]
wb = load_workbook(src)
ws = wb.active
header = [str(c.value).strip() if c.value else "" for c in ws[1]]
col = {name: i for i, name in enumerate(header)}
def parse_time(v):
if v is None or (isinstance(v, str) and not v.strip()):
return None
if isinstance(v, dt.datetime):
return v
if isinstance(v, dt.time):
return dt.datetime.combine(dt.date.today(), v)
if isinstance(v, (int, float)): # Excel 时间序列号
return dt.datetime(1899, 12, 30) + dt.timedelta(days=float(v))
s = str(v).strip()
for fmt in ("%H:%M:%S", "%H:%M", "%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M"):
try:
return dt.datetime.strptime(s, fmt)
except ValueError:
pass
return None
stats = {} # 姓名 -> dict
for row in ws.iter_rows(min_row=2, values_only=True):
if all(v is None or str(v).strip() == "" for v in row):
continue # 整行空:跳过
name = str(row[col["姓名"]]).strip() if row[col["姓名"]] else "?"
cin = parse_time(row[col["上班打卡"]])
cout = parse_time(row[col["下班打卡"]])
st = stats.setdefault(name, {"days": 0, "late": 0, "miss": 0, "hours": 0.0, "full_miss": 0})
if cin is None and cout is None:
st["full_miss"] += 1; st["miss"] += 1
continue
if cin is None or cout is None:
st["miss"] += 1; st["days"] += 1 # 单边缺卡也算出勤
continue
if cout < cin: # 先拦截填反行,别让它污染迟到/出勤统计
st["reversed"] = st.get("reversed", 0) + 1
continue
st["days"] += 1
if cin.time() > dt.time(9, 30):
st["late"] += 1
st["hours"] += (cout - cin).total_seconds() / 3600
out = Workbook()
now = dt.datetime(2026, 1, 1)
out.properties.created = now
out.properties.modified = now
s1 = out.active; s1.title = "月度汇总"
s1.append(["姓名", "出勤天数", "迟到次数", "缺卡次数", "全天缺勤", "时间填反", "总工时(h)"])
for name in sorted(stats):
st = stats[name]
s1.append([name, st["days"], st["late"], st["miss"], st["full_miss"], st.get("reversed", 0), round(st["hours"], 2)])
out.save(dst)
for name in sorted(stats):
st = stats[name]
print(f"{name}: 出勤{st['days']}天 迟到{st['late']} 缺卡{st['miss']} 全天缺勤{st['full_miss']} 填反{st.get('reversed',0)} 工时{st['hours']:.2f}h")
cout < cin 在迟到判断之前 continue),单独进一列。Workbook().properties.created / modified 设成固定时间后,三次连跑 md5 才真正一致。这个坑不影响业务只影响校验,但会让你怀疑代码有问题白查半天。st["days"] += 1 从单边缺卡分支里挪掉即可。口径错了表就废了,所以单独说。🎈 没时间自己跑脚本?喵窝资源铺提供代跑服务:把文件发给我们,AI 执行 + 人工核验后把结果文件发回。查看全部工具与服务 →(虚拟物品,交付后不退款,下单前请确认需求)