YOGYUI

PyQt5 - QtWebEngine::웹브라우저 만들기 (1) 본문

Software/Python

PyQt5 - QtWebEngine::웹브라우저 만들기 (1)

요겨 2021. 9. 3. 18:00
반응형

 

웹크롤링 관련해서 작업을 할 때 간혹 브라우저를 열어서 웹페이지에 직접 접근해야 하는 경우가 있는데, 크롬을 쓰다보니 원하는 동작들을 구현하기 힘든 경우가 간혹 있어서 간단한 수준의 웹브라우저를 직접 구현해보기로 했다 (만들다보니 재미들려서 조금씩 기능을 추가해나가는 중 ㅎㅎ)

 

개발일지 남길 겸 블로그에 포스팅해보도록 한다 (Github 저장소도 만들고...)

1. Package Install

PyQt가 워낙에 익숙하다보니 웹브라우저도 PyQt로 만들어보기로 했다

필요한 패키지인 'PyQtWebEngine'은 최신 PyQt5 패키지에는 포함이 되어 있지 않아서 따로 pip으로 설치해줘야 한다 (https://pypi.org/project/PyQtWebEngine/)

pip install PyQtWebEngine

* 주의: PyQtWebEngine 라이센스는 GPL이니 어플리케이션화하고자 한다면 참고 (pyside2 고려 필요)

 

개발에 사용된 패키지 버전 정보는 다음과 같다 (5.15.4)

>> pip show PyQt5
Name: PyQt5
Version: 5.15.4
Summary: Python bindings for the Qt cross platform application toolkit
Home-page: https://www.riverbankcomputing.com/software/pyqt/
Author: Riverbank Computing Limited
Author-email: info@riverbankcomputing.com
License: GPL v3
Location: c:\python38\lib\site-packages
Requires: PyQt5-sip, PyQt5-Qt5
Required-by: PyQtWebEngine, pyqtkeybind

>> pip show PyQtWebEngine
Name: PyQtWebEngine
Version: 5.15.4
Summary: Python bindings for the Qt WebEngine framework
Home-page: https://www.riverbankcomputing.com/software/pyqtwebengine/
Author: Riverbank Computing Limited
Author-email: info@riverbankcomputing.com
License: GPL v3
Location: c:\python38\lib\site-packages
Requires: PyQt5-sip, PyQtWebEngine-Qt5, PyQt5
Required-by:

2. WebView 위젯 만들기

QWebEngineView를 코어로 URL을 입력할 수 있는 LineEdit를 추가한 위젯을 하나 만들어보자

QWebEngineView API는 Qt 공식 홈페이지에 상세히 기재되어 있다

https://doc.qt.io/qt-5/qwebengineview.html

 

QWebEngineView Class | Qt WebEngine 5.15.5

QWebEngineView Class The QWebEngineView class provides a widget that is used to view and edit web documents. More... Header: #include qmake: QT += webenginewidgets Since: Qt 5.4 Inherits: QWidget This class was introduced in Qt 5.4. Properties Public Funct

doc.qt.io

 

[WebPageWidget.py]

from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QWidget, QLineEdit
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QSizePolicy
from PyQt5.QtWebEngineWidgets import QWebEngineView

class WebView(QWebEngineView):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def release(self):
        self.deleteLater()
        self.close()

class WebPageWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self._editUrl = QLineEdit()
        self._webview = WebView()
        self.initControl()
        self.initLayout()

    def release(self):
        self._webview.release()

    def initLayout(self):
        vbox = QVBoxLayout(self)
        vbox.setContentsMargins(0, 4, 0, 0)
        vbox.setSpacing(4)

        subwgt = QWidget()
        subwgt.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
        hbox = QHBoxLayout(subwgt)
        hbox.setContentsMargins(4, 0, 4, 0)
        hbox.setSpacing(4)
        hbox.addWidget(self._editUrl)
        vbox.addWidget(subwgt)
        vbox.addWidget(self._webview)

    def initControl(self):
        self._editUrl.returnPressed.connect(self.onEditUrlReturnPressed)

    def onEditUrlReturnPressed(self):
        url = self._editUrl.text()
        self._webview.load(QUrl(url))

QLineEdit에 엔터키 입력 이벤트 발생 시(returnPressed) 텍스트를 QUrl로 변환하여 QWebEngineView가 url을 로드(load)하는 간단한 기능을 가진 위젯

 

간단한 실행코드를 작성한 뒤 웹페이지를 하나 불러와보자

if __name__ == '__main__':
    import sys
    from PyQt5.QtCore import QCoreApplication
    from PyQt5.QtWidgets import QApplication

    QApplication.setStyle('fusion')
    app = QCoreApplication.instance()
    if app is None:
        app = QApplication(sys.argv)
    wgt_ = WebPageWidget()
    wgt_.show()
    wgt_.resize(600, 600)

    app.exec_()
    wgt_.release()

네이버 홈페이지 접속 데모

 

 

문제) 도메인명만 입력하면 로드가 되지 않는다

도메인만 입력했을 때 결과

 

