2년간 해왔던 파이썬은 슬슬 그만두고 다른 언어로 가야지.... 생각하다가 드디어 Java로 넘어가는 것을 결정했다.

 

아무래도 우리나라에서 제일 중심적인 언어이고, 많이 쓰이기 때문에 자바로 결정했다.

 

자바가 끝나면 아마 C, C++ 순서대로 이어나가지 않을까 생각한다. 

 

그래서 아마 인터파크 매크로같은 경우에는 지금까지 보고된 문제만 해결하고 기능 업데이트는 힘들 수 있을 것 같다.

'프로그래밍 > Java' 카테고리의 다른 글

Java 공부 시작  (0) 2020.08.14

이번에 내 인터파크 매크로 코드를 보면 try ~ except 구문으로 떡칠되어 있는 모습을 볼 수 있다.

 

아마도 아래와 같은 형태일 것이다.

1
2
3
4
5
6
7
while True:
    try:
        pass # 실행할 구문
    except:
        continue
    else:
        break
cs

셀레니움은 웹페이지가 다 로딩되기 전에 요소를 찾으려고 하면 없다고 예외를 내뿜으면서 꺼진다. 

 

그러나 로딩 속도는 사용자 컴퓨터 성능이나 인터넷 속도 영향을 많이 받고, 때에 따라 다르기 때문에 항상 언제 정확히 몇초 후에 로딩이 될 것이라고 예측할 수가 없다. 

 

보통은 Explicit Waits을 쓰면 되지만, 그래도 불확실성이 완전히 해소된다는 보장도 없고, 내가 써 보니 잘 안되서 위와 같은 형태를 취하게 되었다.

 

해석하자면 일단 실행할 구문을 무한 루프 안에 가둬 놓고 예외가 나지 않을 때까지(셀레니움에서는 로딩이 끝나 정상적으로 요소를 찾을 수 있을 때까지) 실행한다. 그리고 예외 없이 정상적으로 실행되면 루프를 나간다.

 

그리고 몇 번 실행하고 더 이상 안되면 꺼버리고 싶을 때, 아래와 같이 for 구문을 사용하면 된다.

1
2
3
4
5
for i in range(0, time):
    try:
        pass
    except:
        continue
cs

무식한 방법처럼 보이지만, 제일 간단하고 정확하다. 오히려 될 때까지 시도한다는 점 때문에 일정 시간이 지나면 포기하는 Explicit Waits와는 다르게 어떻게든 예매창에 들어가기만 하면 좌석을 알아서 선택하고 나오게 된다.

 

사실 이 방법을 쓰기 전에 고민했는데, 이걸 쓰면 코드가 너무 지저분해지기 때문이다. 당장 구문만 빼고 들어가는 줄 수만 봐도 6줄이나 된다. 이게 몇개씩이나 있어서 코드 가독성을 해친다.

 

그냥 생각하기 싫을 때나 어떤 일이 있어도 절대로 프로그램이 종료되지 말아야 하는 경우에 쓸 수는 있어도, 가급적이면 다른 방법을 사용하는 것을 권한다.

지난번에 매크로를 올리고 정말 많은 오류가 보고되었다.

 

이렇게 복잡한 프로그램을 만들고 배포하긴 처음이라, 많이 당황했다.

 

거의 4개월만에 오류 패치를 했다. 물론 중간고사 끝나고 바로 기말고사라 급하게 해서 몇개 부탁주신 기능 구현은 하지 못했다.

 

그래서 고쳐진 부분과 추가된 부분에 대해 설명을 하고자 한다.

 

패치된 오류

안심예매 단계에서 꺼짐

회차가 두 개 이상일 경우 직접 선택해야 함

구역이 나눠져 있을 경우 작동 안됨

 

새로 바뀐 매크로

바뀐 매크로는 위와 같다.

 

여기서 추가된 부분은 회차와 구역 분리 항목이다.

 

회차의 경우에는 두 개 이상이 있는 경우가 있다.

 

이럴 경우 회차마다 순서대로 001, 002, 003... 이렇게 올라간다.

 

만약 두번째 회차를 원한다면 002라 적으면 된다(반드시 002여야 한다, 2 안됨, 02 안됨).

 

구역 분리같은 경우에 티켓팅할 공연장의 규모가 커서 구역을 선택하는 곳이 나올 때 이용할 수 있다.

 

구역 선택은 직접 해야 하며, 선택만 하면 그 다음은 알아서 한다.

 

아래는 빌드(exe) 파일과 코드이다. 코드는 직접 올리기 번거로워서 파일 형식으로 올렸다.

 

소스 코드 파일에는 pyinstaller 빌드를 위한 .spec 파일도 포함되어 있다.

 

*추가

연령 제한 알림 같은 안내가 있는 경우 수동으로 알림을 꺼야 하는 문제를 해결해 자동으로 끄게 했다.

 

*추가

JS alert가 뜨는 경우 알아서 끌 수 있게 변경했다.

 

빌드 파일

 

38.62 MB file on MEGA

 

mega.nz

소스 코드

 

6.4 KB file on MEGA

 

mega.nz

 

  1. 이전 댓글 더보기
  2. 2020.07.14 20:46

    비밀댓글입니다

  3. 2020.07.15 11:57

    비밀댓글입니다

    • 페이지다운 2020.07.15 18:35 신고

      제가 보기엔 다음날 하는 공연 때문에 뜨는 알림창 때문에 수동으로 선택한 것 같습니다. 그 부분은 수정하겠습니다.

  4. ㅇㅇ 2020.07.15 20:13

    로그인을 누르면 응용 프로그램이 종료되는데 해결방법 아시나요...?

    • 페이지다운 2020.07.15 20:15 신고

      확인하겠습니다.

    • 사람1 2020.07.16 23:08

      저도 그랬는데 본인 컴퓨터에 설치된 크롬 버전이랑 크롬 드라이버 버전이랑 안 맞으면 그렇더라구요. 본인 컴퓨터에 설치된 크롬 버전 확인하시고 거기에 맞는 크롬 드라이버 다운받으시면 잘 돼요

  5. 제크맛집 2020.07.16 09:08

    공연코드는 어디서 확인하나요?

  6. 2020.07.17 16:20

    취켓팅 모드는 정확히 어떻게 하는 건가요?

    • 페이지다운 2020.07.17 16:55 신고

      https://pagedown.n-e.kr/8 참고하세요

    • 2020.07.17 17:42

      제가 이상한건지 잘 안 되는 거 같네요 ㅠㅠ

  7. ㅇㅇ 2020.07.17 17:17

    안심예매 공연에서는 회차 설정이 풀리는 것 같습니다.

  8. 111 2020.07.18 00:32

    선생님 덕분에 티케팅 성공했습니다..감사합니다...... 적게 일하시고 많이 버세요..

  9. ㅇㅇ 2020.07.18 23:54

    패치버전 다운이 안됩니다 ㅠㅠ

  10. 멜멜 2020.07.18 23:58

    패치파일 다운이 안되는데요 ㅠㅠ

  11. 1122 2020.07.23 21:35

    우선 공유 감사합니다. 별다른 오류 없이 실행은 잘 되는 것 같은데, 프로그램이 좌석을 선택하는 기준은 어떻게 되는지 알 수 있을까요?

    • 페이지다운 2020.07.23 23:58 신고

      좌석은 HTML 엘리먼트를 기준으로 제일 위에 있는 것을 고릅니다. 이게 불규칙적이라, 지금 그 엘리먼트에 열이나 등급이 표시된 title을 기반으로 정확한 좌석을 추적하는 기능을 준비하고 있습니다.

  12. 꽁수니 2020.07.30 02:35

    제가 티켓팅이 잘되본적이 없어서
    하고 싶은데 너무 컴알못이라
    설치하고 실험용으로 다른 여유있는 공연 설정해서 해봤는데 프로그램이 꼼짝도 안하더라구요
    원인을 모르겠어서 흑흑
    도움좀 받을수있을까요?

  13. 2020.07.31 01:29

    비밀댓글입니다

  14. ㅇㅇ 2020.08.04 14:33

    자리 널널한 공연으로 시험해보면서 회차 입력해봤는데 하루에 회차가 두개 이상이라서 그런지 001 입력해도 날짜까지만 가더라고요 직접 선택해야하는게 맞나요?

  15. 2020.08.06 11:18

    비밀댓글입니다

  16. ㅇㅇ 2020.08.07 02:41

    취켓팅 모드에서 이미 선택된 좌석 팝업창이 뜨면 자동으로 창 닫은 후 새로고침이 멈추는데 이 경우 프로그램 재시작해야 하나요??? 이선좌 창 닫고 계속 새로고침이 가능하면 좋을 것 같아요!!

  17. 희희 2020.08.17 02:57

    덕분에 얼마전에 티켓팅 성공했습니다ㅠㅠㅠㅠㅠ선생님 진짜 절받으세요ㅠㅠㅠㅠㅠ
    어느 방향에 계신 지 몰라서 사방으로 절하겠습니다ㅠㅠㅠㅠㅠ감사합니다 정말ㅠㅠ

  18. 2020.08.26 14:18

    비밀댓글입니다

  19. 디노 2020.08.26 14:37

    먼저 좋은 프로그램 공유 감사드립니다.
    취겟팅시 구역 분리를 선택해도 구역 선택할 타이밍 없이 새로고침이 되버리는것 같은데요.
    (새로 고침 되기전에 구역 선택해도 다음번에 구역선택없이 새로고침됨)
    혹시 제가 잘못 사용하는 것일까요?

  20. 2020.09.04 13:04

    비밀댓글입니다

    • 페이지다운 2020.09.08 23:39 신고

      아직 좌석 등급을 구분하는 기능을 구현하지 못했습니다. 완전한 랜덤이며 이는 수정할 예정입니다.

  21. 2020.09.15 23:57

    비밀댓글입니다

