HTTPリクエストで画像をPOST送信

普段formを使っているとPOSTでどうやってパラメータが送信されてるとかあんまり意識しないので、Pythonで画像ファイルをPOST経由で送ろうとしたら一瞬で詰んだ。
とりあえずはWiresharkとか使ってHTTPモニタリングして、どんなパケットを送受信してるか観察すると実感が湧くのでやってみるといいと思う。

Pythonでサンプルコード。

#!/usr/bin/env python
# coding: utf-8

from poster.encode import multipart_encode
from poster.streaminghttp import register_openers
import urllib2

if __name__ == '__main__':
	# Register streaming http handlers to urllib2 global object
	register_openers()

	with open("image.jpg", "rb") as f:
		# Start the multipart/form-data encoding of the file
		# @headers: necessary Content-Type and Content-Length
		# @datagen: generator yielding the encoded parameters
		datagen, headers = multipart_encode({"file": f})

		request = urllib2.Request("http://example.com/", datagen, headers)
		response = urllib2.urlopen(request)
		print "---------- RESPONSE HEAD ----------"
		print response.info()
		print "---------- RESPONSE BODY ----------"
		print response.read()

今回はposterというモジュールを使ってみた。pipでインストールできたと思う。
サンプルソースも公式ドキュメントを参考にした(というかほとんどそのまま)。
【参考】Welcome to poster’s documentation! — poster v0.8.1 documentation

で、これで適当な画像をPOSTしたのをWiresharkでキャプチャした結果の抜粋。
f:id:levelfour:20140730225233p:plain

この中で注目すべき点は2つ。

1. Content-Typeがmultipart/form-data
2. User-AgentがPython-urllib

まずはContent-Type。multipart/form-dataっていうのはMIME TYPEの1つで、formの送信などで複数のデータをまとめて送信したいときに使うらしい。
詳しいことはググるといろいろ出るので、要点だけを絞る。
直後にboundaryというのがあって、これをバウンダリ文字列という。
バウンダリ文字列とは何かというと、読んで字のごとく境界線のこと。つまり、送信する複数のデータの区切りになる文字列だ。
そのため、バウンダリ文字列はデータ中に現れてはいけない。通常はブラウザやライブラリによってランダムに生成される。ここでもposterが勝手に生成してくれた、衝突しそうにない無難な乱文字列だ。

POSTするデータの実体のフォーマットの一例はこうなる。

--[boundary]\r\n
Content-Disposition: form-data; name="[name]"; filename="[filename]";\r\n
Content-Type: image/jpeg\r\n
\r\n
[画像のバイナリ列]
\r\n
--[boundary]\r\n
......

そして、複数のデータをバウンダリ文字列ですべて区切って、最後に「--[boundary]--\r\n」で締めくくる。

この辺の雑事をすべてラップしてくれているのがposterだ。もっと低層のモジュールが、2番目に注目すべきと言ったurllibだ。
PythonでHTTPリクエストをするには、基本的にはurllibを使うことになる。
その際は上で述べたバウンダリ文字列等を含んだPOSTデータを自前生成する必要が出てくる。
実用上は便利なモジュールを使えばよいが、やはり一度理解しておきたいという人はここを読んで自分でスクリプトを叩いて実感してみるとよいだろう。
【参考】urllib2 – Library for opening URLs. - Python Module of the Week

おまけになるが、今回同じことを今流行りのSwiftでやってみた。
SwiftにはまだHTTPリクエスト用のサードパーティーライブラリの整備が間に合っていない(そもそも正規の開発環境がまだ整っていないのだから当然だ)ようだが、Objective-C以来のNSURLSessionを使うことで無事POSTリクエストできた。
参考までに載せておく。

func requestWithImageFile(filename: NSString) {
        let url = NSURL(string: SERVER_URL)
        let boundary = NSString(format: "%d", arc4random() %
10000000)
        let config = NSURLSessionConfiguration.defaultSession
Configuration()
        config.HTTPAdditionalHeaders = ["Content-Type": NSStr
ing(format: "multipart/form-data; boundary=%@", boundary)]
        var request = NSMutableURLRequest(URL: url)
        let session = NSURLSession(configuration: config)

        let image = NSData(contentsOfFile: filename)

        if image != nil {
                let post = NSMutableData.data()
                post.appendData(NSString(format: "--%@\r\n", boundary).dataUsingEncoding(NSUTF8StringEncoding))
                post.appendData(NSString(string: "Content-Disposition: form-data;").dataUsingEncoding(NSUTF8StringEncoding))
                post.appendData(NSString(format: "name=\"%@\";", "file").dataUsingEncoding(NSUTF8StringEncoding))
                post.appendData(NSString(format: "filename=\"%@\"\r\n", filename).dataUsingEncoding(NSUTF8StringEncoding))
                post.appendData(NSString(string: "Content-Type: image/jpeg\r\n\r\n").dataUsingEncoding(NSUTF8StringEncoding))
                post.appendData(image)
                post.appendData(NSString(string: "\r\n").dataUsingEncoding(NSUTF8StringEncoding))
                post.appendData(NSString(format: "--%@--\r\n", boundary).dataUsingEncoding(NSUTF8StringEncoding))

                request.HTTPMethod = "POST"
                request.HTTPBody = post
                let task: NSURLSessionDataTask = session.dataTaskWithRequest(request, completionHandler: { data, request, error in println(NSString(format: "<result=\"%@\">", NSString(data: data, encoding: NSUTF8StringEncoding))) })
                task.resume()
        } else {
                let alert = UIAlertView(title: "Alert", message: "draw something in canvas first", delegate: self, cancelButtonTitle: "OK")
                alert.show()
        }
}