原创:志学Python志学Python

01flask中错误处理机制

在Flask应用中爆发错误时会发生哪些?得到答案的最好的方式就是亲身体验一下。启动应用,并确保起码有两个用户注册,以其中一个用户身分登入,打开个人主页并单击“编辑”链接。在个人资料编辑器中,尝试将用户名修改为早已注册的另一个用户的用户名,boom!(爆燃声)这将带来一个可怕的“InternalServerError”页面:

linux修改环境变量 系统崩了_修改linux的环境变量_linux系统修改时间

假如你查看运行应用的终端会话,将见到stacktrace(堆栈跟踪)。堆栈跟踪在调试错误时特别有用,由于它们显示堆栈中调用的次序,仍然到形成错误的行:

(venv) $ flask run * Serving Flask app "microblog" * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)[2017-09-14 22:40:02,027] ERROR in app: Exception on /edit_profile [POST]Traceback (most recent call last): File "/home/miguel/microblog/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1182, in _execute_context context) File "/home/miguel/microblog/venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 470, in do_execute cursor.execute(statement, parameters)sqlite3.IntegrityError: UNIQUE constraint failed: user.username

堆栈跟踪指示了BUG在何处。本应用容许用户修改用户名,但却没有验证所选的新用户名与系统中已有的其他用户有没有冲突。这个错误来自SQLAlchemy,它尝试将新的用户名写入数据库,但数据库拒绝了它,由于username列是用unique=True定义的。

值得注意的是,提供给用户的错误页面并没有提供关于错误的丰富信息,这是正确的做法。我绝对不希望用户晓得崩溃是由数据库错误造成的,或则我正在使用哪些数据库,或则是我的数据库中的一些表和数组名称。所有这种信息都应当对外保密。

并且也有一些不尽人意之处。错误页面破旧不堪,与应用布局不匹配。终端上的日志不断刷新,引起重要的堆栈跟踪信息被吞没,但我却须要不断回顾它,以免有漏网之鱼。其实,我有一个BUG须要修补。我将解决所有的这种问题,但首先,让我们来说说Flask的调试模式。

02调试模式

你在里面听到的处理错误的方法对在生产服务器上运行的系统十分有用。倘若出现错误,用户将得到一个委婉的错误页面(虽然我准备使这个错误页面更友好),错误的重要细节在服务器进程输出或储存到日志文件中。

然而当你正在开发应用时,可以启用调试模式linux修改环境变量 系统崩了,它是Flask在浏览器上直接运行一个友好调试器的模式。要激活调试模式,请停止应用程序,之后设置以下环境变量:

(venv) $ export FLASK_DEBUG=1

假如你使用MicrosoftWindows,记得将export替换成set。

设置环境变量FLASK_DEBUG后,重启服务。相比之前,终端上的输出信息会有所变化:

(venv) microblog2 $ flask run * Serving Flask app "microblog" * Forcing debug mode on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 177-562-960

如今让应用再度崩溃linux修改文件名,以在浏览器中查看交互式调试器:

修改linux的环境变量_linux系统修改时间_linux修改环境变量 系统崩了

该调试器容许你展开每位堆栈框来查看相应的源代码上下文。你也可以在任意堆栈框上打开Python提示符并执行任何有效的Python表达式,比如检测变量的值。

永远不要在生产服务器上以调试模式运行Flask应用,这一点十分重要。调试器容许用户远程执行服务器中的代码,因而对于想要溶入应用或服务器的恶意用户来说,这可能是开门揖盗。作为附加的安全举措,运行在浏览器中的调试器开始被锁定,但是在第一次使用时会要求输入一个PIN码(你可以在flaskrun命令的输出中看见它)。

提到调试模式的话题,我不得不提及的第二个重要的调试模式下的功能linux修改环境变量 系统崩了,就是重载器。这是一个十分有用的开发功能,可以在源文件被更改时手动重启应用。假如在调试模式下运行flaskrun,则可以在开发应用时,每每保存文件,应用就会重新启动以加载新的代码。

03自定义错误页面

Flask为应用提供了一个机制来自定义错误页面,这样用户就不必读到简单而乏味的默认页面。作为反例,让我们为HTTP的404错误和500错误(两个最常见的错误页面)设置自定义错误页面。为其他错误设置页面的形式与之相同。

