PythonのWebフレームワーク「Django」のHttpRequestオブジェクトを徹底解説|翔泳社の本

PythonのWebフレームワーク「Django」のHttpRequestオブジェクトを徹底解説

2021/07/27 07:00

 PythonでWeb開発ができるフルスタックなフレームワーク「Django」について詳しく解説した『実践Django Pythonによる本格Webアプリケーション開発』から、DjangoのHttpRequestオブジェクトの役割と仕組みについて、HTTP 1.1の基礎知識から掘り下げて解説しているパートを抜粋して紹介します。

本記事は『実践Django Pythonによる本格Webアプリケーション開発』の「3.4 HttpRequestとHttpResponseを使いこなす」から一部を抜粋したものです。掲載にあたって編集しています。

HttpRequestを使いこなす

 HttpRequestの踏み込んだ解説に入る前に、簡単にHTTPの基本をおさらいします。curlを用いて、Djangoのスタートページにリクエストを飛ばしてみます。

$ curl -v http://127.0.0.1:8000/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Tue, 16 Mar 2021 19:38:40 GMT
< Server: WSGIServer/0.2 CPython/3.9.0
< Content-Type: text/html
< X-Frame-Options: DENY
< Content-Length: 10697
< X-Content-Type-Options: nosniff
< Referrer-Policy: same-origin
<

<!doctype html>
<html lang="en-us" dir="ltr">
    (中略)
</html>
* Connection #0 to host 127.0.0.1 left intact
* Closing connection 0

 このように-vオプションをつけることで、リクエストやレスポンスの詳細が確認できます。curlが送信したHTTPのリクエストは、次のようになっています。

