7 Flask mega-tutorial 第7章 错误处理 Error Handling

如需转载请注明出处。
win10 64位、Python 3.6.3、Notepad++、Chrome 67.0.3396.99(正式版本)(64 位)
注:作者编写时间2018-01-17,linux、python 3.5.2

以下内容均是加入自己的理解与增删,以记录学习过程。不限于翻译,部分不完全照搬作者Miguel Grinberg的博客,版权属于作者,感谢他提供免费学习的资料。

本章将学习到:如何在Flask应用程序中进行错误处理(策略)。

这里将暂时停止为microblog添加新功能,而是讨论处理bug的策略,因为它们可能总是无处不在。为了帮助说明此主题,故意在第6章中的代码中遗留一个bug。等待着我们去发现它。

在Flask中的错误处理

在Flask应用程序中发生error时会发生什么?找到问题的最佳方法是亲自体验,即重现它。启动应用程序,并确保已有两个注册用户。用其中一个用户身份登录后(在此以用户名+密码 susan cat为例),打开【Profile】页面并点击【Edit you profile】链接。在个人资料编辑页面,尝试将用户名更改为已注册的另一个用户的用户名(以 belen为例)并提交,这将带来一个可怕的“Internal Server Error”页面:
这里写图片描述
这里写图片描述
在cmd里的打印(即运行应用程序的终端会话),将看到这个Error的堆栈跟踪(stack trace)。它在调试Error时很有用,因为它们会显示这个堆栈中的调用序列,一直到产生Error的行:

(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
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [13/Aug/2018 14:26:04] "GET /login?next=%2Fedit_profile HTTP/1.1" 200 -
127.0.0.1 - - [13/Aug/2018 14:26:08] "GET /login HTTP/1.1" 200 -
127.0.0.1 - - [13/Aug/2018 14:26:17] "POST /login HTTP/1.1" 302 -
127.0.0.1 - - [13/Aug/2018 14:26:17] "GET /index HTTP/1.1" 200 -
127.0.0.1 - - [13/Aug/2018 14:26:50] "GET /user/susan HTTP/1.1" 200 -
127.0.0.1 - - [13/Aug/2018 14:27:24] "GET /edit_profile HTTP/1.1" 200 -
[2018-08-13 14:28:45,549] ERROR in app: Exception on /edit_profile [POST]
Traceback (most recent call last):
  File "d:\microblog\venv\lib\site-packages\sqlalchemy\engine\base.py", line 1193, in _execute_context
    context)
  File "d:\microblog\venv\lib\site-packages\sqlalchemy\engine\default.py", line 509, in do_execute
    cursor.execute(statement, parameters)
sqlite3.IntegrityError: UNIQUE constraint failed: user.username
...
...

堆栈跟踪表明了bug是什么。目前应用程序允许用户更改用户名,且并不会验证新用户名是否跟系统中已有的其他用户名发生冲突。这个Error来自SQLAlchemy,它视图将新用户名写入数据库,但数据库拒绝了它,因为username列定义了unique=True

重要的是要注意,呈现给用户的错误页面没有提供有关错误的大量信息,这很好!我们绝对不希望用户知道崩溃是由于数据库错误,或我正在使用数据库,又或在我的数据库中某些表和字段名称引起。所有这些信息都应该保留在内部。

有一些事情远非理想。上述错误页面很难看,跟应用程序布局不匹配。终端上的日志不断刷新,导致重要的堆栈跟踪信息被淹没,而我们不得不不断回顾它,以免遗漏。当然,我们有一个bug需要修复(fix)。我们将解决所有这些问题,但首先,来讨论下Flask的调试模式

Debug模式(调试模式)

上述处理错误的方式对于在生产服务器上运行的系统来说非常有用。如果出现Error,用户会得到一个模糊的错误页面,并且错误的重要细节在服务器进程输出或日志文件中。

