
文件上传的服务端技术解析
常言到,爱有多深、恨有多切。tornado服务器就是这样一个矛盾体,它的缺点和它的优点一样,显著且强烈。有人认为,文件上传是tornado的重大缺陷之一,它把用户上传的文件存放在内存中——这意味着多用户同时上传文件的话,内存的开销会急剧增加。不过我倒是觉得,这反倒让很多事情变得简单了,比如,你想对用户上传的内容做处理的话,不用再打开文件了,因为内容就在内存中。再者说,在tornado的异步机制下,我不确定真的能够多用户同时上传文件。这是一个有趣的问题。
好了,言归正传吧。假定文件上传的表单如下:
1 2 3 4 5 | <form id= "form_upload" action= "/demo/upload" enctype= "multipart/form-data" method= "post" >
<input type= "file" name= "want_to_upload_file_1" /><br/>
<input type= "file" name= "want_to_upload_file_2" /><br/>
<input type= "submit" value= "上传" />
</form>
|
机制是允许一次上传多个文件的。这里有几个问题需要特别说明一下。
在提交表单之前,需要为form指定action和method的属性值,如果是上传文件,还要设置enctype=“multipart/form-data”。这三个属性,可以写在html中,也可以在submit之前用js的方法为其赋值。
文件浏览是file类型的input标签自备的功能,程序员无法在浏览器框架内操作本地文件。该标签的name属性,是用来区别于其他文件的标识,不是文件名,也不是文件对象,更不是文件内容。
上面的表单被提交到/demo/upload(假定上传的第1个文件名为dqd.jpg,第2个文件名为intro.png),这个请求的对象中包含files字典,上传的全部文件的信息都包含在这个结构中。我们来看看这个request.files的真实面貌:
1 2 3 4 5 6 7 8 | def post(self):
print self.request.files.keys() # [u 'want_to_upload_file_1' , u 'want_to_upload_file_2' ]
print type(self.request.files[ 'want_to_upload_file_1' ]) # list,长度为1
meta_file_1 = self.request.files[ 'want_to_upload_file_1' ][0]
print meta_file_1.keys() # [ 'body' , 'content_type' , 'filename' ]
print len(meta_file_1[ 'body' ]) # 31492,文件长度
print meta_file_1[ 'content_type' ] # image/jpeg
print meta_file_1[ 'filename' ] # dqd.jpg
|
有了这些素材,我们就可以无所不能地应对客户需求了。比如,不做任何处理,仅仅用原文件名保存在指定路径下(假设保存在/static/image/wiki目录下):
1 2 3 4 5 | PROJECT_PATH = os.path.split(os.path. realpath ( __file__ ))[0]
upload_path = os.path.join(PROJECT_PATH, 'static' , 'image' , 'wiki' )
file_name = os.path.join(upload_path, meta_file_1[ 'filename' ])
with open(file_name, 'wb' ) as fp:
fp.write(meta_file_1[ 'body' ])
|
很多时候,需要对用户上传的文件重命名(比如,用时间戳为文件名),但文件后缀名不变。
1 2 3 | fn, ext = os.path.splitext(meta_file_1[ 'filename' ])
fn = '%d%s' % (time.time()*1000, ext)
file_name = os.path.join(upload_path, fn)
|
如果需要对用户上传的文件类型做检查,请使用文件的content_type,而不是文件的扩展名,因为前者更规范。比如,JPEG类型的图片文件,其后缀名可能是.jpg|.jpeg|.JPG|.JPEG中的一种,而前者只有“image/jpeg”一种表示法。
关于文件的content_type,网上资料多如牛毛,请自行搜索。
处理用户上传的图片文件时,除了限制文件大小,有时候还要做缩放处理,甚至一并生成缩略图,此时就需要将文件内容转成易于处理的图像对象,比如,pil的Image。
1 2 3 4 | from PIL import Image
import StringIO
pilImg = Image.open(StringIO.StringIO(meta_file_1[ 'body' ]))
print pilImg.size
|
至于如何缩放、如何保存为文件,请自行检索相关资料。
基于Ajax技术实现的文件上传客户端
假定上传文件的表单是这样的:
1 2 3 4 | <form id= "form_upload" action= "/demo/upload" enctype= "multipart/form-data" method= "post" >
<input type= "file" name= "wiki_img" id= "wiki_img" /><br />
<input id= "doUpload" type= "button" value= "上传" />
</form>
|
方法1:使用 ajaxfileupload.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <script src= "jquery.js" ></script>
<script src= "ajaxfileupload.js" ></script>
<script type= "text/javascript" >
$( "#doUpload" ).click( function (){
$.ajaxFileUpload({
url: '/demo/upload' ,
secureuri:false,
fileElementId: 'wiki_img' ,
dataType: 'json' ,
success: function (data){
alert(data);
}
});
});
</script>
|
方法2:仅依赖 jquery.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <script src= "jquery.js" ></script>
<script type= "text/javascript" >
var formData = new FormData();
formData.append( "file" , $( "#wiki_img" )[0].files[0]);
formData.append( "filename" , $( "#wiki_img" ).val());
$.ajax({
url : '/demo/upload' ,
type : 'POST' ,
async : false,
data : formData,
processData : false,
contentType : false,
beforeSend: function (){
$( "#upload_tips" ).html( "正在进行,请稍候" );
},
success : function (data) {
alert(data);
}
});
</script>
|
文件下载的服务端技术解析
相对于上传,文件的下载就简单得多。只需要记住两点:开始前告诉浏览器要传输的文件类型,结束前对浏览器说拜拜。文件类型并不是固定的,需要根据文件的实际情况来选择。详情请自行检索。
1 2 3 4 5 6 7 8 9 | def get(self):
self.set_header ( 'Content-Type' , 'application/octet-stream' )
with open(filename, 'rb' ) as f:
while True:
data = f.read(buf_size)
if not data:
break
self.write(data)
self.finish()
|
配合seek命令,可以实现更复杂的下载请求,比如,断点续传、分块下载、ajax异步请求等。
众多python培训视频,尽在python学习网,欢迎在线学习!