PythonDjangoMySQL,时区、⽇期、时间戳(USE_TZ=True时
的时间存储问题)
Python Django MySQL,时区、⽇期、时间戳,写下这个标题的时候,头脑⾥⾯回荡着⽕车上的经典⼴告词:啤酒、饮料、矿泉⽔,花⽣、⽠⼦、⼋宝粥。当然本⽂跟这些零⾷吃喝没有关系,我们主要来聊聊时间问题。
环境说明:
1、约定:
本⽂中的“时间”,如未特别说明均指“⽇期+时间”,即形如“%Y-%m-%d %H:%M:%S”,或“yyyy-mm-dd HH:MM:SS” 等包含⽇期和时间点的值,可能包含形如 “.fraction” 的毫秒级的数值以及时区标识。
2、软件版本:
Python:2.7.10
Django:1.11.12 final
MySQL:5.7.18
3、基础数据:
Django Model:
class IcsServiceStatusModel(models.Model):
class Meta:
db_table = 'ics_service_status'
app_label = 'ics_meta'
objects = SelfDefinedManager()
id = models.AutoField(primary_key=True)
pub_id = models.CharField(max_length=15)
service_mode = models.SmallIntegerField(max_length=5)
current_session = models.CharField(max_length=50)
last_msg_dt = models.DateTimeField()
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
MySQL 数据表:
CREATE TABLE IF NOT EXISTS `ics_service_status` (
`id` int(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`pub_id` varchar(15) NOT NULL,
`service_mode` tinyint(5) NOT NULL,
`current_session` varchar(50) NOT NULL,
`last_msg_dt` datetime NOT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='智能客服服务状态跟踪表' $$
知识点⼀、计算机上的时间表⽰
1、时间的常规表⽰
时间通常分为⽇期和时刻,表⽰为 y 年 m ⽉ d ⽇ H 时 M 分S 秒。⼏个单位之间的换算简单计为1年=365天(闰年366天),1个⽉=28天、29天、30天或31天(分别对应平年的2⽉,闰年的2⽉,3、6、9、11⽉,1、3、5、7、8、10、12⽉),1天=24⼩时,1⼩时=60分钟,1分钟=60秒。经典格式:yyyy-mm-dd HH:MM:SS。
2、时区
太阳⼀升⼀落⼀天过去了。⽑爷爷说“青年⼈好像早晨⼋九点钟的太阳”,我们可以很直观地感受到⼋九点正式太阳升起后两三个⼩时正挂在半空的时候。同样的这句话换作是西半球的美国⼈、南半球的巴西⼈、⾚道上的印尼⼈来理解也都是不会有问题的。因为我们⽇常⽣活中采⽤的计时⽅式都是参考太阳的移动周期来确定时刻的(⽐如中国的⽇晷):⼀天分为24个⼩时,太阳的照射下,物体的影⼦最短的时候为正午12点,前后均分,各12个⼩时,由此确定的时间为地⽅时,或本地时间(Local Time)。地理知识告诉我们,地球的⾃传、绕太阳公转使得让物体的影⼦最短的太阳需要由东到西逐次照射到地球的每个⾓落。所以我们看到的⼋九点钟的太阳跟外国⼈眼中的⼋九点钟的太阳已经不是同⼀个。这种时间表⽰⽅式⽅便在世界各地的⼈们相对统⼀地建⽴时间与环境的对应关系,由此产⽣便于沟通交流的意识。然⽽不同经度上的⼈们,尤其是跨度⽐较⼤的两个地⽅,如果都使⽤各⾃本地的时间交流的话就要出乱⼦了。已知华盛顿的正午⽐北京的正午来的晚12
个⼩时。假如华盛顿飞北京要13个⼩时,⾝居北京的⼩丽晚上8点吃过晚饭后就给美国的好朋友凯特打电话说:“明天晚上8点去我家参加⽣⽇派对,傍晚6点去机场接你”。凯特是个很守时的⼩朋友。他想傍晚6点要飞到北京,总共⼜要飞13个⼩时,那岂不是要早晨5点就出发了?于是乎第⼆天凯特起了个⼤早,5点天还蒙蒙亮飞机准时起飞了。⼿上捧着精⼼挑选的礼物,期待着快点送给⼩丽。13个⼩时过后飞机平稳地降落在北京的机场,在机场的到达厅他没有看到⼩丽,太阳刚刚升起,路边的摊贩正在
叫卖早餐。凯特打电话给⼩丽,却被臭骂了⼀
顿“你为什么不准时参加我的⽣⽇派对?” 这是为啥捏?
为了便于不同地区的⼈们交流时间,地球⼈需要⼀个统⼀的标准时间(UTC, Coordinated Universal  Time, 协调世界时,诞⽣于1972年;注1:与世界时UT相差±0.9s;注2:UTC 基于TAI,即International Atomic Time,国际原⼦时,计算秒)。⼤伙约定以东经0°(也是西经0°,俗称本初⼦午线)所在地区的地⽅时的中午12点作为世界标准时间的0点。同时东经180°(也是西经180°)设为标准⽇期变更的界线。这么看世界标准时间更像是国际⽇期变更线上的阿留申岛居民的地⽅时。其他地区的本地时间与世界标准时间则⽤时差进⾏换算:由本初⼦午线往东,每隔15°经度时间加1⼩时;往西,每隔15°经度时间减去1⼩时。这样以15°为单位划分的360°÷15°=24个区域就是时区。
回到⼩丽请客的问题,假设他们打电话的时刻是位于东⼋区的北京时间2018年5⽉6⽇晚上8点,那么此时正是世界标准时间的5⽉6⽇中午12点,⽽位于西四区的华盛顿,凯特的家,当地的时间则为5⽉6⽇的上午8点。⽽⼩丽说的⽣⽇派对的时间为北京时间5⽉7⽇晚上8点,对应世界标准时间则为5⽉7⽇中午12点,华盛顿凯特家的5⽉7⽇上午8点。所以凯特应该在当地时间5⽉6⽇下午5点出发,才能赶上⼩丽在机场接他。然⽽他是华盛顿时间5⽉7⽇早晨5点出发的,对应世界标准时间为5⽉7⽇上午9点,是⼩丽所在的北京时间的5⽉7⽇下午5点了,⼀个⼩时可不是要错过吗?
所以⼩丽在说时间的时候⼀定要强调是北京时间的明晚8点,表⽰为:2018-05-07 20:00:00+8:00:00 (或2018-05-07 20:00:00 UTC+8),⽽凯特在做⾏程计划的时候也要记得把约会时间转换为华盛顿的次⽇早上8点:2018-05-07 08:00:00-4:00:00,从⽽推算出发时间是华盛顿时间的当天下午5点,即:2018-05-06 17:00:00-4:00:00。当然,如果他们统⼀⽤世界标准时交流的话会更简单。⼩丽只需要说“2018年5⽉7⽇的12点我⽣⽇,过来 happy,10点去机场接你”,卡特也只要买好世界标准时间2018年5⽉6⽇21点的机票就可以按时赴约了。
总之呢,交流时间的时候声明是哪⾥的时间是很重要滴。⼀个明确的⽇期时间可以表⽰为:
yyyy-mm-dd HH:MM:SS UTC±n
其中“+”表⽰“东”,“-”表⽰“西”,结合 n ⼀起表⽰时区的编号。UTC±n 声明了前⾯的时间是哪个时区的地⽅时。转换为世界标准时间只需要±n ⼩时即可。如果不写后⾯的 UTC±n,默认情况下就是 UTC±0,也就是标准世界时间了。
3、时间戳
时间的表⽰经常使⽤⼀个公认的参考点,⽐如⽇常使⽤中默认的“(公元后)⼏⼏年”,需要明确说明的“公元前⼏⼏年”、“民国⼏⼏年”、“顺治⼏⼏年”,它们分别⽤公认的元年、新国家的成⽴⽇期、统治
者的上位⽇期等作为参考点。在计算机⾥⾯我们以 1970年1⽉1⽇0点作为参考点,⽤偏差值来记录具体的时间,精确到秒。参考时间点的设置有时是为了便于记忆、也便于使⽤。⽽计算机上参考点的设置还受到⼀个客观因素的约束。
早期的CPU和操作系统以32位为主。如果⽤⼀个整数完整地表⽰公元后的时间,精确到秒,那么y年m⽉d⽇H时M时S秒需要⽤整数
(y-1)×365×24×3600+(m-1)×30×24×3600+(d-1)×24×3600+H×3600+M×60+S)
来表⽰(假设⼀年365天、⼀个⽉30天),以2018年7⽉17⽇15点12分46秒为例,对应的整数值为
2017×365×24×3600+6×30×24×3600+16×24×3600+15×3600+12×60+46 = 63,665,190,766
然⽽32位处理器能够表⽰的最⼤整数值为:⽆符号数,2^32-1,即 4,294,967,295;有符号数,2^31-1,即 2,147,483,647,远不
及 63,665,190,766。⼀年(按365天算)等于 31,536,000 秒,32位处理器能表⽰的最⼤整数值只能表⽰ (2^31-1)÷(365×24×3600) ≈ 68(年)也就是只能表⽰公元前68年到公元68年之间的⽇期。
那么该怎么满⾜使⽤计算机处理时间的需求呢?“计算机计时元年” 的概念由此诞⽣。UNIX操作系统考虑到计算
机产⽣的年代和应⽤的时限综合取了1970年1⽉1⽇作为UNIX TIME的纪元时间。于是y年m⽉d⽇H时M时S秒,⽤该时间与纪元时间的整数差值表⽰为
(y年m⽉d⽇与1970年1⽉1⽇的⽇期差)×24×3600+H×3600+M×60+S
该差值也称为时间戳。同样的,哪怕是时间差值,32位的处理器也只能表⽰1970年1⽉1⽇前后68年的时间,也就是 1901年12⽉13⽇20时45分52秒到2038年01⽉19⽇03时14分07秒。所以呢,使⽤32位处理器的⽼机器们届时将⾯临类似“千年⾍”的“2038年问题”。
知识点⼆、MySQL 中的时间表⽰
1、datetime 和 timestamp
在 MySQL 中时间可以⽤ datetime 和 timestamp 两种类型的字段表⽰。(date 类型可以存储⽇期,time 类型可以存储时间)
⼆者的相同点:可通过设置默认值⾃动更新和初始化,默认显⽰格式都为:YYYY-MM-dd HH:mm:ss
⼆者的不同点:
① timestamp 类型的字段实际存储的是距离1970年1⽉1⽇0点的秒数,⽤4字节存储,可以根据时区设置转换为指定时区的时间值,存储范围从'1970-01-01 00:00:01' UTC 到'2038-01-19 03:14:07' UTC。
② datetime 类型的字段⽤8字节存储,对时区设置⽆感知,存储范围从'1000-01-01 00:00:00'到'9999-12-31 23:59:59'。
2、MySQL 中获取时间可以⽤以下函数
① now():当前⽇期时间,例如:"2018-07-18 16:07:23"
② curdate():当天⽇期,例如:"2018-07-18"
③ curtime():当前时间,例如:"16:07:23"
④ timestamp、current_timestamp、current_timestamp()、localtime()、localtimestamp()、unix_timestamp(now())
⑤ date_sub(curdate(),interval 1 day):⽇期减(date_add、timediff)
3、在实验中理解 datetime 和 timestamp 的不同
(1)查看 explicit_defaults_for_timestamp,即,“是否明确地给 timestamp 类型的字段设置默认值”:
注:MySQL 5.6.6 版本启⽤了系统变量 explicit_defaults_for_timestamp,⾼于 5.6.6版本的 MySQL 则有该特性。
当explicit_defaults_for_timestamp=false 时,按照如下规则"初始化":
①未明确声明为 NULL 属性的 TIMESTAMP 列被分配为 NOT NULL 属性。(其他数据类型的列,如果未显式声明为 NOT NULL,则允许 NULL 值。)将此列设置为NULL将其设置为当前时间戳。
②表中的第⼀个 TIMESTAMP 列(如果未声明为 NULL 属性或显式 DEFAULT 或 ON UPDATE ⼦句)将⾃动分配 DEFAULT CURRENT_TIMESTAMP 和 ON UPDATE CURRENT_TIMESTAMP 属性。
③第⼀个之后的 TIMESTAMP 列(如果未声明为 NULL 属性或显式 DEFAULT ⼦句)将⾃动分配 DEFAULT '0000-00-00
00:00:00'(“零”时间戳)。对于不指定此列的显式值的插⼊⾏,该列将分配 “0000-00-00 00:00:00”,并且不会发⽣警告。
当explicit_defaults_for_timestamp=true 时,按照如下规则"初始化":
①未明确声明为 NOT NULL 的 TIMESTAMP 列允许 NULL 值。将此列设置为 NULL,⽽不是当前时间戳。
②没有 TIMESTAMP 列⾃动分配 DEFAULT CURRENT_TIMESTAMP 或 ON UPDATE CURRENT_TIMESTAMP 属性。必须明确指定这些属性。
③声明为 NOT NULL 且没有显式 DEFAULT ⼦句的 TIMESTAMP 列被视为没有默认值。对于不为此列指定显式值的插⼊⾏,结果取决于 SQL 模式。如果启⽤了严格的 SQL 模式,则会发⽣错误。如果未启⽤严格的 SQL 模式,则会为列分配隐式默认值 “0000-00-00
00:00:00”,并发出警告。这类似于 MySQL 如何处理其他时间类型,如 DATETIME。
不同 TIMESTAMP 默认值的作⽤:
① CURRENT_TIMESTAMP
在创建新记录的时候把这个字段设置为当前时间,但以后修改时,不再刷新它
② ON UPDATE CURRENT_TIMESTAMP
mysql下载32位
在创建新记录的时候把这个字段设置为0,以后修改时刷新它
③ CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
在创建新记录和修改现有记录的时候都对这个数据列刷新
④ ‘yyyy-mm-dd hh:mm:ss’ ON UPDATE CURRENT_TIMESTAMP
在创建新记录的时候把这个字段设置为给定值,以后修改时刷新它
(2)查看 time_zone,即,时区设置:
(3)创建表,插⼊数据:
SHOW VARIABLES LIKE '%explicit_defaults_for_timestamp%';
SHOW VARIABLES LIKE '%time_zone%';
CREATE DATABASE test;
USE test;
CREATE TABLE IF NOT EXISTS `z_test` (
`msg_dt` varchar(19) NOT NULL,
`create_dt` datetime NOT NULL,
`update_dt` TIMESTAMP NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='测试时间表⽰';
INSERT INTO z_test VALUES (now(), now(), now());
INSERT INTO z_test VALUES (unix_timestamp(now()), unix_timestamp(now()), unix_timestamp(now()));
INSERT INTO z_test VALUES ('2018-07-18 11:55:32', '2018-07-18 11:55:32', '2018-07-18 11:55:32');
INSERT INTO z_test VALUES (localtime, localtime, localtime);
INSERT INTO z_test VALUES (localtimestamp(), localtimestamp(), localtimestamp());
SELECT * FROM fspis_meta.z_test;
SET time_zone = '+4:00';
SELECT * FROM fspis_meta.z_test;
SET time_zone = 'SYSTEM';
UPDATE z_test SET msg_dt='plain text';
SELECT * FROM fspis_meta.z_test;
------------------------------------------------------------------------------
SET explicit_defaults_for_timestamp = 'ON';
-- 再做⼀遍上述操作
可见,数据类型为TIMESTAMP 的 update_dt 字段被设置了默认值“CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP”(4)时区修改为“+4:00”后的查询结果:
(5)将时区还原为“SYSTEM”,即“+8:00”,更新 msg_dt 字段:
由于 update_dt 字段的 default 值为 “CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP”,于是⾃动更新。
(6)当explicit_defaults_for_timestamp=true 时,实验结果:
更新 msg_dt 字段:
由于 update_dt 字段的 default 值为 null,此时更新记录并没有影响 update_dt 字段的值
知识点三、Django 中时间的使⽤
1、Python 中的时间函数
import datetime, time, pytz 模块
(1)datetime 模块提供:datetime、date、time、timezone、tzinfo、timedelta、struct_time 等类
其中 datetime 类包含:now()、utcnow()、utcoffset()、tzname()、time()、timestamp()、date()、ctime() 等函数⽤于操作时
间,w() 以数组的形式即 struct_time 表⽰时间;
(2)time 模块提供:struct_time 等类;其中,time.time() 以时间戳的形式表⽰时间,time.localtime() 以数组的形式即 struct_time 表⽰时间;
(3)pytz 提供:timezone() 等函数
(4)时间格式化代码
代码作⽤代码作⽤
%a星期⼏的简写%A星期⼏的全称
%b⽉分的简写%B⽉份的全称
%c标准的⽇期的时间串%C年份的后两位数字
%d⼗进制表⽰的每⽉的第⼏天%D⽉/天/年
%e在两字符域中,⼗进制表⽰的每⽉的第⼏天%F年-⽉-⽇
%g年份的后两位数字,使⽤基于周的年%G年分,使⽤基于周的年
%h简写的⽉份名%H24⼩时制的⼩时
%I12⼩时制的⼩时%j⼗进制表⽰的每年的第⼏天
%m⼗进制表⽰的⽉份%M⼗时制表⽰的分钟数
%n新⾏符%p本地的AM或PM的等价显⽰
%r12⼩时的时间%R显⽰⼩时和分钟:hh:mm
%S⼗进制的秒数%t⽔平制表符
%T显⽰时分秒:hh:mm:ss%u每周的第⼏天,星期⼀为第⼀天(值从0到6,星期⼀为0)
%U第年的第⼏周,把星期⽇做为第⼀天(值从0到53)%V每年的第⼏周,使⽤基于周的年
%w⼗进制表⽰的星期⼏(值从0到6,星期天为0)%W每年的第⼏周,把星期⼀做为第⼀天(值从0到53)
%x标准的⽇期串%X标准的时间串
%y不带世纪的⼗进制年份(值从0到99)%Y带世纪部分的⼗制年份
%z,%Z 时区名称,如果不能得到时区名称则返回空字符。%%百分号
代码作⽤代码作⽤
Python 的 datatime.datetime对象有⼀个 tzinfo 属性,该属性是 info ⼦类的⼀个实例,他被⽤来存储时区信息。当某个datetime 对象的 tzinfo 属性被设置并给出⼀个时间偏移量时,我们称该 datetime 对象是 aware (已知) 的。否则称其为 naive (原⽣) 的。可以使⽤ is_aware() 和 is_naive() 函数来判断某个 datetime 对象是 aware 类型或 naive 类型。
2、Django Utils 中的时间函数
import django.utils import timezone, tzinfo, datetime_safe 模块
timezone 模块也提供了 datetime、tzinfo、timedelta 等类和 local() 等函数
timezone.make_aware(w(), _default_timezone()) 可以为 naive 类型的 datetime 添加时区属性
3、Django 默认关闭时区⽀持,开启时区⽀持,需要在 settings 中设置 USE_TZ = True 。最好同时安装 pytz 模块(pip install pytz) 。Django 的 settings.py 中与时间相关的设置
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = True
当设置了 TIME_ZONE 则 Django(Django 默认的 TIME_ZONE = 'America/Chicago' 或 system 时区)将使⽤指定的时区,它将影响datetime.locale、now()等函数的返回值。
当设置了 USE_TZ 为 True 时,Django 与其他系统或服务的交流将强制使⽤ UTC 时间。
可能踩的坑:
当 USE_TZ=True 时,把时间存储到数据库的时候 “INSERT INTO table_name VALUES('datetime_str' 或 datetime实例)” Django 将会把'datetime_str' 和 datetime 实例转换为 UTC 时间。由于MySQL 的 datetime 类型字段对时区是⽆感知的,所以会直接存储由 Django 传递过去的 UTC 形式的时间。在中国,这个问题表现为存储到数据库⾥⾯的时间会晚8个⼩时。
解决⽅法:dt.replace(tzinfo=pytz.utc),也就是在存储前将 datetime 的时区信息改为 UTC。
def datetime_for_db(dt=None, dt_str=None):
if dt:
place(tzinfo=pytz.utc)
elif dt_str:
try:
dt = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S')
place(tzinfo=pytz.utc)
except Exception:
raise Exception
else:
w().replace(tzinfo=pytz.utc)
⼀般不跨时区的应⽤,可以不使⽤时区,即在settings.py设置 USE_TZ=False
启⽤USE_TZ = True后,处理时间⽅⾯,有两条 “黄⾦法则”:
1. 保证存储到数据库中的是 UTC 时间;
2. 在函数之间传递时间参数时,确保时间已经转换成 UTC 时间;
⽐如,通常获取当前时间⽤的是:
import datetime
now = w()
启⽤USE_TZ = True后,需要写成:
import datetime
from django.utils.timezone import utc
now = datetime.datetime.utcnow().replace(tzinfo=utc)
或:
from django.utils import timezone
now = w()
保证now变量存放的是 UTC 时间。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。