(數據科學學習手札115)Python+Dash快速web應用開發——交互表格篇(上)

本文示例代碼已上傳至我的Github倉庫//github.com/CNFeffery/DataScienceStudyNotes

1 簡介

   這是我的系列教程Python+Dash快速web應用開發的第十二期,在以前撰寫過的靜態部件篇(中)那期教程中,我們介紹過在Dash中創建靜態表格的方法。

  而在實際的使用中,我們很多時候在網頁中渲染的表格不僅僅是為了對數據進行展示,還需要更多交互能力,譬如按列排序動態修改表中數值等特性,以及對大型數據表快速渲染查看能力,諸如此類眾多的交互功能在Dash自帶的dash_table中已經實現。

  而接下來的幾期,我們就將針對如何利用dash_table創建具有豐富交互功能的表格進行介紹,今天介紹的是dash_table的基礎使用方法。

圖1

2 dash_table基礎使用

  作為Dash自帶的拓展庫,我們通過下列語句導入dash_table

import dash_table

  接着像之前使用其他的Dash部件一樣,在定義layout時將dash_table.DataTable()對象置於我們定義的合適位置即可,可參考下面的例子配合pandasDataFrame來完成最簡單的表格的渲染。

  其中參數columns用於設置每一列對應的名稱與id屬性,data接受由數據框轉化而成的特殊格式數據,virtualization設置為True代表使用了虛擬化技術來加速網頁中大量表格行數據的渲染:

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__)

# 載入演示數據集
df = sns.load_dataset('iris')
# 創建行下標列
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)

  如果你對數據的展示完全沒要求,看個數就行,那上述的這套基礎的參數設置你就可以當成萬金油來使用,而如果你覺得dash_table.DataTable默認太丑了(大實話),那麼請繼續閱讀今天的教程。

圖2

2.1 自定義表格基礎樣式

  針對DataTable所渲染出的表格的幾個基礎構成部分,我們可以使用到的用於修改表格樣式的參數有style_tablestyle_cellstyle_headerstyle_data等:

  • 使用style_table來自定義表格外層容器樣式

  參數style_table用於對整個表格最外層的容器樣式傳入css鍵值對進行修改,一般用來設定表格的高度、寬度、周圍留白或對齊等屬性:

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__)

# 載入演示數據集
df = sns.load_dataset('iris')
# 創建行下標列
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)

圖3

  • 使用style_cell、style_header與style_data定義單元格樣式

  不同於style_table,使用style_cell可以傳入css將樣式應用到所有單元格,而style_headerstyle_data則更加有針對性,可分別對標題單元格、數據單元格進行設置:

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__)

# 載入演示數據集
df = sns.load_dataset('iris')
# 創建行下標列
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)

圖4

  • 條件樣式設置

  除了像上文所演示的那樣針對某一類表格構成元素進行整體樣式設置外,DataTable還為我們提供了條件樣式設置,比如我們想為特殊的幾列單獨設置樣式,或者為奇數下標與偶數下標行設置不同的樣式,就可以使用到這一特性。

  這在DataTable中我們可以利用style_header_conditionalstyle_data_conditional來傳入列表,列表中每個元素都可看做是帶有額外if鍵值對的css參數字典,而這個if鍵值對的值亦為一個字典,其接受的鍵值對種類豐富,我們今天先來介紹column_idrow_index,它們分別用來指定對應idheader與整行單元格。

  參考下面這個例子,我們分別特殊設置#列的表頭與奇數行的樣式:

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__)

# 載入演示數據集
df = sns.load_dataset('iris')
# 創建行下標列
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': {
                            # 選中行下標為奇數的行
                            'row_index': 'odd'
                        },
                        'background-color': '#cfd8dc'
                    }
                ]
            )
        ],
        style={
            'margin-top': '100px'
        }
    )
)

if __name__ == '__main__':
    app.run_server(debug=True)

圖5

  • 隱藏所有豎直框線

  設置參數style_as_list_view為True可以隱藏所有豎向的框線,app4設置之後的效果如下:

圖6

3 動手製作一個數據入庫應用

  學習完今天的內容之後,我們來動手寫一個簡單的數據入庫應用,通過拖入本地csv文件以及填寫入庫表名,來實現對上傳數據的預覽與數據庫導入,後端會自動檢查用戶輸入的數據表名稱是否合法,並自動檢測上傳csv文件的文件編碼。

  下面就是該應用工作時的情景,其中因為test表在庫中已存在,所以會被檢測出不合法:

圖7

  而當上傳的數據錶行數較多時,右下角會自動出現分頁部件,我們將在下一期中進行討論,完整代碼如下:

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='點擊或拖動文件到此進行上傳!',
                text_completed='已完成上傳文件:',
                cancel_button=True,
                pause_button=True),
            html.Hr(),
            dbc.Form(
                [
                    dbc.FormGroup(
                        [
                            dbc.Label("設置入庫表名", html_for="table-name"),
                            dbc.Input(
                                id='table-name',
                                autoComplete='off'
                            ),
                            dbc.FormText(
                                "表名只允許包含大小寫字母、下劃線或數字,且不能以數字開頭,同時請注意表名是否與庫中現有表重複!", 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('預覽至多前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': {
                                    # 選中行下標為奇數的行
                                    '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:

        # 查詢庫中已存在非系統表名
        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):
    '''
    控制預覽表格的渲染
    '''
    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)

  以上就是本文的全部內容,歡迎在評論區與我進行討論~

Tags: