urllib パッケージを使ってインターネット上のリソースを取得するには
*****************************************************************


はじめに
========


Related Articles
^^^^^^^^^^^^^^^^

同じように Python でインターネットリソースを取得するのに以下の記事が役
に立ちます:

* Basic 認証

     *Basic 認証* についてのチュートリアルで Python の例がついています
     。

**urllib.request** は、URL (Uniform Resource Locator) からリソースを取
得する Python モジュールです。 *urlopen* 関数という、とても単純なイン
ターフェースを提供します。 この関数は、さまざまなプロトコルを使って
URL からリソースを取得できます。 また、Basic 認証、クッキー、プロキシ
など、よくある状況を扱うための、少し複雑なインターフェースも提供します
。 これらは handler や opener と呼ばれるオブジェクトによって提供されま
す。

urllib.request は、多くの「URL スキーム」に対応するネットワークプロト
コル (FTP、HTTP など) を使って、URL からリソースを取得できます。 URL
スキームは URL の "":"" より前の文字列で識別されます。 例えば、
""ftp://python.org/"" の URL スキームは ""ftp"" です。 このチュートリ
アルでは、最も一般的な HTTP に焦点を当てます。

単純な状況では *urlopen* はとても簡単に使うことができます。しかし HTTP
の URL を開くときにエラーが起きたり、特殊なケースに遭遇すると、
HyperText Transfer Protocol に関するいくつかのことを理解する必要があり
ます。 HTTP に関して最も包括的で信頼できる文献は **RFC 2616** です。こ
の文書は技術文書なので簡単には読めません。この HOWTO の目的は *urllib*
の利用法を例示することです、 HTTP についてはその助けになるのに十分に詳
しく載せています。このドキュメントは "urllib.request" のドキュメントの
代わりにはなりませんが、補完する役割を持っています。


URL を取得する
==============

urllib.request を利用する最も簡単な方法は以下です:

   import urllib.request
   with urllib.request.urlopen('http://python.org/') as response:
      html = response.read()

URL によってリソースを取得し、それを一時的な場所に保存しておきたいとき
は、 "shutil.copyfileobj()" と "tempfile.NamedTemporaryFile()" 関数を
使って行うことができます:

   import shutil
   import tempfile
   import urllib.request

   with urllib.request.urlopen('http://python.org/') as response:
       with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
           shutil.copyfileobj(response, tmp_file)

   with open(tmp_file.name) as html:
       pass

多くの urllib の利用法はこのように簡単です ('http:' の代わりに URL を
'ftp:' や 'file:' 等で始めればできます)。しかし、このチュートリアルの
目的は、特に HTTP に絞って、より複雑な状況を説明することです。

HTTP はリクエスト (request) とレスポンス (response) が基本となっていま
す - クライアントがリクエストし、サーバーがレスポンスを送ります。
urllib.request はこれを真似て、作成する HTTP リクエストを表現する
"Request" オブジェクトを備えています。リクエストオブジェクトを作成する
最も簡単な方法は取得したい URL を指定することです。"urlopen" をこのオ
ブジェクトを使って呼び出すと、リクエストした URL のレスポンスオブジェ
クトが返されます。このレスポンスはファイルライクオブジェクトで、これは
つまりレスポンスに ".read()" と呼び出せることを意味しています:

   import urllib.request

   req = urllib.request.Request('http://python.org/')
   with urllib.request.urlopen(req) as response:
      the_page = response.read()

urllib.request は同じリクエストインターフェースを全ての URL スキームに
対して利用できるようにしています。例えば、FTP リクエストの場合はこうで
きます:

   req = urllib.request.Request('ftp://example.com/')

HTTP の場合には、リクエストオブジェクトに対して二つの特別な操作ができ
ます: 一つ目はサーバーに送るデータを渡すことができる、二つ目はサーバー
に送るデータやリクエスト自身に *ついての* 特別な情報 ("metadata")を渡
すことができます - これらの送られる情報は HTTP 「ヘッダ」です。今度は
これらに関してひとつひとつ見ていきましょう。


データ
------

