初めに
Qitta初投稿です!
至らぬ点も多いかと思いますが、暖かくコメント頂けると嬉しいです!
概要
- 無料のapiを提供しているOpenWeatherMapのapiを叩くことで、全国各地の天気予報を取得する。
- HTTPリクエストはhttpclientを使用し、rake taskに実装する。
開発環境
ruby: 2.7.1
rails: 6.0.3.2
手順
- API KEYの取得
- 取得したい都市のCITY IDを取得
- Cityテーブルの作成・保存
- HTTPリクエストの実装
- WeatherForecastテーブルの作成・保存
1. API KEYの取得
OpenWeatherMapのホームページにアクセスし、Sign inからCreate an accountでアカウントを作成しましょう。送られてくるメールから有効化するとAPI KEYが送られてきます。
これを環境変数なりcredentials
なりに保存しておきます。今回はcredentials
を利用していきます。
credentials
に関してはこちらの方の記事がとても参考になります。
EDITOR=vi bin/rails credentials:edit
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
をダウンロードします。
このファイルから、取得したい都市のCITY IDを取得していきます。中身一部抜粋です。
ちなみにlonは経度、latは緯度を指します。
{
"id": 1850147,
"name": "Tokyo",
"state": "",
"country": "JP",
"coord": {
"lon": 139.691711,
"lat": 35.689499
}
},
私はこのid取得を泣く泣く手作業でしました...
同じ名前の都市でも経緯度が違うものが混じっているので注意が必要です!
エクセルやmacならnumbersなどにリストアップしてCSVに変換すると良いと思います!
ちなみに私が作成した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
として保存しています。
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等で試してみると良いと思います。)
{
"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: 降水量
- main
降水がある場合のみ、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.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クラスです。
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
クラスでほぼメソッドを書いたので、後はそれを使うだけです。
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を叩くようにしておくと良いかと思います!
ありがとうございました!
参考
https://openweathermap.org/api
https://qiita.com/yoshito410kam/items/26c3c6e519d4990ed739