2024年5月7日 星期二

Python 學習筆記 : 網頁爬蟲實戰 (五) 從 OpenWeather 擷取氣象資料

在上一篇測試中, 擷取的對象是台北市公開資料平台提供的 Youbike 即時資料, 此平台直接提供 JSON 資料網址, 因此毋須註冊, 可直接取得或下載 JSON 資料. 另外一種提供公開 API 的網站則需要註冊帳號, 取得 API Key 才能獲准取得資料, 此類網站主要是為了控管使用者



我在讀陳會安老師的 "Python 從初學到生活應用超實務" 這本書的 11-6 節時看到 OpenWeather 的介紹, OpenWeather 是位於英國倫敦的 OpenWeather 氣象網站, 該組織自 2014 年起向全球提供世界上超過 20 萬個城市的氣象資訊, 使用者可註冊帳號免費取得 API Key 取得天氣資料. 參考下列教學文件 :





以下是實測紀錄.


一. 註冊 OpenWeather 帳號 :   

請按首頁右上角的 "Sign In" : 




於登入頁面中按右下角的 "Create an Account" : 




於註冊頁面輸入帳號, e-mail, 密碼等並勾選年齡與隱私權欄位後按 "Create an Account" 鈕 :





建立帳號後會收到一封驗證信, 開啟後按信中的驗證按鈕即完成註冊 :



登入帳戶後會先調查使用 OpenWeather 的用途 :




然後會跳轉到付費方案的價格表頁面 : 




OpenWeather 也有依用量計費的 "Pay as You Go" 付費方案, 每天前 1000 個 API 請求免費, 超過的部分每次請求以 0.0012 英鎊計費 : 




另外 OpenWeather 也提供免費方案, 免費帳戶每分鐘可以請求 60 次 (平均每分鐘一次), 但一個月總呼叫量上限為 100 萬次 : 




參考 : 


如果呼叫數超過免費額度會收到如下回應 :

{ "cod": 429,
"message": "Your account is temporary blocked due to exceeding of requests limitation of your subscription type. 
Please choose the proper subscription http://openweathermap.org/price"
}

參考 :

https://openweathermap.org/appid


二. 取得 OpenWeather API Key (金鑰) :   

接下來切到 "API Key" 頁籤, 預設已經為我們產生一個名為 "Default" 的金鑰, 也可以在右方輸入框自訂 Key 名稱, 然後按 Generate 鈕產生新的金鑰, 其數量並無限制 :




將 API Key 複製到記事本中保存備用. 


二. 取得即時氣象資料 :   

取得 API Key (金鑰) 後便可從 OpenWeather 網站取得即時氣象資訊, API 的網址格式如下 :

https://api.openweathermap.org/data/2.5/weather?q={city},{country}&units=metric&lang=zh_tw&appid={api_key}

其中 country 參數為國家名稱, 採用 ISO-3166 兩位字母代碼, 台灣的國家代碼是 'TW'. api_key 參數為上面所取得之金鑰, 而 city 參數為城市名稱, 對台灣而言, 它提供了如下 6 個城市代號 :
  • 'Taipei' : 台北
  • 'Banqiao' : 板橋
  • 'Taoyuan' : 桃園
  • 'Hsinchu' : 新竹
  • 'Taichung' : 台中
  • 'Tainan' : 台南
  • 'Kaohsiung' : 高雄
這樣就可以開始測試了, 首先匯入 requests 與 json 套件 :

>>> import requests  
>>> import json  

然後設定參數 : 

>>> api_key='c1ca9d88d1ctony19664b3c021cedf7e'      # 範例金鑰
>>> city='Kaohsiung'  
>>> country='TW'  

用 f 字串製作請求之 URL : 

>>> url=f'https://api.openweathermap.org/data/2.5/weather?q={city},{country}&units=metric&lang=zh_tw&appid={api_key}'    
>>> url    
'https://api.openweathermap.org/data/2.5/weather?q=Kaohsiung,TW&units=metric&lang=zh_tw&appid=c1ca9d88d1ctony19664b3c021cedf7e'