QWebEngineView는 웹페이지 로드 시작, 프로세싱, 종료에 대한 콜백(pyqtBoundSignal)을 사용할 수 있다

페이지 로드 종료 시 페이지가 로드한 url을 출력해보자

class WebPageWidget(QWidget):
    # ...
    def initControl(self):
        self._editUrl.returnPressed.connect(self.onEditUrlReturnPressed)
        self._webview.loadStarted.connect(self.onWebViewLoadStarted)
        self._webview.loadProgress.connect(self.onWebViewLoadProgress)
        self._webview.loadFinished.connect(self.onWebViewLoadFinished)
 
    def onWebViewLoadStarted(self):
        pass

    def onWebViewLoadProgress(self, progress: int):
        pass

    def onWebViewLoadFinished(self, result: bool):
        print(result, self._webview.url())
http://www.naver.com 입력 시
True, PyQt5.QtCore.QUrl('https://www.naver.com/')

http://naver.com 입력 시
True PyQt5.QtCore.QUrl('https://www.naver.com/')

naver.com 입력 시
True PyQt5.QtCore.QUrl('about:blank')

URL의 scheme을 입력하지 않으면 about:blank 화면을 로드하게 된다

2.1. Check URL Validity

사용자가 입력한 URL을 검증한 뒤 적절한 형태로 변경해주는 기능을 구현해보자

(QUrl 클래스는 url가 관련된 다양한 메서드들을 가지고 있으니 공식 문서 참고)

https://doc.qt.io/qt-5/qurl.html

 

QUrl Class | Qt Core 5.15.5

QUrl Class The QUrl class provides a convenient interface for working with URLs. More... Header: #include qmake: QT += core Note: All functions in this class are reentrant. Public Types enum ComponentFormattingOption { PrettyDecoded, EncodeSpaces, EncodeUn

doc.qt.io

일단은 단순하게 url이 상대경로(relative)인지를 확인한 뒤, 상대경로라는 http scheme을 설정하는 방식만 추가해봤다 (QUrl은 인코딩 문제는 자동으로 다루는 것 같다)

여러가지 구현 방법이 있겠지만, 나는 QWebEngineView를 상속한 WebView 클래스의 load 메서드를 오버라이딩했다

class WebView(QWebEngineView):
    # ...
    def load(self, *args):
        if isinstance(args[0], QUrl):
            qurl: QUrl = args[0]
            if qurl.isRelative():
                qurl.setScheme('http')
            return super().load(qurl)
        return super().load(*args)

그리고 페이지 로딩이 완료되면 URL 입력창도 로딩된 URL 문자열을 쓰도록 추가

class WebPageWidget(QWidget):
    # ...
    def onWebViewLoadFinished(self, result: bool):
        url: QUrl = self._webview.url()
        self._editUrl.setText(url.toString())

scheme 자동 추가 및 url 에디트 창 수정

2.2. 뒤로 가기(Backward), 앞으로 가기(Forward) 버튼 추가

웹브라우저의 핵심 기능 중 하나는 바로 방문이력(history)에 따라 이전 페이지 이동, 다음 페이지 이동 기능이다 

QPushButton 객체들을 추가하고, 아이콘은 무료 아이콘 사이트에서 가져와서 적용시킨 뒤, 페이지 로드가 완료되면 QWebEngineView 객체의 QWebEngineHistory 멤버를 가져온 뒤 뒤로 가기/앞으로 가기 가능 여부에 따라 버튼을 활성화시키도록 구현했다

class WebPageWidget(QWidget):
    def __init__(self, parent=None):
        # ...
        self._btnGoBackward = QPushButton()
        self._btnGoForward = QPushButton()
    
    def initLayout(self):
        # ...
        hbox.addWidget(self._btnGoBackward)
        hbox.addWidget(self._btnGoForward)
    
    def initControl(self):
        # ...
        self._btnGoBackward.setEnabled(False)
        self._btnGoBackward.clicked.connect(lambda: self._webview.back())
        self._btnGoBackward.setIcon(QIcon('./Resource/previous.png'))
        self._btnGoBackward.setFlat(True)
        self._btnGoBackward.setIconSize(QSize(20, 20))
        self._btnGoBackward.setFixedSize(QSize(24, 20))
        self._btnGoForward.setEnabled(False)
        self._btnGoForward.clicked.connect(lambda: self._webview.forward())
        self._btnGoForward.setIcon(QIcon('./Resource/forward.png'))
        self._btnGoForward.setFlat(True)
        self._btnGoForward.setIconSize(QSize(20, 20))
        self._btnGoForward.setFixedSize(QSize(24, 20))

    def onWebViewLoadFinished(self, result: bool):
        # ...
        history: QWebEngineHistory = self._webview.history()
        self._btnGoBackward.setEnabled(history.canGoBack())
        self._btnGoForward.setEnabled(history.canGoForward())

뒤로 가기, 앞으로 가기 기능 추가

2.3. 새로고침(Reload), 중지(Stop) 버튼 추가

브라우저의 주요 기능 중 하나인 새로고침, 중지 버튼도 만들어보자

브라우저가 로딩 중에는 중지 기능을, 로딩이 완료된 후에는 새로고침 기능을 수행하는 버튼을 만들어보자

