17.flask博客项目实战十二之处理日期和时间
配套视频教程
本章将学习如何以适合所有用户的方式使用日期和时间,无论他们身居何处。
目前为止,一直忽略Microblog应用程序显示日期和时间的问题,只是让Python渲染了User
模型中的datetime
对象,并完全忽略Post
模型中的datetime
对象。
时区“地狱”
在服务器上用Python去呈现日期和时间,在Web浏览器上以这种方式渲染给用户可不是一个好主意。如示例,在Python解释器中运行如下内容:
(venv) D:\microblog>python
Python 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from datetime import datetime
>>> str(datetime.now())
'2018-08-23 09:52:14.895893'
>>> str(datetime.utcnow())
'2018-08-23 01:52:28.247986'
>>> quit()
(venv) D:\microblog>
调用datetime.now()
将返回我当前所在地区(中国-北京(时间))的正确的时间,而调用datetime.utcnow()
将返回UTC时区的时间。如果我能让生活在世界不同地区的许多人同时一起运行上述代码,datetime.now()
函数将为每个人返回不同的结果,但datetime.utcnow()
无论地理位置在哪,都将返回相同的时间。那么你认为哪一个更适合一个很可能让用户遍布全球的Web应用程序中使用?
很明显,服务器必须管理 一致且独立于位置的时间。如果这个应用程序增长到世界各地需要多个生产服务器的程序,当然就不希望每个服务器在不同时区写入数据库的时间戳,因为这样就无法使用这些时间。由于UTC是最常用的统一时区,并且在datetime类
中受支持,因此在此将使用它。
但这种方法存在一个重要问题。对于不同时区的用户,如果他们在UTC看到时间,那么将很难弄清楚发布帖子的时间。他们需要提前时间是UTC,以便他们能够“精神上”调整时间到他们自己的时区。想象一下,PDT时区的用户在下午3点发布了一些内容,并立即看到这个帖子在UTC时间晚上10点出现,或更准确的说是22:00。这将是很令人困惑的。
虽然从服务器的角度上,将时间戳标准化很有意义,但这会用户带来可用性问题。本章的目标是解决这个问题,同时保持服务器以UTC格式管理的所有时间戳。
时区转换
这个问题显而易见的解决方案是:将所有时间戳 从存储的UTC单位转换为每个用户的本地时间。这允许服务器继续使用UTC来保持一致性,同时为每个用户量身定制地即时转换解决可用性问题。这个解决方案的棘手部分 是了解每个用户的位置。
许多网站都有一个配置页面,用户可以在其中指定时区。这将要求我添加一个带表单的新页面,这个表单中,我可以向用户显示带有时区列表的下拉列表。作为注册的一部分,用户可以在第一次访问网站时要求输入他们的时区。
虽然这是解决问题的一个不错解决方案,但要求用户输入他们已在其操作系统中配置的信息有点奇怪。如果能从他们的计算机中获取时区设置似乎会更有效率。
事实证明,Web浏览器知道用户的时区,并通过标准日期和时间JavaScript API公开它。实际上,通过JavaScript,有两种方法可利用时区信息:
- “老派”方法是在用户首次登陆应用程序时,让Web浏览器以某种方式将时区信息发送到服务器。这可以通过Ajax调用完成,或更简单地使用
meta refresh tag
。一旦服务器知道时区,它就可以将其保存在用户的会话中,或将其写入数据库中的用户条目,然后在渲染模板时用它调整所有时间戳。 - “新派”方法是不改变服务器中的东西,而在客户端中使用JavaScript在客户端中进行从UTC到本地时区的转换。
这两个选项都是有效的,但第二个选项有很大的优势。光是知道用户的时区,并不足以以用户期望的格式呈现日期和时间。浏览器还可访问系统区域配置,这个配置指定AP/PM与24小时制,DD/MM/YYYY与MM/DD/YYYY,以及许多其他文化或区域样式之类的内容。
如果上述还不够,那么“新派”方法还有一个优势。有一个开源库可完成所有这些工作。
介绍Moment.js和Flask-Moment
Moment.js是一个小型开源JavaScript库,它将日期和时间渲染成另一个级别,因为它提供每一个可想象的格式化选项。不久前,建立了Flask-Moment,它是一个小型Flask扩展,它可以轻易地将moment.js
合并到应用程序中。
首先,安装Flask-Moment:版本0.6.0
(venv) D:\microblog>pip install flask-moment
Collecting flask-moment
Using cached https://files.pythonhosted.org/packages/dd/f7/13e9d7480f9097e0efe945e17309c34e0a547a6cfb3f9728324d2f9bf462/Flask_Moment-0.6.0-py2.py3-none-any.whl
Requirement already satisfied: Flask in d:\microblog\venv\lib\site-packages (from flask-moment)
Requirement already satisfied: Jinja2>=2.10 in d:\microblog\venv\lib\site-packages (from Flask->flask-moment)
Requirement already satisfied: itsdangerous>=0.24 in d:\microblog\venv\lib\site-packages (from Flask->flask-moment)
Requirement already satisfied: Werkzeug>=0.14 in d:\microblog\venv\lib\site-packages (from Flask->flask-moment)
Requirement already satisfied: click>=5.1 in d:\microblog\venv\lib\site-packages (from Flask->flask-moment)
Requirement already satisfied: MarkupSafe>=0.23 in d:\microblog\venv\lib\site-packages (from Jinja2>=2.10->Flask->flask-moment)
Installing collected packages: flask-moment
Successfully installed flask-moment-0.6.0
以常规方式添加到Flask应用程序中:
app/init.py:添加Flask-Moment实例
# ...
from flask_moment import Moment
app = Flask(__name__)
# ...
moment = Moment(app)
与其他扩展不同,Flask-Moment与moment.js
一起使用,因此应用程序的所有模板都必须包含这个库。为了确保这个库始终可用,将在 基础模板中添加它。这可通过两种方式完成。最直接的方法是显示地以导入库的方式添加一个<script>
标签,但Flask-Moment使其变得更容易,即通过公开一个moment.include_moment()
函数,它会生成<script>
标签。
app/templates/base.html:在基础模板中包含moment.js
...
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
在这添加的scripts块
是Flask-Bootstrap的基础模板导出的另一个块。这是包含JavaScript导入的地方。这个块与之前的块不同,因为它已经在基础模板中定义了一些内容。我想的是 添加moment.js
库,而不会失去基本内容。这是通过super()
语句实现的,这个语句将保留基础模板中的内容。如果在没有使用super()
的情况下,在你的模板中定义一个块,那么在基础模板中,为这个块定义的任何内容都将失去。
使用Moment.js
Moment.js使得一个moment类
可供浏览器使用。渲染时间戳的第一步是创建这个类的对象,以ISO 8601格式传递所需的时间戳。下方是例子:
t = moment('2017-09-28T21:45:23Z')
如果你不熟悉日期和时间的ISO 8601标准格式,格式如:{{ year }}-{{ month }}-{{ day }}T{{ hour }}:{{ minute }}:{{ second }}{{ timezone }}
。我已经决定只用UTC时区,所以最后一部分将始终是Z
,它代表ISO 8601标准中的UTC。
moment
对象提供为不同渲染选项提供了几种方法。以下是一些最常见的选项:
moment('2017-09-28T21:45:23Z').format('L')
"09/28/2017"
moment('2017-09-28T21:45:23Z').format('LL')
"September 28, 2017"
moment('2017-09-28T21:45:23Z').format('LLL')
"September 28, 2017 2:45 PM"
moment('2017-09-28T21:45:23Z').format('LLLL')
"Thursday, September 28, 2017 2:45 PM"
moment('2017-09-28T21:45:23Z').format('dddd')
"Thursday"
moment('2017-09-28T21:45:23Z').fromNow()
"7 hours ago"
moment('2017-09-28T21:45:23Z').calendar()
"Today at 2:45 PM"
上述示例 创建了一个时刻对象,初始化为 2017年9月28日晚上9:45 UTC
。上面尝试的所有选项都以UTC-7时区(在中国的话,将用UTC+8)来呈现,因为这是作者的计算机上配置的时区。可在浏览器的控制台中输入上述命令,得确保打开控制台的页面包含moment.js。如果引入了moment.js,也可以在Microblog上操作。或者在https://momentjs.com/上操作。
注意不同方法是如何创建不同的表示的。使用format()
,可以控制字符串的输出格式,类似Python中的strftime()
函数。fromNow()
和calendar()
方法很有趣,因为它们会根据当前时间显示时间戳,所以会得到如“一分钟前”或“两个小时内”的输出。
如果直接使用JavaScript,那么上述调用将返回具有呈现时间戳的字符串。然后,可将此文本插入页面上的适当位置,遗憾的是,需要JavaScript与DOM配合使用。Flask-Moment在模板中启用类似于JavaScript的对象,极大地简化moment.js
的使用。
我们来看一下 个人资料页面 中显示的时间戳。当前 user.html
模板允许使用Python生成时间的字符串表示。现在使用Flask-Moment渲染这个时间戳,如下:
app/templates/user.html:使用moment.js渲染时间戳
...
{% if user.last_seen %}
<p>Last seen on:{{ moment(user.last_seen).format('LLL') }}</p>
{% endif %}
...
Flask-Moment使用的语法类似于 JavaScript库的语法,一个区别是 moment()
的参数现在是一个Python datetime
对象,而不是一个ISO 8601字符串。moment()
从模板发出的调用还会自动生成所需的JavaScript代码,以将呈现的时间戳插入到DOM的适当位置。
可以利用Flask-Moment和moment.js
的第二个地方是_post.html
子模板,它是从/index
和/user
页面中调用的。在当前版本的模板中,每个帖子前面都有一个“username says:”行
。现在添加一个fromNow()
时间戳:
app/templates/_post.html:
<a href="{{ url_for('user', username=post.author.username) }}">
{{ post.author.username }}
</a>
said {{ moment(post.timestamp).fromNow() }}:
<br>
{{ post.body }}
flask run
运行程序,效果:
C:\Users\Administrator>d:
D:\>cd D:\microblog\venv\Scripts
D:\microblog\venv\Scripts>activate
(venv) D:\microblog\venv\Scripts>cd D:\microblog
(venv) D:\microblog>set MAIL_SERVER=smtp.qq.com
(venv) D:\microblog>set MAIL_PORT=465
(venv) D:\microblog>set MAIL_USE_SSL=True
(venv) D:\microblog>set MAIL_USERNAME=your_qq_email@qq.com
(venv) D:\microblog>set MAIL_PASSWORD=16位授权码
(venv) D:\microblog>flask run
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
[2018-08-23 15:10:27,042] INFO in __init__: Microblog startup
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [23/Aug/2018 15:10:36] "GET /user/oldiron HTTP/1.1" 200 -
127.0.0.1 - - [23/Aug/2018 15:11:04] "GET /edit_profile HTTP/1.1" 200 -
127.0.0.1 - - [23/Aug/2018 15:11:05] "GET /index HTTP/1.1" 200 -
127.0.0.1 - - [23/Aug/2018 15:11:08] "GET /user/oldiron HTTP/1.1" 200 -
127.0.0.1 - - [23/Aug/2018 15:11:21] "GET /explore HTTP/1.1" 200 -

目前为止,项目结构
microblog/
app/
templates/
email/
reset_password.html
reset_password.txt
_post.html
404.html
500.html
base.html
edit_profile.html
index.html
login.html
register.html
reset_password.html
reset_password_request.html
user.html
__init__.py
email.py
errors.py
forms.py
models.py
routes.py
logs/
microblog.log
migrations/
venv/
app.db
config.py
microblog.py
tests.py
参考
https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xii-dates-and-times
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架