このブログを検索

第14章 HTTP Webサービス

14.1.  飛び込もう

HTTP Webサービスを短い言葉で表現すると、『HTTP操作だけでデータをリモートサーバと交換する』です。サーバからデータを取得するならHTTP GETを使います。新しいデータをサーバに送信するならHTTP POSTを使います。より発展的なHTTP WebサービスAPIでは、HTTP PUT、HTTP DELETEを使ってデータを作成、修正、削除できます。それで全てです。レジストリなし、エンベロープなし、ラッパーなし、トンネリングなし。HTTPプロトコルに作られた「動詞」(GET、POST、PUT、DELETE)が、アプリケーションレベル操作のデータ取得、作成、修正、削除に対応します。

このアプローチは単純なことが大きな利点で、それ故に人気があります。データ 一 たいていXMLかJSONです 一 は、静的に作られて保管されていることもあれば、サーバサイドスクリプトに動的に生成されていることもありますが、すべてのメジャなプログラミング言語では(もちろんPythonも含めて!)、データをダウンロードするためのHTTPライブラリを備えています。デバッグも簡単です。HTTP Webサービスのリソースはそれぞれユニークなアドレスを(URLの形式で)持っているため、Webブラウザにロードして生データを即座に見ることができます。

HTTP webサービスの例
  • Google Data APlによってGoogleのBloggerやYoutubeといった幅広いサービスと連携することができます
  • FlickerサービスではFlickerに写真をアップロードしたりダウンロードしたりすることができます
  • Twitter APIによってTwitterのステータスを更新することができます
・・・他にも沢山あります。

Python3にはHTTP Webサービスと連携するためのライブラリが2種類あります。
  • http.clientは低レベルのライブラリでHTTPプロトコルRFC2616を実装しています
  • urllib.requestはhttp.clientのトップに作られた抽象レイヤで、HTTPやftpサーバ両方にアクセスする標準APlを提供し、自動的にHTTPリダイレクトしてHTTP認証の共通形式を扱います
どちらを使うべきでしょうか?どちらでもありません。代わりにオープンソースのサードパーティライブラリhttplib2を使いましょう。http.clientよりHTTP機能が揃っていますし、urllib.requestよりも上手く抽出してくれます。

なぜhttplib2が正しい選択なのかを理解するためには、まず先にHTTPを理解しましょう。

14.2. HTTPの特徴

HTTPクライアントがサポートする5つの重要な特徴があります。

14.2.1. キャッシュ

どんなwebサービスであっても、ネットワークアクセスは極めて高くつくことを必ず理解しておくべきです。"ドルやセント"が高いという意味ではありません(確かに帯域は無料という訳ではありませんが)。ここで言いたいのは、接続してリクエストを送り、レスポンスをリモートサーバから受け取るのには非常に長い時間がかかるということです。高速なブロードバンド通信であってもレイテンシ(リクエストを送ってからレスポンスのデータを受け取り始めるまでの時間)は期待より高くなるものです。ルータの動作失敗、パケットの欠落、中間のプロキシが攻撃を受けている ー 公共インターネットでは空いた時間などありませんし、レイテンシに関してできることはないかもしれません。

HTTPはキャッシュを念頭にして設計されています。あなたと世界の間のどこかにあってネットワークアクセスを最小化するだけを目的とした、"キャッシングプロキシ"という装置群があります。意識していないかもしれませんが、あなたの会社やISPは、ほぼ間違いなくキャッシングプロキシを管理しています。キャッシュが動作する例を具体的に見てみましょう。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
Content-Type: image/jpegヘッダのCache-ControlとExpiresはブラウザ(とサーバ間のすべてのキャッシュプロキシ)に対して、この画像は最大1年間キャッシュされることを示しています。1年です!もし来年、この画像を含んだ別のページを訪れたとしてもキャッシュから画像を読み込めるので、ネットワーク上では何もしなくてよいことになります。

これだけではなく、他にも恩恵があります。例えば、何かの理由でブラウザが画像をローカルキャッシュから消してしまったとします。ディスク容量を使い切ったかもしれませんし、手動やその他の方法でキャッシュを消したかもしれません。しかし、HTTPヘッダによると、このデータはパブリック キャッシュプロキシによってキャッシュされています。(厳密には、ヘッダに書かれていないことが重要です。Cache-Controlヘッダにはキーワードprivateがないので、デフォルトでこのデータはキャッシュ可能となります。)
キャッシュプロキシは膨大なストレージ容量を持てるように設計されており、おそらくローカルブラウザに割り当てられたものよりも遥かに大きいです。

あなたの会社やISPがキャッシュプロキシを管理しているならば、プロキシにキャッシュされた画像が残っているかもしれません。diveintomark.orgに再度アクセスしたときに、ブラウザがローカルキャッシュで画像を探しても見つけられず、リモートサーバからダウンロードするためにネットワークリクエストを送ります。このとき、キャッシュプロキシが画像のコピーを持っていたならば、リクエストを止めてキャッシュから画像を渡します。つまり、リクエストはリモートサーバに届くことがなくなり、ネットワークは社内で完結します。これによってダウンロードは速くなり(ネットワークのホップが減る)、お金の節約になります(外からダウンドするデータが減る)。

HTTPキャッシュは、すべての機器が正常に動作した時にだけ有効です。まず、サーバは正常なへッダ情報を返す必要があります。一方で、クライアントはへッダ情報を理解、尊重して、同じデータを2回リクエストしないようにする必要があります。間にあるプロキシは万能薬ではなく、有効かどうかはサーバとクライアント次第です。PythonのHTTPライブラリはキャッシュをサポートしていませんが、httplib2ではサポートします。

14.2.2. Last-Modified (最終更新)の確認

データによっては全く変更されないこともありますし、時々刻々と変わっているものもあります。大部分のデータはその中間にあって、データが変わったように見えて、実は変わっていないのです。CNN.comのフィードは数分毎に更新されていますが、私のweblogのフィードは数日や数週間に1回しか変わらないかもしれません。後者の場合、フィードをキャッシュの設定を数週間に1度するようにとクライアントに伝えたくはありません。そうしてしまうと、実際に投稿したときでも、人々は何週間も新しい投稿を読まない可能性があるからです(私のブログの「このフィードは数週間チェックしないでよい」というキャッシュヘッダを尊重することになるからです)。一方、変更がないのに、1時間に1回すべてのフィードをクライアントにダウンロードしてもらいたくはありません。

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
2回目に(3、4回目でも)同じデータをリクエストするとき、サーバから前回受け取った日時とIf-Modified-Sinceヘッダをリクエストに入れて送ることができます。前回以降データが変更されているならば、サーバは新しいデータをステータスコード200と合わせて送ってくれます。データに変更がないのであれば、サーバは特別なHTTPステータスコードである304を送ります。これは「このデータは前回あなたが見たときから変わっていません」という意味です。これをコマンドラインでcurlを使ってテストすることができます。
you@localhost:~$ 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を確認すれば、変更されていない同じデータを2回ダウンロードすることは確実になくなります。(特別ボーナスとして、この304レスポンスはキャッシュヘッダも含んでいます。プロキシは正式に"期限切れ"になった後も、次にリクエストがあったときにデータが実際には変わっていないことを期待して、データのコピーを保持しています。変わっていなければ304ステータスコードを返してキャッシュ情報を更新します。)

