(資料科學學習手札121)Python+Dash快速web應用開發——專案結構篇

費弗裡發表於2021-05-16

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

1 簡介

   這是我的系列教程Python+Dash快速web應用開發的第十八期,通過前面十七期的內容,如果你有用心學習的話,那麼恭喜你已經具備使用Dash編寫常規web應用的能力了。

  而在使用Dash開發web應用時,頁面內容和功能邏輯簡單倒還好,一旦你的功能內容開始複雜化系統化起來,那麼像過往文章示例中簡單一個app.py存放所有功能程式碼就不適用了。

  而在今天的教程中,我就將為大家介紹我在日常使用過程中總結出的一套針對Dash專案的前後端分離的專案結構基礎正規化,並以搭建全國七普部分資料視覺化看板為例,供大家參考借鑑,從而更有條理的編寫和管理Dash應用專案。

(資料科學學習手札121)Python+Dash快速web應用開發——專案結構篇
圖1

2 Dash專案結構基礎正規化

2.1 總體結構一覽

  開門見山,我們直接先來一覽今天要介紹的Dash基礎專案結構:

+ dash_demo_project/
   + assets/
      + css/
      + img/
      + js/
       • favicon.ico
   + callbacks/
   + models/
   + views/
    • app.py
    • server.py

  在不考慮外部引數匯入使用者登陸驗證應用部署等額外配置檔案及功能內容的前提下,上面的結構就可以滿足常規Dash應用的需求了。

  下面我們基於和鯨上獲取到的第七次全國人口普查公開資料集,以搭建下面這個簡單的資料視覺化看板為例,介紹上述各部分的實際功能意義(完整專案原始碼見文章開頭連結)。

(資料科學學習手札121)Python+Dash快速web應用開發——專案結構篇
圖2

2.2 各部分結構介紹

2.2.1 再談assets

  在頁面佈局篇中我們提到過assets目錄,它是官方推薦的用於存放我們的Dash應用所依賴靜態資原始檔的目錄,如依賴的cssjsfavicon.ico、各種圖片及字型等靜態資源,在本文的視覺化看板案例中,assets目錄資源放置情況如下:

+ assets/
  + css/
      • bootstrap.min.css
      • custom.css
  + img/
      • wxgzh.png
      • zsxq.png
  + js/
   • favicon.ico

  其中img目錄下存放的是首頁的兩張二維碼圖片,在Dash中可以配合Img()get_asset_url()來獲取assets目錄下指定檔案路徑並渲染:

html.Img(src=app.get_asset_url('img/zsxq.png'), style={'width': '100%'})

  而css目錄下則放置了dash_bootstrap-components所依賴的css檔案,而custom.css則是我自己編寫的一些用於樣式美化的css程式碼:

.nav-link.active {
    background-color: #4fc3f7!important;
}

#index-desc > * {
    font-size: 26px;
}

.table td, .table th {
    text-align: center;
}

  直接放置於assets根目錄下的favicon.ico則用來替換Dash預設的網頁圖示:

(資料科學學習手札121)Python+Dash快速web應用開發——專案結構篇
圖3

  你可以根據自己Dash專案的實際需求靈活變通,譬如需要用到echarts就可以在js目錄下放置echarts.min.js檔案。

2.2.2 在server.py中例項化配置Dash物件

  跟以往的例子不同,在嚴謹的Dash工程下,推薦構建單獨的server.py檔案來完成對Dash物件的例項化配置等工作,在今天的視覺化看板案例中server.py比較簡單,內容如下:

import dash

app = dash.Dash(
    __name__,
    suppress_callback_exceptions=True
)

# 設定網頁title
app.title = '七普部分資料看板'

server = app.server

2.2.3 在app.py中編寫前端骨架與路由

  如果你的Dash專案非常簡單,那麼from server import app之後,就可以像往常一樣在app.py中組織你的前端與回撥部分內容。

  但如果你的Dash專案功能較為複雜,亦或是url聯結的頁面較多時,就可以只在app.py中編寫前端layout骨架,包含了必要的Location()部件、保持不變的前端部分以及由url變化所觸發的頁面內容容器,譬如今天的視覺化看板中左側邊欄部分以及Location()監聽部件:

app.layout = html.Div(
    [
        # 監聽url變化
        dcc.Location(id='url'),
        html.Div(
            [
                # 標題區域
                html.Div(
                    html.H3(
                        '七普部分資料看板',
                        style={
                            'marginTop': '20px',
                            'fontFamily': 'SimSun',
                            'fontWeight': 'bold'
                        }
                    ),
                    style={
                        'textAlign': 'center',
                        'margin': '0 10px 0 10px',
                        'borderBottom': '2px solid black'
                    }
                ),

                # 子頁面區域
                html.Hr(),

                dbc.Nav(
                    [
                        dbc.NavLink('首頁', href='/', active="exact"),
                        dbc.NavLink('年齡結構', href='/age', active="exact"),
                        dbc.NavLink('性別結構', href='/sex', active="exact"),
                        dbc.NavLink('六普vs七普', href='/statistics', active="exact"),
                    ],
                    vertical=True,
                    pills=True
                )
            ],
            style={
                'flex': 'none',
                'width': '300px',
                'backgroundColor': '#fafafa'
            }
        ),
        html.Div(
            id='page-content',
            style={
                'flex': 'auto'
            }
        )
    ],
    style={
        'width': '100vw',
        'height': '100vh',
        'display': 'flex'
    }
)

  同樣地,也推薦將監聽url變化從而渲染不同頁面的路由回撥一併寫在app.py中,方便後續的管理與升級:

# 路由總控
@app.callback(
    Output('page-content', 'children'),
    Input('url', 'pathname')
)
def render_page_content(pathname):
    if pathname == '/':
        return index_page

    elif pathname == '/age':
        return age_page

    elif pathname == '/sex':
        return sex_page

    elif pathname == '/statistics':
        return statistics_page

    return html.H1('您訪問的頁面不存在!')

2.2.4 在views子模組中構建多頁面前端內容

  在上一小節的路由回撥中你可能會好奇不同url下的返回值index_pageage_page等都是什麼,這些都構建在子模組views下:

+ views/
   • age.py
   • index.py
   • sex.py
   • statistics.py
   • __init__.py

  譬如其中之一的age.py內容如下:

import dash_html_components as html
import dash_core_components as dcc
import dash_bootstrap_components as dbc

import pandas as pd
import plotly.express as px

from models.age import Age

age_data = (
    pd.DataFrame(Age.fetch_all()).rename(columns={
        'region': '地區',
        'prop_0_to_14': '0到14歲人口占比',
        'prop_15_59': '15到59歲人口占比',
        'prop_60_above': '60歲以上人口占比',
        'prop_65_above': '65歲以上人口占比'
    })
)

fig = px.bar(age_data.melt(id_vars=['地區'],
                           value_vars=['0到14歲人口占比', '15到59歲人口占比', '60歲以上人口占比'],
                           var_name='年齡段',
                           value_name='佔比(%)'),
             y="地區", x="佔比(%)", color="年齡段", title="七普各地區人口年齡結構",
             color_discrete_map={
                 '0到14歲人口占比': '#0868ac',
                 '15到59歲人口占比': '#43a2ca',
                 '60歲以上人口占比': '#a8ddb5'
             },
             orientation='h')

fig.update_layout(
    font=dict(
        family="Times New Roman, SimSun"
    )
)
fig.update_layout(xaxis_range=[0, 100])

fig.update_layout(
    margin=dict(t=50, b=10)
)

age_page = html.Div(
    [
        html.Div(
            dbc.Table.from_dataframe(age_data, striped=True),
            style={
                'overflowY': 'auto',
                'flex': '1'
            }
        ),
        html.Div(
            dcc.Graph(figure=fig, style={'height': '100%'}),
            style={
                'flex': '1',
                'height': '100%'
            }
        )
    ],
    style={
        'display': 'flex',
        'height': '100%'
    }
)

  通過這種方式針對不同頁面構建相應的前端物件,從而在app.py中按照下列方式匯入就可以使用了:

from views.index import index_page
from views.age import age_page
from views.sex import sex_page
from views.statistics import statistics_page

2.2.5 在callbacks子模組中構建多頁面後端邏輯

  當你在views下構建的頁面內容中涉及到回撥互動的功能時,我推薦將對應的後端回撥邏輯拆分到callbacks子模組下同名檔案中,這樣非常便於編寫與維護。

  同時一定要記住在views下對應的前端子模組中,一定要匯入callbacks中對應的回撥子模組內部的至少一個物件,否則Dash在打包應用時是掃描不到相應的回撥函式內容進行編譯的,進而會導致應用啟動時回撥無效,譬如在views/statistics.py中我們就執行了from callbacks.statistics import statistics_data

2.2.6 在models子模組下定義資料模型

  前面說的很多內容都關乎Dash應用的構建,而當你的Dash應用依賴外部資料時,推薦的方式是類似flask專案那樣構建子模組models來定義資料模型,實現與資料庫的關聯。

  而我們今天的視覺化看板案例中就配合整合資料庫篇介紹的peewee相關知識,分別定義了資料模型對應了七普中的年齡結構性別結構以及六普七普對比資料表,並在viewscallbacks等涉及的子模組中匯入並呼叫,以年齡結構models/age.py為例:

from peewee import SqliteDatabase, Model
from peewee import CharField, FloatField

db = SqliteDatabase('models/age.db')


class Age(Model):
    # 地區,唯一
    region = CharField(unique=True)

    # 0-14歲佔比
    prop_0_to_14 = FloatField()

    # 15-59歲佔比
    prop_15_59 = FloatField()

    # 60歲及以上佔比
    prop_60_above = FloatField()

    # 65歲及以上佔比
    prop_65_above = FloatField()

    class Meta:
        database = db
        primary_key = False  # 禁止自動生成唯一id列

    @classmethod
    def fetch_all(cls):
        return list(cls.select().dicts())

  而本文案例中涉及到的資料視覺化內容均由plotlyplotly.express實現,關於這部分內容我會在之後的進階教程中加以概括。

  本文完整專案案例原始碼+附件你可以在文章開頭連結頁面檢視和下載。

  下期我將帶大家學習如何在LinuxWindows等系統中正式部署Dash應用,敬請期待。


  以上就是本文的全部內容,歡迎在評論區發表你的意見和想法。

相關文章