但在开发应用程序时,可启用调试模式,这是Flask在浏览器上直接运行一个友好调试器的模式。要激活调试模式,得先停止应用程序,然后设置以下环境变量

(venv) D:\microblog>set FLASK_DEBUG=1

在设置FLASK_DEBUG后,重新启动服务器。终端上的打印(输出)将与之前看到的略有不同:

(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: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 216-201-609
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

现在再次让应用程序崩溃,以便在浏览器中查看交互式调试器(the interactive debugger ):
这里写图片描述
调试器允许我们展开每个堆栈帧并查看相应的源代码。还可以在任何框架上打开一个Python提示符,执行任何有效的Python表达式,如检查变量的值。

决不在生产服务器以调试模式运行一个Flask应用程序 是非常重要的。调试器允许用户远程执行服务器中的代码,因此对于想侵入你的应用程序或服务器的恶意用户来说,这可能是一个意外礼物。作为额外的安全措施,在浏览器中运行的调试器开始锁定,并在第一次使用时将询问PIN号(flask run命令运行后可看到打印中的PIN号)

由于我们处于调试模式的主题,应该提到使用调试模式启用的第二个重要功能,即 重新加载器 reloader。这是一个非常有用的开发功能,可在修改源文件时自动重新启动应用程序。如果在调试模式下运行flask run命令,则可以在应用程序上运行,并且每次保存文件时,应用程序都将重新启动以获取新代码

自定义Error页面

Flask为应用程序提供了一种安装自己的错误页面的机制,如此,用户就不必看到默认、无聊的默认页面了。例如,为HTTP错误 404 和500定义自定义错误页面,这是两个最常见的错误页面。为其他错误定义页面的方式与此相同。

要声明自定义错误处理程序,得使用@errorhandler装饰器。在此,将错误处理程序放在一个新的app/errors.py模板中:
app/errors.py:自定义错误处理程序

from flask import render_template
from app import app,db

@app.errorhandler(404)
def not_found_error(error):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    db.session.rollback()
    return render_template('500.html'), 500

错误处理函数 与视图函数的工作方式非常相似。对于这俩个错误,将返回各自模板的内容。注意,两个函数都在模板后面返回第二个值,即错误代码编号。对于到目前为止,创建的所有视图函数,都不需要添加第二个返回值,因为默认值为200(成功响应的状态代码)。在这种情况下,这些是错误页面,所以希望响应的状态代码能够反映出来。

在数据库错误之后,可以调用500错误的错误处理程序,实际上是上面的用户名重复情况。为确保任何失败的数据库会话不会干扰模板触发的任何数据库访问,我们发出一个会话回滚(session rollback)。这将会使得会话重置为干净状态。

以下是404错误的模板:
app/templates/404.html:找不到错误模板

{% extends "base.html" %}

{% block content %}
    <h1>File Not Found</h1>
    <p>
        <a href="{{ url_for('index') }}">Back</a>
    </p>
{% endblock %}

这是500错误的模板:
app/templates/500.html:内部服务器错误模板

{% extends "base.html" %}

{% block content %}
    <h1>An unexpected error has occurred</h1>
    <p>The administrator has been notified. Sorry for the inconvenience!</p>
    <p>
        <a href="{{ url_for('index') }}">Back</a>
    </p>
{% endblock %}

两个模板都继承自base.html,因此错误页面具有与应用程序的常规页面相同的外观。

要使用Flask注册这些错误处理程序,需要在创建应用程序实例后导入新的app/errors.py模板:

app/__init__.py:导入错误处理程序

#...

from app import routes,models,errors

如果在终端会话中设置FLASK_DEBUG=0,然后再次触发重复的用户错误,将看到一个稍微友好的错误页面。

(venv) D:\microblog>set FLASK_DEBUG=0

(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
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
[2018-08-13 17:39:21,022] ERROR in app: Exception on /edit_profile [POST]
Traceback (most recent call last):
  File "d:\microblog\venv\lib\site-packages\sqlalchemy\engine\base.py", line 1193, in _execute_context
    context)
  File "d:\microblog\venv\lib\site-packages\sqlalchemy\engine\default.py", line 509, in do_execute
    cursor.execute(statement, parameters)
sqlite3.IntegrityError: UNIQUE constraint failed: user.username

这里写图片描述

用Email发送Error

Flask提供的默认错误处理的另一个问题是:没有通知,错误的堆栈跟踪被打印到终端,这意味着需要监视服务器进程的输出以发现错误。
当在开发期间运行应用程序时,这还算友好,但一旦将应用程序部署在生产服务器上,就没人会查看输出,因此需要更强大的解决方案。

对错误采取积极主动的态度非常重要。如果应用程序的生产版本发生错误,我想立马知道。因此,想到的第一个解决方案是将Flask设置为在发生错误后立即向我发送电子邮件,并在电子邮件正文中显示错误的堆栈跟踪

第一步是将电子邮件服务器详细信息添加到配置文件 config.py中:
microblog/config.py:电子邮件配置

import os

basedir = os.path.abspath(os.path.dirname(__file__))

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'you will never guess'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'sqlite:///' + os.path.join(basedir, 'app.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False

    MAIL_SERVER = os.environ.get('MAIL_SERVER')
    MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)
    MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    ADMINS = ['[email protected]']

电子邮件的配置变量包括:服务器、端口、启用加密链接的布尔标志、可选的用户名和密码。
5个配置变量来自它们的环境变量对应物。如果未在环境中设置电子邮件服务器,那么将使用它作为需要禁用电子邮件错误的标志。电子邮件服务器端口也可以在环境变量中给出,但如果未设置,则使用标准端口25。默认情况下不使用电子邮件服务器凭据,但可以根据需要提供。ADMINS配置变量是将收到错误报告的电子邮件地址的列表,以便自己的电子邮件地址应该在这个列表中。【稍后会将如何在环境中设置】

Flask使用Pythonlogging包来编写日志,这个包已经能够通过电子邮件发送日志。发送有关错误的电子邮件需要做的是将一个SMTPHandler实例添加到Flask记录器对象,即app.logger

app/__init__.py:通过电子邮件记录错误

import logging
from logging.handlers import SMTPHandler

# ...

if not app.debug:
    if app.config['MAIL_SERVER']:
        auth = None
        if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
            auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
        secure = None
        if app.config['MAIL_USE_TLS']:
            secure = ()
        mail_handler = SMTPHandler(
            mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
            fromaddr='no-reply@' + app.config['MAIL_SERVER'],
            toaddrs=app.config['ADMINS'], subject='Microblog Failure',
            credentials=auth, secure=secure)
        mail_handler.setLevel(logging.ERROR)
        app.logger.addHandler(mail_handler)

根据上述代码,在没有打开调试模式app.debug=False),且配置中存在邮件服务器(MAIL_SERVER)时,才会启用电子邮件日志记录器