使用@errorhandler装潢器来申明一个自定义的错误处理器。我将把我的错误处理程序置于一个新的app/errors.py模块中。

修改linux的环境变量_linux系统修改时间_linux修改环境变量 系统崩了

from flask import render_templatefrom 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错误的错误处理程序应该在引起数据库错误后调用,而前面的用户名重复实际上就是这些情况。为了确保任何失败的数据库会话不会干扰模板触发的其他数据库访问,我执行会话回滚来将会话重置为干净的状态。

404错误的模板如下:

{% extends "base.html" %}
{% block content %} 

File Not Found

Back

{% endblock %}

500错误的模板如下:

{% extends "base.html" %}
{% block content %} 

An unexpected error has occurred

The administrator has been notified. Sorry for the inconvenience!

Back

{% endblock %}

这两个模板都从base.html基础模板承继而至,所以错误页面与应用的普通页面有相同的外形布局。

为了让那些错误处理程序在Flask中注册,我须要在应用实例创建后导出新的app/errors.py模块。app/__init__.py:

# ...
from app import routes, models, errors

linux系统修改时间_linux修改环境变量 系统崩了_修改linux的环境变量

04通过电子短信发送错误

linux修改环境变量 系统崩了_linux系统修改时间_修改linux的环境变量

Flask提供的默认错误处理机制的另一个问题是没有通知机制,错误的堆栈跟踪只是被复印到终端,这意味着须要监视服务器进程的输出才会发觉错误。在开发时,这是十分好的,而且一旦将应用布署在生产服务器上,没有人会关心输出,因而须要采用更强悍的解决方案。

我觉得对错误发觉采取积极主动的心态是极其重要的。假如生产环境的应用发生错误,我想立即晓得。所以我的第一个解决方案是配置Flask在发生错误以后立刻向我发送一封电子电邮,短信正文中包含错误堆栈跟踪的正文。

第一步,添加短信服务器的信息到配置文件中:

class Config(object): # ... 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 = ['your-email@example.com']

电子电邮的配置变量包括服务器和端口,启用加密联接的布尔标记以及可选的用户名和密码。这五个配置变量来始于环境变量。假如电子电邮服务器没有在环境中设置,这么我将禁用电子电邮功能。电子电邮服务器端口也可以在环境变量中给出,并且假如没有设置,则使用标准端口25。电子短信服务器账簿默认不使用,但可以依据须要提供。ADMINS配置变量是将收到错误报告的电子电邮地址列表,所以你自己的电子电邮地址应当在该列表中。

Flask使用Python的logging包来写它的日志,并且这个包早已才能通过电子电邮发送日志了。我所须要做的就是为Flask的日志对象app.logger添加一个SMTPHandler的实例:

import loggingfrom 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)

如你所见,仅当应用未以调试模式运行,且配置中存在电邮服务器时,我就会启用电子电邮日志记录器。

设置电子电邮日志记录器的步骤由于处理安全可选项而稍显繁杂。本质上,前面的代码创建了一个SMTPHandler实例,设置它的级别,便于它只报告错误及更严重级别的信息,而不是警告,常规信息或调试消息,最后将它附加到Flask的app.logger对象中。

有两种方式来测试此功能。最简单的就是使用Python的SMTP调试服务器。这是一个模拟的电子电邮服务器,它接受电子电邮,之后复印到控制台。要运行此服务器,请打开第二个终端会话并在其上运行以下命令

(venv) $ python -m smtpd -n -c DebuggingServer localhost:8025

要用这个模拟电邮服务器来测试应用,这么你将设置MAIL_SERVER=localhost和MAIL_PORT=8025。

译者注:本段中去不仅说明设置该端口须要管理员权限的部份,由于这和实际情况不符。原文如下:Totesttheapplicationwiththisserver,thenyouwillsetMAIL_SERVER=localhostandMAIL_PORT=8025.IfyouareonaLinuxorMacOSsystem,youwilllikelyneedtoprefixthecommandwithsudo,sothatitcanexecutewithadministrationprivileges.IfyouareonaWindowssystem,youmayneedtoopenyourterminalwindowasanadministrator.Administratorrightsareneededforthiscommandbecauseportsbelow1024areadministrator-onlyports.Alternatively,youcanchangetheporttoahigherportnumber,say5025,andsetMAIL_PORTvariabletoyourchosenportintheenvironment,andthatwillnotrequireadministrationrights.

