原創(chuàng): 志學(xué)Python 志學(xué)Python
01flask 中錯(cuò)誤處理機(jī)制
在Flask應(yīng)用中爆發(fā)錯(cuò)誤時(shí)會(huì)發(fā)生什么?得到答案的最好的方法就是親身體驗(yàn)一下。啟動(dòng)應(yīng)用,并確保至少有兩個(gè)用戶注冊(cè),以其中一個(gè)用戶身份登錄,打開個(gè)人主頁并單擊“編輯”鏈接。在個(gè)人資料編輯器中,嘗試將用戶名更改為已經(jīng)注冊(cè)的另一個(gè)用戶的用戶名,boom!(爆炸聲) 這將帶來一個(gè)可怕的“Internal Server Error”頁面:
如果你查看運(yùn)行應(yīng)用的終端會(huì)話,將看到stack trace(堆棧跟蹤)。堆棧跟蹤在調(diào)試錯(cuò)誤時(shí)非常有用,因?yàn)樗鼈冿@示堆棧中調(diào)用的順序,一直到產(chǎn)生錯(cuò)誤的行:
(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在何處。本應(yīng)用允許用戶更改用戶名,但卻沒有驗(yàn)證所選的新用戶名與系統(tǒng)中已有的其他用戶有沒有沖突。這個(gè)錯(cuò)誤來自SQLAlchemy,它嘗試將新的用戶名寫入數(shù)據(jù)庫,但數(shù)據(jù)庫拒絕了它,因?yàn)閡sername列是用unique=True定義的。
值得注意的是,提供給用戶的錯(cuò)誤頁面并沒有提供關(guān)于錯(cuò)誤的豐富信息,這是正確的做法。我絕對(duì)不希望用戶知道崩潰是由數(shù)據(jù)庫錯(cuò)誤引起的,或者我正在使用什么數(shù)據(jù)庫,或者是我的數(shù)據(jù)庫中的一些表和字段名稱。所有這些信息都應(yīng)該對(duì)外保密。
但是也有一些不盡人意之處。錯(cuò)誤頁面簡陋不堪,與應(yīng)用布局不匹配。終端上的日志不斷刷新,導(dǎo)致重要的堆棧跟蹤信息被淹沒,但我卻需要不斷回顧它,以免有漏網(wǎng)之魚。當(dāng)然,我有一個(gè)BUG需要修復(fù)。我將解決所有的這些問題,但首先,讓我們來談?wù)凢lask的調(diào)試模式。
02 調(diào)試模式
你在上面看到的處理錯(cuò)誤的方式對(duì)在生產(chǎn)服務(wù)器上運(yùn)行的系統(tǒng)非常有用。如果出現(xiàn)錯(cuò)誤,用戶將得到一個(gè)隱晦的錯(cuò)誤頁面(盡管我打算使這個(gè)錯(cuò)誤頁面更友好),錯(cuò)誤的重要細(xì)節(jié)在服務(wù)器進(jìn)程輸出或存儲(chǔ)到日志文件中。
但是當(dāng)你正在開發(fā)應(yīng)用時(shí),可以啟用調(diào)試模式,它是Flask在瀏覽器上直接運(yùn)行一個(gè)友好調(diào)試器的模式。要激活調(diào)試模式,請(qǐng)停止應(yīng)用程序,然后設(shè)置以下環(huán)境變量:
(venv) $ export FLASK_DEBUG=1
如果你使用Microsoft Windows,記得將export替換成set。
設(shè)置環(huán)境變量FLASK_DEBUG后,重啟服務(wù)。相比之前,終端上的輸出信息會(huì)有所變化:
(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
現(xiàn)在讓應(yīng)用再次崩潰,以在瀏覽器中查看交互式調(diào)試器:
該調(diào)試器允許你展開每個(gè)堆??騺聿榭聪鄳?yīng)的源代碼上下文。你也可以在任意堆??蛏洗蜷_Python提示符并執(zhí)行任何有效的Python表達(dá)式,例如檢查變量的值。
永遠(yuǎn)不要在生產(chǎn)服務(wù)器上以調(diào)試模式運(yùn)行Flask應(yīng)用,這一點(diǎn)非常重要。調(diào)試器允許用戶遠(yuǎn)程執(zhí)行服務(wù)器中的代碼,因此對(duì)于想要滲入應(yīng)用或服務(wù)器的惡意用戶來說,這可能是開門揖盜。作為附加的安全措施,運(yùn)行在瀏覽器中的調(diào)試器開始被鎖定,并且在第一次使用時(shí)會(huì)要求輸入一個(gè)PIN碼(你可以在flask run命令的輸出中看到它)。
談到調(diào)試模式的話題,我不得不提到的第二個(gè)重要的調(diào)試模式下的功能,就是重載器。這是一個(gè)非常有用的開發(fā)功能,可以在源文件被修改時(shí)自動(dòng)重啟應(yīng)用。如果在調(diào)試模式下運(yùn)行flask run,則可以在開發(fā)應(yīng)用時(shí),每當(dāng)保存文件,應(yīng)用都會(huì)重新啟動(dòng)以加載新的代碼。
03 自定義錯(cuò)誤頁面
Flask為應(yīng)用提供了一個(gè)機(jī)制來自定義錯(cuò)誤頁面,這樣用戶就不必看到簡單而枯燥的默認(rèn)頁面。作為例子,讓我們?yōu)镠TTP的404錯(cuò)誤和500錯(cuò)誤(兩個(gè)最常見的錯(cuò)誤頁面)設(shè)置自定義錯(cuò)誤頁面。為其他錯(cuò)誤設(shè)置頁面的方式與之相同。
使用@errorhandler裝飾器來聲明一個(gè)自定義的錯(cuò)誤處理器。我將把我的錯(cuò)誤處理程序放在一個(gè)新的app/errors.py模塊中。
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
錯(cuò)誤函數(shù)與視圖函數(shù)非常類似。對(duì)于這兩個(gè)錯(cuò)誤,我將返回各自模板的內(nèi)容。請(qǐng)注意這兩個(gè)函數(shù)在模板之后返回第二個(gè)值,這是錯(cuò)誤代碼編號(hào)。對(duì)于之前我創(chuàng)建的所有視圖函數(shù),我不需要添加第二個(gè)返回值,因?yàn)槲蚁胍氖悄J(rèn)值200(成功響應(yīng)的狀態(tài)碼)。本處,這些是錯(cuò)誤頁面,所以我希望響應(yīng)的狀態(tài)碼能夠反映出來。
500錯(cuò)誤的錯(cuò)誤處理程序應(yīng)當(dāng)在引發(fā)數(shù)據(jù)庫錯(cuò)誤后調(diào)用,而上面的用戶名重復(fù)實(shí)際上就是這種情況。為了確保任何失敗的數(shù)據(jù)庫會(huì)話不會(huì)干擾模板觸發(fā)的其他數(shù)據(jù)庫訪問,我執(zhí)行會(huì)話回滾來將會(huì)話重置為干凈的狀態(tài)。
404錯(cuò)誤的模板如下:
{% extends "base.html" %}
{% block content %}File Not Found
{% endblock %}
500錯(cuò)誤的模板如下:
{% extends "base.html" %}
{% block content %}An unexpected error has occurred
The administrator has been notified. Sorry for the inconvenience!
{% endblock %}
這兩個(gè)模板都從base.html基礎(chǔ)模板繼承而來,所以錯(cuò)誤頁面與應(yīng)用的普通頁面有相同的外觀布局。
為了讓這些錯(cuò)誤處理程序在Flask中注冊(cè),我需要在應(yīng)用實(shí)例創(chuàng)建后導(dǎo)入新的app/errors.py模塊。app/__init__.py:
# ...
from app import routes, models, errors
04 通過電子郵件發(fā)送錯(cuò)誤
Flask提供的默認(rèn)錯(cuò)誤處理機(jī)制的另一個(gè)問題是沒有通知機(jī)制,錯(cuò)誤的堆棧跟蹤只是被打印到終端,這意味著需要監(jiān)視服務(wù)器進(jìn)程的輸出才能發(fā)現(xiàn)錯(cuò)誤。在開發(fā)時(shí),這是非常好的,但是一旦將應(yīng)用部署在生產(chǎn)服務(wù)器上,沒有人會(huì)關(guān)心輸出,因此需要采用更強(qiáng)大的解決方案。
我認(rèn)為對(duì)錯(cuò)誤發(fā)現(xiàn)采取積極主動(dòng)的態(tài)度是非常重要的。如果生產(chǎn)環(huán)境的應(yīng)用發(fā)生錯(cuò)誤,我想立刻知道。所以我的第一個(gè)解決方案是配置Flask在發(fā)生錯(cuò)誤之后立即向我發(fā)送一封電子郵件,郵件正文中包含錯(cuò)誤堆棧跟蹤的正文。
第一步,添加郵件服務(wù)器的信息到配置文件中:
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']
電子郵件的配置變量包括服務(wù)器和端口,啟用加密連接的布爾標(biāo)記以及可選的用戶名和密碼。這五個(gè)配置變量來源于環(huán)境變量。如果電子郵件服務(wù)器沒有在環(huán)境中設(shè)置,那么我將禁用電子郵件功能。電子郵件服務(wù)器端口也可以在環(huán)境變量中給出,但是如果沒有設(shè)置,則使用標(biāo)準(zhǔn)端口25。電子郵件服務(wù)器憑證默認(rèn)不使用,但可以根據(jù)需要提供。 ADMINS配置變量是將收到錯(cuò)誤報(bào)告的電子郵件地址列表,所以你自己的電子郵件地址應(yīng)該在該列表中。
Flask使用Python的logging包來寫它的日志,而且這個(gè)包已經(jīng)能夠通過電子郵件發(fā)送日志了。我所需要做的就是為Flask的日志對(duì)象app.logger添加一個(gè)SMTPHandler的實(shí)例:
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)
如你所見,僅當(dāng)應(yīng)用未以調(diào)試模式運(yùn)行,且配置中存在郵件服務(wù)器時(shí),我才會(huì)啟用電子郵件日志記錄器。
設(shè)置電子郵件日志記錄器的步驟因?yàn)樘幚戆踩蛇x項(xiàng)而稍顯繁瑣。本質(zhì)上,上面的代碼創(chuàng)建了一個(gè)SMTPHandler實(shí)例,設(shè)置它的級(jí)別,以便它只報(bào)告錯(cuò)誤及更嚴(yán)重級(jí)別的信息,而不是警告,常規(guī)信息或調(diào)試消息,最后將它附加到Flask的app.logger對(duì)象中。
有兩種方法來測(cè)試此功能。最簡單的就是使用Python的SMTP調(diào)試服務(wù)器。這是一個(gè)模擬的電子郵件服務(wù)器,它接受電子郵件,然后打印到控制臺(tái)。要運(yùn)行此服務(wù)器,請(qǐng)打開第二個(gè)終端會(huì)話并在其上運(yùn)行以下命令:
(venv) $ python -m smtpd -n -c DebuggingServer localhost:8025
要用這個(gè)模擬郵件服務(wù)器來測(cè)試應(yīng)用,那么你將設(shè)置MAIL_SERVER=localhost和MAIL_PORT=8025。
譯者注:本段中去除了說明設(shè)置該端口需要管理員權(quán)限的部分,因?yàn)檫@和實(shí)際情況不符。原文如下:To test the application with this server, then you will setMAIL_SERVER=localhost and MAIL_PORT=8025. If you are on a Linux or Mac OS system, you will likely need to prefix the command with sudo, so that it can execute with administration privileges. If you are on a Windows system, you may need to open your terminal window as an administrator. Administrator rights are needed for this command because ports below 1024 are administrator-only ports. Alternatively, you can change the port to a higher port number, say 5025, and set MAIL_PORTvariable to your chosen port in the environment, and that will not require administration rights.
保持調(diào)試SMTP服務(wù)器運(yùn)行并返回到第一個(gè)終端,在環(huán)境中設(shè)置export MAIL_SERVER=localhost和MAIL_PORT=8025(如果使用的是Microsoft Windows,則使用set而不是export)。確保FLASK_DEBUG變量設(shè)置為0或者根本不設(shè)置,因?yàn)閼?yīng)用不會(huì)在調(diào)試模式中發(fā)送電子郵件。運(yùn)行該應(yīng)用并再次觸發(fā)SQLAlchemy錯(cuò)誤,以查看運(yùn)行模擬電子郵件服務(wù)器的終端會(huì)話如何顯示具有完整堆棧跟蹤錯(cuò)誤的電子郵件。
這個(gè)功能的第二個(gè)測(cè)試方法是配置一個(gè)真正的電子郵件服務(wù)器。以下是使用你的Gmail帳戶的電子郵件服務(wù)器的配置:
export MAIL_SERVER=smtp.googlemail.comexport MAIL_PORT=587export MAIL_USE_TLS=1export MAIL_USERNAME=export MAIL_PASSWORD=
如果你使用的是Microsoft Windows,記住在每一條語句中用set替換掉export。
Gmail帳戶中的安全功能可能會(huì)阻止應(yīng)用通過它發(fā)送電子郵件,除非你明確允許“安全性較低的應(yīng)用程序”訪問你的Gmail帳戶??梢蚤喿x此處來了解具體情況,如果你擔(dān)心帳戶的安全性,可以創(chuàng)建一個(gè)輔助郵箱帳戶,配置它來僅用于測(cè)試電子郵件功能,或者你可以暫時(shí)啟用允許不太安全的應(yīng)用程序來運(yùn)行此測(cè)試,完成后恢復(fù)為默認(rèn)值。
05 記錄日志到文件中
通過電子郵件來接收錯(cuò)誤提示非常棒,但在其他場(chǎng)景下,有時(shí)候就有些不足了。有些錯(cuò)誤條件既不是一個(gè)Python異常又不是重大事故,但是他們?cè)谡{(diào)試的時(shí)候也是有足夠用處的。為此,我將會(huì)為本應(yīng)用維持一個(gè)日志文件。
為了啟用另一個(gè)基于文件類型RotatingFileHandler的日志記錄器,需要以和電子郵件日志記錄器類似的方式將其附加到應(yīng)用的logger對(duì)象中。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')
日志文件的存儲(chǔ)路徑位于頂級(jí)目錄下,相對(duì)路徑為logs/microblog.log,如果其不存在,則會(huì)創(chuàng)建它。
RotatingFileHandler類非常棒,因?yàn)樗梢郧懈詈颓謇砣罩疚募?,以確保日志文件在應(yīng)用運(yùn)行很長時(shí)間時(shí)不會(huì)變得太大。本處,我將日志文件的大小限制為10KB,并只保留最后的十個(gè)日志文件作為備份。
logging.Formatter類為日志消息提供自定義格式。由于這些消息正在寫入到一個(gè)文件,我希望它們可以存儲(chǔ)盡可能多的信息。所以我使用的格式包括時(shí)間戳、日志記錄級(jí)別、消息以及日志來源的源代碼文件和行號(hào)。
為了使日志記錄更有用,我還將應(yīng)用和文件日志記錄器的日志記錄級(jí)別降低到INFO級(jí)別。如果你不熟悉日志記錄類別,則按照嚴(yán)重程度遞增的順序來認(rèn)識(shí)它們就行了,分別是DEBUG、INFO、WARNING、ERROR和CRITICAL。
日志文件的第一個(gè)有趣用途是,服務(wù)器每次啟動(dòng)時(shí)都會(huì)在日志中寫入一行。當(dāng)此應(yīng)用在生產(chǎn)服務(wù)器上運(yùn)行時(shí),這些日志數(shù)據(jù)將告訴你服務(wù)器何時(shí)重新啟動(dòng)過。
06修復(fù)用戶名重復(fù)的 BUG
利用用戶名重復(fù)BUG這么久, 現(xiàn)在時(shí)候向你展示如何修復(fù)它了。
你是否還記得,RegistrationForm已經(jīng)實(shí)現(xiàn)了對(duì)用戶名的驗(yàn)證,但是編輯表單的要求稍有不同。在注冊(cè)期間,我需要確保在表單中輸入的用戶名不存在于數(shù)據(jù)庫中。在編輯個(gè)人資料表單中,我必須做同樣的檢查,但有一個(gè)例外。如果用戶不改變?cè)加脩裘敲打?yàn)證應(yīng)該允許,因?yàn)樵撚脩裘呀?jīng)被分配給該用戶。下面你可以看到我為這個(gè)表單實(shí)現(xiàn)了用戶名驗(yàn)證:
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.')
該實(shí)現(xiàn)使用了一個(gè)自定義的驗(yàn)證方法,接受表單中的用戶名作為參數(shù)。這個(gè)用戶名保存為一個(gè)實(shí)例變量,并在validate_username()方法中被校驗(yàn)。如果在表單中輸入的用戶名與原始用戶名相同,那么就沒有必要檢查數(shù)據(jù)庫是否有重復(fù)了。
為了使得新增的驗(yàn)證方法生效,我需要在對(duì)應(yīng)視圖函數(shù)中添加當(dāng)前用戶名到表單的username字段中:
@app.route('/edit_profile', methods=['GET', 'POST'])@login_requireddef edit_profile(): form = EditProfileForm(current_user.username) # ...
現(xiàn)在這個(gè)BUG已經(jīng)修復(fù)了,大多數(shù)情況下,以后在編輯個(gè)人資料時(shí)出現(xiàn)用戶名重復(fù)的提交將被友好地阻止。 但這不是一個(gè)完美的解決方案,因?yàn)楫?dāng)兩個(gè)或更多進(jìn)程同時(shí)訪問數(shù)據(jù)庫時(shí),這可能不起作用。假如存在驗(yàn)證通過的進(jìn)程A和B都嘗試修改用戶名為同一個(gè),但稍后進(jìn)程A嘗試重命名時(shí),數(shù)據(jù)庫已被進(jìn)程B更改,無法重命名為該用戶名,會(huì)再次引發(fā)數(shù)據(jù)庫異常。 除了有很多服務(wù)器進(jìn)程并且非常繁忙的應(yīng)用之外,這種情況是不太可能的,所以現(xiàn)在我不會(huì)為此擔(dān)心。
此時(shí),你可以嘗試再次重現(xiàn)該錯(cuò)誤,以了解新的表單驗(yàn)證方法如何防止該錯(cuò)誤。
最后,我自己是一名從事了多年開發(fā)的Python老程序員,辭職目前在做自己的Python私人定制課程,今年年初我花了一個(gè)月整理了一份最適合2019年學(xué)習(xí)的Python學(xué)習(xí)干貨,可以送給每一位喜歡Python的小伙伴,想要獲取的可以關(guān)注我的頭條號(hào)并在后臺(tái)私信我:01,即可免費(fèi)獲取。
(正文已結(jié)束)
推薦閱讀:廣西都市網(wǎng)
免責(zé)聲明及提醒:此文內(nèi)容為本網(wǎng)所轉(zhuǎn)載企業(yè)宣傳資訊,該相關(guān)信息僅為宣傳及傳遞更多信息之目的,不代表本網(wǎng)站觀點(diǎn),文章真實(shí)性請(qǐng)瀏覽者慎重核實(shí)!任何投資加盟均有風(fēng)險(xiǎn),提醒廣大民眾投資需謹(jǐn)慎!