フラスコメガチュートリアル、パート7:エラー処理(2018年版)

blog.miguelgrinberg.com







ミゲル・グリンバーグ






<<<前 次>>>







この記事は、ミゲル・グリーンバーグによる教科書の新版の第7部の翻訳であり、著者はこの出版物を2018年5月に完成させる予定です。







私の側では、翻訳についていくつもりです。










これは、Flask Mega-Tutorialシリーズの第7章で、Flaskアプリケーションでエラー処理を処理する方法を説明します。







参考までに、以下はこのシリーズの記事のリストです。









注1:このコースの古いバージョンをお探しの場合は、こちらをご覧ください







注2:突然、このブログの私の(ミゲル)の仕事を支持して声を上げたい場合、または単に記事を1週間待つ忍耐がない場合、私(ミゲルグリーンバーグ)はこのガイドの完全版にパッケージ化された電子書籍またはビデオを提供します。 詳細については、 learn.miguelgrinberg.comをご覧ください







この章では、新しい関数のコーディングから微生物学的アプリケーションに移り、代わりに、ソフトウェアプロジェクトで常に発生するエラーに対処するためのいくつかの戦略について説明します。 このトピックを説明するために、 第6章で追加したコードを意図的に誤りました。 読み続ける前に、見つけられるかどうかを確認してください!







この章のGitHubリンク: BrowseZipDiff







Flaskでのエラー処理



Flaskアプリケーションでエラーが発生するとどうなりますか? 見つけるための最良の方法は、自分でそれを体験することです。 アプリケーションを起動し、少なくとも2人のユーザーが登録されていることを確認してください。 ユーザーの1人としてログインし、プロファイルページを開いて[編集]リンクをクリックします。 プロファイルエディターで、ユーザー名を既に登録されている別のユーザーの既存の名前に変更し、修正を適用してみてください。 これにより、「内部サーバーエラー」という恐ろしいページが表示されます。













アプリケーションが実行されているターミナルセッションで、エラースタックのトレースが表示されます。 スタックトレースは、エラーの原因となった行に至るまで、このスタック上の一連の呼び出しを表示するため、エラーのデバッグ時に非常に役立ちます。







