本文示例程式碼已上傳至我的
Github
倉庫https://github.com/CNFeffery/DataScienceStudyNotes
1 簡介
這是我的系列教程Python+Dash快速web應用開發的第十一期,在之前兩期的教程內容中,我們掌握了在Dash
中建立完善的表單控制元件的方法。
而在今天的教程中,我們將介紹如何在Dash
中高效地開發web
應用中非常重要的檔案上傳及下載功能。
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
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)
可以看到,僅僅十幾行程式碼,我們就配合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
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)
但像前面的例子那樣直接在定義app.layout
時就傳入實際的du.Upload()
部件,會產生一個問題——應用啟動後,任何訪問應用的使用者都對應一樣的upload_id
,這顯然不是我們期望的,因為不同使用者的上傳檔案會混在一起。
因此可以參考下面例子的方式,在每位使用者訪問時再渲染隨機id的上傳部件,從而確保唯一性:
app3.py
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
不同,因此不同的會話擁有了不同的子目錄。
2.1.3 配合du.Upload()進行回撥
在du.Upload()
中額外還有isCompleted
與fileNames
兩個屬性,前者用於判斷當前檔案是否上傳完成,後者則對應此次上傳的檔名稱,參考下面這個簡單的例子:
app4.py
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)
2.2 配合flask進行檔案下載
相較於檔案上傳,在Dash
中進行檔案的下載就簡單得多,因為我們可以配合flask
的send_from_directory
以及html.A()
部件來為指定的伺服器端檔案建立下載連結,譬如下面的簡單示例就打通了檔案的上傳與下載:
app5.py
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/<file>')
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)
3 用Dash編寫簡易個人網盤應用
在學習了今天的案例之後,我們就掌握瞭如何在Dash
中開發檔案上傳及下載功能,下面我們按照慣例,結合今天的主要內容,來編寫一個實際的案例;
今天我們要編寫的是一個簡單的個人網盤應用,我們可以通過瀏覽器訪問它,進行檔案的上傳、下載以及刪除:
app6.py
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/<file>')
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)
以上就是本文的全部內容,歡迎在評論區與我進行討論!