지난번 글에서 인터파크 티켓팅 매크로 코드를 공개했는데, 보면 프로그램의 기능을 담당하는 .py 스크립트와 UI를 담당하는 .ui 파일이 분리되어 있는 것을 알 수 있다.

 

나는 보통 pyuic를 통해 .ui 파일에서 .py로 스크립트를 변경해 썼다. 

 

이 방법이 여러모로 편리하지만, 원치 않는 찌거기같은 코드가 마구 딸려와서 코드가 지저분해지는 경향이 있었고, UI의 변경이 어려웠다. 난 코드로 UI 디자인을 거의 못하기 때문에 Qt Designer을 쓰는데, 그런 나에겐 최악의 상황이다.

 

따라서 uic.loadUiType을 통해 ui를 따로 로드해서 프로그램을 만들어 보기로 했고 이 티켓팅 매크로가 그 첫 시도의 결과물이다.

 

ui를 따로 로드했을 때의 단점은 텍스트 에디터에서 보조를 받기 쉽지 않다는 점과, pyinstaller로 빌드 시 파일을 포함해서 빌드하지 않는다는 점이다. 따라서 .py 스크립트만 빌드하면 반드시 ui 파일이 따라다녀야 한다. 코드 공개를 원치 않는 사람에게는 별로 좋지 못하다. 그리고 파일을 한꺼번에 합치고 싶은 사람에게도 별로 좋지 못하다.

 

그래서 방법을 알아보다가 찾아서 공유한다.

 

일단 기본적으로 아래 코드를 최상단에 추가한다. 

1
2
3
4
5
6
7
import sys
import os
 
def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)
cs

stack overflow에서 구한 코드다. 정확히 무슨 역할을 하는지는 모르겠지만 아마도 바이너리 파일 내의 파일을 일반적인 파일처럼 열 수 있도록 해주는 코드 같다.

 

그러고 나서는 이제 pyinstaller에게 ui파일을 포함하도록 알려 줘야 하는데, spec 파일이 그것이다. spec 파일은 프로그램 빌드 시 다양한 옵션을 줄 수 있도록 하는 파일이다. 먼저 .py 스크립트만 빌드를 하면 spec 파일이 자동으로 생성된다. 그러면 그걸 수정하고 다시 빌드하면 된다.

 

우리는 ui 파일만 포함시키면 되므로, 아래와 같이 해주면 된다

1
2
3
4
a = Analysis(
    datas=[ ('TicketingMacro.ui', '.') ],
    ...
    )
cs

그러고 난 후 다시 원래 .py 스크립트로 돌아가서

1
2
3
form = resource_path('TicketingMacro.ui')
 
