楔子

dateutil是一个处理时间的库,可以非常智能的将字符串解析成时间类型,并且这也是pandas所依赖的库。下面来看一下用法:

parser

dateutil下面有一个parser模块,它是专门用来将字符串解析为时间类型的。

from dateutil import parser

直接导入即可,然后调用内部的parse方法。

>>> parser.parse("2018-3-25")
datetime.datetime(2018, 3, 25, 0, 0)
>>> parser.parse("2018-03-25")
datetime.datetime(2018, 3, 25, 0, 0)
>>> parser.parse("2018/3/25")
datetime.datetime(2018, 3, 25, 0, 0)
>>> parser.parse("2018/03/25")
datetime.datetime(2018, 3, 25, 0, 0)

我们看到还是很智能的,可以使用/或者-进行分隔。

# 即使没有分隔符也是可以的,但是必须是yyyymmdd这种形式,比如月份,如果是3月,那么要写03
# 因为在没有分隔符的情况,写成2018325的话,那么会把2018325都当成年来解析
>>> parser.parse("20180325")
datetime.datetime(2018, 3, 25, 0, 0) # 如果只有两部分,那么会自动把前面的当成月、后面当成日(但是有特例,后面说)
# 没指定的部分,默认为当前日期对应的部分
>>> parser.parse("03-25")
datetime.datetime(2020, 3, 25, 0, 0)
# 我们看到月份超过了12,所以报错了
>>> parser.parse("13-5")
ValueError: month must be in 1..12
# 但是如果月份超过了31,那么就不再是月份了,而是会被当成年来解析,那么同理后面的就会变成月。
# 也就是前面的当成是年、后面的当成是月
# 当前是两千多年,所以解释成2032年,而5则解释成5月。而"日"则是25,因为它没有指定,所以和当前日期保持一致
>>> parser.parse("32-5")
datetime.datetime(2032, 5, 25, 0, 0)
# 但是超过70,那么会被解释成1970年,这与unix诞生时间有关
>>> parser.parse("70-5")
datetime.datetime(1970, 5, 25, 0, 0)
# 如果超过了100,那么就是其本身
>>> parser.parse("100-5")
datetime.datetime(100, 5, 25, 0, 0) >>> parser.parse("1225")
# 由于没有分割符那么1225整体会被当成是年来解释,然后月和日则和当前日期保持一致,3月25日
# 所以如果只有两部分,最好指定分隔符。
# 不指定分隔符的情况最好只用于yyyymmdd这种形式,如果只有两部分、还不指定分隔符的话,范围太广了
datetime.datetime(1225, 3, 25, 0, 0) # 213一样会被解释成年,其它部分和当前日期保持一致
>>> parser.parse("213")
datetime.datetime(213, 3, 25, 0, 0) # 但如果小于31,那么会被解释成日
>>> parser.parse("32")
datetime.datetime(2032, 3, 25, 0, 0)
# 这里被解释成"日"了,当然前提是当前月份有31天
>>> parser.parse("31")
datetime.datetime(2020, 3, 31, 0, 0)
>>> parser.parse("06")
datetime.datetime(2020, 3, 6, 0, 0) # 所以dateutil给我们做了很多的处理,但老实说我们基本不会处理像"31"、"1225"这种格式的字符串,因为它太模糊了,所以即便是这种数据,起码也要有分隔符
# 如果有分隔符,那么处理起来会非常简单
# 还是那句话,如果没有分隔符,还要dateutil来处理的话,那么最好是yyyymmdd的格式,其它情况要有分隔符:/或者-都可以

如果不是年月日的顺序呢?

# 首先看一下这种情况,如果都只有两位,那么会自动把最后一部分当成年来解析
# 前面的两部分则是月和日,也就是"月日年"
>>> parser.parse("12/25/18")
datetime.datetime(2018, 12, 25, 0, 0)
>>> parser.parse("12/10/11")
datetime.datetime(2011, 12, 10, 0, 0) # 但18显然超过最大月份12,那么之前的月日年、则会变成日月年
>>> parser.parse("18/10/11")
datetime.datetime(2011, 10, 18, 0, 0)
# 同理年份写全也是一样的
>>> parser.parse("12/10/2011")
datetime.datetime(2011, 12, 10, 0, 0)
>>> parser.parse("18/10/2011")
datetime.datetime(2011, 10, 18, 0, 0)
# 但是年份只能位于开头或者结尾,不能在中间
# 如果年在结尾,那么会按照月日年来解析,但是月份大于12,那么会按照日月年来解析
# 如果年在开头,那么只会按照年月日来解析,不存在年日月这一说

