소스 공유

|  나만의 소스나 나만 알고 있는 좋은 소스를 서로 공유하는 자리입니다.

Atachment
첨부 '2'
?

단축키

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문으로 위 코드를 넣으면 로그인 여부를 확인할 수 있긴 하네요 ^^


소스 공유

나만의 소스나 나만 알고 있는 좋은 소스를 서로 공유하는 자리입니다.

List of Articles
번호 분류 제목 글쓴이 날짜 조회 수
» 코드 [PyQt] sir.kr에서 스크랩한 게시글을 보여주는 윈도우앱 (검색 및 정렬 가능) 1 file 이니스프리 2019.08.09 39
63 코드 [아미나] Dropbox API를 이용한 이미지 호스팅 보드스킨 11 file 이니스프리 2019.07.13 163
62 코드 [Python] 네이버 모바일 이미지 검색에서의 이미지 파일을 멀티스레드로 다운받고 1개의 파일로 병합 10 file 이니스프리 2019.07.12 108
61 코드 [PHP/Javascript] 아미나에 자동으로 게시글을 생성하고 Ajax로 전송하여 결과를 표시하기 2 file 이니스프리 2019.07.09 48
60 코드 [Python] Selenium을 이용하여 특정 element를 캡처하는 스크립트 2 file 이니스프리 2019.07.03 97
59 코드 [Python] 선택한 파일을 Dropbox API를 이용하여 업로드하고 공유링크를 받아서 이미지 호스팅 용도로 URL을 변환하기 1 file 이니스프리 2019.07.02 76
58 자바스크립트 미니 시계 7 도토리묵 2019.02.22 324
57 코드 [JS]클라이언트에서 Ip를 얻어보자 2 Hanam09 2019.01.21 272
56 코드 [JS] http를 https로 리디렉션! 3 Hanam09 2018.12.30 314
55 코드 [PHP] 이미지를 원하는 크기(원본비율 유지)로 리사이즈 하여 출력 (원본 이미지는 수정하지 않습니다) 6 이니스프리 2018.12.20 982
54 코드 [아미나] 네이트 실시간 검색어 순위 위젯 (아미나 캐시 적용) 3 file 이니스프리 2018.12.18 285
53 코드 [아미나] 출석 여부를 나타내는 메인화면 위젯 4 file 이니스프리 2018.12.15 324
52 코드 [PHP] 간단한 캐싱 클래스 3 title: 황금 서버 (30일)humit 2018.12.06 280
51 코드 [Python] 텔레그램을 이용한 게시판 새 글 알림봇 7 이니스프리 2018.12.02 709
50 코드 [아미나] 게시글을 작성하면 ID와 IP로 필터링하여 자동으로 랜덤 댓글을 남기기 (+랜덤 포인트) 7 file 이니스프리 2018.11.18 188
49 코드 [PHP] 그누보드 자동 게시글 작성 - 일본기상협회의 우리나라 날씨를 크롤링한 후 파파고로 번역하여 글 작성 4 file 이니스프리 2018.11.15 188
48 코드 [PHP] 기상청 RSS 시간별 예보 위젯 - cache 적용(?) 9 file 이니스프리 2018.10.28 161
47 코드 [오토핫키] 브라우저를 열어 지난번과 동일한 폴더에 MZK를 다운받고 압축을 네이티브로 해제하는 스크립트 file 이니스프리 2018.10.20 227
46 코드 [PHP] 기상청 중기예보를 캐러셀로 보여주는 위젯 (매우 허접합니다 ㅠㅠ) 10 file 이니스프리 2018.09.28 186
45 코드 [오토핫키] 구글 드라이브의 공유링크를 이미지 호스팅을 위한 다이렉트 링크로 바꿔주는 스크립트 10 file 이니스프리 2018.09.25 653
목록
Board Pagination Prev 1 2 3 4 Next
/ 4
서버에 요청 중입니다. 잠시만 기다려 주십시오...