專題作業-職缺小幫手
一、專題簡介
這是我獨力製作的一個小專題,專題內容是一個簡單的職缺分析網站。使用者可在此網站上輸入想要搜尋的職缺關鍵字後,系統會至104人力銀行搜尋相關職缺,並將職缺分析結果呈現給使用者觀看。
- 專案相關的程式碼可參考我的Github
- 網站畫面
- 使用者輸入想要搜尋的職缺及爬取的搜尋頁數後,網站會去執行爬蟲程式,並在畫面上呈現目前爬蟲程式運作的狀況:
- 使用者在查詢職缺後,可以觀看該職缺的分析結果,此處主要呈現一些資料探索式分析的結果:
- 使用者輸入想要搜尋的職缺及爬取的搜尋頁數後,網站會去執行爬蟲程式,並在畫面上呈現目前爬蟲程式運作的狀況:
網站主要是透過Python的Dash套件製作,資料庫為MySQL,另外我有包成docker-compose,能夠直接快速部署。
我會選擇用Dash的原因是它和我之前學的RStudio邏輯很像,可以快速部署網頁內容與視覺化呈現。
這篇文章有對Dash與Rstudio兩個工具進行比較。 Python Dash vs. R Shiny – Which To Choose in 2021 and Beyond
二、Dash簡介
Dash 是建構於 Plotly.js、React.js 與 Flask 之上的 Python 網頁應用程式框架,能夠將常見的使用者介面元件包含像是下拉式選單、滑桿或圖形>與 資料分析應用快速地連結起來,讓以 Python 為主的資料科學團隊不需要 JavaScript 也可以建立出具備高度互動性的圖表與儀表板。 (文字來源:數據交點-互動式圖表及 Python)
我自己在學習Dash主要是參考Plotly-Dash官方教學手冊,裡面共有6個章節可進行學習,把裡面的範例都看過一遍就可以開始實作Dash:
此處以Dash官方教學手冊的Dash App Layout With Figure and Slider範例,來簡單說明一下Dash運作的方式,程式碼執行後會產生互動式圖表:
# 載入Dash相關套件
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
# 載入Plotly繪圖套件
import plotly.express as px
# 載入pandas資料整理套件
import pandas as pd
# 讀取plotly提供的範例csv檔案
# 此檔案包含各國家在不同年代的人口數、預期壽命與人均GDP等資訊,為一個Panel Data
df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv')
# 要載入的css套件
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
# 建立一個Dash的app
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
# Dash-前台:設定Dash的版面(app.layout)
app.layout = html.Div([
# 互動式圖表放置處,此處有命名id為graph-with-slider
# 這個id很重要,要讓後台辨識用的
dcc.Graph(id='graph-with-slider'),
# 設定一個slider,讓使用者能夠拖曳年份
# 後台依據此slider目前選擇的年份繪製對應的圖表資訊
dcc.Slider(
# slider的id,後台辨識用的
id='year-slider',
# 使用者能夠拖曳slider最小值
min=df['year'].min(),
# 使用者能夠拖曳slider最大值
max=df['year'].max(),
# slider的默認值
value=df['year'].min(),
# slider的選項名稱
marks={str(year): str(year) for year in df['year'].unique()},
step=None
)
])
# Dash-後台
# callback函數:
# 輸入(Input)為前台畫面id='year-slider'的值,即使用者現在選取Slider的年份
# 輸出(Output)為圖表放置到前台畫面id='graph-with-slider'的位置
# 當Input有更動時,此函數即會執行,並將運作完的結果返回到Output
# 在這個範例中,當使用者調整Slider的年份,後台察覺前台有變化,
# 即會執行update_figure函數,依選取年份繪製出的圖形後,
# 輸出到前台畫面的圖片放置處
@app.callback(
Output('graph-with-slider', 'figure'),
Input('year-slider', 'value'))
def update_figure(selected_year):
# 選取前台使用者選取的年份
filtered_df = df[df.year == selected_year]
# 繪製圖表,此處繪製散佈圖,X軸為人均GDP,Y軸為預期壽命
fig = px.scatter(filtered_df, x="gdpPercap", y="lifeExp",
size="pop", color="continent", hover_name="country",
log_x=True, size_max=55)
# 降低圖片動畫速度
fig.update_layout(transition_duration=500)
# 輸出圖表,圖表放置在callback函數指定的Output id位置
return fig
# 執行主程式
if __name__ == '__main__':
app.run_server(debug=True)
從上述可知,在製作Dash時,想要給使用者看到的畫面內容放置在app.layout
內,需要互動的部分,則透過callback function
來達成。
三、專題遇到的問題與解決方法
實作過程當然也不是那麼的順利,這邊我將說明我在這個專案遇到問題的部分,以及我如何處理。
問題1. 決定網站版型
由於Dash套件本身沒有提供Bootstrap,如果要有響應式設計(RWD),需要額外安裝dash-bootstrap-components
套件,套件的官網有詳細的說明。
網站的版型我參考官網的Example,選擇Simple sidebar來模板來進行專題開發。
問題2. 文字雲圖片輸出到Dash畫面做法
依據wordcloud套件的範例程式碼,在Jupyter上執行文字雲的程式碼,可以直接輸出圖片。但如果在Dash中要將文字雲圖片透過callbacks傳回到前台,需要在進行額外的處理。
此處我參考stackoverflow的問題解法(how to show wordcloud image on dash web application),透過Base64編碼和解碼圖片,可順利執行出來。
問題3. 點選「查詢」按鈕後才動作
Dash中的callback只要接受到input的改變,就會馬上進函數運算後改變output。如果想要讓使用者按下「查詢」按鈕後才執行函數,可以參考Dash官網的範例: Button Basic Example。
在Basic Dash Callbacks教學頁中的Dash App With State章節也有提及,主要是在callback引數中利用State()
函數來避免被馬上執行,而按鈕的按鈕次數(n_clicks)做為Input()
,當使用者按下按鈕,按鈕次數一改變時,即會執行函數。
問題4. 下拉式選單動態選項
在職缺分析頁面,有提供一個下拉式的選單,這個選單的選項是要依據資料庫目前有存放的查詢資訊來呈現:
此問題的處理方法我用簡單的程式碼來作範例,我依官網的Dropdown範例進行修改,主要是透過後台的callback函數來更新前台的下拉式選單選項:
import random
import dash
import dash_html_components as html
import dash_core_components as dcc
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
# 前台畫面設定
app.layout = html.Div([
dcc.Dropdown(id='demo-dropdown'),
html.Button(id='submitButton', children='點我改變選項')
])
@app.callback(dash.dependencies.Output('demo-dropdown', 'options'),
dash.dependencies.Output('demo-dropdown', 'value'),
dash.dependencies.Input('submitButton', 'n_clicks'))
def update_dropdown(value):
# 產生5個隨機值
randomList = random.sample(range(1, 100), 5)
# 建立隨機5個選項
options = [{'label': '選項' + str(i), 'value': i} for i in randomList]
# 設定默認值
value = min(randomList)
# 返回dropdown選項與默認值
return options, value
if __name__ == '__main__':
app.run_server(debug=True)
問題5. Loading圖示功能
有時候後台計算的時間會比較長,為讓使用者知道後台正在計算而不是網站當掉,此時就需要有Loading的圖示來提醒使用者。
Dash在Loading的部分有提供dcc.Loading()函數,而在Dash Bootstrap套件中,則有dbc.Spinner()函數。
問題6. 輸出爬蟲程式運作資訊
由於爬蟲執行的時間會較長,為讓使用者能夠知道目前程式的執行進度,需要將目前程式執行的狀況輸出到前端畫面。
處理方法是每當使用者按下查詢按鈕時,會建立一個依當下timestamp檔名的log檔案,接下來爬蟲程式執行時,會將執行的資訊儲存至該log檔案。
而在前台部分,主要透過dcc.Interval()函數,來定時讀取log檔案內的資訊,並將結果返回至前端。
四、結語
這個專案大概做三個禮拜的時間,前期主要是撰寫爬蟲程式碼及探索式資料分析,後期則是學習如何撰寫Dash,提供使用者UI介面進行操作。
目前的專案我發現還有以下問題需要在改善:
-
使用者在執行職缺分析時,網站反應的速度很慢。
這是因為每次執行時,會先依據使用者給的條件到資料庫撈資料出來(花時間),然後還要在後台計算後(更花時間),才會把結果呈現到前端。
目前我想到的改進方法,或許可以採用Redis來處理。在使用者爬蟲完後,先行計算職缺分析結果,並放到記憶體內儲存,這樣使用者在查詢時,可以快速取得資料。
-
多位使用者同時執行職缺爬蟲時,畫面呈現的log紀錄還是會互相干擾。
這部分目前我還在思考要如何處理,可能要先仔細去研究Python的logging套件細節。
comments powered by Disqus