PythonのHTTPライブラリはlast-modifiedの日時確認をサポートしていませんが、httplib2はサポートしています。

14.2.3. ETagによる確認

ETagは、last-modifiedの確認と同じことができます。ETagの場合、サーバはハッシュコードを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回目にリクエストするときは、If-None-MatchヘッダにETagハッシュを入れます。データが変わっていなければ、サーバは304ステータスコードを返します。last-modifiedデータの確認と同様に、サーバは304ステータスコードだけを返し、変更のないデータは送りません。ETagハッシュを2回目のリクエストに含めることで、ハッシュがマッチすれば前回受け取って保持している同じデータを再送する必要がないとサーバに教えるのです。

もう1度curlを使ってみましょう:
you@localhost:~$ 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ではサポートしています。

14.2.4.圧縮

Webサービスでは、テキストベースのデータが回線を通じて行ったり来たりしています。XMLかもしれないし、JSONかもしれないし、単なる平文かもしれません。フォーマットが何であれ、テキストはうまく圧縮することができます。XMLの章で見たフィードの例では、圧縮なしで3070バイトでしたが、gzip圧縮で941バイトになります。元のサイズの30%です!

HTTPではいくつかの圧縮アルゴリズムをサポートしていますが、最もよく使われるのはgzipとdeflateの2つです。HTTPを通じてリソースをリクエストするとき、圧縮フォーマットで送るようにサーバに指示することができます。

サポートしている圧縮アルゴリズムをリストにしたAccept-encodingをへッダをリクエストに入れます。サーバがどれかの圧縮アルゴリズムをサポートしていれば、圧縮したデータを送り返してくれます(Content-encodingヘッダに、サーバがどのエンコーディングを使ったかが書かれています)。解凍するかどうかはあなた次第です。

👉サーバサイドデベロッパーに重要なTips: 圧縮したバージョンのリソースが、圧縮していないバージョンと異なるETagを持っているか確認してください。そうしないと、キャッシュプロキシが混乱してしまい、扱えない圧縮バージョンをクライアントに出してしまいます。この微妙な問題についてはApache bug 39727のディスカッションを読んでください。

Python HTTPライブラリは圧縮をサポートしていませんが、httplib2ではサポートしています。

14.2.5 リダイレクト

クールなURlは変らないものですが、たいていのURIは全くクールではありません。Webサイトが再構築されると、新しいアドレスに移ります。Webサービスであっても再構築されます。いつも使われているフィード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サーバに何かをリクエストすると、サーバからの応答には必ずステータスコードが入っています。ステータスコード200は『すべて順調、これがあなたの要求したページです』という意味です。ステータスコード404は『page not found』です(この404エラーはWebブラウジング中に見たことがあるでしょう)。

300番代のステータスコードはリダイレクトの種類を表しています。HTTPにはリソースが移動したことを示す方法がいくつかありますが、よく使われるのはステータスコード301と302の二種類です。ステータスコード302はテンポラリリダイレクトで『おっと、一時的に移動したよ』という意味です(引き続いてテンポラリアドレスをLocationヘッダに渡します)。ステータスコード301はパーマネントリダイレクトで『おっと、永久的に移動したよ』という意味です(引き続いて新しいアドレスをLocationヘッダに渡します)。302ステータスコードと新しいアドレスを受け取ったときは、このHTTP仕様は新しいアドレスを使ってリクエストを見ることを記述していますが、次回また同じリソースにアクセスするときは古いアドレスにリトライすることになります。一方、301ステータスコードと新しいアドレスを受け取ったときは、それ以降は新しいアドレスを使うことになります。

urllib.requestモジュールは適切なステータスコードをHTTPサーバから受け取ると、自動的にリダイレクトに"従い"ますが、そのことを知らせるわけではありません。結果的にあなたは要求したデータを受け取っているので、ライブラリが影で"気を利かせて"リダイレクトしてくれたことなど知るはずもないのです。urllib.requestが毎回"気を利かせて"リダイレクトしてくれるおかげで、古いアドレスに行ってしまうことなく毎回新しいアドレスにリダイレクトし続けます。言い換えると、urllib.requestがパーマネントリダイレクトをテンポラリリダイレクトと同じように扱っているのです。つまり、1往復で良いところを2往復するので、サーバにとってもあなたにとっても悪いことなのです。

httplib2はパーマネントリダイレクトを扱います。パーマネントリダイレクトが起こったことを通知するだけでなく、ローカルにURLを記録しておいて、リクエストする前にリダイレクトされたURLを自動的に書き換えるのです。

14.3. HTTPからデータをフェッチしないようにするには

AtomフィードのようなリソースをHTTPからダウンロードすることを考えます。フィードですから、ダウンロードは1度だけではありません。何度もダウンロードすることになります(ほとんどのフィードリーダーは、1時間に1度変更を確認します)。先に手っ取り早い方法で試してから、どうすればよりよくなるか見ていきましょう。
>>> import urllib.request
>>> a_url = 'http://diveintopython3.org/examples/feed.xml'
>>> data = urllib.request.urlopen(a_url).read()  # ①
>>> type(data)                                   # ②

>>> print(data)
  dive into mark
  currently between addictions
  tag:diveintomark.org,2001-07-29:/
  2009-03-27T21:56:07Z
  
①Pythonを使ってHTTPからダウンロードすることは極めて容易です。ワンライナーでできます。urllib.requestモジュールには使いやすいurlopen()関数があって、欲しいページのアドレスを受け取ってファイルライクのオブジェクトを返し、read()とするだけでページの全内容を取得することができます。これ以上なくかんたんです。

②urlopen.read()メソッドは文字列を返さずに、バイトオブジェクトを必ず返します。覚えていますか、バイトはバイト、文字というものは概念です。HTTPサーバは概念を扱いません。リソースをリクエストすれば、バイトを取得することになります。文字列が欲しければ、文字列エンコーディングを定義して明示的にバイトを文字列に変換する必要があります。

何か問題があるでしょうか?急いで行う1度のテストや開発であれば、これで問題はありません。私はいつでもこのやり方です。フィードの内容が欲しくて、その内容を受け取りました。同じ方法がどのWebページにでも適用できます。しかし、一定の決まりでアクセスするWebサービス(例えば、このフィードを毎時間リクエストする)として考えはじめると、非効率で、失礼になっているとわかるでしょう。

14.4. 何が繋がっているか?

なぜ非効率で失礼かを知るために、Python HTTPライブラリのデバッグ機能を使って、何が"ワイヤー上で" (つまり、ネットワークで)送られているのか見てみましょう。
>>> from http.client import HTTPConnection
>>> HTTPConnection.debuglevel = 1                                       # ①
>>> from urllib.request import urlopen
>>> response = urlopen('http://diveintopython3.org/examples/feed.xml')  # ②
send: b'GET /examples/feed.xml HTTP/1.1                                 # ③
Host: diveintopython3.org                                               # ④
Accept-Encoding: identity                                               # ⑤
User-Agent: Python-urllib/3.1'                                          # ⑥
Connection: close
reply: 'HTTP/1.1 200 OK'
…further debugging information omitted…
①この章のはじめに出てきたように、urllib.requestは他のPythonの標準ライブラリhttp.clientに依存しています。通常はhttp.ciientを直接触る必要はありません(urllib.requestモジュールが自動的にインポートします)。しかしここでは、urllib.requestがHTTPサーバに接続するために使うHTTPConnectionクラス内でデバッグフラッグをオンにするためにインポートしています。

②デバッグフラグがセットされたので、HTTPリクエストとレスポンスの情報がリアルタイムで出力されます。Atomフィードをリクエストすると、urllib.requestモジュールはサーバに5行を送っていることがわかります。

③1行目は使っているHTTP動詞、 リソース(ドメイン名を除く)のパスを指定します。

④2行目は、このフィードをリクエストしているドメイン名を指定します。

⑤3行目は、クライアントがサポートする圧縮アルゴリズムを指定します。先ほど述べたように、urllib.requestはデフォルトでは圧縮をサポートしていません。

⑥4行目は、リクエストをしているライブラリ名を指定します。デフォルトはPython-urllibとバージョン番号です。urllib.request、httplib2はどちらもユーザエージェントの変更をサポートしていて、変更するためにはUser-Agentヘッダをリクエストに追加するだけでよいのです(デフォルト値がオーバーライドされます)。

ではサーバがレスポンスとして送ってきたものを見てみましょう。
# continued from previous example
>>> print(response.headers.as_string())        # ①
Date: Sun, 31 May 2009 19:23:06 GMT            # ②
Server: Apache
Last-Modified: Sun, 31 May 2009 06:39:55 GMT   # ③
ETag: "bfe-93d9c4c0"                           # ④
Accept-Ranges: bytes
Content-Length: 3070                           # ⑤
Cache-Control: max-age=86400                   # ⑥
Expires: Mon, 01 Jun 2009 19:23:06 GMT
Vary: Accept-Encoding
Connection: close
Content-Type: application/xml
>>> data = response.read()                     # ⑦
>>> len(data)
3070
# ① urllib.request.urlopen()関数から返って来たレスポンスには、サーバが送り返したすべてのHTTPヘッダが含まれています。実際のデータをダウンロードするメソッドも含んでいますので、すぐあとで見ていきます。

② サーバがリクエストを処理した時間です。

③ この応答はlast-modifiedヘッダを含んでいます。

④ この応答はETagヘッダを含んでいます。

# ⑤ データ長は3070バイトです。何かがないことに気がつきましたか。Content-encodingヘッダです。リクエストが宣言したのは圧縮していないデータを受け入れるということだけで(Accept-encoding: identity)、確かに、このレスポンスは非圧縮のデータを含んでいます。

# ⑥ このレスポンスはキャッシュヘッダを含んでいて、フィードは最大24時間(86400秒)キャッシュされると主張しています。

# ⑦ 最後に、実際のデータをresponse.read()を呼び出してダウンロードします。len()関数を使うとわかるように、このフェッチの合計は3070バイトです。

このコードを見ると、非効率だとわかります。非圧縮データを要求して(そして受け取って)いるのですから。このサーバはgzip圧縮をサポートしている事実を知っていますが、HTTP圧縮はオプトインです。求めなかったので、取得しませんでした。つまり941バイトでよかったところを3070バイトのフェッチをしてしまいました。悪い犬だ、ビスケット抜きだよ。

でも待ってください、もっと悪い部分があります!このコードがどれだけ非効率かを見るために、同じフィードを2度リクエストしてみましょう。
# continued from the previous example
>>> response2 = urlopen('http://diveintopython3.org/examples/feed.xml')
send: b'GET /examples/feed.xml HTTP/1.1
Host: diveintopython3.org
Accept-Encoding: identity
User-Agent: Python-urllib/3.1'
Connection: close
reply: 'HTTP/1.1 200 OK'
…further debugging information omitted…
このリクエストが変なことに気がつきましたか? 何も変わっていません! 1度目と全く同じリクエストです。If-Modified-Sinceヘッダがありません。If-None-Matchヘッダがありません。キャッシュヘッダに対する言及がありません。やはり圧縮はありません。

同じことを2回すれば何が起こるでしょうか? 同じレスポンスを受け取るのです。2回です。
# continued from the previous example
>>> print(response2.headers.as_string())     # ①
Date: Mon, 01 Jun 2009 03:58:00 GMT
Server: Apache
Last-Modified: Sun, 31 May 2009 22:51:11 GMT
ETag: "bfe-255ef5c0"
Accept-Ranges: bytes
Content-Length: 3070
Cache-Control: max-age=86400
Expires: Tue, 02 Jun 2009 03:58:00 GMT
Vary: Accept-Encoding
Connection: close
Content-Type: application/xml
>>> data2 = response2.read()
>>> len(data2)                               # ②
3070
>>> data2 == data                            # ③
True
# ① サーバは依然として同じ"スマートな"ヘッダ列を送っています。キャッシュコントロールとキャッシュ期限でキャッシュを許可していて、Last-ModifiedとETagで"更新されていないことを"を追跡しています。要求すればの話ですが、Vary: Accept-Encodingヘッダは、サーバが圧縮をサポートしているヒントになります。しかし要求しませんでした。

② もう1度、このリクエストは3070バイト全部をフェッチするので・・・

③ ・・・全く同じ3070バイトが先ほどと同じように返って来ます。

HTTPはこれよりも上手く動くように設計されています。urllibはHTTP私がスペイン語を話す程度にであれば話すことができます。雑談するには充分ですが会話を続けるためには不十分です。HTTPは会話です。HTTPを流暢に話せるライブラリにアップグレードする時がきました。

14.5. httpli2を導入する

httplib2を使うためには、インストールする必要があります。code.google.com/p/httplib2/にアクセスして最新版をダウンロードします。httplib2はPython2.xでもPython3.xでも手に入りますが、必ずPython3のバージョンを使うようにしてください。名前はhttplib2-python3-0.5.0.zipのようになっています。

アーカイブを展開して、ターミナルウィンドウを開いて、新しくhttplib2ディレクトリを作成して移動します。Windowsではスタートメニューを開いて、「プログラムとファイルの検索」を選択し、cmd.exeとタイプしてエンターを押します。
c:\Users\pilgrim\Downloads> dir
 Volume in drive C has no label.
 Volume Serial Number is DED5-B4F8

 Directory of c:\Users\pilgrim\Downloads

07/28/2009  12:36 PM    <DIR>          .
07/28/2009  12:36 PM    <DIR>          ..
07/28/2009  12:36 PM    <DIR>          httplib2-python3-0.5.0
07/28/2009  12:33 PM            18,997 httplib2-python3-0.5.0.zip
               1 File(s)         18,997 bytes
               3 Dir(s)  61,496,684,544 bytes free