form_class = uic.loadUiType(form)[0
cs

위와 같이 resource_path 함수 안에 .ui파일을 적어 주고 반환값을 uic.loadUiType에다가 넘겨 주면 된다.

  1. helpi 2020.08.14 14:58

    안녕하세요 좋은글 감사드립니다.

    이전(후) 티켓팅 관련 배포파일 잘 보았습니다. 티케팅목적은 아니고 ui파일과 py 파일을 이용하여 exe파일을 만드는 것을 공부하다가 발견하였는데요.
    ui파일을 py파일로 만들지 않고
    pyinstaller를 이용하여 exe파일을 만드려고 하였으나 자꾸 창이 나타나지 않고 바로 꺼지니 이게 무슨이유인가 싶어서 올려주신 파일도 해보았으나 바로 꺼지길래 exe파일로 만드는 과정에서 문제가 있나 해서 질문드립니다.
    올려주신 TicketingMacro 소스파일 3개(ui, spec, py)를 이용해 pyinstaller를 사용하였는데 해당 문구는 다음과 같았습니다.
    pyinstaller --onefile TicketingMacro.py
    이동 경로를 제대로 하고 시도하였는데 exe실행 시 창이 뜨지 않고 바로 종료되었습니다. pyinstaller 시도 당시에 어떻게 해야 해당 세 소스파일을 제대로 빌드 할 수 있을까요??

    • 페이지다운 2020.08.14 16:36 신고

      .py파일 말고 .spec 파일로 빌드해야 합니다. pyinstaller --windowed -F TicketingMacro.spec 이렇게 하시면 됩니다.

  2. helpi 2020.08.19 15:49

    안녕하세요 바로 위에 댓글 올렸던 사람입니다.

    파이썬으로 파일 하나 만들어보겠다고 이곳저곳 끙끙댔는데 문제점에 대해서 알려주셔서 처음으로 프로그램 만들어 보았습니다.
    spec파일로 빌드하는 것, spec 파일에 data부분에 ui를 추가하는 것 두개가 핵심이었네요
    덕분에 PC로 실행하는 첫 프로그램 잘 실행되게 만들었습니다. 도움 주셔서 정말 감사드립니다. 항상 좋은일만 가득하시길 바라겠습니다.

    실행파일만이 아니라 소스파일(py, ui, spec)파일 같이 올려주셔서 감사합니다 덕분에 공부에 큰 도움되었습니다.

  3. lia 2020.09.10 16:45

    안녕하세요.
    저도 계속 ui 파일을 py 파일로 변경하는게 번거로워서 알아보던 중 요 글 보고 큰 도움 받아 해결하였습니다!
    너무 감사드립니다 ^^

  4. s_i 2020.09.18 02:05

    안녕하세요. 저도 올려주신 코드를 보고 이리저리 바꿔서 좀 더 공부해보려고 해당 파일을 수정해 빌드해보았는데요. 위에 질문처럼 exe파일을 만들어 실행하였으나 저는 해당 파일이 여전히 바로 종료되어서 궁금해서 질문 남깁니다. dist 폴더 안에 들어있는 파일을 실행시키는 것이 맞는지요? 처음엔 py파일로 빌드 후, spec 파일로 빌드하는 것이 맞나요? 계속 헤매다가 질문 남깁니다. 감사합니다.

    • 페이지다운 2020.09.18 17:23 신고

      처음에 py로 빌드하고 생성된 spec파일을 수정해 그걸로 빌드하면 됩니다. 만약 오류가 나면서 바로 꺼진다면 pyqt가 문제일 수 있으므로 재설치하고 시도해 보시기 바랍니다

이번엔 인터파크 티켓팅 매크로에 대한 코드를 공유하겠다. 자유롭게 수정해서 쓰고, 재배포 시 출처를 명확히 표기할 경우 허용하지만, 상업적 용도로 내 코드 전체 또는 일부를 사용하는 것은 금지다.

 

일반 버전

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
#인터파크 티켓팅 매크로
#만든 이 : PageDown
#https://pagedown.n-e.kr/
 
 
import sys
import random
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from selenium import webdriver
 
form_class = uic.loadUiType('TicketingMacro.ui')[0
 
 
class MyWindow(QtWidgets.QMainWindow, form_class):
    is_data_disabled = False  # 정보 입력창 비활성화 여부
    time = None  # 스레드로부터 받아오는 현재 시간
 
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.user_password_input.setEchoMode(QtWidgets.QLineEdit.Password)
        self.seats_number_spinbox.setValue(1)
        self.date_edit.setDate(QtCore.QDate.currentDate())
        self.time_edit.setTime(QtCore.QTime.currentTime())
        self.disableElements(self.time_edit, self.label_auto_start)
 
        self.apply_btn.clicked.connect(self.initData)  # 확인 버튼 클릭
        self.login_btn.clicked.connect(self.startLogin)  # 로그인 버튼 클릭
        self.start_ticketing_btn.clicked.connect(self.startTicketing)  # 시작 버튼 클릭
        self.seats_number_spinbox.valueChanged.connect(self.checkSeatsNumber)  # 좌석 개수 변경
        self.use_auto_start.stateChanged.connect(self.useAutoStart)  # 자동 시작 여부 변경
 
    def initData(self):
        if not self.is_data_disabled:  # 정보 입력창 활성화 시
            self.user_id = self.user_id_input.text()  # 아이디
            self.user_password = self.user_password_input.text()  # 비밀번호
            self.product_code = self.product_code_input.text()  # 공연 코드
            self.seats_number = self.seats_number_spinbox.value()  # 좌석 개수
            self.use_random_seat = self.use_seat_select.isChecked()  # 중간(랜덤) 선택 여부
            self.is_canceled_ticketing = self.canceled_ticket_mode.isChecked()  # 취켓팅 여부
            if self.use_auto_start.isChecked():  # 자동 시작 시 시간 24시간 방식 -> 12시간 방식 변경
                self.time = self.time_edit.time()
                self.time = str(self.time.toPyTime())[:6+ '00'
                if int(self.time[0:2]) > 12:
                    self.new_time_h = int(self.time[0:2]) - 12
                    self.time = str(self.new_time_h) + self.time[2:]
                    if self.new_time_h < 10:
                        self.time = '0' + self.time
                elif self.time[0:2== '00':
                    self.time = '12' + self.time[2:]
            self.date = self.date_edit.date()  # 날짜
            self.date = self.date.toPyDate()
            self.date = str(self.date).replace('-''')
            self.disableElements(self.user_id_input, self.user_password_input, self.product_code_input, self.date_edit,
                                 self.seats_number_spinbox, self.use_seat_select, self.use_auto_start, self.time_edit, self.canceled_ticket_mode)  # 요소 비활성화
            self.is_data_disabled = True
            self.apply_btn.setText('수정')
            QtWidgets.QMessageBox.information(self, '완료''정보 입력이 완료되었습니다.')
        else:
            self.is_data_disabled = False
            self.apply_btn.setText('확인')
            self.enableElements(self.user_id_input, self.user_password_input, self.product_code_input, self.date_edit,
                                self.seats_number_spinbox, self.use_seat_select, self.use_auto_start, self.time_edit, self.canceled_ticket_mode)  # 요소 활성화
 
    # 좌석 개수 변경
    def checkSeatsNumber(self):
        if self.seats_number_spinbox.value() > 4:
            self.seats_number_spinbox.setValue(4)
            QtWidgets.QMessageBox.critical(self, '오류''최대 좌석 개수는 4개입니다.')
        if self.seats_number_spinbox.value() < 1:
            self.seats_number_spinbox.setValue(1)
            QtWidgets.QMessageBox.critical(self, '오류''최소 좌석 개수는 1개입니다.')
 
    # 요소들 입력받아 한꺼번에 활성화
    def enableElements(self, *elements):
        for element in elements:
            element.setEnabled(True)
 
    # 요소들 입력받아 한꺼번에 비활성화
    def disableElements(self, *elements):
        for element in elements:
            element.setEnabled(False)
 
    # 자동시작 시
    def useAutoStart(self):
        if self.use_auto_start.isChecked():
            self.enableElements(self.time_edit, self.label_auto_start)
        else:
            self.time = None
            self.disableElements(self.time_edit, self.label_auto_start)
 
    # 로그인
    def startLogin(self):
        self.time_th = TimeThread()
        if self.apply_btn.text() == '확인':  # 확인 버튼을 누르지 않았을 때
            QtWidgets.QMessageBox.critical(self, '오류''좌석 정보를 입력하세요.')
        else:
            # Dictionary 형식으로 정보 전달
            ticketing_data_to_send = {
                'user_id': self.user_id,  # 아이디
                'user_pw': self.user_password,  # 비밀번호
                'product_code': self.product_code,  # 공연 번호
                'date': self.date,  # 공연 날짜
                'time': self.time,  # 자동시작 시간
                'seats_number': self.seats_number,  # 좌석 개수
                'use_random_seat': self.use_random_seat,  # 랜덤 선택 여부
                'time_signal': self.time_th.time_signal,  # 시간 스레드
                'is_canceled': self.is_canceled_ticketing  # 취켓팅 여부
            }
            self.apply_btn.setEnabled(False)  # 확인(수정) 버튼 비활성화
            self.login_btn.setEnabled(False)  # 로그인 버튼 비활성화
            # 시간/티켓팅 스레드 시작
            self.time_th.start()
            self.time_th.time_signal.connect(self.changeTime)
            self.ticketing_th = TicketingThread(ticketing_data=ticketing_data_to_send)
            self.ticketing_th.start()
 
    # 티켓팅 시작
    def startTicketing(self):
        self.ticketing_th.start_ticketing = True
 
    # 타이머 시작
    def startTimer(self):
        self.time_th = TimeThread()
        self.time_th.start()
        self.time_th.time_signal.connect(self.changeTime)
 
    @QtCore.pyqtSlot(str)
    def changeTime(self, time):
        if time == 'error':  # 웹드라이버(시계) 종료시 알림
            QtWidgets.QMessageBox.critical(self, '오류''타이머를 끄지 마십시오.')
        else:
            self.now_time.setText(time)
 
 
class TimeThread(QtCore.QThread):
    time_signal = QtCore.pyqtSignal(str)
 
    def run(self):
        while True:
            self.driver = webdriver.Chrome('chromedriver.exe')
            #네이버 시계 접속
            self.driver.get('https://search.naver.com/search.naver?sm=tab_hty.top&where=nexearch&query=%EB%84%A4%EC%9D%B4%EB%B2%84+%EC%8B%9C%EA%B3%84&oquery=%EC%8B%9C%EA%B3%84&tqi=UEMiLdprvTossFQzFhCssssssKG-165498')
            while True:
                try:
                    text = self.driver.find_element_by_css_selector('#_cs_domestic_clock > div._timeLayer.time_bx > div > div').text  # 네이버 시계 text
                    text = text.replace('\n''').replace(' ''')[0:8]
                except:
                    self.time_signal.emit('error')  # 웹드라이버(시계) 종료 시 error emit
                    break
                self.time_signal.emit(''.join(text))
 
 
class TicketingThread(QtCore.QThread):
    def __init__(self, ticketing_data, parent=None):
        QtCore.QThread.__init__(self, parent)
        self.user_id = ticketing_data['user_id']
        self.user_password = ticketing_data['user_pw']
        self.product_code = ticketing_data['product_code']
        self.date = ticketing_data['date']
        self.set_time = ticketing_data['time']
        self.seats_number = ticketing_data['seats_number']
        self.use_random_seat = ticketing_data['use_random_seat']
        self.time_signal = ticketing_data['time_signal']
        self.is_canceled_ticketing = ticketing_data['is_canceled']
        self.driver = self.driver = webdriver.Chrome('chromedriver.exe')
        self.time_signal.connect(self.checkTime)  # 시간 스레드와 연결(자동시작)
        self.is_logined = False  # 로그인 여부 False
        self.start_ticketing = False  # 시작 여부 False
 
    # 요소는 반드시 CSS 셀렉터로만(ID 우선)
 
    # 로그인
    def login(self):
        self.driver.get('https://ticket.interpark.com/Gate/TPLogin.asp')
        iframes = self.driver.find_elements_by_tag_name('iframe')
        self.driver.switch_to.frame(iframes[0])
        self.driver.find_element_by_id('userId').send_keys(self.user_id)
        self.driver.find_element_by_id('userPwd').send_keys(self.user_password)
        self.driver.find_element_by_id('btn_login').click()
        self.driver.get('https://ticket.interpark.com/')  # 인터파크 메인 페이지로 강제 접속
 
    def selectSeat(self):
        self.failed_to_get_ticket = False  # 취켓팅용-기본적으로 성공으로 표시
        # 직링 생성
        self.url = 'http://poticket.interpark.com/Book/BookSession.asp?GroupCode={}&Tiki=N&Point=N&PlayDate={}&PlaySeq=001&BizCode=&BizMemberCode='.format(self.product_code, self.date)
        self.driver.get(self.url)
 
        # 요소를 찾을 때까지 무한 루프(찾지 못하면 예외)
        while True:
            try:
                first_iframe = self.driver.find_element_by_id('ifrmSeat')  # 첫번째 아이프레임
            except:
                continue
            else:
                break
        self.driver.switch_to.frame(first_iframe)
        while True:
            try:
                next_iframe = self.driver.find_element_by_id('ifrmSeatDetail')  # 두번째 좌석선택 아이프레임
            except:
                continue
            else:
                break
        self.driver.switch_to.frame(next_iframe)
 
        self.loop_time = 0  #취켓팅용
        while True:
            try:
                elements = self.driver.find_elements_by_class_name('stySeat')
                self.loop_time += 1
                # 취켓팅하면서 루프를 100번 돌면 루프 탈출
                if self.is_canceled_ticketing and (self.loop_time > 100):
                    self.failed_to_get_ticket = True  # 잔여 좌석 인식 실패(개수 0)
                    break
            except:
                continue
            else:
                if len(elements) == 0:  # 예외는 나지 않았지만 좌석이 인식되지 못한 경우
                    continue
                else:
                    break
 
        while len(elements) > 0:
            # 첫번째부터 1개 선택
            if (not self.use_random_seat) and (self.seats_number == 1):
                self.driver.find_element_by_css_selector('#TmgsTable > tbody > tr > td > img:nth-child(3)').click()  # 바로 첫번째 요소 선택
 
            # 여러개 선택
            elif not self.use_random_seat:
                seat_count = 0
                for element in elements:
                    if seat_count < self.seats_number:
                        element.click()
                        seat_count += 1
 
            # 랜덤/여러개 선택
            elif self.use_random_seat:
                start_seat = random.randint(
                    int(len(elements) / 2), len(elements) - self.seats_number)
                for i in range(start_seat, start_seat + self.seats_number):
                    if start_seat < self.seats_number + start_seat:
                        elements[i].click()
            try:
                self.driver.switch_to.default_content()  # 제일 바깥으로 아이프레임 벗어남
                self.driver.switch_to.frame(first_iframe)  # 첫번째 아이프레임으로 다시 변경
                self.driver.find_element_by_id('NextStepImage').click()  # 다음 버튼 클릭
                self.driver.find_element_by_class_name('title')  # 임의의 요소 찾아서 크롬 종료 방어
            except:
                continue
            else:
                break
 
    @QtCore.pyqtSlot(str)
    def checkTime(self, data):
        self.time = data
 
    def run(self):
        # 로그인되지 않았다면 로그인
        if not self.is_logined:
            self.login()
            self.is_logined = True
        while True:
            if str(self.set_time) == str(self.time):  # 입력한 시간하고 일치한다면
                self.start_ticketing = True
            if self.start_ticketing:
                self.selectSeat()  # 티켓팅 시작
                if not self.is_canceled_ticketing:  # 취켓팅이 아니고 끝난 경우는 성공으로 처리
                    self.start_ticketing = False
                if not self.failed_to_get_ticket:  # 취켓팅인데 성공한 경우
                    self.start_ticketing = False
                if self.is_canceled_ticketing and self.failed_to_get_ticket:  # 취켓팅인데 실패한 경우
                    pass
 
 
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    myWindow = MyWindow()
    myWindow.show()
    app.exec_()
 
cs

일반 버전 ui 파일

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>TicketingMacro</class>
 <widget class="QMainWindow" name="TicketingMacro">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>304</width>
    <height>429</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>인터파크 티켓팅 매크로</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="QLabel" name="label_title">
      <property name="sizePolicy">
       <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
        <horstretch>0</horstretch>
        <verstretch>0</verstretch>
       </sizepolicy>
      </property>
      <property name="text">
       <string>인터파크 티켓팅 매크로</string>
      </property>
      <property name="alignment">
       <set>Qt::AlignCenter</set>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="basic_data_box">
      <property name="title">
       <string>기본 정보</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_4">
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_0">
         <property name="topMargin">
          <number>0</number>
         </property>
         <item>
          <widget class="QLabel" name="label_id">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>아이디:</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="user_id_input"/>
         </item>
         <item>
          <widget class="QLabel" name="label_pw">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>비밀번호:</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="user_password_input"/>
         </item>
        </layout>
       </item>
       <item>
        <layout class="QHBoxLayout" name="horizentalLayout_1">
         <property name="sizeConstraint">
          <enum>QLayout::SetDefaultConstraint</enum>
         </property>
         <item>
          <widget class="QLabel" name="label_pd_code">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>공연코드: </string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="product_code_input"/>
         </item>
         <item>
          <widget class="QLabel" name="label_date">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>날짜: </string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QDateEdit" name="date_edit"/>
         </item>
        </layout>
       </item>
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_2">
         <property name="topMargin">
          <number>0</number>
         </property>
         <item>
          <widget class="QLabel" name="label_seat_num">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>좌석매수(최대 4개): </string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QSpinBox" name="seats_number_spinbox"/>
         </item>
        </layout>
       </item>
       <item>
        <widget class="QCheckBox" name="use_seat_select">
         <property name="text">
          <string>중간부터 선택(랜덤)</string>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="auto_start_box">
      <property name="title">
       <string>시간 설정</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_3">
       <item>
        <widget class="QLabel" name="label_manual">
         <property name="text">
          <string>*로그인을 클릭하면 자동으로 시계가 시작합니다.
*오후 말고 항상 오전만을 선택하세요.</string>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QCheckBox" name="use_auto_start">
         <property name="text">
          <string>자동 시작 기능 사용</string>
         </property>
        </widget>
       </item>
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_3">
         <property name="topMargin">
          <number>0</number>
         </property>
         <item>
          <widget class="QTimeEdit" name="time_edit"/>
         </item>
         <item>
          <widget class="QLabel" name="label_auto_start">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>에 티켓팅을 자동 시작합니다.</string>
           </property>
          </widget>
         </item>
        </layout>
       </item>
       <item>
        <widget class="QLabel" name="now_time">
         <property name="font">
          <font>
           <pointsize>20</pointsize>
          </font>
         </property>
         <property name="text">
          <string>00 : 00 : 00</string>
         </property>
         <property name="alignment">
          <set>Qt::AlignCenter</set>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="canceled_ticket_box">
      <property name="title">
       <string>취켓팅</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_5">
       <item>
        <widget class="QCheckBox" name="canceled_ticket_mode">
         <property name="text">
          <string>취켓팅 모드</string>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <widget class="QPushButton" name="apply_btn">
      <property name="text">
       <string>확인</string>
      </property>
     </widget>
    </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout_4">
      <item>
       <widget class="QPushButton" name="login_btn">
        <property name="text">
         <string>로그인</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QPushButton" name="start_ticketing_btn">
        <property name="text">
         <string>시작</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
   </layout>
  </widget>
  <action name="action">
   <property name="text">
    <string>고급 설정</string>
   </property>
  </action>
  <action name="action_2">
   <property name="text">
    <string>도움말</string>
   </property>
  </action>
 </widget>
 <resources/>
 <connections/>
</ui>
 
cs

라이트 버전

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
#인터파크 티켓팅 매크로 Light
#만든 이 : PageDown
#https://pagedown.n-e.kr/
 
 
import sys
import random
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from selenium import webdriver
 
form_class = uic.loadUiType('TicketingMacro_Light.ui')[0
 
 
class MyWindow(QtWidgets.QMainWindow, form_class):
    is_data_disabled = False  # 정보 입력창 비활성화 여부
    time = None  # 스레드로부터 받아오는 현재 시간
 
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.user_password_input.setEchoMode(QtWidgets.QLineEdit.Password)
        self.seats_number_spinbox.setValue(1)
        self.date_edit.setDate(QtCore.QDate.currentDate())
        self.time_edit.setTime(QtCore.QTime.currentTime())
        self.disableElements(self.time_edit, self.label_auto_start)
 
        self.apply_btn.clicked.connect(self.initData)  # 확인 버튼 클릭
        self.login_btn.clicked.connect(self.startLogin)  # 로그인 버튼 클릭
        self.start_ticketing_btn.clicked.connect(self.startTicketing)  # 시작 버튼 클릭
 
        self.seats_number_spinbox.valueChanged.connect(self.checkSeatsNumber)  # 좌석 개수 변경
        self.use_auto_start.stateChanged.connect(self.useAutoStart)  # 자동 시작 여부 변경
 
    def initData(self):
        if not self.is_data_disabled:  # 정보 입력창 활성화 시
            self.user_id = self.user_id_input.text()  # 아이디
            self.user_password = self.user_password_input.text()  # 비밀번호
            self.product_code = self.product_code_input.text()  # 공연 코드
            self.seats_number = self.seats_number_spinbox.value()  # 좌석 개수
            self.use_random_seat = self.use_seat_select.isChecked()  # 중간(랜덤) 선택 여부
            if self.use_auto_start.isChecked():  # 자동 시작 시 시간 24시간 방식 -> 12시간 방식 변경
                self.time = self.time_edit.time()
                self.time = str(self.time.toPyTime())[:6+ '00'
                if int(self.time[0:2]) > 12:
                    self.new_time_h = int(self.time[0:2]) - 12
                    self.time = str(self.new_time_h) + self.time[2:]
                    if self.new_time_h < 10:
                        self.time = '0' + self.time
                elif self.time[0:2== '00':
                    self.time = '12' + self.time[2:]
            self.date = self.date_edit.date()  # 날짜
            self.date = self.date.toPyDate()
            self.date = str(self.date).replace('-''')
            self.disableElements(self.user_id_input, self.user_password_input, self.product_code_input, self.date_edit,
                                 self.seats_number_spinbox, self.use_seat_select, self.use_auto_start, self.time_edit)  # 요소 비활성화
            self.is_data_disabled = True
            self.apply_btn.setText('수정')
            QtWidgets.QMessageBox.information(self, '완료''정보 입력이 완료되었습니다.')
        else:
            self.is_data_disabled = False
            self.apply_btn.setText('확인')
            self.enableElements(self.user_id_input, self.user_password_input, self.product_code_input, self.date_edit,
                                self.seats_number_spinbox, self.use_seat_select, self.use_auto_start, self.time_edit)  # 요소 활성화
 
    # 좌석 개수 변경
    def checkSeatsNumber(self):
        if self.seats_number_spinbox.value() > 4:
            self.seats_number_spinbox.setValue(4)
            QtWidgets.QMessageBox.critical(self, '오류''최대 좌석 개수는 4개입니다.')
        if self.seats_number_spinbox.value() < 1:
            self.seats_number_spinbox.setValue(1)
            QtWidgets.QMessageBox.critical(self, '오류''최소 좌석 개수는 1개입니다.')
 
    # 요소들 입력받아 한꺼번에 활성화
    def enableElements(self, *elements):
        for element in elements:
            element.setEnabled(True)
 
    # 요소들 입력받아 한꺼번에 비활성화
    def disableElements(self, *elements):
        for element in elements:
            element.setEnabled(False)
 
    # 자동시작 시
    def useAutoStart(self):
        if self.use_auto_start.isChecked():
            self.enableElements(self.time_edit, self.label_auto_start)
        else:
            self.time = None
            self.disableElements(self.time_edit, self.label_auto_start)
 
    # 로그인
    def startLogin(self):
        self.time_th = TimeThread()
        if self.apply_btn.text() == '확인':  # 확인 버튼을 누르지 않았을 때
            QtWidgets.QMessageBox.critical(self, '오류''좌석 정보를 입력하세요.')
        else:
            # Dictionary 형식으로 정보 전달
            ticketing_data_to_send = {
                'user_id': self.user_id,  # 아이디
                'user_pw': self.user_password,  # 비밀번호
                'product_code': self.product_code,  # 공연 번호
                'date': self.date,  # 공연 날짜
                'time': self.time,  # 자동시작 시간
                'seats_number': self.seats_number,  # 좌석 개수
                'use_random_seat': self.use_random_seat,  # 랜덤 선택 여부
                'time_signal': self.time_th.time_signal  # 시간 스레드
            }
            self.apply_btn.setEnabled(False)  # 확인(수정) 버튼 비활성화
            self.login_btn.setEnabled(False)  # 로그인 버튼 비활성화
            # 시간/티켓팅 스레드 시작
            self.time_th.start()
            self.time_th.time_signal.connect(self.changeTime)
            self.ticketing_th = TicketingThread(ticketing_data=ticketing_data_to_send)
            self.ticketing_th.start()
 
    # 티켓팅 시작
    def startTicketing(self):
        self.ticketing_th.start_ticketing = True
 
    # 타이머 시작
    def startTimer(self):
        self.time_th = TimeThread()
        self.time_th.start()
        self.time_th.time_signal.connect(self.changeTime)
 
    @QtCore.pyqtSlot(str)
    def changeTime(self, time):
        if time == 'error':  # 웹드라이버(시계) 종료시 알림
            QtWidgets.QMessageBox.critical(self, '오류''타이머를 끄지 마십시오.')
        else:
            self.now_time.setText(time)
 
 
class TimeThread(QtCore.QThread):
    time_signal = QtCore.pyqtSignal(str)
 
    def run(self):
        while True:
            self.driver = webdriver.Chrome('chromedriver.exe')
            self.driver.get('https://search.naver.com/search.naver?sm=tab_hty.top&where=nexearch&query=%EB%84%A4%EC%9D%B4%EB%B2%84+%EC%8B%9C%EA%B3%84&oquery=%EC%8B%9C%EA%B3%84&tqi=UEMiLdprvTossFQzFhCssssssKG-165498')
            while True:
                try:
                    text = self.driver.find_element_by_css_selector('#_cs_domestic_clock > div._timeLayer.time_bx > div > div').text  # 네이버 시계 text
                    text = text.replace('\n''').replace(' ''')[0:8]
                except:
                    self.time_signal.emit('error')  # 웹드라이버(시계) 종료 시 error emit
                    break
                self.time_signal.emit(''.join(text))
 
 
class TicketingThread(QtCore.QThread):
    def __init__(self, ticketing_data, parent=None):
        QtCore.QThread.__init__(self, parent)
        self.user_id = ticketing_data['user_id']
        self.user_password = ticketing_data['user_pw']
        self.product_code = ticketing_data['product_code']
        self.date = ticketing_data['date']
        self.set_time = ticketing_data['time']
        self.seats_number = ticketing_data['seats_number']
        self.use_random_seat = ticketing_data['use_random_seat']
        self.time_signal = ticketing_data['time_signal']
        self.driver = self.driver = webdriver.Chrome('chromedriver.exe')
        self.time_signal.connect(self.checkTime)  # 시간 스레드와 연결(자동시작)
        self.is_logined = False  # 로그인 여부 False
        self.start_ticketing = False  # 시작 여부 False
 
    # 요소는 반드시 CSS 셀렉터로만(ID 우선)
 
    # 로그인
    def login(self):
        self.driver.get('https://ticket.interpark.com/Gate/TPLogin.asp')
        iframes = self.driver.find_elements_by_tag_name('iframe')
        self.driver.switch_to.frame(iframes[0])
        self.driver.find_element_by_id('userId').send_keys(self.user_id)
        self.driver.find_element_by_id('userPwd').send_keys(self.user_password)
        self.driver.find_element_by_id('btn_login').click()
        self.driver.get('https://ticket.interpark.com/')  # 인터파크 메인 페이지로 강제 접속
 
    def selectSeat(self):
        # 직링 생성
        self.url = 'http://poticket.interpark.com/Book/BookSession.asp?GroupCode={}&Tiki=N&Point=N&PlayDate={}&PlaySeq=001&BizCode=&BizMemberCode='.format(
            self.product_code, self.date)
        self.driver.get(self.url)
 
        # 요소를 찾을 때까지 무한 루프(찾지 못하면 예외)
        while True:
            try:
                first_iframe = self.driver.find_element_by_id('ifrmSeat')  # 첫번째 아이프레임
            except:
                continue
            else:
                break
        self.driver.switch_to.frame(first_iframe)
        while True:
            try:
                next_iframe = self.driver.find_element_by_id('ifrmSeatDetail')  # 두번째 좌석선택 아이프레임
            except:
                continue
            else:
                break
        self.driver.switch_to.frame(next_iframe)
 
        while True:
            try:
                elements = self.driver.find_elements_by_class_name('stySeat')
            except:
                continue
            else:
                if len(elements) == 0:  # 예외는 나지 않았지만 좌석이 인식되지 못한 경우
                    continue
                else:
                    break
 
        while len(elements) > 0:
            # 첫번째부터 1개 선택
            if (not self.use_random_seat) and (self.seats_number == 1):
                self.driver.find_element_by_css_selector('#TmgsTable > tbody > tr > td > img:nth-child(3)').click()  # 바로 첫번째 요소 선택
 
            # 여러개 선택
            elif not self.use_random_seat:
                seat_count = 0
                for element in elements:
                    if seat_count < self.seats_number:
                        element.click()
                        seat_count += 1
 
            # 랜덤/여러개 선택
            elif self.use_random_seat:
                start_seat = random.randint(
                    int(len(elements) / 2), len(elements) - self.seats_number)
                for i in range(start_seat, start_seat + self.seats_number):
                    if start_seat < self.seats_number + start_seat:
                        elements[i].click()
            try:
                self.driver.switch_to.default_content()  # 제일 바깥으로 아이프레임 벗어남
                self.driver.switch_to.frame(first_iframe)  # 첫번째 아이프레임으로 다시 변경
                self.driver.find_element_by_id('NextStepImage').click()  # 다음 버튼 클릭
                self.driver.find_element_by_class_name('title')  # 임의의 요소 찾아서 크롬 종료 방어
            except:
                continue
            else:
                break
 
    @QtCore.pyqtSlot(str)
    def checkTime(self, data):
        self.time = data
 
    def run(self):
        # 로그인되지 않았다면 로그인
        if not self.is_logined:
            self.login()
            self.is_logined = True
        while True:
            if str(self.set_time) == str(self.time):  # 입력한 시간하고 일치한다면
                self.start_ticketing = True
            if self.start_ticketing:
                self.selectSeat()  # 티켓팅 시작
                self.start_ticketing = False  # 성공으로 처리
 
 
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    myWindow = MyWindow()
    myWindow.show()
    app.exec_()
 
cs

라이트 버전 ui 파일

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>TicketingMacro_Light</class>
 <widget class="QMainWindow" name="TicketingMacro_Light">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>304</width>
    <height>375</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>인터파크 티켓팅 매크로 Light</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="QLabel" name="label_title">
      <property name="sizePolicy">
       <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
        <horstretch>0</horstretch>
        <verstretch>0</verstretch>
       </sizepolicy>
      </property>
      <property name="text">
       <string>인터파크 티켓팅 매크로</string>
      </property>
      <property name="alignment">
       <set>Qt::AlignCenter</set>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="basic_data_box">
      <property name="title">
       <string>기본 정보</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_4">
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_0">
         <property name="topMargin">
          <number>0</number>
         </property>
         <item>
          <widget class="QLabel" name="label_id">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>아이디:</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="user_id_input"/>
         </item>
         <item>
          <widget class="QLabel" name="label_pw">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>비밀번호:</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="user_password_input"/>
         </item>
        </layout>
       </item>
       <item>
        <layout class="QHBoxLayout" name="horizentalLayout_1">
         <property name="sizeConstraint">
          <enum>QLayout::SetDefaultConstraint</enum>
         </property>
         <item>
          <widget class="QLabel" name="label_pd_code">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>공연코드: </string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QLineEdit" name="product_code_input"/>
         </item>
         <item>
          <widget class="QLabel" name="label_date">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>날짜: </string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QDateEdit" name="date_edit"/>
         </item>
        </layout>
       </item>
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_2">
         <property name="topMargin">
          <number>0</number>
         </property>
         <item>
          <widget class="QLabel" name="label_seat_num">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>좌석매수(최대 4개): </string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QSpinBox" name="seats_number_spinbox"/>
         </item>
        </layout>
       </item>
       <item>
        <widget class="QCheckBox" name="use_seat_select">
         <property name="text">
          <string>중간부터 선택(랜덤)</string>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="auto_start_box">
      <property name="title">
       <string>시간 설정</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_3">
       <item>
        <widget class="QLabel" name="label_manual">
         <property name="text">
          <string>*로그인을 클릭하면 자동으로 시계가 시작합니다.
*오후 말고 항상 오전만을 선택하세요.</string>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QCheckBox" name="use_auto_start">
         <property name="text">
          <string>자동 시작 기능 사용</string>
         </property>
        </widget>
       </item>
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_3">
         <property name="topMargin">
          <number>0</number>
         </property>
         <item>
          <widget class="QTimeEdit" name="time_edit"/>
         </item>
         <item>
          <widget class="QLabel" name="label_auto_start">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="text">
            <string>에 티켓팅을 자동 시작합니다.</string>
           </property>
          </widget>
         </item>
        </layout>
       </item>
       <item>
        <widget class="QLabel" name="now_time">
         <property name="font">
          <font>
           <pointsize>20</pointsize>
          </font>
         </property>
         <property name="text">
          <string>00 : 00 : 00</string>
         </property>
         <property name="alignment">
          <set>Qt::AlignCenter</set>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <widget class="QPushButton" name="apply_btn">
      <property name="text">
       <string>확인</string>
      </property>
     </widget>
    </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout_4">
      <item>
       <widget class="QPushButton" name="login_btn">
        <property name="text">
         <string>로그인</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QPushButton" name="start_ticketing_btn">
        <property name="text">
         <string>시작</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
   </layout>
  </widget>
  <action name="action">
   <property name="text">
    <string>고급 설정</string>
   </property>
  </action>
  <action name="action_2">
   <property name="text">
    <string>도움말</string>
   </property>
  </action>
 </widget>
 <resources/>
 <connections/>
</ui>
cs

지난편 저 글에서 인터파크 티켓팅 매크로를 제작하고 공유는 안 한다고 했는데, 개선판 작업한 후 나만 쟁여두긴 아까워서 공유해 보기로 했다. 코드는 좀 더 손보고 GitHub라든지 공유할 생각이며 아직은 실행 파일만 공유하겠다.

 

일난 지난번 버전보다 정말 많은 기능이 추가되었다.

 

기본적으로 있던 기능에 좌석 매수 설정, 중간부터 선택, 자동 시작, 취켓팅 모드가 추가되었다. 

 

원래 있던 기능부터 추가한 것까지 상세히 알아보자.

 

1. 자동 로그인 기능. 자신의 인터파크 아이디와 비밀번호를 입력하면 알아서 로그인해 준다. 

2. 직링 기능. 직링 생성 기능이 탑재되어 있어서 공연 코드와 공연 날짜를 입력하면 알아서 창을 띄워 준다.

3. 좌석 매수 선택 기능. 좌석 매수를 설정할 수 있다. 다만 사재기를 통한 암표 판매를 방지하기 위해 개수는 최대 4개로 제한했다.

4. 중간부터 선택(랜덤) 기능. 중간 이후로부터 랜덤으로 좌석을 선택해 준다. 많은 매크로들이 첫번째 자리에 몰리는 만큼 방어를 위해 넣은 기능이지만 속도가 느릴 수 있다. 엄청 느린 정도까진 아니다.

5. 자동 시작 기능. 자동 시작 기능을 넣든 말든 시간이 표시되는 기능과, 원하면 시작 시간을 설정해서 자동으로 시작하도록 할 수있다. 네이버 시계와 동기화를 한다.

6. 취켓팅 기능. 취소된 티켓을 계속 접속하면서 찾는 기능이다.

 

그럼 본격적으로 사용법을 알아보자.

 

일단 이 매크로를 쓰기 전에 필요한 준비물이 있다.

 

1. Chrome. 크롬이 필요하다. 오직 크롬만 지원한다. 이 매크로는 일반적인 클릭 매크로와 달리 브라우저를 직접 제어하기 때문에 제일 보편적인 브라우저인 크롬을 사용했다. 크롬 다운로드는 https://www.google.com/intl/ko/chrome/ 여기서 하자.

2. ChromeDriver. 크롬 드라이버가 필요하다. 이건 내 프로그램이 크롬을 제어할 수 있도록 해주는 프로그램이다. 이게 없으면 아예 매크로를 사용할 수 없다. 크롬드라이버는 아무거나 막 까는게 아니라 자기 크롬과 일치하는 버전의 드라이버를 써야 한다. 자기 크롬의 버전을 알고 싶다면 크롬 주소창에 chrome://version 을 입력하자. 그러면 아래 사진과 같은 창이 뜰 텐데 거기서 빨간색 박스 안의 숫자, 즉 제일 앞의 . 직전 숫자가 일치하는 버전을 다운받아야 한다. 다운로드는 https://chromedriver.chromium.org/downloads 이 곳에서 할 수 있다. 

 

크롬 버전

보통 최신 버전은 아래 사진처럼 친절하게 무엇을 다운로드해야 하는지 안내해 준다.

 

크롬 드라이버

만약 구형 버전이라 여기 없다면 아래로 스크롤하면 다른 버전도 나와 있다. 나는 81버전이므로 81.0.4044.69를 클릭해서 들어가면 아래와 같은 화면이 나타날 것이다.

크롬드라이버

그러면 우리는 다 윈도우 사용자이므로(내가 Mac이나 Linux 버전으로는 프로그램을 빌드하지 않았다) 아래의 chromedriver_win32.zip를 다운로드해주면 된다.

 

그리고 다운로드하면 .zip 압축 파일이 다운로드될 텐데 그곳 안에 있는 chromedriver.exe 파일을 매크로와 동일한 위치로 복사하면 끝이다.

 

나중에 매크로를 사용할 때 제대로 동작하지 않고 오류를 일으킬 수 있는데 그럴 경우에는 크롬이 업데이트되면서 크롬드라이버와 버전이 일치하지 않아 생기는 오류일 수 있으므로 확인해 보자.

 

이러면 준비는 끝이다. 이제 본격적인 사용법으로 들어가 보자.

 

아이디와 비밀번호 입력에선 딱히 설명할 게 없다. 다만 인터파크 계정이 미리 실명인증되어 있어야 한다.

 

공연 코드는 아래 사진과 같이 공연 정보 페이지 맨 위 주소창에 있는 링크에서 ?GoodsCode= 뒤에 있는 8자리 숫자를 말한다. 뒤에 #같은게 붙어 있을 수 있는데 그건 입력하면 안 된다.

공연 코드

날짜 설정은 공연 날짜를 말한다. 이건 모두 알 것이므로 넘어간다.

 

마찬가지로 좌석 개수도 넘어가겠다.

 

중간부터 선택 기능은 좀 고민을 해야 한다. 중간 이후의 랜덤한 좌석을 골라 주는 기능인데, 인터파크의 좌석 예매 특성상 색상에 따라 좌석 name에 특별한 구분을 두지 않았다(여기서 name은 HTML에서의 name이다). 따라서 모든 좌석이 다 동일하게 취급되기 때문에, 보라색 좌석을 고르지 못할 수도 있다. 또 랜덤 연산하는 시간 때문에 느려질 수 있다. 뭐 그렇다고 엄청 느려지는 정도까진 아니다.

 

시간 설정도 별로 할 말이 없다. 자기가 자동 시작을 사용할 거면 '자동 시작 기능 사용'에 체크해 놓고 시작 시간에 시간을 맞춰 놓으면 된다. 근데 나만 그런지는 모르겠지만, 이걸 하면 티켓팅 시 렉을 먹었다. 이건 컴퓨터마다 다를 수 있으므로 자기가 직접 써보고 판단해야 할 듯 하다.

 

취켓팅 모드는 체크해 놓으면 주기적으로 돌면서 취소된 티켓(남는 좌석)이 있는지 확인하고, 있으면 체크한다. 당연하게도 취켓팅 시에는 좌석 개수는 1개, 중간부터 선택에 체크하면 안 된다.

 

그러한 입력이 끝나면 확인을 눌러 정보를 저장하면 된다.

 

그러면 버튼이 수정으로 바뀌는데, 로그인 전까진 정보를 수정할 수 있다.

 

그리고 로그인을 하면 수정 버튼과 로그인 버튼이 비활성화된다. 더 이상 정보를 수정할 수 없으며, 잘못한 게 있다면, 프로그램을 끄고 다시 해야 한다. 그러니 정보 제대로 확인하자. 로그인하다가 오류나면 껐다가 다시 켜야 한다.

 

로그인 버튼을 누르면 크롬 창 두개가 나타날 것이다. 하나는 네이버 시계, 하나는 인터파크다. 실수로라도 끄지 말자. 끄면 시계의 경우 다시 나타나지만, 인터파크의 경우 꺼버리면 프로그램을 재실행해야 한다. 그리고 검은색 바탕의 프롬프트 창이 나타나는데 그것도 절대 끄면 안된다. 그게 크롬드라이버다.

 

여기까지 아무 문제가 없었다면 이제 결전의 순간을 기다리면 된다. 자동 시작을 걸어놨다면 자동으로 시작할 때까지 기다리고, 직접 시작한다면 기다리다가 시간이 됐을 때 시작 버튼을 누르면 된다.

 

이렇게 설명이 끝났다. 복잡할 수 있기 때문에 몇번 해보면서 익혀야 한다.

 

참고로, 내가 버전을 두개 만들었는데, 하나는 라이트 버전이고 하나는 일반 버전이다. 내가 취켓팅 기능을 넣으면서 일반 티켓팅에까지 불필요한 영향을 미치면서 그게 제거된 버전을 만들었다. 취켓팅하는게 아니라면 라이트 버전 이용을 추천한다.

 

그럼 티켓팅 시 몇가지 팁을 소개하겠다.

 

1. 컴퓨터 자원 정리

티켓팅 시에는 프로그램을 아무것도 켜놓고 있지 않는게 좋다. 특히 유튜브/게임같이 인터넷 트래픽을 잡아먹는 경우에는 치명적이다. 능력이 있다면 작업 관리자에서 쓸모 없는 프로세스들을 정리해도 된다.

 

2. 인터넷 속도 향상

인터넷 속도가 제일 핵심이다. 노트북을 사용중이라면 데스크톱으로 하고, 불가능하다면 유선랜을 쓰도록 하자. 랜 포트가 없다면 유선 USB 3.0 이상 랜 카드를 사서 USB 3.0 이상을 지원하는 포트에 장착하자. USB Type C 포트가 있으면 더더욱 좋다.

 

 

아무래도 나는 전문적으로 개발을 공부한 사람도 아니고 공부중인 고등학생이기 때문에 프로그램이 많이 부족할 수 있다. 비판점과 개선점은 대환영이지만, 비난은 자제해 주셨으면 좋겠다. 실패했다면 실패한 사례를, 성공했다면 성공한 사례를 적어 주시면 도움이 될 것 같다. 

 

특히 오류 발생은 댓글로 제보해 주시면 해결할 수 있도록 노력하겠다.

 

아래는 티켓팅 매크로 일반, 라이트 버전 다운로드 링크다

일반 버전

 

38.62 MB file on MEGA

 

mega.nz

라이트 버전

 

38.62 MB file on MEGA

 

mega.nz

바이러스가 의심되면 직접 VirusTotal같은 곳에서 검사를 해 보시라. 문제 없을 것이다.

 

*추가

이번에 보고된 오류들을 패치하였다. 최근 글에 올라와 있으니 참고하시면 된다.

  1. ㅠㅠ 2020.05.07 01:07

    ㅠㅠ 크롬에서 소프트웨어가 제어되고 있는거 같다고 아예 안들어가지네요 ㅠㅠ 무료로 배포해주셔서 감사한데 정말 슬프네요 ㅠㅠㅠ...

  2. 파이썬바라기 2020.05.18 06:40

    안심예매 상품의 경우 좌석 선택 전 보안문자 입력단계가 있고, 이경우 예매가능한 좌석이 있다면 프로그램이 죽네요..

  3. 2020.05.25 16:37

    [0525/162752.853:ERROR:process_reader_win.cc(123)] NtOpenThread: {액세스 거부} 프로세스가 개체에 액세스를 요구했지만 액세스 권한이 없습니다. (0xc0000022) ....ㅠ이렇게 뜨네요.

  4. 할수있다 2020.05.27 01:06

    좌석선택은 수동으로 할 수 있는 옵션있었으면 좋겠어요. 좌석 클릭하면 바로 선택완료 버튼 눌러주는 거요.

  5. 감사합니당 2020.05.30 00:25

    공유감사합니다. 공연이 하루에 한 회차면 문제없이 실행이 잘되는데 하루에 2개의 공연이 있을때는 직링창에서 회차를 눌러줘야하네요 이 부분 업데이트 가능할까요? ㅠㅠ

  6. 감사감사 2020.06.09 16:08

    좌석선택 전 뜨는 팝업창을 닫을 수 있었으면 좋겠어요.

  7. 2020.06.18 02:11

    비밀댓글입니다

    • 2020.07.10 17:48

      비밀댓글입니다

  8. 2020.07.03 23:46

    비밀댓글입니다

    • 2020.07.10 17:46

      비밀댓글입니다

  9. 2020.07.07 09:06

    좌석 선택 페이지 들어가는 것 까진 작동되는데 그 이후엔 멈추네요. 그 이후엔 직접 해야 하나요?

  10. 2020.07.10 08:35

    비밀댓글입니다

  11. 싼쵸 2020.07.29 20:26

    덕분에 티켓팅 잘했어요!!
    구역 있는거였는데 구역 없이해서 좌석선택하느라 쫄렸지만
    감사드립니다

    • 페이지다운 2020.07.29 21:15 신고

      성공하셨다니 축하드립니다. 앞으로 더 발전시키겠습니다.

    • 싼쵸 2020.07.30 12:56

      혹시! 취케팅 기능 사용 방법 적어주신게 워딩이 헷갈리는데

      좌석은 1개로만 선택해야하고,
      중간부터 선택(랜덤)은 체크하지 말아야하는게 맞는거죠?

      구역 분리가 있는 공연의 경우에는 구역 분리는 선택해줘야 하나요?

  12. 2020.07.31 00:46

    비밀댓글입니다

  13. 문의 2020.08.02 16:14

    훌륭한 프로그램 공유 감사합니다! 그런데 안심예매 걸린 티켓창엔 적용이 안되나 봅니다. 그리고 회차002등을 눌러도 적용이 될때가 있고 안될때가 있고 그러네요. 취켓팅 모드에선 예매성공시 알림이 울렸으면 합니다. 유료로 구매한 프로그램보다 이게 훨씬 편리해서 이거로 계속 연습중인데 아직 불안불안해서 걱정이에요 ㅠㅠ

  14. 살짝오류? 2020.08.21 21:16

    ERROR:process_reader_win.cc(123)] NtOpenThread: {액세스 거부} 프로세스가 개체에 액세스를 요구했지만 액세스 권한이 없습니다. (0xc0000022)
    ERROR:exception_snapshot_win.cc(98)] thread ID 19496 not found in process

    만들어주셔서 너무나도 감사하지만 잘 작동되다가 멈추고 다시 켜도 구동이 안 됩니다 ㅠㅠㅠ 혹시 해결 방안이 있을까요?

    • 페이지다운 2020.08.21 21:17 신고

      재부팅 후 다시 켜 보시고 크롬 버전과 드라이버 버전이 일치하는지 봑인 바랍니다.

  15. 2020.09.08 13:39

    비밀댓글입니다

  16. ㅇㅇ 2020.09.09 08:18

    정말 죄송하지만 혹시 맥북용으로 빌드할 계획은 없으신가요ㅠㅠ??

  17. ㅜㅜ 2020.09.11 11:06

    회차 선택을 미리 해뒀는데도 안넘어가서 수동으로 눌러주니까 넘어갔어요ㅠㅠ 혹시 다른 문제가 있는 걸까요?

    • 2020.09.11 11:19

      비밀댓글입니다

    • 페이지다운 2020.09.11 17:23 신고

      크롬드라이버 버전이 일치하는지 확인해 보시기 바랍니다.

    • ㅜㅜ 2020.09.11 19:47

      오늘 확인하고 설치한거라 일치해요ㅜㅜ 딱 한번 쓰고 그뒤로 못 쓰고있어요ㅠㅠ

    • 페이지다운 2020.09.11 23:34 신고

      공연 번호 알려 주시면 확인하겠습니다.

  18. ㅇㅇ 2020.09.16 17:03

    우선 감사 인사부터 드립니다! 신기하네요 ㅎㅎ
    궁금한게 회차는 어떻게 넣어야해요? 하루에 3회차가 있을 경우, 001 002 003 이런식인가요? 아니면 1,2,3? 한 회차만 있는 경우에는 공란으로 둬도 되나요?

    • 페이지다운 2020.09.16 21:09 신고

      네 그렇게 하시는 것이 맞습니다. 한 회차만 있을 경우 공란으로 둬도 무방합니다.

인터넷에 보면 사람이 많이 몰리는 티켓팅 같은 경우 유료 매크로를 이용해 티켓팅을 하는 사람들이 많다.

 

물론 대다수는 그것조차도 실패한다지만, 손으로 하는 경우 더더욱 힘들다.

 

내가 필요해서 만든 건 아니다. 물론 팔 생각도 없고, 배포할 생각도 없다. 순전히 흥미에 따라 만든 것 뿐이다.

 

많은 티켓팅 프로그램들은 직접 좌표를 설정해서 하는 방식인데 이는 그냥 클릭 매크로와 다를 바가 없다.

 

난 다른 방식을 이용한다.

 

파이썬과 셀레니움 모듈을 이용한다.

 

이럴 경우 복잡한 과정 없이 그냥 프로그램이 알아서 로그인 해 주고 알아서 버튼 눌러주고 알아서 좌석 찾아서 클릭해 주며 다음 버튼도 알아서 눌러준다.

 

사용자가 할 일은 아이디와 비밀번호, 공연 코드와 날짜 입력 뿐이다. 

 

다만 크롬드라이버를 사용하기 때문에 chromedriver 파일이랑 같이 있어야 한다. 안 그러면 예외 난다.

 

티켓팅 매크로 사진

솔직히 내가 배포를 하지 않는게 욕 먹기 싫어서도 있지만 내 프로그램이 100% 정상적으로 작동할 수 있다고 확신할 수 없기 때문이다.

 

어쨌든 내가 만들어 놓고도 속도가 빨라서 놀랐다.

 

그냥 셀레니움 연습한 셈 쳐야겠다. 난 쓸 데도 없는데.

 

*개선판 작업해서 공유했다. 최근 글에 올라와 있으니 참고하시면 된다.

  1. stormnik 2020.03.28 03:01

    stormnik@naver.com 메일 부탁드려도 될까요.. 공연 몇달째 티켓팅 실패해서 진짜 암울합니다.. 도움 부탁드려요..

    • 2020.04.01 10:21

      비밀댓글입니다

    • 2020.04.11 18:54

      비밀댓글입니다

  2. 2020.04.09 01:53

    비밀댓글입니다

    • 2020.04.11 14:18

      비밀댓글입니다

    • 2020.04.11 18:54

      비밀댓글입니다

  3. 물당 2020.07.17 10:09

    저 프로그램 받을수 있나요...
    한번만이라도 성공해보고싶어요...

방금 내가 겪은 일이어서 정보 공유 차원으로 올려본다.

 

가끔씩 파이썬으로 크롤링을 하면서 분명 URL을 제대로 입력했는데도 불구 웹의 내용을 가져올 수 없는 경우가 있다.

 

분명 응답 코드는 200인데 말이다(가끔씩 응답 코드 403을 내보내는 경우도 있다).

 

이럴 경우에는 유저 에이전트를 헤더에 추가시켜주면 된다.

 

유저 에이전트는 https://namu.wiki/w/%EC%82%AC%EC%9A%A9%EC%9E%90%20%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8

 

사용자 에이전트 - 나무위키

아래에 샘플을 준비했다. Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 윈도우 NT 10버전 64비트의 Gecko 같은 브라우저 레이아웃 엔진인 KHTML을 사용하는 크롬 52.0.2743.116버전이고, AppleWebKit 및 Safari 537.36 버전 과 호환되는 브라우저다. (User-

namu.wiki

이 곳에서 참고하자. 내용이 잘 정리되어 있다.

 

그럼 바로 알아보자.

 

일단 가짜 유저 에이전트를 가져오는 방법이다. PyPI에 fake-useragent라 하여 가짜 유저 에이전트를 생성해주는 라이브러리가 있다.

 

https://pypi.org/project/fake-useragent/

 

 

fake-useragent

Up to date simple useragent faker with real world database

pypi.org

1
pip install fake-useragent
cs

 

그럼 어떻게 쓸까?

 

1
2
3
4
from fask_useragent import UserAgent
 
ua = UserAgent()
print(ua.random)
cs

 

이렇게 하면 랜덤한 유저 에이전트가 생성된다. chrome으로 하고 싶으면 chrome을 random 대신 넣어도 된다. 다른 웹브라우저도 마찬가지.

 

유저 에이전트를 헤더에 추가해 보자. requests 모듈을 기준으로 올려 보겠다.

 

1
2
3
4
5
import requests
 
headers = {'user-agent''Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36'}
 
res = requests.get('https://tistory.com/', headers=headers)
cs

 

이렇게 추가해주면 된다.

 

끝!

우리가 컴퓨터에 관한 얘기를 듣다 보면 컴퓨터는 0과 1로 이루어져있단 소리를 듣곤 한다.

 

그러나 우리는 한번도 컴퓨터가 표시한 0과 1을 직접적으로 본 적은 없다.

 

궁금하지 않은가? 그럼 한번 보도록 하자.

 

기본적으로 모든 파일은 0과 1로만 이뤄진다. 이걸 우리가 열어봤을때 컴퓨터는 특정한 방식으로 이 0과 1의 조합을 읽어들이고, 표시한다.

 

내가 쓰고 있는 이 글도 0과 1의 조합일 뿐이지만 이를 특별한 규칙에 따라 읽으면 우리가 읽을 수 있는 글이 된다.

 

대표적인 문자 인코딩은 유니코드다. 원래는 영어와 일부 특수문자만 읽을 수 있는 아스키가 있었지만, 더욱 많은 문자의 표현이 필요해지면서 나타났다.

 

이는 사진, 영상, 음악과 같은 문자로 이루어진게 아닌 파일에도 똑같다. 이런 파일을 그냥 다짜고짜 메모장으로 열어보자.

 

구글 로고 사진을 메모장으로 연 모습

알 수 없다. 이는 문자를 표현하는 규칙에 따른 파일이 아니기 때문에 문자를 읽는 규칙에 따라 읽으면 알아볼 수가 없다.

 

이 사진은 png 사진이므로 png 사진을 읽는 규칙에 따라 읽어야 한다. 그럼 우리는 사진을 볼 수 있다.

 

이렇게 파일을 어떤 방식으로 읽어야 하는지 등을 알려주는 파일의 첫부분을 헤더라고 한다.

 

그럼 바로 시작해 보자.

 

일단 파이썬으로 파일을 읽으면 바이트 자료형으로 읽어들인다. 우리는 0과 1로 이루어진 문자열로 바꿔야 한다.

 

1바이트는 8비트이므로 1바이트당 8개의 0과 1 조합으로 표시하면 된다. 코드를 보자.

 

1
2
def byteToBit(d):
    return ''.join('{:08b}'.format(b) for b in d)
cs

 

그렇다. for 문을 통해 1바이트씩 이진수로 변환해 하나로 합친다.

 

이미지 파일을 불러온 다음 다시 이진수로 이루어진 텍스트 파일을 만드는 코드를 작성해 보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
def bitToByte(d):
    return int(d, 2).to_bytes(len(d) // 8, byteorder='big')
 
= open('image.png''rb')
data = f.read()
data = byteToBit(data)
f.close()
 
= open('bintext.txt''wt')
f.write(data)
f.close()
 
cs

 

이러하다. 그럼 아까 그 구글 로고 사진을 재활용해 보자.

 

원본 사진

 

일부분

원본 사진은 5969바이트이다.

 

그러면 바이트 하나에 8비트이므로 이 텍스트의 총 길이는 5969*8 = 47752이 된다.

 

텍스트 파일의 크기를 보자. 47752바이트인 것을 알 수 있다. 왜일까?

 

여기서의 0과 1은 컴퓨터에서 다루는 진짜 0과 1이 아니다. 텍스트 인코딩(유니코드)의 0과 1이다. 즉, 문자열이다.

 

유니코드에서 숫자는 하나에 1바이트의 공간을 차지하므로 47752바이트가 맞다.

 

근데 이 텍스트 파일에서 다시 이미지를 만들어낼 수 있을까?

 

당연히 된다.

 

다음은 이진수에서 바이트로 바꿔주는 코드다.

 

1
2
def bitToByte(d):
    return int(d, 2).to_bytes(len(d) // 8, byteorder='big')
cs

 

아까와는 좀 다르다.

 

코드를 완성시켜 보자.

 

1
2
3
4
5
6
7
8
9
10
11
def bitToByte(d):
    return int(d, 2).to_bytes(len(d) // 8, byteorder='big')
 
= open('bintext.txt''rt')
data = f.read()
data = bitToByte(data)
f.close()
 
= open('image.png''wb')
f.write(data)
f.close()
cs

 

이번에는 반대다.

 

실행시키면 정상적으로 파일이 돌아온 것을 알 수 있다.

 

자 이렇게 우리는 파일을 이진수로 변환시켜 볼 수 있었다. 이거 말고도 바이너리 파일을 문자열로 다루고 싶은 사람들을 위해 번외로 방법을 알려드리겠다.

 

base64 인코딩을 이용하면 어떤 바이트든 문자열로 바꿀 수 있다.

 

요놈은 애초에 사람이 읽을 수 있도록 설계된 인코딩이 아니라 문자열 암호화 같은 곳에서 문자열을 이용해야 하는데 값이 일반적인 문자열 인코딩으로 변환할 수 없을 때를 위한 인코딩이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import base64
 
= open('image.png''rb')
data = f.read()
data = base64.b64encode(data).decode('utf-8')
f.close()
 
= open('b64text.txt''wb')
f.write(data)
f.close()
 
= open('b64text.txt''rt')
data = f.read()
data = base64.b64decode(data)
f.close()
 
= open('image1.png''wb')
f.write(data)
f.close()
cs

 

(중간에 유니코드로 디코딩하는건 텍스트파일로 써야 하는데 b64encode 함수가 인코딩된 유니코드를 내놓기 때문이다)

 

실행해 보면

이렇게 문자열로 변환되는 모습을 볼 수 있다.

나는 퍼즐을 아주 좋아하는 것은 아니지만 영어학원에서나 학교에서 워드 서치 학습지를 주고 풀어오라고 하면 재밌게 풀던 기억이 있다.

 

그래서 그 기억을 되살려 아예 직접 워드 서치을 만들어보고 싶다는 생각이 들었다. 그리고 그와 함께 든 생각이 "워드 서치을 만들어 주는 프로그램이 있으면 어떨까?"였다.

 

물론 인터넷에 워드 서치를 검색하면 모양까지 선택해서 만들 수 있지만 직접 프로그램을 만들어 보는게 의미있을 것 같았다.

 

따라서 나는 이 프로그램을 만들어 보기로 했다. 사용한 언어는 Python 3, 라이브러리는 GUI 이용을 위해 PyQt5를 사용했으며, 워드 파일 생성을 위한 docx, 라이브러리는 아니지만 응용 프로그램 빌드를 위해 pyinstaller를 이용하였다.

 

코드는 추후에 좀 더 깨끗하게 정리가 되면 공개하도록 하겠다.

 

이 프로그램에서 지원하는 방향은 총 8가지다(이름이 워드 스크램블로 되어 있는데, 처음에 이걸 워드 스크램블이라고 하는 줄 알았다. 워드 스크램블은 단어 철자의 순서를 섞어 맞추도록 하는 것이다. 현재는 수정되어 있다).

<일반>

좌 -> 우

우 -> 좌

상 -> 하

하 -> 상

<대각선>

좌 -> 우, 상 -> 하

좌 -> 우, 하 -> 상

우 -> 좌, 상 -> 하

우 -> 좌, 하 -> 상

 

(알고리즘이 재밌는데, 나중에 코드와 함께 설명하겠다)

 

프로그램을 켰을 때이다.

핵심만 존재하는 화면이다. 가로와 세로줄이 저런 이유는 워드로 출력할 때 가로가 최대 32까지 가능하고 세로는 아래 단어까지 고려할 때 24까지 가능하기 때문이다.

 

가로, 세로를 입력하고 설정 적용을 눌러보자(보통 최대까지 하는게 일반적일 것이다) 세로까지 입력하고 나서 엔터를 누르면 설정 적용된다.

 

설정 그룹박스가 비활성화 되면서 가능한 단어 개수와 글자 수가 바뀐다(저 숫자를 넘어가면 입력이 안된다)

 

이제 단어를 입력해 보자

 

단어 입력 칸에 단어를 입력하고 추가 버튼이나 엔터를 누르면 밑 리스트에 단어가 추가된다.

 

오타를 냈거나 단어가 마음에 들지 않을 경우 단어를 클릭하고 삭제 버튼을 누르면 삭제할 수 있다.

 

다음은 단어 파일 가져오기/내보내기 기능이다. 여기서는 단어를 하나 하나 입력해야 하는데, 많은 양의 단어를 한꺼번에 입력하고 싶을 경우 메모장에 다음과 같이

 

하나 입력하고 엔터를 누르는 방식으로 입력하고(하나하나 입력할 때나 여기나 upper() 함수를 통해 전부 대문자로 바꿔주므로 상관 없다) 단어 파일 가져오기를 클릭한다.

가져올 텍스트 파일을 선택하고 열기를 선택하면 자동으로 파일이 불러와진다.

반대로 내보내기는 입력된 단어들을 텍스트 파일로 내보낸다.

그럼 이제 생성을 해 보자. 생성을 위한 준비물로는 압축 파일에 포함된 template.docx 파일과 D2Coding 코딩 글꼴 파일이다.(코딩 글씨체를 이용한 이유는 글자의 크기가 일정해서 더 찾기가 쉽다) 그리고 템플릿이 이 글꼴을 기반으로 만들어져 있다. 반드시 설치해야 하며 설치하지 않으면 오류가 발생한다. 템플릿 또한 수정하지 않는 것을 권한다.

 

생성하기를 클릭하면

워드 파일(.docx)을 저장할 위치를 물어본다.

저장을 누르거나 엔터를 누르면 즉시 생성이 시작된다(만약 똑같은 이름의 문서가 실행되고 있을 경우 오류가 발생한다)

그러면 자동으로 워드 파일이 생성됨과 동시에 미리보기에 완성본이 뜬다.

 

그리고 나머지 단어와 설정도 초기화된다.

그럼 이제 워드 파일을 열어보자.

첫번째 페이지에는 완성된 학습지가, 두번째 페이지에는 답이 형광색으로 칠해져 있다(이거 구현하느라 진짜 진땀 뺐었다)

 

내 프로그램이 대단한 것은 아니지만 그래도 필요한 분들에게 유용하게 쓰였으면 좋겠다.

 

D2Coding 폰트는 '누구나 사용' 가능하고 '누구나 재배포' 가능한 OFL 라이선스에 따라 함께 공유하며, 이 워드 스크램블 프로그램은 공유할 때에는 반드시 이 블로그 링크를 통한 공유만을 허용하며, 따로 다운로드 받은 채로 직접 공유하는 것은 금지된다.

 

수정을 요청할 사안이나 오류가 발생하면 이 블로그 댓글을 통해 건의하시기 바랍니다.

 

*혹시나 악성코드 걱정하실 분을 위해 바이러스 토탈로 직접 검사까지 해 보았다.

https://www.virustotal.com/gui/file/b704315950a9522cf7426e2559204656aee12866ca9b7be128b1a0fa853b6162/detection

 

VirusTotal

 

www.virustotal.com

접속하면 파일의 악성 여부를 알 수 있다. 70개 엔진 중 2개만이 악성코드로 분류했으나 문제 없는 수준이다.

 

<다운로드 링크>

https://mega.nz/#!mbYggIBZ!nYBMPmHCVuzWNe4Tf-eGmI8rC48YXxRjOuO8LqVmElM

 

MEGA

MEGA provides free cloud storage with convenient and powerful always-on privacy. Claim your free 50GB now

mega.nz

 

+ Recent posts