概述
说明
python的时间模块实在有点让人晕。主要的有两个,一个是time模块,一个是datetime模块。这两个模块的有部分功能还是重叠的,实在不想忍了- - !
我打算重新造一个轮子,只要效率还可以,然后直观好用就行了。
实现的功能有以下几块:
- 字符串和时间戳的互转
- 实现月偏移:月偏移从计算上是不精确的,尽量保证号相同。(例如4.1到5.1,偏移一个月是30天;但5.1到6.1偏移一个月是31天)。不过通常来说,如果要按做偏移尽量还是选1号吧,毕竟每个月都有(月末可就不一定哪天了)。
- 实现秒偏移(周、天、时、分):精确偏移,这个是要换算成时间戳,那就随便整了。
- 其他(没想好,需要的时候再加吧)
时间
时间要同时服务好机器和人类。对于机器而言,就是时间戳;对人而言就是各种形式的字符,这里规定标准的字符格式是yyyy-mm-dd (hh:mm:ss),时分秒可以省略。
要能够做一个沟通人类和计算的时间,就要先按照人类的历法来整理,也就是所谓的万年历。
首先分平年和闰年,闰年的话2月多一天,有2月29日。
其次就是大小月,这个小时候掰着手指头记过(一月大,二月小,三月大…)
万年历, 真的按一万年来规划的话,一共有10000个年份(嗯,废话);如果按照年月来规划的话,一共有12万个年月(嗯,又一句废话)。
我的想法
虽然时间推算的方法是固定的,但是如果要大批量计算(例如一百万行),那么每次都重新根据规则推算是挺浪费计算资源,也挺慢的。
所以,先设定一个基准年份,和可能用到的最大推算年份(例如基准年之后的一万年),把对应的参数一次性计算好放在内存里,然后就近比较偏移就好了。
先看结果
先说结论 百万级别的数据转化要30秒左右(单进程):
- 整体转换效率比pd.to_datetime要快一倍(当然to_datetime更复杂,带了很多方法)
- 万年历和百年历效率查了100倍(看来keep大的内存还是会吃力的)
- 随机抽查,结果是对的。pd.to_datetime直接转为数值是格林尼治时间,差了8小时。
- 如果希望进一步提升速度的化,可以缩小到类似二十年之类的。(但是有可能要改代码,算了,真要快我就用并行算就好了)
...: df_test = pd.concat(df_sub*10, ignore_index=True)
...: df_test.shape
...: t1 = time.time()
...: df_test['dt_str'] = df_test['xx时间'].apply(str)
...: t2 = time.time()
...: print('>>> 字符串化时间', t2- t1)
...: df_test['dt_ts1'] = df_test['dt_str'].apply(bb2.dt2seconds)
...: t3 = time.time()
...: print('>>> 百年历时间', t3 - t2)
...:
...: df_test['dt_ts2'] = df_test['dt_str'].apply(bb1.dt2seconds)
...: t4 = time.time()
...: print('>>>万年历时间', t4- t3)
...: df_test['dt_ts_pd'] = df_test['dt_str'].apply(pd.to_datetime)
...: t5 = time.time()
...: print('>>> pd datetime 时间', t5- t4)
...: df_test['dt_ts2'] = df_test['dt_str'].apply(bb3.dt2seconds)
...: t6 = time.time()
...: print('>>>六十年历时间', t6- t5)
...:
...:
>>> 字符串化时间 0.05991196632385254
>>> 百年历时间 0.31140995025634766
>>>万年历时间 34.879563093185425
>>> pd datetime 时间 0.6795589923858643
>>>六十年历时间 0.2773611545562744
代码
下面是代码,里面有了一些注释,我再捋一捋。@staticmethod可以认为为了代码干净,把相关的函数绑在一个类下面。此外还有@classmethod和@property, classmethod和staticemethod类似,但还可以访问类通用属性。property可以用来做一些类属性的限制(例如密码属性不能直接访问,要通过property函数)
class ATimer():
def __init__(self, base_year=1970, next_years=10000,time_zone=8):
self.base_year = base_year
self.time_zone = time_zone
self.year_bias_days_dict, self.year_bias_seconds_dict ,self.year_list = ATimer.gen_years_base(base_year, next_years=next_years, time_zone=time_zone)
self.a_dict_days, self.b_dict_days, self.a_dict_seconds, self.b_dict_seconds = ATimer.ab_calendar()
self.year_mon_days_dict, self.year_mon_seconds_dict = ATimer.get_year_mon_base(
self.year_bias_days_dict,
self.a_dict_days,
self.b_dict_days,
time_zone=time_zone)
# 判断是否为闰年
@staticmethod
def is_ryear(some_year):
some_year = int(some_year)
return True if some_year % 400 == 0 or (some_year % 4 == 0 and some_year % 100 != 0) else False
# 判断年.月的天数
@staticmethod
def yymon_days(yy, mon):
list31 = [1, 3, 5, 7, 8, 10, 12]
list30 = [4, 6, 9, 11]
if mon in list31:
return 31
elif mon in list30:
return 30
else:
if ATimer.is_ryear(yy):
return 29
else:
return 28
# 计算闰年的天数
@staticmethod
def ryear_days(yy):
if ATimer.is_ryear(yy):
return 366
else:
return 365
# 字符日期和数值的映射
@staticmethod
def mon_next(someday, next = 0):
someday = str(someday)
if len(someday) == 1:
someday = '0'+someday
elif len(someday) ==2:
pass
else:
someday = someday[:2]
mon_list = [str(x) for x in range(1,13)]
mon_list1 = [x if len(x) == 2 else '0'+x for x in mon_list]
try:
current_idx = mon_list1.index(someday)
except:
current_idx = 0
target_idx = current_idx + next
if target_idx <0:
target_idx = 11
elif target_idx >11:
target_idx = 0
else:
pass
return mon_list1[target_idx]
# 获取列表的下一个值
@staticmethod
def get_list_next(current_value, some_list, next=0):
some_list = list(some_list)
try:
current_idx = some_list.index(current_value)
except:
current_idx = None
if current_idx is not None :
if (current_idx + next >=len(some_list)) or (current_idx + next <0):
next_value = None
else:
next_value = some_list[current_idx + next]
else:
next_value = None
return next_value
# 字符日期和数值的映射,对应月末是28, 29, 30, 31四种类型的
@staticmethod
def day_next(someday, next=0, mtype='A'):
# 31 大约
if mtype =='A':
max_day = 32
# 30 小月
elif mtype =='B':
max_day = 31
# 29 二月闰月
elif mtype =='C':
max_day = 30
# 28 二月平月
else:
max_day = 29
someday = str(someday)
if len(someday) == 1:
someday = '0'+someday
elif len(someday) == 2:
pass
else:
someday = someday[:2]
day_list = [str(x) for x in range(1, max_day)]
day_list1 = [x if len(x) == 2 else '0'+x for x in day_list]
try:
current_idx = day_list1.index(someday)
except:
current_idx = 0
target_idx = current_idx + next
if target_idx < 0:
target_idx = max_day-2
elif target_idx > max_day-2:
target_idx = 0
else:
pass
return day_list1[target_idx]
# 计算每个年份的基准偏移(1月1号0时0分0秒和选定的base_year的偏移),直接枚举一万年的偏移方便之后计算
@staticmethod
def gen_years_base(base_year, next_years = 10000, time_zone = 8):
s_ = pd.Series(range(base_year, base_year+next_years + 1))
s1_ = s_.apply(ATimer.ryear_days).shift().fillna(0).cumsum()
# year_bias_days_dict
year_bias_days_dict = cl.OrderedDict(zip(s_.values, s1_.values))
# year_bias_seconds_dict
year_bias_seconds_dict = cl.OrderedDict(zip(s_.values, (s1_.values)*3600*24 - 3600*time_zone))
# 年列表
year_list = list(year_bias_seconds_dict.keys())
return year_bias_days_dict, year_bias_seconds_dict, year_list
# 平闰年枚举(平年日历与闰年日历),返回两个OrderedDict
# a平年, b闰年
@staticmethod
def ab_calendar():
list31 = [1, 3, 5, 7, 8, 10, 12]
list30 = [4, 6, 9, 11]
a_dict = cl.OrderedDict()
a_dict_seconds = cl.OrderedDict()
acnt = 0
for m in list(range(1,13)):
if m in list31:
# 1~31
mday_list = list(range(1, 32))
elif m in list30:
# 1~30
mday_list = list(range(1,31))
else:
# 1~28
mday_list = list(range(1,29))
for md in mday_list:
strm = str(m)
strmd = str(md)
strm1 = strm if len(strm) ==2 else '0'+strm
strmd1 = strmd if len(strmd) == 2 else '0' + strmd
a_dict[strm1 + '-'+ strmd1] = acnt
a_dict_seconds[strm1 + '-' + strmd1] = acnt*3600*24
acnt +=1
b_dict = cl.OrderedDict()
b_dict_seconds = cl.OrderedDict()
acnt = 0
for m in list(range(1, 13)):
if m in list31:
# 1~31
mday_list = list(range(1, 32))
elif m in list30:
# 1~30
mday_list = list(range(1, 31))
else:
# 1~29
mday_list = list(range(1, 30))
for md in mday_list:
strm = str(m)
strmd = str(md)
strm1 = strm if len(strm) == 2 else '0'+strm
strmd1 = strmd if len(strmd) == 2 else '0' + strmd
b_dict[strm1 + '-'+ strmd1] = acnt
b_dict_seconds[strm1 + '-' + strmd1] = acnt*3600*24
acnt += 1
return a_dict, b_dict, a_dict_seconds, b_dict_seconds
# 正则判定 可转时间字符:接受的标准格式是 yyyy-mm-dd (HH:MM:SS)或者是yyyy/mm/dd (HH:MM:SS)
@staticmethod
def is_retime_str(some_str):
# 正则表达式
pass
# 年月基准:使用年偏移,根据平闰年获取年日历,取每个月的1号0时0分0秒作为月偏移
# year_bias_days_dict 是按年的天偏移,a_dict是平年日历按天偏移, b_dict是闰年日历按天偏移
@staticmethod
def get_year_mon_base(year_bias_days_dict, a_dict, b_dict, time_zone=8):
year_mon_days_dict = {}
year_mon_seconds_dict = {}
for yyyy in year_bias_days_dict:
yyyy1 = str(yyyy) + '-'
if ATimer.is_ryear(yyyy):
current_year_dict = b_dict
else:
current_year_dict = a_dict
# 获取12个月的1号
mon_list = [x for x in current_year_dict.keys() if x.endswith('01')]
for mon in mon_list:
mon1 = mon[:2]
year_mon_days_dict[yyyy1+mon1] = year_bias_days_dict[yyyy] + current_year_dict[mon]
year_mon_seconds_dict[yyyy1+mon1] = year_mon_days_dict[yyyy1+mon1] * 86400 - 3600*time_zone
return year_mon_days_dict, year_mon_seconds_dict
# 将时分秒转为秒
@staticmethod
def HMS2seconds(hms_string):
hh, mm, ss = hms_string.replace(':',':').split(':')
if hh.startswith('0'):
hh1 = int(hh[1:])
else:
hh1 = int(hh)
if mm.startswith('0'):
mm1 = int(mm[1:])
else:
mm1 = int(mm)
if ss.startswith('0'):
ss1 = int(ss[1:])
else:
ss1 = int(ss)
seconds = hh1 * 3600 + mm1 * 60 + ss1
return seconds
# 将小于一天86400的秒转为小时
@staticmethod
def HMS2string(seconds):
hh = str(seconds // 3600)
hh_left_seconds = seconds % 3600
mm = str(hh_left_seconds // 60 )
mm_left_seconds = str(int(mm) % 60)
if len(hh) ==1:
hh1 = '0' + hh
else:
hh1 = hh
if len(mm) == 1:
mm1 = '0' + mm
else:
mm1 = mm
if len(mm_left_seconds) == 1:
mm_left_seconds1 = '0' + mm_left_seconds
else:
mm_left_seconds1 = mm_left_seconds
return ':'.join([hh1, mm1, mm_left_seconds1])
# 将日期转为数值(秒)- 默认样式为yyyy-mm-dd (hh:mm:ss),如果原始数据格式不对可以在之前加格式转换
# year_mon_seconds_dict (使用秒级的)
@staticmethod
def DT2seconds(dt_string, mon_year_base):
string_list = dt_string.strip().split(' ')
if len(string_list) == 2:
dt_part, hms_part = string_list[0], string_list[1]
hms_seconds = ATimer.HMS2seconds(hms_part)
else:
dt_part, hms_part = string_list[0], 0
hms_seconds = 0
# 校验 - 有效的日数加上后不应该超过下个月的时间戳
rpos = dt_part.rfind('-')
yymon = dt_part[:rpos]
yymon_next = ATimer.get_list_next(yymon, mon_year_base.keys(), next=1)
dd = dt_part[rpos+1:]
if dd.startswith('0'):
dd1 = int(dd[1:]) -1
else:
dd1 = int(dd) -1
day_seconds = dd1*86400
day_seconds1 = day_seconds + hms_seconds
# 获取当前月份(1号0时0分0秒)的时间戳(和下个月的)
yymon_ts = mon_year_base[yymon]
yymon_ts1 = mon_year_base[yymon_next]
if yymon_ts + day_seconds1 < yymon_ts1:
res = yymon_ts + day_seconds1
else:
res = None
return res
# 将时间戳转为字符 - 如果需要别的格式另外再加
@staticmethod
def DT2str(dt_seconds, mon_year_base , str_mode = None):
dt_seconds = float(dt_seconds)
base_keys = list(mon_year_base.keys())
base_values = np.array(list(mon_year_base.values())).astype(float)
pos = int((dt_seconds >= base_values).sum() -1 )
val = base_keys[pos]
# 获得的val是当月的基准
# 剩下的就用秒数相加了
left_seconds = int(dt_seconds - mon_year_base[val])
the_day = (left_seconds // 86400 + 1)
if len(str(the_day)) ==1 :
the_day1 = '0' + str(the_day)
else:
the_day1 = str(the_day)
hms_seconds = left_seconds % 86400
# 如果是None就自行推断,如果是1就显示带hms, 否则显示不带hms的
if str_mode is None:
if hms_seconds == 0:
res = val + '-' + the_day1
else:
res = val + '-' + the_day1 + ' ' + ATimer.HMS2string(hms_seconds)
elif str_mode == 1:
res = val + '-' + the_day1 + ' ' + ATimer.HMS2string(hms_seconds)
else:
res = res = val + '-' + the_day1
return res
# 实例方法 - 将字符串转为时间戳 - 时区
def dt2seconds(self, dt_string):
return ATimer.DT2seconds(dt_string, self.year_mon_seconds_dict)
# 实例方法 - 将时间戳转为字符串 + 时区
def dt2str(self, dt_seconds, str_mode=None):
return ATimer.DT2str(dt_seconds, self.year_mon_seconds_dict, str_mode = str_mode)
一些函数的说明,基本上也就是从上到下的操作
序号 | 函数名 | 作用 |
---|---|---|
1 | is_ryear | 判断某年是否是闰年,后面是其他函数的基础方法 |
2 | yymon_days | 判断年月的天数(因为平闰年,年月的天数不同) |
3 | ryear_days | 如果是平年返回355天,如果是闰年返回366天 |
4 | mon_next | 返回下一个月,嗯,这个函数好像没什么用 |
5 | get_list_next | 获取列表的下一个值,这个好用,在推算下一个月的时候用了 |
6 | day_next | 根据不同类型的月份(28,29,30,31) 获取下一天,也没啥用 |
7 | gen_years_base | 根据设定的基准年份,和年份周期,设定对应的时区,然后生成周期中每一年1月1日0时0分对应的日偏移和秒偏移 |
8 | ab_calendar | 生成平闰年的日历,按顺序从1-1列到12-31,其中每一天对应年初始值的偏移天数和秒数 |
9 | is_retime_str | 正则判定一个字符串是否是可解析日期,暂时空着 |
10 | get_year_mon_base | 这个厉害了,根据前面获得的周期年日偏移字典,结合平闰年,生成一个年月的日偏移和秒偏移字典,类似 2010-01, 2010-02 … 这样的,后序进行映射就靠这个了 |
13 | HMS2seconds | 把符合 HH:MM:SS 格式的时间转为秒 |
14 | HMS2string | 把秒转回HH:MM:SS格式,请自行控制秒值不大于86400,不然我也不知道会算出什么,哈哈 |
15 | DT2seconds | 把时间日期字符串转为秒(时间戳) |
16 | DT2str | 把时间戳转回字符日期 |
17 | dt2seconds | 实例方法,使用初始化的值直接转换,将日期转为时间戳 |
18 | dt2str | 实例方法,使用初始化的值直接转换,将时间戳转为日期 |
使用方法:
1. 初始化 | 使用1970年开始60年以内(2030年)的日历
bb3 = ATimer( next_years = 60)
2. 转换 | 把dt_str字段转为时间戳
df_test['dt_ts2'] = df_test['dt_str'].apply(bb3.dt2seconds)
3. 逆转换 | 把时间戳转回来
df_test['dt_ts2_inv'] = df_test['dt_ts2'].apply(bb3.dt2str)
--- 成功
In [327]: df_test[['dt_str','dt_ts2','dt_ts2_inv']].head()
Out[327]:
dt_str dt_ts2 dt_ts2_inv
0 2019-12-14 15:21:00 1.576308e+09 2019-12-14 15:21:21
1 2019-12-14 15:21:00 1.576308e+09 2019-12-14 15:21:21
2 2020-05-26 23:02:00 1.590505e+09 2020-05-26 23:02:02
3 2020-05-26 23:02:00 1.590505e+09 2020-05-26 23:02:02
4 2020-05-30 13:16:00 1.590816e+09 2020-05-30 13:16:16
后续
之后大约会把增补的内容列在这里,但是代码就不再贴了,容易乱。
另外关于是否要重复造轮子的问题,之前一股潮流是不要重复造轮子,个人的感觉是有点过了。是否重复造轮子要看具体使用者的需求,专业运动员的装备和普通人随便玩玩的肯定是不同的。
最后
以上就是俊秀眼神为你收集整理的Python一些可能用的到的函数系列6 自定义时间对象说明时间我的想法先看结果代码后续的全部内容,希望文章能够帮你解决Python一些可能用的到的函数系列6 自定义时间对象说明时间我的想法先看结果代码后续所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复