图片上传的基本思想大概是这样,前端用一个表单中type为file的input标签作为图片上传的前台发起上传,并以ajax把整个表单发送到后台,后台则是接收到图对象,加以处理,保存到服务器指定目录即可。首先先介绍下前台的代码,以及上传文件的form表单。
form表单如下:
from flask_wtf import FlaskForm
from wtforms import FileField
from app import label
from flask_wtf.file import FileField, FileRequired, FileAllowed
from config import ALLOWED_EXTENSIONS
class ImgForm(FlaskForm):
photo = FileField(validators=[FileAllowed(ALLOWED_EXTENSIONS, message=label('_allow_img', [str(ALLOWED_EXTENSIONS)]))])
其中的config 如下:
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg'])
其中的label为如下:
def label(name, values=[], path=None):
'''
默认从app目录下的guide导入所需变量
name 导入的数据变量名
values 数据类型必须市列表,提示的动态内容
path 为导入的路径 name
'''
path = ".".join(path.split(".")[:-1]) + ".guide:" if path else "app.guide:"
msg = import_string(path + name)
return msg.format(values) if len(values) else msg
_allow_img = '''图片的格式只能{0[0]}'''
后台的视图函数则是实例化该ImgForm,传到前台,前台的html代码为:
<form id="img_file" method='POST' enctype=multipart/form-data>
{{ imgform.hidden_tag() }}
<div class="fileupload btn btn-primary btn-xs">
<span class="btn-text">上传图片span>
{{ imgform.photo(class="upload") }}
div>
form>
hidden_tag()一定不要漏掉,不然后台将无法获取到我们传到后台的数据。js代码如下:
$('#img_file #photo').on('change',function(){
var imgform = new FormData(document.getElementById("img_file"));
imgform.append ("key" , "value")
$.ajax({
url:'{{ url_for("app.upload_img") }}',
type:"POST",
dataType:'json',
data:imgform,
cache: false,
processData: false,
contentType: false,
success:function(response){
alert('上传成功')
},
error:function(e){
alert('上传失败')
}
})
})
其中的imgform.append,则是给form对象添加我们想要传到后台的数据,比如你要传一个name为MuMu的数据到后台就可以这么写:
imgform.append ("name" , "MuMu");
flask后台接口:
from PIL import Image
@app.route('/upload_img', methods=['POST'])
def upload_img():
form = ImgForm()
if form.validate():
file = request.files['photo']
MuMu = request.form.get('name')
img = Image.open(file)
img_w, img_h = img.size
img_name_path = os.getcwd() + label('_img_name_path')
img_name = get_line(img_name_path, 1)
set_size(img, img_w, img_h, 1.0, img_name_path, img_name+'_L', '.png')
set_size(img, img_w, img_h, 0.5, img_name_path, img_name+'_M', '.png')
set_size(img, img_w, img_h, 0.2, img_name_path, img_name+'_S', '.png')
imgL = img_name+'_L.png'
imgM = img_name+'_M.png'
imgS = img_name+'_S.png'
add_img(imgL, MuMu)
add_img(imgM, MuMu)
add_img(imgS, MuMu)
result = {'success':label('_update_succ')}
else:
result = form.errors
return json.dumps(result)
其中set_size函数为图片处理函数,如下:
def set_size(im, imgw, imgh, size, path, name, Suffix):
img_w, img_h = int(imgwsize), int(imghsize)
img = im.resize((img_w, img_h), Image.ANTIALIAS)
imgpath = path + name + Suffix
img.save(imgpath)
图片的保存到制定目录跟单张的一样,唯一区别在于,批量上传是以压缩文件的形式上传,思路是先上传到制定目录,然后解压出来,再把里面的图片保存到制定目录,最后再移除压缩文件与解压出来的文件。前台代码跟上面的上传图片类似,就不再重复介绍。主要介绍一下上传压缩文件后的从图片图片处理过程。假设现在已经上传了压缩文件。后台处理接口如下:
@app.route('/upload/imgfile', methods=['POST'])
def upload_imgfile():
form = FileForm()
if form.validate():
file = request.files['name']
sku_path = '/app/product/csv/'
uploadname = upload_file(sku_path, file, flag=False)
file_list=[]
imgs={}
child_file = []
if uploadname:
file_path = os.getcwd() + sku_path + uploadname
if uploadname[-3:] == 'zip':
filename=un_zip(file_path)
elif uploadname[-3:] == 'tar':
filename=un_tar(file_path)
for i in os.walk(filename):
file_list.append(i)
for j in file_list:
for k in file_list[1][1]:
pathn = file_list[1][0] + '/' + k
if pathn == j[0]:
imgs[k]=j[2]
for key in imgs:
for img_l in imgs[key]:
file = file_list[1][0]+'/'+key +'/'+img_l
img = Image.open(file)
img_w, img_h = img.size
img_name_path = os.getcwd() + label('_img_name_path')
img_name = get_line(img_name_path, 1)
set_size(img, img_w, img_h, 1.0, img_name_path, img_name+'_L', '.png')
set_size(img, img_w, img_h, 0.5, img_name_path, img_name+'_M', '.png')
set_size(img, img_w, img_h, 0.2, img_name_path, img_name+'_S', '.png')
imgL = img_name+'_L.png'
imgM = img_name+'_M.png'
imgS = img_name+'_S.png'
add_img(imgL, img_l[:-4])
add_img(imgM, img_l[:-4])
add_img(imgS, img_l[:-4])
remove_file(sku_path, filename[:-1], flag=False)
remove_file(sku_path, uploadname)
result = {'success':label('_update_succ')}
else:
result = form.errors
return json.dumps(result)
其中un_zip、un_tar解压函数以及文件移除函数如下,需要注意的是移除单个文件只需要remove即可,而如果想移除一个文件夹则需要用rmtree方法才能移除,详情如下:
def remove_file(path, filename, flag=True):
'''
删除文件
判断文件是否存在,存在则删除
'''
path = BASE_PATH + path
filename = os.path.join(path, filename)
if os.path.exists(filename):
if flag:
os.remove(filename)
else:
shutil.rmtree(filename)
def upload_file(path, file, flag=True):
'''
上传文件
'''
if flag:
name = int(time.time()*1000)
else:
name = file.filename
if file:
path = BASE_PATH + path
file.save(os.path.join(path, str(name)))
return name
def un_zip(file_name):
"""unzip zip file"""
zip_file = zipfile.ZipFile(file_name)
if os.path.isdir(file_name[:-4] + "files"):
pass
else:
os.mkdir(file_name[:-4] + "files")
for names in zip_file.namelist():
zip_file.extract(names, file_name[:-4] + "files/")
zip_file.close()
return (file_name[:-4] + "files/")
def un_tar(file_name):
tar = tarfile.open(file_name)
names = tar.getnames()
if os.path.isdir(file_name[:-4] + "files"):
pass
else:
os.mkdir(file_name[:-4] + "files")
for name in names:
tar.extract(name, file_name[:-4] + "files/")
tar.close()
return (file_name[:-4] + "files/")
其中的get_line函数则是一个操作txt文件的函数,我们事先把图片名放在txt中,上传一张图片就从文件中提取出一行图片名,并删除一行,若删除图片则写回txt中,这是个一个良好的解决命名冲突的问题。代码如下:
def get_line(file, del_line):
with open(file, 'r') as old_file:
with open(file, 'r+') as new_file:
current_line = 0
# 定位到需要删除的行
while current_line < (del_line - 1):
old_file.readline()
current_line += 1
# 当前光标在被删除行的行首,记录该位置
seek_point = old_file.tell()
# 设置光标位置
new_file.seek(seek_point, 0)
# 读需要删除的行,光标移到下一行行首
getline = old_file.readline()
# 被删除行的下一行读给 next_line
next_line = old_file.readline()
# 连续覆盖剩余行,后面所有行上移一行
while next_line:
new_file.write(next_line)
next_line = old_file.readline()
# 写完最后一行后截断文件,因为删除操作,文件整体少了一行,原文件最后一行需要去掉
new_file.truncate()
return (getline.strip())
我是采用h5中a标签的新属性,即a标签中的href为图片的绝对路径,增加一个download属性即为图片下载,且download值则是下载后图片的名称。(有一个不足即使兼容性不怎么好,目前仅支持谷歌浏览器以及火狐浏览器),例子如下:
<a href="MuMu.gif" download="MuMu01.gif">
<button class="btn btn-success btn-xs">下载 button>
a>
批量下载的本质也是跟单张的一样,只不过要点击批量下载,要用js遍历模拟点击a标签实现多张下载,js如下:
$(function(){
$('#panel_head').height('45px')
$('input[name="checkbtn"]').each(function(){
$(this).on('click',function(){
chkval = []
upsku = []
if($("input[name='checkbtn']:checked").length 0 ){
$('#batch_download').removeClass('hide');
}else{
$('#batch_download').addClass('hide')
}
for(var j=0;j<$("input[name='checkbtn']:checked").length;j++){
if($("input[name='checkbtn']:checked")[j].checked){
chkval.push($("input[name='checkbtn']:checked")[j].value)
upsku.push($("input[name='checkbtn']:checked")[j].getAttribute("data-sku"))
}
}
})
})
var btn = document.getElementById('batch_download');
function download(name, href) {
var a = document.createElement("a"), //创建a标签
e = document.createEvent("MouseEvents"); //创建鼠标事件对象
e.initEvent("click", false, false); //初始化事件对象
a.href = href; //设置下载地址
a.download = name; //设置下载文件名
a.dispatchEvent(e); //给指定的元素,执行事件click事件
}
//给多文件下载按钮添加点击事件
btn.onclick = function(){
for (var index = 0; index < chkval.length; index++) {
download(upsku[index], chkval[index]);
}
}
})
如果想实现多文件同时上传(例如:csv、xlsx、xls等),前端的表单需要加个属性:enctype="multipart/form-data"才能实现文件多选,且选择多文件上传的时候需要按住ctrl,按住shift则是把点选当前的文件以及前面的文件全部选定,然后点击打开即可完成文件选定。前端代码如下:
{% from "macros/upload_form.html" import modal %}
<div class="row">
<div class="col-sm-12 col-lg-12 col-md-12">
<div class="panel panel-default card-view">
<div class="panel-heading">
<div class="pull-left">
<h6 class="panel-title txt-dark">多文件上传h6>
<div class="button-list">
<a href="#upload_data" data-toggle="modal" title="Compose">
<div class="fileupload btn btn-default btn-outline btn-rounded btn-anim">
<i class="fa fa-upload">i>
<span class="btn-text">上传待处理数据span>
div>
a>
{{ modal(csvform, mymodal="upload_data", action='', enctype="multipart/form-data", id='upload_form', fade="modal fade", displays="display: none", title='上传待处理数据') }}
div>
div>
<div class="clearfix">div>
div>
div>
div>
div>
其中引用到的宏模板如下:
{# 开始表单字段 #}
{% macro modal_form_field(field) %}
{%- if field.type != 'SubmitField' %}
{%- if field.type == 'FileField' %}
<div class="col-md-12 mb-20">
{{ field(class="form-control", placeholder=field.label.text, multiple="multiple") }}
div>
{% else %}
<div class="col-md-12 mb-20">
{{ field(class="form-control", placeholder=field.label.text) }}
div>
{% endif -%}
{% endif -%}
{% endmacro %}
{# 结束表单字段 #}
{% macro modal(form,
mymodal = "myModal_1",
title = "",
action="",
method="post",
role="form",
form_type="basic",
enctype=None,
button_map={},
id="",
fade="modal fade",
displays="display: none",
closeWin="",
novalidate=False) %}
{%- set _enctype = [] %}
{%- if enctype is none -%}
{%- for field in form %}
{%- if field.type == 'FileField' %}
{%- set _ = _enctype.append('multipart/form-data') -%}
{%- endif %}
{%- endfor %}
{%- else %}
{% set _ = _enctype.append(enctype) %}
{%- endif %}
<div aria-hidden="true" role="dialog" tabindex="-1" id="{{ mymodal }}" class="{{ fade }}" style="{{displays}}">
<div class="modal-dialog">
js实现文件ajax上传如下:
$(function(){
$('#upload_form #submit').on('click',function(e){
e.preventDefault()
var fileList = document.getElementById("csvfile").files;
var fileform = new FormData(document.getElementById("upload_form"));
fileform.append('max_num', $('#upload_form #min_num').val())
fileform.append('fileList', fileList)
swal({
title: "数据上传处理中!",
text: "请耐心等待...",
showConfirmButton: false
});
$.ajax({
url:'{{ url_for("fmdata.upload_csv") }}',
type:"POST",
dataType:'json',
data:fileform,
cache: false,
processData: false,
contentType: false,
success:function(response){
errorinfo=[]
for (var key in response){
errorinfo.push(response[key]+'\n')
}
if(response["success"]){
swal({
title: '上传成功',
type: "success",
timer: 1500,
showConfirmButton: false
},function(){
swal({
title: "数据处理中!",
text: "当前进度:0%",
showConfirmButton: false
});
//定时检测文件是否处理完毕,一旦处理完就下载文件
var time_interval = setInterval(function(){
$.ajax({
url : "{{url_for('fmdata.download_csv')}}",
data : {
'fm_id' : response["fm_id"]
},
dataType:'json',
success : function(responses){
if (responses["progress"]) {
swal({
title: "数据处理中!",
text: "当前进度:" + responses["progress"] + "%",
showConfirmButton: false
});
}
if(responses["download"]){
clearInterval(time_interval)
for (var i = responses["download"].length - 1; i >= 0; i--) {
download(responses["file_name"][i], responses["download"][i])
}
swal({
title: '处理完成',
type: "success",
timer: 1500,
showConfirmButton: false
})
}
}
})
},3500)
});
return false;
}else{
swal({
title: "上传失败!",
type: "error",
text: errorinfo,
timer: 1500,
showConfirmButton: false
},function(){
window.location.reload();
});
return false;
}
},
error:function(e){
swal({
title: '上传失败',
type: "error",
text: "可能文件格式有误或者网络错误!!"+e,
timer: 2000,
showConfirmButton: false
},function(){
window.location.reload();
});
return false;
}
})
})
})
function download(name, href) {
var a = document.createElement("a"), //创建a标签
e = document.createEvent("MouseEvents"); //创建鼠标事件对象
e.initEvent("click", false, false); //初始化事件对象
a.href = href; //设置下载地址
a.download = name; //设置下载文件名
a.dispatchEvent(e); //给指定的元素,执行事件click事件
}
后端接收数据如下:
@fmdata.route('/upload', methods=['GET', 'POST'])
def upload_csv():
form = CsvForm()
if form.validate():
# 获取前端传入的上传文件数据
upfiles = request.files.getlist('csvfile')
fm_id_list = []
for file in upfiles:
csv_path = '/app/static/fmdata/'
uploadname = upload_file(csv_path, file, flag=False)
new_path = os.getcwd() + csv_path + uploadname
# 放入进程池实现异步处理
executor = ThreadPoolExecutor(1)
# depart_data 为处理数据的函数
executor.submit(depart_data, new_path, uploadname)
fm_id_list.append(fm_id)
result = {'success': "ok"}
else:
result = form.errors
return json.dumps(result)
@fmdata.route('/download_csv', methods=['GET', 'POST'])
def download_csv():
# 处理逻辑
#return json.dumps(result)
对于批量(单张)上传,批量(单张)下载就先介绍到这,或许某些地方总结的不够好,大神如果有什么好的建议或补充,欢迎留言。。。