呼叫 requests.get() 函式提出 HTTP GET 請求來取得氣象資訊並使用 json.loads() 將傳回的 JSON 字串轉成 Python 字典 :

>>> res=requests.get(url)  
>>> data=json.loads(res.text)   
>>> data    
{'coord': {'lon': 120.3133, 'lat': 22.6163}, 'weather': [{'id': 701, 'main': 'Mist', 'description': '薄霧', 'icon': '50n'}], 'base': 'stations', 'main': {'temp': 26.81, 'feels_like': 29.93, 'temp_min': 24.05, 'temp_max': 26.97, 'pressure': 1012, 'humidity': 87}, 'visibility': 5000, 'wind': {'speed': 3.09, 'deg': 20}, 'clouds': {'all': 40}, 'dt': 1715022884, 'sys': {'type': 2, 'id': 2002588, 'country': 'TW', 'sunrise': 1715030550, 'sunset': 1715077684}, 'timezone': 28800, 'id': 1673820, 'name': 'Kaohsiung City', 'cod': 200}

注意, 最後一個鍵 cod 為 HTTP 回應狀態碼, 200 表示 OK. 

關於 json 套件用法參考 :


上面的字典輸出內容擠成一團不好閱讀, 可以用 pprint 模組的 pprint() 函式來輸出 : 

>>> from pprint import pprint  
>>> pprint(data)  
{'base': 'stations',
 'clouds': {'all': 40},
 'cod': 200,
 'coord': {'lat': 22.6163, 'lon': 120.3133},
 'dt': 1715022884,
 'id': 1673820,
 'main': {'feels_like': 29.93,
          'humidity': 87,
          'pressure': 1012,
          'temp': 26.81,
          'temp_max': 26.97,
          'temp_min': 24.05},
 'name': 'Kaohsiung City',
 'sys': {'country': 'TW',
         'id': 2002588,
         'sunrise': 1715030550,
         'sunset': 1715077684,
         'type': 2},
 'timezone': 28800,
 'visibility': 5000,
 'weather': [{'description': '薄霧', 'icon': '50n', 'id': 701, 'main': 'Mist'}],
 'wind': {'deg': 20, 'speed': 3.09}}

關於 pprint 模組用法參考 :


我們主要關心的氣象資訊為 weather 鍵中的 discription (天氣描述) 與 main 鍵的溫度, 氣壓, 與濕度資訊, weather 鍵的值是一個單一元素的串列, 可用 data['weather'][0] 取得此串列, 再用 description 鍵取得天氣描述. main 鍵的值是一個字典, 可用 data['main']['temp'] 取得目前溫度 : 

>>> data['weather'][0]['description']   
'薄霧'
>>> data['main']['temp']  
26.81
>>> data['main']['temp_min']  
24.05
>>> data['main']['temp_max']  
26.97
>>> data['main']['pressure']  
1012
>>> data['main']['humidity']  
87

另外回應資料中的 id 鍵 (此處為 1673820) 代表查詢城市 (此處為 Kaohsiung) 的地理位置代號 ( geocode), 這個在查詢未來五天氣象預報時會用到 : 


三. 取得未來 5 天氣象預報資料 :   

透過上面查詢即時氣象資料得到的 geocode 搭配 API Key 可用來查詢該地的未來一周氣象預報, 其 API 網址格式如下 : 

http://api.openweathermap.org/data/2.5/forecast?id={geocode}&appid={api_key}

參考 : 


例如 :

