3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

OpenWeatherMapから地名で天気情報を取得して画像ファイルに出力する

Last updated at Posted at 2020-12-15

はじめに

 ここのアドベントカレンダーのやつです。

 マストドン上で稼働している自作botの機能に”お天気情報取得”があります。”<地名>の天気を教えて!”とお願いすると、その地域の天気情報を返信してくれるというもので、従来”livedoor天気api”を使用していました。

 ところが、2020年7月31日を以ってこの”livedoor天気api”はサービス終了となり、天気情報の機能が使えなくなってしまいました。

 ということで、再び天気情報機能を使えるようになんとかしようと思います。

要件

  • 指定された地名の天気情報を取得できること(既存要件)
  • Pythonで実装すること(既存要件)※botをPythonで作成しているため
  • 天気情報を表やグラフにして画像ファイル(PNG)にすること(追加要件)

※表・グラフの画像ファイル化は前からやってみたかったので、今回合わせてやってみたいと思います。

天気情報をどこから取得するか

 調べてみると、OpenWeatherMapというのが良さそうな感じです。これを使わせてもらいましょう。(ユーザ登録要、無料プランあり)

無料プランで、「60回/分・100万回/月」という制限がありますが、小規模なら十分すぎる感じです。取得できる情報も「現在天気」「1分毎天気」「1時間毎天気」「日毎天気」などこちらも十分そうです。
スクリーンショット 2020-12-08 11.28.12.png

APIにはいくつか種類がありますが、”One Call API"を使えば欲しい情報は持ってきてくれそうです。
スクリーンショット 2020-12-08 11.33.58.png

実際のAPI使用例を見てみると、"lat"、"lon"に経度・緯度指定が必要そうです。("apiid"はユーザ登録すればもらえます。)
グローバルなAPIなので、日本語地名による検索はダメそうです。(アルファベットでの地名検索はできるっぽいですが、主要都市だけかも?色々試しましたが諦めた気がします)
スクリーンショット 2020-12-08 11.37.15.png

取得結果は以下のような情報が含まれるようです。(詳細は公式サイトのドキュメント参照)
image.png

image.png

image.png

地名と緯度・経度の一覧を作る

 前述の通り、天気情報の取得には経度・緯度が必要そうなので、地名を経度・緯度に変換する方法を考えます。

 調べてみると国土交通省のHPより「位置参照情報 ダウンロードサービス」というものがありました。都道府県毎にダウンロードする必要があるものの、全市区町村単位の位置情報は網羅されていそうです。”地名”はとりあえずは”市区町村名”とすることとします。(街区レベルのものもありますが、ここまで細かいのは今回は不必要と思います)
スクリーンショット 2020-12-08 13.10.51.png

 実際のデータの一部はこんな感じです。住所毎に緯度・経度情報があります。
スクリーンショット 2020-12-08 13.14.25.png

 今回はここまで細かくなくていいかなと思うので、市区町村名でグルーピングして緯度・経度のそれぞれの平均値を求め、その値を市区町村の緯度・経度とする感じで、一覧を作成しました。(使用しない情報は除外)

latloc.json
{
    "徳島県": {
        "徳島市": [
            34.0694450186722,
            134.5442937966806
        ],
        "鳴門市": [
            34.182439379310345,
            134.5660390172414
        ],
        "小松島市": [
            33.994815439999996,
            134.59324916
        ],
"<省略>"
    }
}

 以上から、天気情報取得の流れは、

  1. 市区町村名が指定される

  2. 一覧より該当の市区町村の経度・緯度を取得

  3. OpenWeatherMap APIに経度・緯度を渡してコールし、天気情報を受け取る

  4. 天気情報を表・グラフなどに加工(※次項で説明)して返す

こんな感じで行けそうです。

