==========================================
【App A】 GPV解析・キャッシュエンジン (Ver 104.0 究極解放版)
FIX: ガイダンスの正規表現バグ解消・マルチグリッド解像度対応・気象庁独自コード対応
==========================================
import sys, os, glob, re, subprocess, queue, time
from datetime import datetime
import multiprocessing as mp
os.environ[‘QT_API’] = ‘pyqt6’
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QListWidget, QFileDialog, QMessageBox,
QSystemTrayIcon, QMenu)
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QSettings
from PyQt6.QtGui import QIcon, QPixmap, QPainter, QAction
try: import cfgrib
except ImportError: sys.exit(“エラー: cfgribがインストールされていません。”)
APP_DIR = os.getcwd()
DEFAULT_OUTPUT_DIR = os.path.join(APP_DIR, “gpv_cache_npz”)
CURRENT_OUTPUT_DIR = DEFAULT_OUTPUT_DIR
WGRIB2_EXE = os.path.join(APP_DIR, “wgrib2_data”, “wgrib2.exe”)
os.makedirs(CURRENT_OUTPUT_DIR, exist_ok=True)
def write_syslog(msg):
log_file = os.path.join(CURRENT_OUTPUT_DIR, “system_log.txt”)
try:
with open(log_file, “a”, encoding=”utf-8″) as f:
f.write(f”[{datetime.now().strftime(‘%H:%M:%S’)}] {msg}\n”)
except Exception: pass
def create_lightning_icon():
pixmap = QPixmap(64, 64); pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap); font = painter.font(); font.setPixelSize(50)
painter.setFont(font); painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, “⚡”)
painter.end(); return QIcon(pixmap)
==========================================
【裏方ワーカー】 抽出・解析コア
==========================================
def extract_gpv_worker(model, mode, cache, init, target_fts, f1, f2, wgrib2_path, q):
try:
import os, sys, subprocess, warnings
import numpy as np
import cfgrib
warnings.filterwarnings(“ignore”)
os.environ[“ECCODES_MAX_VALUES”] = “5000000”
if sys.platform == "win32":
conda_dir = os.path.dirname(sys.executable)
dll_paths = [os.path.join(conda_dir, "Library", "bin"), os.path.join(conda_dir, "bin")]
for p in dll_paths:
if os.path.exists(p):
os.environ["PATH"] = f"{p};{os.environ.get('PATH', '')}"
try: os.add_dll_directory(p)
except Exception: pass
def safe_max(arr):
return np.nanmax(arr) if not np.all(np.isnan(arr)) else 0.0
d_all = {ft: {} for ft in target_fts}
creationflags = 0x08000000 if sys.platform == "win32" else 0
for t_ft in target_fts:
d = d_all[t_ft]
def slice_with_wgrib2(fin, ft_val, suffix):
if fin == "NONE": return "NONE"
fout = os.path.join(cache, f"temp_{model}_{mode}_{init}_{ft_val}_{suffix}.bin")
d_str = f"{ft_val//24} day" if ft_val % 24 == 0 else "INVALID"
# 💡 修正1: 悪さをしていたエスケープを排除し、シンプルで強力なマッチングに
if mode == "ANAL":
match_str = ":(anl):"
elif ft_val == 0:
match_str = ":(anl|0 hour |[0-9]+-0 hour )"
else:
match_str = f":([0-9]+-)?({ft_val} hour|{d_str}) "
cmd = [wgrib2_path, fin, "-match", match_str, "-grib", fout]
try:
subprocess.run(
cmd, creationflags=creationflags, check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
)
if os.path.exists(fout) and os.path.getsize(fout) > 0: return fout
except Exception: pass
return "NONE"
f1_mini = slice_with_wgrib2(f1, t_ft, "1")
t_ft_p = (t_ft // 3) * 3 if model == "MSM" else t_ft
f2_mini = slice_with_wgrib2(f2, t_ft_p, "2")
def process_file(filepath, is_pall):
if filepath == "NONE": return
try:
dss = []
try:
dss = cfgrib.open_datasets(filepath, backend_kwargs={'indexpath': ''})
except Exception:
import xarray as xr
for stype in ['instant', 'accum', 'avg', 'max', 'min']:
try:
ds_sub = xr.open_dataset(filepath, engine='cfgrib', backend_kwargs={'indexpath': '', 'filter_by_keys': {'stepType': stype}})
dss.append(ds_sub)
except: pass
if not dss:
try:
dss.append(xr.open_dataset(filepath, engine='cfgrib', backend_kwargs={'indexpath': ''}))
except: pass
for ds in dss:
lon = ds['longitude'].values if 'longitude' in ds.coords else (ds['lon'].values if 'lon' in ds.coords else None)
lat = ds['latitude'].values if 'latitude' in ds.coords else (ds['lat'].values if 'lat' in ds.coords else None)
for v in ds.data_vars:
da = ds[v]
is_ens = 'number' in da.dims
da_step = da.isel(step=0) if 'step' in da.dims else da
if is_ens and 'number' in da_step.coords: da_step = da_step.isel(number=0)
grib_short = str(da.attrs.get('GRIB_shortName', '')).lower()
sName = grib_short if grib_short and grib_short != 'unknown' else str(v).lower()
attrs_str = str(da.attrs).lower()
c_num = da.attrs.get('GRIB_parameterCategory', -1)
p_num = da.attrs.get('GRIB_parameterNumber', -1)
# 💡 修正2: 整数型でも確実に2次元まで削る(エラー源を根絶)
val_2d = da_step.values.copy()
while val_2d.ndim > 2:
val_2d = val_2d[0]
# 💡 修正3: サイズが違うグリッド(降水560x480 vs 発雷141x121)の座標衝突を防ぐ
if lon is not None and lat is not None:
shape_key = f"{val_2d.shape[0]}x{val_2d.shape[1]}"
d[f'lon_{shape_key}'] = lon
d[f'lat_{shape_key}'] = lat
# メインのlon/latは「一番解像度が高い(要素数が多い)もの」を優先して保持
if 'lon' not in d or lon.size > d['lon'].size:
d['lon'] = lon
d['lat'] = lat
d['lon_surf'] = lon
d['lat_surf'] = lat
if mode == "GUID":
d['lon_guid'] = lon
d['lat_guid'] = lat
# --- GUIDANCE モードの処理 ---
if mode == "GUID":
# 基本名で保存
if sName != 'unknown':
d[sName] = val_2d
else:
d[f"var_{c_num}_{p_num}"] = val_2d
# 💡 修正4: 気象庁独自のカテゴリー191(天気)等を正確にマッピング
if sName == 'unknown' or (c_num == 191 and p_num == 192) or 'weather' in attrs_str:
d['wea'] = val_2d
elif sName in ['tp', 'apcp', 'precip', 'tprate'] or (c_num == 1 and p_num in [8, 52]):
if 'precip' not in d: d['precip'] = np.nan_to_num(val_2d, nan=0.0)
elif sName in ['asnow', 'weasd', 'snod', 'snow', 'tsrate']:
d['snow'] = val_2d
elif sName in ['tstm', 'thund'] or 'prob' in attrs_str:
d['thund'] = val_2d
elif sName in ['min_vis', 'vis', 'visibility']:
d['vis'] = val_2d
continue
# --- 以下、GPV / ANAL モード用 ---
# (GPV用の処理は変更なしのため省略せずにそのまま残します)
level_dim_name = None
for coord_key in ['isobaricInhPa', 'isobaricInPa', 'level']:
if coord_key in da_step.dims:
level_dim_name = coord_key; break
if level_dim_name and da_step[level_dim_name].size > 1:
levels = da_step[level_dim_name].values
for i, lvl_raw in enumerate(levels):
lvl = float(lvl_raw)
if level_dim_name == 'isobaricInPa': lvl /= 100.0
lvl = int(lvl)
if lvl <= 0: continue
val_l = da_step.with_isobaricInhPa(i).values.copy() if hasattr(da_step, 'with_isobaricInhPa') else da_step.isel(**{level_dim_name: i}).values.copy()
while val_l.ndim > 2: val_l = val_l[0]
if is_pall or mode == "ANAL":
if sName in ['u', 'u-component']: d[f'u{lvl}'] = val_l
elif sName in ['v', 'v-component']: d[f'v{lvl}'] = val_l
elif sName in ['t', 'temp']: d[f't{lvl}'] = val_l - 273.15 if safe_max(val_l) > 150 else val_l
elif sName in ['r', 'rh']: d[f'rh{lvl}'] = val_l
elif sName in ['gh', 'hgt']: d[f'gh{lvl}'] = val_l
elif sName in ['w', 'vvel', 'dzdt']: d[f'w{lvl}'] = val_l
elif sName in ['absv', 'vort']: d[f'vort{lvl}'] = val_l
else:
level_val = -1.0
for coord_key in ['isobaricInhPa', 'isobaricInPa', 'level']:
if coord_key in da_step.coords:
val_coord = np.atleast_1d(da_step.coords[coord_key].values)
if val_coord.size == 1:
level_val = float(val_coord[0])
if coord_key == 'isobaricInPa': level_val /= 100.0
break
if level_val < 0: level_val = float(da_step.attrs.get('GRIB_level', -1))
if mode == "ANAL" or sName.startswith("param_"):
d[sName] = val_2d
if not is_pall or mode == "ANAL":
is_precip = sName in ['tp', 'apcp', 'pr', 'precip', 'prate'] or 'precip' in attrs_str or 'accum' in str(da_step.coords).lower()
if is_precip:
if 'precip' not in d: d['precip'] = np.nan_to_num(val_2d, nan=0.0)
continue
if sName in ['prmsl', 'msl', 'mslet']: d['slp'] = val_2d / 100.0 if safe_max(val_2d) > 2000 else val_2d
elif sName in ['pres', 'sp'] or 'pressure' in attrs_str: d['pres'] = val_2d / 100.0 if safe_max(val_2d) > 2000 else val_2d
elif sName in ['10u', 'u10'] or ('u' in sName and '10' in attrs_str): d['u10'] = val_2d
elif sName in ['10v', 'v10'] or ('v' in sName and '10' in attrs_str): d['v10'] = val_2d
elif sName in ['2t', 't2m', 'tmp2m'] or 'temperature' in attrs_str: d['t2m'] = val_2d - 273.15 if safe_max(val_2d) > 150 else val_2d
elif sName in ['2r', 'rh2m', 'rh'] or 'humidity' in attrs_str: d['rh2m'] = val_2d
elif sName in ['tcc', 'tcdc']: d['tcc'] = val_2d
elif sName in ['hcc', 'hcdc']: d['hcc'] = val_2d
elif sName in ['mcc', 'mcdc']: d['mcc'] = val_2d
elif sName in ['lcc', 'lcdc']: d['lcc'] = val_2d
for ds in dss: ds.close()
except Exception: pass
process_file(f1_mini, False)
process_file(f2_mini, True)
pfx = "GUID_" if mode == "GUID" else ""
out_file = os.path.join(cache, f"{model}_{pfx}{init}_FT{t_ft:02d}.npz")
# データが1つでも入っていれば保存する
if len([k for k in d.keys() if not k.startswith('lon') and not k.startswith('lat')]) > 0:
np.savez_compressed(out_file, **d)
q.put(("SUCCESS", f"{mode}_{t_ft}"))
try:
if f1_mini != "NONE" and os.path.exists(f1_mini): os.remove(f1_mini)
if f2_mini != "NONE" and os.path.exists(f2_mini): os.remove(f2_mini)
except Exception: pass
except Exception as e:
import traceback
q.put(("ERROR", str(e) + "\n" + traceback.format_exc()))
==========================================
スキャンスレッド
==========================================
class DataParserThread(QThread):
log_signal = pyqtSignal(str); progress_signal = pyqtSignal(int, str); finished_signal = pyqtSignal(bool)
def init(self, folder_paths, output_dir, last_reported):
super().init()
self.folder_paths = [path for path in folder_paths if path and os.path.exists(path)]
self.output_dir = output_dir
self.last_reported = last_reported; self.abort = False
def run(self):
if not self.folder_paths: self.finished_signal.emit(False); return
if not os.path.exists(WGRIB2_EXE):
self.log_signal.emit(f"⚠️ wgrib2が見つかりません。"); self.finished_signal.emit(False); return
has_new_data = False; scan_started = False; reported_new_file = False
scan_rules = [
('*MSM*GPV*', 'MSM', 'GPV', range(0, 80)),
('*GSM*GPV*', 'GSM_JP', 'GPV', range(0, 265, 3)),
('*MSM*GUID*', 'MSM', 'GUID', range(0, 80)),
('*GSM*GUID*', 'GSM', 'GUID', range(0, 265, 3)),
('*ANAL*grib2*', 'ANAL', 'ANAL', [0])
]
for folder_path in self.folder_paths:
if self.abort: break
for pattern, model_id, mode, ft_range in scan_rules:
if self.abort: break
files = glob.glob(os.path.join(folder_path, '**', pattern), recursive=True)
if not files: continue
times = set(re.search(r'_(\d{14})_', os.path.basename(f)).group(1) for f in files if re.search(r'_(\d{14})_', f))
if not times: continue
latest_it = max(times)
latest_files = [f for f in files if latest_it in f]
if self.last_reported.get(f"{model_id}_{mode}") != latest_it:
self.log_signal.emit(f"✅ {model_id} ({mode}) 最新データ発見: {latest_it}")
self.last_reported[f"{model_id}_{mode}"] = latest_it; reported_new_file = True
tasks = []
pfx = "GUID_" if mode == "GUID" else ""
for ft in ft_range:
npz_file = os.path.join(self.output_dir, f"{model_id}_{pfx}{latest_it}_FT{ft:02d}.npz")
if not os.path.exists(npz_file): tasks.append(ft)
if not tasks: continue
if not scan_started:
self.log_signal.emit("🔍 新規データを抽出中..."); scan_started = True; has_new_data = True
file_groups = {}
for ft in tasks:
if model_id == "GSM_GL":
f1 = self._get_file_for_ft(latest_files, "", ft, model_id) or "NONE"
f2 = f1
elif mode == "GUID":
# GUIDの場合はP-allとPrr(またはPrrsf)を両方読み込む
f1 = self._get_file_for_ft(latest_files, "P-all", ft, model_id) or "NONE"
if f1 == "NONE": f1 = self._get_file_for_ft(latest_files, "", ft, model_id) or "NONE"
f2 = self._get_file_for_ft(latest_files, "Prr", ft, model_id) or "NONE"
if f2 == "NONE": f2 = self._get_file_for_ft(latest_files, "Prrsf", ft, model_id) or "NONE"
else:
f1 = self._get_file_for_ft(latest_files, "" if mode == "ANAL" else "Lsurf", ft, model_id) or "NONE"
if mode == "ANAL":
f2 = "NONE"
else:
ft_p = (ft // 3) * 3 if model_id == "MSM" else ft
f2 = self._get_file_for_ft(latest_files, "P-all" if any("P-all" in x for x in latest_files) else ("L-pall" if any("L-pall" in x for x in latest_files) else "P"), ft_p, model_id) or "NONE"
if f1 == "NONE" and f2 == "NONE":
f1 = self._get_file_for_ft(latest_files, "", ft, model_id) or "NONE"
f2 = f1
if f1 == "NONE" and f2 == "NONE": continue
key = (f1, f2)
if key not in file_groups: file_groups[key] = []
file_groups[key].append(ft)
if file_groups:
self.log_signal.emit(f"🚀 {model_id} ({mode}) 爆速抽出 (対象: {len(tasks)}件)...")
total_extracted = 0
ctx = mp.get_context('spawn')
for (f1, f2), fts in file_groups.items():
if self.abort: break
for i in range(0, len(fts), 5):
if self.abort: break
chunk_fts = fts[i:i + 5]
q = ctx.Queue()
p = ctx.Process(target=extract_gpv_worker, args=(model_id, mode, self.output_dir, latest_it, chunk_fts, f1, f2, WGRIB2_EXE, q))
p.start()
while p.is_alive() or not q.empty():
if self.abort: p.terminate(); break
try:
msg_type, msg_val = q.get(timeout=0.1)
if msg_type == "SUCCESS":
total_extracted += 1
self.progress_signal.emit(int((total_extracted / len(tasks)) * 100), f"⚡ wgrib2解析中 {model_id} {msg_val}...")
elif msg_type == "ERROR":
self.log_signal.emit(f"⚠️ {msg_val}")
except queue.Empty: pass
p.join()
if has_new_data or reported_new_file: self.log_signal.emit("✅ スキャン・抽出完了")
self.finished_signal.emit(has_new_data)
def _get_file_for_ft(self, files_list, keyword, ft, model_name):
if "GSM" in model_name:
for f in files_list:
if keyword and keyword not in f: continue
m_single = re.search(r'FD(\d{2})(\d{2})_grib2', os.path.basename(f))
if m_single:
file_ft = int(m_single.group(1)) * 24 + int(m_single.group(2))
if file_ft == ft: return f
m_range = re.search(r'FD(\d{2})(\d{2})-(\d{2})(\d{2})', os.path.basename(f))
if m_range:
ft_s = int(m_range.group(1)) * 24 + int(m_range.group(2))
ft_e = int(m_range.group(3)) * 24 + int(m_range.group(4))
if ft_s <= ft <= ft_e: return f
return next((f for f in files_list if (keyword in f or not keyword)), None)
for f in files_list:
if keyword and keyword not in f: continue
m = re.search(r'FH(\d{2})-(\d{2})', os.path.basename(f))
if m and int(m.group(1)) <= ft <= int(m.group(2)): return f
elif not m: return f
return None
==========================================
GUIクラス
==========================================
class EngineStatusWindow(QWidget):
def init(self):
super().init()
self.setWindowTitle(“GPV 解析エンジン (Ver 104.0 究極解放版)”)
self.setFixedSize(650, 600)
self.setStyleSheet(“background-color: #0A192F; color: #E0E0E0; font-family: ‘MS Gothic’;”)
layout = QVBoxLayout(self)
self.status_label = QLabel(“状態: 起動中…”); self.status_label.setStyleSheet(“font-size: 14pt; font-weight: bold; color: #64FFDA;”)
layout.addWidget(self.status_label)
control_layout = QHBoxLayout()
self.pause_btn = QPushButton("⏸ 一時停止")
self.play_btn = QPushButton("▶ 再生(自動再開)")
self.resume_btn = QPushButton("🔄 手動強制スキャン")
self.stop_btn = QPushButton("⏹ 停止")
for btn in [self.pause_btn, self.play_btn, self.resume_btn, self.stop_btn]:
btn.setStyleSheet("background-color: #1D3557; padding: 4px; font-weight: bold; color: #F1FAEE;")
control_layout.addWidget(btn)
layout.addLayout(control_layout)
cache_layout = QHBoxLayout()
self.clean_btn = QPushButton("🧹 24h経過データ削除")
self.clean_btn.setStyleSheet("background-color: #F4A261; padding: 4px; font-weight: bold; color: black;")
self.clear_btn = QPushButton("💥 キャッシュ全削除")
self.clear_btn.setStyleSheet("background-color: #E63946; padding: 4px; font-weight: bold; color: white;")
cache_layout.addWidget(self.clean_btn)
cache_layout.addWidget(self.clear_btn)
layout.addLayout(cache_layout)
out_layout = QHBoxLayout()
self.out_folder_btn = QPushButton("💾 出力先フォルダを設定")
self.out_folder_btn.setStyleSheet("background-color: #457B9D; padding: 4px; font-weight: bold; color: white;")
out_layout.addWidget(self.out_folder_btn)
self.out_folder_label = QLabel("未設定")
self.out_folder_label.setStyleSheet("background: #112240; padding: 4px; border: 1px solid #457B9D;")
out_layout.addWidget(self.out_folder_label, stretch=1)
layout.addLayout(out_layout)
self.log_list = QListWidget()
self.log_list.setStyleSheet("background: #112240; border: 1px solid #444; color: #E0E0E0;")
layout.addWidget(self.log_list)
self.copy_btn = QPushButton("📋 ログをコピー")
self.copy_btn.setStyleSheet("background-color: #1D3557; color: white;")
layout.addWidget(self.copy_btn)
folder_layout = QHBoxLayout()
self.folder_btns = [QPushButton(f"📁 読込元フォルダ {i+1}") for i in range(3)]
for b in self.folder_btns:
b.setStyleSheet("background-color: #1D3557; color: white;")
folder_layout.addWidget(b)
layout.addLayout(folder_layout)
self.exit_btn = QPushButton("システムを完全に終了"); self.exit_btn.setStyleSheet("background-color: #E63946; color: white; font-weight: bold; padding: 6px;")
layout.addWidget(self.exit_btn)
def log(self, message):
self.log_list.addItem(f"[{datetime.now().strftime('%H:%M:%S')}] {message}")
self.log_list.scrollToBottom(); write_syslog(message)
def closeEvent(self, event):
event.ignore(); self.hide()
if hasattr(self, 'tray_msg_callback'): self.tray_msg_callback()
class TrayApp(QApplication):
def init(self, sys_argv):
super().init(sys_argv)
self.setQuitOnLastWindowClosed(False)
self.settings = QSettings(“SapporoWeatherApp”, “GPVEngine”)
self.monitor_folders = [self.settings.value(f”watch_dir_{i+1}”, “”) for i in range(3)]
global CURRENT_OUTPUT_DIR
self.output_dir = self.settings.value("output_dir", DEFAULT_OUTPUT_DIR)
CURRENT_OUTPUT_DIR = self.output_dir
os.makedirs(self.output_dir, exist_ok=True)
self.last_reported = {}
self.window = EngineStatusWindow()
self.window.tray_msg_callback = self.show_tray_message
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setIcon(create_lightning_icon())
self.tray_icon.setToolTip("GPV抽出エンジン (稼働中)")
tray_menu = QMenu()
show_action = QAction("ウィンドウを表示"); show_action.triggered.connect(self.restore_window)
quit_action = QAction("完全に終了する"); quit_action.triggered.connect(self.quit)
tray_menu.addAction(show_action); tray_menu.addAction(quit_action)
self.tray_icon.setContextMenu(tray_menu)
self.tray_icon.activated.connect(self.tray_icon_activated); self.tray_icon.show()
with open(os.path.join(self.output_dir, "system_log.txt"), "w", encoding="utf-8") as f: pass
self.window.log("===============================")
self.window.log("システム起動: Ver 104.0 究極解放版")
self.window.log("===============================")
self.window.out_folder_label.setText(self.output_dir)
self.window.out_folder_btn.clicked.connect(self.select_output_folder)
folders_set = False
for i in range(3):
self.window.folder_btns[i].clicked.connect(lambda checked, idx=i: self.select_folder(idx))
self.update_folder_btn_text(i)
if self.monitor_folders[i]: folders_set = True
if not folders_set: self.window.status_label.setText("状態: 読込元フォルダ未設定")
self.window.copy_btn.clicked.connect(self.copy_logs)
self.window.pause_btn.clicked.connect(self.pause_parsing)
self.window.play_btn.clicked.connect(self.resume_auto_scan)
self.window.resume_btn.clicked.connect(self.force_scan)
self.window.stop_btn.clicked.connect(self.stop_parsing)
self.window.clean_btn.clicked.connect(self.clean_old_data)
self.window.clear_btn.clicked.connect(self.clear_cache)
self.window.exit_btn.clicked.connect(self.quit)
self.timer = QTimer(); self.timer.timeout.connect(self.run_parser); self.timer.start(10000)
self.is_processing = False; self.run_parser(); self.window.show()
def tray_icon_activated(self, reason):
if reason == QSystemTrayIcon.ActivationReason.DoubleClick: self.restore_window()
def restore_window(self): self.window.showNormal(); self.window.activateWindow()
def show_tray_message(self):
self.tray_icon.showMessage("エンジン稼働中", "ウィンドウを隠しました。終了時は右クリックからどうぞ。", QIcon(self.tray_icon.icon()), 3000)
def select_output_folder(self):
global CURRENT_OUTPUT_DIR
folder = QFileDialog.getExistingDirectory(self.window, "出力先フォルダを選択", self.output_dir)
if folder:
self.output_dir = folder; CURRENT_OUTPUT_DIR = folder
self.settings.setValue("output_dir", folder); self.window.out_folder_label.setText(folder)
self.window.log(f"💾 出力先フォルダを更新しました: {folder}"); self.run_parser()
def clean_old_data(self):
now = time.time()
deleted = 0
self.window.log("🧹 24時間経過した古いデータを検索中...")
for f in glob.glob(os.path.join(self.output_dir, "*.npz")):
try:
if os.path.isfile(f) and (now - os.path.getmtime(f)) > 24 * 3600:
os.remove(f); deleted += 1
except: pass
self.window.log(f"✅ {deleted}件の古いデータを自動削除しました")
def clear_cache(self):
reply = QMessageBox.question(self.window, "確認", "バグで壊れたデータをすべて削除し、再抽出しますか?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.Yes:
self.timer.stop(); self.window.log("🗑️ キャッシュを全削除中...")
for f in glob.glob(os.path.join(self.output_dir, "*.npz")) + glob.glob(os.path.join(self.output_dir, "*.json")) + glob.glob(os.path.join(self.output_dir, "*.bin")):
try: os.remove(f)
except: pass
for d in self.monitor_folders:
if d:
for idx_f in glob.glob(os.path.join(d, '**', '*.idx'), recursive=True):
try: os.remove(idx_f)
except: pass
self.last_reported = {}; self.window.log(f"✨ クリーンな状態で再抽出を開始します!"); self.run_parser(); self.timer.start(10000)
def select_folder(self, index):
folder = QFileDialog.getExistingDirectory(self.window, "読込元フォルダ選択")
if folder:
self.monitor_folders[index] = folder; self.settings.setValue(f"watch_dir_{index+1}", folder)
self.update_folder_btn_text(index); self.window.log(f"フォルダ{index+1}を更新: {folder}"); self.run_parser()
def update_folder_btn_text(self, i):
f = self.monitor_folders[i]; self.window.folder_btns[i].setText(f"📁 {os.path.basename(f)}" if f else f"📁 未設定 {i+1}")
def copy_logs(self): QApplication.clipboard().setText("\n".join([self.window.log_list.item(i).text() for i in range(self.window.log_list.count())]))
def pause_parsing(self): self.timer.stop(); self.window.log("⏸ 一時停止しました")
def resume_auto_scan(self): self.timer.start(10000); self.window.log("▶ 再生(自動スキャン)を再開しました"); self.run_parser()
def force_scan(self): self.window.log("手動強制スキャン開始..."); self.timer.start(10000); self.run_parser()
def stop_parsing(self): self.timer.stop(); self.window.log("⏹ 停止しました")
def run_parser(self):
if self.is_processing: return
if not any(self.monitor_folders): self.window.status_label.setText("状態: 読込元フォルダ未設定"); return
self.is_processing = True
self.worker = DataParserThread(self.monitor_folders, self.output_dir, self.last_reported)
self.worker.log_signal.connect(self.window.log)
self.worker.progress_signal.connect(lambda v, t: self.window.status_label.setText(f"状態: {t}"))
self.worker.finished_signal.connect(self.on_parse_finished)
self.worker.start()
def on_parse_finished(self, has_new_data):
self.is_processing = False; self.window.status_label.setText("状態: 待機中 (自動スキャン継続中)")
if name == ‘main‘:
mp.freeze_support()
app = TrayApp(sys.argv)
sys.exit(app.exec())

コメント