由于必须处理许多电子邮件服务器中存在的可选安全选项,因此设置电子邮件记录器有点单调乏味。实际上上述代码创建了一个SMTPHandler实例,设置了它的级别,以便它只报告错误而不是警告,信息或调试消息,最后将它
Flask添加到app.logger对象中。

两种方法可测试此功能。最简单的方法是使用Python的SMTP调试服务器。这是一个虚拟的电子邮件服务器,它接收电子邮件,但不是发送它们,而是将它们打印到控制台。要运行此服务器,先打开第二个终端会话,并运行如下命令:

(venv) D:\microblog>python -m smtpd -n -c DebuggingServer localhost:8025

保持调试SMTP服务器运行,并返回到第一个终端,在环境中设置 set MAIL_SERVER=localhostMAIL_PORT=8025。确保FLASK_DEBUG变量设置为0,或根本不设置,因为应用程序不会在调试模式中发送电子邮件。运行应用程序,并在此触发SQLAlchemy错误,以查看运行模拟电子邮件服务器的终端会话如何显示具有完整堆栈跟踪错误的电子邮件。操作过程如下:第一个终端会话

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=localhost

(venv) D:\microblog>set MAIL_PORT=8025

(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
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

第二个终端会话

(venv) D:\microblog>python -m smtpd -n -c DebuggingServer localhost:8025
---------- MESSAGE FOLLOWS ----------
b'From: no-reply@localhost'
b'To: [email protected]'
b'Subject: Micblog Failure'
b'Date: Tue, 14 Aug 2018 09:47:40 +0800'
b'Content-Type: text/plain; charset="utf-8"'
b'Content-Transfer-Encoding: 7bit'
b'MIME-Version: 1.0'
b'X-Peer: ::1'
b''
b'Exception on /edit_profile [POST]'
b'Traceback (most recent call last):'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\base.py", line 1193, in _execute_context'
b'    context)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\default.py", line 509, in do_execute'
b'    cursor.execute(statement, parameters)'
b'sqlite3.IntegrityError: UNIQUE constraint failed: user.username'
b''
b'The above exception was the direct cause of the following exception:'
b''
b'Traceback (most recent call last):'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\flask\\app.py", line 2292, in wsgi_app'
b'    response = self.full_dispatch_request()'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\flask\\app.py", line 1815, in full_dispatch_request'
b'    rv = self.handle_user_exception(e)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\flask\\app.py", line 1718, in handle_user_exception'
b'    reraise(exc_type, exc_value, tb)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\flask\\_compat.py", line 35, in reraise'
b'    raise value'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\flask\\app.py", line 1813, in full_dispatch_request'
b'    rv = self.dispatch_request()'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\flask\\app.py", line 1799, in dispatch_request'
b'    return self.view_functions[rule.endpoint](**req.view_args)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\flask_login\\utils.py", line 261, in decorated_view'
b'    return func(*args, **kwargs)'
b'  File "D:\\microblog\\app\\routes.py", line 89, in edit_profile'
b'    db.session.commit()'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\scoping.py", line 153, in do'
b'    return getattr(self.registry(), name)(*args, **kwargs)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\session.py", line 943, in commit'
b'    self.transaction.commit()'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\session.py", line 467, in commit'
b'    self._prepare_impl()'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\session.py", line 447, in _prepare_impl'
b'    self.session.flush()'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\session.py", line 2254, in flush'
b'    self._flush(objects)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\session.py", line 2380, in _flush'
b'    transaction.rollback(_capture_exception=True)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\util\\langhelpers.py", line 66, in __exit__'
b'    compat.reraise(exc_type, exc_value, exc_tb)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\util\\compat.py", line 249, in reraise'
b'    raise value'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\session.py", line 2344, in _flush'
b'    flush_context.execute()'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\unitofwork.py", line 391, in execute'
b'    rec.execute(self)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\unitofwork.py", line 556, in execute'
b'    uow'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\persistence.py", line 177, in save_obj'
b'    mapper, table, update)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\persistence.py", line 768, in _emit_update_statements'
b'    execute(statement, multiparams)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\base.py", line 948, in execute'
b'    return meth(self, multiparams, params)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\sql\\elements.py", line 269, in _execute_on_connection'
b'    return connection._execute_clauseelement(self, multiparams, params)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\base.py", line 1060, in _execute_clauseelement'
b'    compiled_sql, distilled_params'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\base.py", line 1200, in _execute_context'
b'    context)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\base.py", line 1413, in _handle_dbapi_exception'
b'    exc_info'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\util\\compat.py", line 265, in raise_from_cause'
b'    reraise(type(exception), exception, tb=exc_tb, cause=cause)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\util\\compat.py", line 248, in reraise'
b'    raise value.with_traceback(tb)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\base.py", line 1193, in _execute_context'
b'    context)'
b'  File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\default.py", line 509, in do_execute'
b'    cursor.execute(statement, parameters)'
b"sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: user.username [SQL: 'UPDATE user SET username=?, about_me=? WHERE user.id = ?'] [parameters: ('belen', '', 1)] (Background on this error at: http://sqlalche.me/e/gkpj)"
------------ END MESSAGE ------------

这个功能的第二种测试方法是配置真实的电子邮件服务器。以下是使用Gmail账户的电子邮件服务器配置:

Gmail账户中的安全功能可能会阻止应用程序通过它发送电子邮件,除非我们明确允许“安全性较低的应用”访问您的Gmail帐户。在此处阅读此内容,如果担心帐户的安全性,可以创建仅为测试电子邮件而配置的辅助帐户,或者可以暂时启用安全性较低的应用程序来运行此测试然后还原到默认值。
这里写图片描述
https://myaccount.google.com/lesssecureapps
这里写图片描述
这表示Gmail开启了SMTP。

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.googlemail.com

(venv) D:\microblog>set MAIL_PORT=587

(venv) D:\microblog>set MAIL_USE_TLS=1

(venv) D:\microblog>set MAIL_USERNAME=<your-gmail-username>

(venv) D:\microblog>set MAIL_PASSWORD=<your-gmail-password>

(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
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

不知道是上述哪里设置错误。并未在本人的Gmail邮箱中收到邮件。改用国内QQ邮箱试试。步骤:
1)更改microblog/config.py

#...

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'you will never guess'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'sqlite:///' + os.path.join(basedir, 'app.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False

    MAIL_SERVER = os.environ.get('MAIL_SERVER')
    MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)
    MAIL_USE_SSL = os.environ.get('MAIL_USE_SSL') is not None
    MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    ADMINS = ['[email protected]']

2)开启QQ邮箱 SMTP:打开邮箱、设置、账户、开启SMTP服务器、生成授权码。授权码赋值给MAIL_PASSWORD环境变量。
3)配置环境变量,并运行程序。1为True,0为False。