Pythonでデータを表・グラフにして、画像ファイルにする

 Pythonでグラフ作成ライブラリといえば、matplotlibとかpandasが挙がりますが(多分)、”表の画像ファイル化”、”画像の挿入(お天気マークを画像に入れたい)"がどうやっても難しそうで難航していました。

 そこで、plotly という作図ライブラリを使わせてもらいました。これなら、表の画像ファイル化が綺麗にできました。図の挿入もバッチリです。詳細な使い方は他の記事に譲るとして、今回の作例とポイントだけ説明したいと思います。

作例1 表の画像ファイル化

  • 図(お天気アイコン)の挿入
    image.png

  • こちらの表ではUV指数の値によって文字色を変えています
    image.png

作例2 棒グラフの画像ファイル化

  • 棒グラフが取る値の範囲は自動ですが、負の値は取らないため0起点にしています image.png
otenki.py

# -*- coding: utf-8 -*-
import os
from pprint import pprint as pp
import requests
import json
from bs4 import BeautifulSoup
from time import sleep
from collections import defaultdict
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
import urllib.request
from pytz import timezone
from datetime import datetime, timedelta
from PIL import Image
import locale
locale.setlocale(locale.LC_TIME, 'ja_JP.UTF-8')

CITY_LATLOC_PATH = "dic/city_latloc.json"
ICON_DIR = "tenki_icon"
IMAGE_H = 1260
IMAGE_W = 800
BG_COLOR = "#100500"
LINE_COLOR = "#5f5050"
FONT_COLOR = "#fff5f3"
WEATHER_IMAGE_PATH = "./media"

# 天気メイン
def get_tenki(quary, appid):
    with open(CITY_LATLOC_PATH, 'r') as fr:
        city_latloc_dict = json.load(fr)
    os.makedirs(WEATHER_IMAGE_PATH, exist_ok=True)
    os.makedirs(os.path.join(WEATHER_IMAGE_PATH, ICON_DIR), exist_ok=True)

    hit_tdfk = [tdfk for tdfk in city_latloc_dict.keys() if tdfk in quary]
    hit_skcs = []
    hit_lat = 0.0
    hit_loc = 0.0
    if len(hit_tdfk) == 1:
        # 都道府県指定あり
        for k_skcs, latloc in city_latloc_dict[hit_tdfk[0]].items():
            if quary.split(hit_tdfk[0])[-1] in [k_skcs, k_skcs[:-1]]:
                hit_skcs.append(hit_tdfk[0] + k_skcs)
                hit_lat = latloc[0]
                hit_loc = latloc[1]
    else:
        # 都道府県指定なし
        for k_tdfk, v in city_latloc_dict.items():
            for k_skcs, latloc in v.items():
                if quary in [k_skcs, k_skcs[:-1]]:
                    hit_skcs.append(k_tdfk + k_skcs)
                    hit_lat = latloc[0]
                    hit_loc = latloc[1]

    if len(hit_skcs) == 0:
        return 900, None, None, None  # 見つからなかった場合
    elif len(hit_skcs) > 1:
        return 901, None, "、".join(hit_skcs), None  # 複数見つかった場合

    # 天気情報取得
    url = "http://api.openweathermap.org/data/2.5/onecall"
    payload = {
        "lat": hit_lat, "lon": hit_loc,
        "lang": "ja",
                "units": "metric",
                "APPID": appid}
    tenki_data = requests.get(url, params=payload).json()
    tz = timezone(tenki_data['timezone'])
    skcs_name = hit_skcs[0]

    return 0, hit_skcs[0]+"の天気", \
        make_weather_image_current(tenki_data['current'], skcs_name, tz),\
        [make_weather_image_daily(tenki_data['daily'], skcs_name, tz),
         make_weather_image_hourly(tenki_data['hourly'], skcs_name, tz),
         make_weather_image_minutely(tenki_data['minutely'], skcs_name, tz)]

