Streamlitを使った天気情報アプリ開発

目次

DEMO

このアプリでは、世界中の都市(特に日本の47都道府県の県庁所在地)の天気情報をリアルタイムで確認できます。日本語表示に対応しており、直感的な操作で天気状況、気温、湿度などを視覚的に把握できます。

主な機能:

  • 日本語での天気情報表示
  • 都市名による検索(日本語入力可能)
  • 日本の主要都市の一覧から選択
  • 天気状態に合わせたアイコンと色表示
  • 詳細な気象データ(気温、湿度、風向き、気圧など)
  • 日の出・日の入り時刻の表示(日本時間)
  • 摂氏/華氏の切り替え機能

前提条件

このプロジェクトを始める前に、以下の条件を満たしていることを確認してください:

  • Python 3.8以上がインストールされていること
  • 基本的なPythonの文法を理解していること
  • コマンドライン操作の基礎知識があること
  • APIの基本概念を理解していること(詳しくなくてもOK)
  • インターネット接続があること
  • テキストエディタまたはVSCodeなどのIDEがインストールされていること

また、OpenWeather APIの無料アカウントが必要になります。アカウント登録は簡単で、無料プランでも十分な機能を利用できます。

プロジェクト概要

このプロジェクトでは、StreamlitというPythonフレームワークを使って、リアルタイムの天気情報を表示するWebアプリを開発します。OpenWeather APIから取得した気象データを、美しく使いやすいインターフェースで表示します。

特に注目すべき点は、日本語対応日本の地域に特化した機能です。海外の天気情報サービスをそのまま使うのではなく、日本の気象表現や習慣に合わせたカスタマイズを行います。また、47都道府県の県庁所在地をワンクリックで選択できる機能も実装します。

プロジェクトの狙い

このプロジェクトを通して、以下のスキルを身につけることができます:

  1. API連携:外部サービスとのデータのやり取り
  2. データ処理:JSONデータの解析と整形
  3. UI/UX設計:見やすく使いやすいインターフェース作成
  4. エラーハンドリング:例外処理による堅牢なアプリ開発
  5. 多言語対応:日本語表示の実装方法
  6. Streamlitフレームワーク:短期間でWebアプリを作成する技術

プロジェクト構成

weather_streamlit_app/
├── venv/                  # 仮想環境フォルダ(自動生成)
├── .gitignore             # Gitの除外設定ファイル
├── config.ini             # APIキー設定ファイル
├── app.py                 # メインアプリケーション
└── requirements.txt       # 必要なライブラリリスト

このシンプルな構成ながら、実用的な天気情報アプリを開発していきます。特にStreamlitの強力な機能を活用することで、少ないコード量でプロフェッショナルなUIを実現します。

各ステップでは、コードの解説だけでなく、なぜそのような実装にしたのかという背景も説明していきます。これにより、単にコピー&ペーストするだけでなく、実際の開発現場で役立つ考え方や手法を学ぶことができます。

Step1: 天気情報APIの準備とプロジェクト基本構造の作成

このステップでは、世界中の都市の天気情報を取得できるOpenWeather APIの利用準備と、Streamlitを使った天気情報アプリのプロジェクト基本構造を作成します。

OpenWeather APIキーの取得

本プロジェクトでは、無料で利用できるOpenWeather APIを活用します。このサービスは、世界中の都市の気象データを簡単に取得できる便利なAPIです。利用するためには個人用APIキーが必要になります。

  1. OpenWeather公式サイトにアクセスし、「Sign In」のから新規ユーザー登録をしましょう
  1. ユーザーネーム、メールアドレス、パスワードを入力して、チェック項目を確認しアカウントを作成します
  2. 登録後、確認メールが届きますので、メール内のリンクをクリックして登録を完了させます
  1. ログイン後、「API keys」タブを選択すると個人APIキーが表示されます

注意: APIキーは絶対に他人と共有しないでください!漏洩した場合は、すぐに無効化して新しいキーを発行する必要があります。

APIリクエストの作成と応答データの確認

APIキーを取得したら、実際にAPIを呼び出して動作確認をしてみましょう。OpenWeatherの公式ドキュメントによると、現在の天気データを取得するためのURLは以下のようになっています:

https://api.openweathermap.org/data/2.5/weather?q={都市名}&appid={APIキー}

この中の変数を実際の値に置き換えます:

  1. {都市名} → 例えば「tokyo」や「osaka」などの都市名
  2. {APIキー} → あなたが取得したAPIキー

例えば、東京の天気を調べるなら、次のようなURLになります:

https://api.openweathermap.org/data/2.5/weather?q=tokyo&appid=あなたのAPIキー

このURLをブラウザに貼り付けると、次のようなJSON形式のデータが返ってきます:

{
  "coord": {
    "lon": 139.6917,
    "lat": 35.6895
  },
  "weather": [
    {
      "id": 520,
      "main": "Rain",
      "description": "light intensity shower rain",
      "icon": "09d"
    }
  ],
  "base": "stations",
  "main": {
    "temp": 278.91,
    "feels_like": 273.81,
    "temp_min": 277.4,
    "temp_max": 279.4,
    "pressure": 1018,
    "humidity": 85,
    "sea_level": 1018,
    "grnd_level": 1016
  },
  "visibility": 7000,
  "wind": {
    "speed": 9.77,
    "deg": 40
  },
  "rain": {
    "1h": 1.15
  },
  "clouds": {
    "all": 75
  },
  "dt": 1743219770,
  "sys": {
    "type": 2,
    "id": 268395,
    "country": "JP",
    "sunrise": 1743193943,
    "sunset": 1743238773
  },
  "timezone": 32400,
  "id": 1850144,
  "name": "Tokyo",
  "cod": 200
}