c:\Users\pilgrim\Downloads> cd httplib2-python3-0.5.0
c:\Users\pilgrim\Downloads\httplib2-python3-0.5.0> c:\python31\python.exe setup.py install
running install
running build
running build_py
running install_lib
creating c:\python31\Lib\site-packages\httplib2
copying build\lib\httplib2\iri2uri.py -> c:\python31\Lib\site-packages\httplib2
copying build\lib\httplib2\__init__.py -> c:\python31\Lib\site-packages\httplib2
byte-compiling c:\python31\Lib\site-packages\httplib2\iri2uri.py to iri2uri.pyc
byte-compiling c:\python31\Lib\site-packages\httplib2\__init__.py to __init__.pyc
running install_egg_info
Writing c:\python31\Lib\site-packages\httplib2-python3_0.5.0-py3.1.egg-info
MacOSXでは、/Applications/Utilities/フォルダにあるTerminal.appアプリケーションを起動します。Linuxでは、通常アクセサリの下にあるアプリケーションメニューやシステムにあるターミナルを起動します。
you@localhost:~/Desktop$ unzip httplib2-python3-0.5.0.zip
Archive:  httplib2-python3-0.5.0.zip
  inflating: httplib2-python3-0.5.0/README
  inflating: httplib2-python3-0.5.0/setup.py
  inflating: httplib2-python3-0.5.0/PKG-INFO
  inflating: httplib2-python3-0.5.0/httplib2/__init__.py
  inflating: httplib2-python3-0.5.0/httplib2/iri2uri.py
you@localhost:~/Desktop$ cd httplib2-python3-0.5.0/
you@localhost:~/Desktop/httplib2-python3-0.5.0$ sudo python3 setup.py install
running install
running build
running build_py
creating build
creating build/lib.linux-x86_64-3.1
creating build/lib.linux-x86_64-3.1/httplib2
copying httplib2/iri2uri.py -> build/lib.linux-x86_64-3.1/httplib2
copying httplib2/__init__.py -> build/lib.linux-x86_64-3.1/httplib2
running install_lib
creating /usr/local/lib/python3.1/dist-packages/httplib2
copying build/lib.linux-x86_64-3.1/httplib2/iri2uri.py -> /usr/local/lib/python3.1/dist-packages/httplib2
copying build/lib.linux-x86_64-3.1/httplib2/__init__.py -> /usr/local/lib/python3.1/dist-packages/httplib2
byte-compiling /usr/local/lib/python3.1/dist-packages/httplib2/iri2uri.py to iri2uri.pyc
byte-compiling /usr/local/lib/python3.1/dist-packages/httplib2/__init__.py to __init__.pyc
running install_egg_info
Writing /usr/local/lib/python3.1/dist-packages/httplib2-python3_0.5.0.egg-info
httplib2を使うためには、httplib2.Httpクラスのインスタンスを作ります。
>>> import httplib2
>>> h = httplib2.Http('.cache')                                                    # ①
>>> response, content = h.request('http://diveintopython3.org/examples/feed.xml')  # ②
>>> response.status                                                                # ③
200
>>> content[:52]                                                                   # ④
b"\r\n<feed xmlns="
>>> len(content)
3070
# ① httpiib2の最初のインスタンスはHTTPオブジェクトです。理由は次のセクションでわかりますが、HTTPオブジェクトを作るときにはディレクトリ名を常に渡さなければなりません。ディレクトリを作っておく必要はありません。必要に応じてhttplibが作ってくれるからです。

# ② HTTPオブジェクトを作ったあとは、データの取得方法は、欲しいデータのアドレスをつけてrequest()メソッドを呼び出すというシンプルなものです。これによって、そのURLでHTTP GET requestを請求(issue)することになります(この章では、POSTのような他のhttp requestを請求する方法も見ていきます)。

# ③ このrequest()メソッドは2つの値を返します。1つはhttplib2.Responseオブジェクトで、サーバが返したHTTPヘッダをすべて含んでいます。例えば、ステータスコード200はリクエストが成功したことを表しています。

# ④ 中にある変数はHTTPサーバから返って来たデータそのものを含んでいます。このデータは文字列ではなく、バイトオブジェクトとして返って来ています。文字列が欲しいならば、文字エンコーディングを特定して自分で変換しなくてはなりません。

👉あなたはhttplib2.Httpオブジェクトを1つだけ必要としているのかもしれません。複数を作成する充分な理由があるのですが、なぜかを知りたいのであれば、1度やってみるべきでしょう。「2つの別のulrからデータをリクエストする必要がある」というのは充分な理由ではありません。HTTPオブジェクトを再利用してrequest()メソッドを2回呼び出せば良いだけです。

14.5.1. 少し脱線: httplib2が文字列ではなくバイトを返す理由について

バイト。文字列。苦痛です。なぜhttplib2には"単なる会話"ができないのでしようか?そこには複雑な理由があります。文字エンコーディングを決めるルールは、どういうリソースをリクエストするかによって決まります。httplib2はどうやってリクエストしているリソースの種類を知るのでしょうか?リソースはContent-Type HTTPヘッダの中に書かれていますが、HTTPのオプション機能ですから、すべてのHTTPサーバが持っているわけではありません。そのへッダがHTTP応答に含まれていなければ、クライアント側の想像に委ねられます。(これは"コンテントスヌフィング"と慣習的に呼ばれているものですが、完璧には程遠いものです)

どのリソースが来るかを知っているときは(この場合はXMLドキュメントです)、"単に"xml.etree.EIementTree.parse()関数に返って来たバイトオブジェクトを渡せばよいのです。 リソースがXMLドキュメントでそれ自身に文字エンコーディングを含んでいる場合に限れば(今のように)、上手くはたらきます。しかし、これはオプション機能であってすべてのXMLドキュメントにできるわけではありません。XMLドキュメントがエンコーディング情報を持っていなければ、クライアントはエンクロージングトランスポートーつまり、charsetパラメータを持つContent-Type HTTPヘッダを調べることになります。

しかし、もっと悪いことが起きます。このとき、文字エンコーディング情報は2箇所に入っている可能性があります。つまり、XMLドキュメントそのものと、Content-Type HTTPヘッダにです。情報が2箇所にあるならば、どちらを優先すればよいでしょう?RFC3023によると(誓って言いますが私が作ったわけではありませんよ)、Content-Type HTTPヘッダが入っているメディアタイプが application/xml、 application/xml-dtd、application/xml-external-parsed-entityやappl ication/xmlのサブタイプであるapplication/atom+xml、application/rss+xml、even application/rdf+xml
であれば、エンコーディングは
  1. Content-Type HTTPヘッダのcharsetパラメータの中にある文字エンコーディングか
  2. ドキュメント内のXML宣言のエンコーディング属性にあるエンコーディングか、
  3. urfー8
を使います。

一方、Content-Type HTTPヘッダのメディアタイプがtext/xml、text/xml-external-parsed-entity、やtext/AnythingAtAll+xmlのようなサブタイプであれば、ドキュメント内のXML宣言のエンコーディング属性にあるエンコーディングは完全に無視されて、
  1. Content-Type HTTPヘッダの文字セットパラメータがエンコーディングになるか、
  2. us-asciiになります。
