序
项目是23年的一个练手小项目,因为环节很完整,从头到尾思考了很多细节,完成后很有成就感。趁过年整理出来,尽量规范化的把整个流程梳理出来,为今后开发培养习惯,希望各位多指教。
项目背景
打卡机生成固定格式的数据,每个月会保存为一个Excel文件,记录了员工打卡的时间点。数据格式如下表:
考勤序号 | 姓名 | 工号 | 所属部门 | 5-4 四 | 5-5 五 | 5-6 六 | 5-8 一 | ... |
---|---|---|---|---|---|---|---|---|
1 | 张三 | 42 | A | 07:40 11:17 17:10 | 07:44 11:19 17:44 | 07:14 11:19 | 07:41 14:51 17:09 | ... |
2 | 李四 | 53 | B | 07:37 08:48 11:12 14:59 16:55 | 07:48 11:08 14:47 16:45 | 07:33 11:18 14:52 16:45 | 08:03 11:14 15:02 17:01 | ... |
需求方的打卡规则如下:
txt
打卡需在指定的四个时间段内完成,同一时段打卡多次也只记一次
1. 06:50 ~ 09:10
2. 11:10 ~ 12:30
3. 13:40 ~ 15:10
4. 16:29 ~ 18:01
要求设计程序,统计员工每月打卡次数。
需求分析
功能性需求
- 数据读写
- Excel格式化读取,提取有效信息
- 时间信息的存储格式及比较方法
- 打卡次数统计
- 每人每日数据统计
- 每人整月数据求和
非功能性需求
- 可用性需求
- 运行环境为Windows 7或Windows 10系统
- 面向无代码开发经验的使用者
- 操作需求
- 打卡计算时间段可修改
- 时间段个数可修改
设计思路
语言、环境选择
最开始需求方打算自己配环境跑代码,鉴于功能需求比较简单,没有性能要求,选择Python进行开发。后来对方提出Python环境配置有困难,不要求源码,所以最后用pyinstaller打包成exe交付。
相关库
- pandas:用于Excel读写、数据格式化存储,杀鸡用牛刀,但真香
- json:用于读取参数配置文件
参数读取
配置文件内容
clock_in_lists
:打卡规则,要保证时间段成对出现,不同时间段不重合。每个时间段的开始在左,结束在右,从小到大以此排列。
num_skip_cols
:跳过表格前面的列数。
in_file_name
和out_file_name
:输入数据文件名和输出数据文件名。
json
{
"clock_in_lists": [
"06:50",
"09:10",
"11:10",
"12:30",
"13:40",
"15:10",
"16:30",
"18:00"
],
"num_skip_cols": 4,
"in_file_name": "data.xlsx",
"out_file_name": "out.xlsx"
}
读取代码
python
import json as js
import pandas as pd
# 配置文件加载
f = open('config.json', 'r')
content = f.read()
root = js.loads(content)
CLOCK_IN_LISTS = root['clock_in_lists']
NUM_SKIP_COLS = root['num_skip_cols']
# 输入/输出文件名可以改,但不能包含中文
IN_FILE_NAME = root['in_file_name']
OUT_FILE_NAME = root['out_file_name']
数据存储
文件读取一行代码即可解决,需要思考的是如何处理每个Excel格子中的内容。原始数据输入后是一个字符串,时间段之间用\n间隔,即如下格式
txt
07:40\n11:56\n15:06\n16:38
考虑到后面有比较大小的要求,这里首先利用\n将时间段互相隔开,再把时间段转换为分钟数,用于后续比较,代码如下
python
# 时间段分割
item = raw_data.loc[i][j]
if pd.isna(item):
continue
one_day_str = item.split('\n')
python
# 时间字符串 --> 分钟数
def time2nmin(time_str):
time_array = time_str.split(':')
result = int(time_array[0])*60+int(time_array[1])
return result
打卡规则处理△
打卡规则可以简单粗暴地用if-else
进行处理,但我猜写出来会很难看。我看到需求中说同一时段多次打卡也只算一次
时,第一时间想到的是桶排序的方法。四个时间段相当于四个桶,每输入一个时间就判断是不是在四个桶里,最后计算不为空的桶有几个。 而第二个问题是如何简化判断规则,不用if-else
,而是将时间的单调性和有序性充分利用。首先对四个时间段的起止点编号,线段表述该段时间内为合法打卡时间段,其余空白位置如B、D、F均为不合法打卡时间段。

