- 系统环境
- config配置文件
- 日志文件管理
- 源数据处理
- 时间类的实现
- read-cal-write
- 打包
- 使用
- 预览
- 准备工作目录
- configs.txt
- StudentID1.xlsx
- 输出文件夹
- 考勤统计表
- 日志文件
- 傻瓜级使用教程
- 最后
做了这么久助管,一直都想写一个统计考勤的exe,但是上学期太忙(其实是因为懒)就一拖再拖。最近略有闲时,同时也为了方便未来的所有助管,就动动手写写代码,实现一个专门为事务大厅考勤系统设计的考勤统计exe(面向“对象”设计)。
为什么要面向对象设计呢?因为对象有需求啊。zezeze
温馨提示:如果您对实现原理丝毫不感兴趣,建议直接跳至“傻瓜级使用教程”章节进行阅读。
系统环境笔者使用环境如下
python 3.8 numpy 1.19.2 oauthlib 3.1.1 olefile 0.46 openpyxl 3.0.9 opt-einsum 3.3.0 pandas 1.3.3 xlrd 1.2.0 xlutils 2.0.0 xlwt 1.3.0 yarl 1.6.3 zipp 3.6.0 easydict 1.9config配置文件
首先定义一些配置信息,你不必一定要事先知道这些变量的含义,当然你可以通过命名尝试性理解,但通常的做法是,当你阅读到代码某一行时,发现了这个变量,再来变量定义处看它是怎么定义的。
from easydict import EasyDict as edict __C = edict() cfg = __C __C.root='./' __C.dataPath='员工刷卡记录表' __C.idPath='in' __C.idFile='StudentID' __C.dateFile='configs.txt' __C.outPath='out' __C.outFile='考勤统计表.xls' __C.startrow=1 __C.startcol=1 __C.hour_earn=12 __C.log='log.txt'日志文件管理
如果你的程序设计基础比较差或者对这部分不感兴趣,你可以跳过这一节。本节与整个系统的实现几乎没有关系,只是为了更好的管理日志,方便后续维护。
定义一个logger对象,将输出到log文件的handler和输出到console的handler添加到logger对象中。一般在大型程序的管理和维护时,这种日志文件的管理很有用。
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import logging
import os
import time
def loggee(isfile=True):
logger = logging.getLogger()
logger.setLevel(level=logging.DEBUG)
if isfile:
time_line = time.strftime('%Y%m%d%H%M', time.localtime(time.time()))
log_path=os.path.join(os.getcwd(),'log')
if not os.path.exists(log_path):
os.mkdir(log_path)
logfile = log_path + '/'+time_line + '.txt'
handler = logging.FileHandler(logfile,mode='w') # 输出到log文件的handler
# handler.setLevel(logging.INFO)
formatter = logging.Formatter("%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
console = logging.StreamHandler() # 输出到console的handler
console.setLevel(logging.DEBUG)
logger.addHandler(console)
return logger
l=loggee()
if __name__=='__main__':
l=loggee()
l.debug('This is a debug message.')
l.info('This is an info message.')
l.warning('This is a warning message.')
l.error('This is an error message.')
l.critical('This is a critical message.')
源数据处理
统计考勤的第一步当然就是要知道我们要统计哪一段日期,比如2.28-3.12,我们需要将这一段字符串输入,交给程序进行解析。
程序知道了统计时间段后,下一步就是要查找对应的文件,比如我这里有如下三个文件:
2.21-2.27员工刷卡记录表 2.28-3.6员工刷卡记录表 3.6-3.11员工刷卡记录表 3.12-3.20员工刷卡记录表
显然,2.28-3.12的考勤信息在后三个文件中,我们需要把这三个文件的路径保存下来,以便后续进行read操作。
值的一题的是,上述时间段存在二义性,例如:2.28-3.12可以有很多种解读方法,2021.2.28-2021.3.12?2022.2.28-2022.3.12?2023.2.28-2023.3.12?作为人类,在当前这个时间点,我们自然很清楚我们需要查询的是2022.2.28-2022.3.12,但是计算机没有这么聪明。而且,更为可怕的是,如果你的2.28-3.6员工刷卡记录表保存的是2021年的信息呢,计算机不会知道,它只会很“正常”的执行完整个流程。
不过你不必担心,笔者在程序中对这种情况进行了处理。理论上,无论你输入month/day或者year/month/day格式的日期,程序均能自动识别并避免二义性。如果某个日期出现了二义性,例如3.12-3.20员工刷卡记录表中保存的是2022.3.12-2022.3.20,,当你在2023执行这个程序时,程序会自动将文件名中的3.12-3.20解读为2023.3.12-2023.3.20,此时程序会将自己解读的结果与文件中的meta信息进行对比(meta信息中包含该文件准确的日期信息),如果不一致,程序会抛(raise)给你一个爱之飞吻(Exception)。
可能你会疑惑,为什么不直接读取文件的meta信息作为该文件的保存记录呢?这当然可以,但是笔者为了我们的人类(小助管们)方便管理,推荐在每个考勤记录表前添加上日期,否则你的文件夹中会放着几十个考勤记录表,你自己不知道每个文件对应的哪段日期,虽然程序对这个一清二楚,但如果程序出现错误呢?(经过我的多次测试,出错不太可能,除非你没有按我的要求来)
好了,打住,废话有点太多了。
对日期的格式处理完后,我们需要读取所有学生的信息。你需要准备一个StudentID1.xlsx文件(.xls亦可),按列依次输入 学号、姓名、班级(、工号、工时/工资),括号内为可选信息(默认工号为学号后八位,默认选择工资)。程序会自动读取这些信息并存储。此外,笔者考虑了小助管们可能需要同时统计多组学生的考勤(比如一个人带两个部门),你可以准备StudentID2.xlsx、StudentID3.xlsx、StudentID4.xlsx多个文件,程序会读取所有文件中的学生信息,并返回给你相同数量的IDx-考勤时间段-考勤统计.xls文件。
以上就是对源数据的处理,实现代码如下:
import xlrd
import os
import numpy as np
from config import cfg
from datetime import datetime
from logg import l
import sys
# 返回当前年份
def nyear():
return str(datetime.now().year)
# 比较两个日期的先后
def bi(a0,a1):
a0_l=a0.split('.')
a1_l = a1.split('.')
for l0,l1 in zip(a0_l,a1_l):
if int(l0)>int(l1):
return 1
elif int(l0)res or day <1:
l.error('输入日期'+date+'非法!')
sys.exit(0)
return res
# 计算两个日期的间隔天数
def lenDate(sb,se):
res=0
sbtmp=sb.split('.')
setmp = se.split('.')
year=int(setmp[0])-int(sbtmp[0])+1
month=int(setmp[1])-int(sbtmp[1])+1
if year==1: # 同年日期
if month==1: # 同月日期
res+=int(setmp[2])-int(sbtmp[2])+1
else:
for i in range(int(sbtmp[1]),int(setmp[1])):
res+=theDay0fMonth(sb,i-int(sbtmp[1]))
res+=lenDate(sbtmp[0]+'.'+setmp[1]+'.'+sbtmp[2],se)
else:
res+=lenDate(sb,sbtmp[0]+'.12.31')
res += lenDate(setmp[0] + '.1.1',se)
for y in range(1,year-1):
res += lenDate(str(int(sbtmp[0])+y) + '.1.1', str(int(setmp[0])+y) + '.12.31')
return res
# 计算两个日期段的重叠天数
def isOverlap(str1,str2,con,date0):
s1b,s1e=str1.split('-')
s2b,s2e=str2.split('-')
assert bi(s1b,s1e)<=0 and bi(s2b,s2e)<=0
if bi(s1e,s2b)<0 or bi(s2e,s1b)<0:
return 0,'',''
elif bi(s1e,s2b)>=0 and bi(s1b,s2b)<0 and bi(s1e,s2e)<=0:
l=lenDate(s2b,s1e)
setContain(con,s2b,l,date0)
return l,s2b,s1e
elif bi(s1b,s2b)>=0 and bi(s1e,s2e)<=0:
l = lenDate(s1b, s1e)
setContain(con, s1b, l, date0)
return l,s1b, s1e
elif bi(s1b,s2b)>=0 and bi(s1b,s2e)<=0 and bi(s1e,s2e)>0:
l = lenDate(s1b, s2e)
setContain(con, s1b, l, date0)
return l,s1b, s2e
elif bi(s2b,s1b)>=0 and bi(s2e,s1e)<=0:
l = lenDate(s2b, s2e)
setContain(con, s2b, l, date0)
return l,s2b, s2e
else:
return -1,'',''
# 根据年月日构造一个标准的日期字符串
def getDate(y,m,d):
return str(y)+'.'+str(m)+'.'+str(d)
# 在当前日期date上增加d天,返回标准日期字符串
def addDate(date,d):
date0 = date
year,month,day=date0.split('.')
year, month, day=eval(year),eval(month),eval(day)
d0=d
while d0>0:
if day==theDay0fMonth(getDate(year, month, day)):
if month==12:
year+=1
month=1
day=1
else:
month += 1
day = 1
d0-=1
if d0>=theDay0fMonth(getDate(year, month, day))-day:
d0 -= theDay0fMonth(getDate(year, month, day)) - day
day=theDay0fMonth(getDate(year, month, day))
else:
day+=d0
d0-=d0
return getDate(year, month, day)
# con是一个状态数组,长度等于输入时间段的天数,1代表该日已被检索到
def setContain(con,begin,l0,date0):
index=lenDate(date0,begin)-1
for i in range(l0):
if con[index+i]==1:
try:raise Exception(addDate(date0,index+i)+"被检索到了两次,这会产生严重错误!")
except:
l.error(addDate(date0,index+i)+"被检索到了两次,这会产生严重错误!")
sys.exit(0)
con[index + i] = 1
return 1
# 如果日期字符串中包含year,则返回原始字符串;如果不包含year,则将当前年份添加至字符串头部
def addyear(date,year=nyear()):
if len(date.split('-')[0].split('.'))==3:
return date
if len(date.split('-'))==2:
ye=str(int(year)-1) if int(date.split('-')[0].split('.')[-2])>int(date.split('-')[1].split('.')[-2]) else year
dat=ye+'.'+date.split('-')[0]+'-'+year+'.'+date.split('-')[1]
else:
dat=year+'.'+date
return dat
# 查询con状态数组,全1(所有日期皆被检索到)则返回1,否则返回-1
def searchCon(con,date0):
for i in range(con.shape[0]):
if con[i]==0:
try:raise Exception(addDate(date0,i)+"的记录未被找到")
except:
l.error(addDate(date0,i)+"的记录未被找到")
sys.exit(0)
# 读取configs文件信息:输入时间段,文件密码
def readDate():
datepath=os.path.join(os.path.join(cfg.root,cfg.idPath),cfg.dateFile)
file=open(datepath,mode='r',encoding='utf-8')
dic={}
k=0
for line in file:
if line.split('n')[0].split(';')[0].split(' ')[0]=='begin::': k=1
elif line.split('n')[0].split(';')[0].split(' ')[0]=='end!!!': break
elif k==1:
key,value=line.split(':')
dic[key]=value.split('n')[0].split(';')[0].split(' ')[0]
file.close()
return dic
# 根据输入时间段 查询命中的表格,将所有命中文件名存储在res中;返回-1代表数据文件夹不存在
def hit_xls(date,res):
dirs=os.listdir(os.getcwd())
if(cfg.dataPath not in dirs):
return -1
dates=addyear(date,nyear())
theDay0fMonth(dates.split('-')[0])
theDay0fMonth(dates.split('-')[1])
l_date=lenDate(dates.split('-')[0],dates.split('-')[1])
l.debug("程序将自动统计 "+dates+" 共 "+str(l_date)+" 天的考勤")
con=np.zeros((l_date,))
xls_path=os.path.join(os.getcwd(),cfg.dataPath)
excels=os.listdir(xls_path)
for excel in excels:
date_tmp=addyear(excel.split('员工刷卡记录表')[0],nyear())
l_over,over1,over2=isOverlap(dates,date_tmp,con,dates.split('-')[0])
if l_over>0:
res.append([excel,over1,l_over])
searchCon(con,dates.split('-')[0])
return 1
# 读取一个文件中的学生id信息---(学号,姓名,班级,工号,工资/工时)
def ID(id_path):
res=[]
table=xlrd.open_workbook(filename=id_path).sheets()[0]
for i in range(1,table.nrows):
ls=table.row_values(rowx=i, start_colx=0, end_colx=5)
if ls[0]=='':continue
if len(ls)<4:
id=str(int(ls[0]))[2:]
sym=0
else:
id=str(int(ls[3])) if ls[3]!='' else str(int(ls[0]))[2:]
sym=1 if ls[3]!='' else 0
if len(ls) < 5:
salary = '0' # 0代表工资
else:
salary=str(int(ls[4])) if ls[4]!='' else '0' # 0代表工资
ls=[str(int(ls[0])),ls[1],ls[2],id,salary,sym]
res.append(ls)
# print(res)
return res
# 读取id目录下所有id文件中的学生信息
def IDs():
ids=[]
idpath=os.path.join(cfg.root,cfg.idPath)
dirs=os.listdir(idpath)
for idfile in dirs:
if idfile.split(".")[0][0:9]==cfg.idFile and idfile.split('.')[-1] in ['xls','xlsx']:
ids.append([ID(os.path.join(idpath,idfile)),idfile.split('.')[0].split('Student')[-1]])
return ids
conf=readDate()
hitxls=[]
hit_xls(conf['Date'],hitxls)
studID=IDs()
if __name__=='__main__':
# main()
s=readDate()
print(s)
时间类的实现
在考勤记录表中的每一个cell都是一个类似于7:59n9:51的字符串,为了方便管理这种时间段并自行计算每个时间段的有效考勤时间,笔者定义了一个clock类来管理诸如7:59这样的时间,定义了一个timeStage类管理诸如7:59n9:51这样的时间段。
clock类
from logg import l
import sys
class clock():
def __init__(self,str,isbegin=0,isSummer=False):
self.h=int(str.split(':')[0])
self.m = int(str.split(':')[1])
if isbegin==0: return
self.hs,self.ms=self.norm(isbegin,isSummer)
if(self.hs==0):
try:raise Exception("标准化时间为0,clock初始化出现了某些异常")
except:
l.error("标准化时间为0,clock初始化出现了某些异常")
sys.exit(0)
# if isbegin:
def norm(self,isbegin,isSummer):
h0=0
m0=0
cha8=self.cha(clock("8:00"))
cha10 = self.cha(clock("10:00"))
cha12 = self.cha(clock("12:00"))
chax1=self.cha(clock("14:30")) if isSummer else self.cha(clock("14:00"))
chax2=chax1-120
chax3 = chax2 - 120
# 12节---------------------------
if self.h==7 or (cha8>=0 and cha8<=10):
h0=8
m0=0
elif cha8>10 and cha8<=30:
h0=8
m0=30 if isbegin==1 else 0
elif cha8>30 and cha8<=60:
h0=9 if isbegin==1 else 8
m0=0
elif cha8>60 and cha8<=90:
h0=10 if isbegin==1 else 9
m0=0
elif cha10>-30 and cha10<-10:
h0 = 10 if isbegin==1 else 9
m0 = 0 if isbegin==1 else 30
# 34节------------------------
elif cha10>=-10 and cha10<=10:
h0 = 10
m0 = 0
elif cha10>10 and cha10<=30:
h0 = 10 if isbegin==1 else 10
m0 = 30 if isbegin==1 else 0
elif cha10>30 and cha10<=60:
h0=11 if isbegin==1 else 10
m0=0
elif cha10>60 and cha10<=90:
h0=12 if isbegin==1 else 11
m0=0
elif cha12>-30 and cha12<-10:
h0 = 12 if isbegin==1 else 11
m0 = 0 if isbegin==1 else 30
# 中午------------------------
elif cha12 >= -10 and cha12 <= 10:
h0 = 12
m0 = 0
elif cha12 > 10 and cha12 <= 20:
h0 = 12
m0 = 30 if isbegin == 1 else 0
elif cha12 > 20 and cha12 <= 35:
h0 = 12
m0 = 30 if isbegin == 1 else 0
elif cha12 > 35 and cha12 <= 60:
h0 = 13
m0 = 0
elif cha12 > 60 and cha12 <= 90:
h0 = 14 if isbegin == 1 else 13
m0 = 0
elif chax1 > -30 and chax1 < -10:
h0 = 14 if isbegin == 1 else 13
m0 = 0 if isbegin == 1 else 30
# 56节
elif chax1 >= -10 and chax1 <= 10:
h0 = 14
m0 = 0
elif chax1 >10 and chax1 <= 30:
h0 = 14 if isbegin == 1 else 14
m0 = 30 if isbegin == 1 else 0
elif chax1 > 30 and chax1 <= 60:
h0 = 15 if isbegin == 1 else 14
m0 = 0
elif chax1 > 60 and chax1 <= 90:
h0 = 16 if isbegin == 1 else 15
m0 = 0
elif chax2 > -30 and chax2 < -10:
h0 = 16 if isbegin == 1 else 15
m0 = 0 if isbegin == 1 else 30
# 78节
elif chax2 >= -10 and chax2 <= 10:
h0 = 16
m0 = 0
elif chax2 >10 and chax2 <= 30:
h0 = 16 if isbegin == 1 else 16
m0 = 30 if isbegin == 1 else 0
elif chax2 > 30 and chax2 <= 60:
h0 = 17 if isbegin == 1 else 16
m0 = 0
elif chax2 > 60 and chax2 <= 90:
h0 = 18 if isbegin == 1 else 17
m0 = 0
elif chax3 > -30 and chax3 < -10:
h0 = 18 if isbegin == 1 else 17
m0 = 0 if isbegin == 1 else 30
# 晚上
elif chax3 >= -10 and chax3<=10:
h0=18
m0=0
elif chax3 >10 and chax3 <= 30:
h0 = 18 if isbegin == 1 else 18
m0 = 30 if isbegin == 1 else 0
elif chax3 > 30 and chax3 <= 60:
h0 = 19 if isbegin == 1 else 18
m0 = 0
elif chax3 > 60 and chax3 <= 90:
h0 = 20 if isbegin == 1 else 19
m0 = 0
elif chax3 > 90 and chax3 < 110:
h0 = 20 if isbegin == 1 else 19
m0 = 0 if isbegin == 1 else 30
elif chax3 >=110:
h0=20
m0=0
return self.add30(h0,m0,isSummer)
def add30(self,h,m,isSummer=False):
if isSummer and h>=14:
m0=(m+30)%60
h0=h+(m+30)//60
return h0,m0
else: return h,m
def cha(self,clocks):
dh=self.h-clocks.h
dm = self.m - clocks.m
return dh*60+dm
def pr(self):
print(str(self.h)+":"+str(self.m)+", normtime="+str(self.hs)+":"+str(self.ms))
timeStage类
class timeStage():
def __init__(self, strs,isSummer=False):
if strs=='':
self.duration=0
return
spl=strs.split('n')
self.whole = False
if(len(spl)==2): self.whole=True
else:
self.duration = 0
return
if not (len(spl[0].split(':'))==2 and len(spl[1].split(':'))==2):
self.duration = 0
return
self.begin=clock(spl[0],isbegin=1,isSummer=isSummer)
if self.whole:
self.end=clock(spl[1],isbegin=2,isSummer=isSummer)
else:
self.end = clock(spl[0], isbegin=2, isSummer=isSummer)
self.duration=self.chas()
def chas(self):
dh=self.end.hs-self.begin.hs
dm = self.end.ms - self.begin.ms
return dh + dm/60
def pr(self):
print(str(self.begin.h)+":"+str(self.begin.m)+"-",end='')
if self.whole:
print(str(self.end.h)+":"+str(self.end.m)+", ",end='')
print("duration= {:.1f} h".format(self.duration))
read-cal-write
接下来进行读数据-计算-写入操作。
根据每个学生的工号检索其考勤信息(二分查找),如果没有检索到(或检索结果为空),则根据学生姓名进行 O ( n ) O(n) O(n)复杂度的简单查找,如果只查找了一个可用结果,则将该结果视为学生的考勤信息,如果查找到了多个可用信息(这代表有重名且该学生并未录入学号),程序将返回错误信息,此时最好的处理方法时在StudentID.xlsx文件中录入学生工号,程序将唯一的根据该工号进行二分查找,绝不会产生二义性。
值的一提的是,如果工号检索结果为空,并根据姓名查找到了唯一的不为空的可用信息,程序会将该可用信息视为学生的考勤信息。因为笔者在作为助管期间,遇到过学生原始工号的指纹无法录入,并且该学生改变工号后重新录入了指纹,所以上述处理是合法的,而且在实际应用场景中较为常见。但是,如果该学生只是单纯的在检索时间段内未值班,并且恰好有一个与他同名的学生在另一个部门值班,程序无法有效的判断这种情况。程序唯一能做的是,给你一个提示等级较弱的warning(而非error):l.debug(id[1]+"的学号检索结果为空(有可能该学生在被检索时间段内未值班),但根据其姓名在文件另一处检索到可用信息")。如果你不是特别确定该学生一定使用了自己的学号作为工号,你最好的解决方法是录入该学生正确的工号。
剩下的就是根据每个学生的考勤信息计算工时了,比较简单了,就不细说了。
import xlrd
import os
import numpy as np
import sys
from xlutils.copy import copy
from config import cfg
import xlwt
from dataloader import lenDate,addyear,addDate,nyear,conf,hitxls,studID
from clock import clock,timeStage
from logg import l
# 根据姓名检索行号(可能返回多个值)
def searchname(table,name):
res=[]
for i in range(table.nrows):
if table.cell_value(rowx=i, colx=11) == name:
res.append(i)
return res
# 根据工号检索行号
def searchid(table,id):
row=table.nrows
i=0
j=row-1
while i<=j:
while table.cell_value(rowx=i, colx=1)!="工号:" and i<=j: i+=1
while table.cell_value(rowx=j, colx=1)!="工号:" and i<=j: j-=1
if int(table.cell_value(rowx=i, colx=3))==int(id): return i
elif int(table.cell_value(rowx=j, colx=3))==int(id): return j
else:
mid=(i+j)//2
k=-1
m=0
while table.cell_value(rowx=mid, colx=1)!="工号:" and mid>=i and mid<=j:
k=1 if k==-1 else -1
m += 1
mid+=k*m
if table.cell_value(rowx=mid, colx=1)!="工号:":break
if int(table.cell_value(rowx=mid, colx=3))==int(id): return mid
elif int(table.cell_value(rowx=mid, colx=3))>int(id):j=mid-1
else:i=mid+1
return -1
# 根据行号检索下面几行的考勤信息
def nextl(table,row,col_l,col_r):
res=[]
nex = row + 1
while nex < table.nrows and nex < row + 100 and table.cell_value(rowx=nex, colx=1) != "工号:":
nex += 1
if nex < table.nrows and table.cell_value(rowx=nex, colx=1) != "工号:":
try:
raise Exception("从"+str(row)+"行开始查询了100行,这里出现了错误")
except:
l.error("从"+str(row)+"行开始查询了100行,这里出现了错误")
sys.exit(0)
id_row = nex - row - 2
for i in range(id_row):
res.append(table.row_values(rowx=row + 2 + i, start_colx=col_l, end_colx=col_r))
return res
# 判断文件名与文件中日期的一致性
def consistent(table,path):
ls0, ls1 = table.cell_value(rowx=2, colx=25).split(':')[-1].split('~')
date_xl = ''
spl = ls0.split('-')
date_xl += spl[0] + '.' + spl[1] + '.' + spl[2]
if not lenDate(date_xl, addyear(path.split('员工刷卡记录表')[0]).split('-')[0]) == 1:
try: raise Exception('文件名与文件中日期不一致')
except:
l.error('文件名与文件中日期不一致')
sys.exit(0)
date_xl = ''
spl = ls1.split('-')
date_xl += spl[0] + '.' + spl[1] + '.' + spl[2]
if not lenDate(date_xl, addyear(path.split('员工刷卡记录表')[0]).split('-')[1]) == 1:
try:raise Exception('文件名与文件中日期不一致')
except:
l.error('文件名与文件中日期不一致')
sys.exit(0)
# 列表元素是否全部为空
def isEmpty(ls):
for s in ls:
for s0 in s:
if s0!='':return False
return True
def extname(table,namels,col_l,col_r):
res=[]
if len(namels)==0:
return -1
for n in namels:
table_list = nextl(table, n, col_l,col_r)
if not isEmpty(table_list):
res.append(table_list)
if len(res)==1:
return res[0]
else:
return -1
def mkd(root,path,filename):
if not os.path.exists(os.path.join(root,path)):
os.mkdir(os.path.join(root,path))
return os.path.join(os.path.join(root,path),filename)
def salines(sheet,row,col):
if sheet.cell_value(rowx=row,colx=col)=='':
try:raise Exception(str(row)+'行,'+str(col)+'列 没有合计工资!')
except:
l.error(str(row)+'行,'+str(col)+'列 没有合计工资!')
sys.exit(0)
row0=row+1
while row0=rsheet.nrows: break
line=salines(rsheet,row,s_col)
wbook.save(os.path.join(path,filename)) # 保存
l.debug(ids[1]+'的simple计算完成!')
def styh():
# 左栏
style1 = xlwt.XFStyle()
alignment = xlwt.Alignment()
# 0x01(左端对齐)、0x02(水平方向上居中对齐)、0x03(右端对齐)
alignment.horz = 0x02
# 0x00(上端对齐)、 0x01(垂直方向上居中对齐)、0x02(底端对齐)
alignment.vert = 0x01
# 设置背景颜色
pattern = xlwt.Pattern()
# 设置背景颜色的模式
pattern.pattern = xlwt.Pattern.SOLID_PATTERN
# 背景颜色
pattern.pattern_fore_colour = 26
style1.alignment = alignment
# style1.pattern = pattern
# 顶栏
style2 = xlwt.XFStyle()
font = xlwt.Font()
font.height = 20 * 10
style2.font=font
style2.alignment = alignment
# style2.pattern=pattern
# 简单居中
style3 = xlwt.XFStyle()
style3.alignment = alignment
return style1,style2,style3
def sty():
# 考勤原始时间段
style1 = xlwt.XFStyle()
font = xlwt.Font()
font.name = 'Times New Roman'
font.height = 20 * 10
font.bold = False
alignment = xlwt.Alignment()
# 0x01(左端对齐)、0x02(水平方向上居中对齐)、0x03(右端对齐)
alignment.horz = 0x02
# 0x00(上端对齐)、 0x01(垂直方向上居中对齐)、0x02(底端对齐)
alignment.vert = 0x01
style1.font = font
style1.alignment=alignment
# 每人每日工时
style2 = xlwt.XFStyle()
font = xlwt.Font()
font.name = 'Times New Roman'
font.height = 20 * 12
font.colour_index = 61
font.bold = True
style2.font = font
style2.alignment = alignment
# 每人总工时
style3 = xlwt.XFStyle()
font = xlwt.Font()
font.name = 'Times New Roman'
font.height = 20 * 16
font.colour_index = 2
font.bold = True
style3.font = font
style3.alignment = alignment
return style1,style2,style3
row_sal=cfg.startrow # 全局变量,用于指示sheet_sal当前要写的行
row_time=cfg.startrow # 全局变量,用于指示sheet_time当前要写的行
def wtfile(sheet,sheet_sal,sheet_time,row,id,ls,header=False):
global row_sal,row_time
start_col = cfg.startcol
style1, style2, style3=sty()
stylel,styled,stylej=styh()
tm=addyear(conf['Date'])
lenn=lenDate(tm.split('-')[0],tm.split('-')[1])
if header:
sheet.write(row, start_col, '学号',styled) # 学号
sheet.write(row, start_col + 1, '姓名',styled) # 姓名
sheet.write(row, start_col + 2, '班级',styled) # 班级
for i in range(lenn):
sheet.write(row, start_col+3+i, addDate(addyear(conf['Date'].split('-')[0]),i).replace('.','/'),styled)
sheet.col(start_col+3+i).width = 10 * 256
sheet.write(row, start_col + 3 + lenn, "合计工时/h",styled)
sheet.col(start_col + 3 + lenn).width = 9 * 256
sheet_sal.write(row_sal, start_col, '学号',stylej) # 学号
sheet_sal.write(row_sal, start_col + 1, '学生姓名',stylej) # 姓名
sheet_sal.write(row_sal, start_col + 2, '班级',stylej) # 班级
sheet_sal.write(row_sal, start_col + 3, '累积工作时间/h',stylej)
sheet_sal.write(row_sal, start_col + 4, '工资',stylej)
sheet_sal.col(start_col + 3 ).width = 15 * 256
sheet_time.write(row_time, start_col, '学号',stylej) # 学号
sheet_time.write(row_time, start_col + 1, '学生姓名',stylej) # 姓名
sheet_time.write(row_time, start_col + 2, '班级',stylej) # 班级
sheet_time.write(row_time, start_col + 3, '累积工作时间/h',stylej)
sheet_time.write(row_time, start_col + 4, '工时',stylej)
sheet_time.col(start_col + 3).width = 15 * 256
row_sal+=1
row_time+=1
return row+1
maxx=0
# 一个学生的所有考勤信息写入
su=np.zeros((lenn,))
stu_sal=0
for table_list,col in ls:
l=len(table_list)
if l > maxx:
maxx=l
for c in range(len(table_list[0])):
month = int(addDate(addyear(conf['Date'].split('-')[0]), col - 1 + c).split('.')[1])
summ=0
for n in range(len(table_list)):
sheet.write(row+n, start_col+2+col+c, table_list[n][c].replace('n','-'),style1)
summ+=timeStage(table_list[n][c],isSummer=(month>=5 and month<10)).duration
if summ!=0:
stu_sal += summ
su[col+c-1]=summ
sheet.write_merge(row,row+maxx, start_col,start_col, id[0],stylel) # 学号
sheet.write_merge(row,row+maxx, start_col + 1,start_col + 1, id[1],stylel) # 姓名
sheet.write_merge(row,row+maxx, start_col + 2,start_col + 2, id[2],stylel) # 班级
for i in range(lenn):
if su[i]==0: continue
sheet.write(row+maxx, start_col + 3 + i, "{:.1f}".format(su[i]),style2)
sheet.write_merge(row , row+maxx,start_col + 3 + lenn,start_col + 3 + lenn, stu_sal,style3)
if id[4]=='1':
sheet_time.write(row_time, start_col, id[0],stylej) # 学号
sheet_time.write(row_time, start_col + 1, id[1],stylej) # 姓名
sheet_time.write(row_time, start_col + 2, id[2],stylej) # 班级
sheet_time.write(row_time, start_col + 3, stu_sal,stylej)
sheet_time.write(row_time, start_col + 4, stu_sal,stylej)
row_time+=1
else:
sheet_sal.write(row_sal, start_col, id[0],stylej) # 学号
sheet_sal.write(row_sal, start_col + 1, id[1],stylej) # 姓名
sheet_sal.write(row_sal, start_col + 2, id[2],stylej) # 班级
sheet_sal.write(row_sal, start_col + 3, stu_sal,stylej)
sheet_sal.write(row_sal, start_col + 4, stu_sal*cfg.hour_earn,stylej)
row_sal+=1
return row+maxx+1
def transDate(date):
return date.replace('-','~').replace('.','-')
# 根据一组id信息读取文件
def read_xls(xls,ids):
global row_sal,row_time
row_sal=cfg.startrow
row_time=cfg.startrow
xls_path = os.path.join(cfg.root, "员工刷卡记录表")
wrbook = xlwt.Workbook(encoding='utf-8') # 新建工作簿
sheet1 = wrbook.add_sheet("考勤统计") # 新建sheet
sheet_sal = wrbook.add_sheet("工资") # 新建sheet
sheet_time = wrbook.add_sheet("工时") # 新建sheet
wr_row = cfg.startcol
wr_row=wtfile(sheet1,sheet_sal,sheet_time,wr_row,0,0, header=True)
for id in ids[0]:
ls=[]
for xl in xls:
path,d0,l0=xl
path_abs=os.path.join(xls_path, path)
index0=lenDate(addyear(path.split('-')[0]),d0)
# filename是文件的路径名称
workbook = xlrd.open_workbook(filename=path_abs)
# 获取第一个sheet表格
table = workbook.sheets()[0]
consistent(table,path)
ind = searchid(table, id[3])
if ind!=-1:
l.debug(id[1]+"的信息被发现在 "+path+" 的第"+str(ind)+"行")
table_list=nextl(table,ind,index0,index0+l0)
if id[5]==0 and isEmpty(table_list): # 并没有手动输入工号且根据学号查找的结果为空
tmp=extname(table,searchname(table, id[1]),index0,index0+l0)
if tmp!=-1:
l.warning(id[1]+"的学号检索结果为空(有可能该学生在被检索时间段内未值班),但根据其姓名在文件另一处检索到可用信息")
table_list=tmp
ls.append([table_list,lenDate(addyear(conf['Date'].split('-')[0]),d0)])
else:
if id[5]==1: # 手动设置了工号
l.error(id[1] + "的工号在 " + path + " 中未被查找到,请确认您输入的工号是否准确无误!")
sys.exit(0)
else:
tmp = extname(table, searchname(table, id[1]), index0, index0 + l0)
if tmp != -1:
l.debug(id[1] + "的工号未出现在文件中,但根据其姓名在文件另一处检索到可用信息")
table_list = tmp
ls.append([table_list,lenDate(addyear(conf['Date'].split('-')[0]),d0)])
else:
# print(id[1] + "的信息在 " + path + " 中未被找到")
try:raise Exception(id[1] + "的信息在 " + path + " 中未被找到")
except:
l.error(id[1] + "的信息在 " + path + " 中未被找到或者出现了重名,您最好自己动手设置工号")
sys.exit(0)
wr_row = wtfile(sheet1,sheet_sal,sheet_time, wr_row, id, ls)
da=addyear(conf['Date'],nyear())
da=transDate(da)
outfilename=ids[1]+'_'+da+'_'+cfg.outFile
wrbook.save(mkd(cfg.root,cfg.outPath,outfilename)) # 保存
def rd_all(xls,allid):
for ids in allid:
if conf['isSimple']in ['false','f','F']: read_xls(xls, ids)
elif conf['isSimple'] in ['true','t','T']:simple(ids)
else:
try:raise Exception('你输入了错误的isSimple指令')
except:
l.error('你输入了错误的isSimple指令')
sys.exit(0)
l.debug('So it's all done')
if __name__=='__main__':
rd_all(hitxls,studID)
打包
打包成exe文件需要pyinstaller这个包,首先安装pyinstaller包
pip install pyinstaller
打包成exe文件的命令格式为:
Pyinstaller -F -w -i [图片名].ico [脚本名].py
在cop.py根目录下执行如下命令即可完成打包
Pyinstaller -F -w -idemo.ico cop.py使用 预览
首先我们先看一下程序执行的效果。
输出文件有3个sheet:考勤统计、工资和工时;考勤统计sheet用于日常每周的统计,工资sheet和工时sheet用于每月中旬上报工时
考勤统计sheet:
笔者在事务大厅当过一段时间助管(尽管现在不是了),熟悉事务大厅的考勤规则。程序对于多数正常的考勤时间段均可正常处理,但对于少许非正常考勤,程序也爱莫能助了,例如上图中右上角的无下班考勤,程序会将该时间段的有效考勤记为0。当然,这对于checkAttendance.exe来说并非不可接受的错误,此时只需要一个简单的simple操作就可以完美解决这个问题(后文会详细解释)。
工资sheet和工时sheet与上报工资时的表格式完全一致,你无需做任何修改,简单地拷贝过去即可。
工资sheet
工时sheet
你需要在工作目录下新建两个文件夹,分别命名为员工刷卡记录表和in,员工刷卡记录表文件夹下存放一个或多个类似于2.28-3.4员工刷卡记录表这样格式的文件,in存放configs.txt和StudentIDx.xlsx,最后,你需要将checkAttendance.exe放在员工刷卡记录表和in的同级目录下。
组织格式如下:
-checkAttendance.exe -员工刷卡记录表 -2.28-3.4员工刷卡记录表 -3.5-3.14员工刷卡记录表 -...... -in -configs.txt -StudentID1.xlsx -StudentID2.xlsx -......
员工刷卡记录表无需输入密码取消保护,这不会对读取文件产生任何影响。此外,请不要在两个文件中保存重复的日期,例如2.28-3.2员工刷卡记录表和3.2-3.4员工刷卡记录表由于有重复日期3.2,这会产生二义性,程序会返回给你一个异常信息并正常退出。
configs.txtconfigs.txt文件中包含读入的Date信息和isSimple信息
this is a demo! “begin:: ”之前的任何语句都被视为注释 “;”之后的语句会被视为注释 begin:: ; 开始 Date:2.28-4.30 ; 要检索的时间段 ,2021.11.29-2021.12.5这样的输入也是合法的 isSimple:false ;true(T/t)或false(F/f) end!!! ;结束 “end!!! ”之后的任何语句都被视为注释
注意:读取信息以begin::开始,以end!!!结束。在begin::之后end!!!之前的所有行均被视为有效信息,所以请不要在有效字段输入空行。
Date就是要检索的时间段。
isSimple和输出文件相关,这里先做一下简单解释:
isSimple为false时,程序将执行完整的操作,根据Date和员工刷卡记录表目录下的所有文件进行检索,统计好的信息放在输出文件中。
isSimple置为true,再运行程序会进行简单计算(不需要原始数据的simple操作)。当你发现输出文件中的某些计算信息未如你所料时,例如下图中蓝框的最后一个cell,对于12:28-这样的单边时间段,程序会将有效时间记为0h,但是该学生给你说了她下班忘记打卡了,你只需要在紫色数据同行的任意一个单元格输入你想添加的工时(或者直接改写紫色数据),此时将isSimple改为true,则程序会执行simple操作(简单的紫色数据相加,返回给红色数据,同时更新工资sheet和工时sheet)。
此外,如果你想表示false,F和f也是有效的;如果你想表示true,T和t也是有效的。
StudentID1.xlsx文件内容如下所示
工号和工资/工时为可选项,可以不写,默认工号为学号后八位,默认为工资(0),即空与0是等效的。如果所有学生都选择工资,你大可不必在最后一列输入很多个0,因为无输入和0是等效的,仅当有学生选择工时时,你可以在个别学生的对应栏输入1。
当工号一栏有输入时,程序会唯一的根据工号进行二分查找,如果检索不到对应工号,程序会返回一个异常信息。
当工号一栏无输入时,程序会根据默认工号(学号后8位)进行二分查找,如果检索不到默认工号,则继续根据姓名进行查找。
一个比较理想的输入是这样的:
学号、姓名和班级必须输入,工号和工资/工时为可选项。仅当有学生的工号不是默认工号,并且该学生的姓名不具有唯一性(重名)时,你需要手动输入工号。仅当有学生选择工时而非工资时,你需要在最后一列的相应位置输入1。
注意:目录栏中的‘’学号‘’、‘’姓名‘’、‘’班级‘’等信息并不是必须输入,这只是为了人类方便阅读,事实上,目录栏的输入与否不会对程序产生任何影响。
输出文件夹如果你的所有输入都合法,此时双击运行checkAttendance.exe,程序会自动运行,根目录下会多出两个文件夹out和log
组织格式如下:
-checkAttendance.exe -员工刷卡记录表 -2.28-3.4员工刷卡记录表 -3.5-3.14员工刷卡记录表 -...... -in -configs.txt -StudentID1.xlsx -StudentID2.xlsx -...... -out -ID1_2021-11-29~2021-12-5_考勤统计表.xls -ID2_2021-11-29~2021-12-5_考勤统计表.xls -...... -log -202204182008.txt -202204182114.txt -......考勤统计表
ID1_2021-11-29~2021-12-5_考勤统计表.xls中有三个sheet:考勤统计、工资和工时
考勤统计sheet中内容如下所示:
表中紫色数字那一行助管们可以自行更改(即便那个cell中没有任何数据)。如果isSimple=true,程序运行时只会将紫色数据那一行的所有数字相加并返回给红色的cell。
工资sheet中保存所有选择工资的学生的总计工时和工资,如果所有学生选择了工时,那该sheet中只有表头而没有任何其他信息。
工时sheet中保存所有选择工时的学生的总计工时,如果所有学生选择了工姿,那该sheet中只有表头而没有任何其他信息。
日志文件日志文件202204182008.txt
2022-04-19 00:01:15,101 - dataloader.py[line:166] - DEBUG: 程序将自动统计 2022.2.28-2022.3.13 共 14 天的考勤 2022-04-19 00:01:15,248 - cop.py[line:334] - DEBUG: xx的信息被发现在 2.28-3.6员工刷卡记录表.xls 的第698行 2022-04-19 00:01:15,296 - cop.py[line:334] - DEBUG: xx的信息被发现在 3.7-3.13员工刷卡记录表.xls 的第713行 2022-04-19 00:01:15,324 - cop.py[line:334] - DEBUG: xx的信息被发现在 2.28-3.6员工刷卡记录表.xls 的第317行 2022-04-19 00:01:15,325 - cop.py[line:339] - DEBUG: xx的学号检索结果为空(有可能该学生在被检索时间段内未值班),但根据其姓名在文件另一处检索到可用信息 2022-04-19 00:01:15,372 - cop.py[line:334] - DEBUG: xx的信息被发现在 3.7-3.13员工刷卡记录表.xls 的第320行 2022-04-19 00:01:15,373 - cop.py[line:339] - DEBUG: xx的学号检索结果为空(有可能该学生在被检索时间段内未值班),但根据其姓名在文件另一处检索到可用信息 2022-04-19 00:01:15,452 - cop.py[line:334] - DEBUG: xx的信息被发现在 2.28-3.6员工刷卡记录表.xls 的第812行 2022-04-19 00:01:15,505 - cop.py[line:334] - DEBUG: xx的信息被发现在 3.7-3.13员工刷卡记录表.xls 的第828行 2022-04-19 00:01:15,558 - cop.py[line:334] - DEBUG: xx的信息被发现在 2.28-3.6员工刷卡记录表.xls 的第1132行 2022-04-19 00:01:15,608 - cop.py[line:334] - DEBUG: xx的信息被发现在 3.7-3.13员工刷卡记录表.xls 的第1151行 2022-04-19 00:01:15,635 - cop.py[line:334] - DEBUG: xx的信息被发现在 2.28-3.6员工刷卡记录表.xls 的第669行 2022-04-19 00:01:15,680 - cop.py[line:334] - DEBUG: xx的信息被发现在 3.7-3.13员工刷卡记录表.xls 的第684行 2022-04-19 00:01:15,687 - cop.py[line:370] - DEBUG: So it's all done
程序运行过程如果没有出现任何错误,最后会输出So it’s all done,否则,相应的错误信息会保存在日志文件中。
笔者在程序中对于大多数异常进行了处理,所以有可能程序执行出错了但没有在电脑界面给你任何信息(那是因为程序代你处理了),所以你最好养成查看日志的好习惯,以判断正常退出或异常退出。
多数错误是由输入日期的二义性和学生检索信息的二义性引起的,将输入日期改为year/month/day表示法 和 手动输入学生工号可以解决大多数问题。当然,笔者在程序中这种二义性问题进行了充分的考虑并修正,理论上,你不必手动输入year和学生工号,程序也能返回给你正确的结果(如果没有正确返回,当我没说)。
傻瓜级使用教程(1)在工作目录下新建员工刷卡记录表和in两个文件夹,将所有员工刷卡记录表拷贝至员工刷卡记录表目录下,并在in文件夹下新建如下configs.txt和StudentID1.xlsx。如下所示:
组织格式如下:
-checkAttendance.exe -员工刷卡记录表 -2.28-3.4员工刷卡记录表 -...... -in -configs.txt -StudentID1.xlsx
(2)将下列内容拷贝至configs.txt中:
begin:: Date:2.28-4.30 ; 要检索的时间段 isSimple:false end!!!
修改Date为你所要检索的时间段。
(3)StudentID1.xlsx文件中添加如下内容:(学号、姓名、班级、工号、工资/工时)
学号、姓名和班级必须输入,工号和工资/工时为可选项。仅当有学生的工号不是默认工号,并且该学生的姓名不具有唯一性(重名)时,你需要手动输入工号。仅当有学生选择工时而非工资时,你需要在最后一列的相应位置输入1。
(4)将checkAttendance.exe放在工作目录下,双击exe文件。工作目录下会多出两个文件夹out和log。输出结果在out文件夹中。
如果执行出错,请转至“使用”章节进行阅读,它会解决你大部分问题。
最后所有源代码均已上传至我的github账户,欢迎批评指正。
由于exe文件过大(约两百多M),我上传至了百度网盘。链接如下:
链接:https://pan.baidu.com/s/1MN4Otra06LsAXv2_TMiDCQ 提取码:yl4p
如果此篇文章对您有用的话,希望您点个赞哦~



