LoginSignup
9
8

More than 3 years have passed since last update.

RailsでOpenWeatherMapから天気予報を取得する

Last updated at Posted at 2020-08-02

初めに

Qitta初投稿です!
至らぬ点も多いかと思いますが、暖かくコメント頂けると嬉しいです!

概要

  • 無料のapiを提供しているOpenWeatherMapのapiを叩くことで、全国各地の天気予報を取得する。
  • HTTPリクエストはhttpclientを使用し、rake taskに実装する。

開発環境

ruby: 2.7.1
rails: 6.0.3.2

手順

  1. API KEYの取得
  2. 取得したい都市のCITY IDを取得
  3. Cityテーブルの作成・保存
  4. HTTPリクエストの実装
  5. WeatherForecastテーブルの作成・保存

1. API KEYの取得

OpenWeatherMapのホームページにアクセスし、Sign inからCreate an accountでアカウントを作成しましょう。送られてくるメールから有効化するとAPI KEYが送られてきます。

これを環境変数なりcredentialsなりに保存しておきます。今回はcredentialsを利用していきます。
credentialsに関してはこちらの方の記事がとても参考になります。

EDITOR=vi bin/rails credentials:edit
credentials.yml.enc
open_weather:
    appid: <API_KEY>
    uri: https://samples.openweathermap.org/data/2.5/forecast

なお、今回は3時間毎の天気を取得しようと思うので、APIドキュメントを参考にリクエストを送るURIを取得しています。

無料枠でも取得できる天気の種類は豊富にあるようです!APIドキュメントを色々と探してみると面白いです。

2. CITY IDの取得

APIドキュメントからcity.list.jsonをダウンロードします。スクリーンショット 2020-08-02 15.54.09.png

このファイルから、取得したい都市のCITY IDを取得していきます。中身一部抜粋です。
ちなみにlonは経度、latは緯度を指します。

city.list.json
  {
    "id": 1850147,
    "name": "Tokyo",
    "state": "",
    "country": "JP",
    "coord": {
      "lon": 139.691711,
      "lat": 35.689499
    }
  },

私はこのid取得を泣く泣く手作業でしました...
同じ名前の都市でも経緯度が違うものが混じっているので注意が必要です!

エクセルやmacならnumbersなどにリストアップしてCSVに変換すると良いと思います!
ちなみに私が作成したCSVはこんな感じです。一部カラムを削ってます。

db/csv/cities.csv
札幌,2128295
青森,2130658
盛岡,2111834
仙台,2111149
秋田,2113126
山形,2110556
福島,2112923
水戸,2111901
宇都宮,1849053
前橋,1857843
さいたま,6940394
千葉,2113015
東京,1850147
横浜,1848354
新潟,1855431
富山,1849876
金沢,1860243
福井,1863983
山梨,1848649
長野,1856215
岐阜,1863640
静岡,1851715
名古屋,1856057
津,1849796
大津,1853574
京都,1857910
大阪,1853909
神戸,1859171
奈良,1855612
和歌山,1926004
鳥取,1849890
松江,1857550
岡山,1854383
広島,1862415
山口,1848689
徳島,1850158
高松,1851100
松山,1926099
高知,1859146
福岡,1863967
佐賀,1853303
長崎,1856177
熊本,1858421
大分,1854487
宮崎,1856717
鹿児島,1860827
那覇,1856035

3. Cityテーブルの作成・保存

seeds.rbtaskにコードを書いて、データベースに保存していきます。今回はlib/tasks以下に実装しました。CITY IDはカラム名をlocation_idとして保存しています。

import_csv.rake
  desc 'Import cities'
  task cities: [:environment] do
    list = []
    CSV.foreach('db/csv/cities.csv') do |row|
      list << {
        name: row[0],
        location_id: row[1],
      }
    end

    puts 'start creating cities'
    begin
      City.create!(list)
      puts 'completed!'
    rescue ActiveModel::UnknownAttributeError
      puts 'raised error: unknown attributes'
    end
  end

CSV.foreachメソッドで先ほど作成したcities.csvを一行ずつ読み込みます。row[0]で1列目の都市名、row[1]で2列目のCITY IDが取得できるので、ハッシュの配列を作成してCity.create!でデータベースに保存しています。

4. HTTPリクエストの実装

レスポンスの解析

HTTPリクエストを実装する前に、まずは、レスポンスのJSONファイルを解析していきます。

APIドキュメントに各項目について詳しく説明が書かれているので、それを参考に欲しいデータのキーを取得していきます。
1つのCityに関するリクエストは以下のようなJSON形式で返ってきます。
(curlコマンドや、VScodeをお使いの方はREST Client等で試してみると良いと思います。)

