bootstrap 表單,【Flask】 結合wtforms的文件上傳表單

 2023-11-19 阅读 25 评论 0

摘要:表單中的文件上傳   基本的表單渲染,表單類設置等等就不多說了,參看另一個文章即可。但是那篇文章里沒有提到對于FileField,也就是上傳文件的表單字段是如何處理,后端又是如何實現接受上傳過來的文件的。因為看到了一篇很好的文章【https://zhu

表單中的文件上傳

  基本的表單渲染,表單類設置等等就不多說了,參看另一個文章即可。但是那篇文章里沒有提到對于FileField,也就是上傳文件的表單字段是如何處理,后端又是如何實現接受上傳過來的文件的。因為看到了一篇很好的文章【https://zhuanlan.zhihu.com/p/23731819?refer=flask】,所以我決定仔細學習一下。下面將按照那篇文章的脈絡,由簡至繁地說明表單中文件上傳的辦法。

■  利用Flask原生的機制進行文件上傳

bootstrap 表單。  首先在前端肯定有一個帶有文件上傳功能的表單。這個表單如果利用wtfoms來實現的話那就是在forms里面得有FileField這個字段。FileField還可以加上validators=[DataRequired()]參數來保證沒有文件選擇時不進行POST動作。在表單點擊確定之后,后端可以這么操作來得到文件對象:

uploaded_file = request.files['file']
#其中'file'是指表單中type是file的那個input的name屬性的值。另外也可以:
uploaded_file = uploadForm.file.data

?

文件上傳自動表單、  可以通過request對象的files屬性的file字段的值來獲得,也可以和其他表單字段獲取數據的方式一樣通過form對象來獲得,兩者取得的是同一個對象。這個對象是一個FileStorage對象,這個對象的屬性有下面這些:

  filename  上傳上來的文件的名字

  headers  上傳文件的頭信息

  name  表單字段的名字

  其中filename值得一用,比如在后面我可以寫filename = os.path.join(app.config['UPLOAD_FOLDER'],uploaded_file.filename)來得到一個上傳后文件的絕對路徑。然后調用uploaded_file.save(filename)就可以把上傳上來的文件的內容固化到服務器的硬盤中了。

  因為涉及到文件,還是需要注意相對路徑和絕對路徑這個坑。比如我之前的路徑都寫的是static/download_data,意思是讓文件全部都保存到項目的static目錄下面,但是并沒有什么用,因為相對路徑是從工作目錄而非腳本所在目錄開始尋找的。為了保證程序得到絕對路徑的信息同時又不想在程序里面寫死的話,就要活用os.path.abspath,os.path.join,os.path.dirname等這些方法了。

  可能已經注意到,對于一些配置我們可以放到app.config中去,比如UPLOAD_FOLDER可以指定上傳上來的文件放到哪個目錄下,ALLOWED_EXTENDSIONS可以賦它一個集合的值,對于不屬于這個集合的后綴名的文件就不允許上傳,只是這個功能還需要我們手動實現;MAX_CONTENT_LENGTH可以指定文件最大可以有多少字節等等。

  

  在我們的后臺得到了上傳上來的文件之后,也許我們還要考慮開放一個接口讓訪問者能供訪問到這些文件。這可能會用到flask自帶的一個send_file或者send_from_directory兩個方法。以后者為例,接受兩個參數,第一個是本地的一個目錄名,第二個是文件名。函數會自動讀取這個文件并返回文件的內容:

@app.route('/upload/<filename>')
def uploaded_file(filename):return send_from_directory(app.config['UPLOAD_FOLDER'],filename)

  這樣我們在表單中上傳后的文件,可以通過? /upload/文件名? 來訪問文件的內容了。

  順帶一提,如果在send_from_directory中加上as_attachment=True的參數的話,那么就可以將文件作為一個待下載的文件發送給客戶端了

?

