【Python】太6了!用Python快速開發(fā)數(shù)據(jù)庫入庫系統(tǒng)
本文示例代碼已上傳至我的
?Github倉庫https://github.com/CNFeffery/DataScienceStudyNotes
1 簡介
這是我的系列教程「Python+Dash快速web應(yīng)用開發(fā)」的第十二期,在以前撰寫過的靜態(tài)部件篇(中)那期教程中,我們介紹過在Dash中創(chuàng)建靜態(tài)表格的方法。
而在實際的使用中,我們很多時候在網(wǎng)頁中渲染的表格不僅僅是為了對數(shù)據(jù)進(jìn)行展示,還需要更多交互能力,譬如「按列排序」、「動態(tài)修改表中數(shù)值」等特性,以及對「大型數(shù)據(jù)表」的「快速渲染查看」能力,諸如此類眾多的交互功能在Dash自帶的dash_table中已經(jīng)實現(xiàn)。
而接下來的幾期,我們就將針對如何利用dash_table創(chuàng)建具有豐富交互功能的表格進(jìn)行介紹,今天介紹的是dash_table的基礎(chǔ)使用方法。

2 dash_table基礎(chǔ)使用
作為Dash自帶的拓展庫,我們通過下列語句導(dǎo)入dash_table:
import dash_table
接著像之前使用其他的Dash部件一樣,在定義layout時將dash_table.DataTable()對象置于我們定義的合適位置即可,可參考下面的例子配合pandas的DataFrame來完成最簡單的表格的渲染。
其中參數(shù)columns用于設(shè)置每一列對應(yīng)的名稱與id屬性,data接受由數(shù)據(jù)框轉(zhuǎn)化而成的特殊格式數(shù)據(jù),virtualization設(shè)置為True代表使用了「虛擬化」技術(shù)來加速網(wǎng)頁中大量表格行數(shù)據(jù)的渲染:
?app1.py
?
import dash
import dash_html_components as html
import dash_bootstrap_components as dbc
import dash_table
import seaborn as sns
app = dash.Dash(__name__)
# 載入演示數(shù)據(jù)集
df = sns.load_dataset('iris')
# 創(chuàng)建行下標(biāo)列
df.insert(loc=0, column='#', value=df.index)
app.layout = html.Div(
dbc.Container(
dash_table.DataTable(
columns=[{'name': column, 'id': column} for column in df.columns],
data=df.to_dict('records'),
virtualization=True
),
style={
'margin-top': '100px'
}
)
)
if __name__ == '__main__':
app.run_server(debug=True)
如果你對數(shù)據(jù)的展示完全沒要求,看個數(shù)就行,那上述的這套基礎(chǔ)的參數(shù)設(shè)置你就可以當(dāng)成萬金油來使用,而如果你覺得dash_table.DataTable「默認(rèn)」太丑了(大實話),那么請繼續(xù)閱讀今天的教程。

2.1 自定義表格基礎(chǔ)樣式
針對DataTable所渲染出的表格的幾個基礎(chǔ)構(gòu)成部分,我們可以使用到的用于修改表格樣式的參數(shù)有style_table、style_cell、style_header、style_data等:
「使用style_table來自定義表格外層容器樣式」
參數(shù)style_table用于對整個表格最外層的容器樣式傳入css鍵值對進(jìn)行修改,一般用來設(shè)定表格的高度、寬度、周圍留白或?qū)R等屬性:
?app2.py
?
import dash
import dash_html_components as html
import dash_bootstrap_components as dbc
import dash_table
import seaborn as sns
app = dash.Dash(__name__)
# 載入演示數(shù)據(jù)集
df = sns.load_dataset('iris')
# 創(chuàng)建行下標(biāo)列
df.insert(loc=0, column='#', value=df.index)
app.layout = html.Div(
dbc.Container(
[
dash_table.DataTable(
columns=[{'name': column, 'id': column} for column in df.columns],
data=df.to_dict('records'),
virtualization=True,
style_table={
'height': '200px',
'margin-top': '100px'
}
),
html.Hr(),
dash_table.DataTable(
columns=[{'name': column, 'id': column} for column in df.columns],
data=df.to_dict('records'),
virtualization=True,
style_table={
'height': '200px',
'margin-left': '80px',
'width': '300px'
}
),
html.Hr(),
dash_table.DataTable(
columns=[{'name': column, 'id': column} for column in df.columns],
data=df.to_dict('records'),
virtualization=True,
style_table={
'height': '150px',
'width': '50%',
'margin-left': '50%'
}
)
],
style={
'background-color': '#bbdefb'
}
)
)
if __name__ == '__main__':
app.run_server(debug=True)

