HF/VHF帯において、突発的に発生して遠距離通信を可能にする「スポラディックE層(Es層)」の状況把握は重要です。

毎回ブラウザを開いてNICT(情報通信研究機構)のページを確認しに行くのは少し面倒ですよね。そこで今回は、Pythonを使って「最新2時間のEs層データを自動取得し、見やすい色分けテーブルでデスクトップに常時表示するGUIアプリ」のコードを解説します。

 

ツールの主な機能

  • Webスクレイピングの自動化: NICTの最新データを自動でパースして取得。

  • マルチスレッド処理: データ取得中に画面がフリーズ(応答なし)するのを防ぎます。

  • 強度の視覚化: 電離層の臨界周波数(fxEs)の数値に応じて、セルを自動で色分け(赤や黄など)。

  • 関連リンクへのアクセス: イオノグラムやデリンジャー現象、ダクト予報のページへ1クリックでジャンプ。(それぞれのデータの見方は各サイトをご確認ください)

  • 10分おきの自動更新: 立ち上げっぱなしで常に最新状態をキープ。

全体のソースコード

まずは今回解説するコードの全体像です。tkinterrequestspandasを使用しています。

 

Python

import tkinter as tk
import requests
import pandas as pd
from io import StringIO
from datetime import datetime
import webbrowser
import threading  # バックグラウンド処理用

 

URL = "https://wdc.nict.go.jp/Ionosphere/realtime/fxEs/latest-fxEs.html"  # スポラディックE層
LINK1_URL = "https://wdc.nict.go.jp/Ionosphere/realtime/"  # イオノグラム
LINK2_URL = "https://wdc.nict.go.jp/Ionosphere/realtime/x-ray/now/mercator.html"  # デリンジャー現象
LINK3_URL = "https://wdc.nict.go.jp/Ionosphere/realtime/oblver.html" # 斜め伝搬可能周波数
LINK4_URL = "https://www.dxinfocentre.com/tropo_eas.html" # Hepburn Tropospheric Ducting Forecasts ダクト予測

 

class EsLayerGridViewer:
    def __init__(self, root):
        now_str = datetime.now().strftime("%Y/%m/%d %H:%M:%S")

        self.root = root
        self.root.title(f"スポラディックE層:最新2時間     {now_str} 更新")
        self.root.geometry("520x300")
        self.root.configure(bg="#f0f0f0")

 

        # --- 上部コントロールエリアのフレーム ---
        self.top_frame = tk.Frame(root, bg="#f0f0f0")
        self.top_frame.pack(pady=5, padx=10, fill="x")

 

        # リンクボタン
        self.link1_btn = tk.Button(
            self.top_frame, text="イオノグラム",
            font=("Meiryo", 9, "underline"), fg="#0000FF", cursor="hand2",
            bg="#f0f0f0", activebackground="#f0f0f0", activeforeground="#0000FF",
            bd=0, command=self.open_link1
        )
        self.link1_btn.pack(side="left", padx=5)

 

        self.link2_btn = tk.Button(
            self.top_frame, text="デリンジャー現象",
            font=("Meiryo", 9, "underline"), fg="#0000FF", cursor="hand2",
            bg="#f0f0f0", activebackground="#f0f0f0", activeforeground="#0000FF",
            bd=0, command=self.open_link2
        )
        self.link2_btn.pack(side="left", padx=5)

 

        self.link3_btn = tk.Button(
            self.top_frame, text="斜め伝搬可能周波数",
            font=("Meiryo", 9, "underline"), fg="#0000FF", cursor="hand2",
            bg="#f0f0f0", activebackground="#f0f0f0", activeforeground="#0000FF",
            bd=0, command=self.open_link3
        )
        self.link3_btn.pack(side="left", padx=5)

 

        self.link4_btn = tk.Button(
            self.top_frame, text="ダクトFCST",
            font=("Meiryo", 9, "underline"), fg="#0000FF", cursor="hand2",
            bg="#f0f0f0", activebackground="#f0f0f0", activeforeground="#0000FF",
            bd=0, command=self.open_link4
        )
        self.link4_btn.pack(side="left", padx=5)

 

        # ステータスラベル
        self.status_label = tk.Label(self.top_frame, text="データを読み込み中...", font=("Meiryo", 9), bg="#f0f0f0", anchor="e")
        self.status_label.pack(side="right", fill="x", expand=True)

 

        # テーブルを描画するためのメインフレーム
        self.table_frame = tk.Frame(root, bg="#d0d0d0", bd=1)
        self.table_frame.pack(expand=True, fill="both", padx=10, pady=5)

 

        # 初回データ取得(スレッドで実行)
        self.start_update_thread()

 

   def open_link1(self):
        webbrowser.open(LINK1_URL)

 

   def open_link2(self):
        webbrowser.open(LINK2_URL)

 

    def open_link3(self):
        webbrowser.open(LINK3_URL)

 

    def open_link4(self):
        webbrowser.open(LINK4_URL)

 

    def get_cell_color(self, val_str):
        if not val_str or "-" in val_str:
            return "#FFFFFF", "#000000"

        try:
            num = float(val_str)
            if num >= 9.0:
                return "#FF0000", "#FFFFFF"
            elif num >= 8.0:
                return "#FF66CC", "#000000"
            elif num >= 7.0:
                return "#FFFF00", "#000000"
            else:
                return "#00FFFF", "#000000"
        except ValueError:
            return "#FFFFFF", "#000000"

 

    def start_update_thread(self):
        """通信フリーズを防ぐため、データ取得を別スレッドで開始する"""
        self.status_label.config(text="データ更新中...")
        thread = threading.Thread(target=self.fetch_data_worker, daemon=True)
        thread.start()

 

    def fetch_data_worker(self):
        """バックグラウンドでデータを取得する関数(サブスレッド)"""
        try:
            headers = {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
            }

 

            # timeoutを設定して無限フリーズを防止
            response = requests.get(URL, headers=headers, timeout=15)
            response.encoding = 'utf-8'

            html_data = StringIO(response.text)
            dfs = pd.read_html(html_data, flavor='html5lib', header=0)

 

            if len(dfs) >= 2:
                # GUIの更新はメインスレッドに安全に引き渡す (after経由)
                self.root.after(0, self.draw_table, dfs[1])
            else:
                self.root.after(0, lambda: self.status_label.config(text="エラー: データテーブルが見つかりません。"))

        except Exception as e:
            self.root.after(0, lambda: self.status_label.config(text={f"データ取得エラー: {e}"}))

 

        # 10分後に次の更新を予約 (600,000ミリ秒)
        self.root.after(600000, self.start_update_thread)

 

    def draw_table(self, df):
        """取得したデータフレームを基にGUIのテーブルを再描画する(メインスレッド)"""
        # 既存のウィジェットを削除
        for widget in self.table_frame.winfo_children():
            widget.destroy()

        columns = [str(col) for col in df.columns]

 

        # 列の伸縮比率を設定 (観測地列を少し広めに、数値列は均等に)
        self.table_frame.columnconfigure(0, weight=2)
        for col_idx in range(1, len(columns)):
            self.table_frame.columnconfigure(col_idx, weight=1)

 

        # ヘッダーの描画
        for col_idx, col_name in enumerate(columns):
            self.table_frame.rowconfigure(0, weight=1)
            lbl = tk.Label(
                self.table_frame, text=col_name, font=("Meiryo", 9, "bold"),
                bg="#e0e0e0", fg="#000000", height=1, relief="solid", bd=1
            )
            lbl.grid(row=0, column=col_idx, sticky="nsew")

 

        # データの描画
        for row_idx, row in df.iterrows():
            grid_row = row_idx + 1
            self.table_frame.rowconfigure(grid_row, weight=1) # 行の伸縮設定

            for col_idx, value in enumerate(row):
                val_str = "" if pd.isna(value) else str(value)

                if col_idx == 0:
                    bg_color, fg_color = "#FFD1D1", "#000000"
                else:
                    bg_color, fg_color = self.get_cell_color(val_str)

                lbl = tk.Label(
                    self.table_frame, text=val_str, font=("Meiryo", 10),
                    bg=bg_color, fg=fg_color, height=1, relief="solid", bd=1
                )
                lbl.grid(row=grid_row, column=col_idx, sticky="nsew")

        self.status_label.config(text=" ")

 