■  利用flask-uploads擴展進行文件上傳

  以上雖然利用flask原生的一些工具進行了文件上傳功能的實現,但是不夠完善,而且文件擴展名驗證之類的工作還是要我們自己手動編碼來完成。根據以往其他一些功能的經驗,我們自然想到了有沒有一個擴展是可以支持flask方便地實現上傳文件的功能呢?答案是有的,就是flask-uploads。

  用flask-uploads上傳文件主要用到flask_uploads中的這些類:

  from flask_uploads import UploadSet,configure_uploads,AUDIO,IMAGE

  其中最重要的是UploadSet。這個方法返回了一個UploadSet對象,即上傳文件集合。他可以這么用 myfile = UploadSet('MYFILE')。參數的字符串為這個上傳文件集取了一個名字,這個名字在后續還將出現再等號左邊,這是比較神奇的一個設定。比如按照上面那樣有了myfile這個對象之后,可以在合適的地方為app對象配置:

from flask_uploads import UploadSet,configure_upload,AUDIO
#...

myfile = UploadSet('MYFILE')
app.config['UPLOADED_MYFILE_DEST'] = os.path.join(os.path.dirname(__file__),'static','download_data')
app.config['UPLOADED_MYFILE_ALLOW'] = AUDIOconfigure_upload(app,myfile)
#別忘了這一步,把上傳文件集的設置和app關聯起來

  可以看到,在為app進行config配置的時候,UPLOADED_%s_DEST和UPLOADED_%s_ALLOW中間的那串字符串是由之前UploadSet創建的時候定義的,兩者分別規定了這個上傳文件集存放的位置以及允許哪些后綴名的文件。那么就可以推斷AUDIO的具體內容是什么了,其實就是一些后綴名的元組。因為是AUDIO,所以是類似于('.mp3','.mp4','.avi'...)之類的。在flask_uploads中還有很多這種預設的后綴名元組,比如IMAGE,TEXT等等,就不舉例了。當相關配置全部完成后,記得最后還要configure_upload(app,myfile)來使得myfile和app關聯起來。之前看到網上有人采坑,把這一步沒有寫到views里面而寫在了forms里面去,這導致了報沒有app上下文的錯,需要注意。

  *如果覺得上面的UPLOAD_%s_ALLOWd的方式來配置允許的格式太麻煩的話,也可以在myfile=Upload('MYFILE',AUDIO)這樣第二個參數來直接指定某個UploadSet對象限定的后綴名。

  那么他為什么要把這個放到forms里面去?因為在forms中,在flask_wtf中存在一個file子模塊,里面含有一些文件相關的表單提交前驗證方法(就像表單那篇中提到的DataReqiured,EqualTo等validator一樣)。flask.wtf.file里面的這個FileAllowed驗證方法接受的參數第一個就是一個UploadSet對象,第二個和其他validator類似是messasge。因為需要這個UploadSet對象,所以為了方便,就把創建UploadSet對象的工作放到了forms里。但是為了避免缺少上下文的那個錯誤,建議還是放在views里面創建,然后讓forms導入;或者把configure_upload和UploadSet兩個方法分在兩個文件中寫,注意引用關系不報錯即可。

  這樣一來,在views和forms以及前端表單的配合下,一個較為完善,模塊化程度較高的上傳文件功能就做好了。:

