date_match = re.search(
r'(\d{4})[년.-](\d{1,2})[월.-](\d{1,2})',
text
)
if date_match:
year = date_match.group(1)
month = date_match.group(2).zfill(2)
day = date_match.group(3).zfill(2)
date_str = f"{year}-{month}-{day}"
예시 (Examples):
"2026년01월03일" → "2026-01-03"
"2026.01.03" → "2026-01-03"
"2026-1-3" → "2026-01-03"
설명 (Explanation):
(\d{4}): 정확히 4자리 숫자 (Exactly 4 digits - year)
[년.-]: "년", ".", "-" 중 하나 (One of these separators)
(\d{1,2}): 1-2자리 숫자 (1-2 digits - month/day)
패턴 3: 당첨번호 (Winning Numbers)
numbers = re.findall(r'\b([1-9]|[1-3][0-9]|4[0-5])\b', text)
winning_numbers = [int(n) for n in numbers[:6]]
예시 (Example):
입력 (Input): "당첨번호: 1, 4, 16, 23, 31, 41"
매칭 (Matches): ["1", "4", "16", "23", "31", "41"]
결과 (Result): [1, 4, 16, 23, 31, 41]
설명 (Explanation):
\b: 단어 경계 (Word boundary)
[1-9]: 1-9 (한 자리, Single digit 1-9)
[1-3][0-9]: 10-39 (두 자리, Two digits 10-39)
4[0-5]: 40-45 (40-45만, Only 40-45)
왜 이렇게 복잡한가? (Why so complex?)
\d+를 쓰면 2026, 12 같은 숫자도 매칭됨 (Matches unwanted numbers)
1-45 범위만 정확히 추출 필요 (Need exact 1-45 range)
패턴 4: 보너스 번호 (Bonus Number)
bonus = int(numbers[6]) if len(numbers) >= 7 else None
설명 (Explanation):
당첨번호 패턴과 동일, 7번째 숫자 사용 (Same pattern, use 7th number)
패턴 5: 당첨금 (Prize Amount)
prize_match = re.search(
r'(\d+(?:,\d{3})*(?:\.\d+)?)\s*(?:억|만|원)',
text
)
if prize_match:
amount_str = prize_match.group(1).replace(',', '')
# "23억 3,499만원" → 2,334,990,000
import re
from datetime import datetime
class LottoTextParser:
"""로또 텍스트 파서 (Lotto Text Parser)"""
def parse(self, text):
"""
텍스트에서 로또 데이터 추출 (Extract lottery data from text)
Args:
text: 로또 정보 텍스트 (Lottery info text)
Returns:
dict: 파싱 결과 (Parsed result)
"""
result = {
'round': self._extract_round(text),
'date': self._extract_date(text),
'numbers': self._extract_numbers(text),
'bonus': self._extract_bonus(text),
'prize': self._extract_prize(text),
'winners': self._extract_winners(text)
}
# 검증 (Validation)
result['is_valid'] = self._validate(result)
return result
def _extract_round(self, text):
"""회차 추출 (Extract round number)"""
match = re.search(r'(\d+)회', text)
return int(match.group(1)) if match else None
def _extract_date(self, text):
"""날짜 추출 (Extract date)"""
match = re.search(
r'(\d{4})[년.\-/](\d{1,2})[월.\-/](\d{1,2})',
text
)
if match:
year = match.group(1)
month = match.group(2).zfill(2)
day = match.group(3).zfill(2)
return f"{year}-{month}-{day}"
return None
def _extract_numbers(self, text):
"""당첨번호 추출 (Extract winning numbers)"""
numbers = re.findall(r'\b([1-9]|[1-3][0-9]|4[0-5])\b', text)
winning = [int(n) for n in numbers[:6]]
return sorted(winning) if len(winning) == 6 else None
def _extract_bonus(self, text):
"""보너스 번호 추출 (Extract bonus number)"""
numbers = re.findall(r'\b([1-9]|[1-3][0-9]|4[0-5])\b', text)
return int(numbers[6]) if len(numbers) >= 7 else None
def _extract_prize(self, text):
"""당첨금 추출 (Extract prize amount)"""
# "23억 3,499만원" 형식 처리 (Handle Korean format)
prize_match = re.search(
r'(\d+(?:,\d{3})*)\s*억',
text
)
if prize_match:
eok = int(prize_match.group(1).replace(',', ''))
amount = eok * 100000000 # 억 단위 (100 million)
# 만원 단위 추가 (Add 10,000 won units)
man_match = re.search(r'(\d+(?:,\d{3})*)\s*만', text)
if man_match:
man = int(man_match.group(1).replace(',', ''))
amount += man * 10000
return amount
return None
def _extract_winners(self, text):
"""당첨자 수 추출 (Extract winner count)"""
match = re.search(r'당첨자.*?(\d+)\s*명', text)
return int(match.group(1)) if match else None
def _validate(self, result):
"""검증 (Validate)"""
checks = [
result['round'] is not None,
result['date'] is not None,
result['numbers'] is not None,
len(result['numbers']) == 6 if result['numbers'] else False,
result['bonus'] is not None
]
return all(checks)
4. 실시간 파싱 UI (Real-time Parsing UI)
🎨 2열 레이아웃 설계 (2-Column Layout Design)
파일: src/web_app.py (일부, Partial)
def data_update_page():
"""🔄 데이터 업데이트 페이지 (Data Update Page)"""
st.title("🔄 데이터 업데이트")
# 3가지 방법 탭 (3 Methods Tabs)
tab1, tab2, tab3 = st.tabs([
"📋 텍스트 파싱 (Text Parsing)",
"🌐 자동 크롤링 (Auto Crawling)",
"✍️ 수동 입력 (Manual Input)"
])
with tab1:
text_parsing_method()
def text_parsing_method():
"""텍스트 파싱 방식 (Text Parsing Method)"""
st.markdown("### 📋 텍스트 파싱 방식")
st.info("""
💡 **사용 방법 (How to Use):**
1. 로또 웹사이트에서 정보 복사 (Copy info from lottery website)
2. 아래 입력창에 붙여넣기 (Paste into textarea below)
3. "파싱 실행" 클릭 (Click "Parse Text")
4. 결과 확인 후 저장 (Verify and save)
""")
# 2열 레이아웃 (2-Column Layout)
col1, col2 = st.columns(2)
with col1:
st.markdown("#### 📥 입력 영역 (Input Area)")
# 텍스트 입력 (Text Input)
text_input = st.text_area(
"로또 정보 텍스트 (Lottery Info Text)",
height=400,
placeholder="""예시 (Example):
1205회 로또 당첨번호
2026년01월03일 추첨
당첨번호: 1, 4, 16, 23, 31, 41
보너스: 2
1등 당첨금: 23억 3,499만원
1등 당첨자: 12명
""",
help="웹사이트에서 복사한 텍스트를 붙여넣으세요. (Paste copied text from website.)"
)
# 파싱 버튼 (Parse Button)
parse_clicked = st.button(
"🔍 파싱 실행 (Parse Text)",
type="primary",
use_container_width=True
)
with col2:
st.markdown("#### 📤 파싱 결과 (Parsing Result)")
if parse_clicked and text_input:
with st.spinner("파싱 중... (Parsing...)"):
# 파싱 실행 (Execute Parsing)
parser = LottoTextParser()
result = parser.parse(text_input)
if result['is_valid']:
# 성공 (Success)
st.success("✅ 파싱 성공! (Parsing Successful!)")
# 결과 표시 (Display Result)
st.markdown("**파싱된 데이터 (Parsed Data):**")
# 각 필드 표시 (Display Each Field)
st.text_input("회차 (Round)", value=result['round'],
disabled=True)
st.text_input("날짜 (Date)", value=result['date'],
disabled=True)
st.text_input("당첨번호 (Numbers)",
value=str(result['numbers']),
disabled=True)
st.text_input("보너스 (Bonus)", value=result['bonus'],
disabled=True)
if result['prize']:
st.text_input("당첨금 (Prize)",
value=f"{result['prize']:,}원",
disabled=True)
if result['winners']:
st.text_input("당첨자 (Winners)",
value=f"{result['winners']}명",
disabled=True)
# 검증 상태 (Validation Status)
st.success("🎯 검증 통과 (Validation Passed)")
# 저장 버튼 (Save Button)
if st.button("💾 CSV에 저장 (Save to CSV)",
type="primary",
use_container_width=True):
save_to_csv(result)
st.success("✅ 저장 완료! (Saved Successfully!)")
st.balloons()
else:
# 실패 (Failure)
st.error("❌ 파싱 실패 (Parsing Failed)")
st.warning("텍스트 형식을 확인해주세요. (Check text format.)")
# 디버그 정보 (Debug Info)
with st.expander("🐛 디버그 정보 (Debug Info)"):
st.json(result)
elif parse_clicked:
st.warning("⚠️ 텍스트를 입력해주세요. (Please enter text.)")
⚡ 실시간 피드백 (Real-time Feedback)
장점 (Advantages):
즉시 확인 (Instant Verification): 파싱 결과 즉시 표시
시각적 피드백 (Visual Feedback): 성공/실패 색상 구분
디버깅 편의 (Easy Debugging): 에러 발생 시 원인 파악 용이
5. 자동 백업 시스템 (Auto Backup System)
🛡️ 데이터 안전성 우선 (Data Safety First)
문제 (Problem):
잘못된 데이터 저장 시 원본 손실 (Data loss if wrong data saved)
복구 불가능 (Cannot recover)
해결책 (Solution):
모든 업데이트 전 자동 백업 (Auto-backup before every update)
타임스탬프 기반 버전 관리 (Timestamp-based versioning)
📁 백업 시스템 구현 (Backup System Implementation)
파일: src/data_updater.py (일부, Partial)
import os
import shutil
from datetime import datetime
class LottoDataUpdater:
"""로또 데이터 업데이터 (Lotto Data Updater)"""
def __init__(self, csv_path, backup_dir="Data/backups"):
self.csv_path = csv_path
self.backup_dir = backup_dir
# 백업 디렉토리 생성 (Create backup directory)
os.makedirs(backup_dir, exist_ok=True)
def create_backup(self):
"""
CSV 파일 백업 (Backup CSV file)
Returns:
str: 백업 파일 경로 (Backup file path)
"""
if not os.path.exists(self.csv_path):
raise FileNotFoundError(f"CSV file not found: {self.csv_path}")
# 타임스탬프 생성 (Generate timestamp)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 백업 파일명 (Backup filename)
base_name = os.path.basename(self.csv_path)
name, ext = os.path.splitext(base_name)
backup_name = f"{name}_backup_{timestamp}{ext}"
# 백업 경로 (Backup path)
backup_path = os.path.join(self.backup_dir, backup_name)
# 복사 (Copy)
shutil.copy2(self.csv_path, backup_path)
print(f"✅ Backup created: {backup_path}")
return backup_path
def add_new_round(self, round_data):
"""
새 회차 데이터 추가 (Add new round data)
Args:
round_data: dict with keys: round, date, numbers, bonus, etc.
"""
# 1. 백업 생성 (Create backup)
backup_path = self.create_backup()
try:
# 2. CSV 읽기 (Read CSV)
import pandas as pd
df = pd.read_csv(self.csv_path, encoding='utf-8-sig', skiprows=1)
# 3. 중복 확인 (Check duplicate)
if round_data['round'] in df['회차'].values:
raise ValueError(f"Round {round_data['round']} already exists!")
# 4. 새 행 추가 (Append new row)
new_row = {
'회차': round_data['round'],
'일자': round_data['date'],
'당첨번호#1': round_data['numbers'][0],
'당첨번호#2': round_data['numbers'][1],
'당첨번호#3': round_data['numbers'][2],
'당첨번호#4': round_data['numbers'][3],
'당첨번호#5': round_data['numbers'][4],
'당첨번호#6': round_data['numbers'][5],
'당첨번호#7': round_data['bonus'],
'1등 당첨액': round_data.get('prize'),
'1등 당첨자수': round_data.get('winners'),
# ... 기타 필드 (Other fields)
}
df = pd.concat([df, pd.DataFrame([new_row])], ignore_index=True)
# 5. 저장 (Save)
df.to_csv(self.csv_path, index=False, encoding='utf-8-sig')
print(f"✅ Round {round_data['round']} added successfully!")
except Exception as e:
# 에러 발생 시 백업에서 복구 (Restore from backup on error)
print(f"❌ Error: {e}")
print(f"🔄 Restoring from backup: {backup_path}")
shutil.copy2(backup_path, self.csv_path)
raise
def list_backups(self):
"""백업 파일 목록 (List backup files)"""
backups = []
for file in os.listdir(self.backup_dir):
if file.endswith('.csv') and 'backup' in file:
path = os.path.join(self.backup_dir, file)
backups.append({
'name': file,
'path': path,
'size': os.path.getsize(path),
'mtime': os.path.getmtime(path)
})
# 최신순 정렬 (Sort by newest)
backups.sort(key=lambda x: x['mtime'], reverse=True)
return backups
def restore_from_backup(self, backup_path):
"""백업에서 복구 (Restore from backup)"""
if not os.path.exists(backup_path):
raise FileNotFoundError(f"Backup not found: {backup_path}")
# 현재 파일도 백업 (Backup current file too)
current_backup = self.create_backup()
# 복구 (Restore)
shutil.copy2(backup_path, self.csv_path)
print(f"✅ Restored from: {backup_path}")
print(f"📦 Current file backed up as: {current_backup}")
def backup_management_page():
"""백업 관리 페이지 (Backup Management Page)"""
st.title("📦 백업 관리 (Backup Management)")
updater = LottoDataUpdater("../Data/645_251227.csv")
# 백업 목록 (Backup List)
backups = updater.list_backups()
if backups:
st.success(f"📊 총 {len(backups)}개 백업 파일 ({len(backups)} backup files)")
# 테이블로 표시 (Display as Table)
import pandas as pd
backup_df = pd.DataFrame([{
'파일명 (Filename)': b['name'],
'크기 (Size)': f"{b['size'] / 1024:.1f} KB",
'생성일시 (Created)': datetime.fromtimestamp(b['mtime']).strftime('%Y-%m-%d %H:%M:%S')
} for b in backups])
st.dataframe(backup_df, use_container_width=True)
# 복구 (Restore)
st.markdown("### 🔄 복구 (Restore)")
selected_backup = st.selectbox(
"복구할 백업 선택 (Select Backup to Restore)",
[b['name'] for b in backups]
)
if st.button("⚠️ 복구 실행 (Restore)", type="secondary"):
selected_path = next(b['path'] for b in backups if b['name'] == selected_backup)
with st.spinner("복구 중... (Restoring...)"):
updater.restore_from_backup(selected_path)
st.success("✅ 복구 완료! (Restore Complete!)")
st.info("🔄 앱을 새로고침하세요. (Refresh the app.)")
else:
st.info("📭 백업 파일이 없습니다. (No backup files.)")