# UV指数
def get_uvi_info(uvi):
    if uvi < 3.0:
        return f"弱い", "rgb(204,242,255)"
    elif uvi < 6.0:
        return f"中程度", "rgb(255,255,204)"
    elif uvi < 8.0:
        return f"強い", "rgb(255,204,153)"
    elif uvi < 11.0:
        return f"非常に強い", "rgb(255,101,101)"
    else:
        return f"極端に強い", "rgb(255,101,255)"

# 16方位名
def get_wind_deg_name(deg):
    import math
    dname = ["北", "北北東", "北東", "東北東", "東", "東南東", "南東", "南南東",
             "南", "南南西", "南西", "西南西", "西", "西北西", "北西", "北北西", "北"]
    return dname[int((deg + 11.25)/22.5)]

# 現在の天気
def make_weather_image_current(wd, skcs_name, tz):
    tmp_dict = {}
    tmp_dict['曇り%'] = wd['clouds']
    tmp_dict['日時'] = datetime.fromtimestamp(
        wd['dt'], tz=tz).strftime("%m/%d %H:%M")
    tmp_dict['体感気温℃'] = f"{float(wd['feels_like']):.1f}"
    tmp_dict['湿度%'] = wd['humidity']
    tmp_dict['気圧hPa'] = wd['pressure']
    tmp_dict['日の出'] = datetime.fromtimestamp(
        wd['sunrise'], tz=tz).strftime("%H:%M:%S")
    tmp_dict['日の入'] = datetime.fromtimestamp(
        wd['sunset'], tz=tz).strftime("%H:%M:%S")
    tmp_dict['気温℃'] = f"{float(wd['temp']):.1f}"
    if 'uvi' in wd:
        uv_text, _ = get_uvi_info(wd['uvi'])
        tmp_dict['UV指数'] = f"{uv_text}({float(wd['uvi']):.1f})"
    else:
        tmp_dict['UV指数'] = "−"
    tmp_dict['天気'] = wd['weather'][0]['description']

    ret_text = f"現在({tmp_dict['日時']}時点)の{skcs_name}の天気\n"
    ret_text += f"{tmp_dict['天気']}:"
    ret_text += f"気温{tmp_dict['気温℃']}℃/湿度{tmp_dict['湿度%']}%/体感気温{tmp_dict['体感気温℃']}\n"
    ret_text += f"気圧{tmp_dict['気圧hPa']}hPa/UV指数「{tmp_dict['UV指数']}」/雲率{tmp_dict['曇り%']}\n"
    ret_text += f"日の出時刻は{tmp_dict['日の出']}、日の入時刻は{tmp_dict['日の入']}\n"
    ret_text += f"  by OpenWeatherMap API https://openweathermap.org/"

    return ret_text

