【開発奮闘記】API制限、謎のエラー、標高差との戦い。Pythonで「自分だけの情報ダッシュボード」を自作した全記録

ALL

こんにちは、Tech Samuraiです!
ふとした思いつきから始まった、「自分だけの情報ダッシュボード」開発プロジェクト。愛用のNVIDIA Jetsonの上で、PythonとPySide6を使い、天気や株価がリアルタイムで更新されるカッコいいGUIアプリを作る。最初はそんな簡単な目標でした。

しかし、その道のりは想像以上に険しく、数々の「壁」が私の前に立ちはだかりました。これは、エラーと格闘し、試行錯誤を繰り返した、一人の開発者の奮闘の記録です。あなたの開発の旅の、小さな道標になれば幸いです。


第一の壁:沈黙するAPIキー「401 Unauthorized」

最初の機能は天気予報。有名な「OpenWeatherMap」を使えば楽勝だろうと高を括っていました。しかし、最初に私を襲ったのは`401 Unauthorized`エラー。APIキーは正しく入力したはずなのに、なぜ…?

原因と解決策:
答えは単純でしたが、初心者には罠でした。OpenWeatherMapのAPIキーは、発行されてから**有効になるまで最大2時間ほどのタイムラグ**があるのです。コードを何度見直しても解決しない問題の答えが、コードの外、つまり「時間」にあったとは。Webブラウザで直接APIのURLを叩くテスト方法を学び、辛抱強く待つことの重要性を知った最初の試練でした。


第二の壁:データが空っぽ?Alpha Vantageの静かなる抵抗

次に挑戦したのは株価表示。ここでも有名な「Alpha Vantage」を選びましたが、これが手強かった。APIキーはすぐに有効になったものの、返ってくるデータは常に`{‘Global Quote’: {}}`という空の状態。エラーメッセージすら出ない、まさに「静かなる抵抗」でした。

原因と解決策:
これもAPIの**無料プランが持つ「見えない壁」**でした。1分間に25回、1日で500回という厳しい呼び出し回数制限があり、開発中の度重なるテスト実行であっという間に上限に達していたのです。この経験から、代替案を探す決断をし、ユーザーコミュニティで人気の**`yfinance`ライブラリ**へと切り替えました。APIキー不要、厳しい制限もなし。この「ピボット(方針転換)」が、プロジェクトを大きく前進させました。


第三の壁:「対話モード」では動くのに… 実行タイミングの謎

`yfinance`は救世主に見えましたが、新たな謎が生まれます。Pythonの対話モードで一行ずつ実行すると株価が取れるのに、アプリとして一気に実行すると2つ目の銘柄だけ取得に失敗するのです。

原因と解決策:
これは、プログラムが優秀すぎたために起きた問題でした。スクリプトは、日経平均とトヨタの株価取得リクエストを人間には不可能な速さ(ミリ秒単位)で連続送信し、Yahoo Financeのサーバーがこれを機械的なアクセスと判断してブロックしていたのです。解決策は驚くほどシンプルでした。各リクエストの間に**`time.sleep(1)`**、つまり1秒間の「待ち」を入れること。これにより、アクセスが人間らしい間隔になり、データを安定して取得できるようになりました。


第四の壁:あったはずの関数がない `AttributeError`

順調に進んでいた開発も終盤、突然の`AttributeError`に見舞われました。「`create_stock_widget`なんて関数は存在しない」とPythonに怒られてしまったのです。

原因と解決策:
これは完全に私の凡ミスでした。コードをリファクタリングする過程で、関数の定義ブロックを丸ごと削除してしまっていたり、インデントがずれてクラスの一部として認識されていなかったりしたのです。エラーメッセージを正確に読み解き、コードの構造を丁寧に見直すこと。基本的なことですが、その大切さを改めて思い知らされました。


最後の発見:気圧が違う?「海面更正気圧」と標高の真実

ついにアプリは安定稼働。しかし、表示された気圧の値(1008hPa)が、実際の計測値(約970hPa)と大きく違うことに気づきました。

原因と解決策:
APIが提供していたのは、標高0mを基準とする**「海面更正気圧」**だったのです。私のいる場所(標高約350m)では、当然気圧は低くなります。この謎を探求した結果、現地の標高を使って「現地気圧」を概算で求めるという、もう一歩踏み込んだ実装にたどり着きました。ただデータを表示するだけでなく、そのデータの「意味」を理解することの面白さを知った瞬間でした。


完成したダッシュボードとコード

数々の壁を乗り越えて完成したのが、このリアルタイムダッシュボードです。

その全コードはこちらです。

import sys
import datetime
import time
import requests
import yfinance as yf
from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel)
from PySide6.QtCore import QTimer, Qt
from PySide6.QtGui import QFont

# --- 設定項目 ---
# TODO: このセクションにご自身の情報を入力してください
OPENWEATHER_API_KEY = "YOUR_OPENWEATHER_API_KEY"  # TODO: ご自身のOpenWeatherMap APIキーを入力
LATITUDE = 35.6812   # TODO: 表示したい場所の緯度 (例: 東京駅)
LONGITUDE = 139.7671 # TODO: 表示したい場所の経度 (例: 東京駅)

