HTTP の命令だけを用いて遠隔にあるサーバーとデータをやりとりする
— HTTPウェブサービスの概念はこの33文字に集約される。
http GET
を使えばいい。
http POST
を使えばいい。もっと高度なhttpウェブサービスのapiもあって、 これらは
http PUT
やhttp DELETE
を利用して、データの作成・修正・削除を行えるようにしてくれる。
httpの命令だけで全て処理できる。
ここにはレジストリも、エンベロープも、ラッパも、トンネリングも必要ない。 要するに、httpプロトコル内に構築されているこれらの「動詞」(GET, POST, PUT, DELETE) は、 データの取得・作成・修正・消去を行うアプリケーションレベルの命令に直接対応するもの。
このやり方の主たる利点は、 その単純さにあり、またこの単純さゆえに広く使われている。
データ — 普通はxmlかjson — は静的に構築して保存することもできるし、 サーバーサイドスクリプトを使って動的に生成することもできる。 そして、メジャーなプログラミング言語はすべて(Pythonも) このデータをダウンロードするためのhttpライブラリを備えている。
さらに、この方式だとデバッグも容易。 というのも、httpウェブサービスの個々のリソースにはユニークなアドレスが(urlの形式で)割り振られているため、 ウェブブラウザにロードすればすぐに生のデータを見ることができる。
httpウェブサービスの例:
Python 3にはhttpウェブサービスと情報をやりとりするためのライブラリが二つ用意されている。
http.client
は、httpプロトコルのrfc 2616を実装した低級ライブラリ。urllib.request
は、http.client
上に構築された抽象化レイヤ。これはhttpサーバーとftpサーバーの両方にアクセスするための標準apiを提供してくれるもので、httpリダイレクトを自動でたどることもできれば、いくつかの一般的なhttp認証方式も扱える。
代わりに、「httplib2
」 がお勧め。
これはオープンソースなサードパーティ製ライブラリで、http.clientよりも完全にhttpを実装しているのだが、しかも urllib.request
よりも優れた抽象化を施しているという代物。
httpクライエントならば必ず備えるべき5つの機能がある。
ウェブサービスの種類にかかわらず知っておくべき最も重要なことは、ネットワークアクセスはとても高くつくということ。
接続を開き、リクエストを送り、そして遠隔サーバーからレスポンスを取得するまでには非常に長い時間がかかる。 レイテンシ(リクエストの送信後、その応答としてデータが受信され始めるまでの時間) は、 想像よりも大きくなる。 ルーターは処理を誤り、パケットは抜け落ち、さらに中継のプロキシはアタックを受ける
httpはキャッシュのことを念頭に置いて設計されている。 実際に、ネットワークアクセスを最小限にするという仕事のみを行うデバイス(「キャッシュプロキシ」と呼ばれている)なんてものもある。
ispはほぼ間違いなくこのキャッシュプロキシを運用している。 これができるのも、キャッシュがhttpプロトコルに組み込まれているため。
キャッシュ がどのように機能するかの具体的な例:
仮にブラウザで diveintomark.org
にアクセスしたとする。
このページの背景には wearehugh.com/m.jpg
という画像が置かれている。
ここでブラウザがこの画像をダウンロードをすると、サーバーは次のようなhttpヘッダを付けてくる:
HTTP/1.1 200 OK
Date: Sun, 31 May 2009 17:14:04 GMT
Server: Apache
Last-Modified: Fri, 22 Aug 2008 04:28:16 GMT
ETag: "3075-ddc8d800"
Accept-Ranges: bytes
Content-Length: 12405
Cache-Control: max-age=31536000, public
Expires: Mon, 31 May 2010 17:14:04 GMT
Connection: close
Content-Type: image/jpeg
この Cache-Control
と Expires
のヘッダは、
ブラウザ(それに加えて、あなたとサーバーとの間にあるキャッシュプロキシすべて)に対して「一年間はこの画像をキャッシュしてもいいよ」ということを伝えるもの。
だからもし、次の年にこの画像へのリンクを含む別のページにアクセスしたとすると、ブラウザはキャッシュにある画像を読み込むので、ネットワークを介したやりとりは全く行われない。
ブラウザが何かの理由でこの画像をローカルキャッシュから消してしまったとしよう。 その原因はディスクスペースが尽きたということかもしれないし、 あるいはあなたが自分でキャッシュを削除したのかもしれない。
その原因が何であれ、httpヘッダは「この画像のデータはパブリックなキャッシュプロキシで保存してもかまわないよ」と述べている(厳密に言えば、ここで重要なのはこのヘッダが述べていないことだ。つまり、Cache-Controlヘッダの中にprivateというキーワードが含まれていないので、このデータはデフォルトでキャッシュできるようになっている)。 キャッシュプロキシは膨大な記憶容量を持つように設計されている。
もし、ispがキャッシュプロキシを運営していたら、
そのプロキシにはまだこの画像がキャッシュされているかもしれない。
ここで再び diveintomark.org
にアクセスしたとする。
すると、
つまり、リクエストが遠隔サーバーに到達することはない。 現に、このリクエストはあなたの会社のネットワークを離れてさえいない。 この仕組みのおかげで、高速なダウンロード(より少ないホップ数での通信)が可能になり、 会社側のコストも節約(外部からダウンロードされるデータをより少なく)できる。
httpキャッシュは, つまり、
その中間に置かれるプロキシは、サーバーとクライアントが上手く処理してくれる限りにおいて機能できるだけ。
Pythonの httpライブラリはキャッシュをサポートしていないが、
httplib2
はサポートしている。
ひっきりなしに変更されるデータがある一方で、決して変わらないデータもある。 その中間には、更新された可能性があったのだが、実際には何も変更されていなかったという類のデータが大量に存在している。
CNN.com
のフィードは数分おきに更新されるが、
数日か数週間は更新されないブログのフィードもある。
後者の場合、クライアントに何週間もフィードをキャッシュしてもらいたいとは思わないだろう。そうすると、実際に何かをブログに投稿しても、 読者が数週間その記事を目にしないということになりかねない(これも読者の皆さんが「数週間はこのフィードをチェックしないで」としている私のヘッダに従ってくれるおかげ)。 その一方で、一時間ごとにフィード全体をダウンロードして更新をチェックされても困る
http
にはこれを解決する方法も用意されている。
最初にデータをリクエストされたときに、
サーバーはLast-Modified
ヘッダを付けて返信することができる。
これはその名前のとおり、データが更新された日時を表すもの。
diveintomark.org
が参照している背景画像にもLast-Modifiedヘッダが付いている。
HTTP/1.1 200 OK
Date: Sun, 31 May 2009 17:14:04 GMT
Server: Apache
Last-Modified: Fri, 22 Aug 2008 04:28:16 GMT
ETag: "3075-ddc8d800"
Accept-Ranges: bytes
Content-Length: 12405
Cache-Control: max-age=31536000, public
Expires: Mon, 31 May 2010 17:14:04 GMT
Connection: close
Content-Type: image/jpeg
同じデータを二度目にリクエストする時、
前回サーバーから返された日時を入れた If-Modified-Since
ヘッダをリクエストに付けて送ることができる。
サーバーは If-Modified-Since
ヘッダを無視し、
ステータスコード200と一緒に新しいデータを送り返してくれる。
http 304
という特別なステータスコードを返す。
これは「このデータは前回リクエストされた時から何も変更されていないよ」ということを表すもの。
ちなみに、curlを使えばこれをコマンドライン上でテストすることもできる。
curl -I -H "If-Modified-Since: Fri, 22 Aug 2008 04:28:16 GMT" http://wearehugh.com/m.jpg
HTTP/1.1 304 Not Modified
Date: Sun, 31 May 2009 18:04:39 GMT
Server: Apache
Connection: close
ETag: "3075-ddc8d800"
Expires: Mon, 31 May 2010 18:04:39 GMT
Cache-Control: max-age=31536000, public
サーバーが304を返す場合には、 データ自体は再び送られてこない。 返送されるのはステータスコードだけになる。
キャッシュされたコピーの有効期限が切れていた場合でも、
last-modified
チェックを使えば、データが変更されていない限り、
同じデータを再びダウンロードしなくても済むようになる(さらなるおまけとして、304が返される時もキャッシュに関するヘッダは送られてくる。データが本当に変更されておらず、さらに次のリクエストで304ステータスコードと最新のキャッシュ情報が返されるかもしれないので、正式には「有効期限切れ」とされているデータのコピーもプロキシは保存し続けるもの)。
Pythonのhttpライブラリは、last-modified
チェックをサポートしていないが、
httplib2
はサポートしている。
ETag
とは、
last-modified
チェックと同じ役割を果たすもの。
Etagsを使った場合には、サーバーはリクエストされたデータに加えて、
ハッシュを納めたETagヘッダを返してくる(このハッシュをどのように生成するかについては完全にサーバーに委ねられている。データが変更されたときにその値が変わりさえすればなんでもいい)。
diveintomark.org
から参照されている背景画像にもETag
ヘッダが含まれている。
HTTP/1.1 200 OK
Date: Sun, 31 May 2009 17:14:04 GMT
Server: Apache
Last-Modified: Fri, 22 Aug 2008 04:28:16 GMT
ETag: "3075-ddc8d800"
Accept-Ranges: bytes
Content-Length: 12405
Cache-Control: max-age=31536000, public
Expires: Mon, 31 May 2010 17:14:04 GMT
Connection: close
Content-Type: image/jpeg
同じデータを2度目にリクエストする時には、
Etagのハッシュを入れた If-None-Match
ヘッダを付けて送る。
データが変わっていなければ、サーバーは 304
ステータスコードを送り返してくれる。
この場合、last-modified
チェックの時と同じく、
サーバーは304ステータスコードだけを返すので、
同じデータが2度送信されることはない。
つまり、Etagのハッシュを2度目のリクエストの際に一緒に送ることで 「最後にリクエストした時のデータがまだ残っているから、まだハッシュが一致するようなら同じデータを再送信する必要は無いよ」とサーバーに伝えていることになる。
curlを使うと:
curl -I -H "If-None-Match: \"3075-ddc8d800\"" http://wearehugh.com/m.jpg ①
HTTP/1.1 304 Not Modified
Date: Sun, 31 May 2009 18:04:39 GMT
Server: Apache
Connection: close
ETag: "3075-ddc8d800"
Expires: Mon, 31 May 2010 18:04:39 GMT
Cache-Control: max-age=31536000, public
ETagは一般に引用符で括られているのだが、実はこの引用符も値の一部。
だから、If-None-Match
ヘッダをサーバーに送り返す時には引用符を付けて返さなくてはならない。
PythonのhttpライブラリはEtagをサポートしていないが、httplib2
はサポートしている。
httpウェブサービスということになると、ほとんどの場合、回線を通じてテキストベースのデータを行き来させるという話になる。 そのデータはxmlかもしれないし、jsonかもしれない。 あるいは単なるプレーンテキストかもしれない。形式が何であれ、 テキストというのは圧縮効率が良いもの。 XMLの章で例に出したフィードは圧縮していない状態だと3070バイトなのだが、gzipで圧縮すると941バイトになる。元のサイズの30%になる。
httpはいくつかの圧縮アルゴリズムをサポートしているが、
最も一般に用いられている形式は gzip
と deflate
の2つ。
httpを使ってリソースをリクエストする時には、圧縮形式で送るようにサーバーに頼むことができる。
サポートしている圧縮アルゴリズムのリストが入った Accept-encoding
ヘッダをリクエストに付け加えればいい。
サーバーがそのアルゴリズムのいずれかをサポートしていれば、
圧縮したデータを返してくれる(この場合、どのアルゴリズムが使われたかを示す
Content-encoding
ヘッダもついてくる)。
後は、送られてきたデータを展開すればいい。
圧縮したデータと未圧縮のデータが異なるEtagを持つようにすること。 でないと、キャッシュプロキシが混乱して、 クライアントが扱えないのに圧縮された形式で返してしまいかねない。 この微妙な問題の詳細については、Apacheバグ 39727に関する議論に書いてある。
Pythonのhttpライブラリは圧縮をサポートしていないが、
httplib2
はサポートしている。
ウェブサイトは再構成されて、 ページは新しいアドレスに移されてしまう。 ウェブサービスですら再編されることがある。
例えば、http://example.com/index.xml
で配信されていたフィードは、http://example.com/xml/atom.xml
に移されてしまうかもしれない。
あるいは、 組織の拡大や再編の一環として、ドメイン自体が変えられることだってある。
例えば、http://www.example.com/index.xml
は http://server-farm-1.example.com/index.xml
に移転するかもしれない。
リソースをhttpサーバーにリクエストした場合にはいつも、 サーバーはステータスコードも送り返してくる。
httpにはリソースが移転したことを知らせる方法がいくつか用意されている。
中でも最もよく使われているのはステータスコードの 302
と 301
を使う方法だ。
「ステータスコード302」は、一時的なリダイレクトを表す。 つまり、「おっと、それは一時的にあっちに移転されてるよ」ということ(その上で、一時的なアドレスをLocationヘッダに入れて渡してくれる)。
一方で、「ステータスコード301」は、恒久的なリダイレクト を表す。つまり、「おっと。それはあっちに完全に移転されてるよ」ということだ(その上で、新しいアドレスをLocationに入れて渡してくれる)。
ステータスコード302と一緒に新しいアドレスを受け取った場合について、httpの仕様は「リクエストしたリソースを取得するには新しいアドレスを使えばいいが、 次に同じリソースにアクセスする時には古いアドレスを試すべし」としている。 ステータスコード301と一緒に新しいアドレスを受け取った場合には、以後その新しいアドレスを使っていけばいい。
urllib.request
モジュールはhttpサーバーから適切なステータスコードを受け取った場合に自動でそのリダイレクトをたどってくれるのだが、
そのように処理したとは何も言ってくれない。
要するに、最終的にリクエストしたデータは取得できるにしても、その処理を支えるライブラリが「ご親切にも」リダイレクトをたどってくれたとは分からない。
だから、あなたは古いアドレスに何度も何度もアクセスし続けることになり、その度に新しいアドレスにリダイレクトされて、しかも毎回urllib.request
が「ご丁寧に」リダイレクトをたどってくれる。
言い換えれば、これは恒久的なリダイレクトを一時的なリダイレクトと同じように扱っているわけだ。
こうすると一回で済むところを二回往復することになるので、
これはサーバーにとってもあなたにとっても良くない。
httplib2
は恒久的なリダイレクトを処理してくれる。
恒久的なリダイレクトが生じたことを教えてくれるのみならず、
そのリダイレクトをローカルに保存し、
リダイレクトされたurlをリクエストの前に自動で書き直してくれる。
httpを使ってAtomフィードなどのリソースをダウンロードしたいと考えたとする。 フィードということなので、一回ダウンロードするだけでは済まず、 何回も何回もダウンロードすることになる (ほとんどのフィードリーダーは一時間に一回、更新をチェックする)。
まずは、こいつを手早く雑に実装してみて、それからどうやったら改善できるかを考える。
import urllib.request
urllib.request
<module 'urllib.request' from 'C:\\Miniconda3\\lib\\urllib\\request.py'>
a_url = 'http://cartman0.hatenablog.com/feed'
どんなものであれhttpを使ってダウンロードするのは、Pythonでは簡単。 実際に、たった一行でできてしまう。
urllib.request
モジュールには便利な urlopen()
という関数が用意されていて、
これはダウンロードしたいページのアドレスを引数にとり、
ファイルに似たオブジェクトを返すもの。
そして、このオブジェクトを read()
するだけでページの内容を全て取得することができる。
data = urllib.request.urlopen(a_url).read()
urlopen().read()
メソッドは常に文字列ではなく、
bytes
オブジェクトを返す。
バイト列はバイト列であって、文字列はそれを抽象化したものだった。
http
は抽象化されたものを扱わないので、リソースをリクエストした時には、
バイト列の形で受け取ることになる。
それを文字列として扱いたいなら、文字コードを定めて明示的に文字列に変換しなくてはならない。
type(data)
bytes
> print(data)
b'<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ja">\n <title>\xe3\x81\xaf\xe3\x81\x97\xe3\x81\x8f\xe3\x82\x8c\xe3\x82\xa8\xe3\x83\xb3\xe3\x82\xb8\xe3\x83\x8b\xe3\x82\xa2\xe3\x82\x82\xe3\x81\xa9\xe3\x81\x8d\xe3\x81\xae\xe3\x83\xa1\xe3\x83\xa2</title>\n \n <subtitle>\xe6\x83\x85\xe5\xa0\xb1\xe3\x83\xbbWeb\xe7\xb3\xbb\xe6\x8a\x80\xe8\xa1\x93\xe3\x81\xae\xe5\x8b\x89\xe5\xbc\xb7\xe3\x83\xa1\xe3\x83\xa2\xe3\x83\xbb\xe5\x82\x99\xe5\xbf\x98\xe9\x8c\xb2\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82</subtitle>\n \n <link href="http://cartman0.hatenablog.com/"/>\n <updated>2016-04-06T20:18:44+09:00</updated>\n <author>\n <name>cartman0</name>\n </author>\n <generator uri="http://blog.hatena.ne.jp/" version="8c91984a041e93c276225325bff2ee2b">Hatena::Blog</generator>\n <id>hatenablog://blog/12921228815722929243</id>\n\n \n \n \n <entry>\n <title>Dive Into Python3 13\xe7\xab\xa0\xe3\x83\xa1\xe3\x83\xa2\xef\xbc\x88Python\xe3\x82\xaa\xe3\x83\x96\xe3\x82\xb8\xe3\x82\xa7\xe3\x82\xaf\xe3\x83\x88\xe3\x82\x92\xe3\x82\xb7\xe3\x83\xaa\xe3\x82\xa2\xe3\x83\xa9\xe3\x82\xa4\xe3\x82\xba\xef\xbc\x89</title>\n
....
テストや開発の際に一回だけ使うお手軽なコードとしては、これで何も悪くない。 フィードの中身を取得しようとしていて、それでフィードの中身が手に入っているわけだし、 この方法でどのウェブページも取得できる。 しかし、定期的にアクセスされるようなウェブサービス (e.g.このフィードを一時間に一回リクエストする場合)の観点からすると、 これは効率が悪いというだけではなく、無礼な方法でもある。
これがなぜ非効率で無礼なのかを理解するために、Pythonのhttpライブラリのデバッグ機能をオンにして何が「回線を通じて」(i.e. ネットワークを介して)送られているのかを見てみる。
from http.client import HTTPConnection
HTTPConnection
http.client.HTTPConnection
urllib.request
は、http.client
という他のPythonの標準ライブラリに依存している。
本来ならhttp.clientに直接触れる必要は無いのだが(urllib.requestモジュールが自動でインポートしてくれる)、
ここではurllib.request
がhttpサーバーに接続する際に使っているHTTPConnection
クラスのデバッグフラグをオンに切り替えるためにインポートしている。
HTTPConnection.debuglevel = 1
from urllib.request import urlopen
デバッグフラグが立っているので、httpのリクエストとレスポンスに関する情報がリアルタイムで出力される。
ご覧のとおり、このAtomフィードをリクエストする際に、
urllib.request
モジュールは
5行のコードを送っている。
response = urlopen('https://raw.githubusercontent.com/chungy/Dive-Into-Python-3/master/examples/feed.xml')
send: b'GET /chungy/Dive-Into-Python-3/master/examples/feed.xml HTTP/1.1\r\nAccept-Encoding: identity\r\nConnection: close\r\nUser-Agent: Python-urllib/3.5\r\nHost: raw.githubusercontent.com\r\n\r\n' reply: 'HTTP/1.1 200 OK\r\n' header: Content-Security-Policy header: X-XSS-Protection header: X-Frame-Options header: X-Content-Type-Options header: Strict-Transport-Security header: ETag header: Content-Type header: Cache-Control header: X-GitHub-Request-Id header: Content-Length header: Accept-Ranges header: Date header: Via header: Connection header: X-Served-By header: X-Cache header: X-Cache-Hits header: Vary header: Access-Control-Allow-Origin header: X-Fastly-Request-ID header: Expires header: Source-Age
最初の行(send:
):
send: b'GET /chungy/Dive-Into-Python-3/master/examples/feed.xml HTTP/1.1
使っているhttpの動詞と、リソースのパス(からドメイン名を引いたもの)を明示している。
2行目(Host):
Host: raw.githubusercontent.com
リクエストしているフィードのあるドメイン名を示している。
3行目(Accept-Encoding:
):
Accept-Encoding: identity
クライアントがサポートしている圧縮アルゴリズムを指定している。
標準では urllib.request
は圧縮をサポートしていない。
4行目(User-Agent:
):
User-Agent: Python-urllib/3.5
リクエストを行っているライブラリの名前を示している。
標準ではPython-urllib
とバージョン番号が記される。
urllib.request
と httplib2
ではこのユーザーエージェントを変更することができ、
そのためには単にUser-Agent
ヘッダをリクエストに加えるだけでいい(こうするとデフォルトの値が置き換えられる)。
サーバーが何を送り返してきたのかを見てみる。
urllib.request.urlopen()
関数から返された response
にはサーバーが返したhttpヘッダが全て入っている。
加えて、このオブジェクトには実際のデータをダウンロードするためのメソッドも入っている。
print(response.headers.as_string())
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline' X-XSS-Protection: 1; mode=block X-Frame-Options: deny X-Content-Type-Options: nosniff Strict-Transport-Security: max-age=31536000 ETag: "bcdda81dde068c9e23eb55a81b23ed86278fdb59" Content-Type: text/plain; charset=utf-8 Cache-Control: max-age=300 X-GitHub-Request-Id: 2BF9481F:5B27:4344A02:57066E88 Content-Length: 3070 Accept-Ranges: bytes Date: Thu, 07 Apr 2016 14:28:25 GMT Via: 1.1 varnish Connection: close X-Served-By: cache-nrt6127-NRT X-Cache: MISS X-Cache-Hits: 0 Vary: Authorization,Accept-Encoding Access-Control-Allow-Origin: * X-Fastly-Request-ID: 4c0366f07b2d933e51b417fbf90838536521137e Expires: Thu, 07 Apr 2016 14:33:25 GMT Source-Age: 0
サーバーはいつリクエストを処理したのかを教えてくれる。
Date: Wed, 06 Apr 2016 16:07:03 GMT
feedによっては、レスポンスに以下のように、Last-Modified
ヘッダが含まれる場合もある。
Last-Modified: Sun, 31 May 2009 06:39:55 GMT
さらには ETag
ヘッダもこのレスポンスに入っている。
ETag: "bcdda81dde068c9e23eb55a81b23ed86278fdb59"
このデータは3070バイト。
ここに何が欠けているかに注意してほしい。
つまり、これには Content-encoding
ヘッダが抜けている。
リクエストでは圧縮していないデータしか受け取れないと明示したので(Accept-encoding: identity
)、当然のことながら、このレスポンスには圧縮されていないデータが入っている。
Content-Length: 3070
このレスポンスにはキャッシュヘッダが含まれていて、これは「300秒までならキャッシュしてもいいよ」と述べている。
Cache-Control: max-age=300```
最後に、response.read()
を呼び出すことで実際のデータをダウンロードしている。
len()
関数の戻り値を見れば分かるように、ここでは一度に3070バイトをダウンロードしている。
data = response.read()
data
b"<?xml version='1.0' encoding='utf-8'?>\r\n<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>\r\n <title>dive into mark</title>\r\n <subtitle>currently between addictions</subtitle>\r\n <id>tag:diveintomark.org,2001-07-29:/</id>\r\n <updated>2009-03-27T21:56:07Z</updated>\r\n <link rel='alternate' type='text/html' href='http://diveintomark.org/'/>\r\n <entry>\r\n <author>\r\n <name>Mark</name>\r\n <uri>http://diveintomark.org/</uri>\r\n </author>\r\n <title>Dive into history, 2009 edition</title>\r\n <link rel='alternate' type='text/html'\r\n href='http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'/>\r\n <id>tag:diveintomark.org,2009-03-27:/archives/20090327172042</id>\r\n <updated>2009-03-27T21:56:07Z</updated>\r\n <published>2009-03-27T17:20:42Z</published>\r\n <category scheme='http://diveintomark.org' term='diveintopython'/>\r\n <category scheme='http://diveintomark.org' term='docbook'/>\r\n <category scheme='http://diveintomark.org' term='html'/>\r\n <summary type='html'>Putting an entire chapter on one page sounds\r\n bloated, but consider this &mdash; my longest chapter so far\r\n would be 75 printed pages, and it loads in under 5 seconds&hellip;\r\n On dialup.</summary>\r\n </entry>\r\n <entry>\r\n <author>\r\n <name>Mark</name>\r\n <uri>http://diveintomark.org/</uri>\r\n </author>\r\n <title>Accessibility is a harsh mistress</title>\r\n <link rel='alternate' type='text/html'\r\n href='http://diveintomark.org/archives/2009/03/21/accessibility-is-a-harsh-mistress'/>\r\n <id>tag:diveintomark.org,2009-03-21:/archives/20090321200928</id>\r\n <updated>2009-03-22T01:05:37Z</updated>\r\n <published>2009-03-21T20:09:28Z</published>\r\n <category scheme='http://diveintomark.org' term='accessibility'/>\r\n <summary type='html'>The accessibility orthodoxy does not permit people to\r\n question the value of features that are rarely useful and rarely used.</summary>\r\n </entry>\r\n <entry>\r\n <author>\r\n <name>Mark</name>\r\n </author>\r\n <title>A gentle introduction to video encoding, part 1: container formats</title>\r\n <link rel='alternate' type='text/html'\r\n href='http://diveintomark.org/archives/2008/12/18/give-part-1-container-formats'/>\r\n <id>tag:diveintomark.org,2008-12-18:/archives/20081218155422</id>\r\n <updated>2009-01-11T19:39:22Z</updated>\r\n <published>2008-12-18T15:54:22Z</published>\r\n <category scheme='http://diveintomark.org' term='asf'/>\r\n <category scheme='http://diveintomark.org' term='avi'/>\r\n <category scheme='http://diveintomark.org' term='encoding'/>\r\n <category scheme='http://diveintomark.org' term='flv'/>\r\n <category scheme='http://diveintomark.org' term='GIVE'/>\r\n <category scheme='http://diveintomark.org' term='mp4'/>\r\n <category scheme='http://diveintomark.org' term='ogg'/>\r\n <category scheme='http://diveintomark.org' term='video'/>\r\n <summary type='html'>These notes will eventually become part of a\r\n tech talk on video encoding.</summary>\r\n </entry>\r\n</feed>\r\n"
len(data)
3070
この時点で既にこのコードは非効率的。 このコードは圧縮されていないデータをリクエストしている(そしてその通り受け取っている)のだ。 httpの圧縮機能を利用するにはgzip のようなものを指定しておかなければならない。 今回はそう指定しなかったので、データは圧縮されなかった。 だから、941バイトで済むところを、3070バイトもダウンロードすることになってしまった。
このコードがどれだけ非効率かを見るために、 もう一度同じフィードをリクエストしてみる。
response2 = urlopen('https://raw.githubusercontent.com/chungy/Dive-Into-Python-3/master/examples/feed.xml')
send: b'GET /chungy/Dive-Into-Python-3/master/examples/feed.xml HTTP/1.1\r\nAccept-Encoding: identity\r\nConnection: close\r\nUser-Agent: Python-urllib/3.5\r\nHost: raw.githubusercontent.com\r\n\r\n' reply: 'HTTP/1.1 200 OK\r\n' header: Content-Security-Policy header: X-XSS-Protection header: X-Frame-Options header: X-Content-Type-Options header: Strict-Transport-Security header: ETag header: Content-Type header: Cache-Control header: X-GitHub-Request-Id header: Content-Length header: Accept-Ranges header: Date header: Via header: Connection header: X-Served-By header: X-Cache header: X-Cache-Hits header: Vary header: Access-Control-Allow-Origin header: X-Fastly-Request-ID header: Expires header: Source-Age
このリクエストは最初のものと何も変わっていない。
内容は最初のリクエストと全く同じで、If-Modified-Since
ヘッダもなければ、
If-None-Match
ヘッダもない。
キャッシュのヘッダを気にかけた様子も全く無く、しかも依然として圧縮を利用していない。
キャッシュのためのCache-Control
とExpires、更新チェックを可能にするLast-Modified
とETag
だ。
しかも、
Vary: Accept-Encoding
ヘッダは、このサーバーは要求さえあればデータの圧縮も扱えるということをほのめかしてさえいる。しかし、ここでそのように要求しなかった。
print(response2.headers.as_string())
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline' X-XSS-Protection: 1; mode=block X-Frame-Options: deny X-Content-Type-Options: nosniff Strict-Transport-Security: max-age=31536000 ETag: "bcdda81dde068c9e23eb55a81b23ed86278fdb59" Content-Type: text/plain; charset=utf-8 Cache-Control: max-age=300 X-GitHub-Request-Id: 2BF9481F:5B27:4344A02:57066E88 Content-Length: 3070 Accept-Ranges: bytes Date: Thu, 07 Apr 2016 14:28:25 GMT Via: 1.1 varnish Connection: close X-Served-By: cache-nrt6121-NRT X-Cache: HIT X-Cache-Hits: 1 Vary: Authorization,Accept-Encoding Access-Control-Allow-Origin: * X-Fastly-Request-ID: d86b916fd1178aa526be4ea549c97474be0f5f25 Expires: Thu, 07 Apr 2016 14:33:25 GMT Source-Age: 1
データを取得するのに丸々3070バイトもダウンロードしている
data2 = response2.read()
len(data2)
3070
先ほどダウンロードしたのと寸分違わぬ3070バイト。
data2 == data
True
httpは、これよりもっと上手く処理できるように設計されている。
httplib2
を使うには、まずこれをインストールする必要がある。
それには、code.google.com/p/httplib2/ に行って最新のバージョンをダウンロードすればいい。
httplib2
はPython 2.xにもPython 3.xにも対応しているのだが、
必ずPython 3用のバージョンを選ぶ。httplib2-python3-0.5.0.zipみたいな名前のものがファイル。
pip からインストールできる。
pip install httplib2
import httplib2
httplib2
<module 'httplib2' from 'C:\\Miniconda3\\lib\\site-packages\\httplib2\\__init__.py'>
httplib2
を使うには、httplib2.Http
クラスのインスタンスを作成する。
httplib2の主要なインターフェイスはHttpオブジェクト。 Httpオブジェクトを作る時は常にディレクトリ名を渡さなければならない。 この時、そのディレクトリはまだ存在しないものであっても構わない。 必要に応じてhttplib2が作成してくれる。
h = httplib2.Http('.cache')
Httpオブジェクトができたら、データを取得するのは簡単で、
request()
メソッドに欲しいデータのアドレスを渡して呼び出すだけでいい。
そうすれば、そのurlに対するhttp GETリクエストが送信される。
(response, content) = h.request('https://raw.githubusercontent.com/chungy/Dive-Into-Python-3/master/examples/feed.xml')
request()
メソッドは二つの値を返す。
一つ目がhttplib2.Response
オブジェクトで、これにはサーバーが送り返してきたhttpヘッダが全て入っている。
例えば、statusの200という値は、リクエストが成功したことを示している。
response.status
200
content
変数にはhttpサーバーから送り返された実際のデータが入っている。
このデータは文字列ではなくbytes
オブジェクトの形式で返されるので、
文字コードを定めて、自分で変換しなくてはならない。
content[:52]
b"<?xml version='1.0' encoding='utf-8'?>\r\n<feed xmlns="
len(content)
3070
バイト列、文字列を「単純に」httplib2が処理してくれればいいのに、と思いがちだが、 しかし、これは現実には厄介な問題。
その原因は、文字コードを決定する規則がリクエストされるリソースの種類によってまちまちだということにある。
httplib2
がリクエストされているリソースの種類を識別する方法としては、
Content-Type http
ヘッダに記されているリソースの種類を使うことである。
しかし、これはhttpのオプション機能なので、 すべてのhttpサーバーがこのヘッダを返してくれるわけではない。 仮に、httpレスポンスにこのヘッダが含まれていなかったとすると、 あとはクライエント側でリソースの種類を推測するしかない(この作業は一般に「content sniffing」と呼ばれているが、こいつはどうやっても完璧にはならない)。
リクエストしているリソースの種類が分かっているなら(このケースだとxmlドキュメント)、 返されたbytesオブジェクトをそのままxml.etree.ElementTree.parse()関数に渡すこともできるかもしれない。
しかし、これができるのは、この例のようにxmlドキュメントが文字コードに関する情報を含んでいる場合だけ。 そして、これもオプション機能なので、あらゆるxmlドキュメントが文字コードを明示しているわけではない。 xmlドキュメントに文字コードの種類が示されていない場合には、 クライアントはドキュメントを運んできたhttpレスポンスの方(i.e. Content-Type httpヘッダ)を見ることになっている。 ここにはcharset変数が含まれているかもしれない。
ここで、文字コードに関する情報は、
の二ヶ所に存在しうることになった。
では、両方に文字コードの情報が入っていたら、どちらが優先されるのか?
RFC 3023 によると、Content-Type http
ヘッダに含まれているメディアタイプが
のいずれかであるか、 あるいはapplication/xmlのサブタイプ(例えば、application/atom+xmlやapplication/rss+xml。ここには、さらにapplication/rdf+xmlも含まれる)ならば、
文字コードは
になる。
一方で、
Content-Type http
ヘッダで与えられるメディアタイプが
text/xml
や text/xml-external-parsed-entity
、
あるいはtext/*+xml
という形式のサブタイプなら、
ドキュメント中のxml宣言にあるencoding属性は全く無視されてしまい、
文字コードは、
になる。
そして以上のことはxmlドキュメントだけに当てはまる話。
htmlドキュメントについては、ウェブブラウザがcontent sniffing のための複雑怪奇な規則[pdf]を作り上げてしまっていて、完全に解明するのは難しい。
先ほどの指定したフォルダ(.cache
)に、キャッシュが保存される。
(response2, content2) = h.request('https://raw.githubusercontent.com/chungy/Dive-Into-Python-3/master/examples/feed.xml')
httpのstatusは同じく200で、前から何も変わっていない。
response2.status
200
ダウンロードした内容にも変化はない。
content2[:52]
b"<?xml version='1.0' encoding='utf-8'?>\r\n<feed xmlns="
len(content2)
3070
import httplib2
この行はhttp.clientでデバッグをオンにするのと同じ役割を果たすもので、
httplib2
がサーバーに送信したデータ全部と、返送されてきた情報の中の主要なものを出力してくれるようになる。
httplib2.debuglevel = 1
前と同じディレクトリ名を渡して、httplib2.Httpオブジェクトを作成する。
h = httplib2.Http('.cache')
前回と同様に、同じurlをリクエストする。しかし何も起こっていないようだ。 もっと正確に言えば、何もサーバーに送られていなければ、 サーバーから返ってきてもいない。 ここではネットワークを通したやりとりが全く行われていない。
(response, content) = h.request('https://raw.githubusercontent.com/chungy/Dive-Into-Python-3/master/examples/feed.xml')
しかし、現実に何らかのデータを「受信」してはいる — 実のところ、 すべてのデータを受け取っている。
len(content)
3070
「リクエスト」が成功したことを示すhttpステータスコードも「受信」している。
response.status
200
問題があるのはこの部分。
この「レスポンス」は httplib2
のローカルキャッシュから生成されたもの。
httplib2.Http
オブジェクトを作るときにディレクトリ名を渡したが
— そのディレクトリは httplib2
がこれまでに行った処理全てをキャッシュしている。
response.fromcache
True
httplib2
のデバッグをオンにしたいなら、
モジュールレベルの定数(httplib2.debuglevel
)を設定してから、新しくhttplib2.Http
オブジェクトを作る必要がある。
デバッグをオフにしたいなら、同じモジュールレベルの定数を変更して、
また新しく httplib2.Http
オブジェクトを作ればいい。
前回、このurlのデータをリクエストした時、そのリクエストは成功していた(status: 200)。
これに対するレスポンスにはフィードのデータだけでなく、
キャッシュのヘッダも一組入っていて、
「このリソースは24時間までならキャッシュしてもいいよ」
(Cache-Control: max-age=86400の部分。
86400は24時間を秒に直したもの)と伝え回っていた。
httplib2
はこのキャッシュのヘッダの内容を理解した上で、それに従って.cacheディレクトリ(これはHttpオブジェクトを作成したときに渡したものだ)に前回のレスポンスを保存しておいた。
そして、
そのキャッシュの期限がまだ切れてなかったので、2度目にこのurlのデータをリクエストした時、httplib2
はネットワークにあたることなく単純にキャッシュしておいた内容を返した。
「単純に」とは言ったが、当然ながらこの単純さの背後にはいくつもの複雑な処理が隠れている。
httplib2
はデフォルトでhttpキャッシュを自動的に処理してくれる。
もし、何らかの理由でレスポンスがキャッシュから生成されたものなのかを知る必要があるなら、response.fromcache
をチェックすればいい。
そういう場合でなければ、これは何の問題もなく上手く動いてくれる。
データのキャッシュを持ってはいるが、
そのキャッシュを無視して遠隔サーバーに再リクエストしたいと思ったとする。
ブラウザはユーザーから特に要求があればこういう処理をする。
例えば、F5
を押すと現在見ているページが更新されるが、
Ctrl+F5
を押せばキャッシュを無視して遠隔サーバーにリクエストが行われる。
ここで「単にキャッシュからデータを削除して、もう一度リクエストする」こともできる。
もちろんそうすることもできるが、あなたと遠隔サーバー以外にもこの処理に関わっているものが存在しているかもしれないということを思い出してほしい。 例えば、中間にあるプロキシサーバーはどうだろうか? これについては完全にあなたの手の外にあるが、 ここにまだデータがキャッシュされているかもしれない。 その場合、(プロキシサーバーにとっては)キャッシュはまだ有効なので、特に何も気にとめることなくキャッシュの方を返してくることになる。
httpの機能を使ってリクエストが確実に遠隔サーバーに届くようにすべき。
httplib2
を使えば、どのリクエストにも任意のhttpヘッダを加えることができる。
全てのキャッシュ(つまり、ローカルディスクにあるキャッシュだけではなく、
あなたと遠隔サーバーの間にあるキャッシュプロキシ全て)を無視するには、
no-cacheヘッダをheaders辞書に加えればいい。
(response2, content2) = h.request('https://raw.githubusercontent.com/chungy/Dive-Into-Python-3/master/examples/feed.xml',
headers={'cache-control':'no-cache'})
send: b'GET /chungy/Dive-Into-Python-3/master/examples/feed.xml HTTP/1.1\r\nHost: raw.githubusercontent.com\r\ncache-control: no-cache\r\nuser-agent: Python-httplib2/0.9.2 (gzip)\r\naccept-encoding: gzip, deflate\r\n\r\n' reply: 'HTTP/1.1 200 OK\r\n' header: Content-Security-Policy header: X-XSS-Protection header: X-Frame-Options header: X-Content-Type-Options header: Strict-Transport-Security header: ETag header: Content-Type header: Cache-Control header: X-GitHub-Request-Id header: Content-Encoding header: Content-Length header: Accept-Ranges header: Date header: Via header: Connection header: X-Served-By header: X-Cache header: X-Cache-Hits header: Vary header: Access-Control-Allow-Origin header: X-Fastly-Request-ID header: Expires header: Source-Age
httplib2
がネットワークを通じたリクエストを開始している。
httplib2
は双方向 — つまり、レスポンスの受信とリクエストの送信の両方においてキャッシュのヘッダを理解し、それに従ってくれる。
ここでは、no-cache
ヘッダが追加されたことをちゃんと認識している。
だからこそ、ローカルキャッシュを全て無視したのであり、
その結果としてネットワークを介したデータのリクエストを行わざるを得なくなった。
response2.status
200
このレスポンスはローカルキャッシュから生成されたものではない。 このことはリクエスト送信に関するデバッグ情報が出力されているのを見れば明らかなのだが、これを手続的に確認できる。
response2.fromcache
False
リクエストが成功したので、
遠隔サーバーからフィード全体を再びダウンロードすることができた。
当然ながら、サーバーはフィードのデータと一緒にhttpヘッダも全て送り返してくれている。
この中にはキャッシュのヘッダも入っていて、
httplib2
はこれを使ってローカルキャッシュを更新する。
次にこのフィードがリクエストされた時に、
ネットワークを通じたアクセスを避けられるかもしれないから。
httpキャッシュに関わるどの部分も、 キャッシュの利用を最大にし、 ネットワークを介したアクセスを最小にするように設計されている。 今回はキャッシュを無視したが、今回のリクエストの結果を次回のリクエストに備えてキャッシュした。
print(dict(response2.items()))
{'x-fastly-request-id': 'db1381a2918cc0dbeb9df46d9c0ea934be532da3', 'connection': 'keep-alive', 'x-xss-protection': '1; mode=block', 'status': '200', 'strict-transport-security': 'max-age=31536000', 'source-age': '1', 'cache-control': 'max-age=300', 'content-type': 'text/plain; charset=utf-8', 'x-cache': 'HIT', 'date': 'Thu, 07 Apr 2016 14:28:27 GMT', 'x-frame-options': 'deny', 'content-length': '3070', 'expires': 'Thu, 07 Apr 2016 14:33:27 GMT', '-content-encoding': 'gzip', 'access-control-allow-origin': '*', 'accept-ranges': 'bytes', 'x-served-by': 'cache-nrt6134-NRT', 'x-github-request-id': '2BF9481F:5B28:51F11CF:57066E8A', 'x-cache-hits': '1', 'via': '1.1 varnish', 'etag': '"bcdda81dde068c9e23eb55a81b23ed86278fdb59"', 'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'", 'vary': 'Authorization,Accept-Encoding', 'content-location': 'https://raw.githubusercontent.com/chungy/Dive-Into-Python-3/master/examples/feed.xml', 'x-content-type-options': 'nosniff'}
Cache-Control
と Expires
の2つのキャッシュヘッダは
freshness indicator
と呼ばれる。
この2つのヘッダは「このキャッシュの期限が切れるまでは、ネットワークを介したアクセスを行う必要はまったくない」と断言するもの。
前の章で見たのはまさしくこの機能で、
このヘッダがあれば、httplib2
は1バイトたりともネットワークを通してやりとりすることなく、
そのままキャッシュのデータを返すのだ(もちろん、明示的にキャッシュを無視した場合は別だが)。
しかし、データが更新された可能性があったのだが、 リクエストを送信してみたら実際には更新されていなかった、 という場合はどうだろう。
httpはこういう時のために Last-Modified
と Etag
というヘッダを用意している。
これらのヘッダは validator
と呼ばれるもので、ローカルキャッシュの有効期限が既に切れている場合には、クライアントは次のリクエストにこの validator
を追加することで、
データが実際に変更されたかどうかを確認することができる。
データが変更されていなければ、サーバーはデータを返送せずに、
304ステータスコードをだけを送り返してくる。
だから、1回だけはネットワークを通したやりとりが行われるのだが、
はるかに少ないバイト数をダウンロードするだけで済む。
import httplib2
httplib2.debuglevel = 1
h = httplib2.Http('.cache')
フィードの代わりに、今回はサイトのホームページ(htmlドキュメント)をダウンロードする。
このページをリクエストするのは今回が始めてなので、httplib2
がやるべき仕事は少ない。
実際、最小限のヘッダだけを付けてリクエストを送信している。
(response, content) = h.request('https://raw.githubusercontent.com/diveintomark/diveintopython3/master/diveintopython3.org')
send: b'GET /diveintomark/diveintopython3/master/diveintopython3.org HTTP/1.1\r\nHost: raw.githubusercontent.com\r\nuser-agent: Python-httplib2/0.9.2 (gzip)\r\nif-none-match: "0dc933db83d0bb51bef9121df2e4a3ebd296212a"\r\naccept-encoding: gzip, deflate\r\n\r\n' reply: 'HTTP/1.1 304 Not Modified\r\n' header: Date header: Via header: Cache-Control header: ETag header: Connection header: X-Served-By header: X-Cache header: X-Cache-Hits header: Vary header: Access-Control-Allow-Origin header: X-Fastly-Request-ID header: Expires header: Source-Age
返ってきたレスポンスにはhttpヘッダがいくつも入っている……が、
キャッシュに関する情報は含まれていない。
しかし、ここにはETagヘッダ
と Last-Modifiedヘッダ
が 2つとも入っている。
print(dict(response.items()))
{'x-fastly-request-id': 'f151127d948911939963b02df0b713193db27b34', 'source-age': '0', '-varied-accept-encoding': 'gzip, deflate', 'x-xss-protection': '1; mode=block', 'status': '304', 'strict-transport-security': 'max-age=31536000', 'connection': 'keep-alive', 'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'", 'content-type': 'text/plain; charset=utf-8', 'x-cache': 'MISS', 'date': 'Thu, 07 Apr 2016 14:28:28 GMT', 'x-frame-options': 'deny', 'via': '1.1 varnish', 'expires': 'Thu, 07 Apr 2016 14:33:28 GMT', '-content-encoding': 'gzip', 'access-control-allow-origin': '*', 'accept-ranges': 'bytes', 'x-served-by': 'cache-nrt6134-NRT', 'x-github-request-id': '2BF94815:2081:27AC3EC:5705DF47', 'x-cache-hits': '0', 'content-length': '1971', 'etag': '"0dc933db83d0bb51bef9121df2e4a3ebd296212a"', 'cache-control': 'max-age=300', 'vary': 'Authorization,Accept-Encoding', 'content-location': 'https://raw.githubusercontent.com/diveintomark/diveintopython3/master/diveintopython3.org', 'x-content-type-options': 'nosniff'}
len(content)
1971
同じページを、同じHttpオブジェクト(と同じローカルキャッシュ)を使って再びリクエストしてみる。
(response, content) = h.request('https://raw.githubusercontent.com/diveintomark/diveintopython3/master/diveintopython3.org')
クライエントの方に戻ると、httplib2は304ステータスコードを認識し、キャッシュからページの内容をロードしている。
response.fromcache
True
ここには実際に2つのステータスコードがある
— すなわち、304(今回サーバーから返されたもの。これが返されたからhttplib2
はキャッシュの方を参照したのだ)と、
200(前回サーバーから返されたもの。
ページのデータと一緒にhttplib2のキャッシュに保存されていた)だ。
response.status
はキャッシュのステータスコードを返す。
response.status # キャッシュのステータスコード
200
サーバーから返された本当のステータスコードが欲しいなら、
response.dict
を参照すればいい。
これはサーバーから返された実際のヘッダを収めた辞書になる。
response.dict['status']
'200'
どうであれcontent変数にはちゃんとデータが入っている。
一般論としては、なぜレスポンスがキャッシュから生成されたのかを知る必要はないだろう(もしかしたらキャッシュから生成されたということすら気にかけないかもしれないが、
それでも全くかまわない。
httplib2
は賢いので、こちらがおろそかでもきちんと処理してくれる)。
request()
の処理が完了するころには、
httplib2
は既にキャッシュをアップデートして、データを返してくれている。
len(content)
1971
httpは数種類の圧縮形式をサポートしているが、 中でも最もよく使われているのは
gzip
とdeflate
の2つ。
httplib2
はこの両方をサポートしている。
response, content = h.request('https://raw.githubusercontent.com/diveintomark/diveintopython3/master/diveintopython3.org')
httplib2が送信するリクエストには、
必ず Accept-Encoding
というヘッダが付けられている。
このヘッダによって deflate
か gzip
のどちらかなら扱えるということをサーバーに伝えている。
accept-encoding: gzip, deflate
サーバーは、gzip
形式で圧縮されたデータを返している。
request()
の処理が完了するころには、
httplib2
は既にデータを展開し、
content変数に入れ終わっているのだ。
返送されたデータが圧縮されたものだったかどうかを知りたいなら、
response['-content-encoding']
をチェックすればいい。
そうでなければ、何も気にする必要はない。
print(dict(response.items()))
{'x-fastly-request-id': 'f151127d948911939963b02df0b713193db27b34', 'source-age': '0', '-varied-accept-encoding': 'gzip, deflate', 'x-served-by': 'cache-nrt6134-NRT', 'status': '200', 'strict-transport-security': 'max-age=31536000', 'connection': 'keep-alive', 'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'", 'content-type': 'text/plain; charset=utf-8', 'x-cache': 'MISS', 'date': 'Thu, 07 Apr 2016 14:28:28 GMT', 'x-frame-options': 'deny', 'content-length': '1971', 'expires': 'Thu, 07 Apr 2016 14:33:28 GMT', '-content-encoding': 'gzip', 'access-control-allow-origin': '*', 'accept-ranges': 'bytes', 'x-xss-protection': '1; mode=block', 'x-github-request-id': '2BF94815:2081:27AC3EC:5705DF47', 'x-cache-hits': '0', 'via': '1.1 varnish', 'etag': '"0dc933db83d0bb51bef9121df2e4a3ebd296212a"', 'cache-control': 'max-age=300', 'vary': 'Authorization,Accept-Encoding', 'content-location': 'https://raw.githubusercontent.com/diveintomark/diveintopython3/master/diveintopython3.org', 'x-content-type-options': 'nosniff'}
httpは2種類のリダイレクトを定義していた。 つまり、
このうち、一時的なリダイレクトについては、 そのリダイレクトをたどるということ (これはhttplib2が自動でやってくれる)以外に、 特に何か処理を行う必要はなかった。
import httplib2
httplib2.debuglevel = 1
h = httplib2.Http('.cache')
response, content = h.request('http://diveintopython3.org/examples/feed-302.xml') ①
connect: (diveintopython3.org, 80)
send: b'GET /examples/feed-302.xml HTTP/1.1 ②
Host: diveintopython3.org
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 302 Found' ③
send: b'GET /examples/feed.xml HTTP/1.1 ④
Host: diveintopython3.org
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 200 OK'
1.このurlにはフィードが入っていない。正しいアドレスに一時的にリダイレクトするようサーバーを設定しておいた。 ② ここでリクエストが行われている。 ③ それに対するレスポンスは302 Foundだ。ここには示されていないが、このレスポンスには正しいurlを示すLocationヘッダが入っている。 ④ httplib2はすぐに方向を変えて、Locationヘッダで与えられたhttp://diveintopython3.org/examples/feed.xml (http://diveintopython3-ja.rdy.jp/http-web-services.html)%E3%81%A8%E3%81%84%E3%81%86url に新たなリクエストを送信し、リダイレクトを「たどって」くれる。
取得したresponseの中には最後のurlに関する情報は含まれているのだが、では、その中間にあったurl、つまりこの最後のurlに至るまでに経由したurlの情報が欲しい時はどうすればいいのだろうか? これについてもhttplib2を使えば調べることができる。
response.previous ①
{'status': '302',
'content-length': '228',
'content-location': 'http://diveintopython3.org/examples/feed-302.xml',
'expires': 'Thu, 04 Jun 2009 02:21:41 GMT',
'server': 'Apache',
'connection': 'close',
'location': 'http://diveintopython3.org/examples/feed.xml',
'cache-control': 'max-age=86400',
'date': 'Wed, 03 Jun 2009 02:21:41 GMT',
'content-type': 'text/html; charset=iso-8859-1'}
>>> type(response) ②
<class 'httplib2.Response'>
>>> type(response.previous)
<class 'httplib2.Response'>
>>> response.previous.previous ③
>>>
response.previous属性を調べれば、httplib2が現在のレスポンスオブジェクトにたどり着く直前に経由したレスポンスオブジェクトを参照することができる。
② responseもresponse.previousのどちらもhttplib2.Responseオブジェクトだ。
③ 従って、response.previous.previousというように調べることで、リダイレクトの道筋をどんどん遡っていけることになる(これが必要になるのは次のような状況だ。つまり、あるurlが二番目のurlにリダイレクトし、さらにそこから三番目のurlにリダイレクトされる。本当にこういうこともあるんだよ!)。ここでは、既にリダイレクトの起点までたどり着いていたので、この属性の値はNoneになる。
同じurlをもう一度リクエストしたらどうなるだろうか?
# 前の例から続く
>>> response2, content2 = h.request('http://diveintopython3.org/examples/feed-302.xml') ①
connect: (diveintopython3.org, 80)
send: b'GET /examples/feed-302.xml HTTP/1.1 ②
Host: diveintopython3.org
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 302 Found' ③
>>> content2 == content ④
True
① 同じurlに、同じhttplib2.Httpオブジェクトだ(したがってキャッシュも同じだ)。 ② 302レスポンスはキャッシュされなかったので、httplib2は別のリクエストを同じurlに送信している。 ③ またしても、サーバーは302を返している。しかし、ここに何が欠けているかに注意してほしい。ここでは最終的なurlの http://diveintopython3.org/examples/feed.xml に対する二回目のリクエストが送られていないのだ。つまり、先ほどのレスポンスはキャッシュされていて(前の例で見たCache-Controlを思い出して欲しい)、さらにhttplib2は302 Foundを受け取ると、新しくリクエストを送信するのに先立ってまずキャッシュをチェックしたということだ。キャッシュにはまだ新しいhttp://diveintopython3.org/examples/feed.xml のコピーがあったので、再びリクエストする必要が無かったのだ。 ④ request()メソッドが処理を完了するころには、フィードのデータは既にキャッシュから読み出され、返されている。もちろん、これは前回受け取ったのと同じデータだ。
要するに、一時的なリダイレクトについては特に何かをする必要はないということだ。httplib2は自動でリダイレクトをたどってくれるし、しかも、あるurlが別のurlにリダイレクトしているという事実は、httplib2が圧縮やキャッシュやETagsなどのhttpの諸機能を扱う上で何の妨げにもならない。
恒久的なリダイレクトも同じく簡単。
response, content = h.request('http://diveintopython3.org/examples/feed-301.xml') ①
connect: (diveintopython3.org, 80)
send: b'GET /examples/feed-301.xml HTTP/1.1
Host: diveintopython3.org
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 301 Moved Permanently' ②
>>> response.fromcache ③
True
① 前と同じく、このurlは実際には存在していない。 そこで、http://diveintopython3.org/examples/feed.xml に向けた恒久的なリダイレクトを送信するようにサーバーを設定しておいた。 ② ほら、ステータスコード301が返ってきた。だが、またここで何が欠けているかに注意してほしい。リダイレクト先のurlに対するリクエストが送信されていないのだ。なぜか? その答えは「既にローカルにキャッシュされているから」だ。 ③ httplib2はリダイレクトを「たどって」、そのままキャッシュに行き着いたのだ。
ここに一時的なリダイレクトと恒久的なリダイレクトの違いがある。 一度httplib2が恒久的なリダイレクトをたどると、 そのurlに対するリクエストはそれから先、 すべて自動でリダイレクト先のurlに書きかえられ、 元のurlにネットワークを介してリクエストが送られることはない。 デバッグがまだオンになっていることを思い出して欲しい。
それなのに、ネットワークを通じたやりとりは全く出力されていない。
response2, content2 = h.request('http://diveintopython3.org/examples/feed-301.xml') ①
>>> response2.fromcache ②
True
>>> content2 == content ③
True
自分のユーザー情報を取得する。 APIを使うに認証が必要。
import httplib2
from base64 import b64encode
httplib2.debuglevel = 1
h = httplib2.Http('.cache')
user = ''
password = ''
auth = b64encode(bytes(user + ':' + password, 'utf-8')).decode('utf-8')
endpoint = 'https://api.github.com'
resp, content = h.request(endpoint+'/user',
'GET',
headers = {'Authorization': 'Basic ' + auth}
)
result:
send: b'GET /user HTTP/1.1\r\nHost: api.github.com\r\n
accept-encoding: gzip, deflate\r\n
if-none-match: "0dc6462cfbaf8e49dfcf4f679ac9d268"\r\n
if-modified-since: Fri, 25 Mar 2016 16:04:44 GMT\r\n
user-agent: Python-httplib2/0.9.2 (gzip)\r\n
authorization: Basic XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\r\n\r\n'
reply: 'HTTP/1.1 200 \r\n'
...
>resp.status
200
> content
b'{"login":"XXXX",
"id":XXXXX,
"avatar_url":"https://avatars.githubusercontent.com/u/10578703?v=3",
"gravatar_id":"",
"url":"https://api.github.com/users/Cartman0",
"html_url":"https://github.com/Cartman0",
"followers_url":"https://api.github.com/users/Cartman0/followers",
"following_url":"https://api.github.com/users/Cartman0/following{/other_user}",
...
}
Twitter も Identi.ca も、140字以内であなたのステータスを投稿し、更新できるようにしてくれるシンプルなhttpベースのapiを公開している。 ステータスを更新するための Identi.ca のapiドキュメントを見てみる。
Identi.ca rest apiメソッド: ステータス/更新
Updates the authenticating user’s status. Requires the status parameter specified below. Request must be a POST.
url
https://identi.ca/api/statuses/update.format
Formats
xml, json, rss, atom
http Method(s)
POST
Requires Authentication
true
Parameters
status. Required. The text of your status update. url-encode as necessary.
これはどのように動くのだろう? 新しいメッセージをIdenti.caに投稿するには、 http POSTリクエストをhttp://identi.ca/api/statuses/update.format に送信しなければならない(formatの部分はurlの一部ではない。ここには、リクエストに対して、サーバーにどんなデータ形式でレスポンスを返信してほしいのかを入れる。 xmlでレスポンスを返してほしければ、 https://identi.ca/api/statuses/update.xml にリクエストを送信する)。 また、リクエストにはstatusという変数を含める必要があり、この変数にステータスを更新するメッセージが入ることになる。さらに、リクエストは認証を通らなくてはならない。
認証だって? もちろん。Identi.caでステータスを更新するには、あなたが誰であるかを証明しなくてはならない。Identi.caはwikiではないのだ。だから、あなただけがあなたのステータスを更新することができる。
Identi.caはsslを介した http Basic
認証(a.k.a. RFC 2617)を利用して、セキュアで扱いやすい認証を提供している。
httplib2
はsslもhttp Basic認証もサポートしているので、
この部分は簡単に済ますことができる。
POSTリクエストとGETリクエストとの違いは、
このペイロードとはサーバーに送信したいデータのこと。 ここで、apiメソッドはデータの一部としてstatusを要求しているが、 これはurlエンコードされている必要がある。 このurlエンコードとは非常にシンプルな符号化形式で、キーと値のペアからなる集合(i.e. 辞書)を引数にとり、それを文字列に変換するというもの。
Python には辞書をurlエンコードするユーティリティ関数が用意されている。
すなわち、urllib.parse.urlencode()
だ。
from urllib.parse import urlencode
urlencode
<function urllib.parse.urlencode>
Identi.ca api が要求しているのはこの種の辞書。
ここには status
というキーが一つだけ入っていて、
それに対応する値は一回分のステータス更新のメッセージになっている。
data = {'status': 'Test update from Python 3'}
urlエンコードされた文字列はこんな感じになる。
これが http POST
リクエストの際に、
「回線を通じて」Identi.ca api
サーバーに送信されるペイロードになる。
urlencode(data)
'status=Test+update+from+Python+3'
from urllib.parse import urlencode
import httplib2
httplib2.debuglevel = 1
h = httplib2.Http('.cache')
data = {'status': 'Test update from Python 3'}
h.add_credentials('diveintomark', 'MY_SECRET_PASSWORD', 'identi.ca') ①
resp, content = h.request('https://identi.ca/api/statuses/update.xml',
... 'POST', ②
... urlencode(data), ③
... headers={'Content-Type': 'application/x-www-form-urlencoded'}) ④
add_credentials()
メソッドを用いてユーザー名とパスワードを記憶する。それから、httplib2
がリクエストを出すと、サーバーは 401 Unauthorized
ステータスコードを返し、
さらにどの認証方式をサポートしているかのリストを(WWW-Authenticateヘッダに入れて)返送してくれる。
httplib2
は自動で Authorizationヘッダを組み立てて、このurlに再びリクエストを送信してくれる。
2番目の変数はhttpリクエストの種類。ここではPOSTになる。
3番目の変数はサーバーに送信するペイロード。ここではステータスメッセージの入った辞書をurlエンコードで変換して送信する。
最後に、ペイロードがurlエンコードで符号化されたものだということをサーバーに伝えなくてはならない。
add_credentials()
メソッドの3番目の変数は、
その認証が通用するドメインを表す。
この部分については必ず明記しておくこと!
このドメインを空白のままにしておくと、 後で別の認証を必要とするサイトに対してhttplib2.Httpオブジェクトを再利用した時に、httplib2が元のサイト用のユーザー名とパスワードをその別のサイトに漏らしてしまいかねないため。
https://developer.github.com/v3/gists/#create-a-gist
Create a gist
POST /gists Input
Name | Type | Description |
---|---|---|
files | object | Required. Files that make up this gist. |
description | string | A description of the gist. |
public | boolean | Indicates whether the gist is public. Default: false |
The keys in the files object are the string filename, and the value is another object with a key of content, and a value of the file contents. For example:
{
"description": "the description for this gist",
"public": true,
"files": {
"file1.txt": {
"content": "String file contents"
}
}
}
Note: Don't name your files "gistfile" with a numerical suffix. This is the format of the automatic naming scheme that Gist uses internally.
import httplib2
from base64 import b64encode
from urllib.parse import urlencode
import json
httplib2.debuglevel = 1
h = httplib2.Http('.cache')
user = ''
password = ''
auth = b64encode(bytes(user + ':' + password, 'utf-8')).decode('utf-8')
endpoint = 'https://api.github.com'
data = {
"description": "Github API Sample",
"public": True,
"files": {
"file1.md": {
"content": "# Create a Gist with Github API"
}
}
}
resp, content = h.request(endpoint+'/gists',
'POST',
json.dumps(data),
headers = {'Authorization': 'Basic ' + auth,
'Content-Type': 'application/json'
})
result:
send: b'POST /gists HTTP/1.1\r\nHost: api.github.com\r\nContent-Length: 123\r\n
accept-encoding: gzip, deflate\r\n
content-type: application/json\r\n
user-agent: Python-httplib2/0.9.2 (gzip)\r\n
authorization: Basic XXXXXXXXXXXXXXXXXXXXXX\r\n\r\n'
send: b'{"files": {"file1.md": {"content": "# Create a Gist with Github API"}}, "public": true, "description": "Github API Sample"}'
reply: 'HTTP/1.1 201 Created\r\n'
header: Server header: Date header: Content-Type header: Content-Length header: Status header: X-RateLimit-Limit header: X-RateLimit-Remaining header: X-RateLimit-Reset header: Cache-Control header: Vary header: ETag header: Location header: X-GitHub-Media-Type header: Access-Control-Expose-Headers header: Access-Control-Allow-Origin header: Content-Security-Policy header: Strict-Transport-Security header: X-Content-Type-Options header: X-Frame-Options header: X-XSS-Protection header: Vary header: X-Served-By header: X-GitHub-Request-Id
> resp.status
201
Gist id を取得。
> content_data = json.loads(content.decode('utf-8'))
> content_data['id']
'1426de5bcd87485909ba2f0a66da6481'
以下のようにgistリポジトリが作られる。
httpは GET
と POST
だけには留まらない。
この2つは(特にウェブブラウザにおいて)最もよく使われているリクエストだが、
ウェブサービスのapiは GET
と POST
以上のものを扱うことができるし、
httplib2
の方もそれに対応する準備ができている。
from xml.etree import ElementTree as etree
tree = etree.fromstring(content) ①
status_id = tree.findtext('id') ②
> status_id
'5131472'
url = 'https://identi.ca/api/statuses/destroy/{0}.xml'.format(status_id) ③
resp, deleted_content = h.request(url, 'DELETE') ④
<id>
要素を探しているだけ。<id>
要素のテキストの内容に基づいてurlを構築する。send: b'DELETE /api/statuses/destroy/5131472.xml HTTP/1.1 ①
Host: identi.ca
Accept-Encoding: identity
user-agent: Python-httplib2/$Rev: 259 $
'
reply: 'HTTP/1.1 401 Unauthorized' ②
send: b'DELETE /api/statuses/destroy/5131472.xml HTTP/1.1 ③
Host: identi.ca
Accept-Encoding: identity
authorization: Basic SECRET_HASH_CONSTRUCTED_BY_HTTPLIB2 ④
user-agent: Python-httplib2/$Rev: 259 $
'
reply: 'HTTP/1.1 200 OK' ⑤
>>> resp.status
200
Gist api でのGistの削除は以下。
https://developer.github.com/v3/gists/#delete-a-gist
Delete a gist
DELETE /gists/:id
Response
Status: 204 No Content
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
import httplib2
from base64 import b64encode
httplib2.debuglevel = 1
h = httplib2.Http('.cache')
user = ''
password = ''
auth = b64encode(bytes(user + ':' + password, 'utf-8')).decode('utf-8')
endpoint = 'https://api.github.com'
gist_id = '1426de5bcd87485909ba2f0a66da6481'
resp, content = h.request(endpoint + '/gists/' + gist_id,
'DELETE',
headers = {'Authorization': 'Basic ' + auth}
)
result:
send: b'DELETE /gists/1426de5bcd87485909ba2f0a66da6481 HTTP/1.1\r\n
Host: api.github.com\r\n
accept-encoding: gzip, deflate\r\n
user-agent: Python-httplib2/0.9.2 (gzip)\r\n
authorization: Basic XXXXXXXXXXXXXXXXXXXXXXXXXXXXX\r\n\r\n'
reply: 'HTTP/1.1 204 No Content\r\n'
header: Server header: Date header: Status header: X-RateLimit-Limit header: X-RateLimit-Remaining header: X-RateLimit-Reset header: X-GitHub-Media-Type header: Access-Control-Expose-Headers header: Access-Control-Allow-Origin header: Content-Security-Policy header: Strict-Transport-Security header: X-Content-Type-Options header: X-Frame-Options header: X-XSS-Protection header: Vary header: X-Served-By header: X-GitHub-Request-Id
> resp.status
204
> content
b''
これで任意のgistを削除できる。