在這篇教程中,我們會用 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 行程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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_() |
當你通過命令列將 url 傳給指令碼時,程式會載入 url 並且在視窗中顯示載入完成的頁面。
現在,看似你已經有一個“命令列瀏覽器”啦!至少比 python 的 requests 模組強多了,甚至比 Lynx 還略高一籌,因為我們的瀏覽器還可以載入 JavaScript 指令碼呢。但是目前為止還沒有跟 Lynx 拉開差距,因為在啟用瀏覽器的時候只能通過命令列傳入 url。那麼,必然需要通過某種方式把需要載入的 url 傳入瀏覽器。沒錯,就是位址列!
新增位址列
其實位址列的實現非常簡單,我們只需要在視窗頂端加一個輸入框就夠了。使用者在文字框中輸入 url 之後,瀏覽器就會載入這個地址。下面,我們將用到 QLineEdit 控制元件來實現輸入框。鑑於我們的瀏覽器現在有位址列和瀏覽器顯示框兩部分,因此還要給我們的應用增加一個網格佈局。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
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_()) |
到這裡,我們已經有一個瀏覽器的雛形啦!看上去和當年的 Google Chrome 還有幾分相像呢,畢竟兩者採用了相同的渲染引擎。現在,你可以在輸入框中輸入 url ,程式便會將地址傳入瀏覽器,接著渲染出所有的 HTML 頁面和 JavaScript 指令碼並展示出來。
新增開發工具
一個瀏覽器最有趣也最重要的部分是什麼?當然是各種各樣的開發工具了!一個沒有開發者控制檯的瀏覽器怎麼能算是瀏覽器呢?所以,我們的 Python 瀏覽器當然也要有一些開發者工具才行。
現在,我們就來新增一些類似於 Chrome 的開發者工具中 “Network” 標籤的功能吧!這個功能就是簡單地追蹤瀏覽器引擎在載入頁面的時候所執行的所有請求。在瀏覽器主頁面的下方,我們將通過一個表來顯示這些請求。簡單起見,我們只會記錄登入的 url、返回的狀態碼和響應的內容型別。
首先我們要通過 QTableWidget 元件建立一個表格,表頭包括需要儲存的欄位名稱,表格可以根據每次新插入的記錄來自動調整大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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)) |
想要追蹤所有請求的話,我們還需要對 PyQt 的內部構件有更深入的瞭解。瞭解到,Qt 提供了一個 NetworkAccessManager類作為 API 介面,通過呼叫它可以監控應用載入頁面時所執行的請求。我們需要自己編寫一個繼承自 NetworkAccessManager 的子類,新增必要的事件監聽器,然後使用我們自己編寫的 manager 來通知 webkit 檢視執行相應的請求。
首先我們需要以 NetworkAccessManager 為基類建立我們自己的網路訪問管理器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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]) |
在這裡需要提醒大家的是, Qt 的某些實現並不像想象中那麼簡單明瞭,比如說從響應中獲取狀態碼就十分繁瑣。首先,你得把請求物件的類屬性作為引數傳入 response 的方法 .attribute()
中,.attribute()
方法的返回值是 QVariant 型別而非 int 型別。接著,需要呼叫內建函式 .toInt()
將其轉換成一個包含兩個元素的元組,最終得到響應的狀態碼。
現在,我們終於有了一個記錄請求的表和一個監控網路的 manager,接下來只要把他們聚攏起來就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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
方法就可以了。
1 2 3 4 5 6 7 8 9 |
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()) |
下面是這個功能的示例。看,我們的開發者工具已經整裝待發了!
1 2 3 4 5 6 7 8 9 10 11 |
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 的網頁物件支援很多不同操作,我們可以把它們全都新增到瀏覽器中。
現在我們可以先嚐試著新增“後退”、“前進”和“重新整理”這幾個操作。你可以在介面上新增這些操作按鈕,簡單起見,這裡只加一個文字框來執行這些動作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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) |
和之前一樣,我們要建立一個 ActionInputBox 的例項,把引數傳入頁面物件並把輸入框物件新增到頁面中。
For reference here’s code for final result 示例程式碼看這裡
[1]: Graphical User Interface,圖形使用者介面,又稱圖形使用者介面,是指採用圖形方式顯示的計算機操作使用者介面。
[2]: WebKit是一個開源的瀏覽器引擎,與之相對應的引擎有 Gecko(Mozilla Firefox 等使用)和 Trident(也稱 MSHTML ,IE 使用)。