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

はじめに

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 によって査読、改訂されました。