├── README.md ├── .vscode └── launch.json ├── .gitignore └── ticketmacro.py /README.md: -------------------------------------------------------------------------------- 1 | ## 티켓 예매 매크로 2 | 3 | - 학습 겸 제작자가 티켓 예매를 하기 위해 제작합니다 4 | 5 | - 매크로 이용은 불법이 아니지만, 매크로를 이용하여 금전적 이득을 취하는 경우 불법입니다. 6 | 7 | - **참고 페이지** 8 | - https://gem1n1.tistory.com/38 9 | - https://github.com/codechacha/InterparkTicketMacro -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File (Integrated Terminal)", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal" 13 | }, 14 | { 15 | "name": "Python: Remote Attach", 16 | "type": "python", 17 | "request": "attach", 18 | "port": 5678, 19 | "host": "localhost", 20 | "pathMappings": [ 21 | { 22 | "localRoot": "${workspaceFolder}", 23 | "remoteRoot": "." 24 | } 25 | ] 26 | }, 27 | { 28 | "name": "Python: Module", 29 | "type": "python", 30 | "request": "launch", 31 | "module": "enter-your-module-name-here", 32 | "console": "integratedTerminal" 33 | }, 34 | { 35 | "name": "Python: Django", 36 | "type": "python", 37 | "request": "launch", 38 | "program": "${workspaceFolder}/manage.py", 39 | "console": "integratedTerminal", 40 | "args": [ 41 | "runserver", 42 | "--noreload", 43 | "--nothreading" 44 | ], 45 | "django": true 46 | }, 47 | { 48 | "name": "Python: Flask", 49 | "type": "python", 50 | "request": "launch", 51 | "module": "flask", 52 | "env": { 53 | "FLASK_APP": "app.py" 54 | }, 55 | "args": [ 56 | "run", 57 | "--no-debugger", 58 | "--no-reload" 59 | ], 60 | "jinja": true 61 | }, 62 | { 63 | "name": "Python: Current File (External Terminal)", 64 | "type": "python", 65 | "request": "launch", 66 | "program": "${file}", 67 | "console": "externalTerminal" 68 | } 69 | ] 70 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | debug.log 3 | 4 | # Created by https://www.gitignore.io/api/python,windows,visualstudiocode 5 | # Edit at https://www.gitignore.io/?templates=python,windows,visualstudiocode 6 | 7 | ### Python ### 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | pip-wheel-metadata/ 31 | share/python-wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # pipenv 77 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 78 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 79 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 80 | # install all needed dependencies. 81 | #Pipfile.lock 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | .spyproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | # Mr Developer 97 | .mr.developer.cfg 98 | .project 99 | .pydevproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | .dmypy.json 107 | dmypy.json 108 | 109 | # Pyre type checker 110 | .pyre/ 111 | 112 | ### VisualStudioCode ### 113 | .vscode/* 114 | !.vscode/settings.json 115 | !.vscode/tasks.json 116 | !.vscode/launch.json 117 | !.vscode/extensions.json 118 | 119 | ### VisualStudioCode Patch ### 120 | # Ignore all local history of files 121 | .history 122 | 123 | ### Windows ### 124 | # Windows thumbnail cache files 125 | Thumbs.db 126 | Thumbs.db:encryptable 127 | ehthumbs.db 128 | ehthumbs_vista.db 129 | 130 | # Dump file 131 | *.stackdump 132 | 133 | # Folder config file 134 | [Dd]esktop.ini 135 | 136 | # Recycle Bin used on file shares 137 | $RECYCLE.BIN/ 138 | 139 | # Windows Installer files 140 | *.cab 141 | *.msi 142 | *.msix 143 | *.msm 144 | *.msp 145 | 146 | # Windows shortcuts 147 | *.lnk 148 | 149 | # End of https://www.gitignore.io/api/python,windows,visualstudiocode -------------------------------------------------------------------------------- /ticketmacro.py: -------------------------------------------------------------------------------- 1 | # library import 2 | import time 3 | from datetime import datetime 4 | from selenium import webdriver 5 | from selenium.webdriver.common.keys import Keys 6 | from selenium.webdriver.common.by import By 7 | from selenium.webdriver.support.ui import WebDriverWait 8 | from selenium.webdriver.support import expected_conditions as EC 9 | import pause 10 | from decouple import config 11 | from bs4 import BeautifulSoup 12 | 13 | ## 예매 정보 14 | # 예매 url 15 | url = config('URL') 16 | play_code = url.split('=')[-1] 17 | book_url = config('BOOK_URL') 18 | 19 | # 예매 날짜 정보 (Datetime 객체 생성 용) 20 | start_year = int(config('START_YEAR')) 21 | start_month = int(config('START_MONTH')) 22 | start_date = int(config('START_DATE')) 23 | start_hour = int(config('START_HOUR')) 24 | start_min = int(config('START_MIN')) 25 | 26 | # 예매 날짜 정보 (예매창 날짜 선택용) 27 | book_date = config('BOOK_DATE') 28 | user_datetime = datetime(year = start_year, month = start_month, day = start_date, hour = start_hour, minute = start_min, second=0, microsecond=850000) 29 | 30 | # 로그인 할 회원 정보 31 | user_id = config('USER_ID') 32 | user_pw = config('USER_PW') 33 | 34 | # 드라이버 실행 35 | driver = webdriver.Chrome('C:/ai/program/chromedriver') 36 | wait = WebDriverWait(driver, 10) 37 | driver.get(url) 38 | 39 | # 로그인하기 40 | driver.find_element_by_css_selector('#logstatus').click() 41 | # WebDriverWait(driver, 10) 42 | driver.switch_to.frame(driver.find_element_by_tag_name("iframe")) 43 | time.sleep(0.5) 44 | 45 | id_input = driver.find_element_by_id('userId').send_keys(user_id) 46 | pw_input = driver.find_element_by_id('userPwd').send_keys(user_pw) 47 | 48 | driver.find_element_by_css_selector('#btn_login').click() 49 | time.sleep(0.5) 50 | 51 | # 공연 기간 조회 52 | play_period=driver.find_element_by_css_selector('.info_Lst > li > dl > dd > span').text 53 | playDateList=[] 54 | 55 | # 공연 기간이 2일 이상인 경우 56 | try: 57 | playPeriodList= play_period.split(' ~ ') 58 | play_interval=int(playPeriodList[-1].split('.')[2]) - int(playPeriodList[0].split('.')[2]) 59 | 60 | # 공연 기간이 2일인 경우 61 | if play_interval == 1: 62 | for date in playPeriodList: 63 | playDateList.append(date.replace('.','')) 64 | 65 | # 공연 기간이 3일인 경우 66 | elif play_interval == 2: 67 | for i in range(0, 3): 68 | ym = playPeriodList[0][:-2].replace('.','') 69 | f_day = playPeriodList[0][-2:].replace('.','') 70 | day = int(f_day) + i 71 | playDateList.append(ym + str(day)) 72 | 73 | # 공연 기간이 4일 이상인 경우 74 | else: 75 | print('공연 기간이 4일 이상임, 예매팝업창에서 공연일정 확인 후 선택할 예정') 76 | 77 | # 공연 기간이 1일인 경우 78 | except: 79 | playDateList=play_period.replace('.','') 80 | 81 | # 코드 구동 확인 82 | for date in playDateList: 83 | print('공연일정', date) 84 | 85 | # 예매 팝업창에서 예매 일자 선택에 사용할 변수 할당 86 | book_index = -1 87 | # 공연 기간과 예매일자(book_date)가 일치하는 지 확인 88 | if playDateList is not None: 89 | for i in range(0, len(playDateList)): 90 | # 입력한 예매일자와 공연 정보가 일치하는 경우 91 | if book_date == playDateList[i]: 92 | book_index = i 93 | print("예매 정보 일치") 94 | break 95 | if book_index == -1: 96 | print('예매일정을 잘못입력함 다시입력하셈') 97 | 98 | # 원하는 시간에 예매창 새로고침 99 | pause.until(user_datetime) 100 | 101 | # 예매창 실행 102 | print(book_url + play_code + '&PlayDate=' + book_date) 103 | book_direct_url = book_url + play_code + '&PlayDate=' + book_date 104 | driver.get(book_direct_url) 105 | 106 | # 다음단계, 2단계 넘어가기 107 | driver.execute_script("javascript:fnNextStep('P');") 108 | 109 | # 좌석 선택하기 110 | seatCheck = False 111 | 112 | try: 113 | driver.switch_to.default_content() 114 | frame = driver.find_element_by_id('ifrmSeat') 115 | driver.switch_to.frame(frame) 116 | 117 | # 안심예매문자 입력창 감지 118 | print('안심예매문자 감지 코드 실행') 119 | time.sleep(0.3) 120 | WebDriverWait(driver, 30).until(EC.invisibility_of_element_located((By.ID, 'divRecaptcha'))) 121 | 122 | # 미니맵 존재여부 검사 123 | try: 124 | wait.until(EC.presence_of_element_located((By.ID, 'ifrmSeatView'))) 125 | frame = driver.find_element_by_id('ifrmSeatView') 126 | driver.switch_to.frame(frame) 127 | wait.until(EC.presence_of_element_located((By.NAME, 'Map'))) 128 | bs4 = BeautifulSoup(driver.page_source, 'html.parser') 129 | elem = bs4.find('map') 130 | except: 131 | elem = None 132 | 133 | # time.sleep(0.5) 134 | 135 | # 좌석 프레임 받아오기 136 | driver.switch_to.default_content() 137 | wait.until(EC.presence_of_element_located((By.ID, 'ifrmSeat'))) 138 | frame = driver.find_element_by_id('ifrmSeat') 139 | driver.switch_to.frame(frame) 140 | frame = driver.find_element_by_id('ifrmSeatDetail') 141 | driver.switch_to.frame(frame) 142 | 143 | # 좌석 정보 읽기 144 | bs4 = BeautifulSoup(driver.page_source, 'html.parser') 145 | seatList = bs4.find_all('img', class_='stySeat') 146 | print('available seat list number: {}'.format(len(seatList))) 147 | 148 | # 좌석 존재 여부 체크 149 | for seat in seatList: 150 | # seat = seatList[i] 151 | 152 | # 선택한 미니맵에 좌석 존재할 경우 153 | if seat is not None: 154 | 155 | # 원하는 구역 좌석 선택하기 156 | # title 태그 안("-") 기호로 나눈 뒤 157 | # 뒤에서 두번째 있는 문자열(예/ B구역)로 구역을 검사하고 158 | # 맨 마지막에 있는 숫자(좌석 번호)를 추출해서 검사하기 159 | # 우선 구역 > 앞자리 우선 순으로 좌석을 선택할 수 있게 로직 짜기 160 | 161 | # 좌석 선택하기 162 | try: 163 | driver.execute_script(seat['onclick'] + ";") 164 | # 2단계 프레임 받아오기 165 | driver.switch_to.default_content() 166 | wait.until(EC.presence_of_element_located((By.ID, 'ifrmSeat'))) 167 | frame = driver.find_element_by_id('ifrmSeat') 168 | driver.switch_to.frame(frame) 169 | 170 | # 3단계 넘어가기 171 | driver.execute_script("javascript:fnSelect();") 172 | seatCheck=True 173 | 174 | # 이선좌 경고창 감지 175 | try: 176 | alert = driver.switch_to.alert() 177 | alert.accept() 178 | time.sleep(0.5) 179 | seatCheck = False 180 | continue 181 | except: 182 | elem = '' 183 | print('no alert window or got exception') 184 | 185 | break 186 | 187 | except: 188 | print('try to move to fianl stage. but failed: {}'.format(seat)) 189 | 190 | # 선택한 미니맵에 좌석이 존재하지 않는 경우 191 | # else: 192 | # driver.switch_to.default_content() 193 | # frame = driver.find_element_by_id('ifrmSeat') 194 | # driver.switch_to.frame(frame) 195 | # frame = driver.find_element_by_id('ifrmSeatView') 196 | # driver.switch_to.frame(frame) 197 | # wait.until(EC.presence_of_element_located((By.NAME, 'Map'))) 198 | # bs4 = BeautifulSoup(driver.page_source, 'html.parser') 199 | # map = bs4.find('map') 200 | 201 | except: 202 | print('got unexpected except') --------------------------------------------------------------------------------