RUMlogNG ADIF > ハムサポcsv変換ツール
移動運用時にログ情報(交信記録)を持ち運べるようiPhone用のログアプリ「HAM交信サポート」(以下ハムサポ)を利用するようになりました。又、ホームでFT8の交信を行うようになってからパソコン用ログアプリはファイルメーカーPro(テンプレート利用)からRUMlogNGへ変更しました。(この時は他の局長さんが作成されたPerlのスクリプトでデータ移行は助かりました。)ログ用アプリが2種類になれば同期をどうするか?が課題になってきます。交信局数が少ないのでiPhoneへ手入力していましたが、忘れていると入力する局数が溜まったり、入力ミス(年齢が増えると集中力は反比例で下がってしまう。)も発生してきました。
ということでRUMlogNGのログ書き出し形式はadif、ハムサポのインポート用ファイルの形式がcsv、それらの形式と諸々を整合する変換スクリプトをChatGPTに問合せてキャッチボール(やりとり)しながら変換ツールを作成してもらいました。忘れたらここを見てなるほどと思えればいいなと思い記しました。
機能
RUMlogNGで書き出したログファイルADIF形式がハムサポのインポートファイル形式csvへ変換されハムサポへ読込が出来ること。
・RUMlogNGのログ(交信記録)

(サンプルなのでコールサインは実在しません。)
・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形式から変換したハムサポ用csvファイル(表計算ソフトで開いた状態)
| 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 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| JC1BBB | 2026-03-26 10:14:00 +0900 | 2026-03-26 10:14:00 +0900 | 55 | 55 | PM96VQ | JC1CCC/1 | 宇都宮市JCC#1501 TG-109寅巳山PM96vq | 埼玉県 PM96pe | かきく | 2m | 144.205000 | SSB | IC-705 | 2mHRH770 | 10 | JA/TG-109寅巳山:JC1CCC/1 | ||||||||||||||||||
| JC1DEF | 1 | 2026-03-26 10:11:00 +0900 | 2026-03-26 10:11:00 +0900 | 59 | 59 | PM96VQ | JC1CCC/1 | 宇都宮市JCC#1501 TG-109寅巳山PM96vq | 茨城県 QM06bg JA/IB-006足尾山 | あいう | 2m | 144.205000 | SSB | IC-705 | 2mHRH770 | 10 | JA/TG-109寅巳山:JC1CCC/1 |
・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 Desktop はデスクトップへ移動)
スクリプトを動かすため、python3 + スペース + ファイル名を入力してエンターします。
スクリプトが起動して、ADIF → HAM交信サポート変換ツールダイアログが表示されます。
(間違った場合はNo Such…見つからないよ みたいなメッセージがターミナルに出ます。コピペしてChatGPTに質問すると回答が得られます。)

ファイルを選ぶダイアログが表示されます。該当ファイルを選択してOpenをクリックします。

error messagingが出る場合も気にせず進めます。(ChatGPTによると重大なエラーではないそうです。)


(例:output260326-3 )Format:CSV files(.csv)を確認してSaveをクリックします。


スクリプト(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-sig") 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)
# -----------------------------
# ★ USERFIELD取得
# -----------------------------
uf1 = row.get("APP_RUMLOG_USERFIELD_1", "")
uf2 = row.get("APP_RUMLOG_USERFIELD_2", "")
uf3 = row.get("APP_RUMLOG_USERFIELD_3", "")
uf4 = row.get("APP_RUMLOG_USERFIELD_4", "")
# -----------------------------
# ★追加:My Callsign抽出
# 例:JA/TG-109寅巳山:JC1CCC/1 → JC1CCC/1
# -----------------------------
my_call = ""
if uf2 and ":" in uf2:
my_call = uf2.split(":", 1)[1].strip()
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", ""))
# -----------------------------
# ★ Grid Zone(Locator優先)
# -----------------------------
elif field == "Grid Zone":
value = row.get("APP_RUMLOG_LOCATOR", "") or row.get("GRIDSQUARE", "")
# -----------------------------
# ★ My Callsign(追加機能)
# -----------------------------
elif field == "My Callsign":
value = my_call
# -----------------------------
# ★ My QTH(COMMENT)
# -----------------------------
elif field == "My QTH":
value = row.get("COMMENT", "").strip()
# -----------------------------
# ★ Other QTH(USERFIELD_3・4追記)
# -----------------------------
elif field == "Other QTH":
base_qth = row.get("QTH", "")
additions = " ".join(filter(None, [uf3, uf4]))
value = f"{base_qth} {additions}".strip()
# -----------------------------
# ★ Rig Model(USERFIELD_1 前)
# -----------------------------
elif field == "Rig Model":
if uf1:
parts = uf1.split("/", 1)
value = parts[0]
# -----------------------------
# ★ Antenna(USERFIELD_1 後)
# -----------------------------
elif field == "Antenna":
if uf1 and "/" in uf1:
parts = uf1.split("/", 1)
value = parts[1]
# -----------------------------
# ★ Other Information(USERFIELD_2)
# -----------------------------
elif field == "Other Information":
value = uf2 if uf2 else 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データベースアップロード用も出来るのか?気になるところです。