##########views.py中部分代碼###########
from forms import myfileapp.config['UPLOAD_MYFILE_DEST'] = os.path.join(os.path.dirname(__file__),'static','downloaddata')
app.config['UPLOAD_MYFILE_ALLOW'] = AUDIOconfigure_upload(app,myfile)@app.route('/form',methods=['GET','POST'])
def form():uploadForm = UploadForm()if uploadForm.validate_on_submit():myfile.save(uploadForm.file.data,name=uploadForm.file.data.filename)#請注意保存文件時是用了UploadSet對象調用了save方法,而且這個save方法的第一個參數是文件對象,第二個參數是文件名return render_template('form.html',form=uploadForm)@app.route('/show/<filename>')
def show(filename):return '<a href="{0}">文件</a>'.format(myfile.url(filename))##########forms.py中部分代碼#########
from flask_wtf.file import FileField,FileAllowed,FileRequired
#請注意,如果要在FileField中加上FileAllowed等驗證函數的話,就不能從wtforms中導入Field類,而是必須從flask_wtf.file中,否則會報錯沒有has_file方法。
myfile = UploadSet('MYFILE')class uploadForm():file = FileField(u'上傳文件',validators=[FileAllowed(myfile,u'文件格式不對'),FileRequired()])submit = SubmitField(u'提交')

  關于視圖路由中的/show部分,調用了UploadSet對象的url方法,其實對于一個UploadSet對象有指定的存放文件的目錄,這也就是說,經這個對象處理的文件不會有重名(經試驗即使傳了重名的,后一個文件會被命名成filename_1.ext這樣子被存放到相關目錄中)。那么就表名,UploadSet對象其實可以維護文件名和完整文件路徑的關系。url方法返回的就是從http://開始包括域名在內的完整文件路徑。

  *在測試的時候還發現了一個flash_upload的小缺陷,就是后綴名大寫的文件將不被識別為可接受文件。比如1.png可以被ALLOW是IMAGES的UploadSet對象接收,但是1.PNG就不行。我的解決辦法是在UploadSet類中的extension_allowed方法中,加上一句ext = ext.lower()。

  如果我們根據上傳過來的文件的格式不同而進行不同的存儲和處理,那么用UploadSet會是一個比較不錯的辦法。

?

■  文件的管理

  在架設了文件上傳的功能之后,可能需要留出接口讓用戶可以通過web界面來進行文件管理。下面介紹一下一些方法來方便我們進行文件管理工作。

  展示文件可以用os.listdir或者walk之類的方法,來獲取后臺所有文件的列表,然后和前端配合將這些文件列表展現出來。

  如果我們需要留出刪除這些文件的接口,那么在后臺可調用os.remove(path)方法來刪除一個文件。路徑參數path可以通過myfile.path(filename)來獲得。因為一個文件集中文件名是不能重復的,所以UploadSet對象的path方法可以返回一個完整的絕對路徑。path方法和前面提到的url方法很容易混淆,兩者都是通過一個filename來返回一串定位用的串,其中url返回的是通過web和HTTP方法來訪問,所以是類似于http://ip:port/path/to/file這樣的,而path方法返回的是類似于C:\path\to\file或者linux上的/path/to/file這樣的。

  重新再來看save方法,經過上面的調用可以看到save方法除了FileStorage對象之外還可以傳一個name參數指定保存到服務器上文件的名字。除此之外save方法其實還有一個folder參數,可以指定在UPLOAD_XXX_DEST中文件還要存放到什么子級目錄中去。比如folder=os.path.join('subdir','tempdata')的話,這個文件就會被放到UPLOAD_XXX_DEST指定目錄下的subdir/tempdata目錄中,若之前路徑還不存在程序會自動創建沒有的目錄。

  ●? 文件名點竄

  關于上面對文件名的默認處理是保持原名,但是實際上這并不是很安全。可以參考的一個做法是提取myfile.path(filename)的md5散列值,然后把這個值的前N位或者整個值作為文件名保存下來。然后把真實文件名的path和這個文件名對應起來存到數據庫中。這樣用戶在調取文件的時候可以從數據庫中定位相關的真實文件名。

 

■  多文件上傳

  在以上實現中,我們的FileField一次只能接受至多一個文件上傳,也就是說點擊瀏覽按鈕彈出來的對話框中我們只能單選一個文件。

  如果要多選,首先在前端的input標簽中應該是要有multiple='multiple'這個屬性。至于到了后端,該如何接受這些文件?按照上面的說明,后端接受文件可以有兩種方式,第一種是通過flask.request對象,接受多個文件時用request.files.getlist('file'),這個方法返回一個FileStorage對象的列表。遍歷這個列表,針對每一個FileStorage對象調用UploadSet對象對其執行save方法即可保存它們。getlist中的'file'其實是表單中文件上傳的那個input的name屬性的值,如果用的是wtforms實現的表單的話那么也是文件上傳field的對象的變量名。

