Python中的时区处理的详解

当一个应用的用户遍布全世界的时候,程序的代码少不了要和时区打交道。服务器端针对用户的定时任务需要定到用户所在时区的时。

在Glow Nurture中,比较典型的一个例子就是:如果用户没有记录服用Prenatal Vitamin,两天后晚上9点给用户发送程序内通知提醒服用Vitamin。 翻译成直白的程序需求就是:获取某用户所在时区某年月日21点对应服务器所在时区的时间戳。

Python提供了什么?

Python 提供了datetime, time, calendar模块,然后感谢Stuart Bishop stuart@stuartbishop.net
, 我们还有pytz可以使用。

由于存在datetime模块,time模块,在datetime模块下又存在datetime类,time类,为避免阅读上的误解,以下说到time, datetime时指模块,datetime.time, datetime.datetime指datetime模块下的time类和datetime类。

datetime模块定义了如下类:

datetime.date     - 理想化的日期对象,假设使用格力高历,有year, month, day三个属性

datetime.time     - 理想化的时间对象,不考虑闰秒(即认为一天总是24*60*60秒),有hour, minute, second, microsecond, tzinfo五个属性

datetime.datetime     - datetime.date和datetime.time的组合

datetime.timedelta     - 后面我们会用到的类,表示两个datetime.date, datetime.time或者datetime.datetime之间的差。

datetime.tzinfo     - 时区信息

*Python 3.2开始提供了datetime.timezone类,不过我们暂时还是使用的2.7,后面代码均以2.7版本测试运行。

time模块提供了各种时间操作转换的方法。

calendar模块则是提供日历相关的方法。

pytz模块,使用Olson TZ Database解决了跨平台的时区计算一致性问题,解决了夏令时带来的计算问题。由于国家和地区可以自己选择时区以及是否使用夏令时,所以pytz模块在有需要的情况下得更新自己的时区以及夏令时相关的信息。比如当前pytz版本的OLSON_VERSON = ‘2013g’, 就是包括了Morocco可以使用夏令时。

如何正确为你所用

不是题外话的题外话,客户端必须正确收集用户的timezone信息。比较常见的一个错误是,保存用户所在时区的偏移值。比如对于中国的时区,保存了+8。这里其实丢失了用户所在的地区(同样的时间偏移,可能对应多个国家或者地区)。而且如果用户所在时区是有夏令时的话,在每年开始和结束夏令时的时候,这个偏移值都是要发生变化的。

我们可以通过pytz模块查看当前全球都有哪些timezone。这是一个挺长的list。我们可以找到自己所在的'Asia/Shanghai’。使用pytz.timezone