「使用style_cell、style_header與style_data定義單元格樣式」
不同于style_table,使用style_cell可以傳入css將樣式應(yīng)用到所有「單元格」,而style_header與style_data則更加有針對性,可分別對標(biāo)題單元格、數(shù)據(jù)單元格進(jìn)行設(shè)置:
?app3.py
?
import dash
import dash_html_components as html
import dash_bootstrap_components as dbc
import dash_table
import seaborn as sns
app = dash.Dash(__name__)
# 載入演示數(shù)據(jù)集
df = sns.load_dataset('iris')
# 創(chuàng)建行下標(biāo)列
df.insert(loc=0, column='#', value=df.index)
app.layout = html.Div(
dbc.Container(
[
dash_table.DataTable(
columns=[{'name': column, 'id': column} for column in df.columns],
data=df.to_dict('records'),
virtualization=True,
style_table={
'height': '300px'
},
style_cell={
'background-color': '#fff9c4',
'font-family': 'Times New Romer',
'text-align': 'center'
}
),
html.Hr(),
dash_table.DataTable(
columns=[{'name': column, 'id': column} for column in df.columns],
data=df.to_dict('records'),
virtualization=True,
style_table={
'height': '300px'
},
style_header={
'background-color': '#b3e5fc',
'font-family': 'Times New Romer',
'font-weight': 'bold',
'font-size': '17px',
'text-align': 'left'
},
style_data={
'font-family': 'Times New Romer',
'text-align': 'left'
}
)
],
style={
'margin-top': '100px'
}
)
)
if __name__ == '__main__':
app.run_server(debug=True)

「條件樣式設(shè)置」
除了像上文所演示的那樣針對某一類表格構(gòu)成元素進(jìn)行整體樣式設(shè)置外,DataTable還為我們提供了條件樣式設(shè)置,比如我們想為特殊的幾列單獨設(shè)置樣式,或者為奇數(shù)下標(biāo)與偶數(shù)下標(biāo)行設(shè)置不同的樣式,就可以使用到這一特性。
這在DataTable中我們可以利用style_header_conditional與style_data_conditional來傳入列表,列表中每個元素都可看做是帶有額外if鍵值對的css參數(shù)字典,而這個if鍵值對的值亦為一個字典,其接受的鍵值對種類豐富,我們今天先來介紹column_id與row_index,它們分別用來指定對應(yīng)「id」的header與整行單元格。
參考下面這個例子,我們分別特殊設(shè)置#列的表頭與奇數(shù)行的樣式:
?app4.py
?
import dash
import dash_html_components as html
import dash_bootstrap_components as dbc
import dash_table
import seaborn as sns
app = dash.Dash(__name__)
# 載入演示數(shù)據(jù)集
df = sns.load_dataset('iris')
# 創(chuàng)建行下標(biāo)列
df.insert(loc=0, column='#', value=df.index)
app.layout = html.Div(
dbc.Container(
[
dash_table.DataTable(
columns=[{'name': column, 'id': column} for column in df.columns],
data=df.to_dict('records'),
virtualization=True,
style_table={
'height': '500px'
},
style_cell={
'font-family': 'Times New Romer',
'text-align': 'center'
},
style_header_conditional=[
{
'if': {
# 選定列id為#的列
'column_id': '#'
},
'font-weight': 'bold',
'font-size': '24px'
}
],
style_data_conditional=[
{
'if': {
# 選中行下標(biāo)為奇數(shù)的行
'row_index': 'odd'
},
'background-color': '#cfd8dc'
}
]
)
],
style={
'margin-top': '100px'
}
)
)
if __name__ == '__main__':
app.run_server(debug=True)

「隱藏所有豎直框線」
設(shè)置參數(shù)style_as_list_view為True可以隱藏所有豎向的框線,app4設(shè)置之后的效果如下:

3 動手制作一個數(shù)據(jù)入庫應(yīng)用
學(xué)習(xí)完今天的內(nèi)容之后,我們來動手寫一個簡單的數(shù)據(jù)入庫應(yīng)用,通過拖入本地csv文件以及填寫入庫表名,來實現(xiàn)對上傳數(shù)據(jù)的預(yù)覽與數(shù)據(jù)庫導(dǎo)入,后端會自動檢查用戶輸入的數(shù)據(jù)表名稱是否合法,并自動檢測上傳csv文件的文件編碼。
下面就是該應(yīng)用工作時的情景,其中因為test表在庫中已存在,所以會被檢測出不合法:

