코드

[PyQt] sir.kr에서 스크랩한 게시글을 보여주는 윈도우앱 (검색 및 정렬 가능)

by 이니스프리 posted Aug 09, 2019
?

단축키

Prev이전 문서

Next다음 문서

ESC닫기

크게 작게 위로 아래로 댓글로 가기 인쇄
Extra Form
라이선스 MIT

안녕하세요?


sir.kr에서 제가 스크랩해놓은 게시글을 찾다가 스크랩한 글이 너무 많아 찾기 힘들어서


PyQt5를 이용하여 스크랩한 내역을 보여주는 다이얼로그(윈도우앱)을 만들어봤어요 ^^





스크립트가 매우 허접하고 군더더기도 많지만 일단 제가 원하는 기능들은 간신히 구현을 했네요.


제목 검색(영문자 대소문자 불문)과 정렬이 가능해요~


PyQt5에서 2바이트 문자와 관련하여 alignment에 약간 문제가 있어서 


영어 울렁증이 있지만 부득이 버튼이나 헤더에 영어를 집어넣었네요 ㅜㅜ

(스크립트를 보시면 Category의 항목을 AlignCenter할 때 우측으로 어긋나는 버그가 있어서 의도적으로 공백을 집어넣어서 맞췄네요.)


그리고 StatusBar까지 추가하려고 했는데 완성한 후에야 비로소 


QDialog에서는 StatusBar를 집어넣을 수 없다는 사실을 알게 되었네요 ㅠㅠ

(QMainWindow에서 가능하다고 하네요)



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
           

def parse_scrap_page(scrap_url, sir_session): # https://sir.kr/bbs/scrap.php에서 자료를 가져옵니다.
    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:
        login_page = s.get('https://sir.kr/bbs/login.php', headers=headers)
        time.sleep(0.5)
        login_req = s.post('https://sir.kr/bbs/login_check.php', data=LOGIN_INFO, headers=headers)
        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: # 스크랩창의 모든 페이지에서 자료를 가져옵니다.
            scrap_url = 'https://sir.kr/bbs/scrap.php?&page=' + str(pageno)
            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 파일까지 합쳐서 올리려고 했는데 따로 올려드리는 것이 수정하실 때 편하실 것 같아서요 ^^


sir_search.ui 

sir_search.gif 



스크립트를 작성하면서 느꼈던 점을 적어볼게요~


1. 


제가 아직 클래스와 함수에 대해 익숙하지 않아서 전반적으로 self. 선언을 너무 남발한 것 같은 느낌이 드네요 ㅠㅠ


코딩 컨벤션이란 것에 대해 아직도 잘 모르겠어요.



2. 


정렬(sort)을 한 후에도 제목을 클릭하면 원래 파싱해놓은 URL을 찾아서 브라우저에서 여는 것을 구현하는 것이 


처음 생각했던 것보다 단순하지 않더군요 ㅠㅠ


이런 것을 쉽게 구현해내시는 개발자분들이 정말 대단하게 느껴지더군요!


저는 처음에 인덱스(0, 1, 2, ...)의 형태로 제목과 URL을 연결했는데 


이런 방식은 일단 한 번이라도 정렬을 해버리면 전혀 쓸모가 없더군요 ㅜㅜ


그래서 제목(키)과 URL(값)을 딕셔너리의 형태로 묶어놓은 다음에


클릭을 하면 제목을 받아와서 해당되는 키로 값을 찾는 형태로 구현했는데요~


아마도 더 효율적인 코딩 컨벤션이 있지 않을까 생각이 드네요 ^^



3. 


UI의 좌측상단에 사용한 아이콘은 의도적으로 SIR과 살짝 비슷한 느낌이 나면서도 


상표법을 준수하기 위하여 짝퉁의 느낌이 물씬 풍기도록 만들어봤네요 ㅋㅋ



장황하고 두서없는 글을 읽어주셔서 감사합니다!


그럼 즐거운 주말 되시고 날씨가 더운데 항상 건강하세요~



Articles

1 2 3 4