(‘Asia/Shanghai’)构建一个tzinfo对象。
>>> import pytz
>>> pytz.all_timezones
[… 'Asia/Shanghai’, ...]
>>> pytz.timezone(‘Asia/Shanghai’)
<DstTzInfo 'Asia/Shanghai' LMT+8:06:00 STD>
我们开始要把timezone加入时间的转换里面了。

首先,timestamp和datetime的转换。timestamp,一个数字,表示从UTC时间1970/01/01开始的秒数。

>>> from datetime import datetime
>>> datetime.fromtimestamp(0, pytz.timezone('UTC'))
datetime.datetime(1970, 1, 1, 0, 0, tzinfo=<UTC>)
>>> tz  = pytz.timezone('Asia/Shanghai')
>>> tz2 = pytz.timezone('US/Eastern')
>>> datetime.fromtimestamp(0, tz)
datetime.datetime(1970, 1, 1, 8, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>)
>>> datetime.fromtimestamp(0, tz2)
datetime.datetime(1969, 12, 31, 19, 0, tzinfo=<DstTzInfo 'US/Eastern' EST-1 day, 19:00:00 STD>)
我们可以看到timestamp是UTC绑定的。给定一个timestamp,构建datetime的时候无论传入的是什么时区,对应出来的结果都是同一个时间。 但是python里面这里有个坑。

>>> ts = 1408071830
>>> dt = datetime.fromtimestamp(ts, tz)
datetime.datetime(2014, 8, 15, 11, 3, 50, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>) 
>>> time.mktime(dt.timetuple())
1408100630.0 
>>> dt.timetuple()
time.struct_time(tm_year=2014, tm_mon=8, tm_mday=15, tm_hour=11, tm_min=3, tm_sec=50, tm_wday=4, tm_yday=227, tm_isdst=0) 
>>> dt.astimezone(pytz.utc)
datetime.datetime(2014, 8, 15, 3, 3, 50, tzinfo=<UTC>) 
>>> time.mktime(dt.astimezone(pytz.utc).timetuple())
1408071830.0
time模块的mktime方法支持从timetuple取得timestamp,datetime对象可以直接转换成timetuple。这时候直接使用time.mktime(dt.timetuple())看起来就是很自然的获取timestamp方法。但是我们注意到timetuple方法是直接把当前时间的年月日时分秒直接取出来的。所以这个转换过程在timetuple这个方法这一步丢了时区信息。根据timestamp的定义,正确的方法是把datetime对象利用asttimezone显式转换成UTC时间。

第二,datetime和date以及time的关系 datetime模块同时提供了datetime对象,time对象,date对象。他们之间的关系可以从如下代码简单看出来。

>>> d = datetime.date(2014, 8, 20)
>>> t = datetime.time(11, 30)
>>> dt = datetime.datetime.combine(d, t)
datetime.datetime(2014, 8, 20, 11, 30) 
>>> dt.date()
datetime.date(2014, 8, 20) 
>>> dt.time()
datetime.time(11, 30)
>>> dt = datetime.datetime.fromtimestamp(1405938446, pytz.timezone('UTC'))
datetime.datetime(2014, 7, 21, 10, 27, 26, tzinfo=<UTC>) 
>>> dt.date()
datetime.date(2014, 7, 21) 
>>> dt.time()
datetime.time(10, 27, 26) 
>>> dt.timetz()
datetime.time(10, 27, 26, tzinfo=<UTC>)
>>> datetime.datetime.combine(dt.date(), dt.time())
datetime.datetime(2014, 7, 21, 10, 27, 26) 
>>> datetime.datetime.combine(dt.date(), dt.timetz())
datetime.datetime(2014, 7, 21, 10, 27, 26, tzinfo=<UTC>)
简单说就是,datetime可以取得date和time对象,datetime和time对象可以带timezone信息。date和time对象可以使用datetime.datetime.combine合并获得datetime对象。

第三,日期的加减 datetime,date对象都可以使用timedelta来进行。

直接看代码

>>> d1 = datetime.datetime(2014, 5, 20)
>>> d2 = d1+datetime.timedelta(days=1, hours=2)
>>> d1
datetime.datetime(2014, 5, 20, 0, 0) 
>>> d2
datetime.datetime(2014, 5, 21, 2, 0) 
>>> x = d2 - d1
>>> x
datetime.timedelta(1, 7200) 
>>> x.seconds
7200 
>>> x.days
1
第四,如何对datetime对象正确设置timezone信息

先看代码。

>>> ddt1 = datetime.datetime(2014, 8, 20, 10, 0, 0, 0, pytz.timezone('Asia/Shanghai'))
>>> ddt1
datetime.datetime(2014, 8, 20, 10, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' LMT+8:06:00 STD>) 
>>> ddt2
 datetime.datetime(2014, 8, 20, 11, 0)
>>> ddt1.astimezone(pytz.utc)
datetime.datetime(2014, 8, 20, 1, 54, tzinfo=<UTC>) 
>>> ddt2.astimezone(pytz.utc)
ValueError: astimezone() cannot be applied to a naive datetime
>>> tz = timezone('Asia/Shanghai')
>>> tz.localize(ddt1)
ValueError: Not naive datetime (tzinfo is already set) 
>>> tz.localize(ddt2)
datetime.datetime(2014, 8, 20, 11, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>)
这里抛出来的ValueError,引入了一个naive datetime的概念。简单说naive datetime就是不知道时区信息的datetime对象。没有timezone信息的datetime理论上讲不能定位到具体的时间点。所以对于设定了timezone的datetime对象,可以使用astimezone方法将timezone设定为另一个。对于不包含timezone的datetime对象,使用timezone.localize方法设定timezone。

但是,这里有没有发现一个问题?我们明明设定的是11点整的,使用astimezone之后跑出来个54分是想怎样?

我们注意到,datetime直接传入timezone对象构建出来的带timezone的datetime对象和使用locallize方法构建出来的datetime对象,在打印出来的时候tzinfo显示有所不同,一个是LMT+8:06,一个是CST+8:00,不用说了,54分就搁这来的吧。LMT学名Local Mean Time,用于比较平均日出时间的。有兴趣的可以自己看看Shanghai和Urumqi的LMT时间。CST是China Standard Time,不用解释了。根据pytz的文档,

Unfortunately using the tzinfo argument of the standard datetime constructors ‘’does not work’’ with pytz for many timezones.

It is safe for timezones without daylight saving transitions though, such as UTC:

The preferred way of dealing with times is to always work in UTC, converting to localtime only when generating output to be read by humans.

...

You can take shortcuts when dealing with the UTC side of timezone conversions. normalize() and localize() are not really necessary when there are no daylight saving time transitions to deal with.

我们按照这个说法再试试看,如下,这回pytz.timezone('Asia/Shanghai’)没有再玩幺蛾子了。

>>> x = datetime.datetime(2014, 8, 20, 10, 0, 0, 0, pytz.utc)
>>> x
datetime.datetime(2014, 8, 20, 10, 0, tzinfo=<UTC>) 
>>> x.astimezone(pytz.timezone('Asia/Shanghai'))
datetime.datetime(2014, 8, 20, 18, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>)
所以最保险的方法是使用locallize方法构造带时区的时间。

顺带说下,里面提到了normalize是用来校正计算的时间跨越DST切换的时候出错的情况,还是参见文档,关键部分摘录如下:

This library differs from the documented Python API for tzinfo implementations; if you want to create local wallclock times you need to use the localize() method documented in this document. In addition, if you perform date arithmetic on local times that cross DST boundaries, the result may be in an incorrect timezone (ie. subtract 1 minute from 2002-10-27 1:00 EST and you get 2002-10-27 0:59 EST instead of the correct 2002-10-27 1:59 EDT). A normalize() method is provided to correct this. Unfortunately these issues cannot be resolved without modifying the Python datetime implementation (see PEP-431).
回到最初的问题,我程序需要给用户两天后的21点发送通知,这个时间怎么计算?

>>> import pytz
>>> import time
>>> import datetime
>>> tz = pytz.timezone('Asia/Shanghai')
>>> user_ts = int(time.time())
>>> d1 = datetime.datetime.fromtimestamp(user_ts)
>>> d1x = tz.localize(d1)
>>> d1x
datetime.datetime(2015, 5, 26, 1, 43, 41, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>)
>>> d2 = d1x + datetime.timedelta(days=2)
>>> d2
datetime.datetime(2015, 5, 28, 1, 43, 41, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>) 
>>> d2.replace(hour=21, minute=0)
>>> d2
datetime.datetime(2015, 5, 28, 21, 0, 41, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>)
基本步骤为,根据时间戳和时区信息构建正确的时间d1x,使用timedelta进行对时间进行加减操作,使用replace方法替换小时等信息。

总结,基本上时间相关的这些方法,大部分你都可以直接按照自己的需要封装到一个独立的utility模块,然后就不需要再去管它了。你要做的是,至少有一个人先正确地管一下。

时间: 2024-11-14 10:51:53

Python中的时区处理的详解的相关文章

Python中的深拷贝和浅拷贝详解

  这篇文章主要介绍了Python中的深拷贝和浅拷贝详解,本文讲解了变量-对象-引用.可变对象-不可变对象.拷贝等内容,需要的朋友可以参考下 要说清楚Python中的深浅拷贝,需要搞清楚下面一系列概念: 变量-引用-对象(可变对象,不可变对象)-切片-拷贝(浅拷贝,深拷贝) [变量-对象-引用] 在Python中一切都是对象,比如说:3, 3.14, 'Hello', [1,2,3,4],{'a':1}...... 甚至连type其本身都是对象,type对象 Python中变量与C/C++/Ja

Python中的推导式使用详解

  这篇文章主要介绍了Python中的推导式使用详解,本文分别讲解了列表推导式.字典推导式.集合推导式使用实例,需要的朋友可以参考下 推导式是Python中很强大的.很受欢迎的特性,具有语言简洁,速度快等优点.推导式包括: 1.列表推导式 2.字典推导式 3.集合推导式 嵌套列表推导式 NOTE: 字典和集合推导是最近才加入到Python的(Python 2.7 和Python 3.1以上版). 下面简要介绍下: [列表推导式] 列表推导能非常简洁的构造一个新列表:只用一条简洁的表达式即可对得到

Python中的zipfile模块使用详解

  这篇文章主要介绍了Python中的zipfile模块使用详解,zipfile模块是用来操作zip文件,需要的朋友可以参考下 zip文件格式是通用的文档压缩标准,在ziplib模块中,使用ZipFile类来操作zip文件,下面具体介绍一下: class zipfile.ZipFile(file[, mode[, compression[, allowZip64]]]) 创建一个ZipFile对象,表示一个zip文件.参数file表示文件的路径或类文件对象(file-like object);参

Python中tell()方法的使用详解

  这篇文章主要介绍了Python中tell()方法的使用详解,是Python入门学习中的基础知识,需要的朋友可以参考下 tell()方法返回的文件内的文件读/写指针的当前位置. 语法 以下是tell()方法的语法: ? 1 fileObject.tell() 参数 NA 返回值 此方法返回该文件中读出的文件/写指针的当前位置. 例子 下面的例子显示了tell()方法的使用. ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #

Python中的rfind()方法使用详解

  这篇文章主要介绍了Python中的rfind()方法使用详解,是Python入门中的基础知识,需要的朋友可以参考下 rfind()方法返回所在子str 被找到的最后一个索引,或者-1,如果没有这样的索引不存在,可选择限制搜索字符串string[beg:end]. 语法 以下是rfind()方法的语法: ? 1 str.rfind(str, beg=0 end=len(string)) 参数 str -- 此选项指定要搜索的字符串 beg -- 这是开始索引,默认情况下为 0 end -- 这

Python中的rjust()方法使用详解

  这篇文章主要介绍了Python中的rjust()方法使用详解,是Python学习入门中的基础知识,需要的朋友可以参考下 rjust()该方法返回字符串合理字符串的右边的长度宽度.填充是通过使用指定的fillchar(默认为空格).如果宽度小于len(s)返回原始字符串. 语法 以下是rjust()方法的语法: ? 1 str.rjust(width[, fillchar]) 参数 width -- 这是字符串填充后总共的长度. fillchar -- 这是填充字符,默认为空格. 返回值 此方

Python中的choice()方法使用详解

 这篇文章主要介绍了Python中的choice()方法使用详解,是Python入门中的基础知识,需要的朋友可以参考下     choice()方法从一个列表,元组或字符串返回一个随机项. 语法 以下是choice()方法的语法: ? 1 choice( seq ) 注意:此函数是无法直接访问的,所以我们需要导入random模块,然后我们需要使用random对象来调用这个函数. 参数 seq -- 这可能是一个列表,元组或字符串... 返回值 该方法返回一个随机项. 例子 下面的例子显示了cho

Python中的变量和作用域详解_python

作用域介绍 python中的作用域分4种情况: L:local,局部作用域,即函数中定义的变量: E:enclosing,嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域,但不是全局的: G:globa,全局变量,就是模块级别定义的变量: B:built-in,系统固定模块里面的变量,比如int, bytearray等. 搜索变量的优先级顺序依次是:作用域局部>外层作用域>当前模块中的全局>python内置作用域,也就是LEGB. x = int(2.9) # int bu

Python中运算符&quot;==&quot;和&quot;is&quot;的详解_python

前言 在讲is和==这两种运算符区别之前,首先要知道Python中对象包含的三个基本要素,分别是:id(身份标识).python type()(数据类型)和value(值).is和==都是对对象进行比较判断作用的,但对对象比较判断的内容并不相同.下面来看看具体区别在哪. Python中比较两个对象是否相等,一共有两种方法,简单来说,它们的区别如下:      is是比较两个引用是否指向了同一个对象(引用比较).      ==是比较两个对象是否相等. >>> a = [1, 2, 3]