此外dateutil还可以识别英文模式的字符串

>>> parser.parse("Mar 15 2018")
datetime.datetime(2018, 3, 15, 0, 0)

rrule

rrule是用于生成多个连续的日期,如果你知道pandas的data_range,那么这个很好理解。

>>> from dateutil import rrule
>>> list(rrule.rrule(freq=rrule.DAILY, dtstart=parser.parse("2018-1-1"), until=parser.parse("2018-1-5")))
[datetime.datetime(2018, 1, 1, 0, 0),
datetime.datetime(2018, 1, 2, 0, 0),
datetime.datetime(2018, 1, 3, 0, 0),
datetime.datetime(2018, 1, 4, 0, 0),
datetime.datetime(2018, 1, 5, 0, 0)]
  • freq:YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY,年、月、星期、天、小时、分、秒,即间隔,我们上面的例子是DAILY,每一天生成一个
  • dtstart:起始时间
  • until:结束时间
>>> list(rrule.rrule(freq=rrule.DAILY, count=3, dtstart=parser.parse("2018-1-1"), until=parser.parse("2018-1-5")))
[datetime.datetime(2018, 1, 1, 0, 0),
datetime.datetime(2018, 1, 2, 0, 0),
datetime.datetime(2018, 1, 3, 0, 0),
]
  • count:只生成多少个
>>> list(rrule.rrule(freq=rrule.DAILY, interval=2, dtstart=parser.parse("2018-1-1"), until=parser.parse("2018-1-5")))
[datetime.datetime(2018, 1, 1, 0, 0),
datetime.datetime(2018, 1, 3, 0, 0),
datetime.datetime(2018, 1, 5, 0, 0)]
  • interval:间隔,freq指的是天,加上interval=2,所以就是每隔两天
>>> list(rrule.rrule(freq=rrule.DAILY, byweekday=(rrule.MO, rrule.SU), dtstart=parser.parse("2018-1-1"), until=parser.parse("2018-1-5")))
[datetime.datetime(2018, 1, 1, 0, 0)]
  • byweekday=(rrule.MO, rrule.SU),表示只保留周一和周日

rrule还可以计算两个日期之间查了多少天、多少月、多少年等等,默认的timedelta则最大只能计算到天。

>>> rrule.rrule(freq=rrule.DAILY, dtstart=parser.parse("2018-1-1"), until=parser.parse("2019-1-5")).count()
370
>>> rrule.rrule(freq=rrule.MONTHLY, dtstart=parser.parse("2018-1-1"), until=parser.parse("2019-1-5")).count()
13
>>> rrule.rrule(freq=rrule.YEARLY, dtstart=parser.parse("2018-1-1"), until=parser.parse("2019-1-5")).count()
2

我们看到还是使用rrule.rrule,如果调用count方法就会计算差值,至于计算什么差值,则取决于freq。

但是它的计算方式要注意,比如计算年,就先按照年来减,然后看月,如果until的"月和日"组合起来大于等于dtstart的"月和日",那么年会加1

>>> rrule.rrule(freq=rrule.YEARLY, dtstart=parser.parse("2018-1-1"), until=parser.parse("2019-1-1")).count()
2

明明都是1月1号,但是两者差了两年。对于月也是同理,先减去月,然后看日,如果until的日大于等于dtstart的日,那么月也会加1

>>> rrule.rrule(freq=rrule.MONTHLY, dtstart=parser.parse("2018-1-1"), until=parser.parse("2018-3-1")).count()
3
>>> rrule.rrule(freq=rrule.MONTHLY, dtstart=parser.parse("2018-1-2"), until=parser.parse("2018-3-1")).count()
2

所以不单单是减完就结束了,还会进行一次判断(减的是年就判断"月和日"、减的是月就判断日),如果until不小于dtstart,那么上一步减完的结果会加1。

但是说实话,这种做法个人觉得很不友好,所以我们计算两个日期之间的差值一般不会使用rrule,rrule主要还是用于生成多个连续日期。

relativedelta

个人非常推荐的一个方法,我们来看一下用法。

