Skip to content

Instantly share code, notes, and snippets.

@Cartman0
Last active April 7, 2016 14:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Cartman0/1c885ad8ce22c021b0350f53ee3a1126 to your computer and use it in GitHub Desktop.
Save Cartman0/1c885ad8ce22c021b0350f53ee3a1126 to your computer and use it in GitHub Desktop.
Dive Into Python 3 14章メモ(HTTPウェブサービス)
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"toc": "true"
},
"source": [
"# Table of Contents\n",
" <p><div class=\"lev1\"><a href=\"#14章-HTTPウェブサービス-1\"><span class=\"toc-item-num\">1&nbsp;&nbsp;</span>14章 HTTPウェブサービス</a></div><div class=\"lev2\"><a href=\"#飛び込む-1.1\"><span class=\"toc-item-num\">1.1&nbsp;&nbsp;</span>飛び込む</a></div><div class=\"lev2\"><a href=\"#HTTPの諸機能-1.2\"><span class=\"toc-item-num\">1.2&nbsp;&nbsp;</span>HTTPの諸機能</a></div><div class=\"lev3\"><a href=\"#キャッシュ-1.2.1\"><span class=\"toc-item-num\">1.2.1&nbsp;&nbsp;</span>キャッシュ</a></div><div class=\"lev3\"><a href=\"#Last-Modifiedチェック-1.2.2\"><span class=\"toc-item-num\">1.2.2&nbsp;&nbsp;</span>Last-Modifiedチェック</a></div><div class=\"lev3\"><a href=\"#ETagチェック-1.2.3\"><span class=\"toc-item-num\">1.2.3&nbsp;&nbsp;</span>ETagチェック</a></div><div class=\"lev3\"><a href=\"#圧縮-1.2.4\"><span class=\"toc-item-num\">1.2.4&nbsp;&nbsp;</span>圧縮</a></div><div class=\"lev3\"><a href=\"#リダイレクト-1.2.5\"><span class=\"toc-item-num\">1.2.5&nbsp;&nbsp;</span>リダイレクト</a></div><div class=\"lev2\"><a href=\"#HTTPを使ってデータを取得するまずいやり方-1.3\"><span class=\"toc-item-num\">1.3&nbsp;&nbsp;</span>HTTPを使ってデータを取得するまずいやり方</a></div><div class=\"lev2\"><a href=\"#何が回線を通っているのか?-1.4\"><span class=\"toc-item-num\">1.4&nbsp;&nbsp;</span>何が回線を通っているのか?</a></div><div class=\"lev2\"><a href=\"#httplib2の紹介-1.5\"><span class=\"toc-item-num\">1.5&nbsp;&nbsp;</span>httplib2の紹介</a></div><div class=\"lev3\"><a href=\"#補足:-httplib2はなぜ文字列の代わりにバイト列を返すのか?-1.5.1\"><span class=\"toc-item-num\">1.5.1&nbsp;&nbsp;</span>補足: httplib2はなぜ文字列の代わりにバイト列を返すのか?</a></div><div class=\"lev3\"><a href=\"#httplib2はキャッシュをどのように扱うのか-1.5.2\"><span class=\"toc-item-num\">1.5.2&nbsp;&nbsp;</span>httplib2はキャッシュをどのように扱うのか</a></div><div class=\"lev3\"><a href=\"#httplib2はどのようにLast-ModifiedヘッダやETagヘッダを扱うのか-1.5.3\"><span class=\"toc-item-num\">1.5.3&nbsp;&nbsp;</span>httplib2はどのようにLast-ModifiedヘッダやETagヘッダを扱うのか</a></div><div class=\"lev3\"><a href=\"#http2libはどのように圧縮を扱うのか-1.5.4\"><span class=\"toc-item-num\">1.5.4&nbsp;&nbsp;</span>http2libはどのように圧縮を扱うのか</a></div><div class=\"lev3\"><a href=\"#httplib2はどのようにリダイレクトを扱うのか-1.5.5\"><span class=\"toc-item-num\">1.5.5&nbsp;&nbsp;</span>httplib2はどのようにリダイレクトを扱うのか</a></div><div class=\"lev3\"><a href=\"#Github-apiでGet-してみる-1.5.6\"><span class=\"toc-item-num\">1.5.6&nbsp;&nbsp;</span>Github apiでGet してみる</a></div><div class=\"lev2\"><a href=\"#HTTP-GETの先へ-1.6\"><span class=\"toc-item-num\">1.6&nbsp;&nbsp;</span>HTTP GETの先へ</a></div><div class=\"lev3\"><a href=\"#Github(Gist)-API-1.6.1\"><span class=\"toc-item-num\">1.6.1&nbsp;&nbsp;</span>Github(Gist) API</a></div><div class=\"lev2\"><a href=\"#HTTP-POSTの先へ-1.7\"><span class=\"toc-item-num\">1.7&nbsp;&nbsp;</span>HTTP POSTの先へ</a></div><div class=\"lev3\"><a href=\"#Gist-api-でGist-を削除してみる-1.7.1\"><span class=\"toc-item-num\">1.7.1&nbsp;&nbsp;</span>Gist api でGist を削除してみる</a></div><div class=\"lev2\"><a href=\"#参考リンク-1.8\"><span class=\"toc-item-num\">1.8&nbsp;&nbsp;</span>参考リンク</a></div>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- [Dive Into Python3 1章メモ](http://nbviewer.jupyter.org/urls/gist.githubusercontent.com/Cartman0/d54093a99b9254c81bf1123adacbc48a/raw/eedc90bbbfc14e259854f2e739fffeec4cb4d8f7/DiveIntoPython3_01.ipynb)\n",
"- [Dive Into Python3 2章メモ(ネイティブデータ型)](http://nbviewer.jupyter.org/urls/gist.githubusercontent.com/Cartman0/988b51d8482ad9ade835bb07efdffb38/raw/784cf276b7cebe254e59f09dcea6f09eea760d38/DiveIntoPython3_02.ipynb)\n",
"- [Dive Into Python3 3章メモ(内包表記)](http://nbviewer.jupyter.org/urls/gist.githubusercontent.com/Cartman0/183ec6f6c835f621106f7c27d215290a/raw/bd7677946d6400bbe6acd257df7fb9c5976c3320/DiveIntoPython3_03.ipynb)\n",
"- [Dive Into Python3 4章メモ(文字列)](http://nbviewer.jupyter.org/urls/gist.githubusercontent.com/Cartman0/bb974f82e8a3fc74ac82c3c2a5b1a4f9/raw/63a80011c9391b451108c1a4dc804ec5ff125f34/DiveIntoPython3_04.ipynb)\n",
"- [Dive Into Python3 5章メモ(正規表現)](http://nbviewer.jupyter.org/urls/gist.githubusercontent.com/Cartman0/b834807a0dabb1c458b87be2f333a5ca/raw/37d6f25f67e017a569e1f0b60a9fcbe49d50515f/DiveIntoPython3_05.ipynb)\n",
"- [Dive Into Python3 6章メモ(クロージャとジェネレータ)](http://nbviewer.jupyter.org/urls/gist.githubusercontent.com/Cartman0/a8998f8f88c5578271495ada56cc4809/raw/4e9b8ef3543016a710f905215693694acd703549/DiveIntoPython3_06.ipynb?flush_cache=true)\n",
"- [Dive Into Python3 7章メモ(クラスとイテレータ)](http://nbviewer.jupyter.org/urls/gist.githubusercontent.com/Cartman0/8eb511c3b2aa6cadad6f6d49666c9db2/raw/60863d1ce1c1e9e4cb336ad79a0e252807beb87c/DiveIntoPython3_07.ipynb?flush_cache=true)\n",
"- [Dive Into Python3 8章メモ(高度なイテレータ)](http://nbviewer.jupyter.org/urls/gist.githubusercontent.com/Cartman0/9ed3037cbdac59af53c20c125aed806e/raw/46ee3c71d72ab7ee665cf0c10f8717734632b462/DiveIntoPython3_08.ipynb)\n",
"- [Dive Into Python3 9章メモ(ユニットテスト)](http://nbviewer.jupyter.org/urls/gist.githubusercontent.com/Cartman0/1833320ab55ae5ea660ce0c635483d26/raw/46034833206366c8011594cbea2efce90168d54b/DiveIntoPython3_09.ipynb)\n",
"- [Dive Into Python3 10章メモ(リファクタリング)](http://nbviewer.jupyter.org/gist/Cartman0/0c0af20eb95236da4da9547258fe2bb3)\n",
"- [Dive Into Python3 11章メモ(ファイル)](http://nbviewer.jupyter.org/gist/Cartman0/152eacd23b793ecd3dbf5f706ae33e92)\n",
"- [Dive Into Python3 12章メモ(XML)](http://nbviewer.jupyter.org/gist/Cartman0/87d2d00c5d64c66778f20d1d056e76c7)\n",
"- [Dive Into Python3 13章メモ(Pythonオブジェクトをシリアライズ)](http://nbviewer.jupyter.org/gist/Cartman0/33cd21bbc84ae3c07ea84884f8354ab3)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 14章 HTTPウェブサービス"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 飛び込む"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**HTTP の命令だけを用いて遠隔にあるサーバーとデータをやりとりする**\n",
"\n",
"— HTTPウェブサービスの概念はこの33文字に集約される。\n",
"- サーバーからデータを取得したければ、\n",
"`http GET` を使えばいい。\n",
"- サーバーにデータを送信したければ`http POST` を使えばいい。\n",
"\n",
"もっと高度なhttpウェブサービスのapiもあって、\n",
"これらは \n",
"- `http PUT` や\n",
"- `http DELETE` を利用して、\n",
"データの作成・修正・削除を行えるようにしてくれる。\n",
"\n",
"httpの命令だけで全て処理できる。\n",
"\n",
"ここにはレジストリも、エンベロープも、ラッパも、トンネリングも必要ない。\n",
"要するに、httpプロトコル内に構築されているこれらの「動詞」(GET, POST, PUT, DELETE) は、\n",
"データの取得・作成・修正・消去を行うアプリケーションレベルの命令に直接対応するもの。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"このやり方の主たる利点は、\n",
"その単純さにあり、またこの単純さゆえに広く使われている。\n",
"\n",
"データ — 普通はxmlかjson — は静的に構築して保存することもできるし、\n",
"サーバーサイドスクリプトを使って動的に生成することもできる。\n",
"そして、メジャーなプログラミング言語はすべて(Pythonも)\n",
"このデータをダウンロードするためのhttpライブラリを備えている。\n",
"\n",
"さらに、この方式だとデバッグも容易。\n",
"というのも、httpウェブサービスの個々のリソースにはユニークなアドレスが(urlの形式で)割り振られているため、\n",
"ウェブブラウザにロードすればすぐに生のデータを見ることができる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpウェブサービスの例:\n",
"\n",
"- Google Data apiを使えば、Blogger やYouTube といった様々なGoogle のサービスとやりとりすることができる。\n",
"- Flickr Servicesを使えばFlickrに写真をアップロードしたり、ダウンロードしたりすることができる。\n",
"- Twitter apiを使えば、Twitterに投稿することができる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Python 3にはhttpウェブサービスと情報をやりとりするためのライブラリが二つ用意されている。\n",
"\n",
"- `http.client` は、httpプロトコルのrfc 2616を実装した低級ライブラリ。\n",
"- `urllib.request` は、`http.client` 上に構築された抽象化レイヤ。\n",
"これはhttpサーバーとftpサーバーの両方にアクセスするための標準apiを提供してくれるもので、httpリダイレクトを自動でたどることもできれば、いくつかの一般的なhttp認証方式も扱える。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"代わりに、「`httplib2`」 がお勧め。\n",
"これはオープンソースなサードパーティ製ライブラリで、http.clientよりも完全にhttpを実装しているのだが、しかも `urllib.request` よりも優れた抽象化を施しているという代物。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## HTTPの諸機能"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpクライエントならば必ず備えるべき5つの機能がある。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### キャッシュ"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"ウェブサービスの種類にかかわらず知っておくべき最も重要なことは、ネットワークアクセスはとても高くつくということ。\n",
"\n",
"接続を開き、リクエストを送り、そして遠隔サーバーからレスポンスを取得するまでには非常に長い時間がかかる。\n",
"**レイテンシ(リクエストの送信後、その応答としてデータが受信され始めるまでの時間)** は、\n",
"想像よりも大きくなる。\n",
"ルーターは処理を誤り、パケットは抜け落ち、さらに中継のプロキシはアタックを受ける"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpはキャッシュのことを念頭に置いて設計されている。\n",
"実際に、ネットワークアクセスを最小限にするという仕事のみを行うデバイス(「キャッシュプロキシ」と呼ばれている)なんてものもある。\n",
"\n",
"ispはほぼ間違いなくこのキャッシュプロキシを運用している。\n",
"これができるのも、キャッシュがhttpプロトコルに組み込まれているため。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**キャッシュ** がどのように機能するかの具体的な例:\n",
"\n",
"仮にブラウザで `diveintomark.org` にアクセスしたとする。\n",
"このページの背景には `wearehugh.com/m.jpg` という画像が置かれている。\n",
"ここでブラウザがこの画像をダウンロードをすると、サーバーは次のようなhttpヘッダを付けてくる:\n",
"\n",
"```\n",
"HTTP/1.1 200 OK\n",
"Date: Sun, 31 May 2009 17:14:04 GMT\n",
"Server: Apache\n",
"Last-Modified: Fri, 22 Aug 2008 04:28:16 GMT\n",
"ETag: \"3075-ddc8d800\"\n",
"Accept-Ranges: bytes\n",
"Content-Length: 12405\n",
"Cache-Control: max-age=31536000, public\n",
"Expires: Mon, 31 May 2010 17:14:04 GMT\n",
"Connection: close\n",
"Content-Type: image/jpeg\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"この `Cache-Control` と `Expires` のヘッダは、\n",
"ブラウザ(それに加えて、あなたとサーバーとの間にあるキャッシュプロキシすべて)に対して「一年間はこの画像をキャッシュしてもいいよ」ということを伝えるもの。\n",
"\n",
"だからもし、次の年にこの画像へのリンクを含む別のページにアクセスしたとすると、ブラウザはキャッシュにある画像を読み込むので、ネットワークを介したやりとりは全く行われない。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"ブラウザが何かの理由でこの画像をローカルキャッシュから消してしまったとしよう。\n",
"その原因はディスクスペースが尽きたということかもしれないし、\n",
"あるいはあなたが自分でキャッシュを削除したのかもしれない。\n",
"\n",
"その原因が何であれ、httpヘッダは「この画像のデータはパブリックなキャッシュプロキシで保存してもかまわないよ」と述べている(厳密に言えば、ここで重要なのはこのヘッダが述べていないことだ。つまり、Cache-Controlヘッダの中にprivateというキーワードが含まれていないので、このデータはデフォルトでキャッシュできるようになっている)。\n",
"キャッシュプロキシは膨大な記憶容量を持つように設計されている。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"もし、ispがキャッシュプロキシを運営していたら、\n",
"そのプロキシにはまだこの画像がキャッシュされているかもしれない。\n",
"ここで再び `diveintomark.org` にアクセスしたとする。\n",
"\n",
"すると、\n",
"1. まずブラウザはこの画像を探してローカルキャッシュを漁る。\n",
"2. しかし、見つけることができないので、今度は遠隔サーバーからダウンロードしようと、ネットワークを通してリクエストを送信するだろう。\n",
"3. そこで、もしキャッシュプロキシの方にまだ画像のコピーがあれば、リクエストはそこで止められて、プロキシのキャッシュから画像が返される。\n",
"\n",
"つまり、リクエストが遠隔サーバーに到達することはない。\n",
"現に、このリクエストはあなたの会社のネットワークを離れてさえいない。\n",
"この仕組みのおかげで、高速なダウンロード(より少ないホップ数での通信)が可能になり、\n",
"会社側のコストも節約(外部からダウンロードされるデータをより少なく)できる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpキャッシュは,\n",
"つまり、\n",
"- 一方ではサーバーが正しいヘッダを返信しなくてはならないし、\n",
"- もう一方ではクライアントがそのヘッダを理解した上で、同じデータを二度リクエストする前にヘッダに従った処理を行わなければならない。\n",
"\n",
"その中間に置かれるプロキシは、サーバーとクライアントが上手く処理してくれる限りにおいて機能できるだけ。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Pythonの httpライブラリはキャッシュをサポートしていないが、\n",
"`httplib2` はサポートしている。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Last-Modifiedチェック"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"ひっきりなしに変更されるデータがある一方で、決して変わらないデータもある。\n",
"その中間には、更新された可能性があったのだが、実際には何も変更されていなかったという類のデータが大量に存在している。\n",
"\n",
"`CNN.com` のフィードは数分おきに更新されるが、\n",
"数日か数週間は更新されないブログのフィードもある。\n",
"\n",
"後者の場合、クライアントに何週間もフィードをキャッシュしてもらいたいとは思わないだろう。そうすると、実際に何かをブログに投稿しても、\n",
"読者が数週間その記事を目にしないということになりかねない(これも読者の皆さんが「数週間はこのフィードをチェックしないで」としている私のヘッダに従ってくれるおかげ)。\n",
"その一方で、一時間ごとにフィード全体をダウンロードして更新をチェックされても困る"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`http` にはこれを解決する方法も用意されている。\n",
"最初にデータをリクエストされたときに、\n",
"サーバーは`Last-Modified`ヘッダを付けて返信することができる。\n",
"これはその名前のとおり、データが更新された日時を表すもの。\n",
"`diveintomark.org` が参照している背景画像にもLast-Modifiedヘッダが付いている。\n",
"\n",
"```\n",
"HTTP/1.1 200 OK\n",
"Date: Sun, 31 May 2009 17:14:04 GMT\n",
"Server: Apache\n",
"Last-Modified: Fri, 22 Aug 2008 04:28:16 GMT\n",
"ETag: \"3075-ddc8d800\"\n",
"Accept-Ranges: bytes\n",
"Content-Length: 12405\n",
"Cache-Control: max-age=31536000, public\n",
"Expires: Mon, 31 May 2010 17:14:04 GMT\n",
"Connection: close\n",
"Content-Type: image/jpeg\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"同じデータを二度目にリクエストする時、\n",
"前回サーバーから返された日時を入れた `If-Modified-Since`ヘッダをリクエストに付けて送ることができる。\n",
"\n",
"- もし、データがその日時以降に変更されていれば、\n",
"サーバーは `If-Modified-Since`ヘッダを無視し、\n",
"ステータスコード200と一緒に新しいデータを送り返してくれる。\n",
"\n",
"- しかし、データがその日時以降に何も変更されていなければ、サーバーは\n",
"`http 304` という特別なステータスコードを返す。\n",
"これは「このデータは前回リクエストされた時から何も変更されていないよ」ということを表すもの。\n",
"ちなみに、curlを使えばこれをコマンドライン上でテストすることもできる。\n",
"\n",
"```\n",
"curl -I -H \"If-Modified-Since: Fri, 22 Aug 2008 04:28:16 GMT\" http://wearehugh.com/m.jpg\n",
"HTTP/1.1 304 Not Modified\n",
"Date: Sun, 31 May 2009 18:04:39 GMT\n",
"Server: Apache\n",
"Connection: close\n",
"ETag: \"3075-ddc8d800\"\n",
"Expires: Mon, 31 May 2010 18:04:39 GMT\n",
"Cache-Control: max-age=31536000, public\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"サーバーが304を返す場合には、\n",
"データ自体は再び送られてこない。\n",
"返送されるのはステータスコードだけになる。\n",
"\n",
"キャッシュされたコピーの有効期限が切れていた場合でも、\n",
"`last-modified`チェックを使えば、データが変更されていない限り、\n",
"同じデータを再びダウンロードしなくても済むようになる(さらなるおまけとして、304が返される時もキャッシュに関するヘッダは送られてくる。データが本当に変更されておらず、さらに次のリクエストで304ステータスコードと最新のキャッシュ情報が返されるかもしれないので、正式には「有効期限切れ」とされているデータのコピーもプロキシは保存し続けるもの)。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Pythonのhttpライブラリは、`last-modified`チェックをサポートしていないが、\n",
"`httplib2` はサポートしている。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### ETagチェック"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`ETag`とは、\n",
"`last-modified`チェックと同じ役割を果たすもの。\n",
"\n",
"Etagsを使った場合には、サーバーはリクエストされたデータに加えて、\n",
"ハッシュを納めたETagヘッダを返してくる(このハッシュをどのように生成するかについては完全にサーバーに委ねられている。データが変更されたときにその値が変わりさえすればなんでもいい)。\n",
"`diveintomark.org` から参照されている背景画像にも`ETag`ヘッダが含まれている。\n",
"\n",
"```\n",
"HTTP/1.1 200 OK\n",
"Date: Sun, 31 May 2009 17:14:04 GMT\n",
"Server: Apache\n",
"Last-Modified: Fri, 22 Aug 2008 04:28:16 GMT\n",
"ETag: \"3075-ddc8d800\"\n",
"Accept-Ranges: bytes\n",
"Content-Length: 12405\n",
"Cache-Control: max-age=31536000, public\n",
"Expires: Mon, 31 May 2010 17:14:04 GMT\n",
"Connection: close\n",
"Content-Type: image/jpeg\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"同じデータを2度目にリクエストする時には、\n",
"Etagのハッシュを入れた `If-None-Match`ヘッダを付けて送る。\n",
"データが変わっていなければ、サーバーは `304`ステータスコードを送り返してくれる。\n",
"この場合、`last-modified`チェックの時と同じく、\n",
"サーバーは304ステータスコードだけを返すので、\n",
"同じデータが2度送信されることはない。\n",
"\n",
"つまり、Etagのハッシュを2度目のリクエストの際に一緒に送ることで\n",
"「最後にリクエストした時のデータがまだ残っているから、まだハッシュが一致するようなら同じデータを再送信する必要は無いよ」とサーバーに伝えていることになる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"curlを使うと:\n",
"\n",
"```\n",
"curl -I -H \"If-None-Match: \\\"3075-ddc8d800\\\"\" http://wearehugh.com/m.jpg ①\n",
"HTTP/1.1 304 Not Modified\n",
"Date: Sun, 31 May 2009 18:04:39 GMT\n",
"Server: Apache\n",
"Connection: close\n",
"ETag: \"3075-ddc8d800\"\n",
"Expires: Mon, 31 May 2010 18:04:39 GMT\n",
"Cache-Control: max-age=31536000, public\n",
"```\n",
"\n",
"ETagは一般に引用符で括られているのだが、実はこの引用符も値の一部。\n",
"だから、`If-None-Match`ヘッダをサーバーに送り返す時には引用符を付けて返さなくてはならない。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"PythonのhttpライブラリはEtagをサポートしていないが、`httplib2` はサポートしている。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 圧縮"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpウェブサービスということになると、ほとんどの場合、回線を通じてテキストベースのデータを行き来させるという話になる。\n",
"そのデータはxmlかもしれないし、jsonかもしれない。\n",
"あるいは単なるプレーンテキストかもしれない。形式が何であれ、\n",
"テキストというのは圧縮効率が良いもの。\n",
"XMLの章で例に出したフィードは圧縮していない状態だと3070バイトなのだが、gzipで圧縮すると941バイトになる。元のサイズの30%になる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpはいくつかの圧縮アルゴリズムをサポートしているが、\n",
"最も一般に用いられている形式は `gzip`と `deflate` の2つ。\n",
"httpを使ってリソースをリクエストする時には、圧縮形式で送るようにサーバーに頼むことができる。\n",
"\n",
"サポートしている圧縮アルゴリズムのリストが入った `Accept-encoding`ヘッダをリクエストに付け加えればいい。\n",
"サーバーがそのアルゴリズムのいずれかをサポートしていれば、\n",
"圧縮したデータを返してくれる(この場合、どのアルゴリズムが使われたかを示す\n",
"`Content-encoding`ヘッダもついてくる)。\n",
"後は、送られてきたデータを展開すればいい。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**圧縮したデータと未圧縮のデータが異なるEtagを持つようにすること**。\n",
"でないと、キャッシュプロキシが混乱して、\n",
"クライアントが扱えないのに圧縮された形式で返してしまいかねない。\n",
"この微妙な問題の詳細については、Apacheバグ 39727に関する議論に書いてある。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Pythonのhttpライブラリは圧縮をサポートしていないが、\n",
"`httplib2` はサポートしている。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### リダイレクト"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"ウェブサイトは再構成されて、\n",
"ページは新しいアドレスに移されてしまう。\n",
"ウェブサービスですら再編されることがある。\n",
"\n",
"例えば、`http://example.com/index.xml` で配信されていたフィードは、`http://example.com/xml/atom.xml` に移されてしまうかもしれない。\n",
"あるいは、 組織の拡大や再編の一環として、ドメイン自体が変えられることだってある。\n",
"\n",
"例えば、`http://www.example.com/index.xml` は `http://server-farm-1.example.com/index.xml` に移転するかもしれない。 "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"リソースをhttpサーバーにリクエストした場合にはいつも、\n",
"サーバーはステータスコードも送り返してくる。\n",
"\n",
"- ステータスコード200が意味するのは「万事異常無し。これがリクエストされたページだ」ということ。\n",
"- ステータスコード404は「ページが見つかりません」ということを意味する(たぶんウェブブラウジングをしていて404エラーに出くわしたことがあるだろう)。\n",
"- 300台のステータスコードは何らかの形のリダイレクトを表している。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpにはリソースが移転したことを知らせる方法がいくつか用意されている。\n",
"中でも最もよく使われているのはステータスコードの `302` と `301` を使う方法だ。\n",
"\n",
"「ステータスコード302」は、一時的なリダイレクトを表す。\n",
"つまり、「おっと、それは一時的にあっちに移転されてるよ」ということ(その上で、一時的なアドレスをLocationヘッダに入れて渡してくれる)。\n",
"\n",
"一方で、「ステータスコード301」は、**恒久的なリダイレクト** を表す。つまり、「おっと。それはあっちに完全に移転されてるよ」ということだ(その上で、新しいアドレスをLocationに入れて渡してくれる)。\n",
"\n",
"ステータスコード302と一緒に新しいアドレスを受け取った場合について、httpの仕様は「リクエストしたリソースを取得するには新しいアドレスを使えばいいが、\n",
"次に同じリソースにアクセスする時には古いアドレスを試すべし」としている。\n",
"ステータスコード301と一緒に新しいアドレスを受け取った場合には、以後その新しいアドレスを使っていけばいい。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`urllib.request`モジュールはhttpサーバーから適切なステータスコードを受け取った場合に自動でそのリダイレクトをたどってくれるのだが、\n",
"そのように処理したとは何も言ってくれない。\n",
"\n",
"要するに、最終的にリクエストしたデータは取得できるにしても、その処理を支えるライブラリが「ご親切にも」リダイレクトをたどってくれたとは分からない。\n",
"だから、あなたは古いアドレスに何度も何度もアクセスし続けることになり、その度に新しいアドレスにリダイレクトされて、しかも毎回`urllib.request` が「ご丁寧に」リダイレクトをたどってくれる。\n",
"言い換えれば、これは恒久的なリダイレクトを一時的なリダイレクトと同じように扱っているわけだ。\n",
"こうすると一回で済むところを二回往復することになるので、\n",
"これはサーバーにとってもあなたにとっても良くない。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`httplib2` は恒久的なリダイレクトを処理してくれる。\n",
"恒久的なリダイレクトが生じたことを教えてくれるのみならず、\n",
"そのリダイレクトをローカルに保存し、\n",
"リダイレクトされたurlをリクエストの前に自動で書き直してくれる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## HTTPを使ってデータを取得するまずいやり方"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpを使ってAtomフィードなどのリソースをダウンロードしたいと考えたとする。\n",
"フィードということなので、一回ダウンロードするだけでは済まず、\n",
"何回も何回もダウンロードすることになる\n",
"(ほとんどのフィードリーダーは一時間に一回、更新をチェックする)。\n",
"\n",
"まずは、こいつを手早く雑に実装してみて、それからどうやったら改善できるかを考える。"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"<module 'urllib.request' from 'C:\\\\Miniconda3\\\\lib\\\\urllib\\\\request.py'>"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import urllib.request\n",
"urllib.request"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"a_url = 'http://cartman0.hatenablog.com/feed'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"どんなものであれhttpを使ってダウンロードするのは、Pythonでは簡単。\n",
"実際に、たった一行でできてしまう。\n",
"\n",
"`urllib.request`モジュールには便利な `urlopen()` という関数が用意されていて、\n",
"これはダウンロードしたいページのアドレスを引数にとり、\n",
"ファイルに似たオブジェクトを返すもの。\n",
"そして、このオブジェクトを `read()`するだけでページの内容を全て取得することができる。\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"data = urllib.request.urlopen(a_url).read()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`urlopen().read()`メソッドは常に文字列ではなく、\n",
"`bytes`オブジェクトを返す。\n",
"バイト列はバイト列であって、文字列はそれを抽象化したものだった。\n",
"\n",
"`http`は抽象化されたものを扱わないので、リソースをリクエストした時には、\n",
"バイト列の形で受け取ることになる。\n",
"それを文字列として扱いたいなら、文字コードを定めて明示的に文字列に変換しなくてはならない。"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": false,
"scrolled": false
},
"outputs": [
{
"data": {
"text/plain": [
"bytes"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"type(data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"> print(data)\n",
"\n",
"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\n",
"....\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" テストや開発の際に一回だけ使うお手軽なコードとしては、これで何も悪くない。\n",
" フィードの中身を取得しようとしていて、それでフィードの中身が手に入っているわけだし、\n",
" この方法でどのウェブページも取得できる。\n",
" しかし、定期的にアクセスされるようなウェブサービス\n",
" (e.g.このフィードを一時間に一回リクエストする場合)の観点からすると、\n",
" これは効率が悪いというだけではなく、無礼な方法でもある。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 何が回線を通っているのか?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"これがなぜ非効率で無礼なのかを理解するために、Pythonのhttpライブラリのデバッグ機能をオンにして何が「回線を通じて」(i.e. ネットワークを介して)送られているのかを見てみる。"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"http.client.HTTPConnection"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from http.client import HTTPConnection\n",
"HTTPConnection"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`urllib.request` は、`http.client` という他のPythonの標準ライブラリに依存している。\n",
"本来ならhttp.clientに直接触れる必要は無いのだが(urllib.requestモジュールが自動でインポートしてくれる)、\n",
"ここでは`urllib.request` がhttpサーバーに接続する際に使っている`HTTPConnection`クラスのデバッグフラグをオンに切り替えるためにインポートしている。"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"HTTPConnection.debuglevel = 1"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"from urllib.request import urlopen"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"デバッグフラグが立っているので、httpのリクエストとレスポンスに関する情報がリアルタイムで出力される。\n",
"ご覧のとおり、このAtomフィードをリクエストする際に、\n",
"`urllib.request`モジュールは\n",
"5行のコードを送っている。"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"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'\n",
"reply: 'HTTP/1.1 200 OK\\r\\n'\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 "
]
}
],
"source": [
"response = urlopen('https://raw.githubusercontent.com/chungy/Dive-Into-Python-3/master/examples/feed.xml')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"最初の行(`send:`):\n",
"\n",
"```\n",
"send: b'GET /chungy/Dive-Into-Python-3/master/examples/feed.xml HTTP/1.1\n",
"```\n",
"\n",
"使っているhttpの動詞と、リソースのパス(からドメイン名を引いたもの)を明示している。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"2行目(Host):\n",
"```\n",
"Host: raw.githubusercontent.com\n",
"```\n",
"\n",
"リクエストしているフィードのあるドメイン名を示している。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"3行目(`Accept-Encoding:`):\n",
"```\n",
"Accept-Encoding: identity\n",
"```\n",
"\n",
"クライアントがサポートしている圧縮アルゴリズムを指定している。\n",
"標準では `urllib.request` は圧縮をサポートしていない。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"4行目(`User-Agent:`):\n",
"\n",
"```\n",
"User-Agent: Python-urllib/3.5\n",
"```\n",
"\n",
"リクエストを行っているライブラリの名前を示している。\n",
"標準では`Python-urllib` とバージョン番号が記される。\n",
"`urllib.request` と `httplib2` ではこのユーザーエージェントを変更することができ、\n",
"そのためには単に`User-Agent`ヘッダをリクエストに加えるだけでいい(こうするとデフォルトの値が置き換えられる)。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"サーバーが何を送り返してきたのかを見てみる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`urllib.request.urlopen()` 関数から返された `response` にはサーバーが返したhttpヘッダが全て入っている。\n",
"加えて、このオブジェクトには実際のデータをダウンロードするためのメソッドも入っている。"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'\n",
"X-XSS-Protection: 1; mode=block\n",
"X-Frame-Options: deny\n",
"X-Content-Type-Options: nosniff\n",
"Strict-Transport-Security: max-age=31536000\n",
"ETag: \"bcdda81dde068c9e23eb55a81b23ed86278fdb59\"\n",
"Content-Type: text/plain; charset=utf-8\n",
"Cache-Control: max-age=300\n",
"X-GitHub-Request-Id: 2BF9481F:5B27:4344A02:57066E88\n",
"Content-Length: 3070\n",
"Accept-Ranges: bytes\n",
"Date: Thu, 07 Apr 2016 14:28:25 GMT\n",
"Via: 1.1 varnish\n",
"Connection: close\n",
"X-Served-By: cache-nrt6127-NRT\n",
"X-Cache: MISS\n",
"X-Cache-Hits: 0\n",
"Vary: Authorization,Accept-Encoding\n",
"Access-Control-Allow-Origin: *\n",
"X-Fastly-Request-ID: 4c0366f07b2d933e51b417fbf90838536521137e\n",
"Expires: Thu, 07 Apr 2016 14:33:25 GMT\n",
"Source-Age: 0\n",
"\n",
"\n"
]
}
],
"source": [
"print(response.headers.as_string()) "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"サーバーはいつリクエストを処理したのかを教えてくれる。\n",
"\n",
"```\n",
"Date: Wed, 06 Apr 2016 16:07:03 GMT\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"feedによっては、レスポンスに以下のように、`Last-Modified`ヘッダが含まれる場合もある。\n",
"\n",
"```\n",
"Last-Modified: Sun, 31 May 2009 06:39:55 GMT\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"さらには `ETag`ヘッダもこのレスポンスに入っている。\n",
"\n",
"```\n",
"ETag: \"bcdda81dde068c9e23eb55a81b23ed86278fdb59\"\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"このデータは3070バイト。\n",
"ここに何が欠けているかに注意してほしい。\n",
"つまり、これには `Content-encoding`ヘッダが抜けている。\n",
"リクエストでは圧縮していないデータしか受け取れないと明示したので(`Accept-encoding: identity`)、当然のことながら、このレスポンスには圧縮されていないデータが入っている。\n",
"\n",
"```\n",
"Content-Length: 3070\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"このレスポンスにはキャッシュヘッダが含まれていて、これは「300秒までならキャッシュしてもいいよ」と述べている。\n",
"\n",
"```\n",
"Cache-Control: max-age=300```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"最後に、`response.read()` を呼び出すことで実際のデータをダウンロードしている。\n",
"`len()`関数の戻り値を見れば分かるように、ここでは一度に3070バイトをダウンロードしている。"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"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 &amp;mdash; my longest chapter so far\\r\\n would be 75 printed pages, and it loads in under 5 seconds&amp;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\""
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"data = response.read()\n",
"data"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"collapsed": false,
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"3070"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"この時点で既にこのコードは非効率的。\n",
"このコードは圧縮されていないデータをリクエストしている(そしてその通り受け取っている)のだ。\n",
"httpの圧縮機能を利用するにはgzip のようなものを指定しておかなければならない。\n",
"今回はそう指定しなかったので、データは圧縮されなかった。\n",
"だから、941バイトで済むところを、3070バイトもダウンロードすることになってしまった。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"このコードがどれだけ非効率かを見るために、\n",
"もう一度同じフィードをリクエストしてみる。"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"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'\n",
"reply: 'HTTP/1.1 200 OK\\r\\n'\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 "
]
}
],
"source": [
"response2 = urlopen('https://raw.githubusercontent.com/chungy/Dive-Into-Python-3/master/examples/feed.xml')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"このリクエストは最初のものと何も変わっていない。\n",
"内容は最初のリクエストと全く同じで、`If-Modified-Since`ヘッダもなければ、\n",
"`If-None-Match`ヘッダもない。\n",
"キャッシュのヘッダを気にかけた様子も全く無く、しかも依然として圧縮を利用していない。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"キャッシュのための`Cache-Control` とExpires、更新チェックを可能にする`Last-Modified` と`ETag`だ。\n",
"しかも、\n",
"`Vary: Accept-Encoding`ヘッダは、このサーバーは要求さえあればデータの圧縮も扱えるということをほのめかしてさえいる。しかし、ここでそのように要求しなかった。"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'\n",
"X-XSS-Protection: 1; mode=block\n",
"X-Frame-Options: deny\n",
"X-Content-Type-Options: nosniff\n",
"Strict-Transport-Security: max-age=31536000\n",
"ETag: \"bcdda81dde068c9e23eb55a81b23ed86278fdb59\"\n",
"Content-Type: text/plain; charset=utf-8\n",
"Cache-Control: max-age=300\n",
"X-GitHub-Request-Id: 2BF9481F:5B27:4344A02:57066E88\n",
"Content-Length: 3070\n",
"Accept-Ranges: bytes\n",
"Date: Thu, 07 Apr 2016 14:28:25 GMT\n",
"Via: 1.1 varnish\n",
"Connection: close\n",
"X-Served-By: cache-nrt6121-NRT\n",
"X-Cache: HIT\n",
"X-Cache-Hits: 1\n",
"Vary: Authorization,Accept-Encoding\n",
"Access-Control-Allow-Origin: *\n",
"X-Fastly-Request-ID: d86b916fd1178aa526be4ea549c97474be0f5f25\n",
"Expires: Thu, 07 Apr 2016 14:33:25 GMT\n",
"Source-Age: 1\n",
"\n",
"\n"
]
}
],
"source": [
"print(response2.headers.as_string())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"データを取得するのに丸々3070バイトもダウンロードしている"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"data2 = response2.read()"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"3070"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(data2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"先ほどダウンロードしたのと寸分違わぬ3070バイト。"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"data2 == data"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpは、これよりもっと上手く処理できるように設計されている。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## httplib2の紹介"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`httplib2` を使うには、まずこれをインストールする必要がある。\n",
"それには、code.google.com/p/httplib2/ に行って最新のバージョンをダウンロードすればいい。\n",
"`httplib2` はPython 2.xにもPython 3.xにも対応しているのだが、\n",
"必ずPython 3用のバージョンを選ぶ。httplib2-python3-0.5.0.zipみたいな名前のものがファイル。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"pip からインストールできる。\n",
"\n",
"```\n",
"pip install httplib2\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"<module 'httplib2' from 'C:\\\\Miniconda3\\\\lib\\\\site-packages\\\\httplib2\\\\__init__.py'>"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import httplib2\n",
"httplib2"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`httplib2` を使うには、`httplib2.Http` クラスのインスタンスを作成する。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httplib2の主要なインターフェイスはHttpオブジェクト。\n",
"Httpオブジェクトを作る時は常にディレクトリ名を渡さなければならない。\n",
"この時、そのディレクトリはまだ存在しないものであっても構わない。\n",
"必要に応じてhttplib2が作成してくれる。"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"h = httplib2.Http('.cache')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Httpオブジェクトができたら、データを取得するのは簡単で、\n",
"`request()`メソッドに欲しいデータのアドレスを渡して呼び出すだけでいい。\n",
"そうすれば、そのurlに対するhttp GETリクエストが送信される。"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"(response, content) = h.request('https://raw.githubusercontent.com/chungy/Dive-Into-Python-3/master/examples/feed.xml') "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`request()`メソッドは二つの値を返す。\n",
"\n",
"一つ目が`httplib2.Response`オブジェクトで、これにはサーバーが送り返してきたhttpヘッダが全て入っている。\n",
"例えば、statusの200という値は、リクエストが成功したことを示している。"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"200"
]
},
"execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"response.status"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`content`変数にはhttpサーバーから送り返された実際のデータが入っている。\n",
"このデータは文字列ではなく`bytes`オブジェクトの形式で返されるので、\n",
"文字コードを定めて、自分で変換しなくてはならない。"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"b\"<?xml version='1.0' encoding='utf-8'?>\\r\\n<feed xmlns=\""
]
},
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"content[:52]"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"3070"
]
},
"execution_count": 22,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(content)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 補足: httplib2はなぜ文字列の代わりにバイト列を返すのか?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"バイト列、文字列を「単純に」httplib2が処理してくれればいいのに、と思いがちだが、\n",
"しかし、これは現実には厄介な問題。\n",
"\n",
"その原因は、文字コードを決定する規則がリクエストされるリソースの種類によってまちまちだということにある。\n",
"\n",
"`httplib2` がリクエストされているリソースの種類を識別する方法としては、 \n",
"`Content-Type http`ヘッダに記されているリソースの種類を使うことである。\n",
"\n",
"しかし、これはhttpのオプション機能なので、\n",
"すべてのhttpサーバーがこのヘッダを返してくれるわけではない。\n",
"仮に、httpレスポンスにこのヘッダが含まれていなかったとすると、\n",
"あとはクライエント側でリソースの種類を推測するしかない(この作業は一般に「content sniffing」と呼ばれているが、こいつはどうやっても完璧にはならない)。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"リクエストしているリソースの種類が分かっているなら(このケースだとxmlドキュメント)、\n",
"返されたbytesオブジェクトをそのままxml.etree.ElementTree.parse()関数に渡すこともできるかもしれない。\n",
"\n",
"しかし、これができるのは、この例のようにxmlドキュメントが文字コードに関する情報を含んでいる場合だけ。\n",
"そして、これもオプション機能なので、あらゆるxmlドキュメントが文字コードを明示しているわけではない。\n",
"xmlドキュメントに文字コードの種類が示されていない場合には、\n",
"クライアントはドキュメントを運んできたhttpレスポンスの方(i.e. Content-Type httpヘッダ)を見ることになっている。\n",
"ここにはcharset変数が含まれているかもしれない。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"ここで、文字コードに関する情報は、\n",
"- xmlドキュメント自体\n",
"- Content-Type httpヘッダ\n",
"\n",
"の二ヶ所に存在しうることになった。\n",
"\n",
"では、両方に文字コードの情報が入っていたら、どちらが優先されるのか? \n",
"RFC 3023 によると、`Content-Type http`ヘッダに含まれているメディアタイプが\n",
"- application/xml\n",
"- application/xml-dtd\n",
"- application/xml-external-parsed-entity\n",
"のいずれかであるか、\n",
"あるいはapplication/xmlのサブタイプ(例えば、application/atom+xmlやapplication/rss+xml。ここには、さらにapplication/rdf+xmlも含まれる)ならば、\n",
"\n",
"文字コードは\n",
"\n",
"\n",
"1. Content-Type httpヘッダのcharset変数に入っている文字コードか、\n",
"2. ドキュメント内のxml宣言に入っているencoding属性の文字コードか、\n",
"3. utf-8\n",
"\n",
"になる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"一方で、\n",
"`Content-Type http`ヘッダで与えられるメディアタイプが\n",
"`text/xml` や `text/xml-external-parsed-entity`、\n",
"あるいは`text/*+xml`という形式のサブタイプなら、\n",
"ドキュメント中のxml宣言にあるencoding属性は全く無視されてしまい、\n",
"\n",
"文字コードは、\n",
"\n",
"1. Content-Type httpヘッダに入っているcharset変数で与えられる文字コードか、\n",
"2. us-ascii\n",
"\n",
"になる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"そして以上のことはxmlドキュメントだけに当てはまる話。\n",
"\n",
"htmlドキュメントについては、ウェブブラウザがcontent sniffing のための複雑怪奇な規則[pdf]を作り上げてしまっていて、完全に解明するのは難しい。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### httplib2はキャッシュをどのように扱うのか"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"先ほどの指定したフォルダ(`.cache`)に、キャッシュが保存される。"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"(response2, content2) = h.request('https://raw.githubusercontent.com/chungy/Dive-Into-Python-3/master/examples/feed.xml') "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpのstatusは同じく200で、前から何も変わっていない。"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"200"
]
},
"execution_count": 24,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"response2.status"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"ダウンロードした内容にも変化はない。"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"b\"<?xml version='1.0' encoding='utf-8'?>\\r\\n<feed xmlns=\""
]
},
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"content2[:52]"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"3070"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(content2)"
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"import httplib2"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"この行はhttp.clientでデバッグをオンにするのと同じ役割を果たすもので、\n",
"`httplib2` がサーバーに送信したデータ全部と、返送されてきた情報の中の主要なものを出力してくれるようになる。"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"httplib2.debuglevel = 1"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"前と同じディレクトリ名を渡して、httplib2.Httpオブジェクトを作成する。"
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"h = httplib2.Http('.cache')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"前回と同様に、同じurlをリクエストする。しかし**何も起こっていないようだ**。\n",
"もっと正確に言えば、何もサーバーに送られていなければ、\n",
"サーバーから返ってきてもいない。\n",
"ここではネットワークを通したやりとりが全く行われていない。"
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"(response, content) = h.request('https://raw.githubusercontent.com/chungy/Dive-Into-Python-3/master/examples/feed.xml') "
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": true
},
"source": [
"しかし、現実に何らかのデータを「受信」してはいる \n",
"— 実のところ、 すべてのデータを受け取っている。"
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"3070"
]
},
"execution_count": 31,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(content)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"「リクエスト」が成功したことを示すhttpステータスコードも「受信」している。"
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"200"
]
},
"execution_count": 32,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"response.status"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"問題があるのはこの部分。\n",
"この「レスポンス」は `httplib2` のローカルキャッシュから生成されたもの。\n",
"`httplib2.Http`オブジェクトを作るときにディレクトリ名を渡したが\n",
"— そのディレクトリは `httplib2` がこれまでに行った処理全てをキャッシュしている。"
]
},
{
"cell_type": "code",
"execution_count": 33,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 33,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"response.fromcache"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`httplib2` のデバッグをオンにしたいなら、\n",
"モジュールレベルの定数(`httplib2.debuglevel`)を設定してから、新しく`httplib2.Http`オブジェクトを作る必要がある。\n",
"デバッグをオフにしたいなら、同じモジュールレベルの定数を変更して、\n",
"また新しく `httplib2.Http`オブジェクトを作ればいい。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"前回、このurlのデータをリクエストした時、そのリクエストは成功していた(status: 200)。\n",
"これに対するレスポンスにはフィードのデータだけでなく、\n",
"キャッシュのヘッダも一組入っていて、\n",
"「このリソースは24時間までならキャッシュしてもいいよ」\n",
"(Cache-Control: max-age=86400の部分。\n",
"86400は24時間を秒に直したもの)と伝え回っていた。\n",
"`httplib2` はこのキャッシュのヘッダの内容を理解した上で、それに従って.cacheディレクトリ(これはHttpオブジェクトを作成したときに渡したものだ)に前回のレスポンスを保存しておいた。\n",
"そして、\n",
"そのキャッシュの期限がまだ切れてなかったので、2度目にこのurlのデータをリクエストした時、`httplib2` はネットワークにあたることなく単純にキャッシュしておいた内容を返した。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"「単純に」とは言ったが、当然ながらこの単純さの背後にはいくつもの複雑な処理が隠れている。\n",
"`httplib2` はデフォルトでhttpキャッシュを自動的に処理してくれる。\n",
"もし、何らかの理由でレスポンスがキャッシュから生成されたものなのかを知る必要があるなら、`response.fromcache` をチェックすればいい。\n",
"そういう場合でなければ、これは何の問題もなく上手く動いてくれる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"データのキャッシュを持ってはいるが、\n",
"そのキャッシュを無視して遠隔サーバーに再リクエストしたいと思ったとする。\n",
"ブラウザはユーザーから特に要求があればこういう処理をする。\n",
"例えば、`F5` を押すと現在見ているページが更新されるが、\n",
"`Ctrl+F5` を押せばキャッシュを無視して遠隔サーバーにリクエストが行われる。\n",
"ここで「単にキャッシュからデータを削除して、もう一度リクエストする」こともできる。\n",
"\n",
"もちろんそうすることもできるが、あなたと遠隔サーバー以外にもこの処理に関わっているものが存在しているかもしれないということを思い出してほしい。\n",
"例えば、中間にあるプロキシサーバーはどうだろうか? \n",
"これについては完全にあなたの手の外にあるが、\n",
"ここにまだデータがキャッシュされているかもしれない。\n",
"その場合、(プロキシサーバーにとっては)キャッシュはまだ有効なので、特に何も気にとめることなくキャッシュの方を返してくることになる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpの機能を使ってリクエストが確実に遠隔サーバーに届くようにすべき。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`httplib2` を使えば、どのリクエストにも任意のhttpヘッダを加えることができる。\n",
"全てのキャッシュ(つまり、ローカルディスクにあるキャッシュだけではなく、\n",
"あなたと遠隔サーバーの間にあるキャッシュプロキシ全て)を無視するには、\n",
"no-cacheヘッダをheaders辞書に加えればいい。"
]
},
{
"cell_type": "code",
"execution_count": 34,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"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'\n",
"reply: 'HTTP/1.1 200 OK\\r\\n'\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 "
]
}
],
"source": [
"(response2, content2) = h.request('https://raw.githubusercontent.com/chungy/Dive-Into-Python-3/master/examples/feed.xml',\n",
" headers={'cache-control':'no-cache'})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`httplib2` がネットワークを通じたリクエストを開始している。\n",
"`httplib2` は双方向 — つまり、レスポンスの受信とリクエストの送信の両方においてキャッシュのヘッダを理解し、それに従ってくれる。\n",
"ここでは、`no-cache`ヘッダが追加されたことをちゃんと認識している。\n",
"だからこそ、ローカルキャッシュを全て無視したのであり、\n",
"その結果としてネットワークを介したデータのリクエストを行わざるを得なくなった。 "
]
},
{
"cell_type": "code",
"execution_count": 35,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"200"
]
},
"execution_count": 35,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"response2.status"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"このレスポンスはローカルキャッシュから生成されたものではない。\n",
"このことはリクエスト送信に関するデバッグ情報が出力されているのを見れば明らかなのだが、これを手続的に確認できる。"
]
},
{
"cell_type": "code",
"execution_count": 36,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"False"
]
},
"execution_count": 36,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"response2.fromcache"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"リクエストが成功したので、\n",
"遠隔サーバーからフィード全体を再びダウンロードすることができた。\n",
"当然ながら、サーバーはフィードのデータと一緒にhttpヘッダも全て送り返してくれている。\n",
"この中にはキャッシュのヘッダも入っていて、\n",
"`httplib2` はこれを使ってローカルキャッシュを更新する。\n",
"次にこのフィードがリクエストされた時に、\n",
"ネットワークを通じたアクセスを避けられるかもしれないから。\n",
"\n",
"httpキャッシュに関わるどの部分も、\n",
"キャッシュの利用を最大にし、\n",
"ネットワークを介したアクセスを最小にするように設計されている。\n",
"今回はキャッシュを無視したが、今回のリクエストの結果を次回のリクエストに備えてキャッシュした。"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'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'}\n"
]
}
],
"source": [
"print(dict(response2.items()))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### httplib2はどのようにLast-ModifiedヘッダやETagヘッダを扱うのか"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`Cache-Control` と `Expires` の2つのキャッシュヘッダは \n",
"`freshness indicator` と呼ばれる。\n",
"この2つのヘッダは「このキャッシュの期限が切れるまでは、ネットワークを介したアクセスを行う必要はまったくない」と断言するもの。\n",
"前の章で見たのはまさしくこの機能で、\n",
"このヘッダがあれば、`httplib2` は1バイトたりともネットワークを通してやりとりすることなく、\n",
"そのままキャッシュのデータを返すのだ(もちろん、明示的にキャッシュを無視した場合は別だが)。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"しかし、データが更新された可能性があったのだが、\n",
"リクエストを送信してみたら実際には更新されていなかった、\n",
"という場合はどうだろう。\n",
"\n",
"httpはこういう時のために `Last-Modified` と `Etag` というヘッダを用意している。\n",
"これらのヘッダは `validator` と呼ばれるもので、ローカルキャッシュの有効期限が既に切れている場合には、クライアントは次のリクエストにこの `validator` を追加することで、\n",
"データが実際に変更されたかどうかを確認することができる。\n",
"データが変更されていなければ、サーバーはデータを返送せずに、\n",
"304ステータスコードをだけを送り返してくる。\n",
"だから、1回だけはネットワークを通したやりとりが行われるのだが、\n",
"はるかに少ないバイト数をダウンロードするだけで済む。"
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"import httplib2\n",
"httplib2.debuglevel = 1\n",
"h = httplib2.Http('.cache')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"フィードの代わりに、今回はサイトのホームページ(htmlドキュメント)をダウンロードする。\n",
"このページをリクエストするのは今回が始めてなので、`httplib2` がやるべき仕事は少ない。\n",
"実際、最小限のヘッダだけを付けてリクエストを送信している。"
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"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'\n",
"reply: 'HTTP/1.1 304 Not Modified\\r\\n'\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 "
]
}
],
"source": [
"(response, content) = h.request('https://raw.githubusercontent.com/diveintomark/diveintopython3/master/diveintopython3.org')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"返ってきたレスポンスにはhttpヘッダがいくつも入っている……が、\n",
"キャッシュに関する情報は含まれていない。\n",
"しかし、ここには`ETagヘッダ` と `Last-Modifiedヘッダ` が 2つとも入っている。"
]
},
{
"cell_type": "code",
"execution_count": 40,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'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'}\n"
]
}
],
"source": [
"print(dict(response.items()))"
]
},
{
"cell_type": "code",
"execution_count": 41,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"1971"
]
},
"execution_count": 41,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(content)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"同じページを、同じHttpオブジェクト(と同じローカルキャッシュ)を使って再びリクエストしてみる。"
]
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"(response, content) = h.request('https://raw.githubusercontent.com/diveintomark/diveintopython3/master/diveintopython3.org')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"クライエントの方に戻ると、httplib2は304ステータスコードを認識し、キャッシュからページの内容をロードしている。"
]
},
{
"cell_type": "code",
"execution_count": 43,
"metadata": {
"collapsed": false,
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 43,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"response.fromcache"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"ここには実際に2つのステータスコードがある\n",
"— すなわち、304(今回サーバーから返されたもの。これが返されたから`httplib2` はキャッシュの方を参照したのだ)と、\n",
"200(前回サーバーから返されたもの。\n",
"ページのデータと一緒にhttplib2のキャッシュに保存されていた)だ。\n",
"`response.status` はキャッシュのステータスコードを返す。"
]
},
{
"cell_type": "code",
"execution_count": 44,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"200"
]
},
"execution_count": 44,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"response.status # キャッシュのステータスコード"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"サーバーから返された本当のステータスコードが欲しいなら、\n",
"`response.dict` を参照すればいい。\n",
"これはサーバーから返された実際のヘッダを収めた辞書になる。"
]
},
{
"cell_type": "code",
"execution_count": 45,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"'200'"
]
},
"execution_count": 45,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"response.dict['status']"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"どうであれcontent変数にはちゃんとデータが入っている。\n",
"一般論としては、なぜレスポンスがキャッシュから生成されたのかを知る必要はないだろう(もしかしたらキャッシュから生成されたということすら気にかけないかもしれないが、\n",
"それでも全くかまわない。\n",
"`httplib2` は賢いので、こちらがおろそかでもきちんと処理してくれる)。\n",
"`request()` の処理が完了するころには、\n",
"`httplib2` は既にキャッシュをアップデートして、データを返してくれている。"
]
},
{
"cell_type": "code",
"execution_count": 46,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"1971"
]
},
"execution_count": 46,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(content)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### http2libはどのように圧縮を扱うのか"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpは数種類の圧縮形式をサポートしているが、\n",
"中でも最もよく使われているのは \n",
"- `gzip` と\n",
"- `deflate` \n",
"\n",
"の2つ。\n",
"`httplib2` はこの両方をサポートしている。"
]
},
{
"cell_type": "code",
"execution_count": 47,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"response, content = h.request('https://raw.githubusercontent.com/diveintomark/diveintopython3/master/diveintopython3.org')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httplib2が送信するリクエストには、\n",
"必ず `Accept-Encoding` というヘッダが付けられている。\n",
"このヘッダによって `deflate` か `gzip` のどちらかなら扱えるということをサーバーに伝えている。\n",
"\n",
"```\n",
"accept-encoding: gzip, deflate\n",
"\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"サーバーは、`gzip`形式で圧縮されたデータを返している。\n",
"`request()` の処理が完了するころには、\n",
"`httplib2` は既にデータを展開し、\n",
"content変数に入れ終わっているのだ。\n",
"返送されたデータが圧縮されたものだったかどうかを知りたいなら、\n",
"`response['-content-encoding']` をチェックすればいい。\n",
"そうでなければ、何も気にする必要はない。"
]
},
{
"cell_type": "code",
"execution_count": 48,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'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'}\n"
]
}
],
"source": [
"print(dict(response.items()))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### httplib2はどのようにリダイレクトを扱うのか"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpは2種類のリダイレクトを定義していた。\n",
"つまり、\n",
"- 一時的なもの\n",
"- 恒久的なもの\n",
"\n",
"このうち、一時的なリダイレクトについては、\n",
"そのリダイレクトをたどるということ\n",
"(これはhttplib2が自動でやってくれる)以外に、\n",
"特に何か処理を行う必要はなかった。"
]
},
{
"cell_type": "code",
"execution_count": 49,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"import httplib2\n",
"httplib2.debuglevel = 1\n",
"h = httplib2.Http('.cache')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"response, content = h.request('http://diveintopython3.org/examples/feed-302.xml') ①\n",
"connect: (diveintopython3.org, 80)\n",
"send: b'GET /examples/feed-302.xml HTTP/1.1 ②\n",
"Host: diveintopython3.org\n",
"accept-encoding: deflate, gzip\n",
"user-agent: Python-httplib2/$Rev: 259 $'\n",
"reply: 'HTTP/1.1 302 Found' ③\n",
"send: b'GET /examples/feed.xml HTTP/1.1 ④\n",
"Host: diveintopython3.org\n",
"accept-encoding: deflate, gzip\n",
"user-agent: Python-httplib2/$Rev: 259 $'\n",
"reply: 'HTTP/1.1 200 OK'\n",
"```\n",
"\n",
"1.このurlにはフィードが入っていない。正しいアドレスに一時的にリダイレクトするようサーバーを設定しておいた。\n",
"②\tここでリクエストが行われている。\n",
"③\tそれに対するレスポンスは302 Foundだ。ここには示されていないが、このレスポンスには正しいurlを示すLocationヘッダが入っている。\n",
"④\thttplib2はすぐに方向を変えて、Locationヘッダで与えられたhttp://diveintopython3.org/examples/feed.xml (http://diveintopython3-ja.rdy.jp/http-web-services.html)というurl に新たなリクエストを送信し、リダイレクトを「たどって」くれる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"取得したresponseの中には最後のurlに関する情報は含まれているのだが、では、その中間にあったurl、つまりこの最後のurlに至るまでに経由したurlの情報が欲しい時はどうすればいいのだろうか? これについてもhttplib2を使えば調べることができる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"response.previous ①\n",
"{'status': '302',\n",
" 'content-length': '228',\n",
" 'content-location': 'http://diveintopython3.org/examples/feed-302.xml',\n",
" 'expires': 'Thu, 04 Jun 2009 02:21:41 GMT',\n",
" 'server': 'Apache',\n",
" 'connection': 'close',\n",
" 'location': 'http://diveintopython3.org/examples/feed.xml',\n",
" 'cache-control': 'max-age=86400',\n",
" 'date': 'Wed, 03 Jun 2009 02:21:41 GMT',\n",
" 'content-type': 'text/html; charset=iso-8859-1'}\n",
">>> type(response) ②\n",
"<class 'httplib2.Response'>\n",
">>> type(response.previous)\n",
"<class 'httplib2.Response'>\n",
">>> response.previous.previous ③\n",
">>> \n",
"```\n",
"\n",
"- response.previous属性を調べれば、httplib2が現在のレスポンスオブジェクトにたどり着く直前に経由したレスポンスオブジェクトを参照することができる。\n",
"- ②\tresponseもresponse.previousのどちらもhttplib2.Responseオブジェクトだ。\n",
"\n",
"- ③\t従って、response.previous.previousというように調べることで、リダイレクトの道筋をどんどん遡っていけることになる(これが必要になるのは次のような状況だ。つまり、あるurlが二番目のurlにリダイレクトし、さらにそこから三番目のurlにリダイレクトされる。本当にこういうこともあるんだよ!)。ここでは、既にリダイレクトの起点までたどり着いていたので、この属性の値はNoneになる。\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"同じurlをもう一度リクエストしたらどうなるだろうか?\n",
"\n",
"```\n",
"# 前の例から続く\n",
">>> response2, content2 = h.request('http://diveintopython3.org/examples/feed-302.xml') ①\n",
"connect: (diveintopython3.org, 80)\n",
"send: b'GET /examples/feed-302.xml HTTP/1.1 ②\n",
"Host: diveintopython3.org\n",
"accept-encoding: deflate, gzip\n",
"user-agent: Python-httplib2/$Rev: 259 $'\n",
"reply: 'HTTP/1.1 302 Found' ③\n",
">>> content2 == content ④\n",
"True\n",
"\n",
"```\n",
"①\t同じurlに、同じhttplib2.Httpオブジェクトだ(したがってキャッシュも同じだ)。\n",
"②\t302レスポンスはキャッシュされなかったので、httplib2は別のリクエストを同じurlに送信している。\n",
"③\tまたしても、サーバーは302を返している。しかし、ここに何が欠けているかに注意してほしい。ここでは最終的なurlの http://diveintopython3.org/examples/feed.xml に対する二回目のリクエストが送られていないのだ。つまり、先ほどのレスポンスはキャッシュされていて(前の例で見たCache-Controlを思い出して欲しい)、さらにhttplib2は302 Foundを受け取ると、新しくリクエストを送信するのに先立ってまずキャッシュをチェックしたということだ。キャッシュにはまだ新しいhttp://diveintopython3.org/examples/feed.xml のコピーがあったので、再びリクエストする必要が無かったのだ。\n",
"④\trequest()メソッドが処理を完了するころには、フィードのデータは既にキャッシュから読み出され、返されている。もちろん、これは前回受け取ったのと同じデータだ。\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"要するに、一時的なリダイレクトについては特に何かをする必要はないということだ。httplib2は自動でリダイレクトをたどってくれるし、しかも、あるurlが別のurlにリダイレクトしているという事実は、httplib2が圧縮やキャッシュやETagsなどのhttpの諸機能を扱う上で何の妨げにもならない。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"恒久的なリダイレクトも同じく簡単。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"response, content = h.request('http://diveintopython3.org/examples/feed-301.xml') ①\n",
"connect: (diveintopython3.org, 80)\n",
"send: b'GET /examples/feed-301.xml HTTP/1.1\n",
"Host: diveintopython3.org\n",
"accept-encoding: deflate, gzip\n",
"user-agent: Python-httplib2/$Rev: 259 $'\n",
"reply: 'HTTP/1.1 301 Moved Permanently' ②\n",
">>> response.fromcache ③\n",
"True\n",
"```\n",
"①\t前と同じく、このurlは実際には存在していない。\n",
"そこで、http://diveintopython3.org/examples/feed.xml に向けた恒久的なリダイレクトを送信するようにサーバーを設定しておいた。\n",
"②\tほら、ステータスコード301が返ってきた。だが、またここで何が欠けているかに注意してほしい。リダイレクト先のurlに対するリクエストが送信されていないのだ。なぜか? その答えは「既にローカルにキャッシュされているから」だ。\n",
"③\thttplib2はリダイレクトを「たどって」、そのままキャッシュに行き着いたのだ。\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"ここに一時的なリダイレクトと恒久的なリダイレクトの違いがある。\n",
"一度httplib2が恒久的なリダイレクトをたどると、\n",
"そのurlに対するリクエストはそれから先、\n",
"すべて自動でリダイレクト先のurlに書きかえられ、\n",
"元のurlにネットワークを介してリクエストが送られることはない。\n",
"デバッグがまだオンになっていることを思い出して欲しい。\n",
"\n",
"それなのに、ネットワークを通じたやりとりは全く出力されていない。\n",
"\n",
"```\n",
"response2, content2 = h.request('http://diveintopython3.org/examples/feed-301.xml') ①\n",
">>> response2.fromcache ②\n",
"True\n",
">>> content2 == content ③\n",
"True\n",
"```\n",
"\t\n",
"- ②\tそう、このレスポンスはキャッシュから取得したものだ。\n",
"- ③\tフィード全体を(キャッシュから)取得できている。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Github apiでGet してみる"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"自分のユーザー情報を取得する。\n",
"APIを使うに認証が必要。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"import httplib2\n",
"from base64 import b64encode\n",
"httplib2.debuglevel = 1\n",
"\n",
"h = httplib2.Http('.cache')\n",
"user = ''\n",
"password = ''\n",
"auth = b64encode(bytes(user + ':' + password, 'utf-8')).decode('utf-8')\n",
"endpoint = 'https://api.github.com'\n",
"resp, content = h.request(endpoint+'/user', \n",
" 'GET',\n",
" headers = {'Authorization': 'Basic ' + auth}\n",
" )\n",
"\n",
"```\n",
"\n",
"result:\n",
"\n",
"```\n",
"send: b'GET /user HTTP/1.1\\r\\nHost: api.github.com\\r\\n\n",
"accept-encoding: gzip, deflate\\r\\n\n",
"if-none-match: \"0dc6462cfbaf8e49dfcf4f679ac9d268\"\\r\\n\n",
"if-modified-since: Fri, 25 Mar 2016 16:04:44 GMT\\r\\n\n",
"user-agent: Python-httplib2/0.9.2 (gzip)\\r\\n\n",
"authorization: Basic XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\\r\\n\\r\\n'\n",
"reply: 'HTTP/1.1 200 \\r\\n'\n",
"...\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": false
},
"source": [
"```\n",
">resp.status\n",
"\n",
"200\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"> content\n",
"\n",
"b'{\"login\":\"XXXX\",\n",
"\"id\":XXXXX,\n",
"\"avatar_url\":\"https://avatars.githubusercontent.com/u/10578703?v=3\",\n",
"\"gravatar_id\":\"\",\n",
"\"url\":\"https://api.github.com/users/Cartman0\",\n",
"\"html_url\":\"https://github.com/Cartman0\",\n",
"\"followers_url\":\"https://api.github.com/users/Cartman0/followers\",\n",
"\"following_url\":\"https://api.github.com/users/Cartman0/following{/other_user}\",\n",
"...\n",
"}\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## HTTP GETの先へ"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Twitter も Identi.ca も、140字以内であなたのステータスを投稿し、更新できるようにしてくれるシンプルなhttpベースのapiを公開している。\n",
"ステータスを更新するための Identi.ca のapiドキュメントを見てみる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"```\n",
"Identi.ca rest apiメソッド: ステータス/更新\n",
"Updates the authenticating user’s status. Requires the status parameter specified below. Request must be a POST.\n",
"\n",
"url\n",
"https://identi.ca/api/statuses/update.format\n",
"\n",
"Formats\n",
"xml, json, rss, atom\n",
"\n",
"http Method(s)\n",
"POST\n",
"\n",
"Requires Authentication\n",
"true\n",
"\n",
"Parameters\n",
"status. Required. The text of your status update. url-encode as necessary.\n",
"\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"これはどのように動くのだろう? \n",
"新しいメッセージをIdenti.caに投稿するには、\n",
"http POSTリクエストをhttp://identi.ca/api/statuses/update.format に送信しなければならない(formatの部分はurlの一部ではない。ここには、リクエストに対して、サーバーにどんなデータ形式でレスポンスを返信してほしいのかを入れる。\n",
"xmlでレスポンスを返してほしければ、\n",
"https://identi.ca/api/statuses/update.xml にリクエストを送信する)。\n",
"また、リクエストにはstatusという変数を含める必要があり、この変数にステータスを更新するメッセージが入ることになる。さらに、リクエストは認証を通らなくてはならない。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"認証だって? もちろん。Identi.caでステータスを更新するには、あなたが誰であるかを証明しなくてはならない。Identi.caはwikiではないのだ。だから、あなただけがあなたのステータスを更新することができる。\n",
"Identi.caはsslを介した `http Basic`認証(a.k.a. RFC 2617)を利用して、セキュアで扱いやすい認証を提供している。\n",
"\n",
"`httplib2` はsslもhttp Basic認証もサポートしているので、\n",
"この部分は簡単に済ますことができる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"POSTリクエストとGETリクエストとの違いは、\n",
"- POSTリクエストには **ペイロード** が入っているということにある。\n",
"\n",
"このペイロードとはサーバーに送信したいデータのこと。\n",
"ここで、apiメソッドはデータの一部としてstatusを要求しているが、\n",
"これはurlエンコードされている必要がある。\n",
"このurlエンコードとは非常にシンプルな符号化形式で、キーと値のペアからなる集合(i.e. 辞書)を引数にとり、それを文字列に変換するというもの。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Python には辞書をurlエンコードするユーティリティ関数が用意されている。\n",
"すなわち、`urllib.parse.urlencode()` だ。"
]
},
{
"cell_type": "code",
"execution_count": 50,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"<function urllib.parse.urlencode>"
]
},
"execution_count": 50,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from urllib.parse import urlencode \n",
"urlencode"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Identi.ca api が要求しているのはこの種の辞書。\n",
"ここには `status` というキーが一つだけ入っていて、\n",
"それに対応する値は一回分のステータス更新のメッセージになっている。"
]
},
{
"cell_type": "code",
"execution_count": 51,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"data = {'status': 'Test update from Python 3'}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"urlエンコードされた文字列はこんな感じになる。\n",
"これが `http POST`リクエストの際に、\n",
"「回線を通じて」`Identi.ca api`サーバーに送信されるペイロードになる。"
]
},
{
"cell_type": "code",
"execution_count": 52,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"'status=Test+update+from+Python+3'"
]
},
"execution_count": 52,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"urlencode(data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"from urllib.parse import urlencode\n",
"import httplib2\n",
"httplib2.debuglevel = 1\n",
"h = httplib2.Http('.cache')\n",
"data = {'status': 'Test update from Python 3'}\n",
"h.add_credentials('diveintomark', 'MY_SECRET_PASSWORD', 'identi.ca') ①\n",
"resp, content = h.request('https://identi.ca/api/statuses/update.xml',\n",
"... 'POST', ②\n",
"... urlencode(data), ③\n",
"... headers={'Content-Type': 'application/x-www-form-urlencoded'}) ④\n",
"```\n",
"\n",
"1. 次のようにしてhttplib2は認証を扱う。まず、`add_credentials()`メソッドを用いてユーザー名とパスワードを記憶する。\n",
"それから、`httplib2` がリクエストを出すと、サーバーは `401 Unauthorized`ステータスコードを返し、\n",
"さらにどの認証方式をサポートしているかのリストを(WWW-Authenticateヘッダに入れて)返送してくれる。\n",
"`httplib2` は自動で Authorizationヘッダを組み立てて、このurlに再びリクエストを送信してくれる。\n",
"\n",
"2. 2番目の変数はhttpリクエストの種類。ここではPOSTになる。\n",
"\n",
"3. 3番目の変数はサーバーに送信するペイロード。ここではステータスメッセージの入った辞書をurlエンコードで変換して送信する。\n",
"\n",
"4. 最後に、ペイロードがurlエンコードで符号化されたものだということをサーバーに伝えなくてはならない。\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`add_credentials()`メソッドの3番目の変数は、\n",
"その認証が通用するドメインを表す。\n",
"この部分については必ず明記しておくこと! \n",
"\n",
"このドメインを空白のままにしておくと、\n",
"後で別の認証を必要とするサイトに対してhttplib2.Httpオブジェクトを再利用した時に、httplib2が元のサイト用のユーザー名とパスワードをその別のサイトに漏らしてしまいかねないため。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Github(Gist) API"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"https://developer.github.com/v3/gists/#create-a-gist\n",
"\n",
"Create a gist\n",
"\n",
"POST /gists\n",
"Input\n",
"\n",
"|Name |\tType | Description |\n",
"|:--:|:--:|:--:|\n",
"|files | object | **Required**. Files that make up this gist. |\n",
"|description |\tstring | A description of the gist.\n",
"|public\t| boolean |Indicates whether the gist is public. Default: false\n",
"\n",
"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:\n",
"\n",
"```\n",
"{\n",
" \"description\": \"the description for this gist\",\n",
" \"public\": true,\n",
" \"files\": {\n",
" \"file1.txt\": {\n",
" \"content\": \"String file contents\"\n",
" }\n",
" }\n",
"}\n",
"```\n",
"\n",
"Note: Don't name your files \"gistfile\" with a numerical suffix. This is the format of the automatic naming scheme that Gist uses internally.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Basic認証 https://josephscott.org/archives/2011/06/http-basic-auth-with-httplib2/"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"import httplib2\n",
"from base64 import b64encode\n",
"from urllib.parse import urlencode\n",
"import json\n",
"\n",
"httplib2.debuglevel = 1\n",
"\n",
"h = httplib2.Http('.cache')\n",
"\n",
"user = ''\n",
"password = ''\n",
"auth = b64encode(bytes(user + ':' + password, 'utf-8')).decode('utf-8')\n",
"endpoint = 'https://api.github.com'\n",
"\n",
"data = {\n",
" \"description\": \"Github API Sample\",\n",
" \"public\": True,\n",
" \"files\": {\n",
" \"file1.md\": {\n",
" \"content\": \"# Create a Gist with Github API\"\n",
" }\n",
" }\n",
"}\n",
"\n",
"resp, content = h.request(endpoint+'/gists', \n",
" 'POST',\n",
" json.dumps(data),\n",
" headers = {'Authorization': 'Basic ' + auth,\n",
" 'Content-Type': 'application/json'\n",
" })\n",
"```\n",
"\n",
"\n",
"result:\n",
"```\n",
"send: b'POST /gists HTTP/1.1\\r\\nHost: api.github.com\\r\\nContent-Length: 123\\r\\n\n",
"accept-encoding: gzip, deflate\\r\\n\n",
"content-type: application/json\\r\\n\n",
"user-agent: Python-httplib2/0.9.2 (gzip)\\r\\n\n",
"authorization: Basic XXXXXXXXXXXXXXXXXXXXXX\\r\\n\\r\\n'\n",
"send: b'{\"files\": {\"file1.md\": {\"content\": \"# Create a Gist with Github API\"}}, \"public\": true, \"description\": \"Github API Sample\"}'\n",
"reply: 'HTTP/1.1 201 Created\\r\\n'\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 \n",
"````"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"> resp.status\n",
"\n",
"201\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Gist id を取得。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"> content_data = json.loads(content.decode('utf-8'))\n",
"> content_data['id']\n",
"\n",
"'1426de5bcd87485909ba2f0a66da6481'\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"以下のようにgistリポジトリが作られる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![create a gist](https://gist.githubusercontent.com/Cartman0/1c885ad8ce22c021b0350f53ee3a1126/raw/03969da9fd1e8061fcce2e961e9c5aaf46e22680/create-gist.jpg)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"参考:https://swarm.workshop.perforce.com/view/guest/stephen_moon/smoon/scripts/py_progs/restapi/restapi.py"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## HTTP POSTの先へ"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"httpは `GET` と `POST` だけには留まらない。\n",
"この2つは(特にウェブブラウザにおいて)最もよく使われているリクエストだが、\n",
"ウェブサービスのapiは `GET` と `POST` 以上のものを扱うことができるし、\n",
"`httplib2` の方もそれに対応する準備ができている。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"from xml.etree import ElementTree as etree\n",
"tree = etree.fromstring(content) ①\n",
"status_id = tree.findtext('id') ②\n",
"\n",
"> status_id\n",
"'5131472'\n",
"\n",
"url = 'https://identi.ca/api/statuses/destroy/{0}.xml'.format(status_id) ③\n",
"resp, deleted_content = h.request(url, 'DELETE') ④\n",
"```\n",
"\n",
"1. サーバーはxmlを返してきたんだったよね? xmlをパースする方法はもう知ってるはずだ。\n",
"2. findtext()メソッドは与えられた表現に最初に合致するものを探しだし、そこからそのテキストの内容を抽出する。ここでは単に`<id>`要素を探しているだけ。\n",
"3. 先ほど投稿したステータスメッセージを削除するために、`<id>`要素のテキストの内容に基づいてurlを構築する。\n",
"4. メッセージを削除するには、単純にこのurlにhttp DELETEリクエストを送ればいい。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"send: b'DELETE /api/statuses/destroy/5131472.xml HTTP/1.1 ①\n",
"Host: identi.ca\n",
"Accept-Encoding: identity\n",
"user-agent: Python-httplib2/$Rev: 259 $\n",
"\n",
"'\n",
"reply: 'HTTP/1.1 401 Unauthorized' ②\n",
"send: b'DELETE /api/statuses/destroy/5131472.xml HTTP/1.1 ③\n",
"Host: identi.ca\n",
"Accept-Encoding: identity\n",
"authorization: Basic SECRET_HASH_CONSTRUCTED_BY_HTTPLIB2 ④\n",
"user-agent: Python-httplib2/$Rev: 259 $\n",
"\n",
"'\n",
"reply: 'HTTP/1.1 200 OK' ⑤\n",
">>> resp.status\n",
"200\n",
"\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Gist api でGist を削除してみる"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Gist api でのGistの削除は以下。\n",
"\n",
"https://developer.github.com/v3/gists/#delete-a-gist\n",
"\n",
"Delete a gist\n",
"\n",
"DELETE `/gists/:id`\n",
"\n",
"Response\n",
"```\n",
"Status: 204 No Content\n",
"X-RateLimit-Limit: 5000\n",
"X-RateLimit-Remaining: 4999\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"import httplib2\n",
"from base64 import b64encode\n",
"\n",
"httplib2.debuglevel = 1\n",
"\n",
"h = httplib2.Http('.cache')\n",
"\n",
"user = ''\n",
"password = ''\n",
"auth = b64encode(bytes(user + ':' + password, 'utf-8')).decode('utf-8')\n",
"endpoint = 'https://api.github.com'\n",
"\n",
"gist_id = '1426de5bcd87485909ba2f0a66da6481'\n",
"\n",
"resp, content = h.request(endpoint + '/gists/' + gist_id, \n",
" 'DELETE',\n",
" headers = {'Authorization': 'Basic ' + auth}\n",
" )\n",
"```\n",
"\n",
"result:\n",
"\n",
"```\n",
"send: b'DELETE /gists/1426de5bcd87485909ba2f0a66da6481 HTTP/1.1\\r\\n\n",
"Host: api.github.com\\r\\n\n",
"accept-encoding: gzip, deflate\\r\\n\n",
"user-agent: Python-httplib2/0.9.2 (gzip)\\r\\n\n",
"authorization: Basic XXXXXXXXXXXXXXXXXXXXXXXXXXXXX\\r\\n\\r\\n'\n",
"reply: 'HTTP/1.1 204 No Content\\r\\n'\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 \n",
"\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"> resp.status\n",
"\n",
"204\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"> content\n",
"\n",
"b''\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"これで任意のgistを削除できる。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 参考リンク"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"- [Dive Into Python3 14章 HTTPウェブサービス](http://diveintopython3-ja.rdy.jp/http-web-services.html)\n",
"\n",
"- [http.client — HTTP プロトコルクライアント python doc](http://docs.python.jp/3.5/library/http.client.html)\n",
"- [urllib.request — URL を開くための拡張可能なライブラリ python doc](http://docs.python.jp/3.5/library/urllib.request.html)\n",
"- [httplib2 pypi](https://pypi.python.org/pypi/httplib2)\n",
"- [httplib2 project](https://code.google.com/archive/p/httplib2/)\n",
"- [requestsモジュール](http://requests-docs-ja.readthedocs.org/en/latest/)\n",
"- [RFC2616 日本語訳:HTTP](http://pentan.info/doc/rfc/j2616.html)\n",
"- [RFC2617 日本語訳:HTTPBasic認証](http://pentan.info/doc/rfc/j2617.html)\n",
"- [RFC1952 日本語訳:gzip](http://pentan.info/doc/rfc/j1952.html)\n",
"- [RFC1951 日本語訳:deflate](http://www.futomi.com/lecture/japanese/rfc1951.html)"
]
}
],
"metadata": {
"gist": {
"data": {
"description": "Dive Into Python 3 14章メモ(HTTPウェブサービス)",
"public": true
},
"id": ""
},
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.5.1"
},
"toc": {
"toc_cell": true,
"toc_number_sections": true,
"toc_threshold": "6",
"toc_window_display": false
}
},
"nbformat": 4,
"nbformat_minor": 0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment