秀啊,用Python快速開(kāi)發(fā)在線數(shù)據(jù)庫(kù)更新修改工具
1 簡(jiǎn)介
這是我的系列教程「Python+Dash快速web應(yīng)用開(kāi)發(fā)」的第十三期,在上一期中,我們一起認(rèn)識(shí)了Dash自帶的交互式表格組件dash_table,并學(xué)會(huì)了如何自定義表格中不同部分的樣式。
而今天的教程,我們將繼續(xù)深入認(rèn)識(shí)dash_table的更多交互方面的功能,學(xué)習(xí)如何為渲染出的表格分頁(yè),并添加動(dòng)態(tài)內(nèi)容修改等交互功能。
圖1
2 dash_table的基礎(chǔ)交互能力
dash_table的核心功能是賦予用戶與圖表進(jìn)行快捷交互的能力,下面我們來(lái)學(xué)習(xí)其基礎(chǔ)常用的一些交互功能:
2.1 分頁(yè)翻頁(yè)
當(dāng)我們要展示的數(shù)據(jù)行數(shù)較多時(shí),在網(wǎng)頁(yè)中渲染可以選擇分頁(yè),這在dash_table中實(shí)現(xiàn)起來(lái)比較方便,根據(jù)數(shù)據(jù)傳遞方式的不同,可以分為「前端分頁(yè)」與「后端分頁(yè)」:
2.1.1 前端分頁(yè)
前端分頁(yè)顧名思義,就是在我們?cè)L問(wèn)Dash應(yīng)用時(shí),表格內(nèi)所有頁(yè)面的數(shù)據(jù)一次性加載完成,適合數(shù)據(jù)量不大的情況,將數(shù)據(jù)存儲(chǔ)壓力轉(zhuǎn)移到瀏覽器端。
通過(guò)參數(shù)page_size設(shè)置每頁(yè)要顯示的記錄行數(shù),Dash會(huì)自動(dòng)幫我們分好頁(yè),并配上翻頁(yè)部件:
app1.py
- import dash
- import dash_bootstrap_components as dbc
- import dash_table
- import seaborn as sns
- df = sns.load_dataset('tips')
- df.insert(0, '#', df.index)
- app = dash.Dash(__name__)
- app.layout = dbc.Container(
- [
- dash_table.DataTable(
- id='dash-table',
- data=df.to_dict('records'),
- columns=[
- {'name': column, 'id': column}
- for column in df.columns
- ],
- page_size=15, # 設(shè)置單頁(yè)顯示15行記錄行數(shù)
- style_header={
- 'font-family': 'Times New Romer',
- 'font-weight': 'bold',
- 'text-align': 'center'
- },
- style_data={
- 'font-family': 'Times New Romer',
- 'text-align': 'center'
- }
- )
- ],
- style={
- 'margin-top': '50px'
- }
- )
- if __name__ == '__main__':
- app.run_server(debug=True)

圖2
2.1.2 后端分頁(yè)
雖然前端分頁(yè)簡(jiǎn)單易用,但當(dāng)我們的數(shù)據(jù)很大時(shí),強(qiáng)行使用前端分頁(yè)會(huì)給「網(wǎng)絡(luò)傳輸」和「瀏覽器端」帶來(lái)不小的延遲和內(nèi)存壓力,嚴(yán)重影響用戶體驗(yàn),因此Dash貼心地為我們準(zhǔn)備了「后端分頁(yè)」方式。
這時(shí)首先我們得為DataTable設(shè)置參數(shù)page_action='custom',這是使用后端分頁(yè)的先決條件,接下來(lái)我們需要認(rèn)識(shí)一些新的參數(shù):
page_current,int型,對(duì)應(yīng)當(dāng)前翻到的頁(yè)碼;
page_count,int型,對(duì)應(yīng)顯示的總頁(yè)數(shù);
我們?cè)谑褂谩负蠖朔猪?yè)」時(shí),實(shí)際上就是通過(guò)用戶當(dāng)前翻到的頁(yè)碼,以及設(shè)定的page_size,來(lái)動(dòng)態(tài)地在翻頁(yè)后加載對(duì)應(yīng)批次的數(shù)據(jù),并控制顯示的總頁(yè)數(shù),參考下面這個(gè)簡(jiǎn)單的例子:
app2.py
- import dash
- import dash_bootstrap_components as dbc
- import dash_table
- from dash.dependencies import Input, Output
- import seaborn as sns
- import pandas as pd
- from tqdm import tqdm
- # 壓力測(cè)試
- df = pd.concat([sns.load_dataset('tips') for _ in tqdm(range(1000))], ignore_index=True)
- df.insert(0, '#', df.index)
- app = dash.Dash(__name__)
- app.layout = dbc.Container(
- [
- dbc.Spinner(
- dash_table.DataTable(
- id='dash-table',
- columns=[
- {'name': column, 'id': column}
- for column in df.columns
- ],
- page_size=15, # 設(shè)置單頁(yè)顯示15行記錄行數(shù)
- page_action='custom',
- page_current=0,
- style_header={
- 'font-family': 'Times New Romer',
- 'font-weight': 'bold',
- 'text-align': 'center'
- },
- style_data={
- 'font-family': 'Times New Romer',
- 'text-align': 'center'
- }
- )
- )
- ],
- style={
- 'margin-top': '50px'
- }
- )
- @app.callback(
- [Output('dash-table', 'data'),
- Output('dash-table', 'page_count')],
- [Input('dash-table', 'page_current'),
- Input('dash-table', 'page_size')]
- )
- def refresh_page_data(page_current, page_size):
- return df.iloc[page_current * page_size:(page_current + 1) * page_size].to_dict('records'), 1 + df.shape[
- 0] // page_size
- if __name__ == '__main__':
- app.run_server(debug=True)
可以看到,即使我們完整的數(shù)據(jù)集被我concat到24萬(wàn)行,加載應(yīng)用以及網(wǎng)頁(yè)內(nèi)翻頁(yè)時(shí)依然輕松自如毫無(wú)壓力,在實(shí)際應(yīng)用中你還可以將翻頁(yè)部分改成受到LIMIT與OFFSET控制的數(shù)據(jù)庫(kù)查詢過(guò)程,使得應(yīng)用運(yùn)行的更加快速高效:

