urllib2 – URL をオープンするライブラリ

目的:カスタムプロトコルハンドラを定義して拡張できる URL をオープンするライブラリ
利用できるバージョン:2.1

urllib2 モジュールは、URL で識別するインターネットリソースのために拡張された API です。個別のアプリケーションで新たなプロトコルをサポートしたり、(HTTP ベーシック認証を処理するといった)既存のプロトコルに修正を加えたりして拡張することを意図して設計されています。

HTTP GET

Note

この記事のサンプルのテストサーバは BaseHTTPServer モジュールのサンプルにある BaseHTTPServer_GET.py です。ターミナルでサーバを起動して、それとは別のターミナルでこの記事のサンプルを実行してください。

urllib と同様に HTTP GET の操作は urllib2 の最も簡単な使用法です。リモートのデータを処理する “ファイルのような” オブジェクトを取得するために urlopen() へ URL を渡してください。

import urllib2

response = urllib2.urlopen('http://localhost:8080/')
print 'RESPONSE:', response
print 'URL     :', response.geturl()

headers = response.info()
print 'DATE    :', headers['date']
print 'HEADERS :'
print '---------'
print headers

data = response.read()
print 'LENGTH  :', len(data)
print 'DATA    :'
print '---------'
print data

このサンプルサーバは、入力データを受け取って、プレーンテキストでレスポンスを返します。 urlopen() からの返り値に対して、 info() を通して HTTP サーバからのヘッダへ、 read()readlines() といったメソッドを通してリモートリソースのデータへのアクセスします。

$ python urllib2_urlopen.py
RESPONSE: <addinfourl at 11940488 whose fp = <socket._fileobject object at 0xb573f0>>
URL     : http://localhost:8080/
DATE    : Sun, 19 Jul 2009 14:01:31 GMT
HEADERS :
---------
Server: BaseHTTP/0.3 Python/2.6.2
Date: Sun, 19 Jul 2009 14:01:31 GMT

LENGTH  : 349
DATA    :
---------
CLIENT VALUES:
client_address=('127.0.0.1', 55836) (localhost)
command=GET
path=/
real path=/
query=
request_version=HTTP/1.1

SERVER VALUES:
server_version=BaseHTTP/0.3
sys_version=Python/2.6.2
protocol_version=HTTP/1.0

HEADERS RECEIVED:
accept-encoding=identity
connection=close
host=localhost:8080
user-agent=Python-urllib/2.6

urlopen() が返すファイルのようなオブジェクトは繰り返し処理できます。

import urllib2

response = urllib2.urlopen('http://localhost:8080/')
for line in response:
    print line.rstrip()

このサンプルは、出力を表示する前にその文字列の改行コードを取り除いています。

$ python urllib2_urlopen_iterator.py
CLIENT VALUES:
client_address=('127.0.0.1', 55840) (localhost)
command=GET
path=/
real path=/
query=
request_version=HTTP/1.1

SERVER VALUES:
server_version=BaseHTTP/0.3
sys_version=Python/2.6.2
protocol_version=HTTP/1.0

HEADERS RECEIVED:
accept-encoding=identity
connection=close
host=localhost:8080
user-agent=Python-urllib/2.6

エンコード引数

urllib.urlencode() で引数をエンコードして URL に追加することでサーバへ引数が渡せます。

import urllib
import urllib2

query_args = { 'q':'query string', 'foo':'bar' }
encoded_args = urllib.urlencode(query_args)
print 'Encoded:', encoded_args

url = 'http://localhost:8080/?' + encoded_args
print urllib2.urlopen(url).read()

CLIENT VALUES のリストにエンコードされたクエリ引数が含まれています。

$ python urllib2_http_get_args.py
Encoded: q=query+string&foo=bar
CLIENT VALUES:
client_address=('127.0.0.1', 55849) (localhost)
command=GET
path=/?q=query+string&foo=bar
real path=/
query=q=query+string&foo=bar
request_version=HTTP/1.1

SERVER VALUES:
server_version=BaseHTTP/0.3
sys_version=Python/2.6.2
protocol_version=HTTP/1.0

HEADERS RECEIVED:
accept-encoding=identity
connection=close
host=localhost:8080
user-agent=Python-urllib/2.6