URL にデータを送りたい場合はよくあります (しばしば、その URL は CGI
(Common Gateway Interface) スクリプトや他の web アプリケーションを参照
することになります)。これは HTTP では、 **POST** リクエストとして知ら
れる方法で行なわれます。これは web 上で HTML フォームを埋めて送信する
ときにブラウザが行なっていることです。全ての POST がフォームから送られ
るとは限りません: 自身のアプリケーションに対して任意のデータを POST を
使って送ることができます。一般的な HTML フォームの場合、データは標準的
な方法でエンコードされている必要があり、リクエストオブジェクトに
"data" 引数として渡します。エンコーディングは "urllib.parse" ライブラ
リの関数を使って行います。

   import urllib.parse
   import urllib.request

   url = 'http://www.someserver.com/cgi-bin/register.cgi'
   values = {'name' : 'Michael Foord',
             'location' : 'Northampton',
             'language' : 'Python' }

   data = urllib.parse.urlencode(values)
   data = data.encode('ascii') # data は bytes にする
   req = urllib.request.Request(url, data)
   with urllib.request.urlopen(req) as response:
      the_page = response.read()

他のエンコーディングが必要な場合があることに注意して下さい (例えば、
HTML フォームからファイルをアップロードするための詳細については HTML
Specification, Form Submission を見て下さい)。

"data" 引数を渡さない場合、urllib は **GET** リクエストを利用します。
GET と POST リクエストの一つの違いは、POST リクエストにしばしば、「副
作用」があることです: POST リクエストはいくつかの方法によってシステム
の状態を変化させます (例えば100ポンドのスパムの缶詰をドアの前まで配達
する注文を web サイトで行う)。 とはいえ HTTP 標準で明確にされている内
容では、POST は *常に* 副作用を持ち、GET リクエストは *決して* 副作用
を持たないことを意図するけれども、GET リクエストが副作用を持つことも、
POST リクエストが副作用を持たないことも、妨げられません。 HTTP の GET
リクエストでも、URL 自体にデータをエンコードすることでデータを渡せます
。

以下のようにして行います:

   >>> import urllib.request
   >>> import urllib.parse
   >>> data = {}
   >>> data['name'] = 'Somebody Here'
   >>> data['location'] = 'Northampton'
   >>> data['language'] = 'Python'
   >>> url_values = urllib.parse.urlencode(data)
   >>> print(url_values)  # The order may differ from below.
   name=Somebody+Here&language=Python&location=Northampton
   >>> url = 'http://www.example.com/example.cgi'
   >>> full_url = url + '?' + url_values
   >>> data = urllib.request.urlopen(full_url)

"?" を URL に加え、それにエンコードされた値が続くことで、完全な URL が
作られていることに注意して下さい。


ヘッダ
------

ここでは特定の HTTP ヘッダについて議論します、 HTTP リクエストにヘッダ
を追加する方法について例示します。

いくつかの web サイト [1] はプログラムからブラウズされることを嫌ってい
たり、異なるブラウザに対して異なるバージョンを送ります [2]。デフォルト
では urllib は自身の情報を "Python-urllib/x.y" として扱います ( "x" と
"y" は Python のリリースバージョンのメジャーバージョンとマイナーバージ
ョンです、例えば "Python-urllib/2.5" など)。これによって web サイト側
が混乱したり、動作しないかもしれません。ブラウザは自身の情報を "User-
Agent" ヘッダ [3] を通して扱っています。リクエストオブジェクトを作ると
きに、ヘッダに辞書を渡すことができます。以下の例は上の例と同じですが、
自身を Internet Explorer [4] のバージョンの一つとして扱っています。

   import urllib.parse
   import urllib.request

   url = 'http://www.someserver.com/cgi-bin/register.cgi'
   user_agent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)'
   values = {'name': 'Michael Foord',
             'location': 'Northampton',
             'language': 'Python' }
   headers = {'User-Agent': user_agent}

   data = urllib.parse.urlencode(values)
   data = data.encode('ascii')
   req = urllib.request.Request(url, data, headers)
   with urllib.request.urlopen(req) as response:
      the_page = response.read()

レスポンスには便利なメソッドが二つあります。 問題が起きたときの動作を
説明した後の、info と geturl の節を参照してください。


例外を処理する
==============

*urlopen* はレスポンスを処理できない場合に "URLError" を送出します (た
だし、他の Python API と同様に、"ValueError" や "TypeError" などの組み
込み例外が送出される場合もあります)。

"HTTPError" は "URLError" のサブクラスで、HTTP URL に固有の場合に送出
されます。

例外クラスは "urllib.error" モジュールから提供されています。


URLError
--------

URLError が送出されることはよく起こります、それはネットワーク接続が無
い場合や、指定したサーバが無い場合です。この場合、例外は 'reason' 属性
を持っていて、この属性はエラーコードとエラーメッセージのテキストを含む
タプルです。