# 1週間天気
def make_weather_image_daily(wd, skcs_name, tz):
    tenki_data_list = []

    for l1 in wd:
        tmp_dict = {}
        tmp_dict['日付'] = datetime.fromtimestamp(
            l1['dt'], tz=tz).strftime("%m/%d(%a)")
        tmp_dict['☀☁'] = ""  # お天気アイコン表示用
        tmp_dict['icon'] = l1['weather'][0]['icon']
        tmp_dict['天気'] = l1['weather'][0]['description']
        tmp_dict['最高気温℃'] = f"{float(l1['temp']['max']):.1f}"
        tmp_dict['最低気温℃'] = f"{float(l1['temp']['min']):.1f}"
        tmp_dict['降水確率%'] = int(float(l1['pop'])*100)
        tmp_dict['UV指数'], tmp_dict['uv_color'] = get_uvi_info(l1['uvi'])
        tmp_dict['font_color'] = FONT_COLOR
        tmp_dict['風速m/s'] = f"{float(l1['wind_speed']):.1f}"
        tmp_dict['風向'] = get_wind_deg_name(l1['wind_deg'])
        tmp_dict['気圧hPa'] = f"{int(l1['pressure']):,}"

        tenki_data_list.append(tmp_dict)

    df_temp = pd.json_normalize(tenki_data_list)
    df = df_temp[['日付', '☀☁', '天気', '最高気温℃',
                  '最低気温℃', '降水確率%', '風向', '風速m/s', '気圧hPa', 'UV指数']]  # テーブルの作成
    fig = go.Figure(data=[go.Table(
        columnwidth=[25, 10, 25, 20, 20, 20, 20, 20, 20, 20],  # カラム幅の変更
        header=dict(values=df.columns, align='center', font=dict(color=FONT_COLOR, size=18), height=30,
                    line_color=LINE_COLOR, fill_color=BG_COLOR),
        cells=dict(values=df.values.T, align='center', font=dict(color=[df_temp.font_color]*9 + [df_temp.uv_color], size=18), height=30, # ポイント:UV指数カラムの色指定
                   line_color=LINE_COLOR, fill_color=BG_COLOR),
    )],
        layout=dict(margin=dict(l=0, r=0, t=30, b=0), paper_bgcolor=BG_COLOR,
                    title=dict(
                        text=skcs_name+"の1週間天気", x=0.5, y=1.0, font=dict(color=FONT_COLOR, size=24), xanchor='center', yanchor='top', pad=dict(l=0, r=0, t=5, b=0))
                    )
    )

    # ポイント:お天気アイコン貼り付け。後から画像を乗せるイメージ。位置調整は手動。
    for i in range(1, len(df)+1, 1):
        # 天気アイコン取得
        icon_name = df_temp['icon'][i-1]
        icon_image = get_weather_icon(icon_name)
        fig.add_layout_image(
            dict(source=icon_image, x=0.125, y=(1.0-1.0/(len(df)+1)*(i+0.5))))
    fig.update_layout_images(dict(
        xref="paper", yref="paper", sizex=0.22, sizey=0.21, xanchor="left", yanchor="middle"))

    imagepath = os.path.join(WEATHER_IMAGE_PATH, "tmp_weather_d.png")
    fig.write_image(imagepath, height=30*(len(df)+2), width=1100, scale=1)

    return imagepath

# 48時間天気
def make_weather_image_hourly(wd, skcs_name, tz):
    tenki_data_list = []

    for l1 in wd:
        tmp_dict = {}
        tmp_dict['日時'] = datetime.fromtimestamp(
            l1['dt'], tz=tz).strftime("%m/%d %H時")
        tmp_dict['☀☁'] = ""  # お天気アイコン表示用
        tmp_dict['icon'] = l1['weather'][0]['icon']
        tmp_dict['天気'] = l1['weather'][0]['description']
        tmp_dict['気温℃'] = f"{float(l1['temp']): .1f}"
        tmp_dict['湿度%'] = l1['humidity']
        tmp_dict['体感気温℃'] = f"{float(l1['feels_like']):.1f}"
        tmp_dict['降水確率%'] = int(float(l1['pop'])*100)
        tmp_dict['風向'] = get_wind_deg_name(l1['wind_deg'])
        tmp_dict['風速m/s'] = f"{float(l1['wind_speed']):.1f}"
        tmp_dict['気圧hPa'] = f"{int(l1['pressure']):,}"

        tenki_data_list.append(tmp_dict)

    df_temp = pd.json_normalize(tenki_data_list)
    df = df_temp[['日時', '☀☁', '天気', '気温℃', '湿度%',
                  '体感気温℃', '降水確率%', '風向', '風速m/s', '気圧hPa']]  # テーブルの作成
    fig = go.Figure(data=[go.Table(
        # columnorder=[10, 20, 30, 40, 50, 25, 70],
        columnwidth=[25, 10, 25, 20, 20, 20, 20, 20, 20, 20],  # カラム幅の変更
        header=dict(values=df.columns, align='center', font=dict(color=FONT_COLOR, size=18), height=30,
                    line_color=LINE_COLOR, fill_color=BG_COLOR),
        cells=dict(values=df.values.T, align='center', font=dict(color=FONT_COLOR, size=18), height=30,
                   line_color=LINE_COLOR, fill_color=BG_COLOR),
    )],
        layout=dict(margin=dict(l=0, r=0, t=30, b=0), paper_bgcolor=BG_COLOR,
                    title=dict(
                        text=skcs_name+"の48時間天気", x=0.5, y=1.0, font=dict(color=FONT_COLOR, size=24), xanchor='center', yanchor='top', pad=dict(l=0, r=0, t=5, b=0))
                    ),

    )
    # お天気アイコン貼り付け
    for i in range(1, len(df)+1, 1):
        # 天気アイコン取得
        icon_name = df_temp['icon'][i-1]
        icon_image = get_weather_icon(icon_name)
        fig.add_layout_image(
            dict(source=icon_image, x=0.125, y=(1.0-1.0/49.0*(i+0.5))))
    fig.update_layout_images(dict(
        xref="paper", yref="paper", sizex=0.05, sizey=0.05, xanchor="left", yanchor="middle"))

    imagepath = os.path.join(WEATHER_IMAGE_PATH, "tmp_weather_h.png")
    fig.write_image(imagepath, height=30*(48+2), width=1100, scale=1)

    return imagepath