HTTP POST

Note

この記事のサンプルのテストサーバは BaseHTTPServer モジュールのサンプルにある BaseHTTPServer_POST.py です。ターミナルでサーバを起動して、それとは別のターミナルでこの記事のサンプルを実行してください。

GET ではなく、リモートサーバへフォームでエンコードされたデータを POST するには、エンコードされたクエリ引数を urlopen() へのデータとして渡してください。

import urllib
import urllib2

query_args = { 'q':'query string', 'foo':'bar' }
encoded_args = urllib.urlencode(query_args)
url = 'http://localhost:8080/'
print urllib2.urlopen(url, encoded_args).read()

このサーバはフォームデータをデコードして名前で個々のデータにアクセスできます。

$ python urllib2_urlopen_post.py
Client: ('127.0.0.1', 55943)
User-agent: Python-urllib/2.6
Path: /
Form data:
    q=query string
    foo=bar

リクエストを直接処理する

urlopen() は、リクエストがどうやって作られて処理されているかの詳細を隠蔽してくれる便利な関数です。より詳細な制御をするには Request オブジェクトを直接インスタンス化して使用すると良いです。

外部向けのヘッダを追加する

前説で説明したサンプルのように、デフォルトの User-agent ヘッダの値は、定数 Python-urllib に続く Python インタープリタのバージョンで構成されます。他人が管理している web リソースへアクセスするアプリケーションを開発しているなら、簡単にアクセス元が分かるのでリクエスト情報の中に本当のユーザエージェント情報を含めるのがお作法です。カスタムエージェントを使用すると robots.txt ファイルでクローラを制御することもできます(robotparser を参照)。

import urllib2

request = urllib2.Request('http://localhost:8080/')
request.add_header('User-agent', 'PyMOTW (http://www.doughellmann.com/PyMOTW/)')

response = urllib2.urlopen(request)
data = response.read()
print data

Request オブジェクトの作成後、そのリクエストをオープンする前にユーザエージェントの値をセットするには add_header() を使用してください。この結果出力の最後の行はカスタム値を表示します。

$ python urllib2_request_header.py
CLIENT VALUES:
client_address=('127.0.0.1', 55876) (localhost)
command=GET
path=/
real path=/
query=
request_version=HTTP/1.1

SERVER VALUES:
server_version=BaseHTTP/0.3
sys_version=Python/2.6.2
protocol_version=HTTP/1.0

