最近要用 Python 模擬表單上傳文件,搜索了一下常見的解決方案。
如果只是要模擬提交一個不包含文件字段的表單,實現起來是很簡單的,但涉及到文件上傳就有一點小復雜,需要自己對文件進行編碼,或者使用第三方模塊。
如果機器上有 PycURL,那么可以使用 PycURL 來上傳文件。
不過,由於 PycURL 需要用到 curl,在 Windows 下安裝可能會有點麻煩,除 PycURL 外,也有一些其它實現 POST 文件上傳的方式,比如 這兒 的 2 樓有人貼出了一個將文件進行編碼之后再 POST 的方法,另外還有MultipartPostHandler、urllib2_file、poster 等第三方模塊。但 MultipartPostHandler 這個模塊似乎比較老了,urllib2_file 我試用了一下遇到錯誤沒有成功,這兒我想介紹的是另外一個第三方模塊 poster。
如果機器上安裝了 Python 的 setuptools,可以通過下面的命令來安裝 poster:
1 sudo easy_install poster
1 # test_client.py 2 3 from poster.encode import multipart_encode 4 from poster.streaminghttp import register_openers 5 import urllib2 6 7 # 在 urllib2 上注冊 http 流處理句柄 8 9 register_openers() 10 11 # 開始對文件 "DSC0001.jpg" 的 multiart/form-data 編碼 12 13 # "image1" 是參數的名字,一般通過 HTML 中的 <input> 標簽的 name 參數設置 14 15 # headers 包含必須的 Content-Type 和 Content-Length 16 # datagen 是一個生成器對象,返回編碼過后的參數,這里如果有多個參數的話依次添加即可 17 18 datagen, headers = multipart_encode({"image1": open("DSC0001.jpg", "rb")}) 19 20 # 創建請求對象 21 22 request = urllib2.Request("http://localhost:5000/upload_image", datagen, headers) 23 24 # 實際執行請求並取得返回 25 26 print urllib2.urlopen(request).read()
很簡單,文件就上傳完成了。
其中那個 register_openers() 相當於以下操作:
1 from poster.encode import multipart_encode 2 from poster.streaminghttp import StreamingHTTPHandler, StreamingHTTPRedirectHandler, StreamingHTTPSHandler 3 4 handlers = [StreamingHTTPHandler, StreamingHTTPRedirectHandler, StreamingHTTPSHandler] 5 6 opener = urllib2.build_opener(*handlers) 7 8 urllib2.install_opener(opener)
另外,poster 也可以攜帶 cookie,比如:
1 opener = poster.streaminghttp.register_openers() 2 3 opener.add_handler(urllib2.HTTPCookieProcessor(cookielib.CookieJar())) 4 5 params = {'file': open("test.txt", "rb"), 'name': 'upload test'} 6 7 datagen, headers = poster.encode.multipart_encode(params) 8 9 request = urllib2.Request(upload_url, datagen, headers) 10 11 result = urllib2.urlopen(request)
如果在上傳過程中遇到Authorization問題:可以自己定義register_openers()方法加入用戶驗證的handler,例如:
1 from poster.encode import multipart_encode 2 3 from poster.streaminghttp import StreamingHTTPHandler, StreamingHTTPRedirectHandler, StreamingHTTPSHandler 4 5 6 7 # create a password manager 8 9 password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() 10 11 # Add the username and password. 12 13 # If we knew the realm, we could use it instead of None. 14 15 # add_password('realm','url','username','password') 16 17 password_mgr.add_password('realm', 'url', 'username', 'password') 18 19 handler = urllib2.HTTPBasicAuthHandler(password_mgr) 20 21 handlers = [handler,StreamingHTTPHandler, StreamingHTTPRedirectHandler, StreamingHTTPSHandler] 22 23 opener = urllib2.build_opener(*handlers) 24 25 urllib2.install_opener(opener)
或者直接將上面的內容加到上傳文件的代碼中。
今天突然圖片不能上傳了,發現上面的認證方式失效了。很是奇怪。於是再來一種解決方案。
其實問題就是解決python環境下HTTP Basic Authorization 的問題
首先我們要理解HTTP基本認證的原理
在HTTP中,基本認證是一種用來允許Web瀏覽器或其他客戶端程序在請求時提供用戶名和口令形式的身份憑證的一種登錄驗證方式。
在發送之前是以用戶名追加一個冒號然后串接上口令,並將得出的結果字符串再用Base64算法編碼。例如,提供的用戶名是Aladdin、口令是open sesame,則拼接后的結果就是Aladdin:open sesame,然后再將其用Base64編碼,得到QWxhZGRpbjpvcGVuIHNlc2FtZQ==。最終將Base64編碼的字符串發送出去,由接收者解碼得到一個由冒號分隔的用戶名和口令的字符串。
雖然對用戶名和口令的Base64算法編碼結果很難用肉眼識別解碼,但它仍可以極為輕松地被計算機所解碼,就像其容易編碼一樣。編碼這一步驟的目的並不是安全與隱私,而是為將用戶名和口令中的不兼容的字符轉換為均與HTTP協議兼容的字符集。
例子
這一個典型的HTTP客戶端和HTTP服務器的對話,服務器安裝在同一台計算機上(localhost),包含以下步驟:
- 客戶端請求一個需要身份認證的頁面,但是沒有提供用戶名和口令。這通常是用戶在地址欄輸入一個URL,或是打開了一個指向該頁面的鏈接。
- 服務端響應一個401應答碼,並提供一個認證域。
- 接到應答后,客戶端顯示該認證域(通常是所訪問的計算機或系統的描述)給用戶並提示輸入用戶名和口令。此時用戶可以選擇確定或取消。
- 用戶輸入了用戶名和口令后,客戶端軟件會在原先的請求上增加認證消息頭(值是base64encode(username+":"+password)),然后重新發送再次嘗試。
- 在本例中,服務器接受了該認證屏幕並返回了頁面。如果用戶憑據非法或無效,服務器可能再次返回401應答碼,客戶端可以再次提示用戶輸入口令。
注意:客戶端有可能不需要用戶交互,在第一次請求中就發送認證消息頭。
客戶端請求(沒有認證信息):
1 GET /private/index.html HTTP/1.0 2 3 Host: localhost
(跟隨一個換行 ,以回車(CR) 加換行(LF) 的形式)
服務端應答:
1 HTTP/1.0 401 Authorization Required 2 Server: HTTPd/1.0 3 Date: Sat, 27 Nov 2004 10:18:15 GMT 4 WWW-Authenticate: Basic realm="Secure Area" 5 Content-Type: text/html 6 Content-Length: 311 7 8 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 9 "http://www.w3.org/TR/1999/REC-html401-19991224/loose.dtd"> 10 <HTML> 11 <HEAD> 12 <TITLE>Error</TITLE> 13 <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=ISO-8859-1"> 14 </HEAD> 15 <BODY><H1>401 Unauthorized.</H1></BODY> 16 </HTML>
客戶端的請求(用戶名“"Aladdin”,口令, password “open sesame”) :
1 GET /private/index.html HTTP/1.0 2 Host: localhost 3 Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
(跟隨一個空行,如上所述)
Authorization消息頭的用戶名和口令的值可以容易地編碼和解碼:
服務端的應答 :
1 HTTP/1.0 200 OK 2 Server: HTTPd/1.0 3 Date: Sat, 27 Nov 2004 10:19:07 GMT 4 Content-Type: text/html 5 Content-Length: 10476
所以,用python解決問題的話:
1 import urllib2 2 import sys 3 import re 4 import base64 5 from urlparse import urlparse 6 7 theurl = 'http://api.minicloud.com.cn/statuses/friends_timeline.xml' 8 9 username = 'qleelulu' 10 password = 'XXXXXX' # 你信這是密碼嗎? 11 12 base64string = base64.encodestring( 13 '%s:%s' % (username, password))[:-1] #注意哦,這里最后會自動添加一個\n 14 authheader = "Basic %s" % base64string 15 req.add_header("Authorization", authheader) 16 try: 17 handle = urllib2.urlopen(req) 18 except IOError, e: 19 # here we shouldn't fail if the username/password is right 20 print "It looks like the username or password is wrong." 21 sys.exit(1) 22 thepage = handle.read()
就是在header中加上:
1 base64string = base64.encodestring('username:password')[:-1] 2 authheader = "Basic %s" % base64string 3 headers['Authorization'] = authheader
r案后再請求就可以了。 原理就是添加了請求頭Authorization。