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

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

# 概要
- 無料のapiを提供している[OpenWeatherMap](https://openweathermap.org/api
)のapiを叩くことで、全国各地の天気予報を取得する。
- HTTPリクエストは[httpclient](https://github.com/nahi/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](https://home.openweathermap.org/users/sign_up)でアカウントを作成しましょう。送られてくるメールから有効化するとAPI KEYが送られてきます。

これを環境変数なり`credentials`なりに保存しておきます。今回は`credentials`を利用していきます。
`credentials`に関してはこちらの方の[記事](https://qiita.com/NaokiIshimura/items/2a179f2ab910992c4d39)がとても参考になります。

```
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ドキュメント](https://openweathermap.org/forecast5)を参考にリクエストを送るURIを取得しています。

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

# 2. CITY IDの取得
[APIドキュメント](https://openweathermap.org/forecast5)から`city.list.json`をダウンロードします。![スクリーンショット 2020-08-02 15.54.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684915/9535a511-db9d-f4b3-f042-028b93b1c498.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.rb`か`task`にコードを書いて、データベースに保存していきます。今回は`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](https://docs.ruby-lang.org/ja/latest/class/CSV.html)メソッドで先ほど作成した`cities.csv`を一行ずつ読み込みます。`row[0]`で1列目の都市名、`row[1]`で2列目のCITY IDが取得できるので、ハッシュの配列を作成して`City.create!`でデータベースに保存しています。

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

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

```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は[こちら](https://openweathermap.org/weather-conditions)を参考にしてください。OpenWeatherMapでは、IDによって天気を分類しています。`description`から天気を取得することも可能です。しかし種類が多いため、今回の実装ではデータベースには天気IDを保存しておき、メソッドで天気を割り振るようにしています。

### HTTPリクエストの実装
まずは、[httpclient](https://rubygems.org/gems/httpclient)をGemfileに追加し、`bundle install`します。

```ruby
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に変換してから保存しています。

タイムゾーンの扱いについてはこちらの[記事](https://qiita.com/jnchito/items/cae89ee43c30f5d6fa2c)が参考になります。

# 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日分ずつデータベースに保存、更新する仕様にしました。
ポイントはリクエスト上限と、データの作成なのか更新なのかという点です。

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

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

こんな感じで表示でしました！
![スクリーンショット 2020-08-02 18.26.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684915/03aea626-f199-5769-2020-c726255ef229.png)


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

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