合法段 | A | C | E | G |
---|---|---|---|---|
左端点编号 | 0 | 2 | 4 | 6 |
右端点编号 | 1 | 3 | 5 | 7 |
段编号 | 0 | 1 | 2 | 3 |
非法段 | B | D | F |
---|---|---|---|
左端点编号 | 1 | 3 | 5 |
右端点编号 | 2 | 4 | 6 |
观察上述表格,可以总结出两条规律 1. 合法段端点编号满足"左偶右奇"; 2. 合法段编号=左/右端点编号 // 2
至此,我们可以设计如下判断流程:首先确认一个输入时间所在的时间段,然后判断该时间段的左右端点为奇数或偶数,比如左端点是奇数,则非法;左端点是偶数,则判断该端点的段编号,给相应的"桶"增加记录。 编写代码如下
python
# 打卡判断
# input:
# clock_in_nmins: 打卡规则
# tic_time: 刷卡时间
# tags: 当日打卡标签
def check_tic(clock_in_nmins, tic_time, tags):
for i in range(0, len(clock_in_nmins)):
# 遍历打卡时间点
if tic_time > clock_in_nmins[i]:
continue
else:
if i % 2 == 1:
tags[i // 2] = 1
break
调试过程
边界检验
设计过程中的思路很清晰,但是实现过程中还要检验边界条件,比如小于最小的时间和大于最大时间的情况能否处理?卡点06:50的打卡,是否计算在内?第一个问题经过验证发现当前逻辑即可解决,第二个问题则需要加以讨论。当前的代码逻辑是先遍历找到右端点,然后检验其他内容。找右端点时,在输入时间≤某时间点时才停下,这种逻辑本质上是把数轴分割成了一系列左开右闭的区间(解释思路源于代码随想录的一期视频)。但是需求方的打卡分割方式其实是四个合法段是闭区间,其他非法段是开区间,可以借助下图理解。
解决的方案就是在加载打卡规则时,将左端点的时间-1,这样尽管还是左开右闭的分割,但实现了合法段形成闭区间的要求。
python
clock_in_nmins = []
clock_in_list_len = len(clock_in_lists)
# 此处的减1与打卡统计算法边界条件有关
for i in range(0,clock_in_list_len):
if i % 2 == 0:
clock_in_nmins.append(time2nmin(clock_in_lists[i]) - 1)
else:
clock_in_nmins.append(time2nmin(clock_in_lists[i]))
其他问题
调试中还遇到一个由于语言特性导致的问题,第一个版本的check_tic()
函数中的continue
是i = i+1
。这个写法如果奏效,逻辑上其实也存在问题,但是这个写法在Python中实际并无作用。对 i值进行的修改只能在该轮循环中有效,下一循环i会被for语句指定的下标变化规则重置。其实原因从语义上也好理解,for i in range(0,n)
就是让i遍历去取range中的值的,根本不存在+1的逻辑,也无从保留这种修改。
最终效果
使用pyinstaller打包成exe,和输入文件、配置文件放在同一文件夹下,双击运行即可。最终输出效果如图

交付内容也非常简单,exe和文档
bash
.
|-- config.json
|-- data.xlsx
|-- exe使用说明.docx
|-- record.exe
`-- out.xlsx
复盘&改进
开发过程中应该在最开始对核心功能构建单元测试,但是还是print调试法,今后有待改进。