if __name__ == "__main__":
    root = tk.Tk()
    app = EsLayerGridViewer(root)
    root.mainloop()

 

コードの重要なポイント解説

1. フリーズさせないための「マルチスレッド(threading)」

GUIアプリでよくある失敗が、「ネットからデータを取り込んでいる間、アプリの画面が固まる」という現象です。

このコードでは start_update_thread メソッドを使い、データ取得処理(fetch_data_worker)をサブスレッド(裏方)で実行しています。これにより、通信中もウィンドウを動かしたり、リンクボタンを押したりといった操作が快適に行えます。

2. Pandasによる超簡単HTMLテーブル解析

NICTのページにあるデータ表を解析するために、pd.read_html を使用しています。

 

html_data = StringIO(response.text)
dfs = pd.read_html(html_data, flavor='html5lib', header=0)

これにより、HTML内の <table> タグを見つけ出し、自動的にPythonで扱いやすいDataFrame(データ表)に変換してくれます。今回は目的のデータが2番目のテーブルにあるため、dfs[1] を抽出してGUIに渡しています。

 

3. スレッドセーフなGUI更新(root.after

Tkinterのルールとして、「メインスレッド以外からGUIのパーツ(ウィジェット)を直接操作してはいけない」というものがあります。

サブスレッドで取得したデータを安全に画面へ反映させるため、self.root.after(0, ...) を使ってメインスレッドに描画処理(draw_table)を依頼しています。

また、関数の最後で self.root.after(300000, self.start_update_thread) を呼び出すことで、10分(600,000ミリ秒)ごとの自動定期更新ループを実現しています。

 

4. 数値に応じた自動色分け(ヒートマップ効果)

get_cell_color メソッドで、臨界周波数の値の大きさに応じて背景色と文字色を決定しています。

  • 9.0 MHz以上:

  • 8.0 MHz以上: ピンク

  • 7.0 MHz以上: 黄色

  • それ未満: 水色

画面を一瞥するだけで、「今、どこで強いEsが出ているか」が直感的にわかる設計です。

 

5. 便利なWebリンク機能

上部に配置された「イオノグラム」「デリンジャー現象」「斜め伝搬可能周波数」「ダクトFCST」のボタンは、クリックすると webbrowser.open() によってPCのデフォルトブラウザで対象ページが開きます。より詳細なチャートが見たくなった時もワンクリックで遷移できます。

 

まとめ

デスクトップの片隅に置いておくだけでリアルタイムにスポラディックE層の状況を監視できるツールを作成してみました。

さらに改造するなら、「9.0MHzを超えたらビープ音を鳴らすアラート機能」や「タスクトレイに常駐させる機能」などを追加してみるのも面白いかもしれません。