from dateutil.relativedelta import relativedelta
from datetime import datetime # 可以自动计算两个日期之间的差值
print(
relativedelta(datetime(2018, 1, 5), datetime(2018, 1, 1))
) # relativedelta(days=+4) print(
relativedelta(datetime(2018, 2, 5), datetime(2018, 1, 9))
) # relativedelta(days=+27) print(
relativedelta(datetime(2018, 2, 5), datetime(2018, 1, 1))
) # relativedelta(months=+1, days=+4) print(
relativedelta(datetime(2019, 2, 5), datetime(2018, 1, 1))
) # relativedelta(years=+1, months=+1, days=+4)

输入两个日期,然后会计算两个日期之间的差值,然后可以获取如下属性,:

  • years: 计算差了多少年
  • months:计算差了多少月
  • days:计算差了多少天
  • hours:计算差了多少小时
  • minutes:计算差了多少分钟
  • seconds:计算差了多少秒
  • microseconds:计算差了多少毫秒

注意:上面的指的是两个日期对应部分的差值,比如:2018-3-1和2017-1-1,之间差了两个月,并不是12+2=14,它获取的是对应部分的差值

from dateutil.relativedelta import relativedelta
from datetime import datetime dt1 = datetime(2018, 12, 11, 19, 15, 25)
dt2 = datetime(2017, 8, 3, 17, 24, 51) diff = relativedelta(dt1, dt2)
print(diff) # relativedelta(years=+1, months=+4, days=+8, hours=+1, minutes=+50, seconds=+34) print(diff.years) # 1
print(diff.months) # 4
print(diff.days) # 8
print(diff.hours) # 1
print(diff.minutes) # 50
print(diff.seconds) # 34

我们看到上面计算的结果不是我们期待的,我们希望在计算差了多少个月的时候,是希望把年算进去的,那么怎么办呢?使用pandas

import pandas as pd

for freq in ("Y", "M", "W", "D", "H", "T", "S"):
"""
Y: 年
M: 月
W: 星期
D: 天
H: 时
T: 分
S: 秒
"""
dt1 = pd.Period("2018-11-12 12:11:10", freq)
dt2 = pd.Period("2017-11-12 11:18:35", freq)
print((dt1 - dt2).n)
"""
1
12
53
365
8761
525653
31539155
"""

这一般是我们期望的结果,计算月的时候,会将差的年份乘上12再和差的月份相加,同理计算天的时候,会将年和月算进去。计算小时,则是将年、月、日都算进去。

因此计算两个日期之间的差值的时候,如果是精确到天,那么我们可以直接将日期相减,得到timedelta。但是精确到年和月,那么我们知道可以使用rrule,但是它涉及到一个问题,就是我们说过的: 2018-2-2和2018-1-1应该差了一个月零一天,但是得到结果是两个月,而relativedelta则是计算每个单独的部分之间的差值,所以我个人推荐使用pandas

那relativedelta都用在什么地方呢?如下:

from dateutil.relativedelta import relativedelta
from datetime import datetime, timedelta dt1 = datetime(2018, 12, 11, 19, 15, 25) # 给dt1加上5个月,变成了19年5五月
diff = relativedelta(months=5)
print(diff + dt1) # 2019-05-11 19:15:25 # 给dt1加上2年15天
diff = relativedelta(years=2, days=15)
print(diff + dt1) # 2020-12-26 19:15:25 # 给dt1加上14个小时、38分、12秒
diff = relativedelta(hours=14, minutes=38, seconds=12)
print(dt1 + diff) # 2018-12-12 09:53:37 # 给dt1加上3星期
diff = relativedelta(weeks=3)
print(dt1 + diff) # 2019-01-01 19:15:25
"""
所以我们可以给指定部分加上或减去任意的时间间隔
当然datetime和timedelta也可以相加减,但是timedelta无法指定月和年
""" # 另外我们指定间隔的时候是可以无视范围的,比如一个月最多有31天,但是我们指定45也是可以的
# 比如:dt1是2018年12月11,那么加上45天。会先拿出21天变成2019年1月1号,因为12月有31天
# 然后剩余24天,2019-1-1再加上24天,所以是2019年1月25号
diff = relativedelta(days=45)
print(dt1 + diff) # 2019-01-25 19:15:25

所以relativedelta最大的用处就是能够给一个日期加上指定的时间间隔。

05-07 15:00