?

■  拖曳上傳

  上面實現的文件上傳表單,界面是很傳統的那種一個瀏覽按鈕加上一串提示字符串。稍微時髦一點的可以使用拖曳文件到瀏覽器窗口進行上傳。要實現拖曳上傳,需要用到Dropzone.js這個擴展JS。

  【如何將這個JS整合進我們的項目我自己沒有研究。。偷了個懶,上面那篇文章的作者寫了一個flask擴展flask-dropzone,我們可以直接pip install之避免重復造輪子。這個擴展的用法:http://greyli.com/flask-dropzone-add-file-upload-capabilities-for-your-project/。這個擴展的github地址:https://github.com/wyzypa/flask-dropzone。】

  簡單來說,我們只要像其他擴展那樣初始化:dropzone = Dropzone(app),然后去前端做一些相關的修改即可。前端的修改可以參考下面:

{% block head %}{# 注意,一定要在head中load,否則dropzone.js和相關css文件無法通過CDN方法導入#}{{ dropzone.load() }}
{% endblock %}{% block page_content %}{{ dropzone.create(action_view='form') }}{{ dropzone.style('border:5px spotted black;width:100%;background:grey') }}{# create方法接收一個action_view參數。dropzone創建的其實是一個表單,這個表單標簽form的action屬性就由這個參數的值決定,
也就是說要寫相關視圖函數的endpoint才行。這里寫了form,意味著在views中一定存在@app.route('/something')下的def form():...#}{# style方法允許手工調整dropzone區域的CSS樣式 #} {% endblock %}

?

  這樣一個基本的dropzone就已經完成了。如果想要對提示文字等作出更多調整可以參考上面那個用法說明中給出的可以配置哪些app.config的項。比如經過如下config設置之后

  app.config['DROPZONE_DEFAULT_MESSAGE'] = u'點擊或將文件拖曳到此區域來上傳文件'  就可以改變默認的提示文字。

  app.config['DROPZONE_MAX_FILE_SIZE'] = 3  設置了上傳文件最大只能到3MB

  app.config['DROPZONE_TOO_BIG'] = u'上傳文件大小超過限制'  設置當上傳文件大小超過限制時的提示文字

  app.config['DROPZONE_INVALID_FILE_TYPE'] = u'文件類型不符合規定'

  app.config['DROPZONE_SERVER_ERROR'] = u'服務器內部發生錯誤:{{ statusCode }}'

  *如果要對上傳文件的格式做出限制,之前一直傻傻地還去該uploadForm里面的FileField的FileAllowed的validator,后來突然意識到,現在已經和form沒關系了。。通過flask-dropzone來設置格式限制是這樣的:

  app.config['DROPZONE_ALLOWED_FILE_CUSTOM'] = True

  app.config['DROPZONE_ALLOWED_FILE_TYPE'] = '一些字符串值'。字符串的格式由dropzone.js規定。基本格式是類似于這樣的:'.jpg, .png, .zip, .rar',請注意每個格式前面到逗號為止是要空出一格的。dropzone還提供了一些預設的比如image/*,audio/*等囊括一些后綴名的集合,也可以用。比如混用兩者:'image/*, .rar, .zip'。

  因為通過dropzone.js來實現的文件上傳表單不通過wtforms渲染,所以就不能再views里面通過我們熟悉的from = XXForm(),然后if form.validate_on_submit()這樣的方式在視圖中控制了。所以我們要回歸原始,采用if request.method == 'POST' and request.files.get('file')之后,利用UploadSet對象的save方法來進行文件存儲。

  順便一提,最開始dropzone.js被開發出來時好像專門是針對圖片上傳用的,所以對圖片的支持特別好。圖片格式被上傳之后,還會再頁面上顯示縮略圖。比如下面這樣:

?

?

  

■  進度條!

  進度條之于上傳的重要性想必不用多講了。其實在上面的dropzone.js中已經實現了一個小進度條了,不過考慮到更加自由的進度條設置方式,還是有必要來看下這塊內容。

  網上現成的進度條解決方案不多,我搜到了這篇文章【http://www.jianshu.com/p/716d470d6434】(怎么感覺老是在copy然后總結別人的文章,果然還是自己水平太~低了【笑哭】)

  利用了flask+jQuery+Bootstrap實現的進度條。正好這幾個技術都還算熟悉,就用這個了。首先來說說他實現的原理,首先肯定是不能用dropzone.js之類已經包裝好上傳方法的組件了,記得paramiko的sftp在上傳下載的時候提供了一個回調函數主要就是用來給開發人員做進度條用的,所以我們要自己實現上傳方法。這里用了jQuery中的ajax的POST方法來實現文件的上傳。另外還有必要來復習一下Bootstrap的進度條組件,BS中的進度條就是一個div包著另一個div。外面的div是.progress,里面的div是.progress-bar,通過改變.progress-bar的width CSS屬性來實現進度條的走動。

  至于文件上傳方法,我們要做的是在ajax中設置XMLHttpRequest對象,并以此為基礎添加事件來監聽上傳的進度。具體的前端代碼如下(一部分):

{% block page_content %}
<
form action="{{ url_for('form') }}" role="form" method="POST" enctype="multipart/form-data"><input type="file" id="file" name="file" /><input type="submit"> </form>//進度條 <div class="process" style="display:none;"><div class="process-bar" style="width:0%;" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div> </div> {% endblock %}
{% block scripts %}
{{ super() }} //千萬別忘了super,否則jQuery和bootstrap.js就不被CDN方式導入了 <script type="text/javascript">$("form").on("submit",function(event){$(".progress").css("display","block"); //顯示進度條 event.preventDefault(); //不是很懂這里是干嘛的,原文說是為了阻止表單提交var formData = new FormData(this);
     //如果需要為表單添加一些其他字段的數據可以調用formData.append('key','value')來實現
//開始用ajax上傳文件 $.ajax({xhr : function(){var xhr = new XMLHttpRequest();xhr.upload.addEventListener('progress' ,function(e){if (e.lengthComputable){var percent = Math.round(e.loaded * 100 / e.total);$(".progress-bar").attr("aria-valuenow",percent).css("width",percent+"%");}});return xhr;},type : 'POST',url : '/form',cache : false,data : formData,processData : false,//這條主要是指出了jQuery不要去處理發送的數據 contentType : false})
//這里是說明不要ajax去設置Content-Type請求頭,原因我也不懂。。
.done( //接在整個ajax請求方法后面,表示處理完成或失敗時調用的函數
function(){alert('success');}).fail(
function(){alert('failed');}); }); </script>
{% endblock %}

  通過這樣的方法構造出來的一個上傳部件就實現了進度條的功能了。要注意在ajax的參數列表中必須設置processData : false和contentType : false這兩條。否則很可能在前端控制臺報錯說append方法用到了沒有實現FormData接口的對象上。

  另外為了提高用戶體驗,加強重復上傳時的工作性能,可以在done和fail兩個回調函數中再添加上比如$(".progress").hide("slow") //上傳結束后進度條重新隱藏。$("progress-bar").css("width","0%").attr("aria-valuenow","0"); //上傳結束后把進度條歸零。

  按照上面的代碼打造出來的進度條很好,但是可能不能附帶表單數據。如果需要表單數據那么需要額外在JS中為formData對象添加一些K-V對來表示表單數據。比如formData.append('name',$('input#formName').val());就是把當前頁面表單中的某個字段的值賦予formData對象的某個Key。然后通過ajax請求上傳的這個表單就既有文件上傳又有表單數據了。還要提醒一句,formData是一個FormData原型的對象而不是一個簡單的Object。所以在控制臺里log出來的也是一個空對象,不要以為沒有append進去,其實只有通過formData.get方法才能順利取出數據的 。

  

轉載于:https://www.cnblogs.com/franknihao/p/7422805.html

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/1/181522.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息