圖3
2.2 對(duì)單元格內(nèi)容進(jìn)行編輯
講完了分頁(yè)翻頁(yè),接下來(lái)我們來(lái)學(xué)習(xí)dash_table中更加強(qiáng)大的功能——單元格內(nèi)容編輯。
一個(gè)現(xiàn)代化的web應(yīng)用當(dāng)然不能局限于僅僅查看數(shù)據(jù)這么簡(jiǎn)單,Dash同樣賦予了我們雙擊數(shù)據(jù)表單元格進(jìn)行數(shù)據(jù)編輯的能力,首先得設(shè)置參數(shù)editable=True,即開(kāi)啟表格編輯模式,接下來(lái)就可以對(duì)數(shù)據(jù)區(qū)域單元格進(jìn)行任意的雙擊選中編輯。
不過(guò)Dash默認(rèn)的單元格被選中的樣式忒丑了(是粉色的你敢信),因此我們可以利用下面的參數(shù)設(shè)置方式來(lái)自定義美化:
- style_data_conditional=[
- {
- # 對(duì)選中狀態(tài)下的單元格進(jìn)行自定義樣式
- "if": {"state": "selected"},
- "background-color": "#b3e5fc",
- "border": "none"
- },
- ]
來(lái)看一個(gè)形象的例子,我們對(duì)「前端分頁(yè)」方式渲染出的表格進(jìn)行隨意的修改,并在下方對(duì)利用pandas的compare比較出的數(shù)據(jù)框之間的差異結(jié)果進(jìn)行打?。?/p>
app3.py
- import dash
- import dash_html_components as html
- import dash_core_components as dcc
- import dash_bootstrap_components as dbc
- import dash_table
- from dash.dependencies import Input, Output
- import seaborn as sns
- import pandas as pd
- df = sns.load_dataset('tips')
- df.insert(0, '#', df.index)
- app = dash.Dash(__name__)
- app.layout = dbc.Container(
- [
- dash_table.DataTable(
- id='dash-table',
- data=df.to_dict('records'),
- columns=[
- {'name': column, 'id': column}
- for column in df.columns
- ],
- fixed_rows={'headers': True},
- page_size=15,
- editable=True,
- style_header={
- 'font-family': 'Times New Romer',
- 'font-weight': 'bold',
- 'text-align': 'center'
- },
- style_data={
- 'font-family': 'Times New Romer',
- 'text-align': 'center'
- },
- style_data_conditional=[
- {
- # 對(duì)選中狀態(tài)下的單元格進(jìn)行自定義樣式
- "if": {"state": "selected"},
- "background-color": "#b3e5fc",
- "border": "none"
- },
- ]
- ),
- html.H4('與原表格內(nèi)容比較:', style={'margin-top': '50px'}),
- dcc.Markdown(
- '無(wú)差別',
- id='markdown',
- dangerously_allow_html=True
- )
- ],
- style={
- 'margin-top': '50px'
- }
- )
- @app.callback(
- Output('markdown', 'children'),
- Input('dash-table', 'data'),
- prevent_initial_call=True
- )
- def compare_difference(dash_table_data):
- print(pd.DataFrame(dash_table_data))
- return df.compare(pd.DataFrame(dash_table_data)).to_html()
- if __name__ == '__main__':
- app.run_server(debug=True)
可以看到,我們成功地對(duì)指定單元格元素進(jìn)行了修改。

