あさぶろ日記

note のほうに書いている記事を保管しているだけです。

天気予報もつぶやかせてみた話。

何の話かと言いますと、ツイッターの話です。みなさんお待ちかねの「APIで色々やってみよう」シリーズです。え?誰も望んでいないですか、そうですか・・。

今回の記事は、読んでいる方がほとんど共感できないであろうニッチで意味不明な内容になると思います、申し訳ない。もしご興味のある方がいらっしゃれば、どうぞ。

というわけで最近ご無沙汰でしたが、趣味というか仕事の延長線上のような技術的な話。


以前、記事で書いたのですが、実は私が note で記事を更新(新規作成)すると、Twitter のほうにも「こんな記事書いたよ!」というツイートを自動で投稿するようにしました。具体的には、Python という言語でスクリプトを作りました。

その時の記事はこちら。

依然として業務で使っているというわけではないので、Python スキルは甚だ低いわけですが、ちょっと今回思い立って、機能追加してみました。後でコードも載せますが、レベルの低い実装なので笑わないでください。いや笑ってもいいですが、できればベストプラクティスを教えて・・。

本題

さて、本題。
どんな機能を追加するかと言いますと、今回は、天気予報。

現状、note に紐づけている私の Twitter アカウントは自動投稿だけさせているのですが、その内容は、

1.note で新しい記事を書いたら、宣伝のツイートをする。
2.note で宣伝すべき記事がなくても、現在の時刻と、その日あった歴史上の出来事とか、その日が誕生日の有名人とかをツイートする。

というものだけ。何とも寂しいわけです。

2.のほうについては、上に紹介した記事に詳しく書きましたが、Wikipedia の API を使って情報を取得しています。そして、そもそもツイートするのは出来事にするのか誕生日にするのか、そしてどの出来事にするか誰の誕生日にするか、というのはランダムで選ぶようにしてます。

ただ、これらは、つぶやく情報の有難みとしてはあんまり高くないわけで。そうするともう少し役に立ちそうな情報もつぶやいてみようと思い、じゃあ天気予報だろうということに勝手に落ち着きました。

やりたいこと

ツイートに追加するのは、こんな感じをイメージしてます。

・どこかの町の天気をつぶやく
・せっかくなので気温とかもつぶやく
・予報のほうもできればつぶやく
・でもあまりたくさんの内容は不要

天気というか気象の知識に関して私は皆無なので、あまりオツムの程度がバレないように、ざっくりした天気と気温くらいにとどめておこうと思っております。

で、そのデータをプログラムで取得するなら、たとえばスクレイピングさせたりとか色んな方法があると思いますが、ここは、当初の作りから方針を変えず、PythonAPI の鉄則でやっていこうかなと。

キミにきめた(API

というわけで、巷には天気予報APIってどんなものがあるのかな、と探してみました。色んなサイトを見てみたけれども、商用だとか、条件指定を経度・緯度でしなければならないとかもあって「うーん」となりました。

結局、一番有名で、サービスとしても安定してそうな「Open Weather Map」に決定。リクエストパラメータも割と多様でしたので。

Сurrent weather and forecast - OpenWeatherMap Get current weather, hourly forecast, daily forecast for 16 d openweathermap.org こちら、有償版と無償版のプランがある模様。どうやらそこまでたくさんリクエスト送らなければ無料のでもいけるようなので、一旦これで使ってみることにしました。公式サイトによれば、1分あたり60回までとか、ひと月あたり1,000,000回までとからしいので。まぁそこまではいかないでしょう。たぶん。

さて、このAPIを使用するには、キーを発行してもらう必要があります。

手順としては、まず、この OpenWeather のサイト上で、アカウント登録が必要。サイト内は全文英語なので一瞬ギョッとするけれども、普通にメールアドレスとパスワード入れてチェックボックスにチェック入れたり入れなかったりすれば、登録は問題なく完了。すぐにAPIキーも発行されますが、数分とか十数分くらい経たないとキーが Activate されなくて「あれっ、キー間違ったかな」と少し焦って何度もリクエスト送ってしまったのはここだけの話。気長に待ちましょう。

そうしたら、Python スクリプトのほうに、天気予報APIで情報取得する処理を実装。

今日の天気情報を取得する関数

いきなりコードをデデーンと載せますが、こんな感じにしました。

def get_today_weather(w_token):
  # """"""""""""""""""""""""""""""""""""""
  # """ 今日の天気情報を取得 """
  # """"""""""""""""""""""""""""""""""""""
  msg = None
  try:
    # 都市一覧のファイル(json.gz)をDLして辞書化
    url = "http://bulk.openweathermap.org/sample/city.list.json.gz"
    response = req.get(url)
    if response.status_code == 200:
        gzip_file = io.BytesIO(response.content)
        with gzip.open(gzip_file, mode='rt', encoding='utf-8') as f:
            json_data = f.read()
    dic_city = json.loads(json_data)

    # 対象の都市を日本に限定してリスト化してランダムに抽出
    l_id_city = [d.get('id') for d in dic_city if d.get('country') == 'JP']
    x = random.choice(l_id_city)

    # OpenWeatherMap API で該当都市の現在の天気情報を取得
    response = req.get(
      "https://api.openweathermap.org/data/2.5/weather",
      params={
          "id":x,
          "appid": w_token,
          "units": "metric",
          "lang": "ja",
      },
    )
    # 戻り値を分解
    ret_current = response.json()
    tp = ret_current["main"]['temp']
    city = ret_current["name"]
    weather = ret_current["weather"][0]['description']
    lat = ret_current["coord"]['lat'] # 緯度
    lon = ret_current["coord"]['lon'] # 経度
    
    # OpenWeatherMap API で該当都市の天気予報を取得
    response = req.get(
      "https://api.openweathermap.org/data/2.5/onecall",
      params={
          "appid": w_token,
          "lon":lon,
          "lat":lat,
          "units": "metric",
          "lang": "ja",
      },
    )
    # 戻り値を分解(翌日以降の情報取得)
    ret_forecast = response.json()
    x = random.randint(1,7)
    next_day = datetime.fromtimestamp(ret_forecast["daily"][x]['dt'])
    next_weather = ret_forecast["daily"][x]["weather"][0]['description']
    next_tp = ret_forecast["daily"][x]["temp"]["day"]

    # 気温によって体感を分岐
    def get_feel_msg(temp):
      feel = None
      if temp >= 28:
        feel = "暑い"
      elif temp >= 20 and temp < 28:
        feel = "わりと暑い"
      elif temp >= 15 and temp < 20:
        feel = "涼しいほう"
      elif temp >= 10 and temp < 15:
        feel = "肌寒い"
      elif temp < 10:
        feel = "寒い"
      return(feel)

    # 適当なメッセージ生成
    if city != None:
      msg = "現在の " + city + " の天気:"+ weather +"/気温:" + str(tp) + "℃の模様。"
      tp_msg = get_feel_msg(tp)
      if tp_msg != None:
        msg += "感覚的に" + tp_msg + "かもしれないです。"
      msg += "\n\n" + str(next_day.month) + "/" + str(next_day.day) + " " + str(next_day.hour) + "時の時点では、"
      msg += "天気:" + next_weather + "/気温:" + str(next_tp) + "℃みたい。"
      tp_msg = get_feel_msg(next_tp)
      if tp_msg != None:
        msg += tp_msg + "かもですね。"        
      msg += "\n\n" + city + " はこの辺り。\n"
      msg += 'https://maps.google.com/maps?q=' + str(lat) + "," + str(lon)

  except Exception as e:
    msg_err = f"Error Occurred: {e}"
    print(msg_err)
    msg = None

  return (msg)

※これらのソースコードにより生じた如何なる損害についても、一切の責任は負いかねますので、あらかじめご了承ください。


解説

では以下解説します。

引数

流れとしては、まず、w_token 変数で、上で書いた OpenWeatherMap のキーを受け取ります。

今回紹介しているのは関数単体ですが、実際のコードは、main 処理から各関数にキーなどのパラメータを渡してあげる感じにしてます。

都市一覧をWebから取得

このAPIでは、「場所(都市)」の条件を指定して情報をとれるらしいので、それを指定するための準備。都市を決め打ちしてもよかったけれど、私では、勤め先の東京とか、住んでいるところの埼玉とか、その辺りくらいしか思い浮かばなかったので、いっそ全ての都市を対象にしてみることにしました。全部の中から適当にその時その時で選ぶアラカルトスタイルです。違うか。

というわけで具体的な方法。http://bulk.openweathermap.org/sample/  というサイト内にある json ファイルに、全ての都市一覧が入っているようなので、プログラム内でそれをダウンロード。

あらかじめダウンロードしてプロジェクトに含めるという手もあったけれども、ここはやっぱりデータソースの取得から全部自動化することにしました。余計なファイル持ちたくないし、都市データが古くなったら更新が面倒なので。実行時にダウンロードできなかったら、それはそれで仕方なし。

都市一覧ファイルの読み込み

ポイントは、このファイルが gzip 形式になっているので、Python の gzip 用のライブラリ(標準)でまずはファイル読み込み。これで解凍せずとも、直接中身を読み取れるらしいです。すごい。

それから、取得した json データ(文字列)を、Dictionary 型に変換。変換元は文字列だったので、json.loads  メソッドを使用しました。load メソッドではだめみたい(無知ですみません)。

国内の都市だけリスト化

それからもう一つポイント。全ての都市を対象にするといっても全世界の都市が含まれているので、さすがにちょっと多すぎる。というか、全然聞いたことない都市名の天気をツイートしても何だかねぇ。

というわけで、日本国内に絞ってリストに抽出することにしました。ここでは、Python ではお馴染みらしい、リスト内包表記という方法で実装。たしかにこれはコードがシンプルになるね。"country"が"JP(日本)"に該当する要素で"id"(都市ID)をまとめてリスト化。

API実行(1回目、現在の天気)

ようやく本題のAPIhttps://api.openweathermap.org/data/2.5/weather)です。ここでは、対象都市の現在の天気をとります。

まず対象の都市のIDを  "id" のパラメータに指定。どの都市を対象にするのかは、これもリスト内からランダムで選ぶようにしてます。ちなみに、今回は"id"に指定しましたが、経度や緯度、都市名とかでも条件指定できるっぽいです。

そしていざ実行すると、json 形式で返ってきます。色々と情報は取れるみたいでしたが、今回は「都市名」(ret_current["name"])と「天気(詳細)」(ret_current["weather"][0]['description'])と「気温」(ret_current["main"]['temp'])くらいにしました。あんまりいっぱいあっても見にくいし。

あと、この後に使うので、ret_current["coord"]というところから緯度と経度も取っておきます。

ちなみに、都市名と天気(詳細)はリクエストパラメータの言語指定を "ja"(日本語)にしているおかげか、だいたい日本語で取れたりします。(取れない場合もあるので絶対ではない模様)

API実行(2回目、未来の天気)

2回目のAPIhttps://api.openweathermap.org/data/2.5/onecall)。1回目はサービス名が「weather」になっていたけれど、2回目のほうはよく見ると「onecall」になっています。1回目のほうは現在の気象データだけれど、2回目のほうは予報のデータが取れるようです。

で、こちらのAPIのリクエストパラメータには、1回目で取得した経度と緯度のデータを使用。

それからAPI実行後に取得した、json形式のレスポンスですが、どうやら、時間ごととか月ごととか様々な切り口での予報データが取れるっぽいです。今回は、日ごとのデータを使おうと思うので、ret_forecast から["daily"]をキーに抜いていきます。中身は、当日を含めて向こう8件分かな、予報データが取れるので、これもランダム抽出(当日[0]は取っても意味ないので除く意図で、インデックスは1~7まで)。

そうして抽出した中から「日時」「天気(詳細)」「気温」を取っておきます。あとは、1回目と2回目のAPIでそれぞれ取得した情報を、ガッチャンコしておしまい。

おまけ

お遊びで、取得した気温の範囲によって、「暑い」とか「寒い」とか適当な感想も詰め込みました。Python 的には関数内部にさらに関数を作れるようなのでちょっとやってみました(でも結構違和感・・)。

あと、都市名を出してあげるんだけど、正直、場所を知らない都市とか結構あると思うので、Google Map の URL も添えました。その際に、1回目のAPIで取得したret["coord"]の経度と緯度を座標位置に指定。

最後に

その選んだ都市の「天気」と「気温」と「肌感覚(私基準)」(当日分と未来の1日分のも併せる)、それと「地図」をくっつけてメッセージ生成。

しかし、よく読んでもらうと分かりますが、日付を加工して文字列にするところの実装がまあ汚い汚い。ちょっとPythonの作法わからなすぎて強引にやりすぎました。反省。後で余力があればリファクタリングします。

最終的に、関数呼び出し元にぶん投げて返してあげます。

※ちなみに、実際のコードでは、ちょこちょことエラーハンドリングとかログ出力してますが、読みにくくなるのでそこは今回記載を割愛しました。

結論

使いやすい。リファレンスも分かりやすくてあんまり悩むところがなかった。PyOWM というラッパーライブラリもあるみたいなので、手早く使いたい人はこっちを触ってみてもいいかも。私は勉強のために、敢えて生APIをいじいじしてみました。というのは建前で、最近仕事でコード書く機会が無かったので、久々に実装ができて純粋に楽しかった。

というわけで、誰の何の役にも立たない記事でした。まぁいつもの記事も役に立たないですけど。

自己満足で失礼。

Twitterアカウント

Tweets by asaburosan

参考サイト

https://www.suzu6.net/posts/78-owm/
https://boook24.com/?p=1507