Pythonコード
コード全文です。コピーして weather_bot.py という名前で保存できます。長いコードは枠の中をスクロールして読めます。
"""天気予報を整形し、明示的な --send 指定時だけLINEに送信する。"""
import argparse
import json
import math
import os
import re
import sys
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
WEATHER = {
0: '快晴', 1: 'おおむね晴れ', 2: '一部曇り', 3: '曇り',
45: '霧', 48: '着氷性の霧', 51: '弱い霧雨', 53: '霧雨', 55: '強い霧雨',
56: '弱い着氷性の霧雨', 57: '強い着氷性の霧雨',
61: '弱い雨', 63: '雨', 65: '強い雨', 66: '弱い着氷性の雨', 67: '強い着氷性の雨',
71: '弱い雪', 73: '雪', 75: '強い雪', 77: '霧雪',
80: '弱いにわか雨', 81: 'にわか雨', 82: '激しいにわか雨',
85: '弱いにわか雪', 86: '強いにわか雪', 95: '雷雨',
96: 'ひょうを伴う雷雨', 99: '激しいひょうを伴う雷雨',
}
def request_json(request, service):
"""HTTPエラーの本文や認証情報はログに出さない。自動再送はしない。"""
try:
with urlopen(request, timeout=20) as response:
return json.load(response)
except HTTPError as exc:
raise RuntimeError(f'{service}: HTTP {exc.code}。設定や利用上限を確認してください。') from None
except (URLError, TimeoutError, OSError):
raise RuntimeError(f'{service}: 通信に失敗しました。送信時は到達状況を確認してから再実行してください。') from None
except (ValueError, UnicodeError):
raise RuntimeError(f'{service}: 応答が正しいJSONではありません。') from None
def fetch_weather(latitude, longitude):
if not (math.isfinite(latitude) and -90 <= latitude <= 90
and math.isfinite(longitude) and -180 <= longitude <= 180):
raise ValueError('緯度は-90〜90、経度は-180〜180で指定してください。')
params = urlencode({
'latitude': latitude, 'longitude': longitude,
'daily': 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max',
'timezone': 'Asia/Tokyo', 'forecast_days': 1,
'temperature_unit': 'celsius',
})
return request_json(Request('https://api.open-meteo.com/v1/forecast?' + params), '天気API')
def format_message(data, city):
if not city.strip() or len(city) > 80 or '\n' in city or '\r' in city:
raise ValueError('地域名は1〜80文字の1行で指定してください。')
try:
daily = data['daily']
date = daily['time'][0]
code = daily['weather_code'][0]
high = daily['temperature_2m_max'][0]
low = daily['temperature_2m_min'][0]
rain = daily['precipitation_probability_max'][0]
if not isinstance(date, str) or not re.fullmatch(r'\d{4}-\d{2}-\d{2}', date):
raise ValueError
if code is not None and (type(code) is not int):
raise ValueError
for value in (high, low, rain):
if value is not None and (type(value) not in (int, float) or not math.isfinite(value)):
raise ValueError
if rain is not None and not 0 <= rain <= 100:
raise ValueError
except (KeyError, IndexError, TypeError, ValueError):
raise ValueError('天気データの形式が不正です。送信を中止しました。') from None
def display(value, unit):
return '取得できませんでした' if value is None else f'{value:g}{unit}'
condition = WEATHER.get(code, '不明')
return (f'{city}の天気予報({date}・日本時間)\n'
f'天気:{condition}\n最高:{display(high, "℃")} / 最低:{display(low, "℃")}\n'
f'降水確率(日内最大):{display(rain, "%")}\n'
'出典:Open-Meteo(予報は変わる場合があります)\nhttps://open-meteo.com/')
def credentials():
token = os.environ.get('LINE_CHANNEL_ACCESS_TOKEN', '').strip()
user_id = os.environ.get('LINE_USER_ID', '').strip()
if not token or '\n' in token or '\r' in token:
raise ValueError('LINE_CHANNEL_ACCESS_TOKENを環境変数に設定してください。')
if not re.fullmatch(r'U[0-9a-fA-F]{32}', user_id):
raise ValueError('LINE_USER_IDに、Uから始まる33文字のユーザーIDを設定してください。')
return token, user_id
def send_line(text, token, user_id):
payload = {'to': user_id, 'messages': [{'type': 'text', 'text': text}]}
request = Request('https://api.line.me/v2/bot/message/push',
data=json.dumps(payload, ensure_ascii=False).encode('utf-8'),
headers={'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'},
method='POST')
request_json(request, 'LINE API')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--city', default='東京', help='表示する地域名。緯度・経度も合わせて変更してください。')
parser.add_argument('--latitude', type=float, default=35.6812)
parser.add_argument('--longitude', type=float, default=139.7671)
mode = parser.add_mutually_exclusive_group()
mode.add_argument('--demo', action='store_true', help='架空データで確認。通信も送信もしません。')
mode.add_argument('--send', action='store_true', help='実際の予報を取得してLINEへ1回送信します。')
args = parser.parse_args()
try:
auth = credentials() if args.send else None
if args.demo:
data = {'daily': {'time': ['2026-01-01'], 'weather_code': [2],
'temperature_2m_max': [12.5], 'temperature_2m_min': [4.0],
'precipitation_probability_max': [30]}}
else:
data = fetch_weather(args.latitude, args.longitude)
message = format_message(data, args.city)
if args.send:
send_line(message, *auth)
print('LINE APIが送信要求を受け付けました。実際の受信はLINEで確認してください。')
else:
print('[架空データのデモ・未送信]' if args.demo else '[実際の予報・未送信]')
print(message)
return 0
except (ValueError, RuntimeError) as exc:
print(str(exc), file=sys.stderr)
return 1
if __name__ == '__main__':
raise SystemExit(main())
設定・実行手順
Pythonを実行できるパソコンで操作してください。このページを開くだけではLINEへ送信されません。
コードをこのページからコピーした場合は、weather-bot フォルダを作成し、その中に weather_bot.py を保存してください。テスト用ファイルはコード一式のZIPに含まれています。
天気予報を取得してLINEへ通知する、学習用のPython制作物です。
コードの理解・修正・動作検証を進めるための初期版です。
構成
- index.html:制作物を紹介するポートフォリオ
- weather-bot/weather_bot.py:予報取得・日本語整形・LINE送信
- weather-bot/test_weather_bot.py:外部通信を使わない自動テスト
- .gitignore:認証情報や一時ファイルの除外
できること
Open-Meteoの当日予報(日本時間)から天気・最高/最低気温・降水確率の日内最大値を取得し、LINE Messaging APIのpush APIで1ユーザーに送ります。
地域名は表示用です。地域を変えるときは緯度・経度も必ず変更します。
欠測は0とせず「取得できませんでした」と表示。不正な応答・通信エラーでは処理を止めます。
Webhookを使う対話Botではなく、実行時に1回通知するプログラムです。
最初に試す(認証不要・外部通信なし)
Python 3.10以上を使用してください。追加ライブラリは不要です。
ターミナルで、このREADMEがあるフォルダへ移動して実行します。
python3 weather-bot/weather_bot.py --demo
2026-01-01の架空データが表示されます。現在の予報ではなく、LINEにも送信されません。
実際の予報を確認(送信なし)
python3 weather-bot/weather_bot.py
初期値は東京駅付近です。例:気仙沼市中心部付近を指定する場合:
python3 weather-bot/weather_bot.py --city 気仙沼 --latitude 38.908 --longitude 141.570
インターネット接続が必要です。Open-Meteoの利用条件・制限に従ってください。
LINEに送信するための設定
1. LINE公式アカウントを作成し、LINE Official Account ManagerでMessaging APIを有効にします。
2. LINE Developersコンソールで、そのチャネルのMessaging APIタブからチャネルアクセストークンを発行します。
3. 自分のLINEで公式アカウントを友だち追加します。
4. チャネル基本設定の「あなたのユーザーID」を確認します。表示名や友だち検索用のLINE IDとは異なる、Uから始まる33文字です。表示されない場合はビジネスIDとLINEアカウントの連携を確認してください。
5. 次の環境変数を、自分の端末のターミナルに設定します。トークンをコード・HTML・README・GitHubへ記載しないでください。
macOS標準のzshでは、以下の入力方法ならトークンを画面に表示せず、コマンド履歴にも値を直接書かずに設定できます。
read -s 'LINE_CHANNEL_ACCESS_TOKEN?チャネルアクセストークン: '
echo
export LINE_CHANNEL_ACCESS_TOKEN
read 'LINE_USER_ID?あなたのユーザーID: '
export LINE_USER_ID
同じターミナルで、送信先が自分であることを確認して実行:
python3 weather-bot/weather_bot.py --city 気仙沼 --latitude 38.908 --longitude 141.570 --send
このコマンドはLINEへ実際に送信します。送信回数は公式アカウントのプランや上限の影響を受けます。
HTTP成功は受信の保証ではありません。自分のLINEで受信を確認してください。
タイムアウト時は送信済みの可能性もあるため、受信状況を見てから再実行してください。重複防止のため自動再送は実装していません。
このプログラムは.envを自動読込しません。通常実行と--demoではトークンは不要です。
検証
python3 -m unittest discover -s weather-bot -v
日本語整形・欠測・不正データ・緯度検証・デモ時の通信禁止・認証未設定時の送信禁止・送信JSON・HTTPエラー表示をテストしています。
実API接続、LINEでの実機受信、定期実行は未検証です。定期実行の仕組みは含めていません。
GitHubへ載せるファイル
このフォルダの構成を保ってリポジトリへ追加します。秘密情報を入れたファイルはアップロードしないでください。
index.htmlにはコードと手順を内蔵しています。Pythonを実行する場合やテストする場合は、weather-botフォルダも使用してください。
HTMLは紹介ページです。GitHub PagesなどでHTMLを公開してもPythonは実行されません。BotはPythonのある端末やサーバーで実行します。
設計を説明するポイント
- fetch_weather:HTTP GETで予報を取得
- format_message:JSONを検証し、人が読めるメッセージに変換
- send_line:認証ヘッダー付きのHTTP POSTで送信
- main:引数でデモ・確認・送信を切り替え
公式ドキュメント・出典
- Open-Meteo Forecast API
- Open-Meteo利用条件
- LINE Messaging APIの開始
- LINEユーザーIDの取得
- LINE Messaging APIリファレンス
天気データの出典はOpen-Meteoです。通知文にも出典リンクを含めています。