안녕하세요?
sir.kr에서 제가 스크랩해놓은 게시글을 찾다가 스크랩한 글이 너무 많아 찾기 힘들어서
PyQt5를 이용하여 스크랩한 내역을 보여주는 다이얼로그(윈도우앱)을 만들어봤어요 ^^
스크립트가 매우 허접하고 군더더기도 많지만 일단 제가 원하는 기능들은 간신히 구현을 했네요.
제목 검색(영문자 대소문자 불문)과 정렬이 가능해요~
PyQt5에서 2바이트 문자와 관련하여 alignment에 약간 문제가 있어서
영어 울렁증이 있지만 부득이 버튼이나 헤더에 영어를 집어넣었네요 ㅜㅜ
(스크립트를 보시면 Category의 항목을 AlignCenter할 때 우측으로 어긋나는 버그가 있어서 의도적으로 공백을 집어넣어서 맞췄네요.)
그리고 StatusBar까지 추가하려고 했는데 완성한 후에야 비로소
QDialog에서는 StatusBar를 집어넣을 수 없다는 사실을 알게 되었네요 ㅠㅠ
(QMainWindow에서 가능하다고 하네요)
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 | import requests, time, webbrowser, sys from bs4 import BeautifulSoup from PyQt5.QtCore import * from PyQt5.QtWidgets import * from PyQt5 import QtWidgets from PyQt5.QtGui import * from PyQt5 import uic CalUI = 'sir_search.ui' class MainDialog(QDialog): def __init__( self ): QDialog.__init__( self , None ) uic.loadUi(CalUI, self ) self .initUI() def initUI( self ): self .setWindowFlags(Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint) model = QStandardItemModel() model.setHorizontalHeaderLabels([ 'Category' , 'Title' ]) self .boards, self .titles, self .urls = parse_sir() self .results = dict ( zip ( self .titles, self .urls)) self .keyword = '' self .search_indices = [] self .treeView.setSortingEnabled( False ) self .treeView.header().setSectionsClickable( True ) count = 0 while count < len ( self .titles): column1 = QStandardItem( self .boards[count] + ' ' ) # 한글 가운데 정렬 버그 column2 = QStandardItem( self .titles[count]) column1.setTextAlignment(Qt.AlignCenter) model.appendRow([column1, column2]) count + = 1 self .treeView.setModel(model) self .treeView.resizeColumnToContents( 0 ) self .treeView.header().setDefaultAlignment(Qt.AlignCenter) self .treeView.header().setFont(QFont( 'Times' , 12 , QFont.Bold)) self .treeView.header().sectionClicked.connect( self .HeaderClicked) self .pushButton.clicked.connect( self .SearchClicked) self .pushButton2.clicked.connect( self .InitializeClicked) self .treeView.doubleClicked.connect( self .TreeClicked) return def HeaderClicked( self ): # treeView의 헤더를 클릭했을 때 정렬을 합니다. self .treeView.setSortingEnabled( True ) time.sleep( 0.1 ) self .treeView.setSortingEnabled( False ) self .treeView.header().setSectionsClickable( True ) return def SearchClicked( self ): # 검색어를 입력했을 때 찾은 결과를 출력합니다. model = QStandardItemModel() self .keyword = self .lineEdit.text() if self .keyword ! = '': # 검색어가 Null이 아닌 경우를 처리합니다. # 대소문자를 구별하지 않습니다. 구별하려면 .lower()를 삭제하세요. indices = [i for i, s in enumerate ( self .titles) if self .keyword.lower() in s.lower()] for index in indices: self .search_indices.append(index) for index in self .search_indices: column1 = QStandardItem( self .boards[index] + ' ' ) # 한글 가운데 정렬 버그 column2 = QStandardItem( self .titles[index]) column1.setTextAlignment(Qt.AlignCenter) model.appendRow([column1, column2]) self .search_indices = [] else : # Null을 입력했을 때에도 초기화를 합니다. count = 0 while count < len ( self .titles): column1 = QStandardItem( self .boards[count] + ' ' ) # 한글 가운데 정렬 버그 column2 = QStandardItem( self .titles[count]) column1.setTextAlignment(Qt.AlignCenter) model.appendRow([column1, column2]) count + = 1 self .treeView.setModel(model) model.setHorizontalHeaderLabels([ 'Category' , 'Title' ]) return def InitializeClicked( self ): # 초기 검색결과로 되돌립니다. model = QStandardItemModel() self .lineEdit.setText('') count = 0 while count < len ( self .titles): column1 = QStandardItem( self .boards[count] + ' ' ) # 한글 가운데 정렬 버그 column2 = QStandardItem( self .titles[count]) column1.setTextAlignment(Qt.AlignCenter) model.appendRow([column1, column2]) count + = 1 self .treeView.setModel(model) model.setHorizontalHeaderLabels([ 'Category' , 'Title' ]) return def TreeClicked( self , model_index): # viewTree의 요소를 클릭했을 때 브라우저를 열고 해당 게시글에 접속합니다. title = self .treeView.selectedIndexes()[ 1 ].data() webbrowser. open ( self .results[title], new = 2 ) return scrap_page = sir_session.get(scrap_url) soup = BeautifulSoup(scrap_page.content, 'html.parser' ) no_data = soup.select_one( 'td.empty_table' ) board_list = [] title_list = [] href_list = [] if no_data is not None : # 마지막 페이지를 넘어간 경우에 False를 반환하여 종료합니다. if '자료가 없습니다.' in no_data.text: return False , board_list, title_list, href_list else : # 스크랩한 게시글의 게시판, 제목, URL을 각각 리스트에 추가합니다. rows = soup.select( 'tbody > tr > td.td_board' ) for row in rows: category = row.text subject = row.find_next( 'td' ).contents[ 0 ].text url = 'https:' + row.find_next( 'td' ).contents[ 0 ][ 'href' ] board_list.append(category) title_list.append(subject) href_list.append(url) return True , board_list, title_list, href_list def parse_sir(): # SIR에 로그인하여 스크랩한 게시글을 가져옵니다. LOGIN_INFO = { 'url' : '%2F%2Fsir.kr' , 'mb_id' : '유저 ID' , # ID와 패스워드를 입력하세요. 'mb_password' : '유저 패스워드' } user_agent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36' headers = { 'User-Agent' : user_agent} board_list = [] subject_list = [] url_list = [] with requests.Session() as s: time.sleep( 0.5 ) time.sleep( 0.5 ) if login_req.status_code ! = 200 : # 접속에 실패했을 때 에러창을 띄웁니다. app = QtWidgets.QApplication([]) error_dialog = QtWidgets.QErrorMessage() error_dialog.showMessage( 'Connection failure!' ) app.exec_() sys.exit() pageno = 1 check = True while check: # 스크랩창의 모든 페이지에서 자료를 가져옵니다. check, boards, subjects, urls = parse_scrap_page(scrap_url, s) board_list.extend(boards) subject_list.extend(subjects) url_list.extend(urls) pageno + = 1 time.sleep( 0.2 ) return board_list, subject_list, url_list def main(): app = QApplication(sys.argv) main_dialog = MainDialog() main_dialog.show() app.exec_() if __name__ = = "__main__" : main() |
PyQt5의 UI 파일과 아이콘 GIF 파일은 별도로 첨부합니다.
원래 UI 파일까지 합쳐서 올리려고 했는데 따로 올려드리는 것이 수정하실 때 편하실 것 같아서요 ^^
스크립트를 작성하면서 느꼈던 점을 적어볼게요~
1.
제가 아직 클래스와 함수에 대해 익숙하지 않아서 전반적으로 self. 선언을 너무 남발한 것 같은 느낌이 드네요 ㅠㅠ
코딩 컨벤션이란 것에 대해 아직도 잘 모르겠어요.
2.
정렬(sort)을 한 후에도 제목을 클릭하면 원래 파싱해놓은 URL을 찾아서 브라우저에서 여는 것을 구현하는 것이
처음 생각했던 것보다 단순하지 않더군요 ㅠㅠ
이런 것을 쉽게 구현해내시는 개발자분들이 정말 대단하게 느껴지더군요!
저는 처음에 인덱스(0, 1, 2, ...)의 형태로 제목과 URL을 연결했는데
이런 방식은 일단 한 번이라도 정렬을 해버리면 전혀 쓸모가 없더군요 ㅜㅜ
그래서 제목(키)과 URL(값)을 딕셔너리의 형태로 묶어놓은 다음에
클릭을 하면 제목을 받아와서 해당되는 키로 값을 찾는 형태로 구현했는데요~
아마도 더 효율적인 코딩 컨벤션이 있지 않을까 생각이 드네요 ^^
3.
UI의 좌측상단에 사용한 아이콘은 의도적으로 SIR과 살짝 비슷한 느낌이 나면서도
상표법을 준수하기 위하여 짝퉁의 느낌이 물씬 풍기도록 만들어봤네요 ㅋㅋ
장황하고 두서없는 글을 읽어주셔서 감사합니다!
그럼 즐거운 주말 되시고 날씨가 더운데 항상 건강하세요~
테스트해보니 응답코드가 200인 것을 확인하는 것만으로는 로그인 여부를 확인할 수 없네요 ㅠㅠ
로그인에 실패했음을 보여주는 페이지도 응답코드는 200일테니깐요.
try
:
if
'회원만 조회하실 수 있습니다.'
in
soup.select_one(
'p.cbg'
).text:
app
=
QtWidgets.QApplication([])
error_dialog
=
QtWidgets.QErrorMessage()
error_dialog.showMessage(
'Login failure!'
)
app.exec_()
sys.exit()
parse_scrap_page() 함수에 try~except문으로 위 코드를 넣으면 로그인 여부를 확인할 수 있긴 하네요 ^^