zoukankan      html  css  js  c++  java
  • 12.flask博客项目实战七之错误处理

    配套视频教程

    本文B站配套视频教程

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

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

    在Flask中的错误处理

    在Flask应用程序中发生error时会发生什么?找到问题的最佳方法是亲自体验,即重现它。启动应用程序,并确保已有两个注册用户。用其中一个用户身份登录后(在此以用户名+密码 susan cat为例),打开【Profile】页面并点击【Edit you profile】链接。在个人资料编辑页面,尝试将用户名更改为已注册的另一个用户的用户名(以 belen为例)并提交,这将带来一个可怕的“Internal Server Error”页面:
    image.png

    image.png

    在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:microblogvenvlibsite-packagessqlalchemyenginease.py", line 1193, in _execute_context
        context)
      File "d:microblogvenvlibsite-packagessqlalchemyenginedefault.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 ):
    image.png

    调试器允许我们展开每个堆栈帧并查看相应的源代码。还可以在任何框架上打开一个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:microblogvenvlibsite-packagessqlalchemyenginease.py", line 1193, in _execute_context
        context)
      File "d:microblogvenvlibsite-packagessqlalchemyenginedefault.py", line 509, in do_execute
        cursor.execute(statement, parameters)
    sqlite3.IntegrityError: UNIQUE constraint failed: user.username
    
    

    image.png

    日志(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。
    image.png

    尝试更改不重名的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
    
    

    参考
    https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-vii-error-handling

  • 相关阅读:
    Vmware Vsphere WebService之vijava 开发一-vcenter连接、及集群信息获取
    Vmware Vsphere WebService SDK开发(第一讲)-基本知识学习
    RabbitMQ安装以及java使用(二)
    redis单机安装以及集群搭建(redis-6.2.6)
    Spring Cloud Gateway中Filter获取Request Body的几种方式
    电子发票插入微信卡包之PDF上传
    Elasticsearch集群搭建详解
    微服务的设计原则
    centos 7.4 64位 mysql的安装
    RabbitMQ安装以及java使用(一)
  • 原文地址:https://www.cnblogs.com/songboriceboy/p/13852028.html
Copyright © 2011-2022 走看看