HEADERS RECEIVED:
accept-encoding=identity
connection=close
host=localhost:8080
user-agent=PyMOTW (http://www.doughellmann.com/PyMOTW/)

フォームデータを POST する

サーバへデータを POST する Request にそのデータをセットできます。

import urllib
import urllib2

query_args = { 'q':'query string', 'foo':'bar' }

request = urllib2.Request('http://localhost:8080/')
print 'Request method before data:', request.get_method()

request.add_data(urllib.urlencode(query_args))
print 'Request method after data :', request.get_method()
request.add_header('User-agent', 'PyMOTW (http://www.doughellmann.com/PyMOTW/)')

print
print 'OUTGOING DATA:'
print request.get_data()

print
print 'SERVER RESPONSE:'
print urllib2.urlopen(request).read()

Request が使用する HTTP メソッドは、自動的にデータが追加された後で GET から POST へ変更します。

$ python urllib2_request_post.py
Request method before data: GET
Request method after data : POST

OUTGOING DATA:
q=query+string&foo=bar

SERVER RESPONSE:
Client: ('127.0.0.1', 56044)
User-agent: PyMOTW (http://www.doughellmann.com/PyMOTW/)
Path: /
Form data:
    q=query string
    foo=bar

Note

add_data() というメソッド名ですが、この処理はデータを追加 しません 。呼び出す毎にその前のデータは置き換えられます。

ファイルをアップロードする

アップロード用にファイルをエンコードすることは、シンプルなフォームよりも一手間あります。サーバがアップロードファイルからフォームフィールドを判別できるように、リクエスト本文に完全な MIME メッセージを作成する必要があります。

import itertools
import mimetools
import mimetypes
from cStringIO import StringIO
import urllib
import urllib2

class MultiPartForm(object):
    """Accumulate the data to be used when posting a form."""

    def __init__(self):
        self.form_fields = []
        self.files = []
        self.boundary = mimetools.choose_boundary()
        return
    
    def get_content_type(self):
        return 'multipart/form-data; boundary=%s' % self.boundary

    def add_field(self, name, value):
        """Add a simple field to the form data."""
        self.form_fields.append((name, value))
        return

    def add_file(self, fieldname, filename, fileHandle, mimetype=None):
        """Add a file to be uploaded."""
        body = fileHandle.read()
        if mimetype is None:
            mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
        self.files.append((fieldname, filename, mimetype, body))
        return
    
    def __str__(self):
        """Return a string representing the form data, including attached files."""
        # リクエストの "行" を含むリストを作成する
        # それぞれのパートはバウンダリ文字列で分割される
        # リストが作成されると '\r\n' で区切られた行を返す
        parts = []
        part_boundary = '--' + self.boundary
        
        # フォームフィールドを追加する
        parts.extend(
            [ part_boundary,
              'Content-Disposition: form-data; name="%s"' % name,
              '',
              value,
            ]
            for name, value in self.form_fields
            )
        
        # アップロードするファイルを追加する
        parts.extend(
            [ part_boundary,
              'Content-Disposition: file; name="%s"; filename="%s"' % \
                 (field_name, filename),
              'Content-Type: %s' % content_type,
              '',
              body,
            ]
            for field_name, filename, content_type, body in self.files
            )
        
        # リストにしてバウンダリ文字列をクローズするマーカーを
        # 追加してから CR+LF で分割されたデータを返す
        flattened = list(itertools.chain(*parts))
        flattened.append('--' + self.boundary + '--')
        flattened.append('')
        return '\r\n'.join(flattened)

if __name__ == '__main__':
    # シンプルなフィールドでフォームを作成する
    form = MultiPartForm()
    form.add_field('firstname', 'Doug')
    form.add_field('lastname', 'Hellmann')
    
    # うそのファイルを追加する
    form.add_file('biography', 'bio.txt', 
                  fileHandle=StringIO('Python developer and blogger.'))

    # リクエストを作成する
    request = urllib2.Request('http://localhost:8080/')
    request.add_header('User-agent', 'PyMOTW (http://www.doughellmann.com/PyMOTW/)')
    body = str(form)
    request.add_header('Content-type', form.get_content_type())
    request.add_header('Content-length', len(body))
    request.add_data(body)

    print
    print 'OUTGOING DATA:'
    print request.get_data()

    print
    print 'SERVER RESPONSE:'
    print urllib2.urlopen(request).read()

MultiPartForm クラスは、添付ファイルのマルチパート MIME メッセージのように任意のフォームを表せます。

$ python urllib2_upload_files.py

OUTGOING DATA:
--192.168.1.17.527.30074.1248020372.206.1
Content-Disposition: form-data; name="firstname"

Doug
--192.168.1.17.527.30074.1248020372.206.1
Content-Disposition: form-data; name="lastname"

Hellmann
--192.168.1.17.527.30074.1248020372.206.1
Content-Disposition: file; name="biography"; filename="bio.txt"
Content-Type: text/plain

Python developer and blogger.
--192.168.1.17.527.30074.1248020372.206.1--


SERVER RESPONSE:
Client: ('127.0.0.1', 57126)
User-agent: PyMOTW (http://www.doughellmann.com/PyMOTW/)
Path: /
Form data:
    lastname=Hellmann
    Uploaded biography as "bio.txt" (29 bytes)
    firstname=Doug

カスタムプロトコルハンドラ

urllib2 は HTTP(S)、FTP、ローカルファイルへのアクセスを組み込み機能でサポートします。その他の URL タイプをサポートする必要があるなら、必要に応じて実行される独自のプロトコルハンドラを登録できます。例えば、リモートの NFS サーバ上に対して、ユーザへ手動でパスをマウントさせずに任意のファイルを参照する URL をサポートしたいなら、 BaseHandler から派生したクラスと nfs_open() メソッドを作成できます。

プロトコルの open() メソッドは Request インスタンスの引数を1つだけ受け取ります。 Request インスタンスは、データを読み込むのに使用される read() メソッド、レスポンスヘッダを返す info() メソッド、読み込まれたファイルの実際の URL を返す geturl() メソッドを持つオブジェクトを返します。これを実装する簡単な方法の1つは、ヘッダ、URL、オープンファイルハンドラをコンストラクタへ渡して urllib.addurlinfo のインスタンスを作成することです。

import mimetypes
import os
import tempfile
import urllib
import urllib2

class NFSFile(file):
    def __init__(self, tempdir, filename):
        self.tempdir = tempdir
        file.__init__(self, filename, 'rb')
    def close(self):
        print
        print 'NFSFile:'
        print '  unmounting %s' % self.tempdir
        print '  when %s is closed' % os.path.basename(self.name)
        return file.close(self)

class FauxNFSHandler(urllib2.BaseHandler):
    
    def __init__(self, tempdir):
        self.tempdir = tempdir
    
    def nfs_open(self, req):
        url = req.get_selector()
        directory_name, file_name = os.path.split(url)
        server_name = req.get_host()
        print
        print 'FauxNFSHandler simulating mount:'
        print '  Remote path: %s' % directory_name
        print '  Server     : %s' % server_name
        print '  Local path : %s' % tempdir
        print '  File name  : %s' % file_name
        local_file = os.path.join(tempdir, file_name)
        fp = NFSFile(tempdir, local_file)
        content_type = mimetypes.guess_type(file_name)[0] or 'application/octet-stream'
        stats = os.stat(local_file)
        size = stats.st_size
        headers = { 'Content-type': content_type,
                    'Content-length': size,
                  }
        return urllib.addinfourl(fp, headers, req.get_full_url())

if __name__ == '__main__':
    tempdir = tempfile.mkdtemp()
    try:
        # シミュレーションのために一時ファイルを作成する
        with open(os.path.join(tempdir, 'file.txt'), 'wt') as f:
            f.write('Contents of file.txt')
        
        # NFS ハンドラの opener を作成して、
        # デフォルトの opener として登録する
        opener = urllib2.build_opener(FauxNFSHandler(tempdir))
        urllib2.install_opener(opener)

        # URL を通してファイルをオープンする
        response = urllib2.urlopen('nfs://remote_server/path/to/the/file.txt')
        print
        print 'READ CONTENTS:', response.read()
        print 'URL          :', response.geturl()
        print 'HEADERS:'
        for name, value in sorted(response.info().items()):
            print '  %-15s = %s' % (name, value)
        response.close()
    finally:
        os.remove(os.path.join(tempdir, 'file.txt'))
        os.removedirs(tempdir)

FauxNFSHandlerNFSFile クラスは、実際の実装がどこで mount と unmound を呼び出すかを理解するためにメッセージ出力します。このサンプルはただのシミュレーションなので、 FauxNFSHandler に一時ディレクトリの場所を教えて、そこにあるファイルを探します。

$ python urllib2_nfs_handler.py

FauxNFSHandler simulating mount:
  Remote path: /path/to/the
  Server     : remote_server
  Local path : /var/folders/9R/9R1t+tR02Raxzk+F71Q50U+++Uw/-Tmp-/tmppv5Efn
  File name  : file.txt

READ CONTENTS: Contents of file.txt
URL          : nfs://remote_server/path/to/the/file.txt
HEADERS:
  Content-length  = 20
  Content-type    = text/plain

NFSFile:
  unmounting /var/folders/9R/9R1t+tR02Raxzk+F71Q50U+++Uw/-Tmp-/tmppv5Efn
  when file.txt is closed

See also

urllib2
本モジュールの標準ライブラリドキュメント
urllib
URL 操作のオリジナルのライブラリ
urlparse
URL 文字列を処理する
urllib2 – The Missing Manual
Michael Foord の urllib2 の記事
Upload Scripts
HTTP でファイルをアップロードしてサーバ上でデータを受け取る方法を説明する Michael Foord のサンプルスクリプト
HTTP client to POST using multipart/form-data
HTTP でエンコード、ファイルを含めたデータの POST を行う Python クックブックのレシピ
Form content types
HTTP フォームでファイルや巨大なデータを解析する W3C の仕様
mimetypes
ファイル名と MIME タイプをマップする
mimetools
MIME メッセージを解析するツール
Bookmark and Share