2026-10-05 · 喵窝资源铺
做工资核算最头疼的不是算钱,是时间列:打卡机导出的是秒级时间戳,另一张表是毫秒级,行政手填的是「10月4日 09:30」,还有 2026/10/5、Excel 序列号 46000.5、甚至空行。手工统一一遍再逐行算工时,一上午就没了。这篇给一个 只依赖 openpyxl 的脚本:6 种脏格式统一解析 + 自动算工时,跨夜夜班也兜得住。文中代码实跑验证,真实输出见下。
| 脏数据样例 | 识别为 |
|---|---|
1780000000(10位) | 秒级时间戳 |
1780080000000(13位) | 毫秒级时间戳 |
10月4日 09:30 | 中文日期+时间(年份取当前年) |
2026/10/5 08:00 | 斜杠分隔日期 |
46000.5 | Excel 序列号 |
| 空行/纯空格 | 标记「解析失败」,不崩 |
存为 time_clean.py,运行:
python time_clean.py 输入.xlsx 输出.xlsx
第 1、2 列视为「开始时间 / 结束时间」,输出时在后面追加 3 列:清洗后的开始、结束、工时(小时,两位小数)。
# -*- coding: utf-8 -*-
from openpyxl import load_workbook, Workbook
import sys, re, datetime as dt
src, dst = sys.argv[1], sys.argv[2]
wb = load_workbook(src)
ws = wb.active
def parse_any(v):
if v is None:
return None
if isinstance(v, dt.datetime):
return v
if isinstance(v, dt.date):
return dt.datetime(v.year, v.month, v.day)
if isinstance(v, (int, float)):
n = str(int(v))
if len(n) == 13:
return dt.datetime.fromtimestamp(int(n) / 1000)
if len(n) == 10:
return dt.datetime.fromtimestamp(int(n))
try:
return dt.datetime(1899, 12, 30) + dt.timedelta(days=float(v))
except Exception:
return None
s = str(v).strip()
if not s:
return None
# 字符串形式的纯数字也可能是时间戳(打卡机CSV导出常见)
if re.fullmatch(r"\d{10}", s):
return dt.datetime.fromtimestamp(int(s))
if re.fullmatch(r"\d{13}", s):
return dt.datetime.fromtimestamp(int(s) / 1000)
m = re.match(r"(\d{1,2})月(\d{1,2})日\s*(.*)$", s)
if m:
mo, d, rest = int(m.group(1)), int(m.group(2)), m.group(3).strip()
t = dt.time(0, 0)
if rest:
mt = re.match(r"(\d{1,2}):(\d{2})", rest)
if mt:
t = dt.time(int(mt.group(1)), int(mt.group(2)))
return dt.datetime(dt.datetime.now().year, mo, d, t.hour, t.minute)
s2 = s.replace("/", "-")
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
try:
return dt.datetime.strptime(s2, fmt)
except ValueError:
pass
return None
rows = list(ws.iter_rows(values_only=True))
header = list(rows[0])
out = Workbook()
o = out.active
o.append(header + ["开始时间(清洗)", "结束时间(清洗)", "工时(小时)"])
for r in rows[1:]:
r = list(r)
a = parse_any(r[0]) if len(r) > 0 else None
b = parse_any(r[1]) if len(r) > 1 else None
if a and b:
if b < a: # 跨夜夜班:结束早于开始,+1天
b = b + dt.timedelta(days=1)
hours = round((b - a).total_seconds() / 3600, 2)
o.append(r + [a.strftime("%Y-%m-%d %H:%M"), b.strftime("%Y-%m-%d %H:%M"), hours])
else:
o.append(r + [str(a) if a else "", str(b) if b else "", "解析失败"])
out.save(dst)
print(f"完成 -> {dst}")
构造了一份混有全部 6 种格式的考勤表(7 行)实际运行,清洗列输出:
('张三', '1780000000', '2026-05-29 04:26', '2026-05-29 06:26', 2.0) # 秒级时间戳
('李四', 1780080000000, '2026-05-30 02:40', '2026-05-30 05:40', 3.0) # 毫秒级时间戳
('王五', '10月4日 09:30', '2026-10-04 09:30', '2026-10-04 17:45', 8.25) # 中文日期
('赵六', '2026/10/5 08:00', '2026-10-05 08:00', '2026-10-05 12:30', 4.5) # 斜杠日期
('钱七', 46000.5, '2025-12-09 12:00', '2025-12-09 18:00', 6.0) # Excel序列号
('周九', '10月4日 22:00'→'06:30', '2026-10-04 22:00', '2026-10-05 06:30', 8.5) # 跨夜夜班
('孙八', '', '', '', '解析失败') # 空行不崩
6 种格式全部正确解析,工时精确到 0.25 小时粒度;夜班 22:00→06:30 正确算出 8.5 小时而不是 -15.5;空行给出「解析失败」标记而不是让整个脚本崩掉。
踩坑实录:第一版有 1 个真 bug
打卡机导出的 CSV 再转 Excel 后,时间戳列经常是文本「1780000000」而不是数字——isinstance(v,(int,float))完全漏掉。第一版实测 6 行只成功 4 行,加了re.fullmatch(r"\d{10}|\d{13}", s)的字符串分支后才全过。任何只按类型分支的解析器,都要再防一层「数字长得像字符串」。
相关文章:Excel 数据清洗四合一 · Excel 对账脚本 · CSV 批量合并去重