GET / HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: curl/7.64.1
Accept: */*

<EOF>

 HTTPリクエストの1行目は、リクエストライン(Request-Line)と呼ばれます。リクエストラインは「メソッド」「リクエストURI」「HTTPバージョン」からなります。今回の場合、メソッドは「GET」、リクエストURIは「/」、プロトコルバージョンはHTTPの1.1です。リクエストラインの次にHTTPヘッダーの値が続き、空行を1行おいてメッセージボディー(Message body)があります。今回はメッセージボディーは空です。次は、Djangoのアプリケーションが返したHTTPレスポンスも確認しましょう。

HTTP/1.1 200 OK
Date: Tue, 16 Mar 2021 19:39:14 GMT
Server: WSGIServer/0.2 CPython/3.9.0
Content-Type: text/html
X-Frame-Options: DENY
Content-Length: 10697
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin

<!doctype html>
<html>
    ...
</html>

 HTTPレスポンスの1行目は、ステータスライン(Status-Line)と呼ばれます。ステータスラインは「HTTPバージョン」「ステータスコード」「リーズンフレーズ(Reason-Phrase)」からなります。ステータスコードが200番台であることから、正常にレスポンスが返ってきていることが確認できます。続いてHTTPヘッダーの値が続き、空行を1行おいてメッセージボディーがあります。メッセージボディーにはHTMLが記述されています。今回はcurlでHTTPリクエストを送信しましたが、Webブラウザでアクセスした際も基本的には変わりません。流れを図にすると図1のようになります。

図1 HTTP/1.1リクエストとレスポンスの様子
図1 HTTP/1.1リクエストとレスポンスの様子

 ここまでの流れを頭に入れておけば、HttpRequestの理解はそれほど難しくありません。WSGIサーバーやDjangoアプリケーションによって、HTTPのリクエストは図2に示すようにHttpRequestオブジェクトに変換され、HttpResponseオブジェクトに書き込んだ値は図3に示すようにHTTPのレスポンスとしてクライアントに返されます。

図2 HTTPのリクエストがHttpRequestオブジェクトに変換される
図2 HTTPのリクエストがHttpRequestオブジェクトに変換される
図3 HttpResponseオブジェクトの各フィールドが、HTTPのレスポンスに対応する
図3 HttpResponseオブジェクトの各フィールドが、HTTPのレスポンスに対応する

x-www-form-urlencoded形式のデータの読み込み

 HTTPリクエストのメッセージボディーを取得する方法は複数用意されています。

  • request.body:リクエストボディー(例:b'{"message": "Hello World"')
  • request.readメソッド:file-likeオブジェクト
  • request.POST:x-www-form-urlencoded形式のリクエストボディー(例:QueryDict({'email': ['hello@example.com']}))
  • request.FILES:multipart/form-data形式のリクエストボディー(例:MultiValueDict({'user': ['shibata']}))

 メッセージボディーは、request.bodyからのバイト文字列で取得できます。しかしメッセージボディーのサイズが大きく一度にメモリに載せたくないケースでは、request.readメソッドによるfile-likeオブジェクトを取得します。またそれ以外にも特定の形式で記述されたデータをパースして取り出すことができます。例えば次のようなHTMLフォームからPOSTのリクエストを送るケースを考えてみます。

<form method="post" action="/accounts/">
    {% csrf_token %}
    <input type="text" name="username">
    <input type="text" name="email">
    <button type="submit">投稿</button>
</form>

 このHTMLフォームはusernameやemailの文字列を/accountsに送信します。実際にHTTPのリクエストは次のようになっています。

POST /accounts/ HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 44

username=shibata&email=shibata%40example.com

 usernameとemailの情報がメッセージボディー内に記述されています。その形式はGETリクエスト時にURLに含めるクエリ文字列の形式と同じで、パラメーターは&によって区切られ、keyとvalueは、=によって区切られます。また、英数字以外はパーセントエンコードをされるため、@は%40に変わっています。このような形式をx-www-form-urlencoded形式と呼びます。HttpRequestオブジェクトのPOST属性は、このx-www-form-urlencoded形式のデータをパースし、QueryDictという辞書ライクなオブジェクトで提供してくれます。form-urlencodedの形式は、keyの重複を許容するため1つのkeyに対して複数の値をもちます。

>>> from django.http import QueryDict
>>> QueryDict("user=shibata&user=masashi&email=shibata%40example.com")
<QueryDict: {'user': ['shibata', 'masashi'], 'email': ['shibata@example.com']}>

multipart/form-data形式のデータの読み込み

 <form>タグのenctype属性または<button>や<input>のformenctype属性を指定するとメッセージボディーの形式が変わります。先程紹介したx-www-form-urlencoded以外にも、multipart/form-dataやtext/plainが指定できます。ここではmultipart/form-dataを指定してみましょう。

<form method="post" action="/accounts/" enctype="multipart/form-data">
    {% csrf_token %}
    <input type="text" name="username">
    <input type="image" name="icon_url">
    <button type="submit">{% trans "Post" %}</button>
</form>

 このフォームのicon_urlフィールドでは、画像ファイルを選択します。

 multipart/form-dataは、このようにHTMLフォームからファイルをアップロードする際に必要になります。メッセージボディーがどのようになっているか確認してみましょう。

POST /accounts/ HTTP/1.1
Host: example.com
Content-Type: multipart/form-data;boundary="boundary"

--boundary
Content-Disposition: form-data; name="username"

shibata
--boundary
Content-Disposition: form-data; name="icon_url"; filename="profile.png"
Content-Transfer-Encoding: binary

contents of profile.png...
--boundary--

 Content-Typeヘッダーで指定される区切り文字列によってメッセージボディーが分割されています。その中にさらにヘッダーが存在し、空行を空けて画像データの中身が記述されています。

 multipart/form-data形式のリクエストには、HttpRequestオブジェクトのFILES属性よりアクセスできます。ファイルアップロードなどを実装する際はこの内容を押さえておくといいでしょう。

conditionデコレーターを使ったETagやLast-Modifiedヘッダーの制御

 HTTPには、クライアントにレスポンス内容をキャッシュさせる仕組みがいくつかあります。もしクライアントが最後にアクセスしてから、まだサーバーのレスポンス内容に変化がないことがわかっていれば、レスポンスのメッセージボディーは空でも問題ないはずです。そこでクライアントが既に最新のコンテンツをキャッシュしていることがわかっている場合には、サーバーは304 Not Modifiedを返します。

 さて、それではどのようにしてクライアントが既に最新のコンテンツをキャッシュしているかを判断するのでしょうか。1つの方法は次のようなLast-Modifiedヘッダーによる制御です。

Last-Modified: Mon, 22 Mar 2021 01:08:18 GMT

 Last-Modifiedヘッダーの値には、HTTP-dateタイムスタンプ形式で最後にコンテンツが更新された日時を指定します。クライアント側は、コンテンツにアクセスした際のLast-Modifiedヘッダーの値を記憶し、次回以降のアクセスの際に次のようなIf-Modified-Sinceヘッダーをリクエストヘッダーに含めます。

If-Modified-Since: Mon, 22 Mar 2021 01:08:18 GMT

 するとサーバーは、現在のコンテンツの最終更新日時とIf-Modified-Sinceヘッダーの日時を比較します。もしコンテンツの最終更新日時の方が新しければ200 OKを返し、古ければ304 Not Modifiedを返します。304 Not Modifiedを返す際には、HTTPレスポンスのメッセージボディーには何も入っていません。Djangoではconditionデコレーターを用いて次のように記述できます。

from django.views.decorators.http import condition


def last_modified_func(request, snippet_id):
    try:
        snippet = Snippet.objects.get(id=snippet_id)
    except Snippet.DoesNotExist:
        return None # コンテンツが存在しなければNoneを返す。
    return snippet.updated_at


@condition(last_modified_func=last_modified_func)
def my_view(request, snippet_id):
    ...

 last_modified_funcは、ビュー関数と同じ引数を受け取って、最終更新日時を示すdatetimeオブジェクトを返す関数です。もしコンテンツがそもそも存在しなければ、Noneを返します。

 Last-Modifiedヘッダーは日時によりキャッシュを制御しますが、日時の比較ではキャッシュの有効性を表現できないこともしばしばあります。そのようなケースではリクエスト内容やコンテンツの情報から、何らかのハッシュ値を計算し、そのハッシュ値が一致するかどうかを見る方法もあります。

 それを実現するのがETagヘッダーです。大まかな流れはLast-Modifiedヘッダーのときと変わりません。サーバーは、コンテンツを返す際にHTTPのレスポンスヘッダーにETag: をセットします。クライアント側がこれを記憶し、If-None-Match: ヘッダーをつけてサーバーにコンテンツを問い合わせます。Djangoで実装する際には@condition(etag_func=...)にハッシュ値を返す関数を指定するだけです。

 またdjango.views.decorators.httpモジュールは、etagデコレーターやlast_modifiedデコレーターも提供します。これらは次のようなただのショートカット関数です。そのためconditionデコレーターが理解できていれば十分です。

def etag(etag_func):
    return condition(etag_func=etag_func)

def last_modified(last_modified_func):
    return condition(last_modified_func=last_modified_func)
実践Django

Amazon SEshop その他


実践Django
Pythonによる本格Webアプリケーション開発

著者:芝田将
発売日:2021年7月19日(月)
定価:3,850円(本体3,500円+税10%)

Djangoの実践的なテストテクニック、ユーザーモデルのカスタマイズ方法、認証処理のベストプラクティスなど、Web開発において必ず知っておくべき内容を幅広く取り上げました。