世界の天気情報を取得してTwitterにあげるようにした話②

# 前回までの流れ
・Pythonを使ってTwitterへ投稿
・OpenWeatherのAPIを使ってJSONデータを取得する

さて、前回は”tokyo”と指定して東京のデータを取得することに成功しました。


# JSONデータを見てみよう
実行すると以下のように出力されました(※2022/12/12 23:10実行）


~~~
{'base': 'stations',
 'clouds': {'all': 40},
 'cod': 200,
 'coord': {'lat': 35.6762, 'lon': 139.6503},
 'dt': 1670854086,
 'id': 1862143,
 'main': {'feels_like': 6.28,
          'humidity': 62,
          'pressure': 1021,
          'temp': 8.43,
          'temp_max': 10.33,
          'temp_min': 5.35},
 'name': '堀ノ内',
 'sys': {'country': 'JP',
         'id': 268395,
         'sunrise': 1670794887,
         'sunset': 1670830110,
         'type': 2},
 'timezone': 32400,
 'visibility': 10000,
 'weather': [{'description': '雲', 'icon': '03n', 'id': 802, 'main': 'Clouds'}],
 'wind': {'deg': 350, 'speed': 3.6}}
~~~

公式サイトに書かれているのと同様の形式で出力されました。
[Weather fields in API response -JSON-]("https://openweathermap.org/current#current_JSON")
(※公式はlang=en、サンプルプログラムはlang=jaなので日本語で出力されている部分もあります)

現在の天気や気温、最低気温や最高気温、
日の出日の入りの時間などが分かります。

さて、ここで疑問。

同じ都市名の場合はどうなるでしょうか…？

例えばロンドンという都市はイギリスのロンドンの他に
カナダにもあります。
カナダのロンドンに行くために天気予報を取得しようと思った場合に
以下のプログラムを実行すると…

~~~ python

#!/usr/bin/env python
# -*- coding:utf-8 -*-

# JSONデータ取得


import json
import requests
import pprint

OPENWEATHER_API = ""


def get_jsondata(city):

    response = requests.get(
        "https://api.openweathermap.org/data/2.5/weather",
        params={
            ## 都市名で取得する場合
            "q": city,
            "appid": OPENWEATHER_API,
            "units": "metric",
            "lang": "ja",
        },
    )
    ret = json.loads(response.text)
    pprint.pprint(ret)



def main():
    get_jsondata("london")


if __name__ == '__main__':
    main()


~~~

結果は以下のようになりました。

~~~
{'base': 'stations',
 'clouds': {'all': 100},
 'cod': 200,
 'coord': {'lat': 51.5085, 'lon': -0.1257},
 'dt': 1670853391,
 'id': 2643743,
 'main': {'feels_like': -1.28,
          'humidity': 95,
          'pressure': 1007,
          'temp': 0.47,
          'temp_max': 1.67,
          'temp_min': -0.57},
 'name': 'ロンドン',
 'sys': {'country': 'GB',
         'id': 2075535,
         'sunrise': 1670831820,
         'sunset': 1670860290,
         'type': 2},
 'timezone': 0,
 'visibility': 7000,
 'weather': [{'description': '厚い雲',
              'icon': '04d',
              'id': 804,
              'main': 'Clouds'}],
 'wind': {'deg': 90, 'speed': 1.54}}
~~~

'country'のところを見てみると
'GB' Great Britain！英国！
予想通りだけど英国の方のロンドンの天気予報が返ってきました。


さて、このAPIですが、都市名だけでなく緯度経度を指定して天気予報を取得することもできます。
そこで考えました。

国名と都市名指定して緯度経度取得してそのデータをAPIに渡せば出来るよね！

# 緯度経度データを取得しよう
ではこの緯度経度データを取得するAPIはあるのか探してみました。

結果、いつもお世話になっているGoogle mapsにありました。
[Google Maps API Geocodeing]("https://developers.google.com/maps/documentation/geocoding")

住所を渡して緯度経度座標の取得、またはその逆ができるAPIです。

さてこのAPIを使うためにはGoogle Cloud APIの認証キーが必要になります。
この辺りの説明を書くと、それだけで記事ひとつ分になりそうなので
公式サイトを参考に登録＆APIキーを取得してください。すみません。

[Google Cloud Documents]("https://cloud.google.com/docs/get-started?hl=ja")


URLを叩いて取得するやり方もありますが
GoogleMapsの各APIを使えるようにするためのライブラリがあります。
[googlemaps -Pypl-]("https://pypi.org/project/googlemaps/")

pipでインストールしましょう。
~~~
$ sudo pip install -U googlemaps
~~~



googlemapsオブジェクトの作成
(GOOGLEAPIKEY:取得したAPIキー)
~~~
gmaps = googlemaps.Client(key=GOOGLEAPIKEY)
~~~
緯度経度座標の取得
~~~
result = gmaps.geocode(city)
~~~
戻り値が座標データになります。


緯度経度の座標を取得したい地名を入れて
以下のプログラムを実行してみましょう。

~~~ python 

#!/usr/bin/env python
# -*- coding:utf-8 -*-

# 緯度経度取得

import googlemaps
import time

GOOGLEAPIKEY = ''

def getgeocode(city):
    gmaps = googlemaps.Client(key=GOOGLEAPIKEY)

    result = gmaps.geocode(city)
    lat = result[0]["geometry"]["location"]["lat"]
    lng = result[0]["geometry"]["location"]["lng"]

    print("%s:%f,%f"%(city,lat,lng))
 

def main():
    getgeocode("london,england")
    getgeocode("london,canada")

if __name__ == '__main__':
    main()


~~~

実行結果は…

~~~
london,england:51.507218,-0.127586
london,canada:42.984923,-81.245277
~~~

英国とカナダ、それぞれの緯度経度を取得すことができました！

この緯度経度を指定して再度OpenWeatherの天気予報を取得してみましょう。

# JSONデータを取得してみる(緯度経度指定)

前回使用したパラメータ都市指定用のqではなく
latとlngを使用します。
こちらにGoogleのGeocode APIで取得した緯度経度座標の値を設定します。

~~~ python
#!/usr/bin/env python
# -*- coding:utf-8 -*-

# JSONデータ取得


import googlemaps
import time
import json
import requests
import pprint

GOOGLEAPIKEY = ''
OPENWEATHER_API = ''


def get_jsondata(city):
    gmaps = googlemaps.Client(key=GOOGLEAPIKEY)

    result = gmaps.geocode(city)
    lat = result[0]["geometry"]["location"]["lat"]
    lng = result[0]["geometry"]["location"]["lng"]

    response = requests.get(
        "https://api.openweathermap.org/data/2.5/weather",
        params={
            ## 緯度・軽度を指定する場合
            "lat": lat,
            "lon": lng,

            ## 都市名で取得する場合
            #"q": "tokyo",

            "appid": OPENWEATHER_API,
            "units": "metric",
            "lang": "ja",
        },
    )
    ret = json.loads(response.text)
    pprint.pprint(ret)



def main():
    get_jsondata("tokyo")
    get_jsondata("london")
    get_jsondata("london,canada")


if __name__ == '__main__':
    main()

~~~

実行結果は以下のようになりました。

~~~
{'base': 'stations',
 'clouds': {'all': 40},
 'cod': 200,
 'coord': {'lat': 35.6762, 'lon': 139.6503},
 'dt': 1670854086,
 'id': 1862143,
 'main': {'feels_like': 6.28,
          'humidity': 62,
          'pressure': 1021,
          'temp': 8.43,
          'temp_max': 10.33,
          'temp_min': 5.35},
 'name': '堀ノ内',
 'sys': {'country': 'JP',
         'id': 268395,
         'sunrise': 1670794887,
         'sunset': 1670830110,
         'type': 2},
 'timezone': 32400,
 'visibility': 10000,
 'weather': [{'description': '雲', 'icon': '03n', 'id': 802, 'main': 'Clouds'}],
 'wind': {'deg': 350, 'speed': 3.6}}
{'base': 'stations',
 'clouds': {'all': 100},
 'cod': 200,
 'coord': {'lat': 51.5072, 'lon': -0.1276},
 'dt': 1670854238,
 'id': 2643743,
 'main': {'feels_like': -1.28,
          'humidity': 95,
          'pressure': 1007,
          'temp': 0.47,
          'temp_max': 1.69,
          'temp_min': -0.55},
 'name': 'ロンドン',
 'sys': {'country': 'GB',
         'id': 2075535,
         'sunrise': 1670831820,
         'sunset': 1670860291,
         'type': 2},
 'timezone': 0,
 'visibility': 6000,
 'weather': [{'description': '厚い雲',
              'icon': '04d',
              'id': 804,
              'main': 'Clouds'}],
 'wind': {'deg': 110, 'speed': 1.54}}
{'base': 'stations',
 'clouds': {'all': 29},
 'cod': 200,
 'coord': {'lat': 42.9849, 'lon': -81.2453},
 'dt': 1670854050,
 'id': 6058560,
 'main': {'feels_like': -4.74,
          'humidity': 80,
          'pressure': 1024,
          'temp': -1.7,
          'temp_max': -1.27,
          'temp_min': -2.21},
 'name': 'ロンドン',
 'sys': {'country': 'CA',
         'id': 20399,
         'sunrise': 1670849228,
         'sunset': 1670881833,
         'type': 2},
 'timezone': -18000,
 'visibility': 10000,
 'weather': [{'description': '雲', 'icon': '03d', 'id': 802, 'main': 'Clouds'}],
 'wind': {'deg': 17, 'speed': 2.24}}

~~~

・東京
・ロンドン(英国)
・ロンドン(カナダ)
の天気情報をそれぞれ取得することができました！


# JSONデータを抜き出してみる

これまではrequestsを使用してJSONデータを取得し
そのまま出力していましたが、JSONデータを扱いやすい形式にするために
今後はurllib.requestモジュールを使用します。

urllib.reqest.urlopenでURLを指定してリクエストした内容を
json.loadsでutf-8にデコードします。
decoded_strの中にutf-8にデコードされたJSONデータが入ってきます。

URLはOpenWeatherの公式ドキュメントを参考に
各パラメータを設定します。

[OpenWeather API Call]("https://openweathermap.org/current#geo")

lat,lon,appidの他
units,langを指定します。

~~~
api_url="https://api.openweathermap.org/data/2.5/weather?
lang=%s&lat=%f&lon=%f&units=%s&mode=json&appid=%s"
~~~

unitsとlangは定数にしました。


~~~ python
#!/usr/bin/env python
# -*- coding:utf-8 -*-


#OpenWeather 途中経過

import urllib
import urllib.request
import urllib.parse
import json
from datetime import datetime, timezone, timedelta
from dateutil.tz import gettz
from twython import Twython
import googlemaps
import time
import requests
import pprint

LANG = 'ja'
UNITS = 'metric'
CONSUMER_KEY = ''
CONSUMER_SECRET = ''
ACCESS_KEY = ''
ACCESS_SECRET = ''
GOOGLEAPIKEY = ''
OPENWEATHER_API = ''



def weather(city):
    
    gmaps = googlemaps.Client(key=GOOGLEAPIKEY)


    result = gmaps.geocode(city)
    lat = result[0]["geometry"]["location"]["lat"]
    lng = result[0]["geometry"]["location"]["lng"]


    api_url="https://api.openweathermap.org/data/2.5/weather?lang=%s&lat=%f&lon=%f&units=%s&mode=json&appid=%s"
    
    
    try:
      
        url = api_url % (LANG,lat,lng,UNITS,OPENWEATHER_API)
        
               
        res=urllib.request.urlopen(url)
        json_str=res.readline()

        decoded_str=json.loads(json_str.decode("utf-8"))
        print (decoded_str)

    finally:
        print("done")
    return



def main():

    weather("Berlin,Germany")


if __name__ == '__main__':
    main()

~~~


これを実行するとdecoded_strに以下のようなデータが入っていることがわかります。
(※見やすくするために成形していますが、実際は一行で出力されます)



~~~
{
    'coord': {
        'lon': 13.405, 
        'lat': 52.52
    }, 
    'weather': [
    {
        'id': 800, 
        'main': 'Clear', 
        'description': '晴天', 
        'icon': '01n'}
    ],
     'base': 'stations', 
     'main': {
        'temp': -7.32, 
        'feels_like': -10.18, 
        'temp_min': -9.44, 
        'temp_max': -5.57, 
        'pressure': 998, 
        'humidity': 87
    }, 
    'visibility': 10000, 
    'wind': {
        'speed': 1.54, 
        'deg': 120
    }, 
    'clouds': {
        'all': 0
    }, 
    'dt': 1670993361, 
    'sys': {
        'type': 2, 
        'id': 2009543, 
        'country': 'DE', 
        'sunrise': 1671001795, 
        'sunset': 1671029529
    }, 
    'timezone': 3600, 
    'id': 6545310, 
    'name': 'Mitte', 
    'cod': 200
}
~~~


出力されたデータの詳細はこちら


[Weather fields in API response -JSON-]("https://openweathermap.org/current#current_JSON")
出力例の下に各フィールドの説明があります

これを元に欲しいデータを抜き出していきます。


# 取得したデータをTwitterに投稿する

目的が「旅行前に現地の天気を知りたい」だったので
どんな情報が欲しいかを考えた時に

・現在の気温
・最高気温
・最低気温
・日の出と日の入りの時間
・現地時間

をTwitterに投稿することにしました。
なぜ日の出日の入り時間が知りたいかというと
冬の日照時間が日本に比べて短く(特に北欧)
行動できる時間が短くなってくるので
スケジュールをたてるために出してみました。

ちなみにサンタクロース村があるFinland Rovaniemiの
日の出日の入り時間は
　日の出：10:56
　日の入り：13:27
です(2022/12/14)。
日照時間３時間半しかない…


さてここでまたまた問題！
JSONデータにある日の出日の入り時間はUNIX時間かつUTCです！

例）
~~~
 'sunrise': 1671001795, 
 'sunset': 1671029529
~~~

このままだとUTCの時間になってしまうため、
現地の時間とはずれてしまいます。
現地のタイムゾーンを取得する必要があります。
JSONにtimezoneというフィールドがあり
3600(分数*秒数)をこの値で割った値がUTCからの時差になるので
もっとスマートにしたい…。
できることなら現地のタイムゾーンを取得して、現地時間を計算したい…。
ってことで探してみました！

探した結果、同じくGoogle MapsにTimeZone APIという
まんまなAPIがありました。
ありがとうGoogle！

このAPIもgeocodingを使うためにインストールした
googlemapsライブラリの中にあります。

同じgooglemapsオブジェクトを使用して
現地のタイムゾーンIDを取得します。
~~~
result = gmaps.timezone((lat, lng))
timezoneId = result['timeZoneId']
~~~

このtimezoneは引数が地名ではなく
緯度経度座標になるので
①緯度経度座標の取得
②タイムゾーンの取得
の順番でやりましょう。

取得したタイムゾーンIDを使用して
タイムゾーンを取得します。
~~~
timezone=gettz(timezoneId)
~~~

ここで取得したtimezoneを使用して
現在時刻を取得したい場合は
~~~
datetime.now(timezone)
~~~

指定したUNIX時間から現地時間に直した時間を取得したい場合は
~~~
datetime.fromtimestamp(unixtime,timezone)
~~~
を実行します。


以下が最終形になります。

``` python
#!/usr/bin/env python
# -*- coding:utf-8 -*-


#OpenWeather最終形

import urllib
import urllib.request
import json
from datetime import datetime
from dateutil.tz import gettz
from twython import Twython
import googlemaps
import time

LANG = 'ja'
UNITS = 'metric'
CONSUMER_KEY = ''
CONSUMER_SECRET = ''
ACCESS_KEY = ''
ACCESS_SECRET = ''
GOOGLEAPIKEY = ''
OPENWEATHER_API = ''



def weather(city):
    
    gmaps = googlemaps.Client(key=GOOGLEAPIKEY)


    result = gmaps.geocode(city)
    lat = result[0]["geometry"]["location"]["lat"]
    lng = result[0]["geometry"]["location"]["lng"]

    result = gmaps.timezone((lat, lng))
    timezoneId = result['timeZoneId']

    timezone=gettz(timezoneId)
    
    time.sleep(0.5)    

    api = Twython(CONSUMER_KEY,CONSUMER_SECRET,ACCESS_KEY,ACCESS_SECRET)
    
    weather_text = u'%sの現在の天気:%s\n'
    temperature_text = u'現在の気温：%s℃\n予想最高気温:%s℃\n予想最低気温:%s℃\n'
    sunset_sunrize_text = u'日の出：%s\n日の入り:%s'


    api_url="https://api.openweathermap.org/data/2.5/weather?lang=%s&lat=%f&lon=%f&units=%s&mode=json&appid=%s"
    
    
    try:
      
        url = api_url % (LANG,lat,lng,UNITS,OPENWEATHER_API)
        
               
        res=urllib.request.urlopen(url)
        json_str=res.readline()

        decoded_str=json.loads(json_str.decode("utf-8"))



        d=datetime.now(timezone)
        day_text =  d.strftime("現地時間 %Y/%m/%d %H:%M:%S")

        w_str=decoded_str["weather"][0]["description"]
        temp=decoded_str["main"]["temp"]
        temp_max=decoded_str["main"]["temp_max"]
        temp_min=decoded_str["main"]["temp_min"]

        
        today_w_txt = weather_text % (city, w_str)
        today_t_txt = temperature_text % (temp,temp_max, temp_min)
    
        try:
            sunrizetime_unix=decoded_str["sys"]["sunrise"]
            sunsettime_unix=decoded_str["sys"]["sunset"]
        except:
            sun_txt = "白夜か極夜期間中"
        else:
            sunrize = datetime.fromtimestamp(sunrizetime_unix,timezone)
            sunset = datetime.fromtimestamp(sunsettime_unix,timezone)
            
            sunrize = sunrize.strftime("%H:%M")
            sunset = sunset.strftime("%H:%M")
            sun_txt = sunset_sunrize_text % (sunrize, sunset)
            
            
        weather_str = today_w_txt + today_t_txt + sun_txt + '\n\n' + day_text 

        print(weather_str)
        api.update_status(status=weather_str)
        
    finally:
        print("tweet")
    return



def main():

    weather("Berlin,Germany")
    weather("Rovaniemi,Finland")


if __name__ == '__main__':
    main()

```

以下のようにTwitterに投稿されました！

![61463360-3DBB-4FC8-AA17-BFEAC3769381_1_201_a.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3007033/86d9b8c8-a1ff-929f-faa3-f785cb48cca8.jpeg)

さっむ！！！！！！

なお、北極圏と南極圏には白夜と極夜という
太陽が沈まない、逆に太陽が出てこない時期があります。
その場合sunrizeとsunsetのデータが入ってこないので
読み込んだ時にエラーになります。
その対策を入れてあります。

# まとめ
計3回の記事で
twitterへの投稿のやり方から
JSONデータを取得して抽出したデータを成形してTwitterにUPする方法を
紹介しました。
楽しんでもらえたでしょうか。

このプログラムを書いたのは2017年で、今から5年前です。
Cronを使って、毎朝8時に自動実行して投稿するようにしていました。
ラズパイを旅行先に持っていき、現地時間に合わせて
現地の朝8時に投稿し、毎朝確認していました。

Raspberry Pi上で動かしていますが、
Pythonが動く環境で必要なライブラリがインストールされていれば
WindowsでもLinuxでもMacでも動きます。

[Colaboratory]("https://colab.research.google.com/?hl=ja")でも動きますし
今はGCP(Google Cloud Platform)上で
Cloud FunctionsとCloud Schedulerを使って
毎朝9時に自動実行してTwitterに投稿しています。

[くにぃらずぱい生活]("https://twitter.com/kuniy_raspi")

GCP上で動かす方法については
またいつか投稿できたらと思います。

# 最後に

この記事はデフエンジニアの会のアドベントカレンダーへの投稿として書きました。
実際耳が聞こえないエンジニア達がどういう仕事をしているか、
仕事をする上でどのような工夫をしているかという内容は他のメンバーが書いているので、
敢えて技術系の話題にしてみました。

プログラミングは独学で知識を身につけることができる分野です。
プログラムを動かすためにハイスペックなPCが必要だったり
IDEを購入する必要があったりした頃に比べて
今はクラウド上で動かすこともできるし
特にスペックが高いPCを購入する必要もなく、タブレットでもプログラミングできる時代です。
Try & Errorを繰り返しても大丈夫な時代です。
※昔は無限ループなんか作成しようものなら大変なことになってました

この記事を読んだ人が、楽しくプログラミングを勉強するきっかけになったら嬉しいです。
そしてろうの学生がエンジニアを目指すきっかけになってくれたらと思います。