""" 동행복권(dhlottery.co.kr) 로또 6/45 자동 구매 스크립트 - 매주 금요일 자동 실행 (cron/스케줄러 연동) - 5게임 자동 구매 - Selenium 기반 웹 자동화 사전 준비: pip install selenium Chrome 브라우저 + ChromeDriver 설치 필요 """ import os import sys import time import json import logging import urllib.request from datetime import datetime # .env 파일 자동 로드 try: from dotenv import load_dotenv load_dotenv() except ImportError: pass from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.keys import Keys # ───────────────────────────────────────────── # 설정 # ───────────────────────────────────────────── CONFIG = { "USER_ID": os.environ.get("LOTTO_USER_ID", ""), "USER_PW": os.environ.get("LOTTO_USER_PW", ""), "BUY_COUNT": 1, # 구매 게임 수 (1~10) "HEADLESS": True, # True: 브라우저 창 숨김, False: 브라우저 창 표시 "LOG_FILE": "logs/lotto_log.json", "TELEGRAM_BOT_TOKEN": os.environ.get("TELEGRAM_BOT_TOKEN", ""), "TELEGRAM_CHAT_ID": os.environ.get("TELEGRAM_CHAT_ID", ""), } # 로깅 설정 logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.StreamHandler(), logging.FileHandler("logs/lotto_auto.log", encoding="utf-8"), ], ) logger = logging.getLogger(__name__) def create_driver(): """Chrome WebDriver 생성""" options = Options() # CI 환경에서 setup-chrome이 설치한 Chrome 바이너리 경로 지정 chrome_bin = os.environ.get("CHROME_BIN", "") if chrome_bin: options.binary_location = chrome_bin if CONFIG["HEADLESS"]: options.add_argument("--headless=new") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--disable-gpu") options.add_argument("--disable-software-rasterizer") options.add_argument("--remote-debugging-port=9222") options.add_argument("--user-data-dir=/tmp/chrome-user-data") options.add_argument("--window-size=1920,1080") # 자동화 탐지 우회 options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) driver = webdriver.Chrome(options=options) driver.execute_cdp_cmd( "Page.addScriptToEvaluateOnNewDocument", {"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"}, ) driver.set_page_load_timeout(30) # 페이지 로드 최대 30초 driver.set_script_timeout(30) # JS 실행 최대 30초 driver.implicitly_wait(5) return driver def login(driver): """동행복권 사이트 로그인""" from selenium.common.exceptions import TimeoutException logger.info("로그인 시도 중...") try: driver.get("https://www.dhlottery.co.kr/login") except TimeoutException: logger.warning("로그인 페이지 로드 타임아웃 — 로드된 부분으로 계속 진행") time.sleep(2) # 아이디/비밀번호 입력 wait = WebDriverWait(driver, 10) id_input = wait.until(EC.element_to_be_clickable((By.ID, "inpUserId"))) id_input.click() id_input.send_keys(CONFIG["USER_ID"]) pw_input = wait.until(EC.element_to_be_clickable((By.ID, "inpUserPswdEncn"))) pw_input.click() pw_input.send_keys(CONFIG["USER_PW"]) # 로그인 버튼 클릭 login_btn = wait.until(EC.element_to_be_clickable((By.ID, "btnLogin"))) login_btn.click() # /login이 URL에서 완전히 사라질 때까지 대기 (최대 15초) try: WebDriverWait(driver, 15).until(lambda d: "/login" not in d.current_url) except: pass logger.info(f"로그인 후 URL: {driver.current_url}") # 로그인 성공 확인 (URL이 /login에서 벗어나면 성공) if "/login" in driver.current_url: logger.error("로그인 실패! 아이디/비밀번호를 확인하세요.") return False logger.info("로그인 성공!") return True def check_balance(driver): """예치금 잔액 확인""" driver.get("https://www.dhlottery.co.kr/mypage/home") time.sleep(2) try: balance_elem = driver.find_element(By.CSS_SELECTOR, "#totalAmt") balance_text = balance_elem.text.replace(",", "").replace("원", "").strip() balance = int(balance_text) if balance_text.isdigit() else 0 logger.info(f"현재 예치금 잔액: {balance:,}원") return balance except Exception as e: logger.warning(f"잔액 확인 실패: {e}") return -1 def buy_lotto(driver, game_count=5): """ 로또 6/45 자동 구매 game_count: 구매할 게임 수 (1~10) """ logger.info(f"로또 {game_count}게임 자동 구매 시작...") # 로또 구매 페이지 이동 (타임아웃 시 로드된 부분으로 계속 진행) from selenium.common.exceptions import TimeoutException try: driver.get("https://ol.dhlottery.co.kr/olotto/game/game645.do") except TimeoutException: logger.warning("페이지 로드 타임아웃 — 로드된 부분으로 계속 진행") time.sleep(2) # iframe 전환 (로또 구매 페이지는 iframe 내부에 있음) try: wait = WebDriverWait(driver, 10) def close_layer_alert_if_present(timeout_s: float = 1.5) -> bool: """
가 떠 있으면 내부 '확인' 버튼을 클릭(= closepopupLayerAlert())해서 닫는다. """ from selenium.common.exceptions import TimeoutException, WebDriverException try: layer = WebDriverWait(driver, timeout_s).until( EC.presence_of_element_located((By.ID, "popupLayerAlert")) ) except TimeoutException: return False try: if not layer.is_displayed(): return False except WebDriverException: # stale 등 예외면 "없음"으로 간주 return False # 우선 버튼 클릭 시도 (DOM: #popupLayerAlert .btns input[value='확인']) try: btn = driver.find_element( By.CSS_SELECTOR, "#popupLayerAlert .btns input[type='button'][value='확인']", ) if btn.is_displayed() and btn.is_enabled(): btn.click() time.sleep(0.3) return True except Exception: pass # fallback: 사이트의 close 함수 직접 호출 try: driver.execute_script( "if (typeof closepopupLayerAlert === 'function') { closepopupLayerAlert(); }" ) time.sleep(0.3) return True except Exception: return False # 모든 레이어 팝업을 JS로 직접 숨기기 (z-index 오버레이 완전 제거) driver.execute_script(""" var popups = document.querySelectorAll( '#popupLayerAlert, #popupLayerConfirm, .layer-alert, .layer-popup, .pop_wrap' ); popups.forEach(function(el) { el.style.display = 'none'; }); """) time.sleep(0.5) logger.info("팝업 레이어 숨김 처리 완료") # "자동" 탭 선택 — onclick 함수(selectWayTab)를 직접 호출 driver.execute_script("selectWayTab(1);") time.sleep(1) time.sleep(1) # 게임 수 설정 (드롭다운에서 선택) from selenium.webdriver.support.ui import Select try: select_elem = driver.find_element(By.ID, "amoundApply") select = Select(select_elem) select.select_by_value(str(game_count)) time.sleep(0.5) except Exception: # 드롭다운이 없는 경우 자동번호 버튼을 game_count번 클릭 for i in range(game_count): auto_select_btn = driver.find_element(By.ID, "btnSelectNum") auto_select_btn.click() time.sleep(0.5) # "자동번호 선택" 버튼 클릭 try: select_num_btn = driver.find_element(By.ID, "btnSelectNum") select_num_btn.click() time.sleep(1) except: pass # "구매하기" 버튼 클릭 # 구매 직전에 레이어 알림이 뜨는 경우 먼저 닫기 if close_layer_alert_if_present(): logger.info("구매 전 popupLayerAlert 감지 — 닫기 처리 완료") buy_btn = wait.until( EC.element_to_be_clickable((By.ID, "btnBuy")) ) buy_btn.click() time.sleep(2) # 구매 확인 팝업 "확인" 클릭 confirm_btn = wait.until( EC.element_to_be_clickable((By.CSS_SELECTOR, "#popupLayerConfirm input[value='확인']")) ) confirm_btn.click() time.sleep(3) logger.info("구매 요청 완료! 구매 결과를 확인합니다...") # 구매 결과 확인 result = extract_purchase_result(driver) return result except Exception as e: logger.error(f"구매 중 오류 발생: {e}") return None def extract_purchase_result(driver): """구매 결과(번호) 추출""" result = { "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "games": [], "status": "unknown", } try: time.sleep(2) # 회차 / 추첨일 추출 try: result["round"] = driver.find_element(By.ID, "buyRound").text result["draw_date"] = driver.find_element(By.ID, "drawDate").text except: pass # 번호 추출: #reportRow li div.nums span game_rows = driver.find_elements(By.CSS_SELECTOR, "#reportRow li") for row in game_rows: nums = row.find_elements(By.CSS_SELECTOR, "div.nums span") numbers = [int(n.text) for n in nums if n.text.strip().isdigit()] if numbers: result["games"].append(numbers) if result["games"]: result["status"] = "success" logger.info(f"구매 성공! {len(result['games'])}게임") for i, game in enumerate(result["games"], 1): logger.info(f" 게임 {i}: {game}") else: result["status"] = "completed_but_numbers_unknown" logger.info("번호 추출 실패. 마이페이지에서 확인하세요.") except Exception as e: result["status"] = "error" result["error"] = str(e) logger.warning(f"결과 추출 중 오류: {e}") return result def send_telegram(result): """Telegram Bot으로 구매 결과 알림 전송""" token = CONFIG["TELEGRAM_BOT_TOKEN"] chat_id = CONFIG["TELEGRAM_CHAT_ID"] if not token or not chat_id: return status = result.get("status", "unknown") games = result.get("games", []) round_no = result.get("round", "") draw_date = result.get("draw_date", "") if status == "success" and games: games_text = "\n".join( f" 게임 {i}: {' '.join(str(n) for n in nums)}" for i, nums in enumerate(games, 1) ) text = ( f"🎟 *로또 구매 완료\!*\n" f"회차: {round_no} 추첨일: {draw_date}\n" f"{games_text}" ) elif status == "failed": text = f"⚠️ *로또 구매 실패*\n{result.get('error', '')}" else: text = f"❌ *로또 구매 오류*\n{result.get('error', status)}" try: payload = json.dumps({ "chat_id": int(chat_id), "text": text, "parse_mode": "MarkdownV2", }).encode("utf-8") req = urllib.request.Request( f"https://api.telegram.org/bot{token}/sendMessage", data=payload, headers={"Content-Type": "application/json"}, ) urllib.request.urlopen(req, timeout=10) logger.info("Telegram 알림 전송 완료") except Exception as e: logger.warning(f"Telegram 알림 전송 실패: {e}") def save_log(result): """구매 기록 저장""" log_file = CONFIG["LOG_FILE"] history = [] if os.path.exists(log_file): try: with open(log_file, "r", encoding="utf-8") as f: history = json.load(f) except: history = [] history.append(result) with open(log_file, "w", encoding="utf-8") as f: json.dump(history, f, ensure_ascii=False, indent=2) logger.info(f"구매 기록이 {log_file}에 저장되었습니다.") def main(): """메인 실행 함수""" logger.info("=" * 50) logger.info("로또 6/45 자동 구매 스크립트 시작") logger.info(f"실행 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") logger.info("=" * 50) # 아이디/비밀번호 확인 if CONFIG["USER_ID"] == "여기에_아이디_입력" or CONFIG["USER_PW"] == "여기에_비밀번호_입력": logger.error("아이디/비밀번호가 설정되지 않았습니다!") logger.error("환경변수(LOTTO_USER_ID, LOTTO_USER_PW)를 설정하거나 스크립트 내 CONFIG를 수정하세요.") sys.exit(1) driver = None try: driver = create_driver() # 1. 로그인 if not login(driver): logger.error("로그인 실패로 종료합니다.") sys.exit(1) # 2. 잔액 확인 balance = check_balance(driver) cost = CONFIG["BUY_COUNT"] * 1000 if balance >= 0 and balance < cost: logger.error(f"예치금 부족! 현재: {balance:,}원, 필요: {cost:,}원") logger.error("동행복권 사이트에서 예치금을 충전해주세요.") send_telegram({ "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "status": "failed", "error": f"예치금 부족! 현재: {balance:,}원, 필요: {cost:,}원", }) sys.exit(1) # 3. 로또 구매 result = buy_lotto(driver, CONFIG["BUY_COUNT"]) # 4. 결과 저장 및 Slack 알림 if result: save_log(result) send_telegram(result) else: failed = { "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "status": "failed", "error": "구매 결과를 확인할 수 없습니다.", } save_log(failed) send_telegram(failed) logger.info("스크립트 실행 완료!") except Exception as e: logger.error(f"예상치 못한 오류: {e}") error_result = { "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "status": "error", "error": str(e), } save_log(error_result) send_telegram(error_result) finally: if driver: driver.quit() logger.info("브라우저 종료") if __name__ == "__main__": main()