これらはXMLドキュメントに関してのみです。HTMLドキュメントに関しては、Webブラウザはコンテントスニッフィングのための厳格なルールが構築されているので<PDFへのリンク>、今でもすべてを理解しようとしている途中なのです。

"パッチはウエルカムです" [訳注: 運用が終了したGoogleCodeへのリンクがあった]

14.5.2. httplib2でのキャッシュの扱い

前のセクションで、httplib2.Httpオブジェクトを作るときはディレクトリ名を使うと言ったことを覚えていますか?これはキャッシュ化が理由です。
# continued from the previous example
>>> response2, content2 = h.request('http://diveintopython3.org/examples/feed.xml')  # ①
>>> response2.status                                                                 # ②
200
>>> content2[:52]                                                                    # ③
b"<?xml version='1.0' encoding='utf-8'?>\r\n<feed xmlns="
>>> len(content2)
3070

# ① これはそこまで驚くようなことではないでしょう。新しい2つの変数に結果を代入していること以外は、前と同じです。

② HTTPステータスはここでも前と全く同じで200です。

③ ダウンロードされた内容も、前と同じです。

つまり・・・どういうことでしょうか?対話式Pythonシェルを閉じて、新しいセッションで開いてみてください。やってみましょう。

#前の例からの続きではありません
#対話式シェルを抜けて新しく立ち上げます
# NOT continued from previous example!
# Please exit out of the interactive shell
# and launch a new one.
>>> import httplib2
>>> httplib2.debuglevel = 1                                                        # ①
>>> h = httplib2.Http('.cache')                                                    # ②
>>> response, content = h.request('http://diveintopython3.org/examples/feed.xml')  # ③
>>> len(content)                                                                   # ④
3070
>>> response.status                                                                # ⑤
200
>>> response.fromcache                                                             # ⑥
True
# ① デバッグをオンにしてどうなるか見てみましょう。http.clientでデバッグをオンにしているのと同じことを、httplib2がやっています。httplib2はサーバに送られたすべてのデータや、送り返された重要な情報を出力します。

② 先ほどと同じディレクトリ名でhttplib2.Httpオブジェクトを作ります。

# ③ 先ほどと同じURLをリクエストします。何も起こらないように見えます。より正確に言うと、何もサーバに送られていませんし、サーバからは何も返ってきていません。ネットワーク活動が何もないことは確かです。

# ④ しかし、何かのデータを"受け取って"います。実は、データのすべてを受け取っていたのです。

# ⑤ また、"リクエスト"が成功していたことを示すステータスコードを"受け取って"いました。

# ⑥ 問題はここにあります。この"レスポンス"はhttplib2のローカルキャッシュから作られています。httplib2.Httpオブジェクトを作るときに渡したディレクトリ名は、これまで実行されてきた操作のすべてによってできたhttplib2キャッシュを保持しているディレクトリのものでした。

👉httplib2デバッグをオンにしたいときは、モジュールレベル定数(httplib2.debuglevel)を設定したあと、新しいhttplib2.Httpオブジェクトを作ります。デバッグをオフにしたければ、同じモジュールレベル定数を変更して、新しいhttplib2.HTTPオブジェクトを作ります。

先ほどはこのURLのデータをリクエストしました。そのリクエストは成功しています(ステータス:200)。レスポンスはフィードデータだけではなく、キャッシュヘッダのセットを持っていて、リソースは最大で24時間キャッシュできることを受信者全員に告げていました(Cache-Controi:max-age=86400というのは24時間を秒で表したものです)。httplib2は、これらのキャッシュヘッダを理解して受け入れて、前回のレスポンスをcacheディレクトリ(HTTPオブジェクトを作るときに指定したもの)に保管します。キャッシュの期限が切れていなければ、このURLに2回目にリクエストしたとき、httplib2はネットワークに繋ぐことすらしないで単にキャッシュ結果を返します。

"単に"と言いましましたが、この単純さの裏に多くの複雑さが隠れているのは明らかです。httpli2はHTTPキャッシュを自動的に取扱うのがデフォルトです。何か理由があって、レスポンスがキャッシュから来ているかどうか知りたい場合は、response.fromcacheで調べることができます。そうでないときは、いつも通りに動作します。

仮に、キャッシュされたデータがあるとして、キャッシュをバイパスしてリモートサーバから再リクエストしたいとします。ユーザが指定することでブラウザはこのような動作をすることがあります。例えば、F5を押すと現在のページを再読込みしますが、Ctrl+F5ではキャッシュをバイパスして現在のページをリモートサーバから再リクエストします。こう考えたのかもしれません。「う一ん、ローカルキャッシュからデータを消して、またリクエストしよう」。そうすることもできますが、あなたとリモートサーバの間には沢山の仲間が関わっていることを覚えていますか。中間のプロキシサーバはどうでしょうか?プロキシサーバは完全にコントロール外で、もしかするとまだデータをキャッシュしているかもしれませんし、(そう認識しているならば)キャッシュが有効なので喜んで返してきます。

ローカルキャッシュを操作してベストを求める代わりに、HTTPの機能を使ってリクエストが本当にリモートサーバに届いたかを確認するべきです。
# continued from the previous example
>>> response2, content2 = h.request('http://diveintopython3.org/examples/feed.xml',
...     headers={'cache-control':'no-cache'})  # ①
connect: (diveintopython3.org, 80)             # ②
send: b'GET /examples/feed.xml HTTP/1.1
Host: diveintopython3.org
user-agent: Python-httplib2/$Rev: 259 $
accept-encoding: deflate, gzip
cache-control: no-cache'
reply: 'HTTP/1.1 200 OK'
…further debugging information omitted…
>>> response2.status
200
>>> response2.fromcache                        # ③
False
>>> print(dict(response2.items()))             # ④
{'status': '200',
 'content-length': '3070',
 'content-location': 'http://diveintopython3.org/examples/feed.xml',
 'accept-ranges': 'bytes',
 'expires': 'Wed, 03 Jun 2009 00:40:26 GMT',
 'vary': 'Accept-Encoding',
 'server': 'Apache',
 'last-modified': 'Sun, 31 May 2009 22:51:11 GMT',
 'connection': 'close',
 '-content-encoding': 'gzip',
 'etag': '"bfe-255ef5c0"',
 'cache-control': 'max-age=86400',
 'date': 'Tue, 02 Jun 2009 00:40:26 GMT',
 'content-type': 'application/xml'}
①httplib2は、出て行くすべてのリクエストに任意のHTTPヘッダを追加することを許可します。すべてのキャッシュをバイパスさせるため(ローカルディスクのキャッシュだけでなく、あなたとリモートサーバの間のすべてのキャッシュプロキシ)、キャッシュの無いへッダをheadersディレクトリに追加します。