# 1時間降水量
def make_weather_image_minutely(wd, skcs_name, tz):
    tenki_data_list = []

    for l1 in wd:
        tmp_dict = {}
        tmp_dict['時刻'] = datetime.fromtimestamp(
            l1['dt'], tz=tz).strftime("%H:%M")
        tmp_dict['降水量mm'] = l1['precipitation']

        tenki_data_list.append(tmp_dict)

    df_temp = pd.json_normalize(tenki_data_list)
    df = df_temp[['時刻', '降水量mm']].round({'降水量mm': 2})  # テーブルの作成
    fig = go.Figure([go.Bar(x=df['時刻'], y=df['降水量mm'], text=df['降水量mm'], textposition='auto',
                            marker=dict(color='rgba(150,150,255,0.8)'),
                            y0=0
                            )],
                    layout=dict(margin=dict(l=0, r=0, t=30, b=0), paper_bgcolor=BG_COLOR, plot_bgcolor=BG_COLOR,
                                title=dict(
                                    text=skcs_name+"の1時間の降水量", x=0.5, y=1.0, font=dict(color=FONT_COLOR, size=24), xanchor='center', yanchor='top', pad=dict(l=0, r=0, t=5, b=0)),
                                font=dict(color=FONT_COLOR, size=18),
                                xaxis=dict(title='時刻', showgrid=False),
                                yaxis=dict(title='降水量mm', showgrid=False,
                                           rangemode='nonnegative') # ポイント:負の値は取らないグラフなので0起点にする
                                )
                    )

    imagepath = os.path.join(WEATHER_IMAGE_PATH, "tmp_weather_m.png")
    fig.write_image(imagepath, height=400, width=1600, scale=1)

    return imagepath

# 天気アイコン取得
def get_weather_icon(icon_name):
    icon_image_path = os.path.join(
        WEATHER_IMAGE_PATH, ICON_DIR, icon_name + ".png")
    if os.path.exists(icon_image_path):
        pass
    else:
        url = f"http://openweathermap.org/img/wn/{icon_name}@4x.png"
        with urllib.request.urlopen(url) as web_file:
            data = web_file.read()
            with open(icon_image_path, mode='wb') as local_file:
                local_file.write(data)

    return Image.open(icon_image_path)

if __name__ == '__main__':
    pp(get_tenki("札幌市中央区", "<<api_key>>"))

おわりに

 とりあえず、イメージした表・グラフの形になりました。
 (アドベントカレンダー感はありませんが、まあよいでしょう。)
 夏に実装したのですが、いつの間にか冬になってました。夏にAPIを叩いたときは降雪量情報は返ってこなかったのですが、今なら返ってくるかもしれないので試してみたいと思います。

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?