""" 修正版データ移行スクリプト gifurogeデータベースからrogdbデータベースへの正確な移行を行う UTCからJSTに変換して移行 """ import psycopg2 from PIL import Image import PIL.ExifTags from datetime import datetime, timedelta import pytz import os import re def get_gps_from_image(image_path): """ 画像ファイルからGPS情報を抽出する Returns: (latitude, longitude) または取得できない場合は (None, None) """ try: with Image.open(image_path) as img: exif = { PIL.ExifTags.TAGS[k]: v for k, v in img._getexif().items() if k in PIL.ExifTags.TAGS } if 'GPSInfo' in exif: gps_info = exif['GPSInfo'] # 緯度の計算 lat = gps_info[2] lat = lat[0] + lat[1]/60 + lat[2]/3600 if gps_info[1] == 'S': lat = -lat # 経度の計算 lon = gps_info[4] lon = lon[0] + lon[1]/60 + lon[2]/3600 if gps_info[3] == 'W': lon = -lon return lat, lon except Exception as e: print(f"GPS情報の抽出に失敗: {e}") return None, None def convert_utc_to_jst(utc_datetime): """ UTCタイムスタンプをJSTに変換する Args: utc_datetime: UTC時刻のdatetimeオブジェクト Returns: JST時刻のdatetimeオブジェクト """ if utc_datetime is None: return None # UTCタイムゾーンを設定 if utc_datetime.tzinfo is None: utc_datetime = pytz.UTC.localize(utc_datetime) # JSTに変換 jst = pytz.timezone('Asia/Tokyo') jst_datetime = utc_datetime.astimezone(jst) # タイムゾーン情報を削除してnaive datetimeとして返す return jst_datetime.replace(tzinfo=None) def parse_goal_time(goal_time_str, event_date, create_at=None): """ goal_time文字列を正しいdatetimeに変換する Args: goal_time_str: "14:58" 形式の時刻文字列 event_date: イベント日付 create_at: goal_timeが空の場合に使用するタイムスタンプ Returns: datetime object または None """ # goal_timeが空の場合はcreate_atを使用(UTCからJSTに変換) if not goal_time_str or goal_time_str.strip() == '': if create_at: return convert_utc_to_jst(create_at) return None try: # "HH:MM" または "HH:MM:SS" 形式の時刻をパース(JST時刻として扱う) if re.match(r'^\d{1,2}:\d{2}(:\d{2})?$', goal_time_str.strip()): time_parts = goal_time_str.strip().split(':') hour = int(time_parts[0]) minute = int(time_parts[1]) second = int(time_parts[2]) if len(time_parts) > 2 else 0 # イベント日付と結合(JST時刻として扱うため変換なし) result_datetime = event_date.replace(hour=hour, minute=minute, second=second, microsecond=0) # 深夜の場合は翌日に調整 if hour < 6: # 午前6時以前は翌日とみなす result_datetime += timedelta(days=1) return result_datetime # すでにdatetime形式の場合 elif 'T' in goal_time_str or ' ' in goal_time_str: return datetime.fromisoformat(goal_time_str.replace('T', ' ').replace('Z', '')) except Exception as e: print(f"時刻パースエラー: {goal_time_str} -> {e}") return None def get_event_date(event_code, target_cur): """ イベントコードからイベント開催日を取得する """ # イベントコード別の実際の開催日を定義 event_dates = { 'FC岐阜': datetime(2024, 10, 25).date(), '美濃加茂': datetime(2024, 5, 19).date(), '岐阜市': datetime(2023, 11, 19).date(), '大垣2': datetime(2023, 5, 14).date(), '各務原': datetime(2023, 10, 15).date(), '郡上': datetime(2023, 10, 22).date(), '中津川': datetime(2024, 4, 14).date(), '下呂': datetime(2024, 1, 21).date(), '多治見': datetime(2023, 11, 26).date(), '大垣': datetime(2023, 4, 16).date(), '揖斐川': datetime(2023, 12, 3).date(), '養老ロゲ': datetime(2023, 4, 23).date(), '高山': datetime(2024, 3, 10).date(), '大垣3': datetime(2024, 8, 4).date(), '各務原2': datetime(2024, 11, 10).date(), '多治見2': datetime(2024, 12, 15).date(), '下呂2': datetime(2024, 12, 1).date(), '美濃加茂2': datetime(2024, 11, 3).date(), '郡上2': datetime(2024, 12, 8).date(), '関ケ原2': datetime(2024, 9, 29).date(), '養老2': datetime(2024, 11, 24).date(), '高山2': datetime(2024, 12, 22).date(), } if event_code in event_dates: return event_dates[event_code] # デフォルト日付 return datetime(2024, 1, 1).date() def get_foreign_keys(zekken_number, event_code, cp_number, target_cur): """ team_id, event_id, checkpoint_idを取得する """ team_id = None event_id = None checkpoint_id = None # team_id を取得 try: target_cur.execute(""" SELECT t.id, t.event_id FROM rog_team t JOIN rog_newevent2 e ON t.event_id = e.id WHERE t.zekken_number = %s AND e.event_code = %s """, (zekken_number, event_code)) result = target_cur.fetchone() if result: team_id, event_id = result except Exception as e: print(f"Team ID取得エラー: {e}") # checkpoint_id を取得 try: target_cur.execute(""" SELECT c.id FROM rog_checkpoint c JOIN rog_newevent2 e ON c.event_id = e.id WHERE c.cp_number = %s AND e.event_code = %s """, (str(cp_number), event_code)) result = target_cur.fetchone() if result: checkpoint_id = result[0] except Exception as e: print(f"Checkpoint ID取得エラー: {e}") return team_id, event_id, checkpoint_id def migrate_gps_data(): """ GPSチェックインデータの移行 """ # コンテナ環境用の接続情報 source_db = { 'dbname': 'gifuroge', 'user': 'admin', 'password': 'admin123456', 'host': 'postgres-db', # Dockerサービス名 'port': '5432' } target_db = { 'dbname': 'rogdb', 'user': 'admin', 'password': 'admin123456', 'host': 'postgres-db', # Dockerサービス名 'port': '5432' } source_conn = None target_conn = None source_cur = None target_cur = None try: print("ソースDBへの接続を試みています...") source_conn = psycopg2.connect(**source_db) source_cur = source_conn.cursor() print("ソースDBへの接続が成功しました") print("ターゲットDBへの接続を試みています...") target_conn = psycopg2.connect(**target_db) target_cur = target_conn.cursor() print("ターゲットDBへの接続が成功しました") # 既存のrog_gpscheckinデータをクリア print("既存のGPSチェックインデータをクリアしています...") target_cur.execute("DELETE FROM rog_gpscheckin") target_conn.commit() print("既存データのクリアが完了しました") print("データの取得を開始します...") source_cur.execute(""" SELECT serial_number, zekken_number, event_code, cp_number, image_address, goal_time, late_point, create_at, create_user, update_at, update_user, buy_flag, colabo_company_memo FROM gps_information ORDER BY event_code, zekken_number, serial_number """) rows = source_cur.fetchall() print(f"取得したレコード数: {len(rows)}") processed_count = 0 error_count = 0 for row in rows: (serial_number, zekken_number, event_code, cp_number, image_address, goal_time, late_point, create_at, create_user, update_at, update_user, buy_flag, colabo_company_memo) = row try: # 関連IDを取得 team_id, event_id, checkpoint_id = get_foreign_keys(zekken_number, event_code, cp_number, target_cur) if not team_id or not event_id: print(f"スキップ: team_id={team_id}, event_id={event_id} for {zekken_number}/{event_code}") error_count += 1 continue # イベント日付を取得 event_date = get_event_date(event_code, target_cur) # 時刻を正しく変換(create_atも渡す) checkin_time = None record_time = None if goal_time: parsed_time = parse_goal_time(goal_time, datetime.combine(event_date, datetime.min.time()), create_at) if parsed_time: checkin_time = parsed_time record_time = parsed_time # goal_timeがない場合はcreate_atを使用(UTCからJSTに変換) if not checkin_time and create_at: checkin_time = convert_utc_to_jst(create_at) record_time = convert_utc_to_jst(create_at) elif not checkin_time: # 最後の手段としてデフォルト時刻 checkin_time = datetime.combine(event_date, datetime.min.time()) + timedelta(hours=12) record_time = checkin_time # GPS座標を取得 latitude, longitude = None, None if image_address and os.path.exists(image_address): latitude, longitude = get_gps_from_image(image_address) # rog_gpscheckinテーブルに挿入 target_cur.execute(""" INSERT INTO rog_gpscheckin ( event_code, zekken, serial_number, cp_number, lat, lng, checkin_time, record_time, mobserver_id, event_id, team_id, checkpoint_id ) VALUES ( %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s ) """, ( event_code, zekken_number, serial_number, str(cp_number), latitude, longitude, checkin_time, record_time, serial_number, event_id, team_id, checkpoint_id )) processed_count += 1 if processed_count % 100 == 0: print(f"処理済みレコード数: {processed_count}") target_conn.commit() except Exception as e: print(f"レコード処理エラー: {e} - {row}") error_count += 1 continue target_conn.commit() print(f"移行完了: {processed_count}件のレコードを処理しました") print(f"エラー件数: {error_count}件") except Exception as e: print(f"エラーが発生しました: {e}") if target_conn: target_conn.rollback() finally: if source_cur: source_cur.close() if target_cur: target_cur.close() if source_conn: source_conn.close() if target_conn: target_conn.close() print("すべての接続をクローズしました") if __name__ == "__main__": migrate_gps_data()