# ==========================================
# 札幌圏1kmメッシュ 気象データ SQLite自動登録システム
# ==========================================
import sys, os, glob, sqlite3, time, re, traceback
from datetime import datetime, timedelta
import numpy as np
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QListWidget, QFileDialog, QProgressBar, QMessageBox)
from PyQt6.QtCore import QThread, pyqtSignal, QTimer, QSettings
DB_FILE = “sapporo_1km_mesh.db”
# 1kmメッシュ(約0.01度刻み)のグリッドを生成 (東経141.00~141.40, 北緯42.50~43.20)
GRID_LONS = np.arange(141.00, 141.401, 0.01)
GRID_LATS = np.arange(42.50, 43.201, 0.01)
LONS_2D, LATS_2D = np.meshgrid(GRID_LONS, GRID_LATS)
FLAT_LONS = LONS_2D.flatten()
FLAT_LATS = LATS_2D.flatten()
def init_db():
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()
c.execute(”’
CREATE TABLE IF NOT EXISTS weather_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
model TEXT,
init_time TEXT,
valid_time TEXT,
forecast_time INTEGER,
lat REAL,
lon REAL,
precip REAL,
temp REAL,
UNIQUE(model, init_time, valid_time, lat, lon)
)
”’)
conn.commit()
conn.close()
class DatabaseWorker(QThread):
log_signal = pyqtSignal(str)
progress_signal = pyqtSignal(int, int)
finished_signal = pyqtSignal()
def __init__(self, npz_dir):
super().__init__()
self.npz_dir = npz_dir
self.is_running = True
def get_var_key(self, data_obj, exact_keys, fuzzy_keys=None):
for k in data_obj.files:
if k.lower() in [ek.lower() for ek in exact_keys]: return k
if fuzzy_keys:
for k in data_obj.files:
kl = k.lower()
if not any(ex in kl for ex in [‘lon’, ‘lat’, ‘time’, ‘valid’]):
if any(fk.lower() in kl for fk in fuzzy_keys): return k
return None
def run(self):
self.log_signal.emit(“🤖 データベース自動登録監視を開始しました。”)
init_db()
processed_files = set()
while self.is_running:
try:
npz_files = glob.glob(os.path.join(self.npz_dir, “*.npz”))
new_files = [f for f in npz_files if f not in processed_files]
if new_files:
self.log_signal.emit(f”📄 新規NPZファイルを {len(new_files)}件 検出。処理開始…”)
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
for i, fpath in enumerate(new_files):
if not self.is_running: break
fname = os.path.basename(fpath)
# 例: MSM_20260524120000_FT00.npz
m = re.match(r'(MSM|GSM_JP|MSM_GUID|GSM_GUID)_(\d{14})_FT(\d+)\.npz’, fname)
if not m:
processed_files.add(fpath)
continue
model, it_str, ft_str = m.groups()
ft = int(ft_str)
init_dt = datetime.strptime(it_str, “%Y%m%d%H%M%S”)
valid_dt = init_dt + timedelta(hours=ft)
try:
data = np.load(fpath)
lon_key = self.get_var_key(data, [‘lon_surf’, ‘lon’, ‘longitude’])
lat_key = self.get_var_key(data, [‘lat_surf’, ‘lat’, ‘latitude’])
if not lon_key or not lat_key:
data.close(); processed_files.add(fpath); continue
data_lon = data[lon_key]
data_lat = data[lat_key]
precip_key = self.get_var_key(data, [‘precip’, ‘precip_raw’, ‘apcp’, ‘tp’], [‘precip’, ‘apcp’])
temp_key = self.get_var_key(data, [‘tmp’, ‘temp’, ‘t’, ‘t2m’, ‘tmp2m’], [‘tmp’, ‘temp’])
p_val = np.squeeze(data[precip_key]) if precip_key else None
t_val = np.squeeze(data[temp_key]) if temp_key else None
# 形状の補正
if data_lon.ndim == 1:
if p_val is not None and p_val.shape == (len(data_lon), len(data_lat)): p_val = p_val.T
if t_val is not None and t_val.shape == (len(data_lon), len(data_lat)): t_val = t_val.T
db_records = []
for lat_pt, lon_pt in zip(FLAT_LATS, FLAT_LONS):
if data_lon.ndim == 1:
i_lon = (np.abs(data_lon – lon_pt)).argmin()
i_lat = (np.abs(data_lat – lat_pt)).argmin()
else:
dist = (data_lon – lon_pt)**2 + (data_lat – lat_pt)**2
i_lat, i_lon = np.unravel_index(dist.argmin(), dist.shape)
precip = float(p_val[i_lat, i_lon]) if p_val is not None else None
temp = float(t_val[i_lat, i_lon]) if t_val is not None else None
if temp is not None and temp > 100: temp -= 273.15 # Kelvin to Celsius
if precip is not None: precip = max(0.0, float(np.nan_to_num(precip)))
db_records.append((model, init_dt.isoformat(), valid_dt.isoformat(), ft, lat_pt, lon_pt, precip, temp))
if db_records:
cursor.executemany(”’
INSERT OR REPLACE INTO weather_data
(model, init_time, valid_time, forecast_time, lat, lon, precip, temp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
”’, db_records)
conn.commit()
data.close()
processed_files.add(fpath)
self.progress_signal.emit(i + 1, len(new_files))
except Exception as e:
self.log_signal.emit(f”⚠️ ファイル処理エラー ({fname}): {e}”)
processed_files.add(fpath)
conn.close()
if self.is_running:
self.log_signal.emit(f”✅ 新規データ {len(new_files)}件 のDB登録が完了しました。”)
except Exception as e:
self.log_signal.emit(f”⚠️ 監視ループエラー: {e}”)
for _ in range(10): # 10秒待機
if not self.is_running: break
time.sleep(1)
def stop(self):
self.is_running = False
class DBApp(QWidget):
def __init__(self):
super().__init__()
self.settings = QSettings(‘WeatherApp’, ‘DatabaseLoader’)
self.npz_dir = self.settings.value(‘npz_dir’, os.path.join(os.getcwd(), “gpv_cache_npz”))
os.makedirs(self.npz_dir, exist_ok=True)
self.worker = None
self.init_ui()
def init_ui(self):
self.setWindowTitle(“札幌圏 1kmメッシュ 気象データベース 自動構築システム”)
self.resize(600, 450)
self.setStyleSheet(“””QWidget { background-color: #2D2D30; color: #CCCCCC; font-family: ‘MS Gothic’; font-size: 10pt; } QPushButton { background-color: #007ACC; border: none; padding: 8px; border-radius: 4px; color: white; font-weight: bold; } QPushButton:hover { background-color: #1C97EA; } QListWidget { background-color: #1E1E1E; border: 1px solid #3F3F46; padding: 5px; color: #4EC9B0; } QProgressBar { text-align: center; color: white; background-color: #333; border-radius: 4px; border: 1px solid #555; } QProgressBar::chunk { background-color: #007ACC; border-radius: 4px; }”””)
layout = QVBoxLayout(self)
title = QLabel(“📡 SQLite データベース自動連携機能”)
title.setStyleSheet(“font-size: 12pt; font-weight: bold; color: white;”)
layout.addWidget(title)
dir_layout = QHBoxLayout()
self.lbl_in = QLabel(f”監視対象 NPZフォルダ: {self.npz_dir}”)
btn_in = QPushButton(“フォルダ変更”)
btn_in.clicked.connect(self.change_dir)
dir_layout.addWidget(self.lbl_in)
dir_layout.addWidget(btn_in)
layout.addLayout(dir_layout)
self.progress_bar = QProgressBar()
layout.addWidget(self.progress_bar)
self.log_list = QListWidget()
layout.addWidget(self.log_list)
ctrl_layout = QHBoxLayout()
self.btn_start = QPushButton(“▶ DB自動登録を開始”)
self.btn_start.setStyleSheet(“background-color: #27AE60; height: 35px;”)
self.btn_start.clicked.connect(self.start_worker)
self.btn_stop = QPushButton(“■ 停止”)
self.btn_stop.setStyleSheet(“background-color: #C0392B; height: 35px;”)
self.btn_stop.setEnabled(False)
self.btn_stop.clicked.connect(self.stop_worker)
ctrl_layout.addWidget(self.btn_start)
ctrl_layout.addWidget(self.btn_stop)
layout.addLayout(ctrl_layout)
def log(self, msg):
self.log_list.addItem(f”[{datetime.now().strftime(‘%H:%M:%S’)}] {msg}”)
self.log_list.scrollToBottom()
def change_dir(self):
d = QFileDialog.getExistingDirectory(self, “監視フォルダ選択”, self.npz_dir)
if d:
self.npz_dir = d
self.settings.setValue(‘npz_dir’, self.npz_dir)
self.lbl_in.setText(f”監視対象 NPZフォルダ: {self.npz_dir}”)
def start_worker(self):
if self.worker and self.worker.isRunning(): return
self.btn_start.setEnabled(False)
self.btn_stop.setEnabled(True)
self.progress_bar.setValue(0)
self.log(“🚀 SQLite連携システムを起動しました。”)
self.worker = DatabaseWorker(self.npz_dir)
self.worker.log_signal.connect(self.log)
self.worker.progress_signal.connect(lambda c, t: self.progress_bar.setValue(int(c/t*100) if t>0 else 0))
self.worker.start()
def stop_worker(self):
if self.worker:
self.worker.stop()
self.worker.wait()
self.worker = None
self.btn_start.setEnabled(True)
self.btn_stop.setEnabled(False)
self.log(“⏹️ システムを停止しました。”)
def closeEvent(self, event):
self.stop_worker()
event.accept()
if __name__ == ‘__main__’:
app = QApplication(sys.argv)
window = DBApp()
window.show()
sys.exit(app.exec())

コメント