(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
      
      





スタックトレースは、エラーの原因を示します。 アプリケーションは、ユーザーが新しいユーザー名が既にシステムにある別のユーザーと一致しないことを確認せずにユーザー名を変更することを許可します。 このエラーは、新しいユーザー名をデータベースに書き込もうとするSQLAlchemyから発生しますが、ユーザー名列がunique = Trueで定義されているため、データベースはそれを拒否します。







ユーザーに表示されるエラーページにエラーに関する多くの情報が含まれていないことが重要であり、これは正しいです。 私は間違いなく、事故がデータベースエラーまたは私が使用しているデータベース、およびデータベース内のテーブルとフィールドの名前が原因であることをユーザーに知らせたくありません。 この情報はすべて内部のものでなければなりません。







理想とはほど遠いものがいくつかあります。 pageいエラーがあり、アプリケーションのレイアウトと一致しないページがあります。 また、ターミナルにダンプされる重要なアプリケーションスタックトレースがあり、エラーを見逃さないように常に確認する必要があります。 そしてもちろん間違いがあります。 これらすべての問題を解決しますが、最初にFlaskデバッグモードについて説明しましょう。







デバッグモード



上記のエラーの処理方法は、運用サーバーで実行されるシステムに最適です。 エラーがある場合、ユーザーには未定義のエラーのページが表示されますが(このエラーページをもっと楽しくするつもりです)、重要なエラーデータはサーバー出力またはログファイルにあります。







ただし、アプリケーションを開発しているときは、デバッグモードをオンにすることができます。デバッグモードは、Flaskがブラウザに直接非常に優れたデバッガを表示するモードです。 デバッグモードをアクティブにするには、アプリケーションを停止し、次の環境変数を設定します。







 (venv) $ export FLASK_DEBUG=1
      
      





Microsoft Windowsを実行している場合は、エクスポートの代わりに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
      
      





次に、アプリケーションを再度クラッシュさせて、ブラウザで対話型デバッガーを確認します。













デバッガーを使用すると、スタックの各レベルを展開し、対応するソースコードを表示できます。 また、任意のフレームでPythonを開き、有効なPython式を実行して、変数の値を確認することもできます。







本番サーバーでは、Flaskアプリケーションをデバッグモードで実行しないでください。 デバッガーを使用すると、サーバー上でコードをリモートで実行できるため、アプリケーションやサーバーに侵入したい攻撃者にとって予期しない贈り物になる可能性があります。 追加のセキュリティ対策として、ブラウザーで実行中のデバッガーが閉じられ、最初の使用時に、Pinkコードが要求されます。これは、 flask run



コマンドの出力で確認できます。







デバッグモードについて説明しているので、デバッグモードで有効になる2番目の重要な機能である再起動について言及する必要があります。 これは、ソースファイルが変更されたときにアプリケーションを自動的に再起動する非常に便利な開発機能です。 flask run



をデバッグモードで実行すると、アプリケーションで作業を続けることができ、ファイルを保存するたびにアプリケーションが再起動して新しいコードを取得します。







カスタムエラーページ



Flaskは、カスタムエラーページを作成するためのアプリケーションメカニズムを提供するため、ユーザーは単純で退屈なデフォルトを見る必要がありません。 例として、最も一般的な2つのHTTPエラー404および500のカスタムエラーページを定義しましょう。 他のエラーのページの定義も同じように機能します。







カスタムエラーハンドラを宣言するには、 @errorhandler



デコレータを@errorhandler



ます。 新しい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
      
      





エラー関数は表示関数と同様に機能します。 これらの2つのエラーについては、それぞれのテンプレートの内容を返します。 両方の関数が、パターンの後の2番目の値、つまりエラーコード番号を返すことに注意してください。 これまでに作成したすべてのプレゼンテーション関数について、デフォルトで200(正常終了のステータスコード)が必要なため、2番目の戻り値を追加する必要はありませんでした。 これらは現在エラーページであるため、応答ステータスコードにこれを反映させる必要があります。







500番目のエラーのエラーハンドラは、データベース障害が発生した後に呼び出すことができます。これは、実際には重複したユーザー名の意図的なケースが原因です。 失敗したデータベースセッションがテンプレートによるデータベースアクセスに干渉しないように、セッションロールバックを発行します。 これにより、セッションがクリーンな状態にリセットされます。







エラー404のテンプレートは次のとおりです。







 {% extends "base.html" %} {% block content %} <h1>File Not Found</h1> <p><a href="{{ url_for('index') }}">Back</a></p> {% endblock %}
      
      





そして、これは500エラーの1つです。







 {% 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



をインポートする必要があります。







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





ターミナルセッションでFLASK_DEBUG = 0



を設定した後、再度ユーザー名の重複エラーが発生した場合は、より良いエラーページが表示されます。













またはそう! 演習として自分で何かを考え出すことをお勧めします。













メールエラー



Flaskが提供するデフォルトのエラー処理のもう1つの問題は、通知がないことです。 エラースタックトレースは端末に出力されます。つまり、サーバープロセスの出力をエラー検出のために監視する必要があります。 開発中にアプリケーションを起動する場合、これは正常ですが、アプリケーションが運用サーバーにデプロイされるとすぐに誰も結果を確認しないため、より信頼性の高いソリューションを作成する必要があります。







エラーに積極的に対応することは非常に重要だと思います。 アプリケーションの製品版でエラーが発生した場合、すぐに知りたいです。 したがって、最初の決定は、電子メールメッセージのエラースタックトレースでエラーが発生した直後に電子メールを送信するように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']
      
      





電子メール構成変数には、サーバーとポート、暗号化された接続を有効にするフラグ、オプションのユーザー名とパスワードが含まれます。 環境変数へのマッピングから5つの構成変数が派生します。 電子メールサーバーが環境にインストールされていない場合、これを電子メールエラーを無効にする必要があるというサインとして使用します。 メールサーバーのポートも環境変数で指定できますが、設定されていない場合は、標準ポート25が使用されます。メールサーバーの資格情報はデフォルトでは使用されませんが、必要に応じて提供できます。 ADMINS



構成ADMINS



は、エラーレポートを受信する電子メールアドレスのリストであるため、独自の電子メールアドレスがそのリストに含まれている必要があります。







FlaskはPython logging



パッケージを使用してログを保持しますが、このパッケージにはすでにログをメールで送信する機能があります。 エラーを含む電子メールを送信するために私がしなければならないことは、 app.loggerであるFlaskログオブジェクトにSMTPHandlerインスタンスを追加することです。







 import logging from 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)
      
      





ご覧のとおり、アプリケーションがapp.debug



True



として定義されているデバッグモードなしでアプリケーションが実行される場合、および構成に電子メールサーバーが存在する場合にのみ、電子メールロガーをオンにしました。







メールレジストラのセットアップは、多くの電子メールサーバーに存在する追加のセキュリティ設定を処理する必要があるため、多少面倒です。 ただし、本質的に、上記のコードはSMTPHandler



インスタンスを作成し、警告、情報、デバッグメッセージではなくエラーメッセージのみを送信するようにレベルを設定し、Flaskからapp.logger



最終的に添付します。







この関数の機能を検証するには、2つのアプローチがあります。 最も簡単な方法は、PythonのSMTPデバッグサーバーを使用することです。 これは電子メールメッセージを受け入れる偽のメールサーバーですが、送信する代わりにコンソールに表示します。 このサーバーを起動するには、2番目のターミナルセッションを開き、次のコマンドを実行します。







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





デバッグSMTPサーバーを実行したままにして、最初のターミナルに戻り、 export



MAIL_SERVER = localhost



およびMAIL_PORT = 8025



set



します(Microsoft Windowsを使用している場合は、 export



代わりにset



を使用します)。 アプリケーションはデバッグモードで電子メールを送信しないため、 FLASK_DEBUG



変数FLASK_DEBUG



0



設定されているか、まったく設定されていないことを確認してください。

アプリケーションを起動し、SQLAlchemyエラーを再度発生させて、偽メールサーバーを実行しているターミナルセッションがエラースタックの全内容を含む電子メールをどのように表示するかを確認します。







この機能の2番目のテスト方法は、実際のメールサーバーを構成することです。 以下は、 Gmailアカウントにメールサーバーを使用するための構成です。







 export MAIL_SERVER=smtp.googlemail.com export MAIL_PORT=587 export MAIL_USE_TLS=1 export MAIL_USERNAME=<your-gmail-username> export MAIL_PASSWORD=<your-gmail-password>
      
      





Microsoft Windowsを使用している場合は、上記の各手順でexport



代わりにset



を使用してください。







Gmailアカウントへのアクセスを「安全性の低いアプリ」に明示的に許可しない限り、Gmailアカウントのセキュリティ機能により、アプリケーションからのメール送信が妨げられる場合があります。 これについてはここで読むことができます。アカウントのセキュリティが心配な場合は、電子メールをチェックするためだけに構成するセカンダリアカウントを作成するか、このテストの間、安全性の低いアプリケーションのアクセス許可を一時的に有効にしてからデフォルトに戻すことができます。







ファイルへのロギング



電子メールのエラーを取得することは便利ですが、時には十分ではありません。 Pythonの例外では説明されておらず、重大な問題ではない障害の場合もありますが、デバッグの目的で保存できるほど興味深い場合があります。 このため、アプリケーションのログファイルもサポートします。







今回はRotatingFileHandler



タイプの別のハンドラーのロギングを有効にするには、電子メールハンドラーと同様にアプリケーションのロガーを有効RotatingFileHandler



する必要があります。







 # ... 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')
      
      





microblog.logというログファイルを、作成していないログディレクトリに作成しています。







RotatingFileHandler



クラスは、ログを上書きし、アプリケーションが長時間実行されている場合にログファイルが大きくなりすぎないようにするので便利です。 この場合、ログファイルのサイズを10 KBに制限し、最後の10個のログファイルをバックアップとして保存します。







logging.Formatter



クラスは、ログメッセージ形式のカスタマイズを提供します。 これらのメッセージはファイルに送信されるため、できるだけ多くの情報を含めるようにします。 そのため、タイムスタンプ、ログレベル、

メッセージ、ソースファイル、およびログエントリの発信元の行番号。







ロギングをさらに便利にするために、アプリケーションロガーとファイルハンドラーの両方で、ロギングレベルをINFO



カテゴリに下げます。 仕訳カテゴリに精通していない場合、これらは重大度の昇順でDEBUG



INFO



WARNING



ERROR



およびCRITICAL



です。







ログファイルの最初の便利な使用法として、サーバーは起動するたびにログに行を書き込みます。 実稼働サーバーでアプリケーションを実行すると、これらのログエントリはサーバーが再起動された時期を示します。







ユーザー名の修正



ユーザー名の重複エラーが長すぎます このようなエラーを処理するアプリケーションを準備する方法を示したので、ようやく修正することができます。







覚えているなら、 RegistrationForm



すでにユーザー名をチェックしていますが、編集フォームの要件は少し異なります。 登録時に、フォームに入力したユーザー名がデータベースに存在しないことを確認する必要があります。 プロファイルの編集フォームでは、同じチェックを実行する必要がありますが、例外が1つあります。 ユーザーが元のユーザー名をそのまま残した場合、このユーザー名は既にこのユーザーに割り当てられているため、チェックで解決する必要があります。 以下に、このフォームのユーザー名チェックの実行方法を示します。







 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()



メソッドでvalidate_username()



されます。 フォームに入力されたユーザー名が元のユーザー名と一致する場合、データベースの重複をチェックする理由はありません。







この新しい検証メソッドを使用するには、フォームオブジェクトが作成されるビュー関数に元のユーザー名引数を追加する必要があります。







 @app.route('/edit_profile', methods=['GET', 'POST']) @login_required def edit_profile(): form = EditProfileForm(current_user.username) # ...
      
      





これでエラーが修正され、ほとんどの場合、編集プロファイルの形での重複が防止されます。 これは、2つ以上のプロセスがデータベースに同時にアクセスすると動作しない場合があるため、理想的なソリューションではありません。 この状況では、競合状態が検証につながる可能性がありますが、しばらくすると、データベースの名前を変更しようとすると別のプロセスによって既に変更されており、ユーザーの名前を変更できません。 多くのサーバープロセスを持つ非常にビジーなアプリケーションを除いて、これはややありそうにないので、今のところこれについては心配しません。







この時点で、エラーを再度再現して、フォーム検証メソッドがどのようにエラーを防ぐかを確認できます。







<<<前 次>>>







PS







エラー処理



翻訳者から







管理者へのエラーメッセージの受信をメールで確認することにしました。 このため、 routes.py



モジュールを台無しにしました。 この非常に「破損」のため、 def edit_profile()



前に@app.route('/edit_profile', methods=['GET', 'POST'])



デコレーター@app.route('/edit_profile', methods=['GET', 'POST'])



をコメントアウトしました。 その結果、エラーが発生し、すべてログファイルに記録されましたが、手紙は届きませんでした。 Python 3.3を使用しています。 おそらく、これは新しいバージョンでは発生しません。 しかし、ロシア語レイアウトのWindows 7では、これが起こりました。







管理者にメッセージを送信しようとしたときに、アプリケーションがメッセージの作成中にエンコードエラーを受け取りました。 コンソールウィンドウには次の行が含まれていました。













ご覧のとおり、リンクは仮想環境ではなく、標準のPythonのディレクトリを指します。







バージョン3でのlogging



は標準のPythonライブラリであるため、 pip



を使用してインストールする必要はありません。







標準モジュールについて

また、PyPIにあるロギングモジュールは廃止されており、Python3互換ではありません。







(READMEファイルによると、その最新バージョンは2005年3月2日にリリースされました。)







したがって、ロギングをインストールしようとしないでください。

標準ライブラリの新しいモジュールを当然のことと考えてください。 仮想ライブラリで使用することが重要な場合。







venvにコピーした後、 lib logging



仮想環境からインポートされます













再びエラーが発生します













logging



仮想になりました。 ただし、 smtplib



標準です。







すべてのライブラリを標準環境から仮想環境にドラッグする必要はないと思います。

これによるエラーは消えません。







標準のメールモジュールについて

メッセージのエンコードの問題は、標準のemail



パッケージを使用して、優先エンコードを示すメッセージを作成することで解決されます。







このパッケージのインターネットからの例を次に示します。







 # -*- coding: utf-8 -*- from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText import quopri def QuoHead(String): s = quopri.encodestring(String.encode('UTF-8'), 1, 0) return "=?utf-8?Q?" + s.decode('UTF-8') + "?=" FIOin = " " emailout = "some@test.ru" emailin = "some2@test.ru" msg = MIMEMultipart() msg["Subject"] = QuoHead("  " + FIOin).replace('=\n', '') msg["From"] = (QuoHead(" ") + " <" + emailout + ">").replace('=\n', '') msg["To"] = (QuoHead(FIOin) + " <" + emailin + ">").replace('=\n', '') m = """ .   . ,    .""" text = MIMEText(m.encode('utf-8'), 'plain', 'UTF-8') msg.attach(text) print(msg.as_string())
      
      





しかし、エラーメッセージを送信するためにそれをどのように適用するのですか?!

誰かが記事のコメントで提案するかもしれません。







flask-mail



. logging



smtplib









. smtplib.py



.







encode('utf-8')















, -, .














All Articles