前回、Werkzeugでいろいろと試してみました。
Werkzeugでリクエスト・レスポンス・Cookieを試してみた - メモ的な思考的な
今回は、WerkzeugでいろいろなHTTPレスポンスを作ってみました。
目次
環境
- Python 3.6.6
- Werkzeug 0.14.1
なお、アプリの構成は前回作成した BaseStructure
クラスをベースにしています。
特定のHTTPメソッドのみレスポンスを許可
Rule
オブジェクトを生成する時の引数 methods
にHTTPメソッドを指定することで、そのHTTPメソッドのみレスポンスを返せます。
http://werkzeug.pocoo.org/docs/0.14/routing/#werkzeug.routing.Rule
class Application: def __init__(self): self.url_map = Map([ ... Rule('/get-only', endpoint='get_only', methods=['GET']), Rule('/post-only', endpoint='post_only', methods=['POST']), ]) def get_only_handler(self, request): return Response('GET Only!\n') def post_only_handler(self, request): return Response(f'POST Only: {request.form.get("foo")}\n')
curlで動作確認します。
GETでアクセスした場合です。
# GETだけレスポンス可能なURL $ curl --include localhost:5000/get-only HTTP/1.0 200 OK Content-Type: text/plain; charset=utf-8 ... GET Only! # POSTだけレスポンス可能なURL $ curl --include 'localhost:5000/post-only' HTTP/1.0 405 METHOD NOT ALLOWED Content-Type: text/html Allow: POST
POSTでアクセスした場合です。
# GETだけレスポンス可能なURL $ curl -w '\n' --include -X POST 'localhost:5000/get-only' --data 'foo=1' HTTP/1.0 405 METHOD NOT ALLOWED Content-Type: text/html Allow: HEAD, GET # POSTだけレスポンス可能なURL $ curl -w '\n' --include -X POST 'localhost:5000/post-only' --data 'foo=1' HTTP/1.0 200 OK Content-Type: text/plain; charset=utf-8 ... POST Only: 1
JSONレスポンス
レスポンスオブジェクトの content_type引数に application/json
をセットすることで、JSONをレスポンスできます。
class Application: def __init__(self): self.url_map = Map([ Rule('/json', endpoint='json'), ]) def json_handler(self, request): input_data = request.form.get('input') result = { 'foo': 'abc', 'bar': ['ham', 'spam', 'egg'], 'result': input_data, } return Response(json.dumps(result), content_type='application/json')
では実際に、データをPOSTしてJSONをAjaxで取得・表示するHTMLを用意して試してみます。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>JSON response</title> </head> <body> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <p> <label for="id_input">POST data: </label> <input id="id_input" type="text"> </p> <button id="id_get_json">Go</button> <p>foo: <span id="id_foo"></span></p> bar: <ul id="id_bar"> </ul> <p>input data: <span id="id_input_result"></span></p> <script> $(function () { $('#id_get_json').on('click', function () { $.ajax({ url: '/json', type: 'POST', dataType: 'json', data: {'input': $('#id_input').val()} }).done((data) => { $('#id_foo').html(data['foo']); data['bar'].forEach((currentValue, index) => { $('#id_bar').append(`<li>No. ${index}: ${currentValue} </li>`) }); $('#id_input_result').html(data['result']); }).fail((data) => { console.log('fail!') }) }) }) </script> </body> </html>
ブラウザで見るとこのような感じです。
データを入力し、GoボタンでAjaxのPOSTを実行すると、以下のように表示が切り替わります。
ファイルをアップロード
ファイルをアップロードするHTMLフォームがあるとします。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>File Upload</title> </head> <body> <form action="/upload" method="post" enctype="multipart/form-data"> <p><input type="file" name="upload_file"></p> <p><input type="submit"></p> </form> </body> </html>
Werkzeugでは、Requestオブジェクトの files
を使うことで、アップロードされたファイルを取得できます。
http://werkzeug.pocoo.org/docs/0.14/wrappers/#werkzeug.wrappers.BaseRequest.files
以下のコードでは、アップロードされたファイルを uploads
ディレクトリに保存します。
class Application: def __init__(self): self.url_map = Map([ Rule('/upload', endpoint='upload'), ]) def upload_handler(self, request): f = request.files.get('upload_file') f.save(str(pathlib.Path(f'./uploads/{f.filename}'))) return Response('hello upload')
ファイルをダウンロード
ファイルをダウンロードするには、 Content-Disposition
ヘッダを追加します。
以下ではURLにアクセスすると foo.csv
がダウンロードされます。
class Application: def __init__(self): self.url_map = Map([ Rule('/download', endpoint='download'), ]) def download_handler(self, request): field_names = ['No', 'Name'] contents = [ {'No': 1, 'Name': 'Apple'}, {'No': 2, 'Name': 'Mandarin'}, {'No': 3, 'Name': 'Grape'}, ] stream = StringIO() writer = csv.DictWriter(stream, fieldnames=field_names) # CSVヘッダの書込 writer.writeheader() # CSVデータの書込 writer.writerows(contents) # ストリームからデータを取得し、レスポンスとする data = stream.getvalue() headers = Headers() headers.add('Content-Disposition', 'attachment', filename='foo.csv') return Response( data, headers=headers, )
参考
- 14.1. csv — CSV ファイルの読み書き — Python 3.6.5 ドキュメント
- python - Create and download a CSV file from a Flask view - Stack Overflow
ただし、上記の方法では日本語名のファイルはダウンロードできません。
日本語ファイルをダウンロードする場合は、
が必要になります。
参考
- Content-Disposition - HTTP | MDN
- RFC 5987 - Character Set and Language Encoding for Hypertext Transfer Protocol (HTTP) Header Field Parameters
- RFC 5987
- 9 URI のエスケープ | 情報セキュリティ技術動向調査(2009 年下期):IPA 独立行政法人 情報処理推進機構
なお、以前、Cookieを調べた時に、いろいろなパーセントエンコードについて知りました。
Pythonで、日本語をCookie値へ設定する方法を調べてみた - メモ的な思考的な
RFC5987でのパーセントエンコードを見たところ、
pct-encoded = "%" HEXDIG HEXDIG ; see [RFC3986], Section 2.1 # https://tools.ietf.org/html/rfc5987#page-3
との記述がありました。
RFC3986についてはCookieの時に調べました。RFC3986でのパーセントエンコードをPythonで実現するには、 urllib.parse.quote(character, safe='~')
が良さそうでした。
そこで、
encoded_filename = quote(request.form['filename'], safe='~') headers.add('Content-Disposition', f"attachment; filename*=UTF-8''{encoded_filename}")
として 一~二.csv
というファイル名でダウンロードしてみたところ、
というファイル名でダウンロードされました。
~
という文字がファイル名に使えなかったため、 _
へと変換されたのでしょう。
ファイルをダウンロードさせたいときのいろんな方法(Servlet, Apacheなど) - Qiita
静的ファイルっぽく見せかけた時のレスポンス
これはどちらかというとルーティングについてです。
以下は拡張子 html
付きでアクセスした場合でも、動的なレスポンスを返すようなルーティングとなっています。
class Application: def __init__(self): self.url_map = Map([ Rule('/extension.html', endpoint='extension'), ]) def extension_handler(self, request): return Response('extension request')
curlで調べてみます。
$ curl localhost:5000/extension.html extension request
ソースコード
GitHubに上げました。
thinkAmi-sandbox/werkzeug-sample
various_responses
ディレクトリ以下が今回のものであり、Werkzeugアプリは various_response_app.py
になります。