>>> import requests  
>>> import json
>>> from pprint import pprint  
>>> api_key='c1ca9d88d1ctony19664b3c021cedf7e'      # 範例金鑰
>>> url=f'http://api.openweathermap.org/data/2.5/forecast?id=1673820&appid={api_key}'     
>>> res=requests.get(url)   
>>> data=json.loads(res.text)   
>>> pprint(data)   
{'city': {'coord': {'lat': 22.6163, 'lon': 120.3133},
          'country': 'TW',
          'id': 1673820,
          'name': 'Kaohsiung City',
          'population': 0,
          'sunrise': 1715030550,
          'sunset': 1715077684,
          'timezone': 28800},
 'cnt': 40,
 'cod': '200',
 'list': [{'clouds': {'all': 60},
           'dt': 1715072400,
           'dt_txt': '2024-05-07 09:00:00',
           'main': {'feels_like': 306.74,
                    'grnd_level': 1010,
                    'humidity': 60,
                    'pressure': 1011,
                    'sea_level': 1011,
                    'temp': 303.54,
                    'temp_kf': 1.61,
                    'temp_max': 303.54,
                    'temp_min': 301.93},
           'pop': 0.24,
           'sys': {'pod': 'd'},
           'visibility': 10000,
           'weather': [{'description': 'broken clouds',
                        'icon': '04d',
                        'id': 803,
                        'main': 'Clouds'}],
           'wind': {'deg': 299, 'gust': 3.67, 'speed': 3.94}},
          {'clouds': {'all': 80},
           'dt': 1715083200,
           'dt_txt': '2024-05-07 12:00:00',
           'main': {'feels_like': 305.54,
                    'grnd_level': 1012,
                    'humidity': 65,
                    'pressure': 1012,
                    'sea_level': 1012,
                    'temp': 302.43,
                    'temp_kf': 0.95,
                    'temp_max': 302.43,
                    'temp_min': 301.48},
           'pop': 0.24,
           'sys': {'pod': 'n'},
           'visibility': 10000,
           'weather': [{'description': 'broken clouds',
                        'icon': '04n',
                        'id': 803,
                        'main': 'Clouds'}],
           'wind': {'deg': 270, 'gust': 2.21, 'speed': 1.55}},
          {'clouds': {'all': 100},
           'dt': 1715094000,
           'dt_txt': '2024-05-07 15:00:00',
           'main': {'feels_like': 303.46,
                    'grnd_level': 1012,
                    'humidity': 71,
                    'pressure': 1013,
                    'sea_level': 1013,
                    'temp': 300.9,
                    'temp_kf': 0,
                    'temp_max': 300.9,
                    'temp_min': 300.9},
           'pop': 0,
           'sys': {'pod': 'n'},
           'visibility': 10000,
           'weather': [{'description': 'overcast clouds',
                        'icon': '04n',
                        'id': 804,
                        'main': 'Clouds'}],
           'wind': {'deg': 220, 'gust': 2.4, 'speed': 2.16}},
         
          ... (略) ... 
          
          {'clouds': {'all': 28},
           'dt': 1715482800,
           'dt_txt': '2024-05-12 03:00:00',
           'main': {'feels_like': 304.34,
                    'grnd_level': 1011,
                    'humidity': 60,
                    'pressure': 1012,
                    'sea_level': 1012,
                    'temp': 302.24,
                    'temp_kf': 0,
                    'temp_max': 302.24,
                    'temp_min': 302.24},
           'pop': 0,
           'sys': {'pod': 'd'},
           'visibility': 10000,
           'weather': [{'description': 'scattered clouds',
                        'icon': '03d',
                        'id': 802,
                        'main': 'Clouds'}],
           'wind': {'deg': 279, 'gust': 2.77, 'speed': 3.11}},
          {'clouds': {'all': 53},
           'dt': 1715493600,
           'dt_txt': '2024-05-12 06:00:00',
           'main': {'feels_like': 305.66,
                    'grnd_level': 1008,
                    'humidity': 59,
                    'pressure': 1009,
                    'sea_level': 1009,
                    'temp': 303.08,
                    'temp_kf': 0,
                    'temp_max': 303.08,
                    'temp_min': 303.08},
           'pop': 0.01,
           'sys': {'pod': 'd'},
           'visibility': 10000,
           'weather': [{'description': 'broken clouds',
                        'icon': '04d',
                        'id': 803,
                        'main': 'Clouds'}],
           'wind': {'deg': 295, 'gust': 4.74, 'speed': 5.17}}],
 'message': 0}