STOCK_SYMBOLS = {
    "日経平均連動ETF": "1321.T",
    "トヨタ自動車": "7203.T"
}

# --- スタイリング ---
class Style:
    MAIN_BG_COLOR = "#2c3e50"
    LABEL_COLOR = "#ecf0f1"
    VALUE_COLOR = "#ffffff"
    TITLE_FONT = QFont("Helvetica", 18, QFont.Bold)
    VALUE_FONT = QFont("Helvetica", 14, QFont.Bold)
    CLOCK_FONT = QFont("Helvetica", 48, QFont.Bold)
    WEATHER_ICON_FONT = QFont("Helvetica", 80, QFont.Bold)

# --- データ取得モジュール ---
def get_weather_data(api_key, lat, lon):
    """OpenWeatherMap APIから気象データを取得する"""
    url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={api_key}&units=metric&lang=ja"
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"気象データの取得に失敗しました: {e}")
        return None

def get_stock_data_yfinance(symbol):
    """yfinanceライブラリを使って株価データを取得する (休日耐性強化版)"""
    try:
        ticker = yf.Ticker(symbol)
        hist = ticker.history(period="5d", auto_adjust=False)

        if len(hist) < 2:
            print(f"銘柄 {symbol} の十分な履歴データが取得できませんでした。(取得件数: {len(hist)}件)")
            return None

        latest_data = hist.iloc[-1]
        previous_data = hist.iloc[-2]

        current_price = latest_data['Close']
        previous_close = previous_data['Close']
        change = current_price - previous_close
        
        return {
            "price": current_price,
            "change": change
        }
    except Exception as e:
        print(f"yfinanceで銘柄 {symbol} のデータ取得に失敗しました: {e}")
        return None

