PyQt 기반 로또번호 생성기앱
이번에는 로또 번호 추첨기 코드를 파이썬 + Pandas + PyQt5를 이용해
만들어 보았습니다.
[프로그램 실행화면] |
[역대 1등 당첨 번호 보기] |
주요 기능으로,
1. 1~10개 까지 6자리 세트 번호 생성이 가능
2. 랜덤 번호 생성 중 역대 1등 번호와 중복된 경우 표시
[1등 번호와 일치하는 경우] |
3. 동행복권 사이트에서 받은 엑셀 파일을 Pandas Dataframe으로 불러들인 후 처리
이 프로그램은 중복되지 않는 1~45 사이의 난수를 6개 생성하고, 당첨
확율을 올리기 위해 역대 1등 번호와 중복되는지 확인을 진행하는 것이
목표였습니다.
여기서 좀 고민이 있었습니다.
역대 당첨번호가 뭔지 모른다는게 문제입니다.
물론, 인터넷에서 역대 당첨번호 리스트를 구해 생성된 난수와 서로 눈으로
비교해 볼 수 도 있겠지만, 매우 귀찮은 일이죠.
그렇다고 코드내부에 모든 역대 1등 당첨 번호를 작성해 넣고, 이를 활용하는
것도 번거로운 일입니다.
그래서 검색을 해보니 로또복권 사이트에서 역대 1등 당첨번호, 당첨금 등
데이터를 정리해 엑셀로 제공해주고 있다는 사실을 알게 되었습니다.
그렇다면, 일은 쉬워지죠. 판다스에서 그 엑셀파일을 불러들여 코드에서 중복
체크하면 간단히 해결될 문제입니다.
거기다 판다스까지 공부해 볼 수 있으니 1석 2조 이네요.
[동행 복권 사이트 접속] |
[당첨결과->회차별당첨번호->엑셀 다운] |
다운 받은 엑셀 파일 모습입니다.
잘 정리되어 있으며, 이 엑셀 파일을 Pandas Dataframe으로 불러들여
프로그램 데이터로 활용할 예정입니다.
다만, 다운로드 후 Pandas에서 read_excel() 함수로 불러들이는 도중 xlrd 관련
오류가 발생합니다. 이유는
Pandas는 내부적으로 excel파일 read 시 xlrd 모듈을 사용하는데 이때
오류가 있는(읽기 금지, 편집불가 파일 등) 엑셀 파일인 경우 이런 현상이
발생됩니다.
엑셀에서 열기 후 새로 저장하면 이 문제는 사라집니다.
[동행복권 사이트에서 다운받은 엑셀파일] |
그럼 소스코드를 살펴보겠습니다.
2개의 파이썬 파일 (main.py, record.py) 로 구성되어 있으며,
main.py는 메인 함수 역할을, record.py는 역대 1등 번호창을 띄우는 역할을
담당합니다.
소스코드
main.py
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QVBoxLayout, QLabel, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView,QAbstractItemView from PyQt5.QtCore import Qt from record import CRecord import pandas as pd import random import sys # 4k 모니터 설정 QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) class CLotto(QWidget): MAX_CNT = 10 def __init__(self): super().__init__() self.setWindowTitle('Ocean Coding School') # crate controls self.btnHistory = QPushButton('역대 당첨 번호보기', self) self.btnNum = QPushButton('번호 생성', self) label = QLabel('생성 번호 개수') self.cmb = QComboBox(self) num_list = [str(i+1) for i in range(CLotto.MAX_CNT)] self.cmb.addItems(num_list) self.table = QTableWidget(self) col_name = [str(i+1) for i in range(CRecord.MAX_NUM)] col_name.append('역대 1등번호 중복여부') self.table.setColumnCount(len(col_name)) self.table.setHorizontalHeaderLabels(col_name) self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) self.table.setSelectionBehavior(QAbstractItemView.SelectRows) self.table.setAlternatingRowColors(True); # layout controls hbox = QHBoxLayout() hbox.addWidget(self.btnHistory) hbox.addWidget(label) hbox.addWidget(self.cmb) hbox.addWidget(self.btnNum) vbox = QVBoxLayout() vbox.addLayout(hbox) vbox.addWidget(self.table) self.setLayout(vbox) # read excel self.initHistory() # signal self.btnHistory.clicked.connect(self.clickHistory) self.btnNum.clicked.connect(self.clickNum) def initHistory(self): # 기존 1등 당첨번호 불러오기 self.df = pd.read_excel('excel.xls', sheet_name='excel', skiprows=[0,1], usecols='N:S') def clickHistory(self): dlg = CRecord(self.df) dlg.exec() def clickNum(self): cnt = self.cmb.currentIndex()+1 self.table.setRowCount(cnt) for r in range(cnt): #랜덤 번호 얻기 numbers = self.getNumbers() #랜덤 번호 테이블 쓰기 for c in range(CRecord.MAX_NUM): item = QTableWidgetItem(str(numbers[c])) item.setFlags(item.flags() & ~Qt.ItemIsEditable) self.table.setItem(r, c, item) #역대 1등 번호와 비교 df = self.df == numbers and_result = df.all(axis='columns') result = and_result[and_result.values == True] info = 'X' # 일치하는 번호가 있는지 if not result.empty: i = len(df.index) - result.index[0] info = f'O {i}회차' item = QTableWidgetItem(info) item.setFlags(item.flags() & ~Qt.ItemIsEditable) self.table.setItem(r, CRecord.MAX_NUM, item) def getNumbers(self): numbers = [] while True: n = random.randint(1, 45) if not n in numbers: numbers.append(n) if len(numbers) == CRecord.MAX_NUM: numbers.sort() break return numbers if __name__ == '__main__': app = QApplication(sys.argv) w = CLotto() w.show() sys.exit(app.exec_())
하나의 CLotto class와 main()함수로 구성되어 있는 모습니다.
14~52번 라인 __init__() 클래스 생성자 함수에서 Widget에 사용할 컨트롤을
생성하고 배치합니다.
사용된 컨트롤은 아래와 같습니다.
- QPushButton 2개
- QLabel 1개
- QComboBox 1개
- QTableWidget 1개
코드가 복잡해 보이지만 Qt Designer를 사용하지 않고, 컨트롤을
코드에서 직접 생성 후 QHBoxLayout 등 을 통해 배치하므로 *.ui 파일이
필요하지 않습니다.
Qt Designer를 쓰면 좀 더 편리하게 UI를 생성할 수 있지만, Qt
class를 상속받아 오버라이딩하는 경우에는 오히려 불편한 경우가 많아
개인적으로 직접 생성하는 것을 선호합니다.
생성자 함수에서 컨트롤 배치가 끝나면 47번 라인 initHistory()
함수를 호출해 다운 받은 엑셀 파일을 Pandas Dataframe으로 불러들입니다.
이부분은 해당 함수에서 자세히 설명하겠습니다.
요약하자면, 생성자 함수의 역할은
컨트롤 생성, 배치, 엑셀파일 로드, 컨트롤 시그널 처리를
수행합니다.
54~56번 라인 initHistory() 함수는 생성자에서 불려지는 함수이며,
엑셀파일을 불러들이는 코드를 수행합니다.
다만 필요없는 데이터 컬럼 (당첨금, 2,3,4,5등 ) 을 제거하고 불러들입니다.
[엑셀파일에서 필요한 부분] |
- pd.read_excel('excel.xls', sheet_name='excel', skiprows=[0,1], usecols='N:S')
Pandas의 read_excel() 함수의 전달인자에 대한 설명은 아래와
같습니다.
1. 읽을 엑셀 파일명 (excel.xls)
2. 읽을 엑셀 파일의 워크 시트 (excel)
3. 스킵할 행 번호 (0, 1 행 필요없음)
4. 사용할 컬럼 번호(1등 번호인 N부터 S까지)
Python keyword 전달인자 형태이며, 이 외에도 더 많은 전달인자가
존재하지만 코드에 필요한 부분은 위에 사용된 부분입니다.
58~60번 라인은 "역대 당첨 번호보기" 버튼을 클릭(시그널)과 연결된 슬롯 함수이며
역대 1등 당첨 내역을 QDialog를 생성해 보여줍니다.
62~89번 라인은 "번호 생성"버튼의 슬롯함수이며, QCombox의 숫자만큼 필요한 로또
번호 셋트를 생성합니다.
1. 생성할 번호 세트 갯수 얻기(QCombox currentIndex() 함수)
2. QTableWidget의 행 수를 번호 세트 수만큼 설정
3. 생성 번호 세트 수 만큼 반복하며 6개의 랜덤번호 얻기 (getNumbers() 함수
)
단, 한 세트(6개 숫자)에
같은 숫자가 포함되지 않도록하며, 오름차순 정렬
ex) [1, 2, 2, 3, 4, 5] (중복 X) [22, 1, 2, 4, 33, 12]
(정렬 X)
4. QTableWidget에 번호 쓰기
5. 역대 1등 번호와 중복여부 체크
self.df (엑셀에서 불러들인 역대 1등 당첨번호들)와 numbers 비교,
만약 같은 숫자(6개) 가 존재하는 경우 아래와 같이 해당 Dataframe이 모두
True
6개 Column boolean 값을 AND 연산 후 결과
0번째 인덱스가 True이므로 6개 숫자가 모두 일치하는 중복상황
다만 0번째 인덱스(행)이 엑셀파일에서는 915회차 이므로 0회차를 915회차로
표시하기 위해 아래 코드를 수행합니다.
- i = len(df.index) - result.index[0] -> 915-0 = 915
-
6. 이제 중복여부를 QTableWidget에 기록
이제 역대 1등 당첨자 번호를 보여주는 record.py 파일을 살펴보겠습니다.
record.py
from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QDialog, QTableWidget, QTableWidgetItem, QHeaderView, QVBoxLayout, QAbstractItemView, QLabel class CRecord(QDialog): MAX_NUM = 6 def __init__(self, df): super().__init__() # create contrl label = QLabel('역대 1등 당첨번호 리스트') self.table = QTableWidget(self) num_col = [str(i+1) for i in range(CRecord.MAX_NUM)] num_col.insert(0, '회차') self.table.setColumnCount(len(num_col)) self.table.setHorizontalHeaderLabels(num_col) self.table.verticalHeader().hide() # get info from dataframe if not df.empty: row = len(df.index) col = len(df.keys()) self.table.setRowCount(row) for r in range(row): item = QTableWidgetItem( str(row-r) ) item.setFlags(item.flags() & ~Qt.ItemIsEditable) self.table.setItem(r, 0, item) for c in range(col): item = QTableWidgetItem( str(df.iat[r, c])) item.setFlags(item.flags() & ~Qt.ItemIsEditable) self.table.setItem(r, c+1, item) self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.table.setSelectionBehavior(QAbstractItemView.SelectRows) self.table.setAlternatingRowColors(True); vbox = QVBoxLayout() vbox.addWidget(label) vbox.addWidget(self.table) self.setLayout(vbox)
CRecord class는 QDialog class에서 상속받았으며, 생성자로 main.py 에서
읽어 들인 엑셀파일에 대한 Dataframe을 df 라는 전달인자로 넘겨 받아
QTableWidget에 정보를 시각화 하는 단순한 클래스 입니다.
DataFrame의 행과 열의 수만큼 2중 반복문을 통해 반복하며 이를
QTableWidget 에 QTableWidetItem으로 추가합니다.
실행시 아래와 같은 모습입니다.
이상으로 코드 분석을 마칩니다.
- 예제에 사용된 엑셀파일 링크 : 엑셀파일
-
개발환경 : MS Window 10 Pro(64bit), VS 2017, Python 3.7(64bit), PyQt5
5.14.2, Pandas 1.0.1
감사합니다.
댓글
댓글 쓰기