可見此 API 會回應 5/7~5/12 共 5 天的氣象預報資料 (每 3 個小時更新一次), 注意, cod 鍵為 HTTP 回應狀態碼, '200' 表示 OK, 注意, 與上面即時氣象回應值為數字不同的是, 此處是字串 '200'. 

此處因輸出資料甚長而略去中間輸出, 完整之輸出參考 :


從上面的輸出可知傳回值是一個 JSON 資料, 透過 json 套件轉成字典後可以很方便地取得內容, 例如 city 鍵儲存所查詢之城市的相關資訊, 例如 geocode(id), 經緯度, 與日昇日落時間等 : 

>>> data['city']  
{'coord': {'lat': 22.6163, 'lon': 120.3133}, 'country': 'TW', 'id': 1673820, 'name': 'Kaohsiung City', 'population': 0, 'sunrise': 1715030550, 'sunset': 1715077684, 'timezone': 28800}

預報資料放在 list 鍵裡, 其值是一個字典串列, 串列元素有 40 個, 每一個間隔 3 小時, 一天有 8 筆資料, 五天就是 40 筆 : 

>>> len(data['list'])   
40

檢視第一筆與最後一筆資料 :

>>> data['list'][0]   
{'clouds': {'all': 60}, 'dt': 1715072400, 'dt_txt': '2024-05-07 09:00:00', 'main': {'feels_like': 306.74, 'grnd_level': 1010, 'humidity': 60, 'pressure': 1011, 'sea_level': 1011, 'temp': 303.54, 'temp_kf': 1.61, 'temp_max': 303.54, 'temp_min': 301.93}, 'pop': 0.24, 'sys': {'pod': 'd'}, 'visibility': 10000, 'weather': [{'description': 'broken clouds', 'icon': '04d', 'id': 803, 'main': 'Clouds'}], 'wind': {'deg': 299, 'gust': 3.67, 'speed': 3.94}}
>>> data['list'][39]   
{'clouds': {'all': 53}, 'dt': 1715493600, 'dt_txt': '2024-05-12 06:00:00', 'main': {'feels_like': 305.66, 'grnd_level': 1008, 'humidity': 59, 'pressure': 1009, 'sea_level': 1009, 'temp': 303.08, 'temp_kf': 0, 'temp_max': 303.08, 'temp_min': 303.08}, 'pop': 0.01, 'sys': {'pod': 'd'}, 'visibility': 10000, 'weather': [{'description': 'broken clouds', 'icon': '04d', 'id': 803, 'main': 'Clouds'}], 'wind': {'deg': 295, 'gust': 4.74, 'speed': 5.17}}

我們關心的氣象資料分別在 dt_txt, main, 與 weather 等鍵底下, 存取方式以第一筆為例 : 

>>> data['list'][0]['dt_txt']   
'2024-05-07 09:00:00'
>>> data['list'][0]['main']['humidity']   
60
>>> data['list'][0]['main']['pressure']   
1011
>>> data['list'][0]['main']['temp']   
303.54
>>> data['list'][0]['weather'][0]['description']  
'broken clouds'

可以迭代 list 鍵的 40 個元素取出每 3 小時的預測氣象數據 :

>>> for i in data['list']:   
 print(f"時間 : {i['dt_txt']}")   
 print(f"溫度 : {i['main']['temp']}")   
 print(f"濕度 : {i['main']['humidity']}")   
 print(f"壓力 : {i['main']['pressure']}")   
 print(f"描述 : {i['weather'][0]['description']}")   
 print('-------------------------')   

結果節錄如下 :

時間 : 2024-05-07 09:00:00
溫度 : 303.54
濕度 : 60
壓力 : 1011
描述 : broken clouds
-------------------------
時間 : 2024-05-07 12:00:00
溫度 : 302.43
濕度 : 65
壓力 : 1012
描述 : broken clouds
-------------------------
時間 : 2024-05-07 15:00:00
溫度 : 300.9
濕度 : 71
壓力 : 1013
描述 : overcast clouds

... (略) ...

