本記事の背景
文系の新卒1年目です。
お天気botの修正をしたので、その記事を書きます。
行ったこと
・gitでブランチを切ってプルリクエストを送る。
・pythonで他の記事を参考にしながらコードを修正する。
・気象庁からjsonを取得し、取得したjsonを整形するまで
・書いたコードを動かして点検する。
目的
調べた部分や詰まった部分を概略としてまとめました。
似たような作業をする際の疑問点を解消できるよう、一つの例として本記事を作成しました。
経緯
会社のslackで、お天気Botなるものが動いていたらしく、Heroku有料化に伴い止まってしまったのでGithub Actionで動かすように改修するそうです。
これにあたり、下記の内容を作成してほしい、とのことでした。
・Pythonを実行すればSlackにメッセージが飛ぶ、という部分を作成する
・従来は海外のサービスで天気を取得していたが、気象庁から天気を取得するように修正する
語句の整理
Heroku、Github Action、Workflowがわからなかったので、調べてみました。
Heroku
Heroku はコンテナベースのクラウド型 PaaS(サービスとしてのプラットフォーム)です。多くの開発者がこのプラットフォームを使って最新鋭のアプリをデプロイ、管理、スケールしています。使いやすいだけでなく、効率性と柔軟性にも優れているため、非常に簡単にアプリを市場に投入できます。
クラウドでアプリを実行したり、運用したりしてくれている、ということを理解しました。
Github Actions
GitHub Actionsを使用すると、ワールドクラスのCI / CDですべてのソフトウェアワークフローを簡単に自動化できます。 GitHubから直接コードをビルド、テスト、デプロイでき、コードレビュー、ブランチ管理、問題のトリアージを希望どおりに機能させます。
まず文中のCI/CD、これはこれでよくわからない単語でした。
「継続的インテグレーション/継続的デリバリー」を意味するとのことです。
インテグレーションはいろいろな人の書いたコードを統合する、デリバリーはサービスをユーザーに提供しますよという意味合いと理解しました。
つまりGithub ActionsはHerokuと似たようなものであるということがわかりました。ここまで調べたところで、同種のものだから移行の候補になりうるということに気が付きました。
Workflow
Github ActionsにWorkflowというものがあるようです。
ワークフローは、1 つ以上のジョブを実行する構成可能な自動化プロセスです。 ワークフローは、リポジトリにチェックインされる YAML ファイルによって定義され、リポジトリ内のイベントによってトリガーされたときに実行されます。また、手動でトリガーしたり、定義されたスケジュールでトリガーしたりすることもできます。
ぽつぽつと理解しきれない語句がありました。
構成可能な自動化プロセスの「構成可能な」
リポジトリにチェックインもわからないですが、これは知識不足のような感じがします。
イベントにトリガーされて実行されるらしいので、AWSでいうlambdaをやってくれる、というような理解をしました。
git
研修以来、gitを使っていなかったため、あまり覚えていませんでした。
cloneする、branchを切る、コードを書いてからcommitしてプルリクエストを送る、という流れをぼんやりと思い出しました。
Configのuser.nameとuser.emailの意味
git bashを起動してから以下のコマンドを打っていましたが、ユーザー名とメールアドレスを何に使うのかを理解していませんでした。
git config --global user.name
git config --global user.email
その後 git clone したところ、認証のエラーが出ました。
fatal: Authentication failed for 'https://github.com/~~~~~~'
user.nameとuser.emailをそれぞれgithubアカウントのものに直してgit cloneを打つとアカウントの認証画面に飛び、認証を済ませて無事cloneができました。
この後ブランチを切ったのですが、クローンした後にクローンしたファイルのディレクトリに入っていなかったので、ブランチが切れない、というミスがありました。
天気アプリの改修
1. コードを見る
コードは主に下記から構成されていました。
・slackにメッセージを飛ばすslack.py
・apiで転機を取得するweather.py
weather.pyで取得したデータをもとにslack.pyでメッセージに整形してslackに送っています。
slackへのメッセージ送信はすでにコードがあるので、送信するメッセージを作るところまでをやっていきます。
2. jsonの取得
気象庁のapiがどうのという話があったので気象庁からデータを取るところから始めてみます。
下記のサイトを参照しました。
下記URLでjson形式の天気情報をとれるらしいです。
ttps://www.jma.go.jp/bosai/forecast/data/forecast/130000.json
太字部分は地区コード(130000)のため気象情報が欲しい地域のコードに適宜変えます。
東京が130000、千葉は120000です。会社があるこの2地点の天気を取っていきます。
URLがわかったのでデータを受け取る方法は下記のコードを参考に作成しました。
import requests, json
url = requests.get('https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json')
data = json.loads(url.text)
元のコードでは取得したjsonをそのまま返却していましたが、データの取得先が変わったことで取得できる情報も変わっているということもあり、天気取得の段階である程度整形するようにします。
3.jsonの編集
jsonを取得することができたので、必要な情報を抜き出して使いやすい形に加工した後、メッセージを送る関数に渡します。
気象庁から取得したjsonは下記のような構造でした。
伊豆諸島の天気情報も本当はありましたが、今回は使わないのと、なくても構造は理解できるので省略します。
[
{
"publishingOffice": "気象庁",
"reportDatetime": "2023-09-09T13:00:00+09:00",
"timeSeries": [
//天気概況
{
"timeDefines": [
"2023-09-09T11:00:00+09:00",
"2023-09-10T00:00:00+09:00",
"2023-09-11T00:00:00+09:00"
],
"areas": [
{
"area": {
"name": "東京地方",
"code": "130010"
},
"weatherCodes": [
"211",
"101",
"101"
],
"weathers": [
"くもり 夜 晴れ 多摩西部 では 夜のはじめ頃 まで 雨 で 雷を伴う",
"晴れ 時々 くもり",
"晴れ 時々 くもり"
],
"winds": [
"南の風 23区西部 では 南の風 やや強く",
"南の風 23区西部 では 南の風 やや強く",
"南の風"
],
"waves": [
"1.5メートル",
"0.5メートル 後 1メートル",
"0.5メートル"
]
}
]
},
//降水確率(今日明日)
{
"timeDefines": [
"2023-09-09T12:00:00+09:00",
"2023-09-09T18:00:00+09:00",
"2023-09-10T00:00:00+09:00",
"2023-09-10T06:00:00+09:00",
"2023-09-10T12:00:00+09:00",
"2023-09-10T18:00:00+09:00"
],
"areas": [
{
"area": {
"name": "東京地方",
"code": "130010"
},
"pops": [
"20",
"20",
"10",
"10",
"10",
"10"
]
}
]
},
//気温(今日明日)
{
"timeDefines": [
"2023-09-09T09:00:00+09:00",
"2023-09-09T00:00:00+09:00",
"2023-09-10T00:00:00+09:00",
"2023-09-10T09:00:00+09:00"
],
"areas": [
{
"area": {
"name": "東京",
"code": "44132"
},
"temps": [
"30",
"30",
"25",
"33"
]
}
]
}
]
},
//多分週間データ
{
"publishingOffice": "気象庁",
"reportDatetime": "2023-09-09T11:00:00+09:00",
"timeSeries": [
//週間天気(天気?/降水確率/信頼性?)
{
"timeDefines": [
"2023-09-10T00:00:00+09:00",
"2023-09-11T00:00:00+09:00",
"2023-09-12T00:00:00+09:00",
"2023-09-13T00:00:00+09:00",
"2023-09-14T00:00:00+09:00",
"2023-09-15T00:00:00+09:00",
"2023-09-16T00:00:00+09:00"
],
"areas": [
{
"area": {
"name": "東京地方",
"code": "130010"
},
"weatherCodes": [
"101",
"101",
"101",
"101",
"101",
"101",
"201"
],
"pops": [
"",
"30",
"20",
"20",
"20",
"20",
"30"
],
"reliabilities": [
"","","A","A","A","A","B"
]
}
]
},
//週間天気(最高気温、最低気温)
{
"timeDefines": [
"2023-09-10T00:00:00+09:00",
"2023-09-11T00:00:00+09:00",
"2023-09-12T00:00:00+09:00",
"2023-09-13T00:00:00+09:00",
"2023-09-14T00:00:00+09:00",
"2023-09-15T00:00:00+09:00",
"2023-09-16T00:00:00+09:00"
],
"areas": [
{
"area": {
"name": "東京",
"code": "44132"
},
"tempsMin": [
"","26","25","24","24","24","24"
],
"tempsMinUpper": [
"","27","26","25","25","25","25"
],
"tempsMinLower": [
"","24","23","22","23","23","23"
],
"tempsMax": [
"","33","34","34","34","33","33"
],
"tempsMaxUpper": [
"","35","36","36","35","35","35"
],
"tempsMaxLower": [
"","32","32","32","31","31","31"
]
}
]
}
],
//平均気温
"tempAverage": {
"areas": [
{
"area": {
"name": "東京",
"code": "44132"
},
"min": "20.9",
"max": "28.2"
}
]
},
//平均降水量?
"precipAverage": {
"areas": [
{
"area": {
"name": "東京",
"code": "44132"
},
"min": "16.6",
"max": "68.8"
}
]
}
}
]
このjsonは下記の天気予報の表示情報を表示しているらしいので、これを参照しながらデータの中身を確認していきます。
取得したjsonをリストに格納したときに、
data[0]:概況
data[0][timeSeries][0]:天気概況
data[0][timeSeries][1]:降水確率
data[0][timeSeries][2]:気温
data[1]:週間予報
data[1][timeSeries][0]:週間天気(天気?/降水確率/信頼性?)
でとれそうです。
週間予報はたぶんいらないので上だけ確認したところでメモをやめました。
取りたいデータの内容を洗い出します。
既存のbotの出力情報はこれです。
今日の
・天気
・最高気温
・最低気温
・湿度
・降水確率
3時間ごとの
・天気
・気温
・降水確率
この情報が気象庁から取得したjsonでとれるかを確認します。
今日の
・天気:○
・最高気温:○
・最低気温:○?
・湿度:×
・降水確率:○
3時間ごとの
・天気:×
・気温:×
・降水確率:×
最低気温は多くの場合「―」となっていました。取得している時間帯にはもう最低気温を記録する時間帯を過ぎている、という意味と解釈していますが、実際のところはわかりません。
取れていない情報がそこそこありました。とりあえず形にしてほしいとのことだったので、元のbotの出力情報は気にせずに、気象庁のjsonから取得できる情報で天気情報を作っていきます。
今回は下記の構造に取得したものを入れていくものとししました。
weather_data
├─ weather
│ ├─ main
│ └─ description
├─ temp
│ ├─ max
│ └─ min //該当なし
├─ humidity //該当なし
└─ pop
天気、最高気温、降水確率を取得することにします。
最低気温と湿度は枠だけ作っておきました。
weatherのmainとdescriptionにはそれぞれ天気コードと天気コードに対応する天気を入れます。
例えば100に対しては晴れ、など
これは天気コードと対応する天気を辞書にまとめて、関数でこれを取得できるようにしました。
{
"100": "晴れ",
"101": "晴れ時々くもり",
"102": "晴れ一時雨",
"103": "晴れ時々雨",
"104": "晴れ一時雪",
"105": "晴れ時々雪",
(後略)
ついでにSlackに送信する文章では天気に対応する絵文字をつけているので、その辞書も作っています。
こちらはslackでメッセージを送るときに呼び出します。
{
"100": "sunny",
"101": "partly_sunny",
"102": "partly_sunny_rain",
"103": "partly_sunny_rain",
"104": "partly_sunny_rain",
"105": "partly_sunny_rain",
(後略)
最終的なコードは下記のようになりました。
ひとつのjsonには各都道府県内の複数の地域の天気情報が入っているので、地域名の一致する天気情報を取得します。
area_name1、area_name2がそうです。地域名が2つ必要な理由については後述します。
weather_data = {
'daily': {
'weather': {
'main': '',
'description': ''
},
'temp': {
'max': '',
'min': ''
},
'pop': ''
}
}
# 天気を取得
for area in response[0]['timeSeries'][0]['areas']:
if area['area']['name'] == area_name1:
weather_data['daily']['weather']['main'] = area['weatherCodes'][0]
weather_data['daily']['weather']['description'] = code_to_weather(weather_data['daily']['weather']['main'])
# 日中の最高気温を取得
for area in response[0]['timeSeries'][2]['areas']:
if area['area']['name'] == area_name2:
weather_data['daily']['temp']['max'] = area['temps'][1]
# 降水確率を取得
for area in response[0]['timeSeries'][1]['areas']:
if area['area']['name'] == area_name1:
weather_data['daily']['pop'] = area['pops'][0]
return(weather_data)
テスト
テストはGoogle Colaboratoryを使いました。
ファイルの読み込み
天気コードを天気に変換するための辞書ファイルを読み込む必要がありました。
左のファイルアイコンを押すと一時的に使えるディスクが表示されるので、使うファイルを置きます。
今回はsrcというフォルダを作成し、配下に辞書として使うテキストファイルを配置しました。
ファイル参照を使う部分のコードはそのままで動きました。
配列と辞書の区別
最初にGoogle Colaboratoryにコードをコピペ(と少し手直し)して動かしてみると、まずこのようなエラーが出ました。
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-6-be5fde5d5678> in <cell line: 236>()
234 CHANNEL = "おてんきくん"
235
--> 236 today_str_tokyo = make_today_str('東京','130000','東京地方')
237 today_str_chiba = make_today_str('千葉','120000','北西部')
238
1 frames
<ipython-input-6-be5fde5d5678> in get_weather(prefecture_code, area_name)
77 for area in response[0]['timeSeries'][0]['areas']:
78 if area['area']['name'] == area_name:
---> 79 weather_data['daily']['weather']['main'] = area['weatherCodes'][0]
80 weather_data['daily']['weather']['description'] = code_to_weather(weather_data['daily']['weather']['main'])
型でエラーが出ているらしく、リストの[]の中身がStringはやめてくださいと言われているようです。
その場合下記も同様にエラーが出そうですがこっちは大丈夫なのでしょうか。
for area in response[0]['timeSeries'][0]['areas']:
リストと辞書型の区別ができていなかったようです。上記はリストの中に辞書の中にリストの中に辞書、の構造なので大丈夫ですが、
天気データは下記のように初期化していました。これはリスト型の初期化でした。
weather_data=[]
辞書型の初期化は下記です。
weather_data={}
リスト型で定義しているのにweather_data['daily']と使い方が辞書型なのがエラーのポイントでした。
これを修正したのですが、まだエラーはそのままです。
2点目のエラーのポイントは辞書型の初期化でした。
思えば配列の定義などは最初から長さを決めることが多かったりします。
同様に辞書型も構造を最初から定義してあげる必要があるらしく、下記のように空の辞書を作ってあげることで解決しました。
weather_data = {
'daily': {
'weather': {
'main': '',
'description': ''
},
'temp': {
'max': '',
'min': ''
},
'pop': ''
}
}
これをするまでに何個か紆余曲折をしたのですが、なぜこれを最初にしなかったのか、と思っていました。
名前違い
いよいよ一通りのコード修正を終えて、実行、エラーもない!と思いきや最高気温が空で表示されてくれません。
どうやら地域名が違うようです。
"areas": [
{
"area": {
"name": "東京地方",
"code": "130010"
},
"pops": [
"20",
"20",
"10",
"10",
"10",
"10"
]
}
]
//平均気温
"tempAverage": {
"areas": [
{
"area": {
"name": "東京",
"code": "44132"
},
"min": "20.9",
"max": "28.2"
}
]
},
天気を教えてくれる部分と最高気温を教えてくれる部分の地域名はそれぞれ別でした。全部の項目を「東京地方」という地域名に一致する項目で取ろうとしていたので、取得してくれなかったということでした。
これを直したことで望んだ出力を得られたので、完了となりました。
感想・反省
気象庁が提供しているXMLを取得することで、ピンポイントに欲しい情報を取得できそうです。
今回は技術不足で実装を見送ったので、ゆくゆくはこの方法にもチャレンジしてみたいです。
コードを書いたらテストをする、というのは当たり前のことですが、その必要性を実感できました。規模に関わらずミスは生じるので、チェックを怠らないようにしたいです。