# --- メインウィンドウ ---
class DashboardApp(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("リアルタイムダッシュボード")
        self.setGeometry(100, 100, 800, 480)
        self.setStyleSheet(f"background-color: {Style.MAIN_BG_COLOR};")

        main_layout = QVBoxLayout(self)
        top_layout = QHBoxLayout()
        bottom_layout = QHBoxLayout() 
        main_layout.addLayout(top_layout, 2)
        main_layout.addLayout(bottom_layout, 3)

        self.clock_label = QLabel()
        self.clock_label.setAlignment(Qt.AlignCenter)
        self.clock_label.setFont(Style.CLOCK_FONT)
        self.clock_label.setStyleSheet(f"color: {Style.VALUE_COLOR};")
        top_layout.addWidget(self.clock_label)

        weather_widget = self.create_weather_widget()
        bottom_layout.addWidget(weather_widget)

        stocks_container_widget = QWidget()
        stocks_vertical_layout = QVBoxLayout(stocks_container_widget)

        self.stock_widgets = {}
        for name, symbol in STOCK_SYMBOLS.items():
            stock_widget, labels = self.create_stock_widget(name, symbol)
            self.stock_widgets[symbol] = labels
            stocks_vertical_layout.addWidget(stock_widget)

        bottom_layout.addWidget(stocks_container_widget)

        self.clock_timer = QTimer(self)
        self.clock_timer.timeout.connect(self.update_clock)
        self.clock_timer.start(1000)

        self.data_timer = QTimer(self)
        self.data_timer.timeout.connect(self.update_data)
        self.data_timer.start(1000 * 60 * 5)

        self.update_clock()
        self.update_data()

    def create_weather_widget(self):
        widget = QWidget()
        main_layout = QVBoxLayout(widget)
        
        self.weather_title = QLabel("現在の天気")
        self.weather_title.setFont(Style.TITLE_FONT)
        self.weather_title.setStyleSheet(f"color: {Style.LABEL_COLOR};")

        desc_icon_layout = QHBoxLayout()
        
        self.weather_desc = QLabel("---")
        self.weather_desc.setFont(Style.VALUE_FONT)
        self.weather_desc.setStyleSheet(f"color: {Style.VALUE_COLOR};")
        self.weather_desc.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        
        self.weather_icon = QLabel("")
        self.weather_icon.setFont(Style.WEATHER_ICON_FONT)
        self.weather_icon.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)

        desc_icon_layout.addWidget(self.weather_desc)
        desc_icon_layout.addWidget(self.weather_icon)
        
        self.temp_label = QLabel("気温: --.-- °C")
        self.temp_label.setFont(Style.VALUE_FONT)
        self.temp_label.setStyleSheet(f"color: {Style.VALUE_COLOR};")
        
        self.humidity_label = QLabel("湿度: -- %")
        self.humidity_label.setFont(Style.VALUE_FONT)
        self.humidity_label.setStyleSheet(f"color: {Style.VALUE_COLOR};")

        self.pressure_label = QLabel("気圧: -- hPa")
        self.pressure_label.setFont(Style.VALUE_FONT)
        self.pressure_label.setStyleSheet(f"color: {Style.VALUE_COLOR};")

        main_layout.addWidget(self.weather_title)
        main_layout.addLayout(desc_icon_layout)
        main_layout.addWidget(self.temp_label)
        main_layout.addWidget(self.humidity_label)
        main_layout.addWidget(self.pressure_label)
        main_layout.addStretch()
        return widget

    def create_stock_widget(self, name, symbol):
        widget = QWidget()
        layout = QVBoxLayout(widget)
        title = QLabel(name)
        title.setFont(Style.TITLE_FONT)
        title.setStyleSheet(f"color: {Style.LABEL_COLOR};")
        price_label = QLabel("価格: --")
        price_label.setFont(Style.VALUE_FONT)
        price_label.setStyleSheet(f"color: {Style.VALUE_COLOR};")
        change_label = QLabel("前日比: --")
        change_label.setFont(Style.VALUE_FONT)
        change_label.setStyleSheet(f"color: {Style.VALUE_COLOR};")
        layout.addWidget(title)
        layout.addWidget(price_label)
        layout.addWidget(change_label)
        layout.addStretch()
        labels = {"price": price_label, "change": change_label}
        return widget, labels

    def update_clock(self):
        self.clock_label.setText(datetime.datetime.now().strftime("%H:%M:%S"))

    def update_data(self):
        self.update_weather()
        self.update_all_stocks()

    def update_weather(self):
        weather_data = get_weather_data(OPENWEATHER_API_KEY, LATITUDE, LONGITUDE)
        if weather_data and "main" in weather_data and "weather" in weather_data:
            main_data = weather_data["main"]
            temp = main_data["temp"]
            humidity = main_data["humidity"]
            sea_level_pressure = main_data["pressure"]

            weather_info = weather_data["weather"][0]
            weather_main = weather_info["main"]
            description = weather_info["description"]
            
            # TODO: 現地気圧を計算する場合は、この場所の標高(m)を設定してください
            ELEVATION_METERS = 4 # (例: 東京駅の標高)
            station_pressure = sea_level_pressure - (ELEVATION_METERS / 10)

            self.weather_desc.setText(description.capitalize())
            self.temp_label.setText(f"気温: {temp:.2f} °C")
            self.humidity_label.setText(f"湿度: {humidity} %")
            self.pressure_label.setText(f"気圧: {station_pressure:.0f} hPa")

            icon_char = "❓"
            if weather_main == "Clear":
                icon_char = "☀️"
            elif weather_main == "Clouds":
                icon_char = "☁️"
            elif weather_main == "Drizzle":
                icon_char = "🌦️"
            elif weather_main == "Rain":
                if "heavy" in description:
                    icon_char = "⛈️"
                else:
                    icon_char = "🌧️"
            elif weather_main == "Snow":
                icon_char = "❄️"
            elif weather_main == "Thunderstorm":
                icon_char = "⚡️"
            
            self.weather_icon.setText(icon_char)
        else:
            self.weather_desc.setText("取得失敗")
            self.temp_label.setText("気温: 取得失敗")
            self.humidity_label.setText("湿度: 取得失敗")
            self.pressure_label.setText("気圧: 取得失敗")
            self.weather_icon.setText("❓")

    def update_all_stocks(self):
        for symbol, labels in self.stock_widgets.items():
            stock_data = get_stock_data_yfinance(symbol)
            if stock_data:
                price = stock_data["price"]
                change = stock_data["change"]
                
                labels["price"].setText(f"価格: ¥{price:,.0f}")
                
                change_text = f"前日比: {change:+.2f}"
                if change > 0:
                    labels["change"].setStyleSheet(f"color: #2ecc71;")
                elif change < 0:
                    labels["change"].setStyleSheet(f"color: #e74c3c;")
                else:
                    labels["change"].setStyleSheet(f"color: {Style.VALUE_COLOR};")
                
                labels["change"].setText(change_text)
            else:
                labels["price"].setText("価格: 取得失敗")
                labels["change"].setText("前日比: 取得失敗")
            
            time.sleep(1)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = DashboardApp()
    window.show()
    sys.exit(app.exec())

まとめ:エラーは最高の道標だった

完成したダッシュボードを眺めていると、これまでの苦労がすべて報われる気がします。今回の開発で学んだことは多いです。

  • APIには「有効化時間」や「見えない制限」がある。
  • ときにはツールを乗り換える「ピボット」の勇気も必要。
  • プログラムは「タイミング」が命。時には人間らしく待つことも大事。
  • エラーメッセージは解決への最大のヒント。焦らず、正確に読み解く。
  • データの「意味」を理解すると、もっと面白くなる。

もしあなたが今、エラーの沼で苦しんでいるなら、伝えたい。そのエラーは障害ではなく、あなたをより深い理解へと導いてくれる、最高の道標なのだと。さあ、次の開発を楽しみましょう!

コメント

タイトルとURLをコピーしました