例:

   >>> req = urllib.request.Request('http://www.pretend_server.org')
   >>> try: urllib.request.urlopen(req)
   ... except urllib.error.URLError as e:
   ...     print(e.reason)
   ...
   (4, 'getaddrinfo failed')


HTTPError
---------

サーバーからのすべての HTTP レスポンスには、数値の「ステータスコード」
が含まれます。 ステータスコードは、サーバーがリクエストを処理できない
ことを示す場合があります。 デフォルトのハンドラーは、そうしたレスポン
スの一部を処理します。 例えば、別の URL から文書を取得するようクライア
ントに要求する「リダイレクト」の場合、urllib が処理します。 処理できな
いレスポンスに対しては、urlopen が "HTTPError" を送出します。 よくある
エラーには、'404' (ページが見つからない)、'403' (リクエストが禁止され
ている)、'401' (認証が必要) があります。

HTTP のエラーコード全てについては **RFC 2616** の10節を参照して下さい
。

送出された "HTTPError" インスタンスには、サーバーが送ったエラーに対応
する整数の 'code' 属性があります。


エラーコード
~~~~~~~~~~~~

デフォルトハンドラーはリダイレクト(コードは300番台にあります) を処理し
、100--299番台のコードは成功を意味しているので、たいていの場合は400--
599番台のエラーコードのみを見るだけですみます。

"http.server.BaseHTTPRequestHandler.responses" は、**RFC 2616** で使わ
れるすべてのレスポンスコードを確認できる便利な辞書です。 辞書からの抜
粋を示します。

   responses = {
       ...
       <HTTPStatus.OK: 200>: ('OK', 'Request fulfilled, document follows'),
       ...
       <HTTPStatus.FORBIDDEN: 403>: ('Forbidden',
                                     'Request forbidden -- authorization will '
                                     'not help'),
       <HTTPStatus.NOT_FOUND: 404>: ('Not Found',
                                     'Nothing matches the given URI'),
       ...
       <HTTPStatus.IM_A_TEAPOT: 418>: ("I'm a Teapot",
                                       'Server refuses to brew coffee because '
                                       'it is a teapot'),
       ...
       <HTTPStatus.SERVICE_UNAVAILABLE: 503>: ('Service Unavailable',
                                               'The server cannot process the '
                                               'request due to a high load'),
       ...
       }

エラーが起きた場合、サーバーは HTTP エラーコード *および* エラーページ
を返して応答します。 "HTTPError" インスタンスはエラーページのレスポン
スとして扱えます。これは code 属性だけでなく、 "urllib.response" モジ
ュールが返すような read, geturl, info メソッドも持つことを意味します:

   >>> req = urllib.request.Request('http://www.python.org/fish.html')
   >>> try:
   ...     urllib.request.urlopen(req)
   ... except urllib.error.HTTPError as e:
   ...     print(e.code)
   ...     print(e.read())
   ...
   404
   b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n\n\n<html
     ...
     <title>Page Not Found</title>\n
     ...


まとめ
------

"HTTPError" *または* "URLError" に対応するには、基本的に二つの方法があ
ります。 筆者は二番目の方法を好んでいます。


その1
~~~~~

   from urllib.request import Request, urlopen
   from urllib.error import URLError, HTTPError
   req = Request(someurl)
   try:
       response = urlopen(req)
   except HTTPError as e:
       print('The server couldn\'t fulfill the request.')
       print('Error code: ', e.code)
   except URLError as e:
       print('We failed to reach a server.')
       print('Reason: ', e.reason)
   else:
       # すべて正常

注釈:

  "except HTTPError" を *必ず* 先に書いてください。 そうしないと、
  "except URLError" が "HTTPError" *も* 捕捉してしまいます。


その2
~~~~~

   from urllib.request import Request, urlopen
   from urllib.error import URLError
   req = Request(someurl)
   try:
       response = urlopen(req)
   except URLError as e:
       if hasattr(e, 'reason'):
           print('We failed to reach a server.')
           print('Reason: ', e.reason)
       elif hasattr(e, 'code'):
           print('The server couldn\'t fulfill the request.')
           print('Error code: ', e.code)
   else:
       # everything is fine


info と geturl
==============

urlopen が返すレスポンス (または "HTTPError" インスタンス) には、便利
なメソッド "info()" と "geturl()" があります。 これらは
"urllib.response" モジュールで定義されています。

* **geturl** - これは取得したページの実際の URL を返します。 "urlopen"
  (または利用される opener オブジェクト) はリダイレクトに追従するため
  、有用です。取得したページの URL は要求した URL と同じとは限りません
  。

* **info** - これは取得したページ (特にサーバからヘッダ)を表す辞書風オ
  ブジェクトを返します。これは現在では "http.client.HTTPMessage" イン
  スタンスです。

典型的なヘッダは 'Content-length', 'Content-type' 等を含んでいます。
HTTP ヘッダの意味と利用法について簡単な説明つきの便利な一覧 Quick
Reference to HTTP Headers を参照して下さい。


Openers と Handlers
===================

URL を取得する場合、opener (混乱を招きやすい名前ですが、
"urllib.request.OpenerDirector" のインスタンス) を利用します。標準的に
はデフォルトの opener を - "urlopen" を通して - 利用していますが、カス
タムの opener を作成することもできます。 opener は handler を利用しま
す。全ての「一番厄介な仕事」はハンドラによって行なわれます。各 handler
は特定の URL スキーム (http, ftp, 等) での URL の開き方を知っていたり
、 URL を開く局面でどう処理するかを知っています、例えば HTTP リダイレ
クションや HTTP のクッキーなど。

インストール済みの特定のハンドラで URL を取得したい場合には、 opener
を作成したいと思うでしょう、例えばクッキーを処理する opener が得たい場
合や、リダイレクションを処理しない opener を得たい場合。

opener を作るには、"OpenerDirector" のインスタンスを作成し、
".add_handler(some_handler_instance)" を繰り返し呼び出します。

代わりに、一度の関数呼び出しで opener オブジェクトを作れる便利な関数
"build_opener" も使えます。 "build_opener" はデフォルトでいくつかのハ
ンドラーを追加しますが、さらにハンドラーを追加したり、デフォルトのハン
ドラーを置き換えたりするのも簡単です。

追加したくなる可能性がある handler としては、プロキシ処理、認証など、
一般的ですがいくらか特定の状況に限られるものでしょう。

"install_opener" を使うと、"opener" オブジェクトを (グローバルな) デフ
ォルトの opener に設定できます。 以降、"urlopen" の呼び出しは、登録し
た opener を使います。

opener オブジェクトは "open" メソッドを持っていて、 "urlopen" 関数と同
じ様に、url を取得するのに直接呼び出すことができます: 利便性を除けば
"install_opener" を使う必要はありません。


Basic 認証
==========

ハンドラーの作成と登録の例として、"HTTPBasicAuthHandler" を使います。
Basic 認証の仕組みを含む詳しい説明は、Basic 認証のチュートリアル を参
照してください。

認証が必要な場合、サーバは認証を要求するヘッダ (401 のエラーコードとと
もに) を送ります。これによって認証スキームと 'realm' が指定されます。
ヘッダはこのようになっています: "WWW-Authenticate: SCHEME
realm="REALM"" 。

例

   WWW-Authenticate: Basic realm="cPanel Users"

クライアントは、その realm に適した名前とパスワードをリクエストヘッダ
ーに含めて、リクエストを再試行します。 これが「Basic 認証」です。 この
処理を簡単にするために、"HTTPBasicAuthHandler" のインスタンスと、その
ハンドラーを使う opener を作成できます。

"HTTPBasicAuthHandler" は、パスワードマネージャーというオブジェクトを
使い、URL と realm をユーザー名とパスワードに対応付けます。 サーバーか
らの認証ヘッダーで realm が分かっている場合は、"HTTPPasswordMgr" を使
えます。 realm を特に気にしないことも多く、その場合は
"HTTPPasswordMgrWithDefaultRealm" が便利です。 これを使うと、URL に対
するデフォルトのユーザー名とパスワードを指定できます。 特定の realm に
別の組み合わせを指定していなければ、このデフォルト値が使われます。 デ
フォルト値を登録するには、"add_password" メソッドの "realm" 引数に
"None" を渡します。

トップレベルの URL は、認証が必要になる最初の URL です。
".add_password()" に渡した URL より「深い」URL も対象になります。

   # パスワードマネージャーを作成する
   password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()

   # ユーザー名とパスワードを追加する。
   # realm が分かっていれば、None の代わりに指定できる。
   top_level_url = "http://example.com/foo/"
   password_mgr.add_password(None, top_level_url, username, password)

   handler = urllib.request.HTTPBasicAuthHandler(password_mgr)

   # "opener" (OpenerDirector のインスタンス) を作成する
   opener = urllib.request.build_opener(handler)

   # opener を使って URL を取得する
   opener.open(a_url)

   # opener を登録する。
   # 以降の urllib.request.urlopen の呼び出しはすべてこの opener を使う。
   urllib.request.install_opener(opener)

注釈:

  上の例では、"build_opener" に "HTTPBasicAuthHandler" だけを渡しまし
  た。 デフォルトの opener には、通常の状況に対応するハンドラーが備わ
  っています。 "ProxyHandler" ("http_proxy" 環境変数などのプロキシ設定
  がある場合)、"UnknownHandler"、"HTTPHandler"、
  "HTTPDefaultErrorHandler"、"HTTPRedirectHandler"、"FTPHandler"、
  "FileHandler"、"DataHandler"、"HTTPErrorProcessor" です。

"top_level_url" は実際には "http://example.com/" のような完全な URL
('http:' スキームとホスト名、オプションとしてポート番号、含む)  *ある
いは* "example.com" や "example.com:8080" (後者はポート番号を含む) の
ような "authority" (つまり、ホスト名とオプションとしてポート番号を含む
) の *どちらでも* かまいません。authority の場合には "userinfo" 要素は
含んではいけません - 例えば "joe:password@example.com" は不適切です。


プロキシ
========

**urllib** は自動でプロキシ設定を認識して使います。これは通常の
handler の組に含まれる "ProxyHandler" を通して行なわれます。たいていの
場合はこれでうまくいきますが、役に立たない場合もあります [5]。この問題
に対処する一つの方法はプロキシを定義しない "ProxyHandler" を組み立てる
ことです。この方法は Basic Authentication handler を設定したときと同じ
ような流れで行うことができます:

   >>> proxy_support = urllib.request.ProxyHandler({})
   >>> opener = urllib.request.build_opener(proxy_support)
   >>> urllib.request.install_opener(opener)

注釈:

  現在 "urllib.request" はプロキシ経由で "https" ロケーションを取得す
  る機能をサポートしていません。しかし、urllib.request をこのレシピ
  [6] で拡張することで可能にすることができます。

注釈:

  変数 "REQUEST_METHOD" が設定されている場合、 "HTTP_PROXY" は無視され
  ます; "getproxies()" のドキュメンテーションを参照してください。


ソケットとレイヤー
==================

Web 上からリソースを取得する Python の機能はレイヤー化されています。
urllib は "http.client" ライブラリを利用していますが、そのライブラリは
さらに socket ライブラリを利用しています。

Python 2.3 ではレスポンスがタイムアウトするまでのソケットの待ち時間を
指定することができます。これは web ページを取得する場合に便利に使うこ
とができます。socket モジュールのデフォルトでは *タイムアウトが無く*
ハングしてしまうかもしれません。現在では socket のタイムアウトは
http.client や urllib.request のレベルからは隠蔽されていています。しか
し、以下を利用することで全てのソケットに対してグローバルにデフォルトタ
イムアウトを設定することができます

   import socket
   import urllib.request

   # 秒単位のタイムアウト
   timeout = 10
   socket.setdefaulttimeout(timeout)

   # この urllib.request.urlopen の呼び出しは、socket モジュールに
   # 設定したデフォルトのタイムアウトを使う
   req = urllib.request.Request('http://www.voidspace.org.uk')
   response = urllib.request.urlopen(req)

======================================================================


脚注
====

このドキュメントは John Lee によって査読、改訂されました。

[1] 例えば Google です。

[2] ブラウザを検知すること (browser sniffing) は web サイトのデザイン
    におけるとても悪い習慣です - web 標準を利用する方が賢明でしょう。
    不幸なことに未だに多くの web サイトが異なるブラウザに対して異なる
    バージョンを返しています。

[3] MSIE 6 のユーザエージェントは *'Mozilla/4.0 (compatible; MSIE 6.0;
    Windows NT 5.1; SV1; .NET CLR 1.1.4322)'* です。

[4] HTTP リクエストヘッダの詳細については、 Quick Reference to HTTP
    Headers を参照して下さい。

[5] 私の場合は仕事中にインターネットにアクセスするにはプロキシを利用す
    る必要があります。*localhost* の URL に対してこのプロキシを経由し
    てアクセスしようとすれば、ブロックされます。IE を proxy を利用する
    ように設定すれば、urllib はその情報を利用します。localhost のサー
    バでスクリプトをテストしようとすると、urllib がプロキシを利用する
    のを止めなければいけません。

[6] SSL プロキシ (CONNECT メソッド) 用の urllib opener: ASPN Cookbook
    Recipe。