class WebPageWidget(QWidget):
    _is_loading: bool = False
    
    def __init__(self, parent=None):
        # ...
        self._btnStopRefresh = QPushButton()
        self._iconRefresh = QIcon('./Resource/reload.png')
        self._iconStop = QIcon('./Resource/cancel.png')
    
    def initLayout(self):
        # ...
        hbox.addWidget(self._btnStopRefresh)
    
    def initControl(self):
        self._btnStopRefresh.setEnabled(False)
        self._btnStopRefresh.clicked.connect(self.onClickBtnStopRefresh)
        self._btnStopRefresh.setIcon(self._iconRefresh)
        self._btnStopRefresh.setFlat(True)
        self._btnStopRefresh.setIconSize(QSize(20, 20))
        self._btnStopRefresh.setFixedSize(QSize(24, 20))
     
    def onWebViewLoadStarted(self):
        self._is_loading = True
        self._btnStopRefresh.setEnabled(True)
        self._btnStopRefresh.setIcon(self._iconStop)
    
    def onWebViewLoadFinished(self, result: bool):
        # ...
        self._is_loading = False
        self._btnStopRefresh.setIcon(self._iconRefresh)
    
    def onClickBtnStopRefresh(self):
        if self._is_loading:
            self._webview.stop()
        else:
            self._webview.reload()

로딩 중지, 새로고침 기능

2.4. 확대, 축소 기능 추가

웹페이지의 zoom factor를 변경하는 버튼도 추가해보자

(10%씩 factor 변경)

class WebPageWidget(QWidget):
    def __init__(self, parent=None):
        # ...
        self._btnZoomIn = QPushButton()
        self._btnZoomOut = QPushButton()
    
    def initLayout(self):
        # ...
        hbox.addWidget(self._btnZoomIn)
        hbox.addWidget(self._btnZoomOut)
    
    def initControl(self):
        # ...
        self._btnZoomIn.clicked.connect(self.onClickBtnZoomIn)
        self._btnZoomIn.setIcon(QIcon('./Resource/zoomin.png'))
        self._btnZoomIn.setFlat(True)
        self._btnZoomIn.setIconSize(QSize(20, 20))
        self._btnZoomIn.setFixedSize(QSize(24, 20))
        self._btnZoomOut.clicked.connect(self.onClickBtnZoomOut)
        self._btnZoomOut.setIcon(QIcon('./Resource/zoomout.png'))
        self._btnZoomOut.setFlat(True)
        self._btnZoomOut.setIconSize(QSize(20, 20))
        self._btnZoomOut.setFixedSize(QSize(24, 20))
    
    def onClickBtnZoomIn(self):
        factor = self._webview.zoomFactor()
        self._webview.setZoomFactor(factor + 0.1)

    def onClickBtnZoomOut(self):
        factor = self._webview.zoomFactor()
        self._webview.setZoomFactor(factor - 0.1)

zoom factor 조정 기능 추가

2.5. 키보드, 마우스 이벤트 추가

앞서 추가한 기능들 중 일부를 키보드나 마우스로도 접근할 수 있도록 구현해주자

(QWebEngineView는 마우스 이벤트를 자체적으로 가져가기 때문에 eventFilter를 오버라이딩하는 방식으로 구현)

class WebView(QWebEngineView):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        QApplication.instance().installEventFilter(self)
    # ...
    def eventFilter(self, a0: QObject, a1: QEvent) -> bool:
        if a0.parent() == self:
            if a1.type() == QEvent.MouseButtonPress:
                if a1.button() == Qt.ForwardButton:
                    self.forward()
                elif a1.button() == Qt.BackButton:
                    self.back()
            elif a1.type() == QEvent.Wheel:
                modifier = QApplication.keyboardModifiers()
                if modifier == Qt.ControlModifier:
                    y_angle = a1.angleDelta().y()
                    factor = self.zoomFactor()
                    if y_angle > 0:
                        self.setZoomFactor(factor + 0.1)
                        return True
                    elif y_angle < 0:
                        self.setZoomFactor(factor - 0.1)
                        return True
        return False
 
class WebPageWidget(QWidget):
    # ...
    def keyPressEvent(self, a0: QKeyEvent) -> None:
        if a0.key() == Qt.Key_F5:
            self._webview.reload()
        elif a0.key() == Qt.Key_Escape:
            self._webview.stop()
        elif a0.key() == Qt.Key_Backspace:
            self._webview.back()

 

진짜 기본적인 기능만 갖춘 웹페이지 위젯을 하나 뚝딱 만들었다

다음 포스트에서는 크롬이나 엣지 등 다른 브라우저들처럼 여러 개의 위젯을 하나의 탭으로 묶어서 관리할 수 있는 윈도우를 만들어보자

 

여기까지 구현한 전체 코드는 깃허브에서 확인할 수 있다

https://github.com/YOGYUI/Projects/tree/rev.1/WebBrowser/python

 

GitHub - YOGYUI/Projects

Contribute to YOGYUI/Projects development by creating an account on GitHub.

github.com

 

 

반응형