RUMlogNG ADIF > SOTA対応変換ツール

前回のスクリプト作成の元データは、同じRUMlogNGなのでお題のツールもトライしました。RUMlogNGの機能には、SOTA データベースupload用csv形式に変換(下記)できます。しかし、時刻をJSTからUTCへ修正が必要です。又、相手の移動運用地がSOTAに登録された山であればJA/XX-xxxのリファレンスも変換されると思うが、RUMlogNGの使い方がわからず反映されません。

RUMlogNGでSOTA データベースuploadcsv形式書出し結果
V2,JC1CCC/1,JA/TG-109,26/03/26,1630,144.205MHz,USB,JC1DEF/1,,

そこで、今年のSOTA Challenge 2026は相手局のグリットロケーターも含めて登録するので、上記課題も含めてcsv形式変換ツールをChatGPTに問合せてキャッチボール(やりとり)しながら作成してもらいました。csv形式へ変換した時の列順は、月刊FB NewsSummits On The Air (SOTA)の楽しみ。その100 2026 SOTAチャレンジ-2を参考にしました。

機能

RUMlogNGで書き出したログadif形式からSOTA データベースupload用csv形式(Challenge 2026対応含む)へ変換する。

・RUMlogNGのログ(交信記録)

例1)相手局が固定や移動局の場合の交信ログ

ユーザーフィールドを含む赤枠を対象にcsvへ変換(架空のコールサインです。)

例2)相手局がSOTAに登録された山へ移動した場合の交信ログ

ユーザーフィールドを含む赤枠を対象にcsvへ変換(架空のコールサインです。)

・ADIF形式で書き出し

ADIF Export from RumLogNG by DL2RUM
For further info visit: https://www.dl2rum.de

<ADIF_VER:5>3.1.6
<CREATED_TIMESTAMP:15>20260406 072628
<PROGRAMID:8>RUMlogNG
<PROGRAMVERSION:3>6.2
<EOH>

<call:6>JC1BBB <qso_date:8>20260326 <time_on:6>101400 <qso_date_off:8>20260326 <time_off:6>101400 <band:2>2m <freq:10>144.205000 <mode:3>SSB <submode:3>USB <rst_sent:2>55 <rst_rcvd:2>55 <app_rumlog_qsl:1>- <qsl_rcvd:1>N <qsl_sent:1>I <dxcc:3>339 <cont:2>AS <name:3>かきく <qth:3>埼玉県 <comment:27>宇都宮市JCC#1501TG-109寅巳山PM96vq <gridsquare:6>PM96VQ <tx_pwr:2>10 <app_rumlog_power:2>10 <eqsl_qsl_rcvd:1>N <eqsl_qsl_sent:1>N <lotw_qsl_rcvd:1>N <lotw_qsl_sent:1>N <pfx:3>JC1 <clublog_qso_upload_status:1>M <app_rumlog_userfield_1:15>IC-705/2mHRH770 <app_rumlog_userfield_2:21>JA/TG-109寅巳山:JC1CCC/1 <app_rumlog_userfield_3:6>PM96pe <app_rumlog_colorcode:1>0 <eor>
<call:8>JC1DEF/1 <qso_date:8>20260326 <time_on:6>101100 <qso_date_off:8>20260326 <time_off:6>101100 <band:2>2m <freq:10>144.205000 <mode:3>SSB <submode:3>USB <rst_sent:2>59 <rst_rcvd:2>59 <app_rumlog_qsl:1>- <qsl_rcvd:1>N <qsl_sent:1>I <dxcc:3>339 <cont:2>AS <name:3>あいう <qth:3>茨城県 <comment:27>宇都宮市JCC#1501TG-109寅巳山PM96vq <gridsquare:6>PM96VQ <tx_pwr:2>10 <app_rumlog_power:2>10 <eqsl_qsl_rcvd:1>N <eqsl_qsl_sent:1>N <lotw_qsl_rcvd:1>N <lotw_qsl_sent:1>N <pfx:3>JC1 <clublog_qso_upload_status:1>M <app_rumlog_userfield_1:15>IC-705/2mHRH770 <app_rumlog_userfield_2:21>JA/TG-109寅巳山:JC1CCC/1 <app_rumlog_userfield_3:6>QM06bg <app_rumlog_userfield_4:12>JA/IB-006足尾山 <app_rumlog_colorcode:1>0 <eor>

ADIF形式から変換したSOTA upload用csvファイル

V2,JC1CCC/1,JA/TG-109,26/03/26,0114,144MHz,SSB,JC1BBB,,%QRA%PM96pe%
V2,JC1CCC/1,JA/TG-109,26/03/26,0111,144MHz,SSB,JC1DEF/1,JA/IB-006,

