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:\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 ):
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:\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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM