如何用Python開發一個簡單的Webkit瀏覽器

jopen 9年前發布 | 36K 次閱讀 Python Python開發


在這篇教程中,我們會用 Python 的 PyQt 框架編寫一個簡單的 web 瀏覽器。關于 PyQt ,你可能已經有所耳聞了,它是 Qt 框架下的一系列 Python 組件,而 Qt(發音類似“cute”)是用來開發
GUI 的 C++ 框架。嚴格來講, Qt 也可用于開發不帶圖形界面的程序,但是開發用戶界面應該是 Qt 框架最為廣泛的應用了。Qt 的主要優勢是可以開發跨平臺的圖形界面程序,基于 Qt 的應用能夠借助于各平臺的原生性在不同類的設備上運行,而無須修改任何代碼庫。

Qt 附帶了 webkit 的接口,你可以直接使用 PyQt 來開發一個基于 webkit 的瀏覽器。

我們本次教程所開發的瀏覽器可以完成如下功能:

  • 加載用戶輸入的url
  • 顯示在渲染頁面過程中發起的所有請求
  • 允許用戶在頁面中執行自定義的 JavaScript 腳本

牛刀小試

讓我們從最簡單的 PyQt 的 Webkit 用例開始吧:輸入 url,打開窗口并在窗口中加載頁面。

這個例子十分短小,連 import語句和空行在內也只有 13 行代碼。

Python

import sys

from PyQt4.QtWebKit import QWebView
from PyQt4.QtGui import QApplication
from PyQt4.QtCore import QUrl

app = QApplication(sys.argv)

browser = QWebView()
browser.load(QUrl(sys.argv[1]))
browser.show()

app.exec_()
importsys

fromPyQt4.QtWebKitimportQWebView
fromPyQt4.QtGuiimportQApplication
fromPyQt4.QtCoreimportQUrl

app=QApplication(sys.argv)

browser=QWebView()
browser.load(QUrl(sys.argv[1]))
browser.show()

app.exec_()

當你通過命令行將 url 傳給腳本時,程序會加載 url 并且在窗口中顯示加載完成的頁面。

現在,看似你已經有一個“命令行瀏覽器”啦!至少比 python 的 requests 模塊強多了,甚至比 Lynx 還略高一籌,因為我們的瀏覽器還可以加載 JavaScript 腳本呢。但是目前為止還沒有跟 Lynx 拉開差距,因為在啟用瀏覽器的時候只能通過命令行傳入 url。那么,必然需要通過某種方式把需要加載的 url 傳入瀏覽器。沒錯,就是地址欄!

添加地址欄

其實地址欄的實現非常簡單,我們只需要在窗口頂端加一個輸入框就夠了。用戶在文本框中輸入 url 之后,瀏覽器就會加載這個地址。下面,我們將用到 QLineEdit 控件來實現輸入框。鑒于我們的瀏覽器現在有地址欄和瀏覽器顯示框兩部分,因此還要給我們的應用增加一個網格布局。

Python

import sys

from PyQt4.QtGui import QApplication
from PyQt4.QtCore import QUrl
from PyQt4.QtWebKit import QWebView
from PyQt4.QtGui import QGridLayout, QLineEdit, QWidget

class UrlInput(QLineEdit):
    def __init__(self, browser):
        super(UrlInput, self).__init__()
        self.browser = browser
        # add event listener on "enter" pressed
        self.returnPressed.connect(self._return_pressed)

    def _return_pressed(self):
        url = QUrl(self.text())
        # load url into browser frame
        browser.load(url)

if __name__ == "__main__":
    app = QApplication(sys.argv)

    # create grid layout
    grid = QGridLayout()
    browser = QWebView()
    url_input = UrlInput(browser)
    # url_input at row 1 column 0 of our grid
    grid.addWidget(url_input, 1, 0)
    # browser frame at row 2 column 0 of our grid
    grid.addWidget(browser, 2, 0)

    # main app window
    main_frame = QWidget()
    main_frame.setLayout(grid)
    main_frame.show()

    # close app when user closes window
    sys.exit(app.exec_())
importsys
fromPyQt4.QtGuiimportQApplication
fromPyQt4.QtCoreimportQUrl
fromPyQt4.QtWebKitimportQWebView
fromPyQt4.QtGuiimportQGridLayout,QLineEdit,QWidget
classUrlInput(QLineEdit):
    def__init__(self,browser):
        super(UrlInput,self).__init__()
        self.browser=browser
        # add event listener on "enter" pressed
        self.returnPressed.connect(self._return_pressed)
    def_return_pressed(self):
        url=QUrl(self.text())
        # load url into browser frame
        browser.load(url)
if__name__=="__main__":
    app=QApplication(sys.argv)
    # create grid layout
    grid=QGridLayout()
    browser=QWebView()
    url_input=UrlInput(browser)
    # url_input at row 1 column 0 of our grid
    grid.addWidget(url_input,1,0)
    # browser frame at row 2 column 0 of our grid
    grid.addWidget(browser,2,0)
    # main app window
    main_frame=QWidget()
    main_frame.setLayout(grid)
    main_frame.show()
    # close app when user closes window
    sys.exit(app.exec_())

到這里,我們已經有一個瀏覽器的雛形啦!看上去和當年的 Google Chrome 還有幾分相像呢,畢竟兩者采用了相同的渲染引擎。現在,你可以在輸入框中輸入 url ,程序便會將地址傳入瀏覽器,接著渲染出所有的 HTML 頁面和 JavaScript 腳本并展示出來。

添加開發工具

一個瀏覽器最有趣也最重要的部分是什么?當然是各種各樣的開發工具了!一個沒有開發者控制臺的瀏覽器怎么能算是瀏覽器呢?所以,我們的 Python 瀏覽器當然也要有一些開發者工具才行。

現在,我們就來添加一些類似于 Chrome 的開發者工具中 “Network” 標簽的功能吧!這個功能就是簡單地追蹤瀏覽器引擎在加載頁面的時候所執行的所有請求。在瀏覽器主頁面的下方,我們將通過一個表來顯示這些請求。簡單起見,我們只會記錄登錄的 url、返回的狀態碼和響應的內容類型。

首先我們要通過 QTableWidget 組件創建一個表格,表頭包括需要存儲的字段名稱,表格可以根據每次新插入的記錄來自動調整大小。

Python

class RequestsTable(QTableWidget):
    header = ["url", "status", "content-type"]

    def __init__(self):
        super(RequestsTable, self).__init__()
        self.setColumnCount(3)
        self.setHorizontalHeaderLabels(self.header)
        header = self.horizontalHeader()
        header.setStretchLastSection(True)
        header.setResizeMode(QHeaderView.ResizeToContents)

    def update(self, data):
        last_row = self.rowCount()
        next_row = last_row + 1
        self.setRowCount(next_row)
        for col, dat in enumerate(data, 0):
            if not dat:
                continue
            self.setItem(last_row, col, QTableWidgetItem(dat))
classRequestsTable(QTableWidget):
header=["url","status","content-type"]
def__init__(self):
    super(RequestsTable,self).__init__()
    self.setColumnCount(3)
    self.setHorizontalHeaderLabels(self.header)
    header=self.horizontalHeader()
    header.setStretchLastSection(True)
    header.setResizeMode(QHeaderView.ResizeToContents)
defupdate(self,data):
    last_row=self.rowCount()
    next_row=last_row+1
    self.setRowCount(next_row)
    forcol,datinenumerate(data,0):
        ifnotdat:
            continue
        self.setItem(last_row,col,QTableWidgetItem(dat))

想要追蹤所有請求的話,我們還需要對 PyQt 的內部構件有更深入的了解。了解到,Qt 提供了一個 NetworkAccessManager類作為 API 接口,通過調用它可以監控應用加載頁面時所執行的請求。我們需要自己編寫一個繼承自 NetworkAccessManager 的子類,添加必要的事件監聽器,然后使用我們自己編寫的 manager 來通知 webkit 視圖執行相應的請求。

首先我們需要以 NetworkAccessManager 為基類創建我們自己的網絡訪問管理器。

Python

class Manager(QNetworkAccessManager):
    def __init__(self, table):
        QNetworkAccessManager.__init__(self)
        # add event listener on "load finished" event
        self.finished.connect(self._finished)
        self.table = table

    def _finished(self, reply):
        """Update table with headers, status code and url.
        """
        headers = reply.rawHeaderPairs()
        headers = {str(k):str(v) for k,v in headers}
        content_type = headers.get("Content-Type")
        url = reply.url().toString()
        # getting status is bit of a pain
        status = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        status, ok = status.toInt()
        self.table.update([url, str(status), content_type])
classManager(QNetworkAccessManager):
def__init__(self,table):
    QNetworkAccessManager.__init__(self)
    # add event listener on "load finished" event
    self.finished.connect(self._finished)
    self.table=table
def_finished(self,reply):
    """Update table with headers, status code and url.
    """
    headers=reply.rawHeaderPairs()
    headers={str(k):str(v)fork,vinheaders}
    content_type=headers.get("Content-Type")
    url=reply.url().toString()
    # getting status is bit of a pain
    status=reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
    status,ok=status.toInt()
    self.table.update([url,str(status),content_type])

在這里需要提醒大家的是, Qt 的某些實現并不像想象中那么簡單明了,比如說從響應中獲取狀態碼就十分繁瑣。首先,你得把請求對象的類屬性作為參數傳入 response 的方法 .attribute()中, .attribute()方法的返回值是 QVariant 類型而非 int 類型。接著,需要調用內置函數 .toInt()將其轉換成一個包含兩個元素的元組,最終得到響應的狀態碼。

現在,我們終于有了一個記錄請求的表和一個監控網絡的 manager,接下來只要把他們聚攏起來就可以了。

Python

if __name__ == "__main__":
    app = QApplication(sys.argv)

    grid = QGridLayout()
    browser = QWebView()
    url_input = UrlInput(browser)
    requests_table = RequestsTable()

    manager = Manager(requests_table)
    # to tell browser to use network access manager
    # you need to create instance of QWebPage
    page = QWebPage()
    page.setNetworkAccessManager(manager)
    browser.setPage(page)

    grid.addWidget(url_input, 1, 0)
    grid.addWidget(browser, 2, 0)
    grid.addWidget(requests_table, 3, 0)

    main_frame = QWidget()
    main_frame.setLayout(grid)
    main_frame.show()

    sys.exit(app.exec_())
if__name__=="__main__":
    app=QApplication(sys.argv)

    grid=QGridLayout()
    browser=QWebView()
    url_input=UrlInput(browser)
    requests_table=RequestsTable()

    manager=Manager(requests_table)
    # to tell browser to use network access manager
    # you need to create instance of QWebPage
    page=QWebPage()
    page.setNetworkAccessManager(manager)
    browser.setPage(page)

    grid.addWidget(url_input,1,0)
    grid.addWidget(browser,2,0)
    grid.addWidget(requests_table,3,0)

    main_frame=QWidget()
    main_frame.setLayout(grid)
    main_frame.show()

    sys.exit(app.exec_())

現在,運行瀏覽器程序,在地址欄鍵入 url,就可以看到在主頁面下方的記錄表中記錄下的所有請求。

如果你有興趣的話,還可以為瀏覽器添加很多新的功能:

  • 通過content-type添加篩選功能
  • 添加記錄表的排序功能
  • 添加計時器
  • 高亮顯示出錯的請求(比如說把錯誤信息置為紅色)
  • 顯示出更為具體的請求內容,比如說完整的頭信息、響應內容、請求方法等。
  • 增加一個重復發送請求并加載出來的選項。比如說用戶可以點擊在記錄表中的請求來重試請求。

其實還有太多的功能可以繼續完善和改進,你可以一一嘗試一下,這會是一個非常有趣而且收獲良多的學習過程。但是如果想把這些功能都說完,估計都能寫一本書了。所以限于篇幅,本文就不一一介紹了,感興趣的朋友可以參考其他書籍和網上教程。

增加解析自定義 JavaScript 腳本的功能

我們終于迎來最后一個功能了!就是解析在頁面中包含的 JavaScript 腳本。

基于我們之前已經打下的基礎,要完成這個功能非常簡單。我們只需要在添加一個 QLineEdit 組件,把它和頁面聯系起來,然后調用 evaulateJavaScript方法就可以了。

Python

class JavaScriptEvaluator(QLineEdit):
    def __init__(self, page):
        super(JavaScriptEvaluator, self).__init__()
        self.page = page
        self.returnPressed.connect(self._return_pressed)

    def _return_pressed(self):
        frame = self.page.currentFrame()
        result = frame.evaluateJavaScript(self.text())
classJavaScriptEvaluator(QLineEdit):
def__init__(self,page):
    super(JavaScriptEvaluator,self).__init__()
    self.page=page
    self.returnPressed.connect(self._return_pressed)
def_return_pressed(self):
    frame=self.page.currentFrame()
    result=frame.evaluateJavaScript(self.text())

下面是這個功能的示例。看,我們的開發者工具已經整裝待發了!

Python

if __name__ == "__main__":
    # ...
    # ...
    page = QWebPage()
    # ...
    js_eval = JavaScriptEvaluator(page)

    grid.addWidget(url_input, 1, 0)
    grid.addWidget(browser, 2, 0)
    grid.addWidget(requests_table, 3, 0)
    grid.addWidget(js_eval, 4, 0)
if__name__=="__main__":
    # ...
    # ...
    page=QWebPage()
    # ...
    js_eval=JavaScriptEvaluator(page)

    grid.addWidget(url_input,1,0)
    grid.addWidget(browser,2,0)
    grid.addWidget(requests_table,3,0)
    grid.addWidget(js_eval,4,0)

現在唯一缺少的就是在頁面中不能執行 Python 腳本。你可以開發自己的瀏覽器,提供對 JavaScript 和 Python 的支持,這樣其他開發者就可以針對你的瀏覽器開發應用了。

后退、前進和其他頁面操作

我們在前面已經使用了 QWebPage 對象來開發瀏覽器,當然作為一個合格的瀏覽器,我們也需要為終端用戶提供一些重要功能。Qt 的網頁對象支持很多不同操作,我們可以把它們全都添加到瀏覽器中。

現在我們可以先嘗試著添加“后退”、“前進”和“刷新”這幾個操作。你可以在界面上添加這些操作按鈕,簡單起見,這里只加一個文本框來執行這些動作。

Python

class ActionInputBox(QLineEdit):
    def __init__(self, page):
        super(ActionInputBox, self).__init__()
        self.page = page
        self.returnPressed.connect(self._return_pressed)

    def _return_pressed(self):
        frame = self.page.currentFrame()
        action_string = str(self.text()).lower()
        if action_string == "b":
            self.page.triggerAction(QWebPage.Back)
        elif action_string == "f":
            self.page.triggerAction(QWebPage.Forward)
        elif action_string == "s":
            self.page.triggerAction(QWebPage.Stop)
classActionInputBox(QLineEdit):
def__init__(self,page):
    super(ActionInputBox,self).__init__()
    self.page=page
    self.returnPressed.connect(self._return_pressed)
def_return_pressed(self):
    frame=self.page.currentFrame()
    action_string=str(self.text()).lower()
    ifaction_string=="b":
        self.page.triggerAction(QWebPage.Back)
    elifaction_string=="f":
        self.page.triggerAction(QWebPage.Forward)
    elifaction_string=="s":
        self.page.triggerAction(QWebPage.Stop)

和之前一樣,我們要創建一個 ActionInputBox 的實例,把參數傳入頁面對象并把輸入框對象添加到頁面中。

For reference here’s code for final result 示例代碼看 這里

[1]: Graphical User Interface,圖形用戶界面,又稱圖形用戶接口,是指采用圖形方式顯示的計算機操作用戶界面。

[2]: WebKit是一個開源的瀏覽器引擎,與之相對應的引擎有 Gecko(Mozilla Firefox 等使用)和 Trident(也稱 MSHTML ,IE 使用)。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!