?

단축키

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 코드 AWSCLI, in a single file (portable, linux) 1 file Seia 2021.04.10 330
77 코드 [Python-Gnuboard] 파이썬으로 구현한 그누보드 자동 글쓰기 함수 1 file 이니스프리 2021.04.08 1560
76 코드 [Python] 휴일지킴이 약국을 크롤링하여 Folium 지도에 마커로 표시하는 PyQt 윈도우 앱 7 file 이니스프리 2021.03.13 1283
75 코드 도박 중독자를 위한 광고 차단 규칙 file 제르엘 2020.08.21 509
74 코드 [Python] 유튜브 영상을 다운받아 일정 간격으로 캡쳐하여 10장씩 merge하기 3 file 이니스프리 2020.05.27 1300
73 자료 [Autohotkey] 매분 정각에 전체화면을 캡쳐하는 스크립트 4 file 이니스프리 2020.05.22 1305
72 코드 [Python/Telegram] Studyforus 알림봇 (댓글, 스티커 파싱) 7 file 이니스프리 2020.05.15 870
71 코드 [Python] url 주소로부터 IP 주소 알아내기 title: 황금 서버 (30일)humit 2020.02.20 2261
70 코드 [Python] 네이버 실시간 검색어 3 title: 황금 서버 (30일)humit 2020.01.23 1351
69 코드 Koa에서 자동으로 라우팅 채워주기 Seia 2020.01.22 750
68 코드 JavaScript에서 파이썬 문자열 처리 함수 중 하나 (바인딩)를 구현 7 Seia 2020.01.20 583
67 코드 [Python] Google Image Search 결과를 받아오기 file 이니스프리 2019.12.09 1211
66 코드 [파이썬] Requests를 사용한 네이버 카페 크롤링 - 일정수 이상의 리플이 달린 게시글만 텔레그램 알림 3 file 이니스프리 2019.11.17 4378
65 코드 [JS] 클라이언트단 GET Parameter Hanam09 2019.11.16 715
64 코드 [Python] 싸이월드 미니홈피 백업 스크립트 1 이니스프리 2019.11.07 2460
63 코드 [Python] PIL을 이용한 Animated GIF의 리사이징 file 이니스프리 2019.11.03 1357
» 코드 [PyQt] sir.kr에서 스크랩한 게시글을 보여주는 윈도우앱 (검색 및 정렬 가능) 7 file 이니스프리 2019.08.09 1229
61 코드 [아미나] Dropbox API를 이용한 이미지 호스팅 보드스킨 12 file 이니스프리 2019.07.13 1621
60 코드 [Python] 네이버 모바일 이미지 검색에서의 이미지 파일을 멀티스레드로 다운받고 1개의 파일로 병합 11 file 이니스프리 2019.07.12 1559
59 코드 [PHP/Javascript] 아미나에 자동으로 게시글을 생성하고 Ajax로 전송하여 결과를 표시하기 2 file 이니스프리 2019.07.09 968
Board Pagination Prev 1 2 3 4 Next
/ 4