最近要用 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] #注意哦,这里最后会自动添加一个 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。