このデータから、私たちのStreamlitアプリで表示したい主な情報を抜き出してみましょう:

  1. 都市名: "name" キーの値(この例では "Tokyo"
  2. 天気の状態: "weather"[0]"description" の値(この例では "light intensity shower rain"
  3. 気温: "main""temp" の値(この例では 278.91 ケルビン)

気温がケルビン単位で表示されていますが、これは日本ではあまり馴染みがありません。摂氏(℃)で表示するには、URLに &units=metric パラメータを追加します:

https://api.openweathermap.org/data/2.5/weather?q=tokyo&units=metric&appid=あなたのAPIキー

このパラメータを追加すると、気温情報が摂氏で返されるようになります:

{
  "coord": {
    "lon": 139.6917,
    "lat": 35.6895
  },
  "weather": [
    {
      "id": 520,
      "main": "Rain",
      "description": "light intensity shower rain",
      "icon": "09d"
    }
  ],
  "base": "stations",
  "main": {
    "temp": 5.46,
    "feels_like": 0.86,
    "temp_min": 4.25,
    "temp_max": 6.25,
    "pressure": 1018,
    "humidity": 85,
    "sea_level": 1018,
    "grnd_level": 1016
  },
  "visibility": 7000,
  "wind": {
    "speed": 7.72,
    "deg": 30
  },
  "rain": {
    "1h": 0.71
  },
  "clouds": {
    "all": 75
  },
  "dt": 1743220676,
  "sys": {
    "type": 2,
    "id": 268395,
    "country": "JP",
    "sunrise": 1743193943,
    "sunset": 1743238773
  },
  "timezone": 32400,
  "id": 1850144,
  "name": "Tokyo",
  "cod": 200
}

これでより理解しやすくなりましたね!私たちのアプリでは、デフォルトで摂氏表示とし、必要に応じて華氏(℉)表示も選べるようにしていきます。

Streamlitアプリのプロジェクト構造を作成する

では、本格的にStreamlitアプリの開発を始めましょう。まずは必要なフォルダとファイルを作成します。

仮想環境の作成と有効化

プロジェクトを始める前に、仮想環境を作成することをおすすめします。仮想環境を使うと、プロジェクトごとに必要なライブラリをインストールできるので、依存関係の問題を避けられます。

Windowsの場合:

# プロジェクトフォルダの作成
mkdir weather_streamlit_app
cd weather_streamlit_app

# 仮想環境の作成
python -m venv venv

# 仮想環境の有効化
venv\Scripts\activate

macOS/Linuxの場合:

# プロジェクトフォルダの作成
mkdir weather_streamlit_app
cd weather_streamlit_app

# 仮想環境の作成
python -m venv venv

# 仮想環境の有効化
source venv/bin/activate

必要なファイルの作成

仮想環境が有効化されたら、必要なファイルを作成しましょう:

# 必要なファイルの作成
touch .gitignore config.ini app.py requirements.txt

手動での作成方法(VSCodeなど)

エディタを使って手動でファイルを作成することもできます:

  1. weather_streamlit_app という名前のフォルダを作成
  2. VSCodeなどのエディタでフォルダを開く
  3. そのフォルダ内に以下のファイルを作成:
    • .gitignore
    • config.ini
    • app.py
    • requirements.txt

作成したフォルダ構造は以下のようになります:

weather_streamlit_app/
├── venv/              # 仮想環境(自動生成)
├── .gitignore
├── config.ini
├── app.py
└── requirements.txt

APIキーの安全な管理

APIキーは公開すべきではないため、バージョン管理システムから除外します。.gitignore ファイルを開き、次のように編集してください:

# .gitignore

# 設定ファイル(APIキーなどの秘密情報を含む)
config.ini

# 仮想環境
venv/

# Python関連
__pycache__/
*.py[cod]
*$py.class

# 環境変数
.env

これで config.ini ファイルが誤ってGitリポジトリに登録されることを防げます。

次に、APIキーを config.ini ファイルに保存します:

; config.ini

[openweather]
api_key=あなたのAPIキー

そして、Pythonコードから安全にAPIキーにアクセスするためのコードを app.py に追加します:

# app.py

from configparser import ConfigParser

def get_api_key():
    """設定ファイルからOpenWeather APIキーを取得する関数
    
    設定ファイル「config.ini」に以下の構造で保存されていることを想定:
    
        [openweather]
        api_key=あなたのAPIキー
        
    Returns:
        str: OpenWeather APIキー
    """
    config = ConfigParser()
    config.read("config.ini")
    return config["openweather"]["api_key"]

必要なライブラリのインストール

最後に、必要なライブラリを requirements.txt ファイルに記述し、インストールしましょう:

# requirements.txt
streamlit==1.44.0
requests==2.32.3
python-dotenv==1.1.0

仮想環境内でこれらのライブラリをインストールします:

pip install -r requirements.txt

これで、Streamlitアプリ開発の準備が整いました!

このステップでは、OpenWeather APIキーの取得方法と、APIから返されるデータの構造を確認しました。また、Streamlitアプリの基本構造を作成し、APIキーを安全に管理する方法も学びました。次のステップでは、このプロジェクト構造をベースに、Streamlitで天気情報を視覚的に表示するウェブアプリの開発を始めていきます。

Step2: Streamlitでユーザー入力を扱うインターフェースの作成

このステップでは、Streamlitを使ってユーザーが都市名を入力し、温度の単位(摂氏/華氏)を選択できるインターフェースを作成します。コマンドラインアプリとは異なり、Streamlitではウェブブラウザ上で直感的に操作できる入力フォームを提供できます。

Streamlitアプリの基本構造

まずは、前のステップで作成した app.py ファイルを開き、以下のように編集して基本的なStreamlitアプリの構造を作りましょう:

# app.py

import streamlit as st
from configparser import ConfigParser
import requests
from urllib import parse

# APIのベースURLを定数として定義
WEATHER_API_URL = "https://api.openweathermap.org/data/2.5/weather"

def get_api_key():
    """設定ファイルからOpenWeather APIキーを取得する関数
    
    設定ファイル「config.ini」に以下の構造で保存されていることを想定:
    
        [openweather]
        api_key=あなたのAPIキー
        
    Returns:
        str: OpenWeather APIキー
    """
    config = ConfigParser()
    config.read("config.ini")
    return config["openweather"]["api_key"]

# アプリのタイトルを設定
st.title("お天気情報アプリ")
st.write("世界中の都市の天気情報を簡単に確認できます!")

ここでは、必要なライブラリをインポートし、APIのベースURLを定数として定義しました。また、前のステップで作成したAPIキーを取得する関数も含めています。st.title()st.write() を使って、アプリのタイトルと簡単な説明を表示しています。

ユーザー入力フォームの作成

次に、ユーザーが都市名を入力し、温度単位を選択できるフォームを作成します:

# 前述のコード...

# サイドバーに入力フォームを配置
st.sidebar.header("検索条件")

# 都市名の入力欄
city_name = st.sidebar.text_input(
    "都市名を入力してください",
    placeholder="例:Tokyo、Osaka、Kyoto",
)

# 温度単位の選択(デフォルトは摂氏)
use_imperial = st.sidebar.checkbox("温度を華氏(°F)で表示する")

# 検索ボタン
search_button = st.sidebar.button("天気を検索")

Streamlitの sidebar を使って、画面の左側に入力フォームを配置しています。ユーザーは都市名をテキスト入力し、チェックボックスで温度単位を選択できます。また、「天気を検索」ボタンをクリックすると、天気情報を取得するようにしました。

API呼び出し用のURLを作成する関数

ユーザーが入力した都市名と選択した温度単位を使って、APIを呼び出すためのURLを作成する関数を定義しましょう:

# 前述のコード...
# def get_api_key()の後に配置

def build_weather_query(city_name, use_imperial=False):
    """OpenWeather APIを呼び出すためのURLを生成する関数
    
    Args:
        city_name (str): 検索する都市の名前
        use_imperial (bool): 華氏表示にするかどうか(デフォルトはFalse=摂氏)
    
    Returns:
        str: APIを呼び出すための完全なURL
    """
    api_key = get_api_key()
    
    # URLエンコードして特殊文字や空白を適切に処理
    encoded_city_name = parse.quote_plus(city_name)
    
    # 温度単位を設定(デフォルトは摂氏=metric)
    units = "imperial" if use_imperial else "metric"

    # URLを構築    # 温度単位を設定(デフォルトは摂氏=metric)
    url = (
        f"{WEATHER_API_URL}?q={encoded_city_name}"
        f"&units={units}&appid={api_key}"
    )
    
    return url

この関数は、以下のような処理を行っています:

  1. APIキーを設定ファイルから取得
  2. 都市名をURLエンコードして、空白や特殊文字を適切に処理
  3. ユーザーの選択に基づいて温度単位を設定(imperial=華氏、metric=摂氏)
  4. 完全なURLを構築して返す

URLエンコードは重要なステップです。例えば、「New York」のような空白を含む都市名をそのままURLに含めると、APIは正しく認識できません。parse.quote_plus() を使うと、「New+York」のように適切にエンコードされます。

検索ボタンが押されたときの処理

最後に、ユーザーが検索ボタンを押したときに、実際にAPIを呼び出して結果を表示する処理を追加します:

# 前述のコード...

# 検索ボタンが押されたときの処理
if search_button:
    if not city_name:  # 都市名が入力されていない場合
        st.warning("都市名を入力してください")
    else:
        # 処理中を表示
        with st.spinner("天気情報を取得中..."):
            # ここで後ほどAPIを呼び出す処理を追加
            query_url = build_weather_query(city_name, use_imperial)
            st.info(f"生成されたURL: {query_url}")
            
            # この時点では実際のAPI呼び出しはまだ行わず、URLだけ表示する
            st.info("次のステップでは、このURLを使って実際に天気情報を取得します!")

ここでは、検索ボタンが押された場合の基本的な流れを実装しています:

  1. 都市名が入力されているかチェック(未入力の場合は警告を表示)
  2. 処理中であることを示すスピナーを表示
  3. build_weather_query() 関数を呼び出してURLを生成
  4. 生成されたURLを表示(この時点では実際のAPI呼び出しはまだ行いません)

完全なコード

以上の部分をまとめると、app.py の完全なコードは次のようになります
# app.py

import streamlit as st
from configparser import ConfigParser
import requests
from urllib import parse

# APIのベースURLを定数として定義
WEATHER_API_URL = "https://api.openweathermap.org/data/2.5/weather"

def get_api_key():
    """設定ファイルからOpenWeather APIキーを取得する関数
    
    設定ファイル「config.ini」に以下の構造で保存されていることを想定:
    
        [openweather]
        api_key=あなたのAPIキー
        
    Returns:
        str: OpenWeather APIキー
    """
    config = ConfigParser()
    config.read("config.ini")
    return config["openweather"]["api_key"]

def build_weather_query(city_name, use_imperial=False):
    """OpenWeather APIを呼び出すためのURLを生成する関数
    
    Args:
        city_name (str): 検索する都市の名前
        use_imperial (bool): 華氏表示にするかどうか(デフォルトはFalse=摂氏)
    
    Returns:
        str: APIを呼び出すための完全なURL
    """
    api_key = get_api_key()
    
    # URLエンコードして特殊文字や空白を適切に処理
    encoded_city_name = parse.quote_plus(city_name)
    
    # 温度単位を設定(デフォルトは摂氏=metric)
    units = "imperial" if use_imperial else "metric"
    
    # URLを構築
    url = (
        f"{WEATHER_API_URL}?q={encoded_city_name}"
        f"&units={units}&appid={api_key}"
    )
    
    return url

# アプリのタイトルを設定
st.title("お天気情報アプリ")
st.write("世界中の都市の天気情報を簡単に確認できます!")

# サイドバーに入力フォームを配置
st.sidebar.header("検索条件")

# 都市名の入力欄
city_name = st.sidebar.text_input(
    "都市名を入力してください",
    placeholder="例:Tokyo、Osaka、Kyoto",
)

# 温度単位の選択(デフォルトは摂氏)
use_imperial = st.sidebar.checkbox("温度を華氏(°F)で表示する")

# 検索ボタン
search_button = st.sidebar.button("天気を検索")

# 検索ボタンが押されたときの処理
if search_button:
    if not city_name:  # 都市名が入力されていない場合
        st.warning("都市名を入力してください")
    else:
        # 処理中を表示
        with st.spinner("天気情報を取得中..."):
            # URLを生成
            query_url = build_weather_query(city_name, use_imperial)
            st.info(f"生成されたURL: {query_url}")
            
            # この時点では実際のAPI呼び出しはまだ行わず、URLだけ表示する
            st.info("次のステップでは、このURLを使って実際に天気情報を取得します!")

アプリの実行方法

Streamlitアプリを実行するには、仮想環境を有効化した状態で、ターミナルで以下のコマンドを実行します:

streamlit run app.py

このコマンドを実行すると、Streamlitがローカルサーバーを起動し、自動的にブラウザでアプリが開きます(通常は http://localhost:8501 )。

このステップで学んだこと

この「Step2」では、以下のことを学びました:

  1. Streamlitの基本的な使い方:タイトル、テキスト表示、サイドバーの使用方法
  2. ユーザー入力の受け取り方:テキスト入力、チェックボックス、ボタンの実装
  3. URLエンコードの重要性:API呼び出しで都市名を適切に処理する方法
  4. 条件分岐を使った基本的なフロー制御:入力チェックや処理の順序付け

次のステップでは、生成したURLを使って実際にAPIを呼び出し、取得した天気情報を適切に表示する方法を学びます。

Step3: APIリクエストの実装とエラー処理

このステップでは、OpenWeather APIから実際に天気データを取得し、適切なエラー処理を行うコードを実装します。APIリクエストが失敗した場合でも、ユーザーにわかりやすいメッセージを表示できるようにしましょう。

リクエストに必要なライブラリの追加

まず、APIリクエストに必要なライブラリを追加しましょう。app.pyの冒頭部分を以下のように更新します:

# app.py

import streamlit as st
import json
import requests
from configparser import ConfigParser
from urllib import parse

# APIのベースURLを定数として定義
WEATHER_API_URL = "https://api.openweathermap.org/data/2.5/weather"

この例では、HTTPリクエストを行うためにrequestsライブラリを使用します。これはurllibよりも使いやすく、Pythonでウェブリクエストを行う際に広く使われているライブラリです。

天気データを取得する関数の実装

次に、生成したURLを使って実際に天気データを取得する関数を実装します:

def get_weather_data(query_url):
    """OpenWeather APIにリクエストを送信し、天気データを取得する関数
    
    Args:
        query_url (str): OpenWeather APIの完全なURL
        
    Returns:
        dict: 特定の都市の天気情報を含む辞書
        None: エラーが発生した場合
    """
    try:
        response = requests.get(query_url)
        
        # レスポンスのステータスコードをチェック
        response.raise_for_status()
        
        # JSONデータを辞書に変換して返す
        return response.json()
        
    except requests.exceptions.HTTPError as http_err:
        if response.status_code == 401:
            st.error("APIキーが無効です。APIキーを確認してください。")
        elif response.status_code == 404:
            st.error(f"指定した都市の天気データが見つかりませんでした。都市名のスペルを確認してください。")
        else:
            st.error(f"HTTPエラーが発生しました: {http_err}")
        return None
        
    except requests.exceptions.ConnectionError:
        st.error("サーバーに接続できませんでした。インターネット接続を確認してください。")
        return None
        
    except requests.exceptions.Timeout:
        st.error("リクエストがタイムアウトしました。後でもう一度お試しください。")
        return None
        
    except requests.exceptions.RequestException as e:
        st.error(f"リクエスト中にエラーが発生しました: {e}")
        return None
        
    except json.JSONDecodeError:
        st.error("サーバーからの応答を読み取れませんでした。")
        return None

この関数では、requestsライブラリを使ってHTTP GETリクエストを行い、以下の主要なエラー処理を行っています:

  1. HTTPエラー(4xx/5xx)
    • 401エラー(認証エラー):APIキーが無効な場合
    • 404エラー(Not Found):指定した都市が見つからない場合
    • その他のHTTPエラー
  2. 接続エラー
    • インターネット接続がない場合
    • サーバーに到達できない場合
  3. タイムアウト
    • リクエストが時間内に完了しなかった場合
  4. JSONデコードエラー
    • サーバーからの応答が有効なJSONではない場合

これらのエラー処理により、ユーザーは問題が発生した場合でも何が起きているのかを理解できます。

ユーザーインターフェースの更新

次に、ユーザーインターフェース部分を更新して、検索ボタンが押されたときに、天気データを取得して表示するようにします:

# 検索ボタンが押されたときの処理
if search_button:
    if not city_name:  # 都市名が入力されていない場合
        st.warning("都市名を入力してください")
    else:
        # 処理中を表示
        with st.spinner("天気情報を取得中..."):
            # APIリクエスト用のURLを生成
            query_url = build_weather_query(city_name, use_imperial)
            
            # 天気データを取得
            weather_data = get_weather_data(query_url)
            
            # データが正常に取得できた場合のみ処理を続行
            if weather_data:
                # この時点では取得したデータを表示するだけ
                st.success(f"「{weather_data['name']}」の天気情報を取得しました!")
                
                # 取得したデータの詳細を表示(次のステップで改良します)
                st.subheader("取得したデータ")
                st.json(weather_data)

ここでは、ユーザーが検索ボタンを押したときに、以下の処理を行います:

  1. 都市名が入力されているかチェック
  2. 処理中であることを示すスピナーを表示
  3. build_weather_query()関数でURLを生成
  4. get_weather_data()関数でAPIリクエストを送信
  5. データが正常に取得できた場合、成功メッセージを表示
  6. 取得したデータの詳細を表示(現時点ではJSON形式のまま表示)

存在しない都市名のテスト

エラー処理が正しく機能しているか確認するため、存在しない都市名や特殊なケースをテストできるようにしてみましょう。app.pyの最後に、以下のコードを追加します:

# デバッグ用:特殊な都市名のサンプル
st.sidebar.markdown("---")
st.sidebar.subheader("テスト用都市名")
st.sidebar.markdown("""
- 存在しない都市: `Nonexistent City`
- 特殊文字: `Münich`
- スペースを含む: `New York`
- 日本語: `東京`
""")

これにより、サイドバーにテスト用の都市名サンプルが表示され、エラー処理がうまく機能しているか簡単に確認できます。

完全なコード

これまでの変更を含めた、app.pyの完全なコードは以下のようになります
# app.py

import streamlit as st
import json
import requests
from configparser import ConfigParser
from urllib import parse

# APIのベースURLを定数として定義
WEATHER_API_URL = "https://api.openweathermap.org/data/2.5/weather"

def get_api_key():
    """設定ファイルからOpenWeather APIキーを取得する関数
    
    設定ファイル「config.ini」に以下の構造で保存されていることを想定:
    
        [openweather]
        api_key=あなたのAPIキー
        
    Returns:
        str: OpenWeather APIキー
    """
    config = ConfigParser()
    config.read("config.ini")
    return config["openweather"]["api_key"]

def build_weather_query(city_name, use_imperial=False):
    """OpenWeather APIを呼び出すためのURLを生成する関数
    
    Args:
        city_name (str): 検索する都市の名前
        use_imperial (bool): 華氏表示にするかどうか(デフォルトはFalse=摂氏)
    
    Returns:
        str: APIを呼び出すための完全なURL
    """
    api_key = get_api_key()
    
    # URLエンコードして特殊文字や空白を適切に処理
    encoded_city_name = parse.quote_plus(city_name)
    
    # 温度単位を設定(デフォルトは摂氏=metric)
    units = "imperial" if use_imperial else "metric"
    
    # URLを構築
    url = (
        f"{WEATHER_API_URL}?q={encoded_city_name}"
        f"&units={units}&appid={api_key}"
    )
    
    return url

def get_weather_data(query_url):
    """OpenWeather APIにリクエストを送信し、天気データを取得する関数
    
    Args:
        query_url (str): OpenWeather APIの完全なURL
        
    Returns:
        dict: 特定の都市の天気情報を含む辞書
        None: エラーが発生した場合
    """
    try:
        response = requests.get(query_url)
        
        # レスポンスのステータスコードをチェック
        response.raise_for_status()
        
        # JSONデータを辞書に変換して返す
        return response.json()
        
    except requests.exceptions.HTTPError as http_err:
        if response.status_code == 401:
            st.error("APIキーが無効です。APIキーを確認してください。")
        elif response.status_code == 404:
            st.error(f"指定した都市の天気データが見つかりませんでした。都市名のスペルを確認してください。")
        else:
            st.error(f"HTTPエラーが発生しました: {http_err}")
        return None
        
    except requests.exceptions.ConnectionError:
        st.error("サーバーに接続できませんでした。インターネット接続を確認してください。")
        return None
        
    except requests.exceptions.Timeout:
        st.error("リクエストがタイムアウトしました。後でもう一度お試しください。")
        return None
        
    except requests.exceptions.RequestException as e:
        st.error(f"リクエスト中にエラーが発生しました: {e}")
        return None
        
    except json.JSONDecodeError:
        st.error("サーバーからの応答を読み取れませんでした。")
        return None

# アプリのタイトルを設定
st.title("お天気情報アプリ")
st.write("世界中の都市の天気情報を簡単に確認できます!")

# サイドバーに入力フォームを配置
st.sidebar.header("検索条件")

# 都市名の入力欄
city_name = st.sidebar.text_input(
    "都市名を入力してください",
    placeholder="例:Tokyo、Osaka、Kyoto",
)

# 温度単位の選択(デフォルトは摂氏)
use_imperial = st.sidebar.checkbox("温度を華氏(°F)で表示する")

# 検索ボタン
search_button = st.sidebar.button("天気を検索")

# 検索ボタンが押されたときの処理
if search_button:
    if not city_name:  # 都市名が入力されていない場合
        st.warning("都市名を入力してください")
    else:
        # 処理中を表示
        with st.spinner("天気情報を取得中..."):
            # APIリクエスト用のURLを生成
            query_url = build_weather_query(city_name, use_imperial)
            
            # 天気データを取得
            weather_data = get_weather_data(query_url)
            
            # データが正常に取得できた場合のみ処理を続行
            if weather_data:
                # この時点では取得したデータを表示するだけ
                st.success(f"「{weather_data['name']}」の天気情報を取得しました!")
                
                # 取得したデータの詳細を表示(次のステップで改良します)
                st.subheader("取得したデータ")
                st.json(weather_data)

# デバッグ用:特殊な都市名のサンプル
st.sidebar.markdown("---")
st.sidebar.subheader("テスト用都市名")
st.sidebar.markdown("""
- 存在しない都市: `Nonexistent City`
- 特殊文字: `Münich`
- スペースを含む: `New York`
- 日本語: `東京`
""")

実行方法

前回と同様に、仮想環境を有効化した状態で以下のコマンドを実行すると、アプリを起動できます:

streamlit run app.py

このステップで学んだこと

この「Step3」では、以下のことを学びました:

  1. APIリクエストの実装requestsライブラリを使ってウェブAPIからデータを取得する方法
  2. 例外処理の重要性:APIリクエスト時に発生する可能性のある様々なエラーを適切に処理する方法
  3. ユーザーフレンドリーなエラーメッセージ:技術的なエラー内容をユーザーにわかりやすく伝える方法
  4. Streamlitの表示機能st.spinner()st.success()st.error()などを使った視覚的なフィードバック

次のステップでは、取得した天気データを美しく視覚的に表示するための方法を学びます。天気アイコンや温度表示など、より魅力的なユーザーインターフェースを構築していきましょう。

Step4: 天気情報の視覚的な表示とレイアウト最適化

このステップでは、取得した天気データから必要な情報を抽出し、Streamlitの機能を使って視覚的に魅力的な形で表示します。ユーザーが一目で天気情報を理解できるような、整理されたレイアウトを作成しましょう。

必要な天気情報の特定

前のステップでは、OpenWeather APIから取得した天気データをJSON形式でそのまま表示していました。しかし、ユーザーにとって本当に必要な情報はその一部だけです。APIレスポンスから以下の主要な情報を抽出しましょう:

  1. 都市名: weather_data['name']
  2. 国名: weather_data['sys']['country']
  3. 天気の状態: weather_data['weather'][0]['description']
  4. 天気アイコン: weather_data['weather'][0]['icon']
  5. 現在の気温: weather_data['main']['temp']
  6. 体感温度: weather_data['main']['feels_like']
  7. 最高/最低気温: weather_data['main']['temp_max']weather_data['main']['temp_min']
  8. 湿度: weather_data['main']['humidity']
  9. 風速: weather_data['wind']['speed']
  10. 気圧: weather_data['main']['pressure']

天気情報表示機能の実装

抽出した情報を整理して表示する関数を作成しましょう。app.pyに以下の関数を追加します:

def display_weather_info(weather_data, use_imperial=False):
    """取得した天気データを視覚的に表示する関数
    
    Args:
        weather_data (dict): OpenWeather APIからのレスポンス
        use_imperial (bool): 華氏表示を使用するかどうか
    """
    # 基本情報の抽出
    city_name = weather_data["name"]
    country = weather_data["sys"]["country"]
    weather_description = weather_data["weather"][0]["description"]
    weather_icon = weather_data["weather"][0]["icon"]
    temperature = weather_data["main"]["temp"]
    feels_like = weather_data["main"]["feels_like"]
    temp_min = weather_data["main"]["temp_min"]
    temp_max = weather_data["main"]["temp_max"]
    humidity = weather_data["main"]["humidity"]
    pressure = weather_data["main"]["pressure"]
    wind_speed = weather_data["wind"]["speed"]
    
    # 温度単位の設定
    temp_unit = "°F" if use_imperial else "°C"
    speed_unit = "mph" if use_imperial else "m/s"
    
    # アイコンのURL
    icon_url = f"http://openweathermap.org/img/wn/{weather_icon}@2x.png"
    
    # ヘッダー部分の表示
    col1, col2 = st.columns([1, 2])
    
    with col1:
        st.image(icon_url, width=120)
    
    with col2:
        st.title(f"{city_name}, {country}")
        st.write(f"**{weather_description.capitalize()}**")
        st.subheader(f"{temperature:.1f}{temp_unit}")
        st.write(f"体感温度: {feels_like:.1f}{temp_unit}")
    
    # 詳細情報の表示
    st.write("---")
    
    # 3つのカラムを作成
    col1, col2, col3 = st.columns(3)
    
    with col1:
        st.markdown("### 気温")
        st.write(f"**現在**: {temperature:.1f}{temp_unit}")
        st.write(f"**最高**: {temp_max:.1f}{temp_unit}")
        st.write(f"**最低**: {temp_min:.1f}{temp_unit}")
    
    with col2:
        st.markdown("### 湿度と気圧")
        st.write(f"**湿度**: {humidity}%")
        st.write(f"**気圧**: {pressure}hPa")
    
    with col3:
        st.markdown("### 風")
        st.write(f"**風速**: {wind_speed}{speed_unit}")
        
        # 風向きがある場合は表示(オプション)
        if "deg" in weather_data["wind"]:
            wind_deg = weather_data["wind"]["deg"]
            st.write(f"**風向き**: {wind_deg}°")

この関数は、以下のような処理を行います:

  1. 天気データから必要な情報を抽出
  2. 温度単位と速度単位をユーザーの選択に基づいて設定
  3. 天気アイコンのURLを生成
  4. Streamlitのカラム機能を使って、情報を複数列に分けて表示
  5. 情報をカテゴリごとに整理して表示

天気表示の強化:追加の視覚要素

天気情報をより魅力的に表示するため、いくつかの視覚的な要素を追加しましょう。

日の出・日の入り時刻の表示

太陽の動きに関する情報も表示してみましょう:

def display_weather_info(weather_data, use_imperial=False):
    # 前述のコード...
    
    # 日の出・日の入りの時刻を取得
    sunrise_timestamp = weather_data["sys"]["sunrise"]
    sunset_timestamp = weather_data["sys"]["sunset"]
    
    # タイムゾーンオフセット(秒)
    timezone_offset = weather_data["timezone"]
    
    # 現地時間に変換
    sunrise_time = datetime.datetime.utcfromtimestamp(sunrise_timestamp + timezone_offset)
    sunset_time = datetime.datetime.utcfromtimestamp(sunset_timestamp + timezone_offset)
    
    # 時刻のフォーマット(時:分)
    sunrise_str = sunrise_time.strftime("%H:%M")
    sunset_str = sunset_time.strftime("%H:%M")
    
    # 表示部分に追加
    st.write("---")
    st.markdown("### 日の出・日の入り")
    col1, col2 = st.columns(2)
    
    with col1:
        st.write("🌅 **日の出**: " + sunrise_str)
    
    with col2:
        st.write("🌇 **日の入り**: " + sunset_str)

このコードを追加するには、datetimeモジュールをインポートする必要があります:

import datetime

天気状態に基づくカラーコーディング

天気の状態に応じて、表示カラーを変えてみましょう:

def get_weather_color(weather_id):
    """天気IDに基づいて表示色を決定する関数
    
    Args:
        weather_id (int): OpenWeather APIの天気ID
    
    Returns:
        str: 対応するカラーコード
    """
    # 天気IDの範囲に基づいて色を決定
    if weather_id < 300:  # 雷雨
        return "#9932CC"  # 暗い紫
    elif weather_id < 400:  # 霧雨
        return "#4682B4"  # スチールブルー
    elif weather_id < 600:  # 雨
        return "#4169E1"  # ロイヤルブルー
    elif weather_id < 700:  # 雪
        return "#B0E0E6"  # パウダーブルー
    elif weather_id < 800:  # 霧など
        return "#708090"  # スレートグレー
    elif weather_id == 800:  # 快晴
        return "#FF8C00"  # ダークオレンジ
    elif weather_id < 803:  # 晴れ(少し曇り)
        return "#FFD700"  # ゴールド
    else:  # 曇り
        return "#A9A9A9"  # ダークグレー

そして、この関数をdisplay_weather_info関数内で使用します:

def display_weather_info(weather_data, use_imperial=False):
    # 基本情報の抽出
    # 前述のコード...
    
    weather_id = weather_data["weather"][0]["id"]
    weather_color = get_weather_color(weather_id)
    
    # ヘッダー部分の表示
    col1, col2 = st.columns([1, 2])
    
    with col1:
        st.image(icon_url, width=120)
    
    with col2:
        st.title(f"{city_name}, {country}")
        st.markdown(f"<h3 style='color:{weather_color}'>{weather_description.capitalize()}</h3>", unsafe_allow_html=True)
        st.subheader(f"{temperature:.1f}{temp_unit}")
        st.write(f"体感温度: {feels_like:.1f}{temp_unit}")
    
    # 以下、同様...

これで、天気の状態に応じた色でテキストが表示されるようになります。

メイン処理の更新

最後に、メイン処理部分を更新して、display_weather_info関数を呼び出すようにします:

# 検索ボタンが押されたときの処理
if search_button:
    if not city_name:  # 都市名が入力されていない場合
        st.warning("都市名を入力してください")
    else:
        # 処理中を表示
        with st.spinner("天気情報を取得中..."):
            # APIリクエスト用のURLを生成
            query_url = build_weather_query(city_name, use_imperial)
            
            # 天気データを取得
            weather_data = get_weather_data(query_url)
            
            # データが正常に取得できた場合のみ処理を続行
            if weather_data:
                # 天気情報を表示
                display_weather_info(weather_data, use_imperial)
                
                # 詳細データを折りたたみ表示(開発者向け)
                with st.expander("詳細なAPI応答データ"):
                    st.json(weather_data)

これで、検索ボタンが押されて天気データが正常に取得できた場合、整理された視覚的な表示が行われるようになります。また、詳細なAPIレスポンスデータは折りたたみ式の「詳細なAPI応答データ」セクションに格納されるので、必要な場合にのみ展開して確認できます。

完全なコード例

以上の変更を含めた、app.pyの完全なコードは以下のようになります(インポート部分の更新を含む)
# app.py

import streamlit as st
import json
import requests
import datetime
from configparser import ConfigParser
from urllib import parse

# APIのベースURLを定数として定義
WEATHER_API_URL = "https://api.openweathermap.org/data/2.5/weather"

def get_api_key():
    """設定ファイルからOpenWeather APIキーを取得する関数
    
    設定ファイル「config.ini」に以下の構造で保存されていることを想定:
    
        [openweather]
        api_key=あなたのAPIキー
        
    Returns:
        str: OpenWeather APIキー
    """
    config = ConfigParser()
    config.read("config.ini")
    return config["openweather"]["api_key"]

def build_weather_query(city_name, use_imperial=False):
    """OpenWeather APIを呼び出すためのURLを生成する関数
    
    Args:
        city_name (str): 検索する都市の名前
        use_imperial (bool): 華氏表示にするかどうか(デフォルトはFalse=摂氏)
    
    Returns:
        str: APIを呼び出すための完全なURL
    """
    api_key = get_api_key()
    
    # URLエンコードして特殊文字や空白を適切に処理
    encoded_city_name = parse.quote_plus(city_name)
    
    # 温度単位を設定(デフォルトは摂氏=metric)
    units = "imperial" if use_imperial else "metric"
    
    # URLを構築
    url = (
        f"{WEATHER_API_URL}?q={encoded_city_name}"
        f"&units={units}&appid={api_key}"
    )
    
    return url

def get_weather_data(query_url):
    """OpenWeather APIにリクエストを送信し、天気データを取得する関数
    
    Args:
        query_url (str): OpenWeather APIの完全なURL
        
    Returns:
        dict: 特定の都市の天気情報を含む辞書
        None: エラーが発生した場合
    """
    try:
        response = requests.get(query_url)
        
        # レスポンスのステータスコードをチェック
        response.raise_for_status()
        
        # JSONデータを辞書に変換して返す
        return response.json()
        
    except requests.exceptions.HTTPError as http_err:
        if response.status_code == 401:
            st.error("APIキーが無効です。APIキーを確認してください。")
        elif response.status_code == 404:
            st.error(f"指定した都市の天気データが見つかりませんでした。都市名のスペルを確認してください。")
        else:
            st.error(f"HTTPエラーが発生しました: {http_err}")
        return None
        
    except requests.exceptions.ConnectionError:
        st.error("サーバーに接続できませんでした。インターネット接続を確認してください。")
        return None
        
    except requests.exceptions.Timeout:
        st.error("リクエストがタイムアウトしました。後でもう一度お試しください。")
        return None
        
    except requests.exceptions.RequestException as e:
        st.error(f"リクエスト中にエラーが発生しました: {e}")
        return None
        
    except json.JSONDecodeError:
        st.error("サーバーからの応答を読み取れませんでした。")
        return None

def get_weather_color(weather_id):
    """天気IDに基づいて表示色を決定する関数
    
    Args:
        weather_id (int): OpenWeather APIの天気ID
    
    Returns:
        str: 対応するカラーコード
    """
    if weather_id < 300:  # 雷雨
        return "#9932CC"  # 暗い紫
    elif weather_id < 400:  # 霧雨
        return "#4682B4"  # スチールブルー
    elif weather_id < 600:  # 雨
        return "#4169E1"  # ロイヤルブルー
    elif weather_id < 700:  # 雪
        return "#B0E0E6"  # パウダーブルー
    elif weather_id < 800:  # 霧など
        return "#708090"  # スレートグレー
    elif weather_id == 800:  # 快晴
        return "#FF8C00"  # ダークオレンジ
    elif weather_id < 803:  # 晴れ(少し曇り)
        return "#FFD700"  # ゴールド
    else:  # 曇り
        return "#A9A9A9"  # ダークグレー

def display_weather_info(weather_data, use_imperial=False):
    """取得した天気データを視覚的に表示する関数
    
    Args:
        weather_data (dict): OpenWeather APIからのレスポンス
        use_imperial (bool): 華氏表示を使用するかどうか
    """
    # 基本情報の抽出
    city_name = weather_data["name"]
    country = weather_data["sys"]["country"]
    weather_description = weather_data["weather"][0]["description"]
    weather_icon = weather_data["weather"][0]["icon"]
    weather_id = weather_data["weather"][0]["id"]
    temperature = weather_data["main"]["temp"]
    feels_like = weather_data["main"]["feels_like"]
    temp_min = weather_data["main"]["temp_min"]
    temp_max = weather_data["main"]["temp_max"]
    humidity = weather_data["main"]["humidity"]
    pressure = weather_data["main"]["pressure"]
    wind_speed = weather_data["wind"]["speed"]
    
    # 天気の状態に基づく色を取得
    weather_color = get_weather_color(weather_id)
    
    # 温度単位の設定
    temp_unit = "°F" if use_imperial else "°C"
    speed_unit = "mph" if use_imperial else "m/s"
    
    # アイコンのURL
    icon_url = f"http://openweathermap.org/img/wn/{weather_icon}@2x.png"
    
    # ヘッダー部分の表示
    col1, col2 = st.columns([1, 2])
    
    with col1:
        st.image(icon_url, width=120)
    
    with col2:
        st.title(f"{city_name}, {country}")
        st.markdown(f"<h3 style='color:{weather_color}'>{weather_description.capitalize()}</h3>", unsafe_allow_html=True)
        st.subheader(f"{temperature:.1f}{temp_unit}")
        st.write(f"体感温度: {feels_like:.1f}{temp_unit}")
    
    # 詳細情報の表示
    st.write("---")
    
    # 3つのカラムを作成
    col1, col2, col3 = st.columns(3)
    
    with col1:
        st.markdown("### 気温")
        st.write(f"**現在**: {temperature:.1f}{temp_unit}")
        st.write(f"**最高**: {temp_max:.1f}{temp_unit}")
        st.write(f"**最低**: {temp_min:.1f}{temp_unit}")
    
    with col2:
        st.markdown("### 湿度と気圧")
        st.write(f"**湿度**: {humidity}%")
        st.write(f"**気圧**: {pressure}hPa")
    
    with col3:
        st.markdown("### 風")
        st.write(f"**風速**: {wind_speed}{speed_unit}")
        
        # 風向きがある場合は表示
        if "deg" in weather_data["wind"]:
            wind_deg = weather_data["wind"]["deg"]
            st.write(f"**風向き**: {wind_deg}°")
    
    # 日の出・日の入りの時刻を取得
    if "sunrise" in weather_data["sys"] and "sunset" in weather_data["sys"]:
        sunrise_timestamp = weather_data["sys"]["sunrise"]
        sunset_timestamp = weather_data["sys"]["sunset"]
        
        # タイムゾーンオフセット(秒)
        timezone_offset = weather_data["timezone"]
        
        # 現地時間に変換
        sunrise_time = datetime.datetime.utcfromtimestamp(sunrise_timestamp + timezone_offset)
        sunset_time = datetime.datetime.utcfromtimestamp(sunset_timestamp + timezone_offset)
        
        # 時刻のフォーマット(時:分)
        sunrise_str = sunrise_time.strftime("%H:%M")
        sunset_str = sunset_time.strftime("%H:%M")
        
        # 表示部分に追加
        st.write("---")
        st.markdown("### 日の出・日の入り")
        col1, col2 = st.columns(2)
        
        with col1:
            st.write("🌅 **日の出**: " + sunrise_str)
        
        with col2:
            st.write("🌇 **日の入り**: " + sunset_str)

# アプリのタイトルを設定
st.title("お天気情報アプリ")
st.write("世界中の都市の天気情報を簡単に確認できます!")

# サイドバーに入力フォームを配置
st.sidebar.header("検索条件")

# 都市名の入力欄
city_name = st.sidebar.text_input(
    "都市名を入力してください",
    placeholder="例:Tokyo、Osaka、Kyoto",
)

# 温度単位の選択(デフォルトは摂氏)
use_imperial = st.sidebar.checkbox("温度を華氏(°F)で表示する")

# 検索ボタン
search_button = st.sidebar.button("天気を検索")

# 検索ボタンが押されたときの処理
if search_button:
    if not city_name:  # 都市名が入力されていない場合
        st.warning("都市名を入力してください")
    else:
        # 処理中を表示
        with st.spinner("天気情報を取得中..."):
            # APIリクエスト用のURLを生成
            query_url = build_weather_query(city_name, use_imperial)
            
            # 天気データを取得
            weather_data = get_weather_data(query_url)
            
            # データが正常に取得できた場合のみ処理を続行
            if weather_data:
                # 天気情報を表示
                display_weather_info(weather_data, use_imperial)
                
                # 詳細データを折りたたみ表示(開発者向け)
                with st.expander("詳細なAPI応答データ"):
                    st.json(weather_data)

# デバッグ用:特殊な都市名のサンプル
st.sidebar.markdown("---")
st.sidebar.subheader("テスト用都市名")
st.sidebar.markdown("""
- 存在しない都市: `Nonexistent City`
- 特殊文字: `Münich`
- スペースを含む: `New York`
- 日本語: `東京`
""")

このステップで学んだこと

この「Step4」では、以下のことを学びました:

  1. Streamlitのレイアウト機能:カラム、セクション分け、拡張パネルなどを使った整理されたUI設計
  2. データの整理と視覚化:必要な情報の抽出と視覚的に魅力的な表示方法
  3. カスタムスタイリング:天気の状態に応じた色付けなど、条件に基づくスタイリング
  4. 日時データの処理:タイムスタンプの変換とフォーマット
  5. エモジを活用した直感的なUI:日の出や日の入りの表示に絵文字を使用

次のステップでは、アプリをさらに拡張して、お気に入りの都市の保存機能や天気の傾向グラフの表示などの高度な機能を追加していきます。

Step5: アプリの最終調整と日本語対応

このステップでは、天気情報アプリの最終調整を行います。特に日本語対応を実装し、天気情報をより日本の文化に合わせた表示にするとともに、天気状態に応じたアイコン表示や色分けを改良します。さらに、タイムゾーンを「Asia/Tokyo」に固定して日本向けのアプリとして完成させます。

日本語表示の対応

OpenWeather APIは、langパラメータを使って言語設定を変更できます。日本語を指定するには、パラメータにjaを設定します。APIリクエストを生成する関数を修正しましょう:

def build_weather_query(city_name, use_imperial=False):
    """OpenWeather APIを呼び出すためのURLを生成する関数
    
    Args:
        city_name (str): 検索する都市の名前
        use_imperial (bool): 華氏表示にするかどうか(デフォルトはFalse=摂氏)
    
    Returns:
        str: APIを呼び出すための完全なURL
    """
    api_key = get_api_key()
    
    # URLエンコードして特殊文字や空白を適切に処理
    encoded_city_name = parse.quote_plus(city_name)
    
    # 温度単位を設定(デフォルトは摂氏=metric)
    units = "imperial" if use_imperial else "metric"
    
    # URLを構築(日本語対応のために言語パラメータを追加)
    url = (
        f"{WEATHER_API_URL}?q={encoded_city_name}"
        f"&units={units}&lang=ja&appid={api_key}"
    )
    
    return url

これにより、天気の説明が日本語で返されるようになります。

日本の天気表現に適した表示の実装

日本の天気予報では、海外の表現とは異なる独自の表現方法があります。特に、OpenWeatherのdescriptionは直訳で違和感があることが多いため、mainを使って主要な天気区分だけを日本語で表示するようにしましょう。

まず、天気のmainに対応する日本語表現を定義します:

# OpenWeatherの主要天気区分に対応する日本語表現
WEATHER_TRANSLATIONS = {
    "Clear": "快晴",
    "Clouds": "曇り",
    "Rain": "雨",
    "Drizzle": "小雨",
    "Thunderstorm": "雷雨",
    "Snow": "雪",
    "Mist": "霧",
    "Smoke": "煙霧",
    "Haze": "もや",
    "Dust": "砂塵",
    "Fog": "濃霧",
    "Sand": "砂",
    "Ash": "火山灰",
    "Squall": "スコール",
    "Tornado": "竜巻"
}

# 47都道府県の県庁所在地:日本語から英語への変換辞書
CITY_TRANSLATIONS = {
    "東京": "Tokyo",           # 東京都
    "札幌": "Sapporo",         # 北海道
    "青森": "Aomori",          # 青森県
    "盛岡": "Morioka",         # 岩手県
    "仙台": "Sendai",          # 宮城県
    "秋田": "Akita",           # 秋田県
    "山形": "Yamagata",        # 山形県
    "福島": "Fukushima",       # 福島県
    "水戸": "Mito",            # 茨城県
    "宇都宮": "Utsunomiya",    # 栃木県
    "前橋": "Maebashi",        # 群馬県
    "さいたま": "Saitama",     # 埼玉県
    "千葉": "Chiba",           # 千葉県
    "横浜": "Yokohama",        # 神奈川県
    "新潟": "Niigata",         # 新潟県
    "富山": "Toyama",          # 富山県
    "金沢": "Kanazawa",        # 石川県
    "福井": "Fukui",           # 福井県
    "甲府": "Kofu",            # 山梨県
    "長野": "Nagano",          # 長野県
    "岐阜": "Gifu",            # 岐阜県
    "静岡": "Shizuoka",        # 静岡県
    "名古屋": "Nagoya",        # 愛知県
    "津": "Tsu",               # 三重県
    "大津": "Otsu",            # 滋賀県
    "京都": "Kyoto",           # 京都府
    "大阪": "Osaka",           # 大阪府
    "神戸": "Kobe",            # 兵庫県
    "奈良": "Nara",            # 奈良県
    "和歌山": "Wakayama",      # 和歌山県
    "鳥取": "Tottori",         # 鳥取県
    "松江": "Matsue",          # 島根県
    "岡山": "Okayama",         # 岡山県
    "広島": "Hiroshima",       # 広島県
    "山口": "Yamaguchi",       # 山口県
    "徳島": "Tokushima",       # 徳島県
    "高松": "Takamatsu",       # 香川県
    "松山": "Matsuyama",       # 愛媛県
    "高知": "Kochi",           # 高知県
    "福岡": "Fukuoka",         # 福岡県
    "佐賀": "Saga",            # 佐賀県
    "長崎": "Nagasaki",        # 長崎県
    "熊本": "Kumamoto",        # 熊本県
    "大分": "Oita",            # 大分県
    "宮崎": "Miyazaki",        # 宮崎県
    "鹿児島": "Kagoshima",     # 鹿児島県
    "那覇": "Naha"             # 沖縄県
}

# 主要都市も追加(ここに適宜追加してください)
CITY_TRANSLATIONS.update({
    "横須賀": "Yokosuka",
    "川崎": "Kawasaki",
    "浦安": "Urayasu",
    "函館": "Hakodate",
    "旭川": "Asahikawa",
    "釧路": "Kushiro",
    "八戸": "Hachinohe",
    "いわき": "Iwaki",
    "郡山": "Koriyama",
    "足利": "Ashikaga",
    "宇都宮": "Utsunomiya",
    "川越": "Kawagoe",
    "浜松": "Hamamatsu",
    "豊橋": "Toyohashi",
    "豊田": "Toyota",
    "四日市": "Yokkaichi",
    "姫路": "Himeji",
    "尼崎": "Amagasaki",
    "西宮": "Nishinomiya",
    "倉敷": "Kurashiki",
    "北九州": "Kitakyushu",
    "久留米": "Kurume",
    "別府": "Beppu"
})

次に、都市名変換関数を作成します:

def translate_city_to_english(jp_city_name):
    """日本語の都市名を英語に変換する関数
    
    Args:
        jp_city_name (str): 日本語の都市名
        
    Returns:
        str: 英語の都市名(辞書にない場合は入力をそのまま返す)
    """
    return CITY_TRANSLATIONS.get(jp_city_name, jp_city_name)

次に、天気情報表示関数を修正して、これらの日本語表現を使用するようにします:

def display_weather_info(weather_data, use_imperial=False):
    # 基本情報の抽出
    city_name = weather_data["name"]
    country = weather_data["sys"]["country"]
    
    # 天気情報
    weather_main = weather_data["weather"][0]["main"]
    weather_icon = weather_data["weather"][0]["icon"]
    weather_id = weather_data["weather"][0]["id"]
    
    # 日本語の天気表現に変換
    weather_jp = WEATHER_TRANSLATIONS.get(weather_main, weather_main)
    
    # その他のデータ
    temperature = weather_data["main"]["temp"]
    feels_like = weather_data["main"]["feels_like"]
    temp_min = weather_data["main"]["temp_min"]
    temp_max = weather_data["main"]["temp_max"]
    humidity = weather_data["main"]["humidity"]
    pressure = weather_data["main"]["pressure"]
    wind_speed = weather_data["wind"]["speed"]
    
    # 以下の表示処理は変わらず...
    # ...

タイムゾーンの設定

日本のタイムゾーン(Asia/Tokyo)を基準とした時刻表示を行うために、日の出・日の入り時刻の処理を修正します:

# 日の出・日の入りの時刻を取得する部分
if "sunrise" in weather_data["sys"] and "sunset" in weather_data["sys"]:
    sunrise_timestamp = weather_data["sys"]["sunrise"]
    sunset_timestamp = weather_data["sys"]["sunset"]
    
    # 常に日本時間(JST)で表示
    jst_offset = 32400  # UTC+9時間(秒単位)
    
    # 日本時間に変換
    sunrise_time = datetime.datetime.utcfromtimestamp(sunrise_timestamp + jst_offset)
    sunset_time = datetime.datetime.utcfromtimestamp(sunset_timestamp + jst_offset)
    
    # 時刻のフォーマット(時:分)
    sunrise_str = sunrise_time.strftime("%H:%M")
    sunset_str = sunset_time.strftime("%H:%M")
    
    # 表示部分
    st.write("---")
    st.markdown("### 日の出・日の入り(日本時間)")
    col1, col2 = st.columns(2)
    
    with col1:
        st.write("🌅 **日の出**: " + sunrise_str)
    
    with col2:
        st.write("🌇 **日の入り**: " + sunset_str)

天気アイコンと色の調整

天気状態に応じたアイコンと色を、よりわかりやすく調整します。日本の気象表現に合わせて、以下のように修正します:

def get_weather_emoji(weather_id):
    """天気IDに基づいて絵文字を決定する関数
    
    Args:
        weather_id (int): 天気ID
    
    Returns:
        str: 対応する絵文字
    """
    if weather_id < 300:  # 雷雨
        return "⚡"
    elif weather_id < 400:  # 霧雨
        return "🌂"
    elif weather_id < 600:  # 雨
        return "☔"
    elif weather_id < 700:  # 雪
        return "❄️"
    elif weather_id < 800:  # 霧など
        return "🌫️"
    elif weather_id == 800:  # 快晴
        return "☀️"
    elif weather_id < 803:  # 晴れ(少し曇り)
        return "🌤️"
    else:  # 曇り
        return "☁️"

これらの関数を使って、天気の表示をカスタマイズします。

天気情報表示の最終調整

最後に、これまでの変更を統合し、日本向けの天気情報表示を完成させます:

def display_weather_info(weather_data, use_imperial=False):
    """取得した天気データを視覚的に表示する関数(日本語対応版)
    
    Args:
        weather_data (dict): OpenWeather APIからのレスポンス
        use_imperial (bool): 華氏表示を使用するかどうか
    """
    # 基本情報の抽出
    city_name = weather_data["name"]
    country = weather_data["sys"]["country"]
    
    # 天気情報
    weather_main = weather_data["weather"][0]["main"]
    weather_icon = weather_data["weather"][0]["icon"]
    weather_id = weather_data["weather"][0]["id"]
    
    # 日本語の天気表現に変換
    weather_jp = WEATHER_TRANSLATIONS.get(weather_main, weather_main)
    
    # 絵文字と色を取得
    emoji = get_weather_emoji(weather_id)
    weather_color = get_weather_color(weather_id)
    
    # その他のデータ
    temperature = weather_data["main"]["temp"]
    feels_like = weather_data["main"]["feels_like"]
    temp_min = weather_data["main"]["temp_min"]
    temp_max = weather_data["main"]["temp_max"]
    humidity = weather_data["main"]["humidity"]
    pressure = weather_data["main"]["pressure"]
    wind_speed = weather_data["wind"]["speed"]
    
    # 温度単位の設定
    temp_unit = "°F" if use_imperial else "°C"
    speed_unit = "mph" if use_imperial else "m/s"
    
    # アイコンのURL
    icon_url = f"http://openweathermap.org/img/wn/{weather_icon}@2x.png"
    
    # ヘッダー部分の表示
    col1, col2 = st.columns([1, 2])
    
    with col1:
        st.image(icon_url, width=120)
    
    with col2:
        st.title(f"{city_name}, {country}")
        st.markdown(f"<h2 style='color:{weather_color}'>{emoji} {weather_jp}</h2>", unsafe_allow_html=True)
        st.subheader(f"{temperature:.1f}{temp_unit}")
        st.write(f"体感温度: {feels_like:.1f}{temp_unit}")
    
    # 詳細情報の表示
    st.write("---")
    
    # 3つのカラムを作成
    col1, col2, col3 = st.columns(3)
    
    with col1:
        st.markdown("### 気温")
        st.write(f"**現在**: {temperature:.1f}{temp_unit}")
        st.write(f"**最高**: {temp_max:.1f}{temp_unit}")
        st.write(f"**最低**: {temp_min:.1f}{temp_unit}")
    
    with col2:
        st.markdown("### 湿度と気圧")
        st.write(f"**湿度**: {humidity}%")
        st.write(f"**気圧**: {pressure}hPa")
    
    with col3:
        st.markdown("### 風")
        st.write(f"**風速**: {wind_speed}{speed_unit}")
        
        # 風向きがある場合は表示
        if "deg" in weather_data["wind"]:
            wind_deg = weather_data["wind"]["deg"]
            # 風向きを方角に変換
            directions = ["北", "北北東", "北東", "東北東", "東", "東南東", "南東", "南南東", 
                          "南", "南南西", "南西", "西南西", "西", "西北西", "北西", "北北西"]
            index = round(wind_deg / 22.5) % 16
            wind_direction = directions[index]
            st.write(f"**風向き**: {wind_direction}({wind_deg}°)")

風向きを角度から日本語の方角(「北」、「北東」など)に変換する機能も追加しました。これにより、より直感的に風向きを理解できます。

地域特化の追加機能:日本の主要都市リスト

日本のユーザーがよく検索する都市をクイック選択できるようにするため、サイドバーに日本の主要都市リストを追加します:

# サイドバーに入力フォームを配置
st.sidebar.header("検索条件")

# 検索方法の選択
search_method = st.sidebar.radio(
    "検索方法を選択",
    ["都市名を入力", "日本の主要都市から選択"]
)

if search_method == "都市名を入力":
    # 都市名の入力欄
    city_name = st.sidebar.text_input(
        "都市名を入力してください",
        placeholder="例:東京、大阪、京都",
    )
else:
    # 日本の主要都市リスト
    jp_cities = [
        "東京", "大阪", "名古屋", "札幌", "福岡", "京都", "神戸", 
        "横浜", "広島", "仙台", "那覇", "金沢", "新潟", "松山", "鹿児島"
    ]
    selected_jp_city = st.sidebar.selectbox("都市を選択", jp_cities)
    # 選択された日本語の都市名を英語に変換
    city_name = translate_city_to_english(selected_jp_city)

# 温度単位の選択(デフォルトは摂氏)
use_imperial = st.sidebar.checkbox("温度を華氏(°F)で表示する")

# 検索ボタン
search_button = st.sidebar.button("天気を検索")

ページのタイトルとカスタマイズ

最後に、アプリの見た目をよりプロフェッショナルにするため、ページのタイトルやファビコンなどを設定します:

# ページ設定
st.set_page_config(
    page_title="日本の天気情報アプリ",
    page_icon="🌦️",
    layout="wide",
    initial_sidebar_state="expanded"
)

# アプリのタイトルを設定
st.title("🗾 日本の天気情報アプリ")
st.write("日本や世界中の都市の天気情報を簡単に確認できます!")

# フッター
st.markdown("---")
st.markdown(
    """
    <div style="text-align: center; color: gray; font-size: small;">
        OpenWeather APIを利用した天気情報アプリ | 
        データ更新:10分ごと | 
        © 2025 天気情報アプリ
    </div>
    """, 
    unsafe_allow_html=True
)

メイン処理の更新

リクエスト処理部分を更新して、日本語対応を適用します:

# 検索ボタンが押されたときの処理
if search_button:
    if not city_name:  # 都市名が入力されていない場合
        st.warning("都市名を入力してください")
    else:
        # 処理中を表示
        with st.spinner("天気情報を取得中..."):
            # APIリクエスト用のURLを生成
            query_url = build_weather_query(city_name, use_imperial)
            
            # 天気データを取得
            weather_data = get_weather_data(query_url)
            
            # データが正常に取得できた場合のみ処理を続行
            if weather_data:
                # 天気情報を表示
                display_weather_info(weather_data, use_imperial)
                
                # 詳細データを折りたたみ表示(開発者向け)
                with st.expander("詳細なAPI応答データ"):
                    st.json(weather_data)

最終的なアプリの完成

最終的なコード
# app.py

import streamlit as st
import json
import requests
import datetime
from configparser import ConfigParser
from urllib import parse

# ページ設定
st.set_page_config(
    page_title="日本の天気情報アプリ",
    page_icon="🌦️",
    layout="wide",
    initial_sidebar_state="expanded"
)

# APIのベースURLを定数として定義
WEATHER_API_URL = "https://api.openweathermap.org/data/2.5/weather"

# OpenWeatherの主要天気区分に対応する日本語表現
WEATHER_TRANSLATIONS = {
    "Clear": "快晴",
    "Clouds": "曇り",
    "Rain": "雨",
    "Drizzle": "小雨",
    "Thunderstorm": "雷雨",
    "Snow": "雪",
    "Mist": "霧",
    "Smoke": "煙霧",
    "Haze": "もや",
    "Dust": "砂塵",
    "Fog": "濃霧",
    "Sand": "砂",
    "Ash": "火山灰",
    "Squall": "スコール",
    "Tornado": "竜巻"
}

# 47都道府県の県庁所在地:日本語から英語への変換辞書
CITY_TRANSLATIONS = {
    "東京": "Tokyo",           # 東京都
    "札幌": "Sapporo",         # 北海道
    "青森": "Aomori",          # 青森県
    "盛岡": "Morioka",         # 岩手県
    "仙台": "Sendai",          # 宮城県
    "秋田": "Akita",           # 秋田県
    "山形": "Yamagata",        # 山形県
    "福島": "Fukushima",       # 福島県
    "水戸": "Mito",            # 茨城県
    "宇都宮": "Utsunomiya",    # 栃木県
    "前橋": "Maebashi",        # 群馬県
    "さいたま": "Saitama",     # 埼玉県
    "千葉": "Chiba",           # 千葉県
    "横浜": "Yokohama",        # 神奈川県
    "新潟": "Niigata",         # 新潟県
    "富山": "Toyama",          # 富山県
    "金沢": "Kanazawa",        # 石川県
    "福井": "Fukui",           # 福井県
    "甲府": "Kofu",            # 山梨県
    "長野": "Nagano",          # 長野県
    "岐阜": "Gifu",            # 岐阜県
    "静岡": "Shizuoka",        # 静岡県
    "名古屋": "Nagoya",        # 愛知県
    "津": "Tsu",               # 三重県
    "大津": "Otsu",            # 滋賀県
    "京都": "Kyoto",           # 京都府
    "大阪": "Osaka",           # 大阪府
    "神戸": "Kobe",            # 兵庫県
    "奈良": "Nara",            # 奈良県
    "和歌山": "Wakayama",      # 和歌山県
    "鳥取": "Tottori",         # 鳥取県
    "松江": "Matsue",          # 島根県
    "岡山": "Okayama",         # 岡山県
    "広島": "Hiroshima",       # 広島県
    "山口": "Yamaguchi",       # 山口県
    "徳島": "Tokushima",       # 徳島県
    "高松": "Takamatsu",       # 香川県
    "松山": "Matsuyama",       # 愛媛県
    "高知": "Kochi",           # 高知県
    "福岡": "Fukuoka",         # 福岡県
    "佐賀": "Saga",            # 佐賀県
    "長崎": "Nagasaki",        # 長崎県
    "熊本": "Kumamoto",        # 熊本県
    "大分": "Oita",            # 大分県
    "宮崎": "Miyazaki",        # 宮崎県
    "鹿児島": "Kagoshima",     # 鹿児島県
    "那覇": "Naha"             # 沖縄県
}

# 主要都市も追加
CITY_TRANSLATIONS.update({
    "横須賀": "Yokosuka",
    "川崎": "Kawasaki",
    "浦安": "Urayasu",
    "函館": "Hakodate",
    "旭川": "Asahikawa",
    "釧路": "Kushiro",
    "八戸": "Hachinohe",
    "いわき": "Iwaki",
    "郡山": "Koriyama",
    "足利": "Ashikaga",
    "川越": "Kawagoe",
    "浜松": "Hamamatsu",
    "豊橋": "Toyohashi",
    "豊田": "Toyota",
    "四日市": "Yokkaichi",
    "姫路": "Himeji",
    "尼崎": "Amagasaki",
    "西宮": "Nishinomiya",
    "倉敷": "Kurashiki",
    "北九州": "Kitakyushu",
    "久留米": "Kurume",
    "別府": "Beppu"
})

def get_api_key():
    """設定ファイルからOpenWeather APIキーを取得する関数
    
    設定ファイル「config.ini」に以下の構造で保存されていることを想定:
    
        [openweather]
        api_key=あなたのAPIキー
        
    Returns:
        str: OpenWeather APIキー
    """
    config = ConfigParser()
    config.read("config.ini")
    return config["openweather"]["api_key"]

def translate_city_to_english(jp_city_name):
    """日本語の都市名を英語に変換する関数
    
    Args:
        jp_city_name (str): 日本語の都市名
        
    Returns:
        str: 英語の都市名(辞書にない場合は入力をそのまま返す)
    """
    return CITY_TRANSLATIONS.get(jp_city_name, jp_city_name)

def build_weather_query(city_name, use_imperial=False):
    """OpenWeather APIを呼び出すためのURLを生成する関数
    
    Args:
        city_name (str): 検索する都市の名前
        use_imperial (bool): 華氏表示にするかどうか(デフォルトはFalse=摂氏)
    
    Returns:
        str: APIを呼び出すための完全なURL
    """
    api_key = get_api_key()
    
    # URLエンコードして特殊文字や空白を適切に処理
    encoded_city_name = parse.quote_plus(city_name)
    
    # 温度単位を設定(デフォルトは摂氏=metric)
    units = "imperial" if use_imperial else "metric"
    
    # URLを構築(日本語対応のために言語パラメータを追加)
    url = (
        f"{WEATHER_API_URL}?q={encoded_city_name}"
        f"&units={units}&lang=ja&appid={api_key}"
    )
    
    return url

def get_weather_data(query_url):
    """OpenWeather APIにリクエストを送信し、天気データを取得する関数
    
    Args:
        query_url (str): OpenWeather APIの完全なURL
        
    Returns:
        dict: 特定の都市の天気情報を含む辞書
        None: エラーが発生した場合
    """
    try:
        response = requests.get(query_url)
        
        # レスポンスのステータスコードをチェック
        response.raise_for_status()
        
        # JSONデータを辞書に変換して返す
        return response.json()
        
    except requests.exceptions.HTTPError as http_err:
        if response.status_code == 401:
            st.error("APIキーが無効です。APIキーを確認してください。")
        elif response.status_code == 404:
            st.error(f"指定した都市の天気データが見つかりませんでした。都市名のスペルを確認してください。")
        else:
            st.error(f"HTTPエラーが発生しました: {http_err}")
        return None
        
    except requests.exceptions.ConnectionError:
        st.error("サーバーに接続できませんでした。インターネット接続を確認してください。")
        return None
        
    except requests.exceptions.Timeout:
        st.error("リクエストがタイムアウトしました。後でもう一度お試しください。")
        return None
        
    except requests.exceptions.RequestException as e:
        st.error(f"リクエスト中にエラーが発生しました: {e}")
        return None
        
    except json.JSONDecodeError:
        st.error("サーバーからの応答を読み取れませんでした。")
        return None

def get_weather_emoji(weather_id):
    """天気IDに基づいて絵文字を決定する関数
    
    Args:
        weather_id (int): 天気ID
    
    Returns:
        str: 対応する絵文字
    """
    if weather_id < 300:  # 雷雨
        return "⚡"
    elif weather_id < 400:  # 霧雨
        return "🌂"
    elif weather_id < 600:  # 雨
        return "☔"
    elif weather_id < 700:  # 雪
        return "❄️"
    elif weather_id < 800:  # 霧など
        return "🌫️"
    elif weather_id == 800:  # 快晴
        return "☀️"
    elif weather_id < 803:  # 晴れ(少し曇り)
        return "🌤️"
    else:  # 曇り
        return "☁️"

def get_weather_color(weather_id):
    """天気IDに基づいて表示色を決定する関数
    
    Args:
        weather_id (int): 天気ID
    
    Returns:
        str: 対応する色コード
    """
    if weather_id < 300:  # 雷雨
        return "#9932CC"  # 暗い紫
    elif weather_id < 400:  # 霧雨
        return "#4682B4"  # スチールブルー
    elif weather_id < 600:  # 雨
        return "#4169E1"  # ロイヤルブルー
    elif weather_id < 700:  # 雪
        return "#B0E0E6"  # パウダーブルー
    elif weather_id < 800:  # 霧など
        return "#708090"  # スレートグレー
    elif weather_id == 800:  # 快晴
        return "#FF8C00"  # ダークオレンジ
    elif weather_id < 803:  # 晴れ(少し曇り)
        return "#FFD700"  # ゴールド
    else:  # 曇り
        return "#A9A9A9"  # ダークグレー

def display_weather_info(weather_data, use_imperial=False):
    """取得した天気データを視覚的に表示する関数(日本語対応版)
    
    Args:
        weather_data (dict): OpenWeather APIからのレスポンス
        use_imperial (bool): 華氏表示を使用するかどうか
    """
    # 基本情報の抽出
    city_name = weather_data["name"]
    country = weather_data["sys"]["country"]
    
    # 天気情報
    weather_main = weather_data["weather"][0]["main"]
    weather_icon = weather_data["weather"][0]["icon"]
    weather_id = weather_data["weather"][0]["id"]
    
    # 日本語の天気表現に変換
    weather_jp = WEATHER_TRANSLATIONS.get(weather_main, weather_main)
    
    # 絵文字と色を取得
    emoji = get_weather_emoji(weather_id)
    weather_color = get_weather_color(weather_id)
    
    # その他のデータ
    temperature = weather_data["main"]["temp"]
    feels_like = weather_data["main"]["feels_like"]
    temp_min = weather_data["main"]["temp_min"]
    temp_max = weather_data["main"]["temp_max"]
    humidity = weather_data["main"]["humidity"]
    pressure = weather_data["main"]["pressure"]
    wind_speed = weather_data["wind"]["speed"]
    
    # 温度単位の設定
    temp_unit = "°F" if use_imperial else "°C"
    speed_unit = "mph" if use_imperial else "m/s"
    
    # アイコンのURL
    icon_url = f"http://openweathermap.org/img/wn/{weather_icon}@2x.png"
    
    # ヘッダー部分の表示
    col1, col2 = st.columns([1, 2])
    
    with col1:
        st.image(icon_url, width=120)
    
    with col2:
        st.title(f"{city_name}, {country}")
        st.markdown(f"<h2 style='color:{weather_color}'>{emoji} {weather_jp}</h2>", unsafe_allow_html=True)
        st.subheader(f"{temperature:.1f}{temp_unit}")
        st.write(f"体感温度: {feels_like:.1f}{temp_unit}")
    
    # 詳細情報の表示
    st.write("---")
    
    # 3つのカラムを作成
    col1, col2, col3 = st.columns(3)
    
    with col1:
        st.markdown("### 気温")
        st.write(f"**現在**: {temperature:.1f}{temp_unit}")
        st.write(f"**最高**: {temp_max:.1f}{temp_unit}")
        st.write(f"**最低**: {temp_min:.1f}{temp_unit}")
    
    with col2:
        st.markdown("### 湿度と気圧")
        st.write(f"**湿度**: {humidity}%")
        st.write(f"**気圧**: {pressure}hPa")
    
    with col3:
        st.markdown("### 風")
        st.write(f"**風速**: {wind_speed}{speed_unit}")
        
        # 風向きがある場合は表示
        if "deg" in weather_data["wind"]:
            wind_deg = weather_data["wind"]["deg"]
            # 風向きを方角に変換
            directions = ["北", "北北東", "北東", "東北東", "東", "東南東", "南東", "南南東", 
                          "南", "南南西", "南西", "西南西", "西", "西北西", "北西", "北北西"]
            index = round(wind_deg / 22.5) % 16
            wind_direction = directions[index]
            st.write(f"**風向き**: {wind_direction}({wind_deg}°)")
    
    # 日の出・日の入りの時刻を取得
    if "sunrise" in weather_data["sys"] and "sunset" in weather_data["sys"]:
        sunrise_timestamp = weather_data["sys"]["sunrise"]
        sunset_timestamp = weather_data["sys"]["sunset"]
        
        # 常に日本時間(JST)で表示
        jst_offset = 32400  # UTC+9時間(秒単位)
        
        # 日本時間に変換
        sunrise_time = datetime.datetime.utcfromtimestamp(sunrise_timestamp + jst_offset)
        sunset_time = datetime.datetime.utcfromtimestamp(sunset_timestamp + jst_offset)
        
        # 時刻のフォーマット(時:分)
        sunrise_str = sunrise_time.strftime("%H:%M")
        sunset_str = sunset_time.strftime("%H:%M")
        
        # 表示部分に追加
        st.write("---")
        st.markdown("### 日の出・日の入り(日本時間)")
        col1, col2 = st.columns(2)
        
        with col1:
            st.write("🌅 **日の出**: " + sunrise_str)
        
        with col2:
            st.write("🌇 **日の入り**: " + sunset_str)

# アプリのタイトルを設定
st.title("🗾 日本の天気情報アプリ")
st.write("日本や世界中の都市の天気情報を簡単に確認できます!")

# サイドバーに入力フォームを配置
st.sidebar.header("検索条件")

# 検索方法の選択
search_method = st.sidebar.radio(
    "検索方法を選択",
    ["都市名を入力", "日本の主要都市から選択"]
)

if search_method == "都市名を入力":
    # 都市名の入力欄
    city_name = st.sidebar.text_input(
        "都市名を入力してください",
        placeholder="例:東京、大阪、京都",
    )
    
    # 入力された都市名が日本語の場合は英語に変換
    if city_name:
        city_name = translate_city_to_english(city_name)
else:
    # 日本の主要都市リスト
    jp_cities = [
        "東京", "大阪", "名古屋", "札幌", "福岡", "京都", "神戸", 
        "横浜", "広島", "仙台", "那覇", "金沢", "新潟", "松山", "鹿児島"
    ]
    selected_jp_city = st.sidebar.selectbox("都市を選択", jp_cities)
    # 選択された日本語の都市名を英語に変換
    city_name = translate_city_to_english(selected_jp_city)

# 温度単位の選択(デフォルトは摂氏)
use_imperial = st.sidebar.checkbox("温度を華氏(°F)で表示する")

# 検索ボタン
search_button = st.sidebar.button("天気を検索")

# 検索ボタンが押されたときの処理
if search_button:
    if not city_name:  # 都市名が入力されていない場合
        st.warning("都市名を入力してください")
    else:
        # 処理中を表示
        with st.spinner("天気情報を取得中..."):
            # APIリクエスト用のURLを生成
            query_url = build_weather_query(city_name, use_imperial)
            
            # 天気データを取得
            weather_data = get_weather_data(query_url)
            
            # データが正常に取得できた場合のみ処理を続行
            if weather_data:
                # 天気情報を表示
                display_weather_info(weather_data, use_imperial)
                
                # 詳細データを折りたたみ表示(開発者向け)
                with st.expander("詳細なAPI応答データ"):
                    st.json(weather_data)

# フッター
st.markdown("---")
st.markdown(
    """
    <div style="text-align: center; color: gray; font-size: small;">
        OpenWeather APIを利用した天気情報アプリ | 
        データ更新:10分ごと | 
        © 2025 天気情報アプリ
    </div>
    """, 
    unsafe_allow_html=True
)

これでアプリの日本語対応と最終調整が完了しました。

完成したアプリは以下の特徴を持ちます:

  1. 日本語表示対応:天気情報を日本語で表示
  2. 日本の天気表現:日本の天気予報に適した表現方法を採用
  3. 日本時間表示:日の出・日の入り時刻を日本時間で表示
  4. 直感的な風向き表示:角度だけでなく、方角(北、南東など)も表示
  5. 日本の主要都市クイック選択:よく検索される日本の主要都市をすぐに選べる
  6. 視覚的な天気表示:天気状態に応じたアイコンと色で直感的に理解できる
  7. 日本語から英語への自動変換:日本語で都市を入力・選択しても適切に検索できる

このステップで学んだこと

この「Step5」では、以下のことを学びました:

  1. API言語設定のカスタマイズ:OpenWeather APIの言語パラメータを使った多言語対応
  2. 地域に適した表現方法の実装:日本の気象表現に合わせたカスタマイズ
  3. タイムゾーン処理:世界各地のデータを特定のタイムゾーンに合わせて表示する方法
  4. 風向きの直感的な表示:数値データを人間が理解しやすい形式に変換する方法
  5. ユーザー体験の向上:クイック選択リストや視覚的な情報表示による使いやすさの改善
  6. 言語間の変換処理:日本語の入力を英語に変換する辞書の活用

このアプリをさらに発展させる方法としては、以下のような機能が考えられます:

  • 複数日の天気予報表示
  • お気に入りの都市の保存機能
  • 天気に応じた服装のアドバイス
  • 地図上での天気表示
  • プッシュ通知機能(雨予報など)

お疲れさまでした!これで日本向け天気情報アプリの開発が完了しました。このプロジェクトで作成したアプリは、あなたのプログラミングスキルを示すポートフォリオとして活用できます。また、API連携や日本語対応など、実際の業務でも役立つ技術を身につけることができました。

まとめ

このプロジェクトでは、StreamlitとOpenWeather APIを使って、日本語対応の天気情報アプリを開発しました。初心者でも理解しやすいように段階的に進めることで、実践的なプログラミングスキルを身につけることができました。

学んだ技術とスキル

  1. API連携の基本
    • OpenWeather APIを使ったデータ取得方法
    • APIキーの安全な管理方法
    • URLパラメータの設定とリクエスト送信
  2. Streamlitの活用
    • インタラクティブなUI要素の実装
    • レイアウト設計とコンポーネント配置
    • データの視覚的な表示方法
  3. エラーハンドリング
    • 例外処理を使った堅牢なアプリ設計
    • ユーザーにわかりやすいエラーメッセージの表示
    • ネットワークエラーなどの対応方法
  4. データ処理と表示
    • JSONデータの解析と必要情報の抽出
    • 天気情報に応じた視覚的な表現の実装
    • 日時データの変換と表示
  5. 多言語対応
    • 日本語表示の実装方法
    • 日本の気象表現に合わせたカスタマイズ
    • 言語間の自動変換機能

作成したアプリの価値

このアプリは単なる学習プロジェクトを超えて、実用的な価値を持っています:

  • 日本語対応:国内ユーザーにとって使いやすいインターフェース
  • 視覚的な情報表示:天気状態を一目で理解できるデザイン
  • エラー耐性:不安定なネットワーク環境でも適切に動作
  • カスタマイズ性:各地域の特性に合わせた表示設定

次のステップ

このプロジェクトをベースに、さらに機能を拡張することができます:

  • 複数日の天気予報表示:先の予報も確認できるように拡張
  • お気に入り都市の保存機能:よく確認する都市を記録
  • 地図上での天気表示:地理的な視点から天気を確認
  • 天気変化の通知機能:急な天候変化を事前に知らせる
  • 過去の天気データの分析:気象傾向のグラフ表示

ポートフォリオとしての活用

このプロジェクトは、あなたのプログラミングスキルを示す優れたポートフォリオ作品になります。以下のポイントをアピールできます:

  • 実用的なアプリケーション開発能力
  • APIを活用したデータ連携スキル
  • ユーザー中心の設計思考
  • 日本語環境に特化したローカライゼーション能力
  • Pythonとモダンなフレームワークの活用スキル

ぜひこのプロジェクトをGitHubなどで公開し、あなたのプログラミングスキルをアピールしてください。また、ここで学んだ技術は、さまざまなデータ連携アプリケーションの開発に応用できます。

Streamlitとの組み合わせで、Pythonだけでこれだけ洗練されたWebアプリが作れることを実感できたのではないでしょうか。今後も新しい機能を追加してアプリを成長させていってください!

この記事が気に入ったら
フォローしてね!

この記事が参考になった方はシェアしてね!
  • URLをコピーしました!

本コンテンツへの意見や質問

コメントする

目次