https://mp.weixin.qq.com/s?__biz=MzU2NTc1MTc5MQ==&mid=2247484688&idx=1&sn=2d929fb15d90e568595ecf2c81f7720b&chksm=fcb7bf90cbc03686593da90811693fdd612561616e7f509cf77bf5f4b05ca007666c1b6ea043#rd点击上方蓝字关注我们欢迎关注我的公众号、知雪Python01flaskFlask应用程序中的错误处理机制爆发时会发生什么?获得答案的最好方法是亲身体验。启动应用程序并确保至少有两个用户注册,以其中一个用户身份登录,打开个人资料并单击“编辑”链接。在配置文件编辑器中,尝试将用户名更改为另一个已注册用户的用户名,砰!(爆炸)这将打开一个可怕的“内部服务器错误”页面:如果您查看应用程序运行的终端会话,您将看到堆栈跟踪。堆栈跟踪在调试错误时非常有用,因为它们显示堆栈中的调用顺序,一直到产生错误的行:(venv)$flaskrun*ServingFlaskapp"microblog"*Runningonhttp://127.0.0.1:5000/(按CTRL+C退出)[2017-09-1422:40:02,027]应用程序错误:/edit_profile[POST]上的异常[POST]回溯(最近调用最后):文件“/home/miguel/microblog/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py”,第1182行,在_execute_context上下文中)文件“/home/miguel/microblog/venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py”,第470行,在do_executecursor.execute(statement,parameters)sqlite3.IntegrityError:UNIQUEconstraintfailed:user.username堆栈跟踪表明BUG在哪里。该应用程序允许用户更改用户名,但不会验证所选的新用户名是否与系统中已有的其他用户不冲突。这个错误来自SQLAlchemy,它试图向数据库写入一个新的用户名,但是数据库拒绝了它,因为用户名列是用unique=True定义的。值得注意的是,呈现给用户的错误页面不会提供有关错误的丰富信息,这是正确的做法。我绝对不想让用户知道崩溃是由数据库错误引起的,或者我使用的是什么数据库,或者我数据库中的某些表和字段名称。所有这些信息都应该保密。但也有一些不尽如人意的地方。错误页面很破旧,与应用程序布局不匹配。终端上的日志不断刷新,淹没了重要的堆栈跟踪,但我需要不断回头查看它,以防有任何遗漏。当然,我有一个需要修复的错误。我将解决所有这些问题,但首先,让我们谈谈Flask的调试模式。02调试模式上面看到的处理错误的方式对于运行在生产服务器上的系统非常有用。如果发生错误,用户将得到一个神秘的错误页面(尽管我打算让这个错误页面更加用户友好),错误的重要细节由服务器进程输出或存储在日志文件中。但是当您开发应用程序时,您可以启用调试模式,这是Flask直接在浏览器上运行友好调试器的模式。要激活调试模式,请停止应用程序,然后设置以下环境变量:(venv)$exportFLASK_DEBUG=1如果您使用MicrosoftWindows,请记住将export替换为set。设置环境变量FLASK_DEBUG后,重启服务。与之前相比,终端输出信息会发生变化:(venv)microblog2$flaskrun*ServingFlaskapp"microblog"*Forcingdebugmodeon*Runningonhttp://127.0.0.1:5000/(PressCTRL+Ctoquit)*使用stat重新启动*调试器处于活动状态!*调试器PIN:177-562-960现在让应用程序再次崩溃以在浏览器中查看交互式调试器:此调试器允许您展开每个堆栈帧以查看相应的源代码上下文。您还可以在任何堆栈帧上打开Python提示符并执行任何有效的Python表达式,例如检查变量的值。切勿在生产服务器上以调试模式运行Flask应用程序,这一点非常重要。调试器允许用户在服务器中远程执行代码,因此这可能为想要渗透到应用程序或服务器的恶意用户打开大门。作为一项额外的安全措施,在浏览器中运行的调试器最初是锁定的,并且会在首次使用时要求输入PIN(您可以在flaskrun命令的输出中看到这一点)。说到debug模式这个话题,不得不提的debug模式中第二个重要的功能就是reloader。这是一个非常有用的开发功能,可以在修改源文件时自动重新启动应用程序。如果你在调试模式下运行flask,你就可以开发应用程序,每当你保存文件时,应用程序将重新启动以加载新代码03自定义错误页面Flask提供了一种机制让应用程序自定义错误页面,以便用户可以不必看到一个简单而乏味的默认页面。例如,让我们为HTTP404错误和500错误(两个最常见的错误页面)设置自定义错误页面。以相同的方式为其他错误设置页面。使用@errorhandler装饰器声明自定义错误处理程序。我会将我的错误处理程序放在一个新的app/errors.py模块中。从flaskimportrender_templatefromappimportapp,db@app.errorhandler(404)defnot_found_error(error):returnrender_template('404.html'),404@app.errorhandler(500)definternal_error(error):db.session.rollback()returnrender_template('500.html'),500错误函数和view函数很相似。对于这两个错误,我将返回它们各自模板的内容。请注意,这两个函数在模板之后返回第二个值,即错误代码编号。对于我之前创建的所有视图函数,我不需要添加第二个返回值,因为我想要的是默认值200(成功响应的状态代码)。在这里,这些是错误页面,所以我希望响应的状态代码能够反映这一点。出现数据库错误后,应调用500错误的错误处理程序,上面的重复用户名实际上就是这种情况。为确保任何失败的数据库会话不会干扰模板触发的其他数据库访问,我执行会话回滚以将会话重置为干净状态。404错误的模板如下:{%extends"base.html"%}{%blockcontent%}
FileNotFound
返回
{%endblock%}500错误模板如下:{%extends"base.html"%}{%blockcontent%}
Anunexpectederrorhas已发生
已通知管理员。抱歉给您带来不便!
返回
{%endblock%}这两个模板继承自base.html基本模板,因此错误页面与应用程序的正常页面具有相同的外观和布局。为了在Flask中注册这些错误处理程序,我需要在创建应用程序实例后导入新的app/errors.py模块。app/__init__.py:#...fromappimportroutes,models,errors04EmailerrorsFlask提供的默认错误处理的另一个问题是没有通知机制,错误的堆栈跟踪只是打印到终端,这意味着需要监视服务器进程的输出以发现错误。在开发过程中,这很好,但是一旦将应用程序部署到生产服务器上,就没有人关心输出,因此需要更健壮的解决方案。我认为主动发现错误非常重要。如果生产应用程序出现问题,我想立即知道。所以我的第一个解决方案是将Flask配置为在发生错误后立即向我发送一封电子邮件,其中包含错误堆栈跟踪的文本。第一步是将邮件服务器信息添加到配置文件中:classConfig(object):#...MAIL_SERVER=os.environ.get('MAIL_SERVER')MAIL_PORT=int(os.environ.get('MAIL_PORT')或25)MAIL_USE_TLS=os.environ.get('MAIL_USE_TLS')不是无@example.com']电子邮件包括服务器和端口、启用加密连接的布尔标志以及可选的用户名和密码。这五个配置变量派生自环境变量。如果环境中未设置电子邮件服务器,那么我将禁用电子邮件功能。电子邮件服务器端口也可以在环境变量中给出,但如果未设置,则使用标准端口25。默认情况下不使用电子邮件服务器凭据,但可以根据需要提供。ADMINS配置变量是将接收错误报告的电子邮件地址列表,因此您自己的电子邮件地址应该在该列表中。Flask使用Python的logging包来编写它的日志,并且这个包已经具有通过电子邮件发送日志的能力。我需要做的就是将SMTPHandler的实例添加到Flask的日志对象app.logger:importloggingfromlogging.handlersimportSMTPHandler#...ifnotapp.debug:ifapp.config['MAIL_SERVER']:auth=None如果app.config['MAIL_USERNAME']或app.config['MAIL_PASSWORD']:auth=(app.config['MAIL_USERNAME'],app.config['MAIL_PASSWORD'])secure=None如果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='微博失败',credentials=auth,secure=secure)mail_handler.setLevel(logging.ERROR)app.logger.addHandler(mail_handler)可以看到,只有当应用程序未在调试模式下运行并且配置中有邮件服务器时,我只启用电子邮件记录器。由于处理安全选项,设置电子邮件记录器的步骤有点乏味。本质上,上面的代码创建了一个SMTPHandler实例,设置它的级别,以便它只报告错误和更严重的消息,而不是警告、一般信息或调试消息,最后将它附加到Flask的app.logger对象中间。有两种方法可以测试此功能。最简单的是使用Python的SMTP调试服务器。这是一个模拟的电子邮件服务器,它接受电子邮件并打印到控制台。要运行此服务器,请打开第二个终端会话并在其上运行以下命令:(venv)$python-msmtpd-n-cDebuggingServerlocalhost:8025要使用此模拟邮件服务器测试应用程序,您可以设置MAIL_SERVER=localhost和MAIL_PORT=8025。译者注:本段中,去掉了需要管理员权限设置该端口的部分,因为与实际情况不符。原文如下:要使用此服务器测试应用程序,则设置MAIL_SERVER=localhost和MAIL_PORT=8025。如果您使用的是Linux或MacOS系统,您可能需要在命令前加上sudo前缀,以便它可以以管理权限执行。如果您使用的是Windows系统,您可能需要以管理员身份打开终端窗口。此命令需要管理员权限,因为1024以下的端口是管理员专用端口。或者,您可以将端口更改为更高的端口号,例如5025,并将MAIL_PORT变量设置为您在环境中选择的端口,这不需要管理权限。保持调试SMTP服务器运行并返回到第一个终端,在环境8025中设置exportMAIL_SERVER=localhost和MAIL_PORT=(如果您使用的是MicrosoftWindows,请使用set而不是export)。确保FLASK_DEBUG变量设置为0或根本不设置,因为应用程序不会在调试模式下发送电子邮件。运行应用程序并再次触发SQLAlchemy错误,以查看运行模拟电子邮件服务器的终端会话如何显示包含错误的完整堆栈跟踪的电子邮件。测试此功能的第二种方法是配置真实的电子邮件服务器。以下是使用您的Gmail帐户的电子邮件服务器的配置:exportMAIL_SERVER=smtp.googlemail.comexportMAIL_PORT=587exportMAIL_USE_TLS=1exportMAIL_USERNAME=
exportMAIL_PASSWORD=如果对于MicrosoftWindows,请记住在每个语句中将export替换为set。您的Gmail帐户中的一项安全功能可能会阻止应用程序通过它发送电子邮件,除非您明确允许“安全性较低的应用程序”访问您的Gmail帐户。您可以阅读此处了解,如果您担心您帐户的安全,您可以创建一个恢复电子邮件帐户并将其配置为仅用于测试电子邮件功能,或者您可以暂时启用允许安全性较低的应用程序运行此功能测试并在完成后恢复为默认值。05记录到文件并通过电子邮件接收错误通知很棒,但在其他场景中,有时这还不够。有些错误条件既不是Python异常也不是致命事故,但在调试时它们足够有用。为此,我将为该应用程序维护一个日志文件。为了启用另一个基于文件类型RotatingFileHandler的记录器,它需要以类似于电子邮件记录器的方式附加到应用程序的记录器对象。app/__init__.py:#...fromlogging.handlersimportRotatingFileHandlerimportos#...如果不是app.debug:#...如果不是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('微博启动')日志文件的存放路径位于顶层目录,相对路径为logs/microblog.log。如果它不存在,它将被创建。RotatingFileHandler类很棒,因为它对日志文件进行切片和清理,以确保在应用程序长时间运行时日志文件不会变得太大。在这里,我将日志文件的大小限制为10KB,并且只保留最后十个日志文件作为备份。logging.Formatter类为日志消息提供自定义格式。由于这些消息被写入文件,我希望它们存储尽可能多的信息。所以我使用的格式包括时间戳、日志记录级别、消息、源代码文件和日志来自的行号。为了使日志记录更有用,我还将应用程序和文件记录器的日志记录级别降低到INFO级别。如果您不熟悉日志记录类别,只需按照严重性的递增顺序了解它们:DEBUG、INFO、WARNING、ERROR和CRITICAL。日志文件的第一个有趣用途是每次服务器启动时,它都会向日志中写入一行。当此应用程序在生产服务器上运行时,此日志数据将告诉您服务器何时重新启动。06修复用户名重复BUG用户名重复BUG被利用了这么久,是时候告诉你如何修复它了。如果您还记得,RegistrationForm已经实现了对用户名的验证,但编辑表单的要求略有不同。在注册过程中,我需要确保数据库中不存在表单中输入的用户名。在编辑配置文件表单中,我必须做同样的检查,但有一个例外。如果用户不更改原始用户名,则身份验证应该允许,因为该用户名已分配给该用户。下面你可以看到我为这个表单实现了用户名验证:=0,max=140)])submit=SubmitField('Submit')def__init__(self,original_username,*args,**kwargs):super(EditProfileForm,self).__init__(*args,**kwargs)自我。original_username=original_usernamedefvalidate_username(self,username):ifusername.data!=self.original_username:user=User.query.filter_by(username=self.username.data).first()ifuserisnotNone:raiseValidationError('请使用不同的用户名。')此实现使用自定义验证方法,该方法接受表单中的用户名作为参数。此用户名存储为实例变量,并在validate_username()方法中进行验证。如果在表单中输入的用户名与原始用户名相同,则无需检查数据库是否重复。为了让新的验证方式生效,我需要在对应的视图函数中将当前用户名添加到表单的用户名字段:@app.route('/edit_profile',methods=['GET','POST'])@login_requireddefedit_profile():form=EditProfileForm(current_user.username)#...现在这个错误已经修复,在大多数情况下,将来在编辑配置文件时提交重复的用户名将被友好地阻止。但这不是一个完美的解决方案,因为当两个或多个进程同时访问数据库时,这可能不起作用。如果有进程A和B验证通过,都尝试修改同一个用户名,但是后来进程A尝试重命名时,数据库已经被进程B更改,无法重命名为用户名,数据库异常将再次被抛出。这种情况不太可能发生,除非应用程序有很多服务器进程并且非常繁忙,所以我暂时不担心。此时,您可以尝试再次重现该错误,看看新的表单验证方法如何防止它。◆带你认识flask用户登录◆带你认识flask中的数据库◆带你认识flaskwebform◆带你认识flask模板