ADIF → 「HAM交信サポート(csv)」変換ツール

移動運用時にログ情報(交信記録)を持ち運べるようiPhone用のログアプリ「HAM交信サポート」を利用するようになりました。又、ホームでFT8の交信を行うようになってからパソコン用ログアプリはファイルメーカーPro(テンプレート利用)からRUMlogNGへ変更しました。(この時は他の局長さんが作成されたPerlのスクリプトでデータ移行は助かりました。)ログ用アプリが2種類になれば同期をどうするか?が課題になってきます。交信局数が少ないのでiPhoneへ手入力していましたが、忘れていると入力する局数が溜まったり、入力ミス(年齢が増えると集中力は反比例で下がってしまう。)も発生してきました。

ということでRUMlogNGのログ書き出し形式はadif、「HAM交信サポート」のインポート用ファイルの形式がcsv、それらの形式と諸々を整合する変換スクリプトをChatGPTに問合せてキャッチボール(やりとり)しながら変換ツールを作成してもらいました。忘れたらここを見てなるほどと思えればいいなと思い記しました。

機能

RUMlogNGで書き出したログファイルADIF形式を「HAM交信サポート」のインポートファイル形式csvへ変換して「HAM交信サポート」へ読込が出来ること。

RUMlogNGの修正画面(例:コールサインJC1DEF/1 サンプルなのでコールサインは実在しません。)
ユーザーフィールド含む赤枠を対象にしました。自局のQTHはQSO Noteに入力しています。
Date Timeは日本時間です。

変換結果

変換したcsv形式のファイルをエクセル等の表計算アプリで開いた状態を再現しています。(RUMlogNGは自局コールサイン(Jx1xxx、Jx1xxx/P、Jx1xxx/6など)の入力欄が無いため、HAM交信サポートのMy Callsignへどう反映するか?が課題として残っています。)

CallsignPortableTimeTime EndRST SentRST ReceivedWho CalledGrid ZoneMy CallsignMy QTHOther QTHOther LatitudeOther LongitudeDistanceOther NameBandFrequencyModeQSL CardQSL WayQSL FlagNR ReceivedNR SentRig ModelAntennaTXPowerLatitudeLongitudeJCC/JGCAltitude above sea levelWeatherOther InformationQSL CommentContest NameContest Points
JC1ABC2026-03-26 10:14:00 +09002026-03-26 10:14:00 +09005555PM96VQ宇都宮市JCC#1501 TG-109寅巳山PM96vq埼玉県大宮市 PM96peあいう2m144.205000SSBIC-7052mHRH7705JA/TG-109寅巳山
JC1DEF12026-03-26 10:11:00 +09002026-03-26 10:11:00 +09005959PM96VQ宇都宮市JCC#1501 TG-109寅巳山PM96vq茨城県日立市 QM06bg JA/IB-006足尾山かきく2m144.205000SSBIC-7052mHRH77010JA/TG-109寅巳山

ファイルをiPhoneの「HAM交信サポート」に読込み(iPhoneの画面キャプチャ)

交信終了時刻は開始時刻と同じです。
相手のQTHはRUMlogNGのQTH + Your Locater + Your SOTA Ref.を連結しています。
無線機種類とアンテナはRUMlogNGのRIG/ANTを/で分割しています。

スクリプト作成時の環境

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

スクリプトの動作手順

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

2.ターミナルから

~(ホームディレクトリ)からスクリプトを保存したディレクトリ(フォルダ)へcdコマンドで移動します。
(例:cd Desktop はデスクトップへ移動)
スクリプトを動かすため、python3 + スペース + ファイル名を入力してエンターします。
スクリプトが起動して、ADIF → HAM交信サポート変換ツールダイアログが表示されます。
(間違った場合はNo Such…見つからないよ みたいなメッセージがターミナルに出ます。コピペしてChatGPTに質問すると回答が得られます。)
ADIF → HAM交信サポート変換ツールダイアログの参照をクリックすると
ファイルを選ぶダイアログが表示されます。該当ファイルを選択してOpenをクリックします。
変換実行をクリックします。
error messagingが出る場合も気にせず進めます。(ChatGPTによると重大なエラーではないそうです。)
変換が完了しました! ダイアログが出るのでOKをクリックします。
変換後のファイル名入力と保存先を指定するダイアログが出まるので記述します。
(例:output260326-3 )Format:CSV files(.csv)を確認してSaveをクリックします。
ADIF → HAM交信サポート変換ツールダイアログを閉じるとターミナルは次のコマンド待ちになります。
exitを入力してエンターで終了。

スクリプト(adi_to_csv_script.py)

ChatGPTで出来たコードをコピー&Visual Studio Code(Mac用アプリ)へペースト ファイル名をadi_to_csv_script.pyで保存しました。

