332 lines
12 KiB
Python
332 lines
12 KiB
Python
"""
|
||
修正版データ移行スクリプト
|
||
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()
|