# 爬山健行推薦系統 **[Github](https://github.com/little-air-1019/Hiking-trails-Recommendation-System)** ## 一、發想與目的 &emsp;&emsp;登山活動風氣盛行!根據國民健康局的資料推估,臺灣登山健行人口約有500萬人(佔總人口約22%)且持續成長中。 &emsp;&emsp;利用程式省下行前查資料的時間:爬取++健行筆記的步道資料++與++中央氣象局的天氣預報++,接著,依據山友們的需求(所在縣市、步道難易度與型態、是否需要申請入園...等)推薦適合的步道並提供天氣預報資訊。 ## 二、實作架構 ### 系統設計: ![](https://i.imgur.com/8FF77Ep.png) ### 爬取健行筆記的步道資料 * 使用==requests==函式庫進行HTTP請求 * 使用==BeautifulSoup==函式庫解析網頁原始碼 * 使用==collections==函式庫中的defaultdict ```python= # 抓取步道資料 import requests from bs4 import BeautifulSoup import collections data = collections.defaultdict(dict) ``` * 使用fstring將健行筆記連結中的id設為變數,以利後續網頁翻頁 ```python=+ # 抓取步道資料 i = 1 while i < 2000: url = f'https://hiking.biji.co/index.php?q=trail&act=detail&id={i}' re = requests.get(url) soup = BeautifulSoup(re.text, "html.parser") title = soup.find('h1', {'class': "text-3xl font-bold"}) info = soup.find_all('dd', {'class': "flex-1 p-4"}) ``` * 僅抓取台灣地區資料,因此若地區顯示為其他區域或是分頁不存在資料就換下一個id搜尋 ```python=+ try: if info[0].text == '香港' or info[0].text == '西班牙': i += 1 continue except IndexError: i+=1 continue if info[0].text == '香港' or info[0].text == '西班牙': continue ``` * 爬取所需資料並忽略空值欄位 ```python=+ try: print(title.text) data[title.text]['所在縣市'] = info[0].text data[title.text]['里程'] = info[1].text data[title.text]['步道類型'] = info[2].text data[title.text]['步道型態'] = info[3].text data[title.text]['海拔高度'] = info[4].text data[title.text]['高度落差'] = info[5].text data[title.text]['路面狀況'] = info[6].text data[title.text]['所需時間'] = info[7].text data[title.text]['難易度'] = info[8].text print(f'所在縣市:{info[0].text}') print(f'里程:{info[1].text}') print(f'步道類型:{info[2].text}') print(f'步道型態:{info[3].text}') print(f'海拔高度:{info[4].text}') print(f'高度落差:{info[5].text}') print(f'路面狀況:{info[6].text}') print(f'所需時間:{info[7].text}') print(f'難易度:{info[8].text}') if info[9].text != '-': data[title.text]['所屬園區'] = info[9].text print(f'所屬園區:{info[9].text}') if info[10].text != '-': data[title.text]['山系'] = info[10].text print(f'山系:{info[10].text}') data[title.text]['最適季節'] = info[11].text print(f'最適季節:{info[11].text}') apply_m = info[12].text.replace(' ', '').replace('\n', '') apply_p = info[13].text.replace(' ', '').replace('\n', '') data[title.text]['申請入山'] = apply_m print(f'申請入山:{apply_m}') if info[9].text != '-': if apply_p == '否': data[title.text]['申請入園'] = '否' print(f'申請入園:否') else: data[title.text]['申請入園'] = '是' print(f'申請入園:是') print('') ``` * 若爬蟲完畢或是分頁不存在,即找不到對應欄位值,執行下一個id繼續爬取資料 ```python=+ except AttributeError: i += 1 continue i += 1 ``` ### 步道資料整理 * 因健行筆記資料豐富,每次爬取約需7分鐘的時間,若讓使用者每次搜尋都重新蒐集資料較不理想,故先將所有資料爬取完畢後下載為csv檔 * 載入csv檔,使用==pandas==函式庫 * 將步道名稱設為index,把NaN變成'無' ```python= import pandas as pd df = pd.read_csv('mountain_data.csv') df2 = df.set_index('Unnamed: 0') df2.fillna('無', inplace=True) ``` * 將時間轉到對應的 「1小時以下」、「1-2小時」、「2-4小時」、「4小時以上」四個時間區段,並存到'class_list' ```python= class_list = [] ref_list = df['所需時間'].values.tolist().copy() for item in ref_list: item = item.replace(' ','').replace('(往返)','') if '天' in item: class_list.append('4小時以上') else: if '小時' not in item: class_list.append('1小時以下') else: H = int(item.split('小時')[0]) if H>=1 and H<2: class_list.append('1~2小時') elif H>=2 and H<4: class_list.append('2~4小時') elif H>=4: class_list.append('4小時以上') ``` ### 爬取中央氣象局的天氣預報資料 * weather_data函式:傳入使用者所在地,回傳當地天氣資料 * Wx: 天氣現象 * MaxT: 最高溫度 * MinT: 最低溫度 * CI: 舒適度 * PoP: 降雨機率 * 使用網站提供的API * 取得資料的HTTP請求方式為GET * 需傳入兩個Parameter: Authorization, locationName * Authorization為中央氣象局開放資料平台提供給會員的API授權碼 * locationName預設為回傳台灣所有縣市 ```python= import requests import json from bs4 import BeautifulSoup # 此函式輸入地名'台'須寫為'臺' def weather_data(location): url = 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001' params = { 'Authorization': 'CWB-9E6E7429-A9A0-46B1-88AA-CF702E6934B6', 'locationName': location, } response = requests.get(url, params=params) if response.status_code == 200: data = json.loads(response.text) location = data['records']['location'][0]['locationName'] weather_elements = data['records']['location'][0]['weatherElement'] date = weather_elements[0]['time'][0]['startTime'][:10] start_time = weather_elements[0]['time'][0]['startTime'][-8:] end_time = weather_elements[0]['time'][0]['endTime'][-8:] weather_state = weather_elements[0]['time'][0]['parameter']['parameterName'] rain_prob = weather_elements[1]['time'][0]['parameter']['parameterName'] min_tem = weather_elements[2]['time'][0]['parameter']['parameterName'] comfort = weather_elements[3]['time'][0]['parameter']['parameterName'] max_tem = weather_elements[4]['time'][0]['parameter']['parameterName'] return location, date, str(start_time) + '-' + str(end_time), weather_state, rain_prob, min_tem, max_tem, comfort else: return None ``` ### 使用者介面 * 使用==Anvil==網站:用Python撰寫,不須使用HTML及CSS就可以建立介面並連接後端資料的工具 * 介面設計:利用模組建立輸入方塊以及選單 ![](https://i.imgur.com/pjaJvjv.png) * 在==Colab==上進行資料篩選:將爬取到的步道及天氣資訊根據使用者的選擇做篩選 ```python= # 推薦步道 @anvil.server.callable def search(time,type_1,type_2,type_c,place,easy,application): # 步道型態 if type_1: type1_list = df.iloc[:,0][list(map(lambda x: x=='必須折返',df['步道型態'].values))].tolist() else: type1_list = [] if type_c: typec_list = df.iloc[:,0][list(map(lambda x: x=='環狀',df['步道型態'].values))].tolist() else: typec_list = [] if type_2: type2_list = df.iloc[:,0][list(map(lambda x: x=='雙向進出',df['步道型態'].values))].tolist() else: type2_list = [] type_list = type2_list + typec_list + type1_list # 所需時間 is_time_list = df.iloc[:,0][list(map(lambda x: x==time,class_list))].tolist() # 申請入山 application_or_not_list = df.iloc[:,0][list(map(lambda x: x==application,df['申請入山'].values))].tolist() # 難易度 easy_hard_list = df.iloc[:,0][list(map(lambda x: x==easy,df['難易度'].values))].tolist() # 所在縣市 place_list = df.iloc[:,0][list(map(lambda x: x[:3]==place,df['所在縣市'].values))].tolist() union_list = list(set(place_list) & set(easy_hard_list) & set(type_list) & set(is_time_list) & set(application_or_not_list)) filter_dict = df2.loc[union_list].to_dict('records') filter_list = [] for j in range(len(filter_dict)): filter_dict[j]['步道'] = union_list[j] filter_list.append(filter_dict[j]) # if union_list !=[]: # txt = ', '.join(i for i in union_list) # else: # txt = '查無資訊!' return filter_list # 搜尋指定縣市天氣 @anvil.server.callable def search_weather(place): if '台' in place: place = place.replace('台','臺') if weather_data(place) is not None: location, date, time, weather_state, rain_prob, min_tem, max_tem, comfort = weather_data(place) else: weather_value_txt ='查無資訊' weather_name_txt ='' nl = '\n' weather_value_txt = f'{location}{nl}{date}{nl}{time}{nl}{weather_state}{nl}{weather_state}{nl}{rain_prob}%{nl}{min_tem}˚C{nl}{max_tem}˚C{nl}{comfort}' weather_name_txt = f'地點:{nl}日期:{nl}時間:{nl}天氣:{nl}天氣:{nl}降雨機率:{nl}最高溫度:{nl}最低溫度:{nl}舒適度:' return weather_value_txt,weather_name_txt ``` * 在Anvil的server連結Colab ```python= !pip install anvil-uplink import anvil.server anvil.server.connect("JCGNZVCRESJZ55NJUV5LR6PK-XHDA7WOZDBWIYGZF") ``` * 讓讓colab保持運行狀態、anvil視窗處於持續接收訊息狀態 ```python= anvil.server.wait_forever() ``` ## 三、結果呈現 ### 網頁介面 #### 使用者可選擇++所在縣市++、步道++難易度++、++步道型態++、是否需++申請入山++、爬山++時間++ ![](https://i.imgur.com/CXIMDxA.png) ### 查詢當地天氣 ![](https://i.imgur.com/AYXoI8p.png) ### 查詢步道資訊 ![](https://i.imgur.com/1A46oPo.png) ![](https://i.imgur.com/UzVQEAS.png) ## 四、優化方向 * 目前僅能顯示搜尋時的當日天氣資訊,可以改為讓使用者自行輸入日期,並顯示出對應日期的天氣資訊 * 擴充後端資料量:除健行筆記外,可爬取更多登山網站資料 * 新增搜尋功能,讓使用者能自行輸入想去的步道並顯示出其他資訊