②httplib2がネットワークリクエストを開始することがわかります。httplib2はキャッシュヘッダを理解して、受け入れます。やってくるレスポンスと出て行くレスポンスの両方においてです。キャッシュ無しのへッダを追加したことに気がついたので、ローカルキャッシュをすべてバイパスして、ネットワークに繋いでデータをリクエストするしか方法がありませんでした。

# ③ このレスポンスはあなたのローカルキャッシュで作られました。もちろん、知っていたでしょう。出て行くリクエスト上のデバッグ情報を見ていましたから。ですが、プログラムで確認していたのでよかったです。

# ④ リクエストが成功しました。すべてのフィードをリモートサーバから再度ダウンロードします。もちろん、このサーバはHTTPヘッダの完全版をデータフィードと合わせて送り返します。これにはキャッシュヘッダが含まれます。httplib2は次回このフィードをリクエストしたときのネットワークアクセスを避けることを期待して、ローカルキャッシュを更新するためにこれを使います。HTTPキャッシュはすべてにおいてキャッシュ利用率を最又大化してネットワークアクセスを最小化するために設計されています。今回キャッシュをバイパスしたとしても、次回のために結果をキャッシュしたことにリモートサーバは大いに感謝してくれます。

14.5.3.httplib2はどのようにLast-ModifiedとETagヘッダを扱うのか

Cache-ControlやExpiresといったキャッシュヘッダは"鮮度指標"と呼ばれます。キャッシュに対して、期限が切れるまでは絶対にネットワークにアクセスしないと明確に伝えます。これは、前のセクションで見た挙動そのものです。鮮度指標が与えられたので、httplib2は1バイトもネットワークアクセスをすることなく、キャッシュデータを使いました(もちろん、敢えてキャッシュをバイパスしない場合に限ります)。

データが変更された可能性があったものの、実はされていなかったときはどうなるのでしょうか?このためにHTTPにはlast-Modified、ETagヘッダがあります。これらのへッダはvalidatorsと呼ばれています。ローカルキャッシュが新しくなくなった場合は、クライアントは次のリクエストと共にvalidatorsを送ってデータが実際に変更されたかを確認します。データが変わっていなければ、サーバは304ステータスコードを送り返して、データは送りません。ネットワークの往復はありますが、すこしのバイトをダウンロードするだけで済みます。
>>> import httplib2
>>> httplib2.debuglevel = 1
>>> h = httplib2.Http('.cache')
>>> response, content = h.request('http://diveintopython3.org/')  # ①
connect: (diveintopython3.org, 80)
send: b'GET / HTTP/1.1
Host: diveintopython3.org
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 200 OK'
>>> print(dict(response.items()))                                 # ②
{'-content-encoding': 'gzip',
 'accept-ranges': 'bytes',
 'connection': 'close',
 'content-length': '6657',
 'content-location': 'http://diveintopython3.org/',
 'content-type': 'text/html',
 'date': 'Tue, 02 Jun 2009 03:26:54 GMT',
 'etag': '"7f806d-1a01-9fb97900"',
 'last-modified': 'Tue, 02 Jun 2009 02:51:48 GMT',
 'server': 'Apache',
 'status': '200',
 'vary': 'Accept-Encoding,User-Agent'}
>>> len(content)                                                  # ③
6657
# ① フィードの代わりに、今回はサイトのHTMLホームページをダウンロードしようとしています。
このページのリクエストは初めてなので、httplib2はほとんど働きません、最小限のへッダをリクエストと共に返します。

# ② レスポンスは大量のHTTPヘッダがありますが・・・キャッシュ情報はありません。しかし、ETagとLast-Modifiedヘッダを含んでいます。

# ③ この例を作ったときは、このページは6657バイトでした。おそらくそれから変わっているでしょう。しかし、心配することはありません。
# continued from the previous example
>>> response, content = h.request('http://diveintopython3.org/')  # ①
connect: (diveintopython3.org, 80)
send: b'GET / HTTP/1.1
Host: diveintopython3.org
if-none-match: "7f806d-1a01-9fb97900"                             # ②
if-modified-since: Tue, 02 Jun 2009 02:51:48 GMT                  # ③
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 304 Not Modified'                                # ④
>>> response.fromcache                                            # ⑤
True
>>> response.status                                               # ⑥
200
>>> response.dict['status']                                       # ⑦
'304'
>>> len(content)                                                  # ⑧
6657
# ① 同じHTTPオブジェクト(と同じローカルキャッシュ)で、同じページを再度リクエストしました。

② httplib2はIf-None-Matchヘッダの中にETag validatorを入れてサーバに送り返しました。

③ httplib2はIf-Modified-Sinceヘッダの中に入れてLast-Modified Validatorをサーバに送り返します。

# ④ サーバはこれらのvalidatorを見て、リクエストしたページを見て、ページは前回リクエストされたときから変わっていないと判断しました。そのため304コードを送って、データは送りませんでした。
# ⑤ クライアントに戻ります。httpiib2は304ステータスコードに気がついて、キャッシュからページ内容をロードします。

# ⑥ これはすこし混乱するかもしれません。実際には2つのステータスコードがあります。304(今回サーバから返ったコードで、httplib2にキャッシュを見るようにさせた)、200(前回サーバから返っていたコードで、httplib2のキャッシュにページデータとともに保存されている)。response.statusはキャッシュからステータスを返します。

# ⑦ 生のステータスコードをサーバに返してほしいなら、response.dictを見ます。サーバから返る、正味のへッダの辞書です。

⑧しかし、データはcontent変数から得られます。一般的に、なぜ応答がキャッシュから出てくるかを知る必要はありません(キャッシュから来ていることを気にしてすらいないかもしれませんが、それで構いません。httplib2はあなたが愚かでいられるくらい賢いのです)。request()メソッドが呼び出し側に返るときには、httplib2は既にキャッシュを更新してあなたにデータを返しています。

14.5.4. httplib2はどのように圧縮を扱うか

HTTPはいくつかの圧縮をサポートしています。最も使われている2つの種類はgzipとdeflateです。httplib2はどちらもサポートしています。
>>> response, content = h.request('http://diveintopython3.org/')
connect: (diveintopython3.org, 80)
send: b'GET / HTTP/1.1
Host: diveintopython3.org
accept-encoding: deflate, gzip                          # ①
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 200 OK'
>>> print(dict(response.items()))
{'-content-encoding': 'gzip',                           # ②
 'accept-ranges': 'bytes',
 'connection': 'close',
 'content-length': '6657',
 'content-location': 'http://diveintopython3.org/',
 'content-type': 'text/html',
 'date': 'Tue, 02 Jun 2009 03:26:54 GMT',
 'etag': '"7f806d-1a01-9fb97900"',
 'last-modified': 'Tue, 02 Jun 2009 02:51:48 GMT',
 'server': 'Apache',
 'status': '304',
 'vary': 'Accept-Encoding,User-Agent'}
# ① httplib2がリクエストを送るときはいつもAccept-Encodingヘッダを含んでいて、deflateもgzip圧縮も使えることをサーバに伝えています。

# ② この場合は、サーバはgzip圧縮ぺイロードで応答しています。request()メソッドが返るまでには、httpiib2は応答の本体を解凍してcontent変数の中に置きました。応答が圧縮されているか興味があるときは、response['-content-encoding']を使って調べることができます。興味がなければ、気にしないで構いません。

14.5.5. httplib2によるリダイレクト処理

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'
# ① URLにフィードはありません。正しいアドレスへのテンポラリリダイレクトを発行するように設定しています。

② リクエストがきました。

# ③ レスポンスは302Foundです。ここには表示されませんが、この応答はLocationヘッダを含んでいて真のURLを示しています。

# ④ httplib2は直ぐに方向を変えて、LocationヘッダにあるURLに別のリクエストを出してリダイレクトを"フォロー"します。

リダイレクトを"フォロー"するとはまさにこの例のことです。httplib2はあなたが望んだURLにリクエストを送ります。サーバはこのような応答で返ってきます「ちがうちがう、代わりにあれを見て」。httplib2は新しいURLに別のリクエストを送ります。
# continued from the previous example
>>> response                                                          # ①
{'status': '200',
 'content-length': '3070',
 'content-location': 'http://diveintopython3.org/examples/feed.xml',  # ②
 'accept-ranges': 'bytes',
 'expires': 'Thu, 04 Jun 2009 02:21:41 GMT',
 'vary': 'Accept-Encoding',
 'server': 'Apache',
 'last-modified': 'Wed, 03 Jun 2009 02:20:15 GMT',
 'connection': 'close',
 '-content-encoding': 'gzip',                                         # ③
 'etag': '"bfe-4cbbf5c0"',
 'cache-control': 'max-age=86400',                                    # ④
 'date': 'Wed, 03 Jun 2009 02:21:41 GMT',
 'content-type': 'application/xml'}
# ① この1度のrequest()メソッドへのコールによって受け取ったresponseは最終URLからの応答です。

# ② httplib2は最終URLをcontent-locationとしてresponse辞書に追加します。これはサーバから来るへッダではありません。httplib2の特徴です。

③ 唐突ですが、このフィードは圧縮されています。

④ そして、キャッシュできます(すぐにわかるように、これは重要なことです)。

受け取るresponseには最終URLの情報があります。最終URLにリダイレクトする間のURL情報が欲しい場合、どうしたらよいでしょうか?これも、httplib2が教えてくれます。
 continued from the previous example
>>> 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)                                                        # ②

>>> type(response.previous)

>>> response.previous.previous                                            # ③
>>>
# ① response.previous属性はhttplib2が現在の応答オブジェクトまでに経由した、以前の応答オブジェクトへの参照を保持しています。

② response、response.previousはどちらもhttplib2.Responseオブジェクトです。

# ③ つまり、response.previous.previousを調べることで一連のリダイレクトのさらに前を見ることができます(シナリオ:1番目のURLは2番目のURLにリダリレクトし、2番目は3番目のURLにリダイレクトします。こういうことが起きているんです!)。この場合、リダイレクト連鎖の開始点に到達していますので、属性はNoneです。