時間 : 2024-05-12 00:00:00
溫度 : 300.68
濕度 : 65
壓力 : 1012
描述 : broken clouds
-------------------------
時間 : 2024-05-12 03:00:00
溫度 : 302.24
濕度 : 60
壓力 : 1012
描述 : scattered clouds
-------------------------
時間 : 2024-05-12 06:00:00
溫度 : 303.08
濕度 : 59
壓力 : 1009
描述 : broken clouds
-------------------------

注意, 氣象預報傳回的溫度為卡氏溫度 (絕對溫度, 單位 K), 需減掉 273.15 才是攝氏溫度. 


四. 氣象爬蟲函式 :   

將上面的程式碼改寫為函式如下 :

import requests  
import json

def get_weather_now(country, city, api_key):
    url=(f'https://api.openweathermap.org/data/2.5/weather?'
         f'q={city},{country}&units=metric&lang=zh_tw&appid={api_key}')
    try:
        res=requests.get(url)
        data=json.loads(res.text)
        if data['cod']==200:    # 注意是數值
            ret={'geocode': data['id'],
                 'description': data['weather'][0]['description'],
                 'temperature': data['main']['temp'],
                 'min_temperature': data['main']['temp_min'], 
                 'max_temperature': data['main']['temp_max'],
                 'presure': data['main']['pressure'],
                 'humidity': data['main']['humidity']}
            return ret
        else:
            return None
    except Exceptions as e:
        return None

def get_weather_forecast(geocode, api_key):
    url=(f'http://api.openweathermap.org/data/2.5/forecast?'
         f'id={geocode}&appid={api_key}')
    try:
        res=requests.get(url)
        data=json.loads(res.text)
        ret=[]
        if data['cod']=='200':     # 注意是字串
            for i in data['list']:
                d={'time': i['dt_txt'],
                   'temperature': round(i['main']['temp']-273.15),    # 卡式溫度轉攝氏
                   'humidity': i['main']['humidity'],
                   'pressure': i['main']['pressure'],
                   'description': i['weather'][0]['description']}
                ret.append(d)
            return ret
        else:
            return None
    except Exceptions as e:
        return None

if __name__ == '__main__':
    country='TW'
    city='Kaohsiung'
    api_key='c1ca9d88d1ctony19664b3c021cedf7e'   # 範例金鑰
    ret=get_weather_now(country, city, api_key)
    print(f'目前氣象: {ret}')
    geocode=ret['geocode']
    print(f'未來五天氣象:')
    ret=get_weather_forecast(geocode, api_key)
    for i in ret:   
       print(f"時間 : {i['time']}")   
       print(f"溫度 : {i['temperature']}")   
       print(f"濕度 : {i['humidity']}")   
       print(f"壓力 : {i['pressure']}")   
       print(f"描述 : {i['description']}")   
       print('-------------------------')   
    
此處已經將五天氣象預測中的卡氏溫度轉成攝氏, 結果如下 :

>>> %Run get_open_weather.py   
目前氣象: {'geocode': 1673820, 'description': '多雲', 'temperature': 27.12, 'min_temperature': 25.05, 'max_temperature': 27.24, 'presure': 1013, 'humidity': 80}
未來五天氣象:
時間 : 2024-05-08 15:00:00
溫度 : 27
濕度 : 81
壓力 : 1013
描述 : light rain
-------------------------
時間 : 2024-05-08 18:00:00
溫度 : 27
濕度 : 77
壓力 : 1013
描述 : light rain
-------------------------
時間 : 2024-05-08 21:00:00
溫度 : 26
濕度 : 72
壓力 : 1013
描述 : light rain
-------------------------

... (略) ...

-------------------------
時間 : 2024-05-13 09:00:00
溫度 : 28
濕度 : 64
壓力 : 1009
描述 : light rain
-------------------------
時間 : 2024-05-13 12:00:00
溫度 : 27
濕度 : 63
壓力 : 1012
描述 : light rain
-------------------------

因我目前尚無具體應用會用到氣象資料, 故先測到這樣就好了, 有需要再於此基礎上進一步開發. 

沒有留言 :