import re
import csv
import tkinter as tk
from tkinter import filedialog, messagebox
#—————————–
#ADIF解析
#—————————–
def parse_adif(file_path):
with open(file_path, ‘r’, encoding=’utf-8′, errors=’ignore’) as f:
content = f.read()

if '<EOH>' in content:
    content = content.split('<EOH>')[1]

content = content.replace('\n', ' ').replace('\r', ' ')
records = re.split(r'<eor>', content, flags=re.IGNORECASE)

parsed_data = []

for record in records:
    fields = re.findall(r'<(.*?):(\d+).*?>([^<]*)', record)
    record_dict = {}

    for field_name, length, value in fields:
        record_dict[field_name.upper()] = value.strip()

    if record_dict:
        parsed_data.append(record_dict)

return parsed_data

#—————————–
#日時整形
#—————————–
def format_datetime(date, time):
if date and time and len(date) == 8 and len(time) == 6:
d = f”{date[0:4]}-{date[4:6]}-{date[6:8]}”
t = f”{time[0:2]}:{time[2:4]}:{time[4:6]}”
return f”{d} {t} +0900″
return “”
#—————————–
#Callsign解析
#—————————–
def parse_callsign(call):
base = call
portable = “”
info = “”

if "/" in call:
    parts = call.split("/")
    base = parts[0]
    suffix = parts[1].upper()

    if suffix.isdigit():
        portable = suffix
    elif suffix == "P":
        portable = "P"
    elif suffix == "MM":
        info = "Maritime Mobile"
    elif suffix == "QRP":
        info = "QRP"
    else:
        info = suffix

return base, portable, info

#—————————–
#CSV出力
#—————————–

def write_log_csv(data, output_file):

output_order = [
    'Callsign', 'Portable', 'Time', 'Time End', 'RST Sent', 'RST Received',
    'Who Called', 'Grid Zone', 'My Callsign', 'My QTH', 'Other QTH',
    'Other Latitude', 'Other Longitude', 'Distance', 'Other Name',
    'Band', 'Frequency', 'Mode', 'QSL Card', 'QSL Way', 'QSL Flag',
    'NR Received', 'NR Sent', 'Rig Model', 'Antenna', 'TXPower',
    'Latitude', 'Longitude', 'JCC/JGC', 'Altitude above sea level',
    'Weather', 'Other Information', 'QSL Comment',
    'Contest Name', 'Contest Points'
]

mapping = {
    "RST_SENT": "RST Sent",
    "RST_RCVD": "RST Received",
    "GRIDSQUARE": "Grid Zone",
    "NAME": "Other Name",
    "QTH": "Other QTH",
    "BAND": "Band",
    "FREQ": "Frequency",
    "MODE": "Mode",
    "TX_PWR": "TXPower"
}

with open(output_file, "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(output_order)

    for row in data:
        new_row = []

        raw_call = row.get("CALL", "")
        callsign, portable, extra_info = parse_callsign(raw_call)

        for field in output_order:
            value = ""

            if field == "Callsign":
                value = callsign

            elif field == "Portable":
                value = portable

            elif field == "Time":
                value = format_datetime(row.get("QSO_DATE", ""), row.get("TIME_ON", ""))

            elif field == "Time End":
                value = format_datetime(row.get("QSO_DATE", ""), row.get("TIME_OFF", ""))

            elif field == "Other Information":
                value = extra_info

            else:
                adif_key = next((k for k, v in mapping.items() if v == field), None)
                value = row.get(adif_key, "") if adif_key else ""

            new_row.append(value)

        writer.writerow(new_row)

#—————————–
#GUI処理
#—————————–
def select_file():
file_path = filedialog.askopenfilename(filetypes=[(“ADIF files”, “*.adi”)])
entry_input.delete(0, tk.END)
entry_input.insert(0, file_path)

def convert():
input_file = entry_input.get()

if not input_file:
    messagebox.showerror("エラー", "ファイルを選択してください")
    return

output_file = filedialog.asksaveasfilename(defaultextension=".csv",
                                           filetypes=[("CSV files", "*.csv")])

if not output_file:
    return

try:
    data = parse_adif(input_file)
    write_log_csv(data, output_file)
    messagebox.showinfo("完了", "変換が完了しました!")
except Exception as e:
    messagebox.showerror("エラー", str(e))

#—————————–
#GUI画面
#—————————–

root = tk.Tk()
root.title(“ADIF → HAM交信サポート変換ツール”)

frame = tk.Frame(root, padx=10, pady=10)
frame.pack()

tk.Label(frame, text=”ADIFファイル:”).grid(row=0, column=0)

entry_input = tk.Entry(frame, width=40)
entry_input.grid(row=0, column=1)

tk.Button(frame, text=”参照”, command=select_file).grid(row=0, column=2)

tk.Button(frame, text=”変換実行”, command=convert, bg=”lightblue”).grid(row=1, column=1, pady=10)

root.mainloop()

以上です。
応用編としてSOTAデータベースアップロード用も出来るのか?気になるところです。