example_resopnse.json
{
  "cod": "200",
  "message": 0,
  "cnt": 40,
  "list": [
    {
      "dt": 1578409200,
      "main": {
        "temp": 284.92,
        "feels_like": 281.38,
        "temp_min": 283.58,
        "temp_max": 284.92,
        "pressure": 1020,
        "sea_level": 1020,
        "grnd_level": 1016,
        "humidity": 90,
        "temp_kf": 1.34
      },
      "weather": [
        {
          "id": 804,
          "main": "Clouds",
          "description": "overcast clouds",
          "icon": "04d"
        }
      ],
      "clouds": {
        "all": 100
      },
      "wind": {
        "speed": 5.19,
        "deg": 211
      },
      "sys": {
        "pod": "d"
      },
      "dt_txt": "2020-01-07 15:00:00"
    },

今回は以下の項目を使ってテーブルを作成しています。

  • list
    • main
      • feels_like: 体感気温
      • temp_max: 最高気温
      • temp_min: 最低気温
    • weather
      • id: 天気ID
    • rain
      • 3h: 降水量

降水がある場合のみ、rainがlistに追加されます。

なお、天気IDはこちらを参考にしてください。OpenWeatherMapでは、IDによって天気を分類しています。descriptionから天気を取得することも可能です。しかし種類が多いため、今回の実装ではデータベースには天気IDを保存しておき、メソッドで天気を割り振るようにしています。

HTTPリクエストの実装

まずは、httpclientをGemfileに追加し、bundle installします。

gem 'httpclient'

次にlib/api/open_weather_map/request.rbを作成します。
このapiに関して他のクラスを実装したり、他のapiクラスを実装することを考慮してこのようなファイル配置にしました。

lib以下はデフォルトではtask以外読み込まれないので、config/application.rbに、以下の設定が必要です。eager_load_pathsなので本番環境も大丈夫です。

config/application.rb
config.paths.add 'lib', eager_load: true

リクエストを保存するWeatherForecastテーブル↓

WeatherForecast
temp_max float
temp_min float
temp_feel float
weather_id int
rainfall float
date datetime
aquired_at datetime

以下が実装したRequestクラスです。

request.rb
module Api
  module OpenWeatherMap
    class Request
      attr_accessor :query

      def initialize(location_id)
        @query = {
          id: location_id,
          units: 'metric',
          appid: Rails.application.credentials.open_weather[:appid],
        }
      end

      def request
        client = HTTPClient.new
        request = client.get(Rails.application.credentials.open_weather[:uri], query) # 戻り値は3時間ごとのデータ5日分
        JSON.parse(request.body)
      end

      def self.attributes_for(attrs)
        rainfall = attrs['rain']['3h'] if attrs['rain']
        date = attrs['dt_txt'].in_time_zone('UTC').in_time_zone

        {
          temp_max: attrs['main']['temp_max'],
          temp_min: attrs['main']['temp_min'],
          temp_feel: attrs['main']['feels_like'],
          weather_id: attrs['weather'][0]['id'],
          rainfall: rainfall,
          date: date,
          aquired_at: Time.current,
        }
      end
    end
  end
end

initializeでクエリストリングを設定しています。今回のリクエストで必要なクエリストリングは、CITY IDを示すlocation_idとAPI KEY, そして気温の表示を摂氏表示に変更するためにunits: 'metric'を加えています。

返ってきたリクエストをデータベースに保存できる形に直すためにattributes_forメソッドをクラスメソッドにしています。

注意が必要なのは、降水量と予報日付です。

  • 降水量は、降水がない場合は項目が存在しません。なのである場合のみ取得するように条件分岐しています。
  • 予報日付に関しては、タイムゾーンに注意が必要です。OpenWeatherMapのタイムゾーンはUTCなので、JTCに変換してから保存しています。

タイムゾーンの扱いについてはこちらの記事が参考になります。

5. WeatherForecastテーブルの作成・保存

rake taskの作成

今回のようにアクションの度にではなく、定期的にapiを叩きたい場合は、処理をrake taskに書くのが一般的です。と言っても、先ほどのRequestクラスでほぼメソッドを書いたので、後はそれを使うだけです。

open_weather_api.rake
namespace :open_weather_api do
  desc 'Requests and save in database'
  task weather_forecasts: :environment do
    City.all.each do |city|
      open_weather = Api::OpenWeatherMap::Request.new(city.location_id)

      # リクエスト上限:60回/min
      response = open_weather.request

      # 3時間ごとのデータ2日分を保存
      16.times do |i|
        params = Api::OpenWeatherMap::Request.attributes_for(response['list'][i])
        if weather_forecast = WeatherForecast.where(city: city, date: params[:date]).presence
          weather_forecast[0].update!(params)
        else
          city.weather_forecasts.create!(params)
        end
      end
    end
    puts 'completed!'
  end
end

今回は、3時間毎のデータを2日分ずつデータベースに保存、更新する仕様にしました。
ポイントはリクエスト上限と、データの作成なのか更新なのかという点です。

  • リクエスト上限は無料プランでは60calls/minです。登録している都市が60を超える場合は、分けてリスエストを送る必要があります。今回は47なので問題ありません。
  • presenceメソッドはpresent?メソッドを呼び出して、trueだった場合はレシーバー自身を返すメソッドです。同じ都市の同時刻に関する予報がすでにデータベースに存在する場合はupdate!を、存在しない場合はcreate!を呼び出しています。

最後に

保存したweather_idに対応する天気のアイコンを用意すると天気予報らしい見た目になると思います!
cronや、herokuならheroku schedularなどで定期的にapiを叩くようにしておくと良いかと思います!

こんな感じで表示でしました!
スクリーンショット 2020-08-02 18.26.45.png

ありがとうございました!

参考

https://openweathermap.org/api
https://qiita.com/yoshito410kam/items/26c3c6e519d4990ed739

9
8
1

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
9
8