# 爬山健行推薦系統
**[Github](https://github.com/little-air-1019/Hiking-trails-Recommendation-System)**
## 一、發想與目的
  登山活動風氣盛行!根據國民健康局的資料推估,臺灣登山健行人口約有500萬人(佔總人口約22%)且持續成長中。
  利用程式省下行前查資料的時間:爬取++健行筆記的步道資料++與++中央氣象局的天氣預報++,接著,依據山友們的需求(所在縣市、步道難易度與型態、是否需要申請入園...等)推薦適合的步道並提供天氣預報資訊。
## 二、實作架構
### 系統設計:

### 爬取健行筆記的步道資料
* 使用==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就可以建立介面並連接後端資料的工具
* 介面設計:利用模組建立輸入方塊以及選單

* 在==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()
```
## 三、結果呈現
### 網頁介面
#### 使用者可選擇++所在縣市++、步道++難易度++、++步道型態++、是否需++申請入山++、爬山++時間++

### 查詢當地天氣

### 查詢步道資訊


## 四、優化方向
* 目前僅能顯示搜尋時的當日天氣資訊,可以改為讓使用者自行輸入日期,並顯示出對應日期的天氣資訊
* 擴充後端資料量:除健行筆記外,可爬取更多登山網站資料
* 新增搜尋功能,讓使用者能自行輸入想去的步道並顯示出其他資訊