?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄 첨부
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄 첨부
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과 살짝 비슷한 느낌이 나면서도 


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



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


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


  • profile
    이니스프리 2019.08.10 02:13

    테스트해보니 응답코드가 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문으로 위 코드를 넣으면 로그인 여부를 확인할 수 있긴 하네요 ^^

  • profile
    이니스프리 2019.08.27 23:12

    애당초 QMainWindow로 만들지 않은 점은 아쉽지만

    소소하게나마 이런저런 기능을 추가했으니 조만간에 개선된 버전을 올리도록 할게요~

    반드시 SIR이 아니더라도 그누보드 계열 사이트에서는 대체로 적용 가능한 것 같더군요!

    +) PyQt를 사용하여 제작된 스크립트를 EXE 파일로 변환하는 것은 다소 까다롭네요 ㅠㅠ

  • ?
    해피보이 2020.03.27 00:12
    안녕하세요?^^
    이 코드 그누보드5.3이나 5.4 버전에서 사용 가능한가요?
    만약 안된다면 호환되게 수정 후 포인트로 다운받게 하셔도 좋을것 같아서요.
    꼭 한번 검토 부탁드립니다.
    즐거운 나날 되세요~~^^
  • profile
    이니스프리 2020.03.27 01:04

    안녕하세요? ^^

    SIR에서 오신 분인가 보군요~ 방가워요! :)


    당시에 냑에도 소스를 올리려고 생각을 했지만 그 무렵에는 제가 이런저런 일들로 경황이 없었네요 ㅠㅠ

    그리고 냑은 PHP & JS 위주의 사이트라서 PyQt 모듈을 별도로 설치해야 하는 파이썬 스크립트를 올려도 사용하시는 분이 많지 않을 것 같아서요~

    그래서 EXE 파일로 컴파일(?)을 해서 올리려고 했지만 당시에 실패했던 기억이 나는군요 ㄷㄷ

    (윈도우 환경에서 PyQt 모듈을 사용한 스크립트를 PyInstaller 등을 통해 EXE 파일로 변환할 때 발생하는 버그라고 알고 있네요)


    방금 오래간만에 다시 실행을 해봤더니 현재에도 냑에서 잘 작동하네요 ^^

    그누보드의 로그인 관련 로직을 건드리지 않고, 스크랩 창에 뜨는 HTML 태그를 커스텀하지 않았다면 그누보드 기반의 다른 사이트에서도 잘 작동할거에요 :)


    말씀해주신대로 좀 더 보완해서 냑에 올리도록 할게요~!

    그럼 굿밤 되세요 ^-^


    +) 

    사실 SIR에 올리지 못한 또 다른 이유가 있었는데 스크립트에 SIR을 크롤링하는 코드가 포함되었기 때문이었어요 ㅠㅠ

    SIR 사이트를 운영하시는 분들 입장에서는 딱히 좋아하시지는 않을 것 같아서요~ 

  • ?
    해피보이 2020.03.27 13:41
    자세한 설명 감사합니다.
    냑에서 호스팅 문의했던 휴매니아입니다.
    이니스프리님 덕분에 현재 호스팅 분양받았는데 설정부분에서 많이 헤메고 있네요~~^^;;
    늘 즐거운 시간 되세요~~^^
  • profile
    이니스프리 2020.03.27 16:37

    앗 SIR의 휴매니아 님이셨군요~! ^-^

    정말 반갑네요 :)

    호스팅에 관한 문제는 '도와주세요' 게시판에 문의하시면 마스터 님이나 다른 고수님들께서 빠른 답변을 해주실거에요~!

    부디 문제가 잘 해결되어서 호스팅을 잘 사용하시면 좋겠네요 ^^

    그럼 휴매니아 님께서도 즐거운 불금과 주말 되시고 항상 건강하세요오!!

    항상 감사드려요~!

  • ?
    해피보이 2020.03.27 17:34
    감사는 제가 드려야지요~~
    항상 행복한 시간 보내세요~~^^

List of Articles
번호 분류 제목 글쓴이 날짜 조회 수
78 코드 세린서버에서 시도중인 백업 스크립트 입니다. 4 NoYeah 2017.06.27 834
77 자료 소셜XE / 기존 통합 로그인 스킨 V2.2 2 file NoYeah 2017.06.28 1061
76 자료 경험치 현황 위젯 6 file NoYeah 2017.06.28 864
75 자료 Gentelella 3 file NoYeah 2017.06.29 1043
74 자료 Gentelella 레이아웃에 사용가능한 가격 테이블 위젯입니다. 3 file NoYeah 2017.07.03 688
73 자료 AdminLTE용 에디터 스타일 4 file title: 은메달도다 2017.07.07 825
72 코드 엑셀파일 불러서 히스토그램 그려주는 함수 국내산라이츄 2017.08.03 873
71 코드 엑셀 읽어서 그래프 그려주는 함수 1 국내산라이츄 2017.08.03 1551
70 자료 RBGE - 이쁘고 깔끔한 에러페이지 4 file title: 열려라 맛스타의 자물쇠TVJ 2017.08.08 766
69 자료 even_move - 감성적인 에러 페이지 7 file title: 열려라 맛스타의 자물쇠TVJ 2017.08.08 890
68 자료 [XE / Rhymix] Bootstrap 패널 위젯 스타일 file title: 은메달도다 2017.08.09 698
67 자료 [1.8a] Bootstrap 'Panel' 위젯 스타일 1 file title: 은메달도다 2017.08.09 670
66 코드 Git 저장소에서 자동으로 받아 업데이트하는 쉘 스크립트 5 NoYeah 2017.09.16 750
65 자료 [Bootstrap] xeACE 레이아웃 3 title: 은메달도다 2017.09.17 723
64 자료 링크 파싱 애드온용 스킨 (트위터 스타일) 3 file SNAX 2017.10.03 580
63 자료 이게 팔릴까 - Xe/라이믹스 에러페이지 [2017-10-04] 3 file title: 열려라 맛스타의 자물쇠TVJ 2017.10.04 750
62 자료 AdBlock 접근 방지 애드온 v0.1 3 file 네모 2017.10.05 739
61 코드 브라우저 언어에 따라 다른 폴더를 사용하는 PHP 코드 4 file 네모 2017.10.10 867
60 코드 파이썬을 이용한 텔레그램 새 글 알림 (허접합니다) 4 이니스프리 2017.11.19 2516
59 코드 새 글 자동 댓글 스크립트 (AutoHotkey) 9 이니스프리 2017.11.26 3561
Board Pagination Prev 1 2 3 4 Next
/ 4