而當(dāng)上傳的數(shù)據(jù)表行數(shù)較多時,右下角會自動出現(xiàn)分頁部件,我們將在下一期中進(jìn)行討論,完整代碼如下:
?app5.py
?
import dash
import dash_html_components as html
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State
import dash_table
import dash_uploader as du
import re
import os
import pandas as pd
from sqlalchemy import create_engine
import cchardet as chardet # 用于自動識別文件編碼
postgres_url = 'postgresql://postgres:CUDLCUDL@localhost:5432/Dash'
engine = create_engine(postgres_url)
app = dash.Dash(__name__)
du.configure_upload(app, 'upload')
app.layout = html.Div(
dbc.Container(
[
du.Upload(
id='upload',
filetypes=['csv'],
text='點擊或拖動文件到此進(jìn)行上傳!',
text_completed='已完成上傳文件:',
cancel_button=True,
pause_button=True),
html.Hr(),
dbc.Form(
[
dbc.FormGroup(
[
dbc.Label("設(shè)置入庫表名", html_for="table-name"),
dbc.Input(
id='table-name',
autoComplete='off'
),
dbc.FormText(
"表名只允許包含大小寫字母、下劃線或數(shù)字,且不能以數(shù)字開頭,同時請注意表名是否與庫中現(xiàn)有表重復(fù)!", color="secondary"
),
dbc.FormFeedback(
"表名合法!", valid=True
),
dbc.FormFeedback(
"表名不合法!",
valid=False,
),
]
),
dbc.FormGroup(
[
dbc.Button('提交入庫', id='commit', outline=True)
]
)
],
style={
'background-color': 'rgba(224, 242, 241, 0.4)'
}
),
dbc.Spinner(
[
html.P(id='commit-status-message', style={'color': 'red'}),
dbc.Label('預(yù)覽至多前10000行', html_for='uploaded-table'),
dash_table.DataTable(
id='uploaded-table',
style_table={
'height': '400px'
},
virtualization=True,
style_as_list_view=True,
style_cell={
'font-family': 'Times New Romer',
'text-align': 'center'
},
style_header={
'font-weight': 'bold'
},
style_data_conditional=[
{
'if': {
# 選中行下標(biāo)為奇數(shù)的行
'row_index': 'odd'
},
'background-color': '#cfd8dc'
}
]
)
]
)
],
style={
'margin-top': '30px'
}
)
)
@app.callback(
[Output('table-name', 'invalid'),
Output('table-name', 'valid')],
Input('table-name', 'value')
)
def check_table_name(value):
''''
檢查表名是否合法
'''
if value:
# 查詢庫中已存在非系統(tǒng)表名
exists_table_names = (
pd
.read_sql('''SELECT tablename FROM pg_tables''', con=engine)
.query('~(tablename.str.startswith("pg") or tablename.str.startswith("sql_"))')
)
if (re.findall('^[A-Za-z0-9_]+$', value)[0].__len__() == value.__len__()) \
and not re.findall('^\d', value) \
and value not in exists_table_names['tablename'].tolist():
return False, True
return True, False
return dash.no_update
@app.callback(
Output('commit-status-message', 'children'),
Input('commit', 'n_clicks'),
[State('table-name', 'valid'),
State('table-name', 'value'),
State('upload', 'isCompleted'),
State('upload', 'fileNames'),
State('upload', 'upload_id')]
)
def control_table_commit(n_clicks,
table_name_valid,
table_name,
isCompleted,
fileNames,
upload_id):
'''
控制已上傳表格的入庫
'''
if all([n_clicks, table_name_valid, table_name, isCompleted, fileNames, upload_id]):
uploaded_df = pd.read_csv(os.path.join('upload', upload_id, fileNames[0]),
encoding=chardet.detect(open(os.path.join('upload', upload_id, fileNames[0]),
'rb').read())['encoding'])
uploaded_df.to_sql(table_name, con=engine)
return '入庫成功!'
return dash.no_update
@app.callback(
[Output('uploaded-table', 'data'),
Output('uploaded-table', 'columns')],
Input('upload', 'isCompleted'),
[State('upload', 'fileNames'),
State('upload', 'upload_id')]
)
def render_table(isCompleted, fileNames, upload_id):
'''
控制預(yù)覽表格的渲染
'''
if isCompleted:
uploaded_df = pd.read_csv(os.path.join('upload', upload_id, fileNames[0]),
encoding=chardet.detect(open(os.path.join('upload', upload_id, fileNames[0]),
'rb').read())['encoding']).head(10000)
uploaded_df.insert(0, '#', range(uploaded_df.shape[0]))
return uploaded_df.to_dict('record'), [{'name': column, 'id': column} for column in uploaded_df.columns]
return dash.no_update
if __name__ == '__main__':
app.run_server(debug=True)
以上就是本文的全部內(nèi)容,歡迎在評論區(qū)與我進(jìn)行討論~
往期精彩回顧
本站qq群851320808,加入微信群請掃碼:
