1. 程式人生 > >(資料科學學習手札114)Python+Dash快速web應用開發——上傳下載篇

(資料科學學習手札114)Python+Dash快速web應用開發——上傳下載篇

> 本文示例程式碼已上傳至我的`Github`倉庫[https://github.com/CNFeffery/DataScienceStudyNotes](https://github.com/CNFeffery/DataScienceStudyNotes) # 1 簡介    這是我的系列教程**Python+Dash快速web應用開發**的第十一期,在之前兩期的教程內容中,我們掌握了在`Dash`中建立完善的表單控制元件的方法。   而在今天的教程中,我們將介紹如何在`Dash`中高效地開發`web`應用中非常重要的**檔案上傳**及**下載**功能。
圖1
# 2 在Dash中實現檔案上傳與下載 ## 2.1 在Dash中配合dash-uploader實現檔案上傳   其實在自帶的`dash_core_components`中就封裝了基於`html5`原生API的`dcc.Upload()`元件,可以實現簡單的檔案上傳功能,但說實話,非常的**不好用**,其主要缺點有: - **檔案大小有限制,150M到200M左右即出現瓶頸** - **策略是先將使用者上傳的檔案存放在瀏覽器記憶體,再通過base64形式傳遞到服務端再次解碼,非常低效** - **整個上傳過程無法配合準確的進度條**   正是因為`Dash`自帶的上傳部件如此不堪,所以一些優秀的第三方拓展湧現出來,其中最好用的要數`dash-uploader`,它解決了上面提到的`dcc.Upload()`的所有短板。通過`pip install dash-uploader`進行安裝之後,就可以直接在`Dash`應用中使用了。   我們先從極簡的一個例子出發,看一看在`Dash`中使用`dash-uploader`的正確姿勢: >
app1.py ```Python import dash import dash_uploader as du import dash_bootstrap_components as dbc import dash_html_components as html app = dash.Dash(__name__) # 配置上傳資料夾 du.configure_upload(app, folder='temp') app.layout = html.Div( dbc.Container( du.Upload() ) ) if __name__ == '__main__': app.run_server(debug=True) ```
圖2
  可以看到,僅僅十幾行程式碼,我們就配合`dash-uploader`實現了簡單的檔案上傳功能,其中涉及到`dash-uploader`兩個必不可少的部分: ### 2.1.1 利用du.configure_upload()進行配置   要在`Dash`中正常使用`dash-uploader`,我們首先需要利用`du.configure_upload()`進行相關配置,其主要引數有:   **app**,即對應已經例項化的`Dash`物件;   **folder**,用於設定上傳的檔案所儲存的根目錄,可以是相對路徑,也可以是絕對路徑;   **use_upload_id**,bool型,預設為True,這時被使用者上傳的檔案不會直接置於**folder**引數指定目錄,而是會存放於`du.Upload()`部件的`upload_id`對應的子資料夾之下;設定為False時則會直接存放在根目錄,當然沒有特殊需求還是不要設定為False。   通過`du.configure_upload()`我們就完成了基本的配置。 ### 2.1.2 利用du.Upload()建立上傳部件   接下來我們就可以使用到`du.Upload()`來建立在瀏覽器中渲染供使用者使用的上傳部件了,它跟常規的`Dash`部件一樣具有**id**引數,也有一些其他的豐富的引數供開發者充分自由地自定義功能和樣式:   **text**,字元型,用於設定上傳部件內顯示的文字;   **text_completed**,字元型,用於設定上傳完成後顯示的文字內容字首;   **cancel_button**,bool型,用於設定是否在上傳過程中顯示“取消”按鈕;   **pause_button**,bool型,用於設定是否在上傳過程中顯示“暫停”按鈕;   **filetypes**,用於限制使用者上傳檔案的格式範圍,譬如`['zip', 'rar', '7zp']`就限制使用者只能上傳這三種格式的檔案。預設為None即無限制;   **max_file_size**,int型,單位MB,用於限制單次上傳的大小上限,預設為1024即1GB;   **default_style**,類似常規`Dash`部件的`style`引數,用於傳入css鍵值對,對部件的樣式進行自定義;   **upload_id**,用於設定部件的唯一id資訊作為`du.configure_upload()`中所設定的快取根目錄的下級子目錄,用於存放上傳的檔案,預設為None,會在`Dash`應用啟動時自動生成一個隨機值;   **max_files**,int型,用於設定一次上傳最多可包含的檔案數量,預設為1,也推薦設定為1,因為目前對於多檔案上傳仍有**進度條異常**、**上傳結束顯示異常**等bug,所以不推薦設定大於1。   知曉了這些引數的作用之後,我們就可以創建出更符合自己需求的上傳部件: >
app2.py ```Python import dash import dash_uploader as du import dash_bootstrap_components as dbc import dash_html_components as html app = dash.Dash(__name__) # 配置上傳資料夾 du.configure_upload(app, folder='temp') app.layout = html.Div( dbc.Container( du.Upload( id='uploader', text='點選或拖動檔案到此進行上傳!', text_completed='已完成上傳檔案:', cancel_button=True, pause_button=True, filetypes=['md', 'mp4'], default_style={ 'background-color': '#fafafa', 'font-weight': 'bold' }, upload_id='我的上傳' ) ) ) if __name__ == '__main__': app.run_server(debug=True) ```
圖3
  但像前面的例子那樣直接在定義`app.layout`時就傳入實際的`du.Upload()`部件,會產生一個問題——應用啟動後,任何訪問應用的使用者都對應一樣的`upload_id`,這顯然不是我們期望的,因為不同使用者的上傳檔案會混在一起。   因此可以參考下面例子的方式,在每位使用者訪問時再渲染隨機id的上傳部件,從而確保唯一性: > app3.py ```Python import dash import dash_uploader as du import dash_bootstrap_components as dbc import dash_html_components as html import uuid app = dash.Dash(__name__) # 配置上傳資料夾 du.configure_upload(app, folder='temp') def render_random_id_uploader(): return du.Upload( id='uploader', text='點選或拖動檔案到此進行上傳!', text_completed='已完成上傳檔案:', cancel_button=True, pause_button=True, filetypes=['md', 'mp4'], default_style={ 'background-color': '#fafafa', 'font-weight': 'bold' }, upload_id=uuid.uuid1() ) def render_layout(): return html.Div( dbc.Container( render_random_id_uploader() ) ) app.layout = render_layout if __name__ == '__main__': app.run_server(debug=True) ```   可以看到,每次訪問時由於`upload_id`不同,因此不同的會話擁有了不同的子目錄。
圖4
### 2.1.3 配合du.Upload()進行回撥   在`du.Upload()`中額外還有`isCompleted`與`fileNames`兩個屬性,前者用於判斷當前檔案是否上傳完成,後者則對應此次上傳的檔名稱,參考下面這個簡單的例子: > app4.py ```Python import dash import dash_uploader as du import dash_bootstrap_components as dbc import dash_html_components as html from dash.dependencies import Input, Output, State app = dash.Dash(__name__) # 配置上傳資料夾 du.configure_upload(app, folder='temp') app.layout = html.Div( dbc.Container( [ du.Upload(id='uploader'), html.H5('上傳中或還未上傳檔案!', id='upload_status') ] ) ) @app.callback( Output('upload_status', 'children'), Input('uploader', 'isCompleted'), State('uploader', 'fileNames') ) def show_upload_status(isCompleted, fileNames): if isCompleted: return '已完成上傳:'+fileNames[0] return dash.no_update if __name__ == '__main__': app.run_server(debug=True, port=8051) ```
圖5
## 2.2 配合flask進行檔案下載   相較於檔案上傳,在`Dash`中進行檔案的下載就簡單得多,因為我們可以配合`flask`的`send_from_directory`以及`html.A()`部件來為指定的伺服器端檔案建立下載連結,譬如下面的簡單示例就打通了檔案的上傳與下載: > app5.py ```Python from flask import send_from_directory import dash import dash_uploader as du import dash_html_components as html import dash_bootstrap_components as dbc from dash.dependencies import Input, Output import os app = dash.Dash(__name__) du.configure_upload(app, 'temp', use_upload_id=False) app.layout = html.Div( dbc.Container( [ du.Upload(id='upload'), html.Div( id='download-files' ) ] ) ) @app.server.route('/download/') def download(file): return send_from_directory('temp', file) @app.callback( Output('download-files', 'children'), Input('upload', 'isCompleted') ) def render_download_url(isCompleted): if isCompleted: return html.Ul( [ html.Li(html.A(f'/{file}', href=f'/download/{file}', target='_blank')) for file in os.listdir('temp') ] ) return dash.no_update if __name__ == '__main__': app.run_server(debug=True) ```
圖6
# 3 用Dash編寫簡易個人網盤應用   在學習了今天的案例之後,我們就掌握瞭如何在`Dash`中開發檔案上傳及下載功能,下面我們按照慣例,結合今天的主要內容,來編寫一個實際的案例;   今天我們要編寫的是一個簡單的個人網盤應用,我們可以通過瀏覽器訪問它,進行檔案的上傳、下載以及刪除:
圖7
> app6.py ```Python import dash import dash_bootstrap_components as dbc import dash_html_components as html from dash.dependencies import Input, Output, State import dash_uploader as du import os from flask import send_from_directory import time app = dash.Dash(__name__, suppress_callback_exceptions=True) du.configure_upload(app, 'NetDisk', use_upload_id=False) app.layout = html.Div( dbc.Container( [ html.H3('簡易的個人雲盤應用'), html.Hr(), html.P('檔案上傳區:'), du.Upload(id='upload', text='點選或拖動檔案到此進行上傳!', text_completed='已完成上傳檔案:', max_files=1000), html.Hr(), dbc.Row( [ dbc.Button('刪除選中的檔案', id='delete-btn', outline=True), dbc.Button('打包下載選中的檔案', id='download-btn', outline=True) ] ), html.Hr(), dbc.Spinner( dbc.Checklist( id='file-list-check' ) ), html.A(id='download-url', target='_blank') ] ) ) @app.server.route('/download/') def download(file): return send_from_directory('NetDisk', file) @app.callback( [Output('file-list-check', 'options'), Output('download-url', 'children'), Output('download-url', 'href')], [Input('upload', 'isCompleted'), Input('delete-btn', 'n_clicks'), Input('download-btn', 'n_clicks')], State('file-list-check', 'value') ) def render_file_list(isCompleted, delete_n_clicks, download_n_clicks, check_value): # 獲取上下文資訊 ctx = dash.callback_context if ctx.triggered[0]['prop_id'] == 'delete-btn.n_clicks': for file in check_value: try: os.remove(os.path.join('NetDisk', file)) except FileNotFoundError: pass if ctx.triggered[0]['prop_id'] == 'download-btn.n_clicks': import zipfile with zipfile.ZipFile('NetDisk/打包下載.zip', 'w') as zipobj: for file in check_value: try: zipobj.write(os.path.join('NetDisk', file)) except FileNotFoundError: pass return [ {'label': file, 'value': file} for file in os.listdir('NetDisk') if file != '打包下載.zip' ], '打包下載連結', '/download/打包下載.zip' time.sleep(2) return [ {'label': file, 'value': file} for file in os.listdir('NetDisk') if file != '打包下載.zip' ], '', '' if __name__ == '__main__': app.run_server(debug=True) ``` ---   以上就是本文的全部內容,歡迎在評論區與我進行