(1行目タイトルは削除するスクリプトです。
V2,My call,SOTA Ref.,Date,Time(UTC),Freq,Mode,Callsign,Your SOTA Ref.,Your Locator

スクリプト作成時の環境

・OS :macOS 15.7.3
・Hard:Mac Book Air M4 2025
・pythonがインストールしてあること

スクリプトの動作手順(CSV形式ファイル作成)

1.準備するファイル(拡張子を間違えなければファイル名は任意です。)
 RUMlogNGからADIF形式で書き出したファイル名:input.adi
 スクリプトのファイル名           :sota_script.py

2.ターミナルから

~(ホームディレクトリ)からスクリプトを保存したディレクトリ(フォルダ)へcdコマンドで移動します。
(例:cd Desktop はデスクトップへ移動)
スクリプトを動かすため、python3 + スペース + sota_script.pyを入力してエンターします。
上部のRUMlogNG ADIF→CSV変換ダイアログの変換開始をクリックします。
ADIFファイル選択のダイアログ表示 選択後にOpenをクリックします。
csv保存先のダイアログのSave Asへファイル名を記入後、Saveをクリックする。(.cvsの拡張子はいらない。)
2件変換しました。のメッセージダイアログが表示されるのでOKをクリックする。メッセージの2件変換しましたは2QSOを変換した事になります。RUMlogNG ADIF→CSV変換ダイアログを閉じる。
ターミナルはコマンド待ちになります。(変換ができてればエラーは気にせず。)これで変換手順は終了しました。

スクリプト(sota_script.py)

import tkinter as tk
from tkinter import filedialog, messagebox
import re
import csv
from datetime import datetime, timedelta

def parse_adif(file_path):

    # =========================
    # ① ファイル読み込み
    # =========================
    with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
        data = f.read()

    # =========================
    # ② BOM除去(1行目消失対策)
    # =========================
    data = data.lstrip("\ufeff")

    # =========================
    # ③ <EOH>安全除去(超重要)
    # =========================
    pos = data.upper().find("<EOH>")
    if pos != -1:
        data = data[pos + len("<EOH>"):]

    # =========================
    # ④ レコード分割
    # =========================
    records = re.split(r"<EOR>", data, flags=re.IGNORECASE)

    result = []

    # =========================
    # ⑤ 各レコード処理
    # =========================
    for rec in records:

        # 空行除去
        if not rec.strip():
            continue

        fields = {}

        # =========================
        # ⑥ フィールド抽出
        # =========================
        for match in re.finditer(r"<([^:>]+):(\d+)>([^<]*)", rec, re.IGNORECASE):
            key = match.group(1).upper()
            value = match.group(3).strip()
            fields[key] = value

        # CALLが無いものは無視
        if not fields or "CALL" not in fields:
            continue

        try:
            no = "V2"

            # =========================
            # ⑦ 自局情報(RUMlogNG独自)
            # =========================
            uf2 = fields.get("APP_RUMLOG_USERFIELD_2", "")
            parts = uf2.split(":")

            if len(parts) >= 2:
                my_sota = parts[0][:9]
                my_call = parts[1]
            else:
                my_sota = ""
                my_call = ""

            # =========================
            # ⑧ 日付変換
            # =========================
            date_raw = fields.get("QSO_DATE", "")
            date_fmt = datetime.strptime(date_raw, "%Y%m%d").strftime("%d/%m/%y") if date_raw else ""

            # =========================
            # ⑨ 時刻変換(JST→UTC)
            # =========================
            time_raw = fields.get("TIME_ON", "")
            time_fmt = ""

            if time_raw:
                if len(time_raw) == 6:
                    dt = datetime.strptime(time_raw, "%H%M%S")
                elif len(time_raw) == 4:
                    dt = datetime.strptime(time_raw, "%H%M")
                else:
                    dt = None

                if dt:
                    dt -= timedelta(hours=9)
                    time_fmt = dt.strftime("%H%M")

            # =========================
            # ⑩ 周波数整形
            # =========================
            freq_raw = fields.get("FREQ", "")
            freq = ""

            if freq_raw:
                try:
                    f = float(freq_raw)
                    freq = f"{f:.3f}MHz" if f < 30 else f"{int(f)}MHz"
                except:
                    pass

            # =========================
            # ⑪ 通信情報
            # =========================
            mode = fields.get("MODE", "")
            call = fields.get("CALL", "")

            # =========================
            # ⑫ 相手SOTA
            # =========================
            your_sota = fields.get("APP_RUMLOG_USERFIELD_4", "")[:9]

            # =========================
            # ⑬ グリッド処理
            # =========================
            grid_raw = fields.get("APP_RUMLOG_USERFIELD_3", "")

            if your_sota:
                your_grid = ""
            else:
                if grid_raw:
                    grid = grid_raw.strip()
                    if len(grid) >= 6:
                        grid = grid[:4] + grid[4:6].lower()
                    your_grid = f"%QRA%{grid}%"
                else:
                    your_grid = ""

            # =========================
            # ⑭ 出力
            # =========================
            result.append([
                no,
                my_call,
                my_sota,
                date_fmt,
                time_fmt,
                freq,
                mode,
                call,
                your_sota,
                your_grid
            ])

        except Exception as e:
            print("エラー:", e)
            continue

    return result

def convert():

    # 入力ファイル選択
    input_file = filedialog.askopenfilename(
        title="ADIFファイル選択",
        filetypes=[("ADIF files", "*.adi")]
    )
    if not input_file:
        return

    # 出力ファイル選択
    output_file = filedialog.asksaveasfilename(
        title="CSV保存先",
        defaultextension=".csv"
    )
    if not output_file:
        return

    data = parse_adif(input_file)

    if not data:
        messagebox.showwarning("警告", "変換データがありません")
        return

    # =========================
    # ★ ヘッダー無し(元仕様)
    # =========================
    with open(output_file, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.writer(f)
        writer.writerows(data)

    messagebox.showinfo("完了", f"{len(data)}件変換しました")

# =========================
# GUI
# =========================
root = tk.Tk()
root.title("RUMlogNG ADIF → CSV 変換")
root.geometry("420x180")

tk.Label(root, text="ADIF → CSV変換ツール(SOTA対応)").pack(pady=10)

tk.Button(root, text="変換開始", command=convert, width=25, height=2).pack(pady=10)

root.mainloop()

次回から交信記録は、これを使ってSOTAデータベースへアップロードするのが楽しみ。