---
# System prepended metadata

title: 爬山健行推薦系統

---

# 爬山健行推薦系統
**[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)


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