(venv) D:\microblog>set MAIL_SERVER=smtp.qq.com

(venv) D:\microblog>set MAIL_PORT=465

(venv) D:\microblog>set MAIL_USE_SSL=1

(venv) D:\microblog>set MAIL_USE_TLS=0

(venv) D:\microblog>set [email protected]

(venv) D:\microblog>set MAIL_PASSWORD=授权码

(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
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

然而并没有接收到邮件!!以后再测试了!

正确设置查看第10章 QQ邮箱 发送电子邮件部分

日志(log)写到文件中

通过电子邮件接收错误虽然很好,但有时这还不够。有些失败条件既不是Python异常,也不是主要问题,但它们在调试时,也是有足够用处的。因此,为应用程序维护一个日志文件。

为了启用另一个基于文件类型RotatingFileHandler的日志记录器,需要以和电子邮件日志记录器类似的方式将其附加到应用程序的logger对象中。

app/__init__.py:电子邮件配置

# ...
from logging.handlers import RotatingFileHandler
import os

# ...

if not app.debug:
    # ...

    if not os.path.exists('logs'):
        os.mkdir('logs')
    file_handler = RotatingFileHandler('logs/microblog.log', maxBytes=10240,
                                       backupCount=10)
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
    file_handler.setLevel(logging.INFO)
    app.logger.addHandler(file_handler)

    app.logger.setLevel(logging.INFO)
    app.logger.info('Microblog startup')

logs目录中写入带有名称microblog.log的日志文件,如果它尚不存在,那么将创建这个日志文件。

这个RotatingFileHandler类很棒,因为它可切割、清理日志文件,以确保日志文件在应用程序运行很长一段时间也不会变得太大。在这种情况下,将日志文件大小限制为10kb,并且将最后10个日志文件保留为备份。

这个logging.Formatter类提供自定义格式的日志消息。由于这些消息正在写入到一个文件,我们希望它们可存储尽可能多的信息。所以我们使用的格式包括 时间戳、日志记录级别、消息、日志来源的源代码文件、行号

为使日志记录更有用,还将应用程序、日志记录级别降低到INFO。如果不熟悉日志记录类别,它们分别是DEBUG、INFO、WARNING、ERROR、CRITICAL(按严重程度递增)

日志文件第一个有趣的用途是 服务器每次启动时都会在日志中写入一行。当这个应用程序在生产服务器上运行时,这些日志数据将告诉我们服务器何时重新启动过。

修复重复的用户名bug

利用用户名重复这个BUG很久了,接下来将展示如何修复它。

应该还记得,RegistrationForm已实现了对用户名的验证,但是编辑表单的要求略有不同。在注册过程中,我们需要确保在表单中输入的用户名不存在于数据库中。在编辑个人资料表单中,我们必须执行相同的检查,但有一个例外。如果用户保存原有用户名不变,则验证应该允许,因为该用户名已分配给该用户。下面将展示如何为这个表单实现用户名验证:

app/forms.py:在编辑个人资料表单中验证用户名

class EditProfileForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    about_me = TextAreaField('About me', validators=[Length(min=0, max=140)])
    submit = SubmitField('Submit')

    #验证用户名
    def __init__(self, original_username, *args, **kwargs):
        super(EditProfileForm, self).__init__(*args, **kwargs)
        self.original_username = original_username

    def validate_username(self, username):
        if username.data != self.original_username:
            user = User.query.filter_by(username=self.username.data).first()
            if user is not None:
                raise ValidationError('Please use a different username.')

上述实现使用了一个自定义的验证方法,有一个重载的构造函数接受原始用户名作为参数。这个用户名保存为实例变量,并在validate_username()方法中进行检查。如果在表单中输入的用户名与原始用户名相同,那么就没必要检查数据库是否有重复了。

要使用这个新验证方法,还需要在对应的视图函数中添加原始用户名到表单的username参数中:

app/routes.py:在编辑个人资料表单中验证用户名

#...
@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
    form = EditProfileForm(current_user.username)
    #...

现在修复了这个bug,并在大多数情况下,在编辑个人资料表单中出现用户名重复的提交将被友好地阻止。不过这不是一个完美的解决方案,因为当两个或多个进程同时访问数据库时,它可能不起作用。在这种情形下,竞争条件可能导致验证通过,但稍后当重命名时,数据库已经被另一个进程更改,并且无法重命名用户。除了非常繁忙的具有大量服务器进程的应用程序之外,这种情况不太可能发生,所以现在我们不用为此担心。

flask run运行程序,再次重现错误,查看新表单验证方法如何阻止这个bug。
这里写图片描述
尝试更改不重名的username,效果:
这里写图片描述
查看数据库,看是否成功更改:

(venv) D:\microblog>sqlite3 app.db
SQLite version 3.16.2 2017-01-06 16:32:41
Enter ".help" for usage hints.
sqlite> select * from user;
1|susan2018|susan@example.com|pbkdf2:sha256:50000$OONOkVyy$8d008c6647ab95a5793cf60bf57eaa3bb1123d6e5b3135c5cc5e42e02eddae32|I rename my name.|2018-08-14 09:09:35.986028
2|belen|belen@example.com|pbkdf2:sha256:50000$PEDt5NxS$cf6c958c97b6ad28d9495d138cb5a310f6f2389534b0cafa3002dd3cec9af9d1|学 习Flask超级教程,Python Web开发学习,坚持!|2018-08-13 03:54:02.884780
sqlite> .quit

目前为止,项目结构:

microblog/
    app/
        templates/
            _post.html
            404.html
            500.html
            base.html
            edit_profile.html
            index.html
            login.html
            register.html
            user.html
        __init__.py
        errors.py
        forms.py
        models.py
        routes.py
    logs/
        microblog.log
    migrations/
    venv/
    app.db
    config.py
    microblog.py

参考:
作者博客

如需转载请注明出处。

猜你喜欢

转载自blog.csdn.net/weixin_38256474/article/details/81590339
今日推荐