参考ページ
はじめに
仕事で気象庁APIを取り扱った。政府ドメイン(goドメイン)で信頼はできそうだが、クセが強いうえにまともにドキュメントがないのでまとめる。これは共有すべきナレッジだな
データの観察とマスタデータの作成
まずはマスタをつくっていく。このプログラムはいわゆるシーダーデータを作成し、その後に控える予報データ取得プログラムが、それを参照し、データを処理していく(以下のurlを見ると .../const/...
と書いてあるね)
https://www.jma.go.jp/bosai/common/const/area.json
https://www.jma.go.jp/bosai/forecast/const/forecast_area.json
https://www.jma.go.jp/bosai/amedas/const/amedastable.json
プロパティ | 階層 |
---|---|
centers | 地方予報区(11区) |
offices | 府県予報区(58区) |
class10s | 一次細分区域(142区) |
class15s | 市町村等をまとめた地域 |
class20s | 二次細分区域 |
jma_area
いわゆる地方区分。生データでは center
という名前で取り扱われているので、area
と名付けた。
code | name |
---|---|
010600 | 近畿地方 |
jma_prefecture
いわゆる都道府県(北海道と沖縄が、より分割されている)。生データでは office
という名前で取り扱われているので、prefecture
と名付けた。
code | jma_area_id | name |
---|---|---|
250000 | 010600 | 滋賀県 |
260000 | 010600 | 京都府 |
270000 | 010600 | 大阪府 |
280000 | 010600 | 兵庫県 |
290000 | 010600 | 奈良県 |
300000 | 010600 | 和歌山県 |
jma_region
ひとつの都道府県の、ある範囲(北部とか南部)。生データでは class10
という名前で取り扱われているので region
と名付けた。
この region がキモの概念になる
code | jma_prefecture_id | name |
---|---|---|
280010 | 280000 | 南部 |
280020 | 280000 | 北部 |
jma_city
ひとつの都道府県のひとつの市区町村(一部市町村を分割している)。生データでは class20
という名前で取り扱われているので city
と名付けた。
no. | code | jma_region_id | name |
---|---|---|---|
1 | 2810000 | 280010 | 神戸市 |
2 | 2820200 | 280010 | 尼崎市 |
3 | 2820400 | 280010 | 西宮市 |
4 | 2820600 | 280010 | 芦屋市 |
5 | 2820700 | 280010 | 伊丹市 |
6 | 2821400 | 280010 | 宝塚市 |
7 | 2821700 | 280010 | 川西市 |
8 | 2821900 | 280010 | 三田市 |
9 | 2830100 | 280010 | 猪名川町 |
10 | 2821300 | 280010 | 西脇市 |
11 | 2822100 | 280010 | 丹波篠山市 |
12 | 2822300 | 280010 | 丹波市 |
13 | 2836500 | 280010 | 多可町 |
14 | 2822700 | 280010 | 宍粟市 |
15 | 2844200 | 280010 | 市川町 |
16 | 2844300 | 280010 | 福崎町 |
17 | 2844600 | 280010 | 神河町 |
18 | 2850100 | 280010 | 佐用町 |
19 | 2820300 | 280010 | 明石市 |
20 | 2821000 | 280010 | 加古川市 |
21 | 2821500 | 280010 | 三木市 |
22 | 2821600 | 280010 | 高砂市 |
23 | 2821800 | 280010 | 小野市 |
24 | 2822000 | 280010 | 加西市 |
25 | 2822800 | 280010 | 加東市 |
26 | 2838100 | 280010 | 稲美町 |
27 | 2838200 | 280010 | 播磨町 |
28 | 2820100 | 280010 | 姫路市 |
29 | 2820800 | 280010 | 相生市 |
30 | 2821200 | 280010 | 赤穂市 |
31 | 2822900 | 280010 | たつの市 |
32 | 2846400 | 280010 | 太子町 |
33 | 2848100 | 280010 | 上郡町 |
34 | 2820500 | 280010 | 洲本市 |
35 | 2822400 | 280010 | 南あわじ市 |
36 | 2822600 | 280010 | 淡路市 |
jma_amedas
いわゆる気象観測所。生データでは amedas
という名前で取り扱われているのでそのまま amedas
と名付けた。
id | jma_region_id |
---|---|
63518 | 280010 |
63576 | 280010 |
63571 | 280010 |
63383 | 280010 |
ER図
クセ強ポイント
amedasのグルーピングについて
- 市区町村別のアメダスリスト
forecast_area.json
はリージョン単位でグルーピングされている - region に複数のアメダスがある場合、代表cityがある
- ということは amedas が city へ紐づかないといけないが、class10(=region)へ紐づける
{
:
"280000": [
{
"class10": "280010", ←region: 南部
"amedas": [
"63518", ←amedas: 神戸
"63576", ←amedas: (欠番!)
"63571", ←amedas: 洲本
"63383" ←amedas: 姫路
],
"class20": "2810000" ←city: 神戸市(南部代表都市)
},
{
"class10": "280020", ←region: 北部
"amedas": [
"63051" ←amedas: 豊岡
],
"class20": "2820900" ←city: 豊岡市(北部代表都市)
}
]
:
}
予報データについて
- 天気予報ファイルは prefecture で取得する(例:
280000.json
)- 気温情報がアメダス単位で収録されている(アメダスは region に紐づいている)
- アプリ側のターゲットとなる建物等の住所から region を特定できる
- 神戸は南部なので、せめて兵庫県南部の気温を抽出したい
- 「本日」の予報データは時間の経過とともに値の要素数が減っていく(あたりまえっちゃあたりまえか...)
[
{
"publishingOffice": "神戸地方気象台",
"reportDatetime": "2024-03-28T17:00:00+09:00",
"timeSeries": [
{
"areas": [
{
"area": {"name": "神戸", "code": "63518"}, ←region: 南部
"temps": ["13", "19"]
},
{
"area": {"name": "豊岡", "code": "63051"}, ←region: 北部
"temps": ["12", "22"]
},
{
"area": {"name": "洲本", "code": "63571"}, ←region: 南部
"temps": ["14", "20"]
},
{
"area": {"name": "姫路", "code": "63383"}, ←region: 南部
"temps": ["14", "21"]
}
]
}
]
}
:
]
amedasマスタについて
- アメダスidは市区町村と紐づかない ←
{
"63051": {
"type": "B",
"elems": "11111111",
"lat": [35, 32.1],
"lon": [134, 49.3],
"alt": 3,
"kjName": "豊岡",
"knName": "トヨオカ",
"enName": "Toyooka"
},
"63518": {
"type": "A",
"elems": "11111111",
"lat": [34, 41.8],
"lon": [135, 12.7],
"alt": 5,
"kjName": "神戸",
"knName": "コウベ",
"enName": "Kobe"
},
"63571": {
"type": "B",
"elems": "11111011",
"lat": [34, 18.6],
"lon": [134, 50.9],
"alt": 69,
"kjName": "洲本",
"knName": "スモト",
"enName": "Sumoto"
},
"63383": {
"type": "B",
"elems": "11111011",
"lat": [34, 50.3],
"lon": [134, 40.2],
"alt": 38,
"kjName": "姫路",
"knName": "ヒメジ",
"enName": "Himeji"
}
}
気象庁データについて
もう一度この絵を見てみよう。姫路市を選んで表示しても神戸市を選んで表示しても3つの都市が必ず表示される。そして、表示された3つの都市以外の兵庫県南部の都市(南あわじ市 など)を表示してもこの3つの代表都市が表示される。ということはアメダスと市区町村は紐づかず(アメダスマスタ全量の天気データはない)、結局リージョン単位なんだなということがわかる。それ以上のデータがほしければ金払って民間APIだね。
姫路市 | 南あわじ市 |
---|---|
アプリからの使われかたを考える
アプリにおける使われ方としては、建物Aを登録するときに、市区町村を気象庁APIベースのマスタからドロップダウンで選択させるのがいいだろうな。
マスタを保存する仕組みを作ろう
create root directory
mkdir jma_weather
cd jma_weather
venv
python -m venv venv311
create project
pip install django
django-admin startproject config .
create app
python manage.py startapp weather
update settings
INSTALLED_APPS = [
:
+ "weather",
]
create model
from django.db import models
class JmaArea(models.Model):
"""
an area in the JMA
生データでは center という名前で取り扱われている
Attributes:
code (str): エリアコード
name (str): エリア名
"""
code = models.CharField(unique=True, max_length=6)
name = models.CharField(max_length=100)
class JmaPrefecture(models.Model):
"""
a prefecture in the JMA
生データでは office という名前で取り扱われている
Attributes:
code (CharField): 都道府県コード
jma_area (ForeignKey): FK to JmaArea
name (CharField): 都道府県名
"""
code = models.CharField(unique=True, max_length=6)
jma_area = models.ForeignKey(JmaArea, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
class JmaRegion(models.Model):
"""
a region in the JMA
生データでは class10 という名前で取り扱われている
Attributes:
code (str): リージョンコード
jma_prefecture (JmaPrefecture): FK to JmaPrefecture
name (str): リージョン名
"""
code = models.CharField(unique=True, max_length=6)
jma_prefecture = models.ForeignKey(JmaPrefecture, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
class JmaCity(models.Model):
"""
a city in the JMA
生データでは class20 という名前で取り扱われている
Attributes:
code (str): 市区町村コード
jma_region (JmaRegion): FK to JMA region
name (str): 市区町村名
"""
code = models.CharField(unique=True, max_length=7)
jma_region = models.ForeignKey(JmaRegion, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
class JmaAmedas(models.Model):
"""
a AMeDas in the JMA
Attributes:
code (str): アメダス観測所コード
jma_region (JmaRegion): FK to JMA region
"""
code = models.CharField(unique=True, max_length=5)
jma_region = models.ForeignKey(JmaRegion, on_delete=models.CASCADE)
class JmaWeatherCode(models.Model):
"""
天気コードマスタ
Attributes:
code (CharField): "123"
summary_code (CharField): "100"
image (FileField): "100.svg"
name (CharField): "晴"
name_en (CharField): "CLEAR, FREQUENT SNOW FLURRIES LATER"
"""
code = models.CharField(unique=True, max_length=3)
summary_code = models.CharField(max_length=3)
image = models.CharField(max_length=7)
name = models.CharField(max_length=20)
name_en = models.CharField(max_length=100)
class JmaWeather(models.Model):
"""
日々の天気(1時間ごとにバッチで取得)
See Also: https://www.jma.go.jp/bosai/forecast/#area_type=class20s&area_code=2820100
Attributes:
jma_region (JmaRegion): A ForeignKey field representing the region for which the weather data is recorded.
reporting_date (DateField): A DateField representing the date on which the weather data is reported.
jma_weather_code (JmaWeatherCode): A ForeignKey field representing the weather code for the recorded data.
weather_text (CharField): A CharField representing the text description of the weather.
wind_text (CharField): A CharField representing the text description of the wind conditions.
wave_text (CharField): A CharField representing the text description of the wave conditions.
avg_rain_probability (FloatField, optional): 降水確率
avg_min_temperature (FloatField, optional): 最低気温
avg_max_temperature (FloatField, optional): 最高気温
avg_max_wind_speed (FloatField, optional): 最大風速
"""
jma_region = models.ForeignKey(JmaRegion, on_delete=models.CASCADE)
reporting_date = models.DateField()
jma_weather_code = models.ForeignKey(JmaWeatherCode, on_delete=models.CASCADE)
weather_text = models.CharField(max_length=255)
wind_text = models.CharField(max_length=255)
wave_text = models.CharField(max_length=255)
avg_rain_probability = models.FloatField(null=True)
avg_min_temperature = models.FloatField(null=True)
avg_max_temperature = models.FloatField(null=True)
avg_max_wind_speed = models.FloatField(null=True)
class JmaWarning(models.Model):
"""
Model representing a JMA warning.
See Also: https://www.jma.go.jp/bosai/warning/#area_type=class20s&area_code=2810000&lang=ja
Attributes:
jma_region (ForeignKey): The JMA region associated with the warning.
warnings (CharField): The description of the warning.
"""
jma_region = models.ForeignKey(JmaRegion, on_delete=models.CASCADE)
warnings = models.CharField(max_length=100)
migration
python manage.py makemigrations weather
python manage.py migrate
create vo
from dataclasses import dataclass
# マスタデータ
@dataclass
class JmaConstWeatherCode:
"""
気象庁の定数マスタ(天気コード)
Attributes:
code (str): The code of the weather.
image_day (str): The image path for day weather.
image_night (str): The image path for night weather.
summary_code (str): The summary code for the weather.
name (str): The name of the weather.
name_en (str): The English name of the weather.
"""
code: str
image_day: str
image_night: str
summary_code: str
name: str
name_en: str
@dataclass
class JmaConstGeographicArea:
"""
気象庁の定数マスタ(area, prefecture, region, city_group, city)
Attributes:
code (str): The code of the constant.
name (str): The name of the constant.
children (list[str]): The list of children constants' codes.
parent (str): The code of the parent constant.
"""
code: str
name: str
children: list[str]
parent: str
# 予報データ
class MeanCalculable:
"""
平均を出すための生値とその平均値
Args:
float_list (list[float]): 生値
"""
def __init__(self, float_list: list[float]) -> None:
self.raw: list[float] = float_list
self.mean: float = (
round(sum(float_list) / len(float_list), 1) if float_list else 0
)
@dataclass(frozen=True)
class Region:
"""
A class representing a region.
Attributes:
code (str): "280010"
name (str): "南部"
"""
code: str
name: str
@dataclass(frozen=True)
class SummaryText:
"""
気象情報: サマリー情報
Attributes:
weather (str): "雨 所により 雷を伴い 非常に 激しく 降る"
wind (str): "東の風 やや強く 海上 では 東の風 強く"
wave (str): "1.5メートル ただし 淡路島南部 では 3メートル うねり を伴う"
"""
weather: str
wind: str
wave: str
@dataclass(frozen=True)
class RainData:
"""
気象情報: 降水確率
Attributes:
values (MeanCalculable): 生値とその平均値(時間帯平均)
unit (str): "%"
"""
values: MeanCalculable
unit: str = "%"
@dataclass(frozen=True)
class TemperatureData:
"""
気象情報: 気温
Attributes:
min_values (MeanCalculable): 生値とその平均値(region平均)
max_values (MeanCalculable): 生値とその平均値(region平均)
unit (str): "℃"
"""
min_values: MeanCalculable
max_values: MeanCalculable
unit: str = "℃"
class WindData:
"""
気象情報: 最大風速
Attributes:
values (MeanCalculable): 生値とその平均値(時間帯平均)
unit (str): "以下" or "メートル毎秒"
Notes: 風速値が9のばあい、その object に condition というキーが現れて "以下" という値が入るが
表現を間引いて、平均値が9以下だったら "以下" にすることにした
"""
def __init__(self, values: MeanCalculable):
self.values = values
self.unit = "以下" if self.values.mean <= 9 else "メートル毎秒"
def __str__(self):
return f"最大風速(時間帯平均)は {self.values.mean} {self.unit}"
@dataclass(frozen=True)
class WarningData:
"""
A class representing warning data.
Attributes:
code (str): 14
status (str): 継続
"""
code: str
status: str
create repository
from weather.models import JmaPrefecture, JmaRegion, JmaAmedas
class JmaRepository:
@staticmethod
def get_amedas_code_list(prefecture_id: str, special_add_region_ids: dict) -> dict:
amedas_code_in_region = {}
jma_regions = JmaRegion.objects.filter(
jma_prefecture=JmaPrefecture.objects.get(code=prefecture_id)
).prefetch_related("jmaamedas_set")
for region in jma_regions:
amedas_code_in_region[region.code] = [
amedas.code for amedas in region.jmaamedas_set.all()
]
if prefecture_id in special_add_region_ids:
region = JmaRegion.objects.get(code=special_add_region_ids[prefecture_id])
special_add_region_code = special_add_region_ids[prefecture_id]
amedas_code_in_region[special_add_region_code] = list(
JmaAmedas.objects.filter(jma_region=region).values_list(
"code", flat=True
)
)
return amedas_code_in_region
create test
from datetime import datetime
from unittest import mock
from django.test import TestCase
from weather.management.commands import fetch_weather_forecast
from weather.management.commands.fetch_weather_forecast import (
get_data,
get_indexes,
)
class TestFetchWeatherForecast(TestCase):
def setUp(self):
# 天気予報取得バッチの 十勝地方、奄美地方 変換
self.func = fetch_weather_forecast.update_prefecture_ids
def test_update_prefecture_ids_with_invalid_id(self):
prefecture_ids = ["014030", "032010", "082050", "460040"]
updated_prefecture_ids, special_add_region_ids = self.func(prefecture_ids)
self.assertListEqual(
updated_prefecture_ids, ["032010", "082050", "014100", "460100"]
)
self.assertEqual(
special_add_region_ids, {"014100": "014030", "460100": "460040"}
)
def test_update_prefecture_ids_without_invalid_id(self):
prefecture_ids = ["014100", "032010", "082050", "460100"]
updated_prefecture_ids, special_add_region_ids = self.func(prefecture_ids)
self.assertListEqual(
updated_prefecture_ids, ["014100", "032010", "082050", "460100"]
)
self.assertEqual(special_add_region_ids, {})
def test_update_prefecture_ids_with_proxy_id(self):
# [("014030", "014100"), ("460040", "460100")]
prefecture_ids = [
"014030",
"032010",
"082050",
"460040",
"014100",
"460100",
]
updated_prefecture_ids, special_add_region_ids = self.func(prefecture_ids)
self.assertListEqual(
updated_prefecture_ids, ["032010", "082050", "014100", "460100"]
)
self.assertEqual(
special_add_region_ids, {"014100": "014030", "460100": "460040"}
)
def test_update_prefecture_ids_with_empty_list(self):
prefecture_ids = []
updated_prefecture_ids, special_add_region_ids = self.func(prefecture_ids)
self.assertListEqual(updated_prefecture_ids, [])
self.assertEqual(special_add_region_ids, {})
class TestGetDataAndIndexes(TestCase):
THREE_DAYS = 0
TYPE_OVERVIEW = 0
def setUp(self):
self.target_date = datetime.strptime("2024-08-31", "%Y-%m-%d").date()
self.mock_response_data = [
{
# This corresponds to the THREE_DAYS constant
"timeSeries": [
{
"timeDefines": [
"2024-08-30T17:00:00+09:00",
"2024-08-31T00:00:00+09:00",
"2024-09-01T00:00:00+09:00",
],
"areas": [{"area": {"name": "南部", "code": "280010"}}],
},
"dummy POPS",
"dummy TEMP",
]
},
"dummy a week forecast",
]
self.mock_response = mock.MagicMock()
self.mock_response.status_code = 200
self.mock_response.json.return_value = self.mock_response_data
@mock.patch("requests.get")
def test_get_data(self, mock_requests_get):
# Mocking requests.get to return the prepared data
mock_requests_get.return_value = self.mock_response
url = "http://test.url" # This url doesn't matter as we mock the requests.get
data_time_series = get_data(url)
self.assertEqual(
self.mock_response_data[TestGetDataAndIndexes.THREE_DAYS]["timeSeries"],
data_time_series,
)
def test_get_indexes(self):
indexes = get_indexes(
data_time_defines=self.mock_response_data[TestGetDataAndIndexes.THREE_DAYS][
"timeSeries"
][TestGetDataAndIndexes.TYPE_OVERVIEW]["timeDefines"],
desired_date=self.target_date,
)
self.assertEqual(indexes, [1])
create import_weather_master_manual.py
import os
import shutil
import requests
from django.core.management.base import BaseCommand
from soil_analysis.domain.valueobject.weather.jma import JmaConstWeatherCode
from soil_analysis.models import (
JmaWeatherCode,
)
class Command(BaseCommand):
help = "Import jma weather code master from manual data."
def handle(self, *args, **options):
"""
任意の都市の天気ページの `ページのソースを表示` から `TELOPS=` でページ内検索して data に当て込む
https://www.jma.go.jp/bosai/forecast/#area_type=class20s&area_code=2810000
Args:
*args: Additional positional arguments.
**options: Additional keyword arguments.
"""
data = {
100: ["100.svg", "500.svg", "100", "\u6674", "CLEAR"],
101: [
"101.svg",
"501.svg",
"100",
"\u6674\u6642\u3005\u66C7",
"PARTLY CLOUDY",
],
102: [
"102.svg",
"502.svg",
"300",
"\u6674\u4E00\u6642\u96E8",
"CLEAR, OCCASIONAL SCATTERED SHOWERS",
],
103: [
"102.svg",
"502.svg",
"300",
"\u6674\u6642\u3005\u96E8",
"CLEAR, FREQUENT SCATTERED SHOWERS",
],
104: [
"104.svg",
"504.svg",
"400",
"\u6674\u4E00\u6642\u96EA",
"CLEAR, SNOW FLURRIES",
],
105: [
"104.svg",
"504.svg",
"400",
"\u6674\u6642\u3005\u96EA",
"CLEAR, FREQUENT SNOW FLURRIES",
],
106: [
"102.svg",
"502.svg",
"300",
"\u6674\u4E00\u6642\u96E8\u304B\u96EA",
"CLEAR, OCCASIONAL SCATTERED SHOWERS OR SNOW FLURRIES",
],
107: [
"102.svg",
"502.svg",
"300",
"\u6674\u6642\u3005\u96E8\u304B\u96EA",
"CLEAR, FREQUENT SCATTERED SHOWERS OR SNOW FLURRIES",
],
108: [
"102.svg",
"502.svg",
"300",
"\u6674\u4E00\u6642\u96E8\u304B\u96F7\u96E8",
"CLEAR, OCCASIONAL SCATTERED SHOWERS AND/OR THUNDER",
],
110: [
"110.svg",
"510.svg",
"100",
"\u6674\u5F8C\u6642\u3005\u66C7",
"CLEAR, PARTLY CLOUDY LATER",
],
111: [
"110.svg",
"510.svg",
"100",
"\u6674\u5F8C\u66C7",
"CLEAR, CLOUDY LATER",
],
112: [
"112.svg",
"512.svg",
"300",
"\u6674\u5F8C\u4E00\u6642\u96E8",
"CLEAR, OCCASIONAL SCATTERED SHOWERS LATER",
],
113: [
"112.svg",
"512.svg",
"300",
"\u6674\u5F8C\u6642\u3005\u96E8",
"CLEAR, FREQUENT SCATTERED SHOWERS LATER",
],
114: [
"112.svg",
"512.svg",
"300",
"\u6674\u5F8C\u96E8",
"CLEAR,RAIN LATER",
],
115: [
"115.svg",
"515.svg",
"400",
"\u6674\u5F8C\u4E00\u6642\u96EA",
"CLEAR, OCCASIONAL SNOW FLURRIES LATER",
],
116: [
"115.svg",
"515.svg",
"400",
"\u6674\u5F8C\u6642\u3005\u96EA",
"CLEAR, FREQUENT SNOW FLURRIES LATER",
],
117: [
"115.svg",
"515.svg",
"400",
"\u6674\u5F8C\u96EA",
"CLEAR,SNOW LATER",
],
118: [
"112.svg",
"512.svg",
"300",
"\u6674\u5F8C\u96E8\u304B\u96EA",
"CLEAR, RAIN OR SNOW LATER",
],
119: [
"112.svg",
"512.svg",
"300",
"\u6674\u5F8C\u96E8\u304B\u96F7\u96E8",
"CLEAR, RAIN AND/OR THUNDER LATER",
],
120: [
"102.svg",
"502.svg",
"300",
"\u6674\u671D\u5915\u4E00\u6642\u96E8",
"OCCASIONAL SCATTERED SHOWERS IN THE MORNING AND EVENING, CLEAR DURING THE DAY",
],
121: [
"102.svg",
"502.svg",
"300",
"\u6674\u671D\u306E\u5185\u4E00\u6642\u96E8",
"OCCASIONAL SCATTERED SHOWERS IN THE MORNING, CLEAR DURING THE DAY",
],
122: [
"112.svg",
"512.svg",
"300",
"\u6674\u5915\u65B9\u4E00\u6642\u96E8",
"CLEAR, OCCASIONAL SCATTERED SHOWERS IN THE EVENING",
],
123: [
"100.svg",
"500.svg",
"100",
"\u6674\u5C71\u6CBF\u3044\u96F7\u96E8",
"CLEAR IN THE PLAINS, RAIN AND THUNDER NEAR MOUTAINOUS AREAS",
],
124: [
"100.svg",
"500.svg",
"100",
"\u6674\u5C71\u6CBF\u3044\u96EA",
"CLEAR IN THE PLAINS, SNOW NEAR MOUTAINOUS AREAS",
],
125: [
"112.svg",
"512.svg",
"300",
"\u6674\u5348\u5F8C\u306F\u96F7\u96E8",
"CLEAR, RAIN AND THUNDER IN THE AFTERNOON",
],
126: [
"112.svg",
"512.svg",
"300",
"\u6674\u663C\u9803\u304B\u3089\u96E8",
"CLEAR, RAIN IN THE AFTERNOON",
],
127: [
"112.svg",
"512.svg",
"300",
"\u6674\u5915\u65B9\u304B\u3089\u96E8",
"CLEAR, RAIN IN THE EVENING",
],
128: [
"112.svg",
"512.svg",
"300",
"\u6674\u591C\u306F\u96E8",
"CLEAR, RAIN IN THE NIGHT",
],
130: [
"100.svg",
"500.svg",
"100",
"\u671D\u306E\u5185\u9727\u5F8C\u6674",
"FOG IN THE MORNING, CLEAR LATER",
],
131: [
"100.svg",
"500.svg",
"100",
"\u6674\u660E\u3051\u65B9\u9727",
"FOG AROUND DAWN, CLEAR LATER",
],
132: [
"101.svg",
"501.svg",
"100",
"\u6674\u671D\u5915\u66C7",
"CLOUDY IN THE MORNING AND EVENING, CLEAR DURING THE DAY",
],
140: [
"102.svg",
"502.svg",
"300",
"\u6674\u6642\u3005\u96E8\u3067\u96F7\u3092\u4F34\u3046",
"CLEAR, FREQUENT SCATTERED SHOWERS AND THUNDER",
],
160: [
"104.svg",
"504.svg",
"400",
"\u6674\u4E00\u6642\u96EA\u304B\u96E8",
"CLEAR, SNOW FLURRIES OR OCCASIONAL SCATTERED SHOWERS",
],
170: [
"104.svg",
"504.svg",
"400",
"\u6674\u6642\u3005\u96EA\u304B\u96E8",
"CLEAR, FREQUENT SNOW FLURRIES OR SCATTERED SHOWERS",
],
181: [
"115.svg",
"515.svg",
"400",
"\u6674\u5F8C\u96EA\u304B\u96E8",
"CLEAR, SNOW OR RAIN LATER",
],
200: ["200.svg", "200.svg", "200", "\u66C7", "CLOUDY"],
201: [
"201.svg",
"601.svg",
"200",
"\u66C7\u6642\u3005\u6674",
"MOSTLY CLOUDY",
],
202: [
"202.svg",
"202.svg",
"300",
"\u66C7\u4E00\u6642\u96E8",
"CLOUDY, OCCASIONAL SCATTERED SHOWERS",
],
203: [
"202.svg",
"202.svg",
"300",
"\u66C7\u6642\u3005\u96E8",
"CLOUDY, FREQUENT SCATTERED SHOWERS",
],
204: [
"204.svg",
"204.svg",
"400",
"\u66C7\u4E00\u6642\u96EA",
"CLOUDY, OCCASIONAL SNOW FLURRIES",
],
205: [
"204.svg",
"204.svg",
"400",
"\u66C7\u6642\u3005\u96EA",
"CLOUDY FREQUENT SNOW FLURRIES",
],
206: [
"202.svg",
"202.svg",
"300",
"\u66C7\u4E00\u6642\u96E8\u304B\u96EA",
"CLOUDY, OCCASIONAL SCATTERED SHOWERS OR SNOW FLURRIES",
],
207: [
"202.svg",
"202.svg",
"300",
"\u66C7\u6642\u3005\u96E8\u304B\u96EA",
"CLOUDY, FREQUENT SCCATERED SHOWERS OR SNOW FLURRIES",
],
208: [
"202.svg",
"202.svg",
"300",
"\u66C7\u4E00\u6642\u96E8\u304B\u96F7\u96E8",
"CLOUDY, OCCASIONAL SCATTERED SHOWERS AND/OR THUNDER",
],
209: ["200.svg", "200.svg", "200", "\u9727", "FOG"],
210: [
"210.svg",
"610.svg",
"200",
"\u66C7\u5F8C\u6642\u3005\u6674",
"CLOUDY, PARTLY CLOUDY LATER",
],
211: [
"210.svg",
"610.svg",
"200",
"\u66C7\u5F8C\u6674",
"CLOUDY, CLEAR LATER",
],
212: [
"212.svg",
"212.svg",
"300",
"\u66C7\u5F8C\u4E00\u6642\u96E8",
"CLOUDY, OCCASIONAL SCATTERED SHOWERS LATER",
],
213: [
"212.svg",
"212.svg",
"300",
"\u66C7\u5F8C\u6642\u3005\u96E8",
"CLOUDY, FREQUENT SCATTERED SHOWERS LATER",
],
214: [
"212.svg",
"212.svg",
"300",
"\u66C7\u5F8C\u96E8",
"CLOUDY, RAIN LATER",
],
215: [
"215.svg",
"215.svg",
"400",
"\u66C7\u5F8C\u4E00\u6642\u96EA",
"CLOUDY, SNOW FLURRIES LATER",
],
216: [
"215.svg",
"215.svg",
"400",
"\u66C7\u5F8C\u6642\u3005\u96EA",
"CLOUDY, FREQUENT SNOW FLURRIES LATER",
],
217: [
"215.svg",
"215.svg",
"400",
"\u66C7\u5F8C\u96EA",
"CLOUDY, SNOW LATER",
],
218: [
"212.svg",
"212.svg",
"300",
"\u66C7\u5F8C\u96E8\u304B\u96EA",
"CLOUDY, RAIN OR SNOW LATER",
],
219: [
"212.svg",
"212.svg",
"300",
"\u66C7\u5F8C\u96E8\u304B\u96F7\u96E8",
"CLOUDY, RAIN AND/OR THUNDER LATER",
],
220: [
"202.svg",
"202.svg",
"300",
"\u66C7\u671D\u5915\u4E00\u6642\u96E8",
"OCCASIONAL SCCATERED SHOWERS IN THE MORNING AND EVENING, CLOUDY DURING THE DAY",
],
221: [
"202.svg",
"202.svg",
"300",
"\u66C7\u671D\u306E\u5185\u4E00\u6642\u96E8",
"CLOUDY OCCASIONAL SCCATERED SHOWERS IN THE MORNING",
],
222: [
"212.svg",
"212.svg",
"300",
"\u66C7\u5915\u65B9\u4E00\u6642\u96E8",
"CLOUDY, OCCASIONAL SCCATERED SHOWERS IN THE EVENING",
],
223: [
"201.svg",
"601.svg",
"200",
"\u66C7\u65E5\u4E2D\u6642\u3005\u6674",
"CLOUDY IN THE MORNING AND EVENING, PARTLY CLOUDY DURING THE DAY,",
],
224: [
"212.svg",
"212.svg",
"300",
"\u66C7\u663C\u9803\u304B\u3089\u96E8",
"CLOUDY, RAIN IN THE AFTERNOON",
],
225: [
"212.svg",
"212.svg",
"300",
"\u66C7\u5915\u65B9\u304B\u3089\u96E8",
"CLOUDY, RAIN IN THE EVENING",
],
226: [
"212.svg",
"212.svg",
"300",
"\u66C7\u591C\u306F\u96E8",
"CLOUDY, RAIN IN THE NIGHT",
],
228: [
"215.svg",
"215.svg",
"400",
"\u66C7\u663C\u9803\u304B\u3089\u96EA",
"CLOUDY, SNOW IN THE AFTERNOON",
],
229: [
"215.svg",
"215.svg",
"400",
"\u66C7\u5915\u65B9\u304B\u3089\u96EA",
"CLOUDY, SNOW IN THE EVENING",
],
230: [
"215.svg",
"215.svg",
"400",
"\u66C7\u591C\u306F\u96EA",
"CLOUDY, SNOW IN THE NIGHT",
],
231: [
"200.svg",
"200.svg",
"200",
"\u66C7\u6D77\u4E0A\u6D77\u5CB8\u306F\u9727\u304B\u9727\u96E8",
"CLOUDY, FOG OR DRIZZLING ON THE SEA AND NEAR SEASHORE",
],
240: [
"202.svg",
"202.svg",
"300",
"\u66C7\u6642\u3005\u96E8\u3067\u96F7\u3092\u4F34\u3046",
"CLOUDY, FREQUENT SCCATERED SHOWERS AND THUNDER",
],
250: [
"204.svg",
"204.svg",
"400",
"\u66C7\u6642\u3005\u96EA\u3067\u96F7\u3092\u4F34\u3046",
"CLOUDY, FREQUENT SNOW AND THUNDER",
],
260: [
"204.svg",
"204.svg",
"400",
"\u66C7\u4E00\u6642\u96EA\u304B\u96E8",
"CLOUDY, SNOW FLURRIES OR OCCASIONAL SCATTERED SHOWERS",
],
270: [
"204.svg",
"204.svg",
"400",
"\u66C7\u6642\u3005\u96EA\u304B\u96E8",
"CLOUDY, FREQUENT SNOW FLURRIES OR SCATTERED SHOWERS",
],
281: [
"215.svg",
"215.svg",
"400",
"\u66C7\u5F8C\u96EA\u304B\u96E8",
"CLOUDY, SNOW OR RAIN LATER",
],
300: ["300.svg", "300.svg", "300", "\u96E8", "RAIN"],
301: [
"301.svg",
"701.svg",
"300",
"\u96E8\u6642\u3005\u6674",
"RAIN, PARTLY CLOUDY",
],
302: [
"302.svg",
"302.svg",
"300",
"\u96E8\u6642\u3005\u6B62\u3080",
"SHOWERS THROUGHOUT THE DAY",
],
303: [
"303.svg",
"303.svg",
"400",
"\u96E8\u6642\u3005\u96EA",
"RAIN,FREQUENT SNOW FLURRIES",
],
304: ["300.svg", "300.svg", "300", "\u96E8\u304B\u96EA", "RAINORSNOW"],
306: ["300.svg", "300.svg", "300", "\u5927\u96E8", "HEAVYRAIN"],
308: [
"308.svg",
"308.svg",
"300",
"\u96E8\u3067\u66B4\u98A8\u3092\u4F34\u3046",
"RAINSTORM",
],
309: [
"303.svg",
"303.svg",
"400",
"\u96E8\u4E00\u6642\u96EA",
"RAIN,OCCASIONAL SNOW",
],
311: [
"311.svg",
"711.svg",
"300",
"\u96E8\u5F8C\u6674",
"RAIN,CLEAR LATER",
],
313: [
"313.svg",
"313.svg",
"300",
"\u96E8\u5F8C\u66C7",
"RAIN,CLOUDY LATER",
],
314: [
"314.svg",
"314.svg",
"400",
"\u96E8\u5F8C\u6642\u3005\u96EA",
"RAIN, FREQUENT SNOW FLURRIES LATER",
],
315: ["314.svg", "314.svg", "400", "\u96E8\u5F8C\u96EA", "RAIN,SNOW LATER"],
316: [
"311.svg",
"711.svg",
"300",
"\u96E8\u304B\u96EA\u5F8C\u6674",
"RAIN OR SNOW, CLEAR LATER",
],
317: [
"313.svg",
"313.svg",
"300",
"\u96E8\u304B\u96EA\u5F8C\u66C7",
"RAIN OR SNOW, CLOUDY LATER",
],
320: [
"311.svg",
"711.svg",
"300",
"\u671D\u306E\u5185\u96E8\u5F8C\u6674",
"RAIN IN THE MORNING, CLEAR LATER",
],
321: [
"313.svg",
"313.svg",
"300",
"\u671D\u306E\u5185\u96E8\u5F8C\u66C7",
"RAIN IN THE MORNING, CLOUDY LATER",
],
322: [
"303.svg",
"303.svg",
"400",
"\u96E8\u671D\u6669\u4E00\u6642\u96EA",
"OCCASIONAL SNOW IN THE MORNING AND EVENING, RAIN DURING THE DAY",
],
323: [
"311.svg",
"711.svg",
"300",
"\u96E8\u663C\u9803\u304B\u3089\u6674",
"RAIN, CLEAR IN THE AFTERNOON",
],
324: [
"311.svg",
"711.svg",
"300",
"\u96E8\u5915\u65B9\u304B\u3089\u6674",
"RAIN, CLEAR IN THE EVENING",
],
325: [
"311.svg",
"711.svg",
"300",
"\u96E8\u591C\u306F\u6674",
"RAIN, CLEAR IN THE NIGHT",
],
326: [
"314.svg",
"314.svg",
"400",
"\u96E8\u5915\u65B9\u304B\u3089\u96EA",
"RAIN, SNOW IN THE EVENING",
],
327: [
"314.svg",
"314.svg",
"400",
"\u96E8\u591C\u306F\u96EA",
"RAIN,SNOW IN THE NIGHT",
],
328: [
"300.svg",
"300.svg",
"300",
"\u96E8\u4E00\u6642\u5F37\u304F\u964D\u308B",
"RAIN, EXPECT OCCASIONAL HEAVY RAINFALL",
],
329: [
"300.svg",
"300.svg",
"300",
"\u96E8\u4E00\u6642\u307F\u305E\u308C",
"RAIN, OCCASIONAL SLEET",
],
340: ["400.svg", "400.svg", "400", "\u96EA\u304B\u96E8", "SNOWORRAIN"],
350: [
"300.svg",
"300.svg",
"300",
"\u96E8\u3067\u96F7\u3092\u4F34\u3046",
"RAIN AND THUNDER",
],
361: [
"411.svg",
"811.svg",
"400",
"\u96EA\u304B\u96E8\u5F8C\u6674",
"SNOW OR RAIN, CLEAR LATER",
],
371: [
"413.svg",
"413.svg",
"400",
"\u96EA\u304B\u96E8\u5F8C\u66C7",
"SNOW OR RAIN, CLOUDY LATER",
],
400: ["400.svg", "400.svg", "400", "\u96EA", "SNOW"],
401: [
"401.svg",
"801.svg",
"400",
"\u96EA\u6642\u3005\u6674",
"SNOW, FREQUENT CLEAR",
],
402: [
"402.svg",
"402.svg",
"400",
"\u96EA\u6642\u3005\u6B62\u3080",
"SNOWTHROUGHOUT THE DAY",
],
403: [
"403.svg",
"403.svg",
"400",
"\u96EA\u6642\u3005\u96E8",
"SNOW,FREQUENT SCCATERED SHOWERS",
],
405: ["400.svg", "400.svg", "400", "\u5927\u96EA", "HEAVYSNOW"],
406: ["406.svg", "406.svg", "400", "\u98A8\u96EA\u5F37\u3044", "SNOWSTORM"],
407: ["406.svg", "406.svg", "400", "\u66B4\u98A8\u96EA", "HEAVYSNOWSTORM"],
409: [
"403.svg",
"403.svg",
"400",
"\u96EA\u4E00\u6642\u96E8",
"SNOW, OCCASIONAL SCCATERED SHOWERS",
],
411: [
"411.svg",
"811.svg",
"400",
"\u96EA\u5F8C\u6674",
"SNOW,CLEAR LATER",
],
413: [
"413.svg",
"413.svg",
"400",
"\u96EA\u5F8C\u66C7",
"SNOW,CLOUDY LATER",
],
414: ["414.svg", "414.svg", "400", "\u96EA\u5F8C\u96E8", "SNOW,RAIN LATER"],
420: [
"411.svg",
"811.svg",
"400",
"\u671D\u306E\u5185\u96EA\u5F8C\u6674",
"SNOW IN THE MORNING, CLEAR LATER",
],
421: [
"413.svg",
"413.svg",
"400",
"\u671D\u306E\u5185\u96EA\u5F8C\u66C7",
"SNOW IN THE MORNING, CLOUDY LATER",
],
422: [
"414.svg",
"414.svg",
"400",
"\u96EA\u663C\u9803\u304B\u3089\u96E8",
"SNOW, RAIN IN THE AFTERNOON",
],
423: [
"414.svg",
"414.svg",
"400",
"\u96EA\u5915\u65B9\u304B\u3089\u96E8",
"SNOW, RAIN IN THE EVENING",
],
425: [
"400.svg",
"400.svg",
"400",
"\u96EA\u4E00\u6642\u5F37\u304F\u964D\u308B",
"SNOW, EXPECT OCCASIONAL HEAVY SNOWFALL",
],
426: [
"400.svg",
"400.svg",
"400",
"\u96EA\u5F8C\u307F\u305E\u308C",
"SNOW, SLEET LATER",
],
427: [
"400.svg",
"400.svg",
"400",
"\u96EA\u4E00\u6642\u307F\u305E\u308C",
"SNOW, OCCASIONAL SLEET",
],
450: [
"400.svg",
"400.svg",
"400",
"\u96EA\u3067\u96F7\u3092\u4F34\u3046",
"SNOW AND THUNDER",
],
}
# Download svg
url_base = "https://www.jma.go.jp/bosai/forecast/img/"
download_dir = os.path.expanduser("~/Downloads/images/")
os.makedirs(download_dir, exist_ok=True)
for code in data.keys():
svg_name = f"{code}.svg"
url = f"{url_base}{svg_name}"
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(os.path.join(download_dir, svg_name), "wb") as f:
response.raw.decode_content = True
shutil.copyfileobj(response.raw, f)
print(f"Downloaded {url}")
else:
print(f"Error downloading {url}")
# Clear
JmaWeatherCode.objects.all().delete()
weather_code_list = [
JmaConstWeatherCode(key, *values) for key, values in data.items()
]
jma_weather_code_list = [
JmaWeatherCode(
code=x.code,
image=x.image_day,
summary_code=x.summary_code,
name=x.name,
name_en=x.name_en,
)
for x in weather_code_list
]
JmaWeatherCode.objects.bulk_create(jma_weather_code_list)
self.stdout.write(
self.style.SUCCESS(
"Successfully imported all jma weather code master from manual data."
)
)
jma_weather> python manage.py import_weather_master_manual
Successfully imported all jma weather code master from manual data.
create import_weather_master.py
import sys
import requests
from django.core.management.base import BaseCommand
from weather.domain.valueobject.weather.jma import JmaConstGeographicArea
from weather.models import (
JmaArea,
JmaRegion,
JmaPrefecture,
JmaCity,
JmaAmedas,
)
def get_data_from_url(url: str):
try:
# Obtain the response from the URL
response = requests.get(url)
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(
f"データの取得でエラーが発生しました。URL: {url} エラー詳細: {e}",
file=sys.stderr,
)
sys.exit(1)
# Parse the JSON response into a Python dictionary
return response.json()
class Command(BaseCommand):
help = "Import jma const master from data."
def handle(self, *args, **options):
"""
Args:
*args: Additional positional arguments (unused).
**options: Additional keyword arguments (unused).
"""
# Part1: from const
url = "https://www.jma.go.jp/bosai/common/const/area.json"
raw_data = get_data_from_url(url)
# Create and save JmaArea: 010600 近畿地方
JmaArea.objects.all().delete()
jma_area_list = [
JmaConstGeographicArea(
code=code,
name=data["name"],
children=data.get("children", []),
parent=data.get("parent"),
)
for code, data in raw_data["centers"].items()
]
JmaArea.objects.bulk_create(
[JmaArea(code=vo.code, name=vo.name) for vo in jma_area_list]
)
jma_area_cache = {obj.code: obj for obj in JmaArea.objects.all()}
# Create and save JmaPrefecture: 280000 兵庫県
JmaPrefecture.objects.all().delete()
jma_prefecture_list = [
JmaConstGeographicArea(
code=code,
name=data["name"],
children=data.get("children", []),
parent=data.get("parent"),
)
for code, data in raw_data["offices"].items()
]
JmaPrefecture.objects.bulk_create(
[
JmaPrefecture(
code=vo.code, name=vo.name, jma_area=jma_area_cache.get(vo.parent)
)
for vo in jma_prefecture_list
]
)
jma_prefecture_cache = {obj.code: obj for obj in JmaPrefecture.objects.all()}
# Create and save JmaRegion: 280010 南部
JmaRegion.objects.all().delete()
jma_region_list = [
JmaConstGeographicArea(
code=code,
name=data["name"],
children=data.get("children", []),
parent=data.get("parent"),
)
for code, data in raw_data["class10s"].items()
]
JmaRegion.objects.bulk_create(
[
JmaRegion(
code=vo.code,
name=vo.name,
jma_prefecture=jma_prefecture_cache.get(vo.parent),
)
for vo in jma_region_list
]
)
jma_region_cache = {obj.code: obj for obj in JmaRegion.objects.all()}
# Create JmaCityGroup
jma_city_group_list = [
JmaConstGeographicArea(
code=code,
name=data["name"],
children=data.get("children", []),
parent=data.get("parent"),
)
for code, data in raw_data["class15s"].items()
]
jma_city_group_cache = {
jma_city_group.code: jma_city_group
for jma_city_group in jma_city_group_list
}
# Create and save JmaCity with parents via JmaCityGroup: 2820100 姫路市
JmaCity.objects.all().delete()
jma_city_list = [
JmaConstGeographicArea(
code=code,
name=data["name"],
children=data.get("children", []),
parent=data.get("parent"),
)
for code, data in raw_data["class20s"].items()
]
JmaCity.objects.bulk_create(
[
JmaCity(
code=vo.code,
name=vo.name,
jma_region=jma_region_cache.get(
jma_city_group_cache.get(vo.parent).parent
),
)
for vo in jma_city_list
]
)
# Part2: from forecast_area.json
url = "https://www.jma.go.jp/bosai/forecast/const/forecast_area.json"
raw_data = get_data_from_url(url)
JmaAmedas.objects.all().delete()
jma_amedas_list = []
for prefecture_code, entries in raw_data.items():
for entry in entries:
region_code = entry["class10"]
for amedas_code in entry["amedas"]:
jma_amedas_list.append(
JmaAmedas(
code=amedas_code,
jma_region=jma_region_cache.get(region_code),
)
)
JmaAmedas.objects.bulk_create(jma_amedas_list)
self.stdout.write(
self.style.SUCCESS("jma const master data import has been completed.")
)
jma_weather> python manage.py import_weather_master
jma const master data import has been completed.
予報と警報を保存する仕組みを作ろう
警報の名称
複数の日の天気情報が混ざってる
これを取り出すの頭使ったな...データ構造としてひねくれとる
regionの気温はアメダスの平均
※(鬼畜の仕様)十勝地方と奄美地方は天気の取得が失敗する!
たまたま台風の注意報を確認したくて奄美地方の建物作って天気を取得してみたら処理が事故った。調査してみると代理のprefectureに天気予報が収録されている。area, prefecture, region, city の関係をしっかりと捉えていたから事故ったときにピンときてソースを見るというアタリがついた。こ、これがドキュメント化されていないというのは鬼畜の所業だぜ
(天気予報のページからソースを開いて...)
create fetch_weather_forecast.py
from datetime import datetime, date
import requests
from django.core.management import BaseCommand
from weather.domain.repository.weather.jma import JmaRepository
from weather.domain.valueobject.weather.jma import (
WindData,
MeanCalculable,
Region,
SummaryText,
RainData,
TemperatureData,
)
from weather.models import (
JmaWeather,
JmaRegion,
JmaWeatherCode,
)
THREE_DAYS = 0
TYPE_OVERVIEW = 0
TYPE_RAIN = 1
TYPE_TEMPERATURE = 2
TYPE_WIND = 1
WIND_SPEED = 3
LAND = 0
def update_prefecture_ids(prefecture_ids: list[str]) -> tuple[list[str], dict]:
"""
気象庁特別ルール
"014030" があって "014100" がないときに "014030" → "014100" に置換 / 十勝地方 → 釧路・根室地方
"460040" があって "460100" がないときに "460040" → "460100" に置換 / 奄美地方 → 鹿児島県(奄美地方除く)
Args:
prefecture_ids (list[str]): The list of JMA prefecture ids to be updated.
Returns:
tuple[list[str], dict]: JMA prefecture ids and special add region ids.
Example:
prefecture_ids = ["014030", "460040"]
updated_ids, special_add_region_ids = update_prefecture_ids(prefecture_ids)
print(updated_ids) # Output: ["014100", "460100"]
print(special_add_region_ids) # Output: {"014100": "014030", "460100": "460040"}
"""
pairs_to_check = [("014030", "014100"), ("460040", "460100")]
special_add_region_ids = {}
for invalid_id, proxy_id in pairs_to_check:
if invalid_id in prefecture_ids:
if proxy_id not in prefecture_ids:
prefecture_ids.append(proxy_id)
prefecture_ids.remove(invalid_id)
special_add_region_ids[proxy_id] = invalid_id
return prefecture_ids, special_add_region_ids
def get_data(url: str):
response = requests.get(url)
response.raise_for_status()
data = response.json()
return data[THREE_DAYS]["timeSeries"]
def get_indexes(data_time_defines, desired_date: date) -> list[int]:
"""
get_indexes する箇所が複数あるので独立さした
Args:
data_time_defines: ["2024-08-31", "2024-09-01", "2024-09-01"]
desired_date: "2024-09-01"
Returns: [1, 2]
"""
return [
i
for i, date_str in enumerate(data_time_defines)
if datetime.fromisoformat(date_str).date() == desired_date
]
class Command(BaseCommand):
help = "get weather forecast"
def handle(self, *args, **options):
jma_weather_list: list[JmaWeather] = []
# TODO: 例えば建物の jma_prefecture_ids をdbから取得(重複を削って)
jma_prefecture_ids = ["280000", "050000", "130000", "014030", "460040"]
jma_prefecture_ids, special_add_region_ids = update_prefecture_ids(
jma_prefecture_ids
)
if not jma_prefecture_ids:
raise Exception("facility is empty")
region_master = {x.code: x for x in JmaRegion.objects.all()}
weather_code_master = {x.code: x for x in JmaWeatherCode.objects.all()}
# 日付のリストだけ取得
url = f"https://www.jma.go.jp/bosai/forecast/data/forecast/{jma_prefecture_ids[0]}.json"
time_series_data = get_data(url)
target_date_list = [
datetime.fromisoformat(date_str).date()
for date_str in time_series_data[TYPE_OVERVIEW]["timeDefines"]
]
JmaWeather.objects.all().delete()
for target_date in target_date_list:
for prefecture_id in jma_prefecture_ids:
print(f"{target_date} の {prefecture_id=}")
forecasts_by_region = {}
print(" 風速:")
url = f"https://www.jma.go.jp/bosai/probability/data/probability/{prefecture_id}.json"
time_series_data = get_data(url)
indexes = get_indexes(
data_time_defines=time_series_data[TYPE_WIND]["timeDefines"],
desired_date=target_date,
)
for region_data in time_series_data[TYPE_WIND]["areas"]:
region_code = region_data["code"]
forecasts_by_region.setdefault(region_code, {})
time_cells_wind_data = region_data["properties"][WIND_SPEED][
"timeCells"
]
wind_data = WindData(
values=MeanCalculable(
[
int(time_cell["locals"][LAND]["value"])
for i, time_cell in enumerate(time_cells_wind_data)
if i in indexes
]
)
)
forecasts_by_region[region_code]["wind_data"] = wind_data
print(f" {region_code} の {wind_data}")
print(" 天気サマリ:")
url = f"https://www.jma.go.jp/bosai/forecast/data/forecast/{prefecture_id}.json"
time_series_data = get_data(url)
indexes = get_indexes(
data_time_defines=time_series_data[TYPE_OVERVIEW]["timeDefines"],
desired_date=target_date,
)
if len(indexes) != 1:
print(f" 要素数は必ず1になります{len(indexes)}")
continue
index = indexes.pop()
for region_data in time_series_data[TYPE_OVERVIEW]["areas"]:
region = Region(
code=region_data["area"]["code"],
name=region_data["area"]["name"],
)
forecasts_by_region.setdefault(region.code, {})
# weather_code
forecasts_by_region[region.code]["weather_code"] = region_data[
"weatherCodes"
][index]
# summary_text を3種
summary_text = SummaryText(
weather=(
region_data["weathers"][index]
if "weathers" in region_data
and index < len(region_data["weathers"])
else "なし"
),
wind=(
region_data["winds"][index]
if "winds" in region_data
and index < len(region_data["winds"])
else "なし"
),
wave=(
region_data["waves"][index]
if "waves" in region_data
and index < len(region_data["waves"])
else "なし"
),
)
forecasts_by_region[region.code]["summary_text"] = summary_text
summary_text_merge = f"{summary_text.weather[:4]}|{summary_text.wind[:4]}|{summary_text.wave[:4]}|"
print(
f" {region.code} は |{region_data['weatherCodes'][index]}|{summary_text_merge}"
)
print(" 降水確率:")
indexes = get_indexes(
data_time_defines=time_series_data[TYPE_RAIN]["timeDefines"],
desired_date=target_date,
)
for region_data in time_series_data[TYPE_RAIN]["areas"]:
region = Region(
code=region_data["area"]["code"],
name=region_data["area"]["name"],
)
forecasts_by_region.setdefault(region.code, {})
rain_data = RainData(
values=MeanCalculable(
[
int(time_cell)
for i, time_cell in enumerate(region_data["pops"])
if i in indexes
]
)
)
forecasts_by_region[region.code]["rain_data"] = rain_data
print(
f" {region.code} の {rain_data.values.mean} {rain_data.unit}"
)
print(" 気温:")
amedas_code_in_region = JmaRepository.get_amedas_code_list(
prefecture_id, special_add_region_ids
)
indexes = get_indexes(
data_time_defines=time_series_data[TYPE_TEMPERATURE]["timeDefines"],
desired_date=target_date,
)
for region_data in time_series_data[TYPE_OVERVIEW]["areas"]:
region = Region(
code=region_data["area"]["code"],
name=region_data["area"]["name"],
)
forecasts_by_region.setdefault(region.code, {})
amedas_min_temps: list[float] = []
amedas_max_temps: list[float] = []
if indexes:
min_index, max_index = indexes
for amedas_data in time_series_data[TYPE_TEMPERATURE]["areas"]:
amedas_code = amedas_data["area"]["code"]
if amedas_code not in amedas_code_in_region.get(
region.code
):
continue
amedas_min_temps.append(
float(amedas_data["temps"][min_index])
)
amedas_max_temps.append(
float(amedas_data["temps"][max_index])
)
temperature_data = TemperatureData(
min_values=MeanCalculable(amedas_min_temps),
max_values=MeanCalculable(amedas_max_temps),
)
forecasts_by_region[region.code][
"temperature_data"
] = temperature_data
msg1 = f" {region.code} の最低気温 {temperature_data.min_values.mean} {temperature_data.unit}"
msg2 = f"最高気温 {temperature_data.max_values.mean} {temperature_data.unit}"
print(f"{msg1} / {msg2}")
# 天気・風・波・降水確率・気温 を1日分としてガッチャンコ
for region_data in time_series_data[TYPE_OVERVIEW]["areas"]:
region = Region(
code=region_data["area"]["code"],
name=region_data["area"]["name"],
)
weather_code = forecasts_by_region[region.code]["weather_code"]
summary_text = forecasts_by_region[region.code]["summary_text"]
rain_data = forecasts_by_region[region.code]["rain_data"]
temperature_data = forecasts_by_region[region.code][
"temperature_data"
]
wind_data = forecasts_by_region[region.code]["wind_data"]
jma_weather_list.append(
JmaWeather(
jma_region=region_master.get(region.code),
reporting_date=target_date,
jma_weather_code=weather_code_master.get(weather_code),
weather_text=summary_text.weather,
wind_text=summary_text.wind,
wave_text=summary_text.wave,
avg_rain_probability=(
None
if len(rain_data.values.raw) == 0
else rain_data.values.mean
),
avg_min_temperature=(
None
if len(temperature_data.min_values.raw) == 0
else temperature_data.min_values.mean
),
avg_max_temperature=(
None
if len(temperature_data.max_values.raw) == 0
else temperature_data.max_values.mean
),
avg_max_wind_speed=(
None
if len(wind_data.values.raw) == 0
else wind_data.values.mean
),
)
)
JmaWeather.objects.bulk_create(jma_weather_list)
self.stdout.write(
self.style.SUCCESS("weather forecast data retrieve has been completed.")
)
dev\portfolio> python manage.py fetch_weather_forecast
2024-09-01 の prefecture_id='280000'
風速:
280010 の 最大風速(時間帯平均)は 9.0 以下
280020 の 最大風速(時間帯平均)は 9.0 以下
天気サマリ:
280010 は |200|くもり |北西の風|1メート|
280020 は |200|くもり |北の風 |1.5メ|
降水確率:
280010 の 40.0 %
280020 の 40.0 %
気温:
280010 の最低気温 0 ℃ / 最高気温 0 ℃
280020 の最低気温 0 ℃ / 最高気温 0 ℃
2024-09-02 の prefecture_id='280000'
風速:
280010 の 最大風速(時間帯平均)は 9.0 以下
280020 の 最大風速(時間帯平均)は 9.0 以下
天気サマリ:
280010 は |313|雨 昼前|南西の風|1メート|
280020 は |203|くもり |南の風 |1メート|
降水確率:
280010 の 40.0 %
280020 の 50.0 %
気温:
280010 の最低気温 24.3 ℃ / 最高気温 33.0 ℃
280020 の最低気温 24.0 ℃ / 最高気温 33.0 ℃
2024-09-03 の prefecture_id='280000'
風速:
280010 の 最大風速(時間帯平均)は 0 以下
280020 の 最大風速(時間帯平均)は 0 以下
天気サマリ:
280010 は |201|くもり |北の風 |0.5メ|
280020 は |201|くもり |北の風|1メート|
降水確率:
280010 の 0 %
280020 の 0 %
気温:
280010 の最低気温 0 ℃ / 最高気温 0 ℃
280020 の最低気温 0 ℃ / 最高気温 0 ℃
create fetch_weather_warning.py
import requests
from django.core.management import BaseCommand
from weather.domain.valueobject.weather.jma import WarningData
from weather.models import JmaWarning, JmaRegion
LATEST_WARNING = 0
M_TARGET_WARNINGS = {
"02": "暴風雪警報",
"03": "大雨警報",
"04": "洪水警報",
"05": "暴風警報",
"06": "大雪警報",
"07": "波浪警報",
"08": "高潮警報",
"10": "大雨注意報",
"12": "大雪注意報",
"13": "風雪注意報",
"14": "雷注意報",
"15": "強風注意報",
"16": "波浪注意報",
"17": "融雪注意報",
"18": "洪水注意報",
"19": "高潮注意報",
"20": "濃霧注意報",
"21": "乾燥注意報",
"22": "なだれ注意報",
"23": "低温注意報",
"24": "霜注意報",
"25": "着氷注意報",
"26": "着雪注意報",
"27": "その他の注意報",
"32": "暴風雪特別警報",
"33": "大雨特別警報",
"35": "暴風特別警報",
"36": "大雪特別警報",
"37": "波浪特別警報",
"38": "高潮特別警報",
}
def get_data(url: str):
response = requests.get(url)
response.raise_for_status()
return response.json()
class Command(BaseCommand):
help = "get weather warning"
def handle(self, *args, **options):
# TODO: facilityテーブルから prefecture_ids を取得
jma_prefecture_ids = ["280000", "050000", "130000", "014030", "460040"]
if not jma_prefecture_ids:
raise Exception("facility is empty")
region_master = {x.code: x for x in JmaRegion.objects.all()}
JmaWarning.objects.all().delete()
jma_warning_list: list[JmaWarning] = []
for prefecture_id in jma_prefecture_ids:
url = (
f"https://www.jma.go.jp/bosai/warning/data/warning/{prefecture_id}.json"
)
latest_warning_data = get_data(url)["areaTypes"][LATEST_WARNING]
for region_data in latest_warning_data["areas"]:
region_code = region_data["code"]
warning_data_list = [
WarningData(code=x["code"], status=x["status"])
for x in region_data["warnings"]
if "code" in x
]
if not warning_data_list:
continue
warnings: list[str] = list(
set(
[
M_TARGET_WARNINGS[warning_data.code]
for warning_data in warning_data_list
]
)
)
jma_warning_list.append(
JmaWarning(
jma_region=region_master.get(region_code),
warnings=",".join(warnings),
)
)
JmaWarning.objects.bulk_create(jma_warning_list)
self.stdout.write(
self.style.SUCCESS("weather warning data retrieve has been completed.")
)
jma_weather> python manage.py fetch_weather_warning
280000/280010 の 雷注意報
280000/280020 の 雷注意報, 高潮注意報
050000/050010 の 雷注意報
050000/050020 の 雷注意報
130000/130010 の 大雨警報, 雷注意報
130000/130020 の 雷注意報, 波浪注意報
130000/130030 の 雷注意報, 波浪注意報
weather warning data retrieve has been completed.
さいごに
まぁリージョンごとに予報データをしまうことができたのであとはタテモノと紐づけるなりしてよりよいものを作ることはできるだろう。予報と警報はひとつのバッチでもいいかもしれない。仕事では、天気は日次バッチ、警報はリアルタイムという要件だったので割れただけだ
Next Action