main() blog

プログラムやゲーム、旅、愛する家族について綴っていきます。

【pyxel】Webから外部データを取得してみよう!

概要

『ムゲンヒメクリ』の日本の祝日に対応するために祝日の情報をウェブから取得して表示するようにしました。
ローカルにデータを用意して読み込ませるでも良かったのですが、
どうせなら公開されているデータを取得して反映させるようにすることで、 データの更新も不要になります。

内閣府の「国民の祝日」のデータは以下で公開されています。
こちらはcsvでデータがアップされている様です。

https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv

また、以下のGitHubにも日本の祝日のデータがアップされている様です。
holidays-jp.github.io

以下のURLでjsonデータで取得することができます。
https://holidays-jp.github.io/api/v1/date.json

公開されているデータはどちらも2年間のみのものとなっています。

jsonファイルの取得方法

標準ライブラリのurllib.requestもしくはrequestsライブラリを使用して指定したURLからデータを取得する形になります。
今回は標準ライブラリのurllib.requestを使用して実装してみたいと思います。

   # 日本の祝日のデータの取得.
    def fetch_jp_holidays(self):
        # urllibで試してみる.
        url = "https://holidays-jp.github.io/api/v1/date.json"

        try:
            import urllib.request
            with urllib.request.urlopen(url) as response:
                if response.status == 200:
                    import json
                    self._holidays = json.load(response)
                else:
                    print(f"サーバエラー: ステータスコード {response.status}")
                    pass
        except Exception as e:
            print(f"祝日のデータの取得に失敗: {e}")
            pass          

urllib.requests.urlopen()でURLを指定することでjsonデータを取得することができます。
response.statusの値でデータ取得の成功、失敗の判定が行えます。
200が返ってくると成功なのでjson.load()でデータを取得できます。

ブラウザ版

ブラウザ版の場合はこの方法ではデータの取得は行えない様です。
ブラウザ版Pyxelは、内部で Pyodide(Pythonをブラウザで動かす仕組み)を使用しています。
このPyodide上のPythonには、標準の urllib でネットワーク通信(HTTPS)を行う機能が制限されているという制約があります。
そのため、https://... というURLを理解できずにエラーになってしまいます。

pyodide.http.pyfetchを使用することでブラウザ版の処理を実装してみます。

   # 日本の祝日のデータの取得.
    def fetch_jp_holidays(self):
        # urllibで試してみる.
        url = "https://holidays-jp.github.io/api/v1/date.json"

        if IS_BROWSER:
            # ブラウザ版: pyodide.http.pyfetch を非同期で実行
            #import pyodide_js
            import asyncio

            async def download():
                try:
                    from pyodide.http import pyfetch
                    # pyfetch を使う(こちらの方がPyodideでは推奨されます)
                    response = await pyfetch(url)

                    if response.status == 200:
                        # await を忘れずに
                        js_obj = await response.json()
                        self._holidays = js_obj
                        #print(f"祝日のデータを取得: {len(self._holidays)}件")
                        #print(f"祝日のデータの例: {self._holidays}")
                        self._is_loading = False
                    else:
                        print(f"サーバエラー: ステータスコード {response.status}")  
                        self._is_loading = False
                except Exception as e:
                    print(f"祝日のデータの取得に失敗: {e}")
                    self._is_loading = False

            # asyncioのイベントループにタスクを投げる(プログラムは止めない)
            asyncio.create_task(download())
        else:
            try:
                import urllib.request
                with urllib.request.urlopen(url) as response:
                    if response.status == 200:
                        import json
                        self._holidays = json.load(response)
                        #print(f"祝日のデータを取得: {len(self._holidays)}件")
                        #print(f"祝日のデータの例: {self._holidays}")
                        self._is_loading = False
                    else:
                        print(f"サーバエラー: ステータスコード {response.status}")
                        self._is_loading = False
                        pass
            except Exception as e:
                print(f"祝日のデータの取得に失敗: {e}")
                self._is_loading = False
                pass

注意点としてブラウザ版ではダウンロード処理は非同期処理で実装する必要があることです。
urllibを使用した実装の時はその場で結果を待って成功、失敗を処理していました。
これはurlopenを実行した際に同期処理で行われるためダウンロードが完了するまでブロックしていまします。
今回はデータ量が小さいため気になりませんが、データ量が多い場合は処理が停止してしまいます。
ブラウザ版のpyfetchは非同期で処理されるため、ゲーム側はブロックされませんが、
完了時のコールバック関数などを用意してデータ取得後の処理などを実祖する必要が出てきます。

今回は祝日のデータでしたが、他のデータでも基本的な対応は同じになると思いますので、
必要なデータに合わせて適宜実装していただければと思います。

参考

github.com