Python urllib/urllib2

ここでは、標準モジュールのみを使って、前節で示したrequestsモジュールを使ったスクリプトと同等の機能をもつpython スクリプトを作って見ます。

Python3 のurllib.request モジュールあるいは Python2 の urllib2 モジュールは、 HTTP リクエストを表現するRequestクラスを持っています。 この Request クラスで POST メソッドのリクエストを送信すると、 既定の Content-type としてapplication/x-www-form-urlencodedが設定されます。 POST リクエストの内容をmultipart/form-dta形式で送信するためには、 明示的にContent-typeとして、multipart/form-data を指定し、 データ本体部分を mulitipart 形式にそって用意する必要があります。 このmultipart形式のデータは email.mime のMIMEmultipartクラスを利用して作成できます1 。 また、バウンダリ文字列文字列もこのクラスの:py:func`get_boundary`関数を使って生成することができます。 MIMEMultipartは、as_bytes()メソッド呼び出しなどの際にboundary文字列を生成します。

Requestオブジェクトのデータ本体部分(.data) はbytes列として作成する必要があります。 ここで、Python3/Python2のバイト列/文字列/ユニコードの対応の違いが問題になります。具体的には、email.mime以下のMIME型に対応するクラスはPython3ではbyte列を値とするas_bytesメソッドを持ちますが、Python2のそれは as_bytesメソッドを持たず、byte列(=str)を返すのはas_stringメソッドとなっています。 作成したプログラムでは、Python2で実行された場合、as_string の同意語として as_bytes を定義して、この違いを吸収しています。 このプログラムでは、Requestオブジェクトのデータ本体部分(.data) は MIMEMultipart オブジェクトでの as_bytes メソッドで作成されたバイト列を使わずに、生成されたバウンダリ文字列と MIMIEMultipart オブジェクトに追加した MIME 要素から作成される bytes 列および画像データの bytes 列から直接に生成しています。 これは、バイナリデータを含むMIMEオブジェクトをas_bytes(as_string)メソッドで変換した場合、バイナリデータが正しく送信されないことがあったための迂回策です2

このような制限を考慮して、作成したスクリプトを次に示します。

ここからダウンロード

1

email.MIMEモジュールは e-mail でのMIME利用を前提に考えれらているため、画像データはbase64でエンコードするのが既定になっているなど、HTTPで使うためには少し注意が必要なことがあります。

2

as_bytes()メソッドで展開されたバイナリデータは、"\r" あるいは "\n"が ポリシーに指定されている 改行文字列("\r\n"など)に変換されてしまいます。 MIMEmessageオブジェクトのオプションでこれを避けることができるのかもしれませんが、今の所不明です。 Pythonの公式ドキュメント[ref:python_Email_message]にも "Note A cte of binary does not actually work correctly yet. The EmailMessage object as modified by set_content is correct, but BytesGenerator does not serialize it correctly. "とあるのはこの事かもしれません。

リスト 3 urllib/urllib2 を用いた画像ファイルアップローダ用python スクリプト.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#!python3
#-*- coding:utf-8 -*-
"""
"""
import time, sys, os.path
import getpass
#
from email.encoders import encode_noop, encode_7or8bit
from email.message import EmailMessage, MIMEPart
from email.policy import HTTP as HTTP_policy

from urllib.request import urlopen, Request

Uloader_url = 'http://webserver.xxx.yyy/ScrShot/upload.php'

def UploadScrShot3(f, title):
    #check if file named f exists
    if not os.path.isfile(f):
        raise RuntimeError("file %s does not exist"%f)
    fext=os.path.splitext(f)[1][1:]
    # clear HTTP_PROXY setting 
    os.environ['http_proxy']=""
    os.environ['HTTP_PROXY']=""
    #
    request = Request(Uloader_url)
    request.add_header('User-Agent',"Python-urllib")
    request.add_header('Accept-Encoding', 'gzip, deflate')
    request.add_header('Accept', '*/*')
    request.add_header('Connection', 'keep-alive')
    # parameters passed to uploader
    ts=time.localtime()
    raw_params = dict(
        year="%4d"%ts.tm_year, 
        month="%02d"%ts.tm_mon,
        day="%02d"%ts.tm_mday,
        Comment=title,
        )
    #
    #prepara payload as MIME/Multipart form data.
    mp=MIMEPart(policy=HTTP_policy)
    mp.add_header("Content-Type", "multipart/form-data")
    for k,v in raw_params.items():
        t=MIMEPart(policy=HTTP_policy)
        t.set_content(v, "plain",
                      charset="utf-8",cte="8bit",
                      disposition="form-data")
        t.set_param("name", k, header="Content-Disposition")
        mp.attach(t)
    raw_image=open(f,"rb").read()
    img=MIMEPart(policy=HTTP_policy)
    img.set_content(raw_image, "image", fext, 
                    cte="8bit", disposition="form-data", 
                    )
    img.set_param("name", "UserFile", header="Content-Disposition")
    img.set_param("filename", "image.png", header="Content-Disposition")
    mp.attach(img)
    #generate boundary delimeter string
    b=mp.as_bytes(False) # 
    boundary=mp.get_boundary()
    #
    request.add_header(
        'Content-Type',
        'multipart/form-data; boundary=\"{boundary}\"'.format(boundary=boundary)
        )
    # Python document says:
    #  Note A cte of binary does not actually work correctly yet. 
    #   The EmailMessage object as modified by set_content is correct, 
    #   but BytesGenerator does not serialize it correctly. 
    # rebuild request data.
    img.set_payload(b"") # clear raw image data.
    request.data = b""
    for e in mp.get_payload():
        request.data +=("\r\n--{boundary}\r\n".format(boundary=boundary)).encode()
        request.data +=e.as_bytes(False).rstrip()
    # here we assume that img is the last element of mp.get_payload()
    request.data += b"\r\n\r\n"
    request.data += raw_image
    
    request.data += ("\r\n--{boundary}--\r\n".format(boundary=boundary)).encode()
    request.add_header("Content-Length", len(request.data))
    
    res=urlopen(request)
    return