保持调试SMTP服务器运行并返回到第一个终端,在环境中设置exportMAIL_SERVER=localhost和MAIL_PORT=8025(假如使用的是MicrosoftWindows,则使用set而不是export)。确保FLASK_DEBUG变量设置为0或则根本不设置,由于应用不会在调试模式中发送电子电邮。运行该应用并再度触发SQLAlchemy错误,以查看运行模拟电子电邮服务器的终端会话怎么显示具有完整堆栈跟踪错误的电子电邮。

这个功能的第二个测试方式是配置一个真正的电子电邮服务器。以下是使用你的Gmail账户的电子电邮服务器的配置:

export MAIL_SERVER=smtp.googlemail.comexport MAIL_PORT=587export MAIL_USE_TLS=1export MAIL_USERNAME=export MAIL_PASSWORD=

假如你使用的是MicrosoftWindows,记住在每一条句子中用set替换掉export。

Gmail账户中的安全功能可能会制止应用通过它发送电子电邮,除非你明晰准许“安全性较低的应用程序”访问你的Gmail账户。可以阅读此处来了解具体情况,若果你害怕账户的安全性,可以创建一个辅助邮箱账户,配置它来仅用于测试电子电邮功能,或则你可以暂时启用容许不太安全的应用程序来运行此测试,完成后恢复为默认值。

05记录日志到文件中

通过电子短信来接收错误提示十分棒,但在其他场景下,有时侯就有些不足了。有些错误条件既不是一个Python异常又不是重大车祸,而且她们在调试的时侯也是有足够好处的。因此,我将会为本应用维持一个日志文件。

为了启用另一个基于文件类型RotatingFileHandler的日志记录器,须要以和电子电邮日志记录器类似的方法将其附加到应用的logger对象中。app/__init__.py:

# ...from logging.handlers import RotatingFileHandlerimport 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,并只保留最后的十个日志文件作为备份。

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

为了使日志记录更有用,我还将应用和文件日志记录器的日志记录级别增加到INFO级别。假如你不熟悉日志记录类别,则根据严重程度递增的次序来认识它们就行了,分别是DEBUG、INFO、WARNING、ERROR和CRITICAL。

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

06修补用户名重复的BUG

借助用户名重复BUG那么久,如今时侯向你展示怎样修补它了。

你是否还记得linux系统介绍,RegistrationForm早已实现了对用户名的验证,并且编辑表单的要求稍有不同。在注册期间,我须要确保在表单中输入的用户名不存在于数据库中。在编辑个人资料表单中,我必须做同样的检测,但有一个例外。假如用户不改变原始用户名,这么验证应当准许,由于该用户名早已被分配给该用户。下边你可以看见我为这个表单实现了用户名验证:

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.route('/edit_profile', methods=['GET', 'POST'])@login_requireddef edit_profile(): form = EditProfileForm(current_user.username) # ...

如今这个BUG早已修补了,大多数情况下,之后在编辑个人资料时出现用户名重复的递交将被友好地制止。但这不是一个完美的解决方案,由于当两个或更多进程同时访问数据库时,这可能不起作用。如果存在验证通过的进程A和B都尝试更改用户名为同一个,但稍后进程A尝试重命名时,数据库已被进程B修改,未能重命名为该用户名,会再度引起数据库异常。不仅有好多服务器进程而且十分忙碌的应用之外,这些情况是不太可能的,所以如今我不会因此担忧。

此时,你可以尝试再度再现该错误,以了解新的表单验证方式怎样避免该错误。

最后,我自己是一名从事了多年开发的Python老程序员,离职目前在做自己的Python私人订制课程,明年年初我花了一个月整理了一份最适宜2019年学习的Python学习干货,可以献给每一位喜欢Python的男子伴,想要获取的可以关注我的头条号并在后台私信我:01,即可免费获取。

本文原创地址:https://www.linuxprobe.com/pzxpzfyyzbfc.html编辑:刘遄,审核员:暂无