同じURLを再度リクエストしたらどうなるでしょうか?
# continued from the previous example
>>> 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://diveintapython3.org/examples/feed.xml)に対して、2回目のリクエスト
は起こっていません。応答はキャッシュされていたのです(前の例で見たCache-Controiヘッダを覚えていますか)。httplib2は302Foundコードを受け取ると、別のリクエストを出す前にキャッシュを調べます。キャッシュには(http://diveintopython3.org/examples/feed.xml)のフレッシュなコピーが入っていますから、再度リクエストする必要はありません。

# ④ request()メソッドが返るまでに、フィードデータをキャッシュから読んで返します。もちろん、前回受け取ったデータと同じです。

言い換えると、テンポラリ リダイレクトに対しては何も特別なことをする必要がありません。httplib2が自動で追跡してくれるので、あるURLは別のURLにリダイレクトするという事実はhttplib2の圧縮、キャッシュ、ETag、他HTTPの機能とは関係がありません。

パーマネントリダイレクトはシンプルそのものです。
# continued from the previous example
>>> 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://diveintopython3org/examples/feed.xmlにしています。

# ② そしてこれです、ステータスコード301。ここでも、何が起こらなかったかに注意します。リダイレクトURLへのリクエストはありません。なぜでしょうか?ローカルにキャッシュされているからです。
③ httplib2はキャッシュの中でリダイレクトを"フォロー"します。

でも待ってください。まだあります!
# continued from the previous example
>>> response2, content2 = h.request('http://diveintopython3.org/examples/feed-301.xml')  # ①
>>> response2.fromcache                                                                  # ②
True
>>> content2 == content                                                                  # ③
True

# ① これがテンポラリ、パーマネントリダイレクトの違いです。httplib2がパーマネントリダイレクトをフォローしたら、それ以降のリクエストはすべて素通りになって元のURLでネットワークに接続することはなく、目的のURLに書き換えられます。覚えていますか、デバッグは今もオンですので、ここまでネットワーク活動に対して出力はありません。

② そう、この応答はローカルキャッシュから得られました。

③ そう、すべてのフィードを手に入れました(キャッシュから)。

HTTP。役に立ちます。

14.6.HTTP GETを超えて

httpWebServicesはGETリクエストに限定されるものではありません。新しいものを作りたいときはどうしたらよいでしょうか?フォーラムにコメントを投稿したいとき、ブログを更新したいとき、TwitterやIdenti.caのようなマイクロプログサービスで自分の状況を発信したいとき、すでにHTTP POSTを使っていることでしょう。

TwitterもIdenti.caのどちらもHTTPベースのシンプルなAPIを提供していて、140字以内であなたの今を投稿したり更新したりすることができます。Identi.caのAPIドキュメントを見て、あなたのステータスを更新してみましょう。
    Identi.ca REST API Method: statuses/update
    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://identLca/api/statuses/update.formatに出す必要があります。(formatの部分はURLの一部ではありません。リクエストでサーバから返して欲しいデータフォーマットに置き換えます。応答がxmlでほしいならば、リクエストの投稿はこのようになります。https://identi.ca/api/statuses/update.xmL)リクエストにはstatusというステータス更新のテキストパラメータが含まれている必要があります。また、リクエストは認証されていなければなりません。

認証?そうです。ステータスをIdenti.caで更新するには、自分が誰であるかを証明する必要があります。Identi.caはwikiではありませんので、あなただけが自分のステータスを更新できるのです。Identi.caでは安全で使いやすい証明、http Basic Authentication (別名RFC2617)をSSLで使っています。httpl ib2はSSLもhttp Basic Authentivationをどちらもサポートしているので、この部分は簡単です。

POSTリクエストはペイロードを含んでおり、GETリクエストとは異なります。ペイロードはサーバに送りたいデータのことです。このAPIメソッドが必須とするデータはstatus1つだけです。statusはURLエンコードされている必要があります。これは非常にシンプルなシリアル化で、キー値のペア(辞書)の1セットを受け取って文字列に変換します。
>>> from urllib.parse import urlencode              # ①
>>> data = {'status': 'Test update from Python 3'}  # ②
>>> urlencode(data)                                 # ③
'status=Test+update+from+Python+3'
# ① Pythonには辞書をURLエンコードするためのユーティリティ関数があります。urllib.parse.urlencode()です。

② これがIdenti.ca APIが望んでいたような辞書です。1つのキー、statusがあって、値は1つのstatusアップデートのテキストです。

③ URLエンコードされた文字列がこのようになります。この文字列がペイロードで、http POSTリクエスト内でIdenti.ca APlサーバに"回線を通じて"送信されるのです。
>>> 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'})      # ④
# ① httplib2はこのように認証を扱います。ユーザ名とパスワードをadd_credentials()メソッドを使って保存します。httplib2がリクエストを出そうとしたとき、サーバは401未承認ステータスコードで応答し、どの証明メソッドが使えるかのリスト出しています(RW-Authenticateヘッダ内)。httplib2は自動でAuthorizationヘッダを構築して、URLを再リクエストします。

② 2番目のパラメータはHTTPリクエスト型で、今回はPOSTです。

# ③ 3番目のパラメータはサーバに送られるぺイロードです。URLエンコードされた辞書をstatusメッセージと共に送ろうとしています。

# ④ 最後に、ペイロードはURLエンコードされたデータであることをサーバに伝える必要があります。

👉add_credentials()への3番目のパラメータは証明が有効なドメインです。これを常に指定しましょう!ドメインを外していて後で別の証明のサイトでhttplib2.Httpオブジェクトを再利用すると、httplib2はサイトのユーザ名やパスワードを別のサイトに漏洩させてしまうかもしれません。

これが通信で起こっていることです。
# continued from the previous example
send: b'POST /api/statuses/update.xml HTTP/1.1
Host: identi.ca
Accept-Encoding: identity
Content-Length: 32
content-type: application/x-www-form-urlencoded
user-agent: Python-httplib2/$Rev: 259 $

status=Test+update+from+Python+3'
reply: 'HTTP/1.1 401 Unauthorized'                        # ①
send: b'POST /api/statuses/update.xml HTTP/1.1            # ②
Host: identi.ca
Accept-Encoding: identity
Content-Length: 32
content-type: application/x-www-form-urlencoded
authorization: Basic SECRET_HASH_CONSTRUCTED_BY_HTTPLIB2  # ③
user-agent: Python-httplib2/$Rev: 259 $

status=Test+update+from+Python+3'
reply: 'HTTP/1.1 200 OK'                                  # ④
# ① 最初のリクエストのあと、サーバは401未認証ステータスコードで応答しています。httplib2はサーバが明確に要求してこなければ認証ヘッダを送りません。これがサーバが要求してくる方法です。

② httplib2はすぐに折り返して、同じURLに2回目のリクエストをします。

# ③ 今回は、add_credentials()を使って追加したユーザ名とパスワードが含まれています。

④ 動きました!

成功したリクエストのあと、サーバは何を送り返すのでしょうか?それは、WebServicesのAPlによって決まります。 あるプロトコル(例えばAtom Publishing Protocol)では、サーバは新しく作られたリソースの位置をLocationヘッダに入れて201 Createdステータスコードを送り返します。 Identi.caは200 OKを送って、新しく作られたリソースの情報の入ったXMLドキュメントを送ります。
# continued from the previous example
>>> print(content.decode('utf-8'))                             ①
<?xml version="1.0" encoding="UTF-8"?>
<status>
 <text>Test update from Python 3</text>                        ②
 <truncated>false</truncated>
 <created_at>Wed Jun 10 03:53:46 +0000 2009</created_at>
 <in_reply_to_status_id></in_reply_to_status_id>
 <source></source>api
 <id>5131472</id>                                              ③
 <in_reply_to_user_id></in_reply_to_user_id>
 <in_reply_to_screen_name></in_reply_to_screen_name>
 <favorited>false</favorited>
 <user>
  <id>3212</id>
  <name>Mark Pilgrim</name>
  <screen_name>diveintomark</screen_name>
  <location>27502, US</location>
  <description>tech writer, husband, father</description>
  <profile_image_url>http://avatar.identi.ca/3212-48-20081216000626.png</profile_image_url>
  <url>http://diveintomark.org/</url>
  <protected>false</protected>
  <followers_count>329</followers_count>
  <profile_background_color></profile_background_color>
  <profile_text_color></profile_text_color>
  <profile_link_color></profile_link_color>
  <profile_sidebar_fill_color></profile_sidebar_fill_color>
  <profile_sidebar_border_color></profile_sidebar_border_color>
  <friends_count>2</friends_count>
  <created_at>Wed Jul 02 22:03:58 +0000 2008</created_at>
  <favourites_count>30768</favourites_count>
  <utc_offset>0</utc_offset>
  <time_zone>UTC</time_zone>
  <profile_background_image_url></profile_background_image_url>
  <profile_background_tile>false</profile_background_tile>
  <statuses_count>122</statuses_count>
  <following>false</following>
  <notifications>false</notifications>
</user>
</status> 



# ① 覚えていますか?httplib2が返すデータは常にバイトで、文字列ではありません。これを文字列に変換するには、適切な文字列エンコードを使ってデコードする必要があります。Identi.caのAPIは常にutf-8で結果を返しますので、この部分は簡単です。

② 投稿したstatusメッセージのテキストです。

# ③ 新しいstatusメッセージのユニークな識別子です。Identi.caはこれを使ってwebにメッセージを見せるためのURLを作ります。

このようになります。

14.7. HTTP POSTを超えて

HTTPはGETやPOSTに限定されるものではありません。特にwebブラウザでは、一番多いリクエストであることは間違いありません。しかし、WebサービスAPIはGETやPOST以上のことができますし、httplib2にはそれらにも対応した機能が用意されています。
# continued from the previous example
>>> 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')                          # ④
① サーバはxmlを返しました。そうですね?xmlのパース方法はもう知っています。
# ② findtext()メソッドは、与えられた表現の最初のインスタンスを見つけてテキストを抽出します。この場合では、<id>要素を探しています。

# ③ <id>要素のテキスト内容に基づいてURLを作り、先ほど投稿したstatusメッセージを削除します。

④ メッセージを削除するためには、HTTP DELETEリクエストをその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
① "このstatusメッセージを削除して"

② "ごめんなさい、Dave。申し訳ないけどできません"

③ "認証されていない?むむむ。どうかこのstatusメッセージを削除してください..."

④ "これが私のユーザ名とパスワードです"

⑤ "完了です!"

こんな風に、パッと消えていきました。

14.8. もっと読むには

httplib2:

    httplib2 project page
    More httplib2 code examples
    Doing HTTP Caching Right: Introducing httplib2
    httplib2: HTTP Persistence and Authentication

HTTP caching:

    HTTP Caching Tutorial by Mark Nottingham
    How to control caching with HTTP headers on Google Doctype

RFCs:

    RFC 2616: HTTP
    RFC 2617: HTTP Basic Authentication
    RFC 1951: deflate compression
    RFC 1952: gzip compression

0 件のコメント:

コメントを投稿