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

著者:
   Michael Foord

注釈:

  この HOWTO の古いバージョンのフランス語訳が urllib2 - Le Manuel
  manquant で見られます。


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


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

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

* Basic Authentication

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

**urllib.request** は URLs (Uniform Resource Locators) を取得するため
の Python モジュールです。このモジュールはとても簡単なインターフェース
を *urlopen* 関数の形式で提供しています。また、このモジュールは一般的
な状況で利用するためにいくらか複雑なインターフェースも提供しています -
basic 認証やクッキー、プロキシ等。これらは handler や opener と呼ばれ
るオブジェクトとして提供されます。

urllib.request は多くの "URL スキーム" (URL の ":" の前の文字列で識別
されるもの - 例えば "ftp://python.org/" では "ftp") の 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://www.voidspace.org.uk')
   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 should be 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
リクエストでもデータ自身をエンコーディングすることでデータを渡すことが
できます。

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

   >>> 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 URLs の特定の状況で送出
されます。

例外クラスは "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' (page not found),
'403' (request forbidden) と '401' (authentication required) が含まれ
ます。

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

送出された "HTTPError" インスタンスは整数の 'code' 属性を持っていて、
サーバーによって送られた応答に対応しています。


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

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

"http.server.BaseHTTPRequestHandler.responses" は **RFC 2616** で利用
されるレスポンスコード全てを示す便利な辞書です。この辞書は便利なのでこ
こに載せておきます

   # Table mapping response codes to messages; entries have the
   # form {code: (shortmessage, longmessage)}.
   responses = {
       100: ('Continue', 'Request received, please continue'),
       101: ('Switching Protocols',
             'Switching to new protocol; obey Upgrade header'),

       200: ('OK', 'Request fulfilled, document follows'),
       201: ('Created', 'Document created, URL follows'),
       202: ('Accepted',
             'Request accepted, processing continues off-line'),
       203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
       204: ('No Content', 'Request fulfilled, nothing follows'),
       205: ('Reset Content', 'Clear input form for further input.'),
       206: ('Partial Content', 'Partial content follows.'),

       300: ('Multiple Choices',
             'Object has several resources -- see URI list'),
       301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
       302: ('Found', 'Object moved temporarily -- see URI list'),
       303: ('See Other', 'Object moved -- see Method and URL list'),
       304: ('Not Modified',
             'Document has not changed since given time'),
       305: ('Use Proxy',
             'You must use proxy specified in Location to access this '
             'resource.'),
       307: ('Temporary Redirect',
             'Object moved temporarily -- see URI list'),

       400: ('Bad Request',
             'Bad request syntax or unsupported method'),
       401: ('Unauthorized',
             'No permission -- see authorization schemes'),
       402: ('Payment Required',
             'No payment -- see charging schemes'),
       403: ('Forbidden',
             'Request forbidden -- authorization will not help'),
       404: ('Not Found', 'Nothing matches the given URI'),
       405: ('Method Not Allowed',
             'Specified method is invalid for this server.'),
       406: ('Not Acceptable', 'URI not available in preferred format.'),
       407: ('Proxy Authentication Required', 'You must authenticate with '
             'this proxy before proceeding.'),
       408: ('Request Timeout', 'Request timed out; try again later.'),
       409: ('Conflict', 'Request conflict.'),
       410: ('Gone',
             'URI no longer exists and has been permanently removed.'),
       411: ('Length Required', 'Client must specify Content-Length.'),
       412: ('Precondition Failed', 'Precondition in headers is false.'),
       413: ('Request Entity Too Large', 'Entity is too large.'),
       414: ('Request-URI Too Long', 'URI is too long.'),
       415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
       416: ('Requested Range Not Satisfiable',
             'Cannot satisfy request range.'),
       417: ('Expectation Failed',
             'Expect condition could not be satisfied.'),

       500: ('Internal Server Error', 'Server got itself in trouble'),
       501: ('Not Implemented',
             'Server does not support this operation'),
       502: ('Bad Gateway', 'Invalid responses from another server/proxy.'),
       503: ('Service Unavailable',
             'The server cannot process the request due to a high load'),
       504: ('Gateway Timeout',
             'The gateway server did not receive a timely response'),
       505: ('HTTP Version Not Supported', 'Cannot fulfill request.'),
       }

エラーが起きた場合、サーバーは 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:
       # everything is fine

注釈:

  "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)" を呼び出します。

それに代わる方法として、 "build_opener" を利用することもできます、これ
は opener オブジェクトを一回の関数呼び出しで作成できる便利な関数です。
"build_opener" はいくつかのハンドラをデフォルトで追加しますが、デフォ
ルトのハンドラに対して追加、継承のどちらかまたは両方を行うのに手っ取り
早い方法を提供してくれます。

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

"install_opener" も (グローバルな) デフォルト "opener" オブジェクトの
作成に利用できます。つまり、 "urlopen" を呼び出すと、インストールした
opener が利用されます。

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


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

ハンドラの作成とインストールを例示するのに、 "HTTPBasicAuthHandler" を
利用してみます。この話題についてのより詳しい議論は -- Basic 認証がどう
やって動作するのかの説明も含んでいる Basic Authentication Tutorial を
参照して下さい。

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

例

   WWW-Authenticate: Basic realm="cPanel Users"

クライアントはリクエストヘッダに含まれる realm に対して適切な名前とパ
スワードとともにリクエストを再試行する必要があります。これが 'basic 認
証' です。一連の流れを簡単化するために、 "HTTPBasicAuthHandler" のイン
スタンスを作成し、 opener が handler を利用するようにします。

"HTTPBasicAuthHandler" はパスワードマネージャーと呼ばれる、 URL と
realm をパスワードとユーザ名への対応づけを処理する、オブジェクトを利用
します。 realm が何なのか(サーバから返される認証ヘッダから) 知りたい場
合には、 "HTTPPasswordMgr" を利用できます。多くの場合、realm が何なの
かについて気にすることはありません。そのような場合には
"HTTPPasswordMgrWithDefaultRealm" を使うと便利です。これは URL に対し
てデフォルトのユーザ名とパスワードを指定できます。これによって特定の
realm に対する代替の組み合わせを提供することなしに利用できるようになり
ます。このことは "add_password" メソッドの realm 引数として "None" を
与えることで明示することができます。

トップレベルの URL が認証が必要なはじめに URL です。この URL よりも「
深い」URL を渡しても .add_password() は同様にマッチします。:

   # create a password manager
   password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()

   # Add the username and password.
   # If we knew the realm, we could use it instead of None.
   top_level_url = "http://example.com/foo/"
   password_mgr.add_password(None, top_level_url, username, password)

   handler = urllib.request.HTTPBasicAuthHandler(password_mgr)

   # create "opener" (OpenerDirector instance)
   opener = urllib.request.build_opener(handler)

   # use the opener to fetch a URL
   opener.open(a_url)

   # Install the opener.
   # Now all calls to urllib.request.urlopen use our 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 in seconds
   timeout = 10
   socket.setdefaulttimeout(timeout)

   # this call to urllib.request.urlopen now uses the default timeout
   # we have set in the socket module
   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] urllib opener for SSL proxy (CONNECT method): ASPN Cookbook
    Recipe.