圖4
3 開(kāi)發(fā)數(shù)據(jù)庫(kù)內(nèi)容在線更新工具
在學(xué)習(xí)完今天的內(nèi)容之后,我們就可以開(kāi)發(fā)一個(gè)簡(jiǎn)單的,可在線自由修改并同步變動(dòng)到數(shù)據(jù)庫(kù)的小工具,這里我們以MySQL數(shù)據(jù)庫(kù)為例,對(duì)示例表進(jìn)行修改和更新:
首先我們利用下列代碼向示例數(shù)據(jù)庫(kù)中新建表格tips:
- from sqlalchemy import create_engine
- import seaborn as sns
- df = sns.load_dataset('tips')
- df.insert(0, '#', df.index)
- engine = create_engine('mysql+pymysql://root:mysql@localhost/DASH')
- df.to_sql('tips', con=engine, if_exists='replace', index=False)
圖5
接下來(lái)我們就以創(chuàng)建好的tips表為例,開(kāi)發(fā)一個(gè)Dash應(yīng)用,進(jìn)行數(shù)據(jù)的修改和更新到數(shù)據(jù)庫(kù):

圖6
效果非常的不錯(cuò),你可以在我這個(gè)簡(jiǎn)單示例的基礎(chǔ)上,拓展更多新功能,也可以采取后端分頁(yè)+條件修改的方式來(lái)應(yīng)對(duì)大型數(shù)據(jù)表的修改,全部代碼如下:
app4.py
- import dash
- import dash_bootstrap_components as dbc
- import dash_core_components as dcc
- import dash_html_components as html
- import dash_table
- from dash.dependencies import Input, Output, State
- from sqlalchemy import create_engine
- import pandas as pd
- engine = create_engine('mysql+pymysql://root:mysql@localhost/DASH')
- app = dash.Dash(__name__)
- app.layout = dbc.Container(
- [
- dbc.Row(
- [
- dbc.Col(dbc.Button('更新數(shù)據(jù)表', id='refresh-tables', style={'width': '100%'}), width=2),
- dbc.Col(dcc.Dropdown(id='table-select', style={'width': '100%'}), width=2)
- ]
- ),
- html.Hr(),
- dash_table.DataTable(
- id='dash-table',
- editable=True,
- page_size=15,
- style_header={
- 'font-family': 'Times New Romer',
- 'font-weight': 'bold',
- 'text-align': 'center'
- },
- style_data={
- 'font-family': 'Times New Romer',
- 'text-align': 'center'
- },
- style_data_conditional=[
- {
- # 對(duì)選中狀態(tài)下的單元格進(jìn)行自定義樣式
- "if": {"state": "selected"},
- "background-color": "#b3e5fc",
- "border": "none"
- },
- ]
- ),
- dbc.Button('同步變動(dòng)到數(shù)據(jù)庫(kù)', id='update-tables', style={'display': 'none'}),
- html.P(id='message')
- ],
- style={
- 'margin-top': '50px'
- }
- )
- @app.callback(
- Output('table-select', 'options'),
- Input('refresh-tables', 'n_clicks')
- )
- def refresh_tables(n_clicks):
- if n_clicks:
- return [
- {
- 'label': table,
- 'value': table
- }
- for table in pd.read_sql_query('SHOW TABLES', con=engine)['Tables_in_dash']
- ]
- return dash.no_update
- @app.callback(
- [Output('dash-table', 'data'),
- Output('dash-table', 'columns'),
- Output('update-tables', 'style')],
- Input('table-select', 'value')
- )
- def render_dash_table(value):
- if value:
- df = pd.read_sql_table(value, con=engine)
- return df.to_dict('records'), [
- {'name': column, 'id': column}
- for column in df.columns
- ], {'margin-top': '25px'}
- else:
- return [], [], {'display': 'none'}
- @app.callback(
- [Output('message', 'children'),
- Output('message', 'style')],
- Input('update-tables', 'n_clicks'),
- [State('dash-table', 'data'),
- State('table-select', 'value')]
- )
- def update_to_database(n_clicks, data, value):
- if n_clicks:
- try:
- pd.DataFrame(data).to_sql(value, con=engine, if_exists='replace', index=False)
- return '更新成功!', {'color': 'green'}
- except Exception as e:
- return f'更新失敗!{e}', {'color': 'red'}
- return dash.